using JM1Vision; using MainShell.Common; using MainShell.Log; using MainShell.Models; using MaxwellFramework.Core.Attributes; using SemiconductorVisionAlgorithm.SemiParams; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MainShell.Vision { /// /// Die 定位服务。 /// public class FindDieService : IFindDieService { private readonly IImageCaptureService _imageCaptureService; public FindDieService(IImageCaptureService imageCaptureService) { _imageCaptureService = imageCaptureService ?? throw new ArgumentNullException(nameof(imageCaptureService)); } public async Task> ProcessAsync( FindDieRequest request, CancellationToken cancellationToken = default(CancellationToken)) { if (request == null) { throw new ArgumentNullException(nameof(request)); } FindDieImageRequest imageRequest = CreateImageRequest(request); return await ExecuteWithCaptureAsync( request.CameraSource, request.CaptureOptions, imageRequest, ProcessImageAsync, cancellationToken).ConfigureAwait(false); } public Task> ProcessImageAsync( MxImage image, FindDieImageRequest request, CancellationToken cancellationToken = default(CancellationToken)) { if (image == null) { throw new ArgumentNullException(nameof(image)); } if (request == null) { throw new ArgumentNullException(nameof(request)); } FindDieImageRequest normalizedRequest = NormalizeRequest(request); VisionProcessResult validationResult = ValidateRequest(normalizedRequest); if (validationResult != null) { return Task.FromResult(CreateFailureResult(validationResult)); } try { cancellationToken.ThrowIfCancellationRequested(); VisionProcessResult result = ExecuteSingleMatch(image, normalizedRequest); if (!result.Succeeded) { result.Message.LogSysError(); } return Task.FromResult(result); } catch (OperationCanceledException ex) { string message = $"FindDieService: process image was cancelled. Timeout={normalizedRequest.TimeoutMilliseconds} ms."; message.LogInfo(); return Task.FromResult(VisionProcessResult.Failure( VisionFailureCategory.Cancelled, VisionErrorCode.OperationCancelled, null, MessageKey.VisionOperationCancelled, message, ex)); } catch (Exception ex) { string message = $"FindDieService: process image failed. {ex.Message}"; message.LogSysError(); return Task.FromResult(VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateMatchFailed, VisionAlarmIds.TemplateMatchFailed, MessageKey.VisionTemplateMatchFailed, message, ex)); } } public async Task> ProcessMultipleAsync( FindDieRequest request, CancellationToken cancellationToken = default(CancellationToken)) { if (request == null) { throw new ArgumentNullException(nameof(request)); } FindDieImageRequest imageRequest = CreateImageRequest(request); return await ExecuteWithCaptureAsync( request.CameraSource, request.CaptureOptions, imageRequest, ProcessMultipleImageAsync, cancellationToken).ConfigureAwait(false); } public Task> ProcessMultipleImageAsync( MxImage image, FindDieImageRequest request, CancellationToken cancellationToken = default(CancellationToken)) { if (image == null) { throw new ArgumentNullException(nameof(image)); } if (request == null) { throw new ArgumentNullException(nameof(request)); } FindDieImageRequest normalizedRequest = NormalizeRequest(request); VisionProcessResult validationResult = ValidateRequest(normalizedRequest); if (validationResult != null) { return Task.FromResult(CreateFailureResult(validationResult)); } try { cancellationToken.ThrowIfCancellationRequested(); VisionProcessResult result = ExecuteMultipleMatch(image, normalizedRequest); if (!result.Succeeded) { result.Message.LogSysError(); } return Task.FromResult(result); } catch (OperationCanceledException ex) { string message = $"FindDieService: multiple die process image was cancelled. Timeout={normalizedRequest.TimeoutMilliseconds} ms."; message.LogInfo(); return Task.FromResult(VisionProcessResult.Failure( VisionFailureCategory.Cancelled, VisionErrorCode.OperationCancelled, null, MessageKey.VisionOperationCancelled, message, ex)); } catch (Exception ex) { string message = $"FindDieService: multiple die process image failed. {ex.Message}"; message.LogSysError(); return Task.FromResult(VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateMatchFailed, VisionAlarmIds.TemplateMatchFailed, MessageKey.VisionTemplateMatchFailed, message, ex)); } } private static FindDieImageRequest NormalizeRequest(FindDieImageRequest request) { FindDieImageRequest normalizedRequest = new FindDieImageRequest(); normalizedRequest.TimeoutMilliseconds = request.TimeoutMilliseconds > 0 ? request.TimeoutMilliseconds : 3000; normalizedRequest.Parameters = CloneParameters(request.Parameters); return normalizedRequest; } private static VisionProcessResult ValidateRequest(FindDieImageRequest request) { if (request.Parameters == null) { return VisionProcessResult.Failure( VisionFailureCategory.Validation, VisionErrorCode.RequestInvalid, VisionAlarmIds.RequestInvalid, MessageKey.VisionRequestInvalid, "FindDieService: parameters cannot be null."); } if (request.TimeoutMilliseconds <= 0 || request.TimeoutMilliseconds > 60000) { return VisionProcessResult.Failure( VisionFailureCategory.Validation, VisionErrorCode.TimeoutInvalid, VisionAlarmIds.RequestInvalid, MessageKey.VisionTimeoutInvalid, $"FindDieService: TimeoutMilliseconds {request.TimeoutMilliseconds} is invalid. Expected range is [1, 60000]."); } if (string.IsNullOrWhiteSpace(request.Parameters.TemplatePath)) { return VisionProcessResult.Failure( VisionFailureCategory.Validation, VisionErrorCode.TemplatePathEmpty, VisionAlarmIds.TemplatePathEmpty, MessageKey.VisionTemplatePathEmpty, "FindDieService: TemplatePath cannot be empty."); } if (string.IsNullOrWhiteSpace(request.Parameters.SerialNumber)) { return VisionProcessResult.Failure( VisionFailureCategory.Validation, VisionErrorCode.RequestInvalid, VisionAlarmIds.RequestInvalid, MessageKey.VisionRequestInvalid, "FindDieService: SerialNumber cannot be empty."); } if (request.Parameters.CurrentRuler == null) { return VisionProcessResult.Failure( VisionFailureCategory.Validation, VisionErrorCode.RequestInvalid, VisionAlarmIds.RequestInvalid, MessageKey.VisionRequestInvalid, "FindDieService: CurrentRuler cannot be null."); } if (request.Parameters.DieWidth <= 0d || request.Parameters.DieHeight <= 0d) { return VisionProcessResult.Failure( VisionFailureCategory.Validation, VisionErrorCode.RequestInvalid, VisionAlarmIds.RequestInvalid, MessageKey.VisionRequestInvalid, $"FindDieService: DieWidth {request.Parameters.DieWidth} and DieHeight {request.Parameters.DieHeight} must be greater than 0."); } if (request.Parameters.ScoreThreshold <= 0d || request.Parameters.ScoreThreshold > 1d) { return VisionProcessResult.Failure( VisionFailureCategory.Validation, VisionErrorCode.MinScoreInvalid, VisionAlarmIds.TemplateMinScoreInvalid, MessageKey.VisionTemplateMinScoreInvalid, $"FindDieService: ScoreThreshold {request.Parameters.ScoreThreshold} is invalid. Expected range is (0, 1]."); } return null; } private static FindDieParameters CloneParameters(FindDieParameters parameters) { if (parameters == null) { return new FindDieParameters(); } FindDieParameters clone = new FindDieParameters(); clone.MinScore = parameters.MinScore; clone.TemplatePath = parameters.TemplatePath; clone.SerialNumber = parameters.SerialNumber; clone.CurrentRuler = parameters.CurrentRuler == null ? null : new Point(parameters.CurrentRuler.X, parameters.CurrentRuler.Y); clone.OverlapX = parameters.OverlapX; clone.OverlapY = parameters.OverlapY; clone.DieWidth = parameters.DieWidth; clone.DieHeight = parameters.DieHeight; clone.ScoreThreshold = parameters.ScoreThreshold > 0d ? parameters.ScoreThreshold : parameters.MinScore; clone.AngleTolerance = parameters.AngleTolerance; return clone; } private static FindDieImageRequest CreateImageRequest(FindDieRequest request) { FindDieImageRequest imageRequest = new FindDieImageRequest(); imageRequest.TimeoutMilliseconds = request.TimeoutMilliseconds; imageRequest.Parameters = CloneParameters(request.Parameters); return imageRequest; } private static VisionProcessResult ExecuteSingleMatch(MxImage image, FindDieImageRequest request) { VisionProcessResult multipleResult = ExecuteMultipleMatch(image, request); if (!multipleResult.Succeeded) { return CreateFailureResult(multipleResult); } FindDieResult result = SelectPrimaryResult(multipleResult.Data); if (result == null) { return VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateResultInvalid, VisionAlarmIds.TemplateResultInvalid, MessageKey.VisionTemplateMatchFailed, $"FindDieService: no valid die result was found. TemplatePath={request.Parameters.TemplatePath}."); } return VisionProcessResult.Success(result); } private static VisionProcessResult ExecuteMultipleMatch(MxImage image, FindDieImageRequest request) { Camera camera = image.Image; if (camera == null) { return VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.ImageNull, VisionAlarmIds.ImageNull, MessageKey.VisionImageIsNull, "FindDieService: input image does not contain a valid camera object."); } double[] uniqueRows = null; double[] uniqueCols = null; double[] uniqueAngles = null; double[] overlapRows = null; double[] overlapCols = null; double[] overlapAngles = null; double[] pixelRows = null; double[] pixelCols = null; double[] pixelAngles = null; bool succeeded = JM1Manager.Instance.get_target_pos_nodisp( camera, request.Parameters.TemplatePath, request.Parameters.SerialNumber, request.Parameters.CurrentRuler, request.Parameters.OverlapX, request.Parameters.OverlapY, request.Parameters.DieWidth, request.Parameters.DieHeight, request.Parameters.ScoreThreshold, out uniqueRows, out uniqueCols, out uniqueAngles, out overlapRows, out overlapCols, out overlapAngles, out pixelRows, out pixelCols, out pixelAngles); if (!succeeded) { return VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateMatchFailed, VisionAlarmIds.TemplateMatchFailed, MessageKey.VisionTemplateMatchFailed, $"FindDieService: die match failed. TemplatePath={request.Parameters.TemplatePath}."); } if (!HasConsistentResult(uniqueRows, uniqueCols, uniqueAngles) || !HasConsistentResult(overlapRows, overlapCols, overlapAngles) || !HasConsistentResult(pixelRows, pixelCols, pixelAngles)) { return VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateResultInvalid, VisionAlarmIds.TemplateResultInvalid, MessageKey.VisionTemplateMatchFailed, $"FindDieService: die match returned inconsistent result data. TemplatePath={request.Parameters.TemplatePath}."); } if (uniqueRows.Length + overlapRows.Length == 0) { return VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateMatchFailed, VisionAlarmIds.TemplateMatchFailed, MessageKey.VisionTemplateMatchFailed, $"FindDieService: die match returned no results. TemplatePath={request.Parameters.TemplatePath}."); } FindDiesResult result = CreateMultipleResult(uniqueRows, uniqueCols, uniqueAngles, overlapRows, overlapCols, overlapAngles, pixelRows, pixelCols, pixelAngles); ApplyAngleTolerance(result, request.Parameters.AngleTolerance); if (result.Items.Count == 0) { return VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateResultInvalid, VisionAlarmIds.TemplateResultInvalid, MessageKey.VisionTemplateMatchFailed, $"FindDieService: die results were empty after angle filtering. TemplatePath={request.Parameters.TemplatePath}."); } return VisionProcessResult.Success(result); } private static FindDiesResult CreateMultipleResult( double[] uniqueRows, double[] uniqueCols, double[] uniqueAngles, double[] overlapRows, double[] overlapCols, double[] overlapAngles, double[] pixelRows, double[] pixelCols, double[] pixelAngles) { FindDiesResult result = new FindDiesResult(); AppendDieResults(result.Items, uniqueRows, uniqueCols, uniqueAngles, false); AppendDieResults(result.Items, overlapRows, overlapCols, overlapAngles, true); AppendPixelResults(result.PixelItems, pixelRows, pixelCols, pixelAngles); return result; } private static void AppendDieResults( IList destination, double[] rows, double[] cols, double[] angles, bool isOverlap) { if (destination == null || rows == null || cols == null || angles == null) { return; } for (int i = 0; i < rows.Length; i++) { FindDieResult item = new FindDieResult(); item.CenterX = cols[i]; item.CenterY = rows[i]; item.Angle = angles[i]; item.PixelX = double.NaN; item.PixelY = double.NaN; item.IsOverlap = isOverlap; item.Match = CreateMatchResult(angles[i], isOverlap); destination.Add(item); } } private static void AppendPixelResults( IList destination, double[] rows, double[] cols, double[] angles) { if (destination == null || rows == null || cols == null || angles == null) { return; } for (int i = 0; i < rows.Length; i++) { FindDiePixelResult item = new FindDiePixelResult(); item.PixelX = cols[i]; item.PixelY = rows[i]; item.Angle = angles[i]; destination.Add(item); } } private static VisionMatchResult CreateMatchResult(double angle, bool isOverlap) { VisionMatchResult match = new VisionMatchResult(); match.OffsetX = 0d; match.OffsetY = 0d; match.Angle = angle; match.Score = 0d; match.Tag = isOverlap ? "Overlap" : "Unique"; return match; } private static void ApplyAngleTolerance(FindDiesResult result, double? angleTolerance) { if (result == null || !angleTolerance.HasValue || angleTolerance.Value <= 0d) { return; } List itemAngles = new List(); foreach (FindDieResult item in result.Items) { itemAngles.Add(item.Angle * 180.0d / Math.PI); } if (itemAngles.Count == 0) { return; } double averageAngle = itemAngles.Average(); List filteredItems = new List(); foreach (FindDieResult item in result.Items) { double angleDegree = item.Angle * 180.0d / Math.PI; if (Math.Abs(angleDegree - averageAngle) <= angleTolerance.Value) { filteredItems.Add(item); } } List filteredPixelItems = new List(); foreach (FindDiePixelResult item in result.PixelItems) { double angleDegree = item.Angle * 180.0d / Math.PI; if (Math.Abs(angleDegree - averageAngle) <= angleTolerance.Value) { filteredPixelItems.Add(item); } } result.Items.Clear(); foreach (FindDieResult item in filteredItems) { result.Items.Add(item); } result.PixelItems.Clear(); foreach (FindDiePixelResult item in filteredPixelItems) { result.PixelItems.Add(item); } } private static FindDieResult SelectPrimaryResult(FindDiesResult result) { if (result == null || result.Items == null || result.Items.Count == 0) { return null; } foreach (FindDieResult item in result.Items) { if (!item.IsOverlap) { return item; } } return result.Items[0]; } private static bool HasConsistentResult(double[] rows, double[] cols, double[] angles) { if (rows == null || cols == null || angles == null) { return false; } if (rows.Length != cols.Length || rows.Length != angles.Length) { return false; } for (int i = 0; i < rows.Length; i++) { if (double.IsNaN(rows[i]) || double.IsInfinity(rows[i]) || double.IsNaN(cols[i]) || double.IsInfinity(cols[i]) || double.IsNaN(angles[i]) || double.IsInfinity(angles[i])) { return false; } } return true; } private async Task> ExecuteWithCaptureAsync( CameraType cameraSource, CameraCaptureOptions captureOptions, FindDieImageRequest imageRequest, Func>> imageProcessor, CancellationToken cancellationToken) { ImageCaptureResult captureResult = null; MxImage image = null; try { captureResult = await _imageCaptureService.CaptureAsync( cameraSource, captureOptions, cancellationToken).ConfigureAwait(false); if (captureResult == null) { return VisionProcessResult.Failure( VisionFailureCategory.Capture, VisionErrorCode.NoFrame, VisionAlarmIds.NoFrame, MessageKey.VisionNoFrameReturned, $"FindDieService: image capture returned null result for camera '{cameraSource}'."); } if (!captureResult.Succeeded) { return CreateFailureResult(VisionResultMapper.CreateCaptureFailure(captureResult)); } image = captureResult.Image; if (image == null) { string message = $"FindDieService: image capture succeeded but returned null image for camera '{cameraSource}'."; message.LogSysError(); return VisionProcessResult.Failure( VisionFailureCategory.Capture, VisionErrorCode.ImageNull, VisionAlarmIds.ImageNull, MessageKey.VisionImageIsNull, message); } return await imageProcessor(image, imageRequest, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException ex) { string message = $"FindDieService: capture orchestration was cancelled for camera '{cameraSource}'."; message.LogInfo(); return VisionProcessResult.Failure( VisionFailureCategory.Cancelled, VisionErrorCode.OperationCancelled, null, MessageKey.VisionOperationCancelled, message, ex); } catch (Exception ex) { string message = $"FindDieService: capture orchestration failed for camera '{cameraSource}'. {ex.Message}"; message.LogSysError(); return VisionProcessResult.Failure( VisionFailureCategory.Algorithm, VisionErrorCode.TemplateMatchFailed, VisionAlarmIds.TemplateMatchFailed, MessageKey.VisionTemplateMatchFailed, message, ex); } finally { if (image != null) { image.Dispose(); } } } private static VisionProcessResult CreateFailureResult(VisionProcessResult result) { return VisionProcessResult.Failure( result.FailureCategory, result.ErrorCode, result.AlarmId, result.UserMessageKey, result.Message, result.Exception); } } }