气泡测量第三步:魔棒flood fill,灰度阈值连通区域标记,空隙率实时计算

This commit is contained in:
李伟
2026-04-27 13:11:00 +08:00
parent 0c8d8afc51
commit 179a6a6755
3 changed files with 139 additions and 3 deletions
@@ -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");
}
/// <summary>魔棒:在点击位置做 flood fill</summary>
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();
}
/// <summary>从 ImageSource 提取灰度像素数组</summary>
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;
}
/// <summary>清除气泡测量所有状态</summary>
@@ -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);
}
@@ -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}%";
}
};
}
}
@@ -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} 条测量");