using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; using OpenCvSharp; using Point = OpenCvSharp.Point; namespace Hawkeye.VisionBuilder.Workflow.Operations; [Category("Simple/Blobs")] public class FindManyBlobsExtOperation : BaseOperation { private readonly WorkflowList _workflowList; public int MinArea { get; set; } = 100; public int MaxArea { get; set; } = 99999; public int MinRadius { get; set; } = 0; public int MaxRadius { get; set; } = 99999; public bool CheckInternalRadius { get; set; } = false; public int MinInternalRadius { get; set; } = 0; public int MaxInternalRadius { get; set; } = 99999; public bool CheckWidth { get; set; } = false; public int MinWidth { get; set; } = 0; public int MaxWidth { get; set; } = 99999; public bool CheckHeight { get; set; } = false; public int MinHeight { get; set; } = 0; public int MaxHeight { get; set; } = 99999; public Guid ReferenceId { get; set; } = new Guid(); public RectangleElement SearchArea { get; set; } public FindManyBlobsExtOperation(WorkflowList workflowList) { _workflowList = workflowList; SearchArea = new RectangleElement() { Editable = true, Location = Vector2.One * 100, Size = Vector2.One * 100, IsGood = true }; } public record ContourMatch(Point[] c, int area, int radius, int internalRadius, int width, int height) { public override string ToString() { return $"{{area={area}, radius={radius}, internalRadius={internalRadius}, width={width}, height={height}}}"; } } protected override void InterpretInternal(Context context) { Result = true; if (!CheckImageExists(context)) return; if (!CheckGrayscale(context)) return; if (SearchArea == null) { SearchArea = new RectangleElement() { Editable = true, Location = Vector2.Zero, Size = new Vector2(context.ActiveImage.ImageData.Width, context.ActiveImage.ImageData.Height), IsGood = true }; } SearchArea.MovePivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); var img = context.ActiveImage.ImageData; var mat = new Mat(img.Size(), img.Type(), Scalar.Black); SearchArea.FillMat(mat); var contoursMat = img.BitwiseAnd(mat).ToMat(); var contours = contoursMat.Threshold(128, 255, ThresholdTypes.Binary) .FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); context.GraphicsElements.Add(SearchArea); // Case: no blobs found if (contours.Length == 0) { Status = "No blobs found"; SearchArea.IsGood = true; return; } int GetContourRadius(Point[] contour) { Cv2.MinEnclosingCircle(contour, out _, out var radius); return (int)radius; } int GetContourInternalRadius(Point[] contour) { // Create a mask for the contour with the size of the bounding rectangle. Mat mask = new Mat(contoursMat.Height, contoursMat.Width, MatType.CV_8UC1, Scalar.All(0)); // Fill the mask with the polygon defined by the contour. Cv2.FillPoly(mask, new Point[][] { contour }, Scalar.White); // Compute the distance transform. using Mat dist = new Mat(); Cv2.DistanceTransform(mask, dist, DistanceTypes.C, DistanceTransformMasks.Mask3); Cv2.MinMaxLoc(dist, out double minVal, out var maxVal); return (int)maxVal; } (int width, int height) GetContourDimensions(Point[] contour) { var rect = Cv2.BoundingRect(contour); return (rect.Width, rect.Height); } var contourMatches = contours.Select(c => { int area = (int)Cv2.ContourArea(c); int radius =-1; radius = GetContourRadius(c); int innerCircle=-1; if (CheckInternalRadius) innerCircle = GetContourInternalRadius(c); var (width, height) = GetContourDimensions(c); return new ContourMatch(c, area, radius, innerCircle, width, height); }) .OrderBy(cm => Math.Min(Math.Abs(cm.area - MinArea), Math.Abs(cm.area - MaxArea))) .ToList(); var matches = contourMatches.Where(cm => cm.c.Length > 1) .Where(cm => cm.area >= MinArea && cm.area <= MaxArea) .Where(cm => cm.radius >= MinRadius && cm.radius <= MaxRadius); if (CheckInternalRadius) { matches = matches.Where(cm => cm.internalRadius >= MinInternalRadius && cm.internalRadius <= MaxInternalRadius); } if (CheckWidth) { matches = matches.Where(cm => cm.width >= MinWidth && cm.width <= MaxWidth); } if (CheckHeight) { matches = matches.Where(cm => cm.height >= MinHeight && cm.height <= MaxHeight); } var finalMatches = matches.ToList(); int badCount = 0; foreach (var contourMatch in finalMatches) { PolyElement poly = new PolyElement() { IsGood = false }; poly.Points = contourMatch.c.Select(p => new Vector2(p.X, p.Y)).ToArray(); context.GraphicsElements.Add(poly); badCount++; } if (badCount > 0) { Status = $"Found: {badCount} First: {finalMatches.First()}"; Result = false; SearchArea.IsGood = false; return; } SearchArea.IsGood = true; Status = $"Best: {contourMatches.First()}"; Result = true; } public override void SetParameters(Dictionary parameters) { base.SetParameters(parameters); SearchArea.SetPivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); } public override void Save(BinaryWriter bw) { bw.Write(Id.ToString()); bw.Write(Label); bw.Write(MinArea); bw.Write(MaxArea); bw.Write(MinRadius); bw.Write(MaxRadius); bw.Write(CheckInternalRadius); bw.Write(MinInternalRadius); bw.Write(MaxInternalRadius); bw.Write(CheckWidth); bw.Write(MinWidth); bw.Write(MaxWidth); bw.Write(CheckHeight); bw.Write(MinHeight); bw.Write(MaxHeight); bw.Write(ReferenceId.ToString()); SearchArea.Save(bw); } public override void Load(BinaryReader br) { Id = new Guid(br.ReadString()); Label = br.ReadString(); MinArea = br.ReadInt32(); MaxArea = br.ReadInt32(); MinRadius = br.ReadInt32(); MaxRadius = br.ReadInt32(); CheckInternalRadius = br.ReadBoolean(); MinInternalRadius = br.ReadInt32(); MaxInternalRadius = br.ReadInt32(); CheckWidth = br.ReadBoolean(); MinWidth = br.ReadInt32(); MaxWidth = br.ReadInt32(); CheckHeight = br.ReadBoolean(); MinHeight = br.ReadInt32(); MaxHeight = br.ReadInt32(); ReferenceId = new Guid(br.ReadString()); SearchArea.Load(br); } public override void Save(Dictionary dict) { base.Save(dict); dict[nameof(MinArea)] = MinArea; dict[nameof(MaxArea)] = MaxArea; dict[nameof(MinRadius)] = MinRadius; dict[nameof(MaxRadius)] = MaxRadius; dict[nameof(CheckInternalRadius)] = CheckInternalRadius; dict[nameof(MinInternalRadius)] = MinInternalRadius; dict[nameof(MaxInternalRadius)] = MaxInternalRadius; dict[nameof(CheckWidth)] = CheckWidth; dict[nameof(MinWidth)] = MinWidth; dict[nameof(MaxWidth)] = MaxWidth; dict[nameof(CheckHeight)] = CheckHeight; dict[nameof(MinHeight)] = MinHeight; dict[nameof(MaxHeight)] = MaxHeight; dict[nameof(ReferenceId)] = ReferenceId.ToString(); // Save SearchArea properties manually dict["SearchArea_Editable"] = SearchArea.Editable; dict["SearchArea_IsGood"] = SearchArea.IsGood; dict["SearchArea_LocationX"] = SearchArea.Location.X; dict["SearchArea_LocationY"] = SearchArea.Location.Y; dict["SearchArea_OffsetX"] = SearchArea.Offset.X; dict["SearchArea_OffsetY"] = SearchArea.Offset.Y; dict["SearchArea_SizeX"] = SearchArea.Size.X; dict["SearchArea_SizeY"] = SearchArea.Size.Y; } public override void Load(Dictionary dict) { base.Load(dict); if (dict.ContainsKey(nameof(MinArea))) MinArea = Convert.ToInt32(dict[nameof(MinArea)]); if (dict.ContainsKey(nameof(MaxArea))) MaxArea = Convert.ToInt32(dict[nameof(MaxArea)]); if (dict.ContainsKey(nameof(MinRadius))) MinRadius = Convert.ToInt32(dict[nameof(MinRadius)]); if (dict.ContainsKey(nameof(MaxRadius))) MaxRadius = Convert.ToInt32(dict[nameof(MaxRadius)]); if (dict.ContainsKey(nameof(CheckInternalRadius))) CheckInternalRadius = Convert.ToBoolean(dict[nameof(CheckInternalRadius)]); if (dict.ContainsKey(nameof(MinInternalRadius))) MinInternalRadius = Convert.ToInt32(dict[nameof(MinInternalRadius)]); if (dict.ContainsKey(nameof(MaxInternalRadius))) MaxInternalRadius = Convert.ToInt32(dict[nameof(MaxInternalRadius)]); if (dict.ContainsKey(nameof(CheckWidth))) CheckWidth = Convert.ToBoolean(dict[nameof(CheckWidth)]); if (dict.ContainsKey(nameof(MinWidth))) MinWidth = Convert.ToInt32(dict[nameof(MinWidth)]); if (dict.ContainsKey(nameof(MaxWidth))) MaxWidth = Convert.ToInt32(dict[nameof(MaxWidth)]); if (dict.ContainsKey(nameof(CheckHeight))) CheckHeight = Convert.ToBoolean(dict[nameof(CheckHeight)]); if (dict.ContainsKey(nameof(MinHeight))) MinHeight = Convert.ToInt32(dict[nameof(MinHeight)]); if (dict.ContainsKey(nameof(MaxHeight))) MaxHeight = Convert.ToInt32(dict[nameof(MaxHeight)]); if (dict.ContainsKey(nameof(ReferenceId))) ReferenceId = new Guid(dict[nameof(ReferenceId)].ToString()); // Load SearchArea properties manually if (dict.ContainsKey("SearchArea_Editable")) SearchArea.Editable = Convert.ToBoolean(dict["SearchArea_Editable"]); if (dict.ContainsKey("SearchArea_IsGood")) SearchArea.IsGood = Convert.ToBoolean(dict["SearchArea_IsGood"]); if (dict.ContainsKey("SearchArea_LocationX") && dict.ContainsKey("SearchArea_LocationY")) SearchArea.Location = new Vector2(Convert.ToSingle(dict["SearchArea_LocationX"]), Convert.ToSingle(dict["SearchArea_LocationY"])); if (dict.ContainsKey("SearchArea_OffsetX") && dict.ContainsKey("SearchArea_OffsetY")) SearchArea.Offset = new Vector2(Convert.ToSingle(dict["SearchArea_OffsetX"]), Convert.ToSingle(dict["SearchArea_OffsetY"])); if (dict.ContainsKey("SearchArea_SizeX") && dict.ContainsKey("SearchArea_SizeY")) SearchArea.Size = new Vector2(Convert.ToSingle(dict["SearchArea_SizeX"]), Convert.ToSingle(dict["SearchArea_SizeY"])); } }