// ============================================================================ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件? ContourProcessor.cs // 描述: 轮廓查找算子,用于检测和分析图像中的轮廓 // 功能: // - 检测图像中的外部轮? // - 根据面积范围过滤轮廓 // - 计算轮廓的几何特征(面积、周长、中心、外接矩形等? // - 输出轮廓信息供后续处理使? // 算法: 基于OpenCV的轮廓检测算? // 作? 李伟 wei.lw.li@hexagon.com // ============================================================================ using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; using Serilog; using System.Drawing; using XP.ImageProcessing.Core; namespace XP.ImageProcessing.Processors; /// /// 轮廓查找算子 /// public class ContourProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); public ContourProcessor() { Name = LocalizationHelper.GetString("ContourProcessor_Name"); Description = LocalizationHelper.GetString("ContourProcessor_Description"); } protected override void InitializeParameters() { Parameters.Add("TargetColor", new ProcessorParameter( "TargetColor", LocalizationHelper.GetString("ContourProcessor_TargetColor"), typeof(string), "White", null, null, LocalizationHelper.GetString("ContourProcessor_TargetColor_Desc"), new string[] { "White", "Black" })); Parameters.Add("UseThreshold", new ProcessorParameter( "UseThreshold", LocalizationHelper.GetString("ContourProcessor_UseThreshold"), typeof(bool), false, null, null, LocalizationHelper.GetString("ContourProcessor_UseThreshold_Desc"))); Parameters.Add("ThresholdValue", new ProcessorParameter( "ThresholdValue", LocalizationHelper.GetString("ContourProcessor_ThresholdValue"), typeof(int), 120, 0, 255, LocalizationHelper.GetString("ContourProcessor_ThresholdValue_Desc"))); Parameters.Add("UseOtsu", new ProcessorParameter( "UseOtsu", LocalizationHelper.GetString("ContourProcessor_UseOtsu"), typeof(bool), false, null, null, LocalizationHelper.GetString("ContourProcessor_UseOtsu_Desc"))); Parameters.Add("MinArea", new ProcessorParameter( "MinArea", LocalizationHelper.GetString("ContourProcessor_MinArea"), typeof(double), 10.0, 0.0, 10000.0, LocalizationHelper.GetString("ContourProcessor_MinArea_Desc"))); Parameters.Add("MaxArea", new ProcessorParameter( "MaxArea", LocalizationHelper.GetString("ContourProcessor_MaxArea"), typeof(double), 100000.0, 0.0, 1000000.0, LocalizationHelper.GetString("ContourProcessor_MaxArea_Desc"))); Parameters.Add("Thickness", new ProcessorParameter( "Thickness", LocalizationHelper.GetString("ContourProcessor_Thickness"), typeof(int), 2, 1, 10, LocalizationHelper.GetString("ContourProcessor_Thickness_Desc"))); _logger.Debug("InitializeParameters"); } public override Image Process(Image inputImage) { string targetColor = GetParameter("TargetColor"); bool useThreshold = GetParameter("UseThreshold"); int thresholdValue = GetParameter("ThresholdValue"); bool useOtsu = GetParameter("UseOtsu"); double minArea = GetParameter("MinArea"); double maxArea = GetParameter("MaxArea"); int thickness = GetParameter("Thickness"); _logger.Debug("Process started: TargetColor = '{TargetColor}', UseThreshold = {UseThreshold}, ThresholdValue = {ThresholdValue}, UseOtsu = {UseOtsu}", targetColor, useThreshold, thresholdValue, useOtsu); OutputData.Clear(); // 创建输入图像的副本用于处? Image processImage = inputImage.Clone(); // 步骤1:如果启用阈值分割,先进行二值化 if (useThreshold) { _logger.Debug("Applying threshold processing"); Image thresholdImage = new Image(processImage.Size); if (useOtsu) { // 使用Otsu自动阈? CvInvoke.Threshold(processImage, thresholdImage, 0, 255, ThresholdType.Otsu); _logger.Debug("Applied Otsu threshold"); } else { // 使用固定阈? CvInvoke.Threshold(processImage, thresholdImage, thresholdValue, 255, ThresholdType.Binary); _logger.Debug("Applied binary threshold with value {ThresholdValue}", thresholdValue); } // 保存阈值处理后的图像用于调? try { string debugPath = Path.Combine("logs", $"contour_threshold_{DateTime.Now:yyyyMMdd_HHmmss}.png"); Directory.CreateDirectory("logs"); thresholdImage.Save(debugPath); _logger.Information("Saved threshold image to: {DebugPath}", debugPath); } catch (Exception ex) { _logger.Warning(ex, "Failed to save threshold image for debugging"); } processImage.Dispose(); processImage = thresholdImage; } // 步骤2:如果目标是黑色区域,需要反转图? bool isBlackTarget = targetColor != null && (targetColor.Equals("Black", StringComparison.OrdinalIgnoreCase) || targetColor.Equals("黑色", StringComparison.OrdinalIgnoreCase)); if (isBlackTarget) { _logger.Debug("Inverting image for black region detection"); CvInvoke.BitwiseNot(processImage, processImage); // 保存翻转后的图像用于调试 try { string debugPath = Path.Combine("logs", $"contour_inverted_{DateTime.Now:yyyyMMdd_HHmmss}.png"); Directory.CreateDirectory("logs"); processImage.Save(debugPath); _logger.Information("Saved inverted image to: {DebugPath}", debugPath); } catch (Exception ex) { _logger.Warning(ex, "Failed to save inverted image for debugging"); } } // 步骤3:查找轮? using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint()) { Mat hierarchy = new Mat(); CvInvoke.FindContours(processImage, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple); _logger.Debug("Found {TotalContours} total contours before filtering", contours.Size); List contourInfos = new(); for (int i = 0; i < contours.Size; i++) { double area = CvInvoke.ContourArea(contours[i]); if (area >= minArea && area <= maxArea) { var moments = CvInvoke.Moments(contours[i]); var boundingRect = CvInvoke.BoundingRectangle(contours[i]); double perimeter = CvInvoke.ArcLength(contours[i], true); var circle = CvInvoke.MinEnclosingCircle(contours[i]); contourInfos.Add(new ContourInfo { Index = i, Area = area, Perimeter = perimeter, CenterX = moments.M10 / moments.M00, CenterY = moments.M01 / moments.M00, BoundingBox = boundingRect, Points = contours[i].ToArray(), CircleCenter = circle.Center, CircleRadius = circle.Radius }); _logger.Debug("Contour {Index}: Area = {Area}, Center = ({CenterX:F2}, {CenterY:F2})", i, area, moments.M10 / moments.M00, moments.M01 / moments.M00); } else { _logger.Debug("Contour {Index} filtered out: Area = {Area} (not in range {MinArea} - {MaxArea})", i, area, minArea, maxArea); } } OutputData["ContourCount"] = contourInfos.Count; OutputData["Contours"] = contourInfos; OutputData["Thickness"] = thickness; hierarchy.Dispose(); processImage.Dispose(); _logger.Information("Process completed: TargetColor = '{TargetColor}', Found {ContourCount} contours (filtered from {TotalContours})", targetColor, contourInfos.Count, contours.Size); return inputImage.Clone(); } } } /// /// 轮廓信息 /// public class ContourInfo { public int Index { get; set; } public double Area { get; set; } public double Perimeter { get; set; } public double CenterX { get; set; } public double CenterY { get; set; } public Rectangle BoundingBox { get; set; } public Point[] Points { get; set; } = Array.Empty(); public PointF CircleCenter { get; set; } public float CircleRadius { get; set; } }