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);
}