254 lines
9.6 KiB
C#
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; }
|
|
} |