254 lines
9.6 KiB
C#
254 lines
9.6 KiB
C#
// ============================================================================
|
|
// Copyright 穢 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
|
// ��辣�? ContourProcessor.cs
|
|
// �讛膩: 頧桀��交𪄳蝞堒�嚗𣬚鍂鈭擧�瘚见�����曉�銝剔�頧桀�
|
|
// �蠘�:
|
|
// - 璉�瘚见㦛�譍葉����刻蔭撱?
|
|
// - �寞旿�Y妖��凒餈�誘頧桀�
|
|
// - 霈∠�頧桀����雿閧鸌敺���Y妖��𪂹�踴��葉敹�����亦畆敶Y�嚗?
|
|
// - 颲枏枂頧桀�靽⊥�靘𥕦�蝏剖���蝙�?
|
|
// 蝞埈�: �箔�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;
|
|
|
|
/// <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; }
|
|
} |