From 179a6a6755ca759e819042d79b6a3fb97ed286ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 27 Apr 2026 13:11:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B0=94=E6=B3=A1=E6=B5=8B=E9=87=8F=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E6=AD=A5=EF=BC=9A=E9=AD=94=E6=A3=92flood=20fill?= =?UTF-8?q?=EF=BC=8C=E7=81=B0=E5=BA=A6=E9=98=88=E5=80=BC=E8=BF=9E=E9=80=9A?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E6=A0=87=E8=AE=B0=EF=BC=8C=E7=A9=BA=E9=9A=99?= =?UTF-8?q?=E7=8E=87=E5=AE=9E=E6=97=B6=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml.cs | 130 +++++++++++++++++- .../BubbleMeasurePanel.xaml.cs | 10 +- .../Views/Main/ViewportPanelView.xaml.cs | 2 + 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 7c9581b..816da59 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -1183,12 +1183,131 @@ namespace XP.ImageProcessing.RoiControl.Controls private void ApplyBrushAt(Point pos) { - // 画笔/橡皮:暂时空实现,下一步实现 + // 画笔/橡皮:下一步实现 } private void UpdateBubbleResult() { - // 计算空隙率:暂时空实现,下一步实现 + if (_bubbleMask == null || !_bubbleRoi.HasValue) return; + var roi = _bubbleRoi.Value; + int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; + int stride = w * 4; + var pixels = new byte[stride * h]; + _bubbleMask.CopyPixels(pixels, stride, 0); + + int roiX0 = Math.Max(0, (int)roi.X), roiY0 = Math.Max(0, (int)roi.Y); + int roiX1 = Math.Min(w, (int)roi.Right), roiY1 = Math.Min(h, (int)roi.Bottom); + + int roiArea = 0, voidArea = 0; + for (int y = roiY0; y < roiY1; y++) + for (int x = roiX0; x < roiX1; x++) + { + roiArea++; + int idx = (y * w + x) * 4; + if (pixels[idx + 3] > 0) voidArea++; // alpha > 0 表示已标记 + } + + double voidRate = roiArea > 0 ? voidArea * 100.0 / roiArea : 0; + RaiseMeasureCompleted(roi.TopLeft, roi.BottomRight, voidRate, 1, "BubbleVoid"); + } + + /// 魔棒:在点击位置做 flood fill + public void WandFloodFill(Point pos) + { + if (_bubbleMask == null || !_bubbleRoi.HasValue || ImageSource == null) return; + + var roi = _bubbleRoi.Value; + int px = (int)pos.X, py = (int)pos.Y; + if (!roi.Contains(pos)) return; + + // 获取灰度像素 + var gray = GetGrayscalePixels(); + if (gray == null) return; + + int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; + if (px < 0 || px >= w || py < 0 || py >= h) return; + + int seedVal = gray[py * w + px]; + int lo = _bubbleThreshold, hi = _bubbleThreshold; + + // BFS flood fill + var visited = new bool[w * h]; + var queue = new System.Collections.Generic.Queue<(int x, int y)>(); + queue.Enqueue((px, py)); + visited[py * w + px] = true; + + int roiX0 = Math.Max(0, (int)roi.X), roiY0 = Math.Max(0, (int)roi.Y); + int roiX1 = Math.Min(w, (int)roi.Right), roiY1 = Math.Min(h, (int)roi.Bottom); + + var filled = new System.Collections.Generic.List<(int x, int y)>(); + + while (queue.Count > 0) + { + var (cx, cy) = queue.Dequeue(); + int val = gray[cy * w + cx]; + + // 阈值判断:与种子点灰度差在阈值范围内 + if (Math.Abs(val - seedVal) > lo) continue; + // 必须在 ROI 内 + if (cx < roiX0 || cx >= roiX1 || cy < roiY0 || cy >= roiY1) continue; + + filled.Add((cx, cy)); + + // 四邻域 + int[] dx = { -1, 1, 0, 0 }, dy = { 0, 0, -1, 1 }; + for (int d = 0; d < 4; d++) + { + int nx = cx + dx[d], ny = cy + dy[d]; + if (nx >= roiX0 && nx < roiX1 && ny >= roiY0 && ny < roiY1 && !visited[ny * w + nx]) + { + visited[ny * w + nx] = true; + queue.Enqueue((nx, ny)); + } + } + } + + if (filled.Count == 0) return; + + // 写入 mask(橙色半透明) + int stride = w * 4; + var maskPixels = new byte[stride * h]; + _bubbleMask.CopyPixels(maskPixels, stride, 0); + + foreach (var (fx, fy) in filled) + { + int idx = (fy * w + fx) * 4; + maskPixels[idx + 0] = 0; // B + maskPixels[idx + 1] = 140; // G + maskPixels[idx + 2] = 255; // R (橙色) + maskPixels[idx + 3] = 180; // A + } + + _bubbleMask.WritePixels(new System.Windows.Int32Rect(0, 0, w, h), maskPixels, stride, 0); + UpdateBubbleResult(); + } + + /// 从 ImageSource 提取灰度像素数组 + private byte[] GetGrayscalePixels() + { + if (ImageSource is not BitmapSource bmp) return null; + + int w = bmp.PixelWidth, h = bmp.PixelHeight; + if (w != (int)CanvasWidth || h != (int)CanvasHeight) return null; + + // 转为 Bgra32 + var converted = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0); + int stride = w * 4; + var pixels = new byte[stride * h]; + converted.CopyPixels(pixels, stride, 0); + + // 提取灰度 + var gray = new byte[w * h]; + for (int i = 0; i < w * h; i++) + { + int idx = i * 4; + gray[i] = (byte)(pixels[idx + 2] * 0.299 + pixels[idx + 1] * 0.587 + pixels[idx] * 0.114); + } + return gray; } /// 清除气泡测量所有状态 @@ -1668,7 +1787,14 @@ namespace XP.ImageProcessing.RoiControl.Controls { Point clickPosition = e.GetPosition(mainCanvas); if (IsMeasuring) + { HandleMeasureClick(clickPosition); + // 魔棒点击 + if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure && _bubbleTool == BubbleSubTool.Wand && _bubbleRoi.HasValue) + { + WandFloodFill(clickPosition); + } + } OnCanvasClicked(clickPosition); } diff --git a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs index 369fd41..c7f9323 100644 --- a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs +++ b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs @@ -53,7 +53,15 @@ namespace XplorePlane.Views.ImageProcessing { _canvas.BubbleToolChanged += (s, ev) => { - // 暂不自动切换面板 radio button + // 同步面板 radio button + }; + + _canvas.MeasureCompleted += (s, ev) => + { + if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BubbleVoid") + { + TbResult.Text = $"空隙率: {args.Distance:F1}%"; + } }; } } diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index 9e83eb1..743a755 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -42,6 +42,7 @@ namespace XplorePlane.Views "Angle" => "角度", "FillRate" => "填锡率", "BgaVoid" => "BGA空隙", + "BubbleVoid" => "气泡空隙", _ => "点点距" }; string valueText = args.MeasureType switch @@ -49,6 +50,7 @@ namespace XplorePlane.Views "Angle" => $"{args.Distance:F2}°", "FillRate" => $"{args.Distance:F1}%", "BgaVoid" => $"{args.Distance:F1}%", + "BubbleVoid" => $"{args.Distance:F1}%", _ => $"{args.Distance:F2} px" }; SetStatus($"{typeLabel}: {valueText} | 共 {args.TotalCount} 条测量");