tbf/TBF/Rig/Network/Camera/CJMS11/GrabImagesOp.cs

1123 lines
23 KiB
C#

///
/// Copyright (c) 2017 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Common;
using Config.Entities;
using log4net;
using TBF.Rig.Network.Camera.CJMS11.POJO;
using TBF.Rig.Network.Telnet;
namespace TBF.Rig.Network.Camera.CJMS11
{
public class GrabImagesOp : IOperation
{
private static readonly ILog log = LogManager.GetLogger(typeof(GrabImagesOp));
private enum GrabOperationMode
{
None,
SingleImage,
SendImages
}
private readonly Camera camera;
private readonly string[] imgFileNames;
private readonly bool blackAndWhite;
private readonly bool lowResolution;
private readonly ImageRotation imageRotation;
private readonly object imageSync = new object();
private bool grabImageCommandSent;
private bool transferringGrabbedImage;
private GrabOperationMode operationMode;
private Image grabedImage;
private readonly Dictionary<int, Image> grabedImages = new Dictionary<int, Image>();
private Task<Image> grabImageTask;
private Task<IReadOnlyList<TimedCameraImage>> sendImagesTask;
private Task saveImageTask;
private IReadOnlyList<TimedCameraImage> timedCameraImages;
private readonly List<string> savedFileNames = new List<string>();
public override string ToString() { return "GrabImageOp()"; }
public GrabImagesOp(
Camera camera,
string[] imgFileNames,
bool blackAndWhite,
bool lowResolution,
ImageRotation imageRotation)
{
if (camera == null)
{
throw new ArgumentNullException(nameof(camera));
}
this.camera = camera;
this.imgFileNames = imgFileNames;
this.blackAndWhite = blackAndWhite;
this.lowResolution = lowResolution;
this.imageRotation = imageRotation;
SubscribeEvents();
}
private void SubscribeEvents()
{
Camera.ImageCameraHandler -= ImageReceived;
Camera.ImageCameraHandler += ImageReceived;
Camera.ImagesCameraHandler -= ImagesReceived;
Camera.ImagesCameraHandler += ImagesReceived;
}
private void ImageReceived(
object sender,
PromptReceivedImageEventArgs args)
{
if (args == null || args.idxCamera != camera.CameraIdx || args.image == null)
{
return;
}
/*
* Tento obrázok používame iba pre kompatibilitu
* s existujúcim eventovým mechanizmom.
*
* Hlavným zdrojom výsledku je Task<Image>.
*/
lock (imageSync)
{
grabedImage = args.image;
}
}
private void ImagesReceived( object sender, PromptReceivedImagesEventArgs args)
{
if (args == null || args.idxCamera != camera.CameraIdx || args.image == null)
{
return;
}
/*
* Náhľadové obrázky z eventu nebudeme ukladať
* na disk. Originálne obrázky dostaneme vo
* výsledku sendImagesTask.
*/
lock (imageSync)
{
grabedImage = args.image;
grabedImages[args.idxImage] = args.image;
}
}
public void Start()
{
SubscribeEvents();
grabImageCommandSent = false;
transferringGrabbedImage = false;
operationMode = GrabOperationMode.None;
grabImageTask = null;
sendImagesTask = null;
saveImageTask = null;
timedCameraImages = null;
lock (imageSync)
{
grabedImage = null;
grabedImages.Clear();
}
savedFileNames.Clear();
DeletePreviousOutputFiles();
}
public Event Run()
{
if (!HasValidOutputFileName())
{
log.Warn(
$"No output image file configured: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabPassed;
}
/*
* 1. Spustenie príslušnej operácie.
*/
if (!grabImageCommandSent)
{
return StartGrabOperation();
}
/*
* 2. Spracovanie podľa režimu.
*/
switch (operationMode)
{
case GrabOperationMode.SingleImage:
return RunSingleImageMode();
case GrabOperationMode.SendImages:
return RunSendImagesMode();
default:
log.Error(
$"Grab operation mode was not selected: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabFailed;
}
}
private Event StartGrabOperation()
{
grabImageCommandSent = true;
try
{
int requestedImageCount =
GetRequestedImageCount();
if (requestedImageCount <= 1)
{
operationMode =
GrabOperationMode.SingleImage;
log.Debug(
$"Starting single asynchronous grab: " +
$"cameraIdx={camera.CameraIdx}");
grabImageTask =
camera.StartGrabImageListenerAsync(
retryUntilValidImage: true,
maxAttempts: 5,
retryDelayMs: 200);
}
else
{
operationMode =
GrabOperationMode.SendImages;
int delayMs =
GetRequestedDelayMs();
string taskId =
CreateSendTaskId();
log.Debug(
$"Starting send images operation: " +
$"cameraIdx={camera.CameraIdx}, " +
$"count={requestedImageCount}, " +
$"delayMs={delayMs}, " +
$"taskId={taskId}");
sendImagesTask =
camera.StartSendImagesPreviewAsync(
count: requestedImageCount,
delayMs: delayMs,
taskId: taskId);
}
return Event.CameraBusy;
}
catch (Exception exc)
{
log.Error(
$"Cannot start image operation: " +
$"cameraIdx={camera.CameraIdx}, " +
$"mode={operationMode}, " +
$"error={exc.Message}",
exc);
FinishOperation();
return Event.GrabFailed;
}
}
private Event RunSingleImageMode()
{
if (grabImageTask == null)
{
log.Error(
$"Single grab task was not created: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabFailed;
}
if (!grabImageTask.IsCompleted)
{
return Event.CameraBusy;
}
if (grabImageTask.IsCanceled)
{
log.Warn(
$"Single grab task was cancelled: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabFailed;
}
if (grabImageTask.IsFaulted)
{
log.Error(
$"Single grab task failed: " +
$"cameraIdx={camera.CameraIdx}",
grabImageTask.Exception?.Flatten());
FinishOperation();
return Event.GrabFailed;
}
if (!transferringGrabbedImage)
{
Image imageToSave;
try
{
imageToSave =
grabImageTask
.GetAwaiter()
.GetResult();
}
catch (Exception exc)
{
log.Error(
$"Cannot obtain grabbed image: " +
$"cameraIdx={camera.CameraIdx}, " +
$"error={exc.Message}",
exc);
FinishOperation();
return Event.GrabFailed;
}
if (imageToSave == null)
{
log.Error(
$"Single grab completed without image: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabFailed;
}
lock (imageSync)
{
grabedImage = imageToSave;
}
string outputFileName =
imgFileNames[0];
try
{
saveImageTask =
camera.StartSaveImageToFileAsync(
imageToSave,
outputFileName);
savedFileNames.Clear();
savedFileNames.Add(
outputFileName);
transferringGrabbedImage = true;
}
catch (Exception exc)
{
log.Error(
$"Cannot start single image save: " +
$"cameraIdx={camera.CameraIdx}, " +
$"file={outputFileName}, " +
$"error={exc.Message}",
exc);
FinishOperation();
return Event.GrabFailed;
}
return Event.CameraBusy;
}
return CheckSaveTaskAndFinish();
}
private Event RunSendImagesMode()
{
if (sendImagesTask == null)
{
log.Error(
$"Send images task was not created: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabFailed;
}
if (!sendImagesTask.IsCompleted)
{
return Event.CameraBusy;
}
if (sendImagesTask.IsCanceled)
{
log.Warn(
$"Send images task was cancelled: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabFailed;
}
if (sendImagesTask.IsFaulted)
{
log.Error(
$"Send images task failed: " +
$"cameraIdx={camera.CameraIdx}",
sendImagesTask.Exception?.Flatten());
FinishOperation();
return Event.GrabFailed;
}
if (!transferringGrabbedImage)
{
try
{
timedCameraImages =
sendImagesTask
.GetAwaiter()
.GetResult();
}
catch (Exception exc)
{
log.Error(
$"Cannot obtain send image results: " +
$"cameraIdx={camera.CameraIdx}, " +
$"error={exc.Message}",
exc);
FinishOperation();
return Event.GrabFailed;
}
if (timedCameraImages == null ||
timedCameraImages.Count == 0)
{
log.Error(
$"Send operation returned no images: " +
$"cameraIdx={camera.CameraIdx}");
FinishOperation();
return Event.GrabFailed;
}
int requestedCount =
GetRequestedImageCount();
if (timedCameraImages.Count !=
requestedCount)
{
log.Error(
$"Send operation returned unexpected image count: " +
$"cameraIdx={camera.CameraIdx}, " +
$"requested={requestedCount}, " +
$"received={timedCameraImages.Count}");
DisposeTimedImages();
FinishOperation();
return Event.GrabFailed;
}
IReadOnlyList<TimedCameraImage>
orderedImages =
timedCameraImages
.OrderBy(image => image.Index)
.ToList();
try
{
/*
* Camera.StartSaveImageToFileAsync používa
* jeden interný save task, preto viac
* obrázkov uložíme sekvenčne v jednom
* samostatnom tasku.
*/
saveImageTask =
Task.Run(
() => SaveTimedImages(
orderedImages));
transferringGrabbedImage = true;
}
catch (Exception exc)
{
log.Error(
$"Cannot start send images save task: " +
$"cameraIdx={camera.CameraIdx}, " +
$"error={exc.Message}",
exc);
DisposeTimedImages();
FinishOperation();
return Event.GrabFailed;
}
return Event.CameraBusy;
}
return CheckSaveTaskAndFinish();
}
private void SaveTimedImages( IReadOnlyList<TimedCameraImage> images)
{
if (images == null)
{
throw new ArgumentNullException(nameof(images));
}
if (images.Count == 0)
{
throw new ArgumentException( "No timed camera images were provided.", nameof(images));
}
if (!HasValidOutputFileName())
{
throw new InvalidOperationException( "No base output image file name is configured.");
}
savedFileNames.Clear();
IReadOnlyList<TimedCameraImage> orderedImages =
images
.Where(image => image != null)
.OrderBy(image => image.Index)
.ToList();
if (orderedImages.Count != images.Count)
{
throw new InvalidOperationException( $"One or more timed camera images are null. cameraIdx={camera.CameraIdx}, " +
$"expected={images.Count}, valid={orderedImages.Count}");
}
log.Debug( $"Starting timed images save: cameraIdx={camera.CameraIdx}, imagesCount={orderedImages.Count}, " +
$"baseFile='{imgFileNames[0]}'");
foreach (TimedCameraImage timedImage in orderedImages)
{
if (timedImage.Image == null)
{
throw new InvalidOperationException( $"Timed camera image contains no image data: " +
$"cameraIdx={camera.CameraIdx}, imageIndex={timedImage.Index}, frameTime={timedImage.FrameTime:O}");
}
/*
* Každý obrázok uložíme pod unikátnym názvom
* obsahujúcim index obrázka a časovú značku.
*/
string timedFileName = CreateTimedImageFileName( timedImage, orderedImages.Count);
log.Debug( $"Saving timed camera image: cameraIdx={camera.CameraIdx}, imageIndex={timedImage.Index}, " +
$"frameTime={timedImage.FrameTime:O}, file='{timedFileName}'");
camera.SaveImageToFile(
timedImage.Image,
timedFileName);
savedFileNames.Add(
timedFileName);
log.Debug( $"Timed camera image saved successfully: cameraIdx={camera.CameraIdx}, " +
$"imageIndex={timedImage.Index}, file='{timedFileName}'");
/*
* Prvý obrázok uložíme ešte raz pod pôvodným názvom.
*
* Tento súbor používa existujúci dialóg cez
* startImages[] alebo endImages[].
*
* Napríklad:
* TestAdvance-1-start.bmp
*/
if (timedImage.Index == 1)
{
string compatibilityFileName =
imgFileNames[0];
log.Debug($"Saving first timed image under compatibility name: cameraIdx={camera.CameraIdx}, " +
$"imageIndex={timedImage.Index}, frameTime={timedImage.FrameTime:O}, file='{compatibilityFileName}'");
camera.SaveImageToFile(timedImage.Image, compatibilityFileName);
savedFileNames.Add(compatibilityFileName);
log.Debug($"First timed image saved under compatibility name: cameraIdx={camera.CameraIdx}, " +
$"imageIndex={timedImage.Index}, file='{compatibilityFileName}'");
}
}
log.Debug(
$"Timed images save completed: cameraIdx={camera.CameraIdx}, receivedImages={orderedImages.Count}, " +
$"savedFiles={savedFileNames.Count}");
}
private Event CheckSaveTaskAndFinish()
{
if (saveImageTask == null)
{
log.Error( $"Save image task was not created: cameraIdx={camera.CameraIdx}, mode={operationMode}");
FinishOperation();
return Event.GrabFailed;
}
if (!saveImageTask.IsCompleted)
{
return Event.CameraBusy;
}
if (saveImageTask.IsCanceled)
{
log.Warn( $"Save image task was cancelled: cameraIdx={camera.CameraIdx}, mode={operationMode}");
DisposeOperationImages();
FinishOperation();
return Event.GrabFailed;
}
if (saveImageTask.IsFaulted)
{
log.Error( $"Save image task failed: cameraIdx={camera.CameraIdx}, mode={operationMode}",
saveImageTask.Exception?.Flatten());
DisposeOperationImages();
FinishOperation();
return Event.GrabFailed;
}
bool allFilesSaved = VerifySavedFiles();
if (allFilesSaved)
{
log.Debug( $"Image operation completed successfully: cameraIdx={camera.CameraIdx}, " +
$"mode={operationMode}, savedFiles={savedFileNames.Count}");
}
else
{
log.Error( $"One or more image files were not saved: cameraIdx={camera.CameraIdx}, mode={operationMode}");
}
DisposeOperationImages();
FinishOperation();
return allFilesSaved
? Event.GrabPassed
: Event.GrabFailed;
}
private bool VerifySavedFiles()
{
if (savedFileNames.Count == 0)
{
return false;
}
foreach (string fileName
in savedFileNames)
{
try
{
if (string.IsNullOrWhiteSpace(fileName) ||
!File.Exists(fileName))
{
return false;
}
var fileInfo =
new FileInfo(fileName);
if (fileInfo.Length <= 0)
{
return false;
}
}
catch (Exception exc)
{
log.Error(
$"Cannot verify saved image: " +
$"cameraIdx={camera.CameraIdx}, " +
$"file={fileName}, " +
$"error={exc.Message}",
exc);
return false;
}
}
return true;
}
private string CreateTimedImageFileName(
TimedCameraImage timedImage,
int totalImageCount)
{
if (timedImage == null)
{
throw new ArgumentNullException(nameof(timedImage));
}
if (!HasValidOutputFileName())
{
throw new InvalidOperationException(
"No base output image file name is configured.");
}
int imagePosition = timedImage.Index;
if (imagePosition <= 0)
{
throw new InvalidOperationException(
$"Invalid image position: {imagePosition}.");
}
// Prvý obrázok musí zostať pod pôvodným názvom,
// pretože tento názov používa startImages/endImages.
if (imagePosition == 1)
{
return imgFileNames[0];
}
string configuredFileName = imgFileNames[0];
string directory =
Path.GetDirectoryName(configuredFileName);
string baseName =
Path.GetFileNameWithoutExtension(configuredFileName);
string extension =
Path.GetExtension(configuredFileName);
if (string.IsNullOrWhiteSpace(extension))
{
extension = ".png";
}
DateTime timestampUtc =
timedImage.FrameTime.HasValue
? timedImage.FrameTime.Value.UtcDateTime
: DateTime.UtcNow;
string timestampText =
timestampUtc.ToString(
"yyyyMMdd_HHmmss_fff",
CultureInfo.InvariantCulture);
int digits = Math.Max(
2,
totalImageCount.ToString(
CultureInfo.InvariantCulture).Length);
string indexText =
imagePosition.ToString(
"D" + digits,
CultureInfo.InvariantCulture);
string newFileName =
$"{baseName}_img{indexText}_{timestampText}{extension}";
return string.IsNullOrWhiteSpace(directory)
? newFileName
: Path.Combine(directory, newFileName);
}
private string GetConfiguredFileName(
int zeroBasedIndex)
{
if (imgFileNames != null &&
zeroBasedIndex >= 0 &&
zeroBasedIndex < imgFileNames.Length &&
!string.IsNullOrWhiteSpace(
imgFileNames[zeroBasedIndex]))
{
return imgFileNames[zeroBasedIndex];
}
/*
* Ak nie je pripravený názov pre každý obrázok,
* použijeme prvý názov ako základ.
*/
return imgFileNames[0];
}
private int GetRequestedImageCount()
{
var roi = camera.CamRoi;
if (roi == null)
{
return 1;
}
return Math.Max(1, roi.RoiPicCount);
}
private int GetRequestedDelayMs()
{
const int MinDelayMs = 500;
var roi = camera.CamRoi;
if (roi == null)
{
return MinDelayMs;
}
int configuredDelay = roi.RoiDeltaTimeMs;
if (configuredDelay < MinDelayMs)
{
log.Warn( $"Configured ROI delay {configuredDelay} ms is below minimum. Using {MinDelayMs} ms instead.");
}
return Math.Max(MinDelayMs, configuredDelay);
}
private string CreateSendTaskId()
{
/*
* Timestamp + kamera + krátka časť GUID.
* ID je čitateľné a zároveň odolné voči
* kolíziám pri rýchlom opakovaní.
*/
string timestamp = DateTime.UtcNow.ToString( "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture);
string uniquePart = Guid.NewGuid() .ToString("N") .Substring(0, 8);
return $"{timestamp}_cam{camera.CameraIdx}_" + uniquePart;
}
private bool HasValidOutputFileName()
{
return
imgFileNames != null &&
imgFileNames.Length > 0 &&
!string.IsNullOrWhiteSpace(
imgFileNames[0]);
}
public void Stop()
{
try
{
switch (operationMode)
{
case GrabOperationMode.SingleImage:
camera.CancelGrabImage();
break;
case GrabOperationMode.SendImages:
camera.CancelSendImagesPreview();
break;
}
}
catch (Exception exc)
{
log.Warn( $"Cancel image operation failed: cameraIdx={camera.CameraIdx}, mode={operationMode}, " +
$"error={exc.Message}");
}
finally
{
FinishOperation();
}
}
private void FinishOperation()
{
UnsubscribeEvents();
}
private void UnsubscribeEvents()
{
Camera.ImageCameraHandler -= ImageReceived;
Camera.ImagesCameraHandler -= ImagesReceived;
}
private static string AppendTimestampToFileName(
string originalFileName,
DateTime timestamp)
{
if (string.IsNullOrWhiteSpace(originalFileName))
{
throw new ArgumentException(
"File name is required.",
nameof(originalFileName));
}
string directory =
Path.GetDirectoryName(originalFileName);
string fileNameWithoutExtension =
Path.GetFileNameWithoutExtension(originalFileName);
string extension =
Path.GetExtension(originalFileName);
string timestampText =
timestamp
.ToUniversalTime()
.ToString(
"yyyyMMdd_HHmmss_fff",
System.Globalization.CultureInfo.InvariantCulture);
string newFileName =
$"{fileNameWithoutExtension}_{timestampText}{extension}";
return string.IsNullOrWhiteSpace(directory)
? newFileName
: Path.Combine(directory, newFileName);
}
private void DisposeOperationImages()
{
if (operationMode ==
GrabOperationMode.SendImages)
{
DisposeTimedImages();
return;
}
Image imageToDispose = null;
lock (imageSync)
{
imageToDispose = grabedImage;
grabedImage = null;
}
try
{
imageToDispose?.Dispose();
}
catch
{
}
}
private void DisposeTimedImages()
{
if (timedCameraImages == null)
{
return;
}
foreach (TimedCameraImage timedImage
in timedCameraImages)
{
try
{
timedImage?.Image?.Dispose();
}
catch
{
}
}
timedCameraImages = null;
}
private void DeletePreviousOutputFiles()
{
if (!HasValidOutputFileName())
{
return;
}
var filesToDelete = new HashSet<string>(
StringComparer.OrdinalIgnoreCase);
foreach (string configuredFileName in imgFileNames)
{
if (string.IsNullOrWhiteSpace(configuredFileName))
{
continue;
}
string fullConfiguredFileName;
try
{
fullConfiguredFileName =
Path.GetFullPath(configuredFileName);
}
catch (Exception exc)
{
log.Warn(
$"Invalid configured image file name: " +
$"cameraIdx={camera.CameraIdx}, " +
$"file='{configuredFileName}', " +
$"error={exc.Message}");
continue;
}
string directory =
Path.GetDirectoryName(fullConfiguredFileName);
string baseName =
Path.GetFileNameWithoutExtension(
fullConfiguredFileName);
string extension =
Path.GetExtension(
fullConfiguredFileName);
if (string.IsNullOrWhiteSpace(directory) ||
string.IsNullOrWhiteSpace(baseName))
{
continue;
}
if (!Directory.Exists(directory))
{
log.Debug(
$"Image output directory does not exist; " +
$"nothing to delete: " +
$"cameraIdx={camera.CameraIdx}, " +
$"directory='{directory}'");
continue;
}
/*
* Pôvodný kompatibilný súbor:
*
* TestAdvance-1-start.bmp
*/
filesToDelete.Add(
fullConfiguredFileName);
/*
* Časované súbory:
*
* TestAdvance-1-start_img01_20260715_120000_100.bmp
* TestAdvance-1-start_img02_20260715_120000_600.bmp
*
* Časovú značku neanalyzujeme.
* Hľadáme podľa stabilného prefixu názvu.
*/
string searchPattern =
$"{baseName}_img*{extension}";
try
{
foreach (string foundFile in
Directory.GetFiles(
directory,
searchPattern,
SearchOption.TopDirectoryOnly))
{
filesToDelete.Add(
Path.GetFullPath(foundFile));
}
}
catch (Exception exc)
{
log.Warn(
$"Cannot search previous timed images: " +
$"cameraIdx={camera.CameraIdx}, " +
$"directory='{directory}', " +
$"pattern='{searchPattern}', " +
$"error={exc.Message}");
}
}
log.Debug(
$"Previous image files found for deletion: " +
$"cameraIdx={camera.CameraIdx}, " +
$"count={filesToDelete.Count}");
foreach (string fileName in filesToDelete)
{
try
{
if (!File.Exists(fileName))
{
continue;
}
File.Delete(fileName);
log.Debug(
$"Previous image file deleted: " +
$"cameraIdx={camera.CameraIdx}, " +
$"file='{fileName}'");
}
catch (Exception exc)
{
log.Warn(
$"Cannot delete previous image file: " +
$"cameraIdx={camera.CameraIdx}, " +
$"file='{fileName}', " +
$"error={exc.Message}");
}
}
}
}
}