From eb6ee48a5eb4754117f1490c6e4ed7a04c5657fd Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Tue, 19 May 2026 13:11:47 +0800 Subject: [PATCH] =?UTF-8?q?CNC=E9=AB=98=E7=BA=A7=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E7=9A=84=E8=BF=90=E8=A1=8C=E5=90=8E=E7=9A=84=E5=8F=AF=E8=A7=86?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../检测分析/BgaVoidRateProcessor.cs | 64 +++++++++++++++++++ .../检测分析/VoidMeasurementProcessor.cs | 53 +++++++++++++++ .../ImageProcessing/ImageConverter.cs | 12 ++++ .../ImageProcessing/ImageProcessingService.cs | 19 +++++- 4 files changed, 147 insertions(+), 1 deletion(-) diff --git a/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs b/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs index 450d0f1..cb1fede 100644 --- a/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs +++ b/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs @@ -216,6 +216,9 @@ public class BgaVoidRateProcessor : ImageProcessorBase OutputData["Thickness"] = thickness; OutputData["ResultText"] = $"Void: {overallVoidRate:F1}% | {classification} | BGA×{bgaResults.Count}"; + // 渲染带标注的彩色结果图像 + OutputData["RenderedResultImage"] = RenderAnnotatedResult(inputImage, bgaResults, voidLimit, thickness); + roiMask?.Dispose(); return inputImage.Clone(); } @@ -372,6 +375,67 @@ public class BgaVoidRateProcessor : ImageProcessorBase mask.Dispose(); voidImg.Dispose(); } + + /// + /// 渲染带标注的彩色结果图像(轮廓、编号、气泡填充、总览信息) + /// + private Image RenderAnnotatedResult(Image grayImage, List bgaResults, double voidLimit, int thickness) + { + var colorImage = new Image(grayImage.Width, grayImage.Height); + CvInvoke.CvtColor(grayImage, colorImage, ColorConversion.Gray2Bgr); + + if (bgaResults.Count == 0) + return colorImage; + + // 半透明气泡填充 + var overlay = colorImage.Clone(); + foreach (var bga in bgaResults) + { + var fillColor = new MCvScalar(0, 200, 255); + foreach (var v in bga.Voids) + { + if (v.ContourPoints.Length > 0) + { + using var vop = new VectorOfPoint(v.ContourPoints); + using var vvop = new VectorOfVectorOfPoint(vop); + CvInvoke.DrawContours(overlay, vvop, 0, fillColor, -1); + } + } + } + CvInvoke.AddWeighted(overlay, 0.4, colorImage, 0.6, 0, colorImage); + overlay.Dispose(); + + // 绘制焊球轮廓 + 编号 + int ngCount = 0; + foreach (var bga in bgaResults) + { + var bgaColor = bga.Classification == "PASS" + ? new MCvScalar(0, 255, 0) : new MCvScalar(0, 0, 255); + if (bga.Classification != "PASS") ngCount++; + + if (bga.ContourPoints.Length > 0) + { + using var vop = new VectorOfPoint(bga.ContourPoints); + using var vvop = new VectorOfVectorOfPoint(vop); + CvInvoke.DrawContours(colorImage, vvop, 0, bgaColor, thickness); + } + + var bbox = CvInvoke.BoundingRectangle(new VectorOfPoint(bga.ContourPoints)); + CvInvoke.PutText(colorImage, $"#{bga.Index}", + new Point(bbox.X + bbox.Width / 2 - 10, bbox.Bottom + 16), + FontFace.HersheySimplex, 0.45, new MCvScalar(255, 100, 0), 2); + } + + // 左上角总览 + int okCount = bgaResults.Count - ngCount; + var overallColor = ngCount > 0 ? new MCvScalar(0, 0, 255) : new MCvScalar(0, 255, 0); + CvInvoke.PutText(colorImage, + $"Total: {bgaResults.Count} | OK: {okCount} | NG: {ngCount}", + new Point(10, 25), + FontFace.HersheySimplex, 0.55, overallColor, 2); + + return colorImage; + } } /// diff --git a/XP.ImageProcessing.Processors/检测分析/VoidMeasurementProcessor.cs b/XP.ImageProcessing.Processors/检测分析/VoidMeasurementProcessor.cs index b63d591..19355a1 100644 --- a/XP.ImageProcessing.Processors/检测分析/VoidMeasurementProcessor.cs +++ b/XP.ImageProcessing.Processors/检测分析/VoidMeasurementProcessor.cs @@ -207,12 +207,65 @@ public class VoidMeasurementProcessor : ImageProcessorBase OutputData["Voids"] = voids; OutputData["ResultText"] = $"Void: {voidRate:F1}% | {classification} | {voids.Count} voids | ROI: {roiArea}px"; + // 渲染带标注的彩色结果图像 + OutputData["RenderedResultImage"] = RenderAnnotatedResult(inputImage, voids, voidRate, voidLimit, classification); + blurred.Dispose(); voidImg.Dispose(); roiMask.Dispose(); return inputImage.Clone(); } + + /// + /// 渲染带标注的彩色结果图像(轮廓、编号、半透明填充、总览信息) + /// + private Image RenderAnnotatedResult(Image grayImage, List voids, double voidRate, double voidLimit, string classification) + { + var colorImage = new Image(grayImage.Width, grayImage.Height); + CvInvoke.CvtColor(grayImage, colorImage, ColorConversion.Gray2Bgr); + + if (voids.Count == 0) + return colorImage; + + // 半透明气泡填充 + var overlay = colorImage.Clone(); + foreach (var v in voids) + { + if (v.ContourPoints.Length > 0) + { + using var vop = new VectorOfPoint(v.ContourPoints); + using var vvop = new VectorOfVectorOfPoint(vop); + CvInvoke.DrawContours(overlay, vvop, 0, new MCvScalar(0, 200, 255), -1); + } + } + CvInvoke.AddWeighted(overlay, 0.4, colorImage, 0.6, 0, colorImage); + overlay.Dispose(); + + // 绘制轮廓 + 编号 + foreach (var v in voids) + { + if (v.ContourPoints.Length > 0) + { + using var vop = new VectorOfPoint(v.ContourPoints); + using var vvop = new VectorOfVectorOfPoint(vop); + CvInvoke.DrawContours(colorImage, vvop, 0, new MCvScalar(0, 255, 255), 1); + } + CvInvoke.PutText(colorImage, $"#{v.Index}", + new Point((int)v.CenterX - 8, (int)v.CenterY + 5), + FontFace.HersheySimplex, 0.35, new MCvScalar(255, 100, 0), 1); + } + + // 左上角总览 + var overallColor = classification == "PASS" + ? new MCvScalar(0, 255, 0) : new MCvScalar(0, 0, 255); + CvInvoke.PutText(colorImage, + $"Void: {voidRate:F1}% | Limit: {voidLimit:F0}% | {voids.Count} voids | {classification}", + new Point(10, 25), + FontFace.HersheySimplex, 0.5, overallColor, 2); + + return colorImage; + } } /// diff --git a/XplorePlane/Services/ImageProcessing/ImageConverter.cs b/XplorePlane/Services/ImageProcessing/ImageConverter.cs index ce28cfd..dbc11a0 100644 --- a/XplorePlane/Services/ImageProcessing/ImageConverter.cs +++ b/XplorePlane/Services/ImageProcessing/ImageConverter.cs @@ -67,5 +67,17 @@ namespace XplorePlane.Services return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride); } + + public static BitmapSource ToBitmapSourceFromBgr(Image emguImage) + { + if (emguImage == null) throw new ArgumentNullException(nameof(emguImage)); + + int width = emguImage.Width; + int height = emguImage.Height; + byte[] pixels = emguImage.Bytes; + int stride = pixels.Length / height; + + return BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr24, null, pixels, stride); + } } } diff --git a/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs b/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs index 177e30f..a87f67c 100644 --- a/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs +++ b/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs @@ -258,13 +258,30 @@ namespace XplorePlane.Services var processedEmgu = processor.Process(emguImage); progress?.Report(0.9); - var result = ImageConverter.ToBitmapSource(processedEmgu); + BitmapSource result; + + // 如果处理器输出了渲染后的彩色结果图像,优先使用它 + if (processor.OutputData.TryGetValue("RenderedResultImage", out var renderedObj) + && renderedObj is Emgu.CV.Image renderedImage) + { + result = ImageConverter.ToBitmapSourceFromBgr(renderedImage); + renderedImage.Dispose(); + } + else + { + result = ImageConverter.ToBitmapSource(processedEmgu); + } + result.Freeze(); progress?.Report(1.0); var snapshot = new Dictionary(processor.OutputData.Count); foreach (var kv in processor.OutputData) + { + // 不将大型图像对象序列化到快照中 + if (kv.Key == "RenderedResultImage") continue; snapshot[kv.Key] = kv.Value; + } return (result, snapshot); }