Files
XplorePlane/XP.ImageProcessing.Processors/检测分析/ContourDetectionProcessor.cs
T
2026-04-14 17:12:31 +08:00

254 lines
9.6 KiB
C#

// ============================================================================
// 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 XP.ImageProcessing.Core;
using Serilog;
using System.Drawing;
namespace XP.ImageProcessing.Processors;
/// <summary>
/// 轮廓查找算子
/// </summary>
public class ContourProcessor : ImageProcessorBase
{
private static readonly ILogger _logger = Log.ForContext<ContourProcessor>();
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<Gray, byte> Process(Image<Gray, byte> inputImage)
{
string targetColor = GetParameter<string>("TargetColor");
bool useThreshold = GetParameter<bool>("UseThreshold");
int thresholdValue = GetParameter<int>("ThresholdValue");
bool useOtsu = GetParameter<bool>("UseOtsu");
double minArea = GetParameter<double>("MinArea");
double maxArea = GetParameter<double>("MaxArea");
int thickness = GetParameter<int>("Thickness");
_logger.Debug("Process started: TargetColor = '{TargetColor}', UseThreshold = {UseThreshold}, ThresholdValue = {ThresholdValue}, UseOtsu = {UseOtsu}",
targetColor, useThreshold, thresholdValue, useOtsu);
OutputData.Clear();
// 创建输入图像的副本用于处理
Image<Gray, byte> processImage = inputImage.Clone();
// 步骤1:如果启用阈值分割,先进行二值化
if (useThreshold)
{
_logger.Debug("Applying threshold processing");
Image<Gray, byte> thresholdImage = new Image<Gray, byte>(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<ContourInfo> 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();
}
}
}
/// <summary>
/// 轮廓信息
/// </summary>
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<Point>();
public PointF CircleCenter { get; set; }
public float CircleRadius { get; set; }
}