fix: remove legacy wizard ball detection and add overlay context
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace XplorePlane.Models.Inspection.Tasks
|
||||
@@ -5,6 +6,7 @@ namespace XplorePlane.Models.Inspection.Tasks
|
||||
/// <summary>
|
||||
/// BGA 球识别引擎参数。默认取值与 BgaVoidRateProcessor 保持行为一致。
|
||||
/// </summary>
|
||||
[Obsolete("旧格式兼容字段。Ball 识别参数必须配置在 PipelineSnapshot 的 BgaVoidRate 算子中。")]
|
||||
public record BgaDetectionOptions
|
||||
{
|
||||
/// <summary>高斯模糊核大小,奇数;0 表示不模糊。</summary>
|
||||
|
||||
@@ -50,7 +50,11 @@ namespace XplorePlane.Services.Inspection
|
||||
context.Acquisition.Image,
|
||||
definition.Analysis.PipelineSnapshot,
|
||||
cancellationToken);
|
||||
_ballOverlay?.Publish(analysis.Balls);
|
||||
_ballOverlay?.Publish(analysis.Balls, new BallOverlayContext(
|
||||
BallCoordinateSystem.StitchedImage,
|
||||
context.Acquisition.Image.PixelWidth,
|
||||
context.Acquisition.Image.PixelHeight,
|
||||
SourceId: definition.Id.ToString()));
|
||||
return BuildPipelineOutcome(context, grid, bounds, analysis.Balls, cancellationToken);
|
||||
}
|
||||
var excluded = new HashSet<string>(grid.ExcludedTargetIds ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
/// </summary>
|
||||
public interface IBallOverlayService
|
||||
{
|
||||
void Publish(IReadOnlyList<BGABall> balls);
|
||||
void Publish(IReadOnlyList<BGABall> balls, BallOverlayContext context);
|
||||
void Clear();
|
||||
}
|
||||
|
||||
@@ -24,17 +24,18 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
_overlay = overlay ?? throw new ArgumentNullException(nameof(overlay));
|
||||
}
|
||||
|
||||
public void Publish(IReadOnlyList<BGABall> balls)
|
||||
public void Publish(IReadOnlyList<BGABall> balls, BallOverlayContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(balls);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
_overlay.PublishBgaBalls(balls.Select(ball => new BallOverlayItem
|
||||
{
|
||||
Id = ball.Id,
|
||||
CenterX = ball.X,
|
||||
CenterY = ball.Y,
|
||||
Diameter = ball.Diameter,
|
||||
CenterX = (ball.X - context.OriginX) * context.ScaleX,
|
||||
CenterY = (ball.Y - context.OriginY) * context.ScaleY,
|
||||
Diameter = ball.Diameter * ((context.ScaleX + context.ScaleY) / 2),
|
||||
IsFailed = ball.Judgement is BallJudgement.NG or BallJudgement.Missing
|
||||
}).ToArray());
|
||||
}).ToArray(), context);
|
||||
}
|
||||
|
||||
public void Clear() => _overlay.ClearOverlay();
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
|
||||
/// <summary>
|
||||
/// 检测池/CNC 共用的 Ball 分析入口。
|
||||
/// PipelineSnapshot 存在时,流水线是唯一参数来源;没有流水线输出 Ball 时才回退到独立引擎。
|
||||
/// PipelineSnapshot 是唯一参数来源;没有流水线输出 BgaBalls 时直接失败,禁止切换到另一套识别算法。
|
||||
/// </summary>
|
||||
public interface IPoolBallAnalysisService
|
||||
{
|
||||
|
||||
@@ -154,7 +154,11 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
acquired.Image, task.PipelineSnapshot, _internalCts.Token);
|
||||
var detectedBalls = PoolBallMatcher.MatchBlock(
|
||||
block, analysis.Balls, acquired.Image.PixelWidth, acquired.Image.PixelHeight);
|
||||
_ballOverlay.Publish(detectedBalls);
|
||||
_ballOverlay.Publish(detectedBalls, new BallOverlayContext(
|
||||
BallCoordinateSystem.ReferenceImage,
|
||||
pool.ReferenceImageWidth,
|
||||
pool.ReferenceImageHeight,
|
||||
SourceId: task.Id.ToString()));
|
||||
_logger.LogInformation("Block {Id}: Ball engine={Engine}, detected={Count}",
|
||||
block.Id, analysis.Engine, analysis.Balls.Count);
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
/// <summary>球列表数据(BgaBalls 类型时有效)。</summary>
|
||||
public IReadOnlyList<BallOverlayItem>? Balls { get; init; }
|
||||
|
||||
/// <summary>Ball 坐标系及其来源图像上下文。</summary>
|
||||
public BallOverlayContext? BallContext { get; init; }
|
||||
|
||||
/// <summary>Block 网格数据(BlockGrid 类型时有效)。</summary>
|
||||
public IReadOnlyList<BlockOverlayItem>? Blocks { get; init; }
|
||||
|
||||
@@ -64,6 +67,25 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
public bool IsFailed { get; init; }
|
||||
}
|
||||
|
||||
public enum BallCoordinateSystem
|
||||
{
|
||||
ReferenceImage,
|
||||
BlockImage,
|
||||
StitchedImage,
|
||||
ViewportImage
|
||||
}
|
||||
|
||||
/// <summary>描述 Ball 坐标来自哪张图,以及显示前需要执行的线性变换。</summary>
|
||||
public sealed record BallOverlayContext(
|
||||
BallCoordinateSystem CoordinateSystem,
|
||||
int ImageWidth,
|
||||
int ImageHeight,
|
||||
double OriginX = 0,
|
||||
double OriginY = 0,
|
||||
double ScaleX = 1,
|
||||
double ScaleY = 1,
|
||||
string SourceId = "");
|
||||
|
||||
/// <summary>Block 叠加层元素。</summary>
|
||||
public class BlockOverlayItem
|
||||
{
|
||||
@@ -109,7 +131,7 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
event EventHandler<WizardOverlayPayload>? OverlayChanged;
|
||||
|
||||
/// <summary>发布 BGA 球叠加层。</summary>
|
||||
void PublishBgaBalls(IReadOnlyList<BallOverlayItem> balls);
|
||||
void PublishBgaBalls(IReadOnlyList<BallOverlayItem> balls, BallOverlayContext? context = null);
|
||||
|
||||
/// <summary>发布 Block 网格叠加层。</summary>
|
||||
void PublishBlockGrid(IReadOnlyList<BlockOverlayItem> blocks);
|
||||
@@ -142,13 +164,14 @@ namespace XplorePlane.Services.Inspection.Tasks
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PublishBgaBalls(IReadOnlyList<BallOverlayItem> balls)
|
||||
public void PublishBgaBalls(IReadOnlyList<BallOverlayItem> balls, BallOverlayContext? context = null)
|
||||
{
|
||||
_logger.LogDebug("发布 BGA 球叠加层: {Count} 个球", balls.Count);
|
||||
_currentPayload = new WizardOverlayPayload
|
||||
{
|
||||
Type = OverlayType.BgaBalls,
|
||||
Balls = balls,
|
||||
BallContext = context,
|
||||
StatusText = $"BGA 球: {balls.Count} 个"
|
||||
};
|
||||
_logger.LogInformation("[BGA-Debug] service cached type={Type}, balls={Count}",
|
||||
|
||||
@@ -56,6 +56,7 @@ namespace XplorePlane.ViewModels.Inspection.TaskWizard
|
||||
private readonly IInspectionTaskStore _taskStore;
|
||||
private readonly ITaskWizardOverlayService _overlayService;
|
||||
private readonly IBallOverlayService _ballOverlay;
|
||||
private readonly IPoolBallAnalysisService _ballAnalysis;
|
||||
private readonly IInspectionTeachInService _teachInService;
|
||||
private readonly IPoolTaskDefinitionMapper _definitionMapper;
|
||||
private readonly IInspectionDefinitionRepository _definitionRepository;
|
||||
@@ -383,6 +384,7 @@ namespace XplorePlane.ViewModels.Inspection.TaskWizard
|
||||
IInspectionTaskStore taskStore,
|
||||
ITaskWizardOverlayService overlayService,
|
||||
IBallOverlayService ballOverlay,
|
||||
IPoolBallAnalysisService ballAnalysis,
|
||||
IInspectionTeachInService teachInService,
|
||||
IPoolTaskDefinitionMapper definitionMapper,
|
||||
IInspectionDefinitionRepository definitionRepository,
|
||||
@@ -397,6 +399,7 @@ namespace XplorePlane.ViewModels.Inspection.TaskWizard
|
||||
_taskStore = taskStore;
|
||||
_overlayService = overlayService;
|
||||
_ballOverlay = ballOverlay;
|
||||
_ballAnalysis = ballAnalysis;
|
||||
_teachInService = teachInService;
|
||||
_definitionMapper = definitionMapper;
|
||||
_definitionRepository = definitionRepository;
|
||||
@@ -405,7 +408,7 @@ namespace XplorePlane.ViewModels.Inspection.TaskWizard
|
||||
// 注册命令
|
||||
LoadWorkpieceCommand = new DelegateCommand(ExecuteLoadWorkpiece);
|
||||
ApplyPipelineCommand = new DelegateCommand(ExecuteApplyPipeline);
|
||||
RunBgaDetectionCommand = new DelegateCommand(async () => await ExecuteRunBgaDetection());
|
||||
RunBgaDetectionCommand = new DelegateCommand(async () => await ExecuteRunBgaDetectionUnified());
|
||||
GeneratePoolCommand = new DelegateCommand(ExecuteGeneratePool);
|
||||
ApplyBubbleParametersCommand = new DelegateCommand(ExecuteApplyBubbleParameters);
|
||||
RunCalibrationCommand = new DelegateCommand(ExecuteRunCalibration);
|
||||
@@ -608,221 +611,33 @@ namespace XplorePlane.ViewModels.Inspection.TaskWizard
|
||||
CurrentStage = InspectionTaskStage.BGADetected;
|
||||
}
|
||||
|
||||
private Task ExecuteRunBgaDetection()
|
||||
private async Task ExecuteRunBgaDetectionUnified()
|
||||
{
|
||||
var pipelineBalls = TryReadPipelineBgaBalls();
|
||||
if (pipelineBalls != null)
|
||||
var pipeline = InspectionTask?.PipelineSnapshot;
|
||||
var sourceImage = _viewportService?.LatestDetectorImage as BitmapSource
|
||||
?? _viewportService?.CurrentDisplayImage as BitmapSource;
|
||||
if (pipeline == null || sourceImage == null)
|
||||
{
|
||||
DetectedBalls = pipelineBalls;
|
||||
_logger.LogInformation("[BGA-Pipeline] 复用流水线 BgaVoidRate 输出: {Count} 个球", DetectedBalls.Count);
|
||||
_ballOverlay.Publish(DetectedBalls);
|
||||
_eventAggregator.GetEvent<BgaBallsDetectedEvent>().Publish(DetectedBalls);
|
||||
CurrentStage = InspectionTaskStage.BGADetected;
|
||||
return Task.CompletedTask;
|
||||
_logger.LogError("[BGA-Pipeline] 缺少 PipelineSnapshot 或可执行的原始图像");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"[BGA-Pipeline] 未找到流水线 BgaVoidRate 输出,operator={Operator}, keys={Keys},回退到真实检测路径",
|
||||
_viewportService?.LatestPipelineOperatorKey ?? string.Empty,
|
||||
_viewportService?.LatestPipelineOutputData == null
|
||||
? "none"
|
||||
: string.Join(",", _viewportService.LatestPipelineOutputData.Keys));
|
||||
|
||||
_logger.LogError(
|
||||
"[BGA-Pipeline] 流水线未输出 BgaBalls,已拒绝使用独立 Ball 引擎。请在流水线中启用 BgaVoidRate 算子。"
|
||||
);
|
||||
return Task.CompletedTask;
|
||||
|
||||
#if false
|
||||
_logger.LogInformation("▶ ExecuteRunBgaDetection 开始");
|
||||
|
||||
try
|
||||
{
|
||||
// 阈值针对 16 位值域(0~65535)设定,对应 8 位约 5~40 的灰度范围
|
||||
var options = new BgaDetectionOptions
|
||||
{
|
||||
// 参考图中的焊球是大圆目标:当前 2512x2512 图像中预计直径约 250~500px,
|
||||
// 面积约 5 万~20 万像素。旧 MaxArea=5000 会把所有真实焊球直接过滤掉,
|
||||
// 最终只留下少量小圆噪声。
|
||||
MinArea = 15000,
|
||||
MaxArea = 300000,
|
||||
MinCircularity = 0.65,
|
||||
// 16 位图中焊球为暗目标,允许从 0 开始,避免切掉最暗的焊球区域。
|
||||
ThresholdLow = 0,
|
||||
ThresholdHigh = 12000,
|
||||
BlurSize = 3,
|
||||
AreaConsistency = 0.75
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"[BGA-Debug] options area=[{MinArea}..{MaxArea}], circularity={Circularity:F2}, threshold=[{Low}..{High}], consistency={Consistency:F2}",
|
||||
options.MinArea, options.MaxArea, options.MinCircularity,
|
||||
options.ThresholdLow, options.ThresholdHigh, options.AreaConsistency);
|
||||
|
||||
BGABall[] balls;
|
||||
|
||||
// 优先使用流水线输出的 16 位原始结果图(精度无损),
|
||||
// 回退到主视口当前显示图(可能是 8 位)
|
||||
var image16 = _viewportService?.LatestResultImage16 as BitmapSource;
|
||||
var imageFallback = _viewportService?.CurrentDisplayImage as BitmapSource;
|
||||
|
||||
if (image16 != null)
|
||||
{
|
||||
_logger.LogInformation("BGA 识别: 使用 LatestResultImage16 ({W}x{H}, {Format})",
|
||||
image16.PixelWidth, image16.PixelHeight, image16.Format);
|
||||
}
|
||||
else if (imageFallback != null)
|
||||
{
|
||||
_logger.LogInformation("BGA 识别: LatestResultImage16 不可用, 回退到 CurrentDisplayImage ({W}x{H}, {Format})",
|
||||
imageFallback.PixelWidth, imageFallback.PixelHeight, imageFallback.Format);
|
||||
}
|
||||
|
||||
var sourceImage = image16 ?? imageFallback;
|
||||
|
||||
if (sourceImage == null)
|
||||
{
|
||||
_logger.LogWarning("BGA 识别: 主视口无可用图像, 回退到仿真数据");
|
||||
balls = GenerateSimulatedBgaBalls();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
using var gray16 = BitmapSourceToGray16(sourceImage);
|
||||
balls = _bgaEngine.DetectBalls(gray16, options).ToArray();
|
||||
_logger.LogInformation("BGA 识别: 真实检测完成, {Count} 个球", balls.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "BGA 引擎检测失败, 回退到仿真数据");
|
||||
balls = GenerateSimulatedBgaBalls();
|
||||
}
|
||||
}
|
||||
|
||||
DetectedBalls = balls;
|
||||
InspectionTask = InspectionTask with { BgaDetectionOptions = options };
|
||||
|
||||
_logger.LogInformation(
|
||||
"[BGA-Debug] detected count={Count}, image={Width}x{Height}, balls={Balls}",
|
||||
DetectedBalls.Count,
|
||||
sourceImage?.PixelWidth ?? 0,
|
||||
sourceImage?.PixelHeight ?? 0,
|
||||
string.Join("; ", DetectedBalls.Take(8).Select(ball =>
|
||||
$"id={ball.Id},xy=({ball.X:F1},{ball.Y:F1}),d={ball.Diameter:F1},judge={ball.Judgement}")));
|
||||
|
||||
_logger.LogInformation("✓ BGA 识别完成: {Count} 个球", DetectedBalls.Count);
|
||||
|
||||
_ballOverlay.Publish(DetectedBalls);
|
||||
var analysis = await _ballAnalysis.AnalyzeAsync(sourceImage, pipeline);
|
||||
DetectedBalls = analysis.Balls;
|
||||
_ballOverlay.Publish(DetectedBalls, new BallOverlayContext(
|
||||
BallCoordinateSystem.ViewportImage,
|
||||
sourceImage.PixelWidth,
|
||||
sourceImage.PixelHeight,
|
||||
SourceId: InspectionTask.Id.ToString()));
|
||||
_eventAggregator.GetEvent<BgaBallsDetectedEvent>().Publish(DetectedBalls);
|
||||
CurrentStage = InspectionTaskStage.BGADetected;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "BGA 识别异常");
|
||||
_logger.LogError(ex, "[BGA-Pipeline] 统一流水线 Ball 识别失败");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>16 位灰度 BitmapSource → Emgu Image<Gray, ushort>(8 位输入升位,零精度损失路径优先)。</summary>
|
||||
private IReadOnlyList<BGABall>? TryReadPipelineBgaBalls()
|
||||
{
|
||||
if (!string.Equals(_viewportService?.LatestPipelineOperatorKey, "BgaVoidRate", StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
var output = _viewportService?.LatestPipelineOutputData;
|
||||
if (output == null || !output.TryGetValue("BgaBalls", out var value))
|
||||
return null;
|
||||
|
||||
if (value is not IEnumerable<BgaBallInfo> rawBalls)
|
||||
return null;
|
||||
|
||||
return rawBalls.Select(BgaDetectionEngineService.ToBGABall).ToArray();
|
||||
}
|
||||
|
||||
private static Image<Gray, ushort> BitmapSourceToGray16(BitmapSource bmp)
|
||||
{
|
||||
if (bmp.Format == PixelFormats.Gray16)
|
||||
{
|
||||
int w = bmp.PixelWidth, h = bmp.PixelHeight;
|
||||
int stride = w * 2;
|
||||
var pixels16 = new ushort[w * h];
|
||||
bmp.CopyPixels(pixels16, stride, 0);
|
||||
|
||||
var gray = new Image<Gray, ushort>(w, h);
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
gray.Data[y, x, 0] = pixels16[y * w + x];
|
||||
return gray;
|
||||
}
|
||||
|
||||
var formatted = new FormatConvertedBitmap(bmp, PixelFormats.Gray8, null, 0);
|
||||
int width = formatted.PixelWidth, height = formatted.PixelHeight;
|
||||
int stride8 = width;
|
||||
var pixels8 = new byte[height * stride8];
|
||||
formatted.CopyPixels(pixels8, stride8, 0);
|
||||
|
||||
var result = new Image<Gray, ushort>(width, height);
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
result.Data[y, x, 0] = (ushort)Math.Round(pixels8[y * stride8 + x] / 255.0 * 65535.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按行列聚类给焊球分配 A1~D4 风格分组编号,发布到主界面实时预览叠加层(绿色圆 + 编号)。
|
||||
/// 聚类失败(如球数不足)时回退为按 Id 显示的扁平序号,仍能画圆。
|
||||
/// </summary>
|
||||
private void PublishBgaBallsOverlay(IReadOnlyList<BGABall> balls)
|
||||
{
|
||||
var labels = BgaBallGroupLabeler.AssignLabels(balls);
|
||||
var items = balls.Select(ball => new BallOverlayItem
|
||||
{
|
||||
Id = ball.Id,
|
||||
CenterX = ball.X,
|
||||
CenterY = ball.Y,
|
||||
Diameter = ball.Diameter,
|
||||
Label = labels.TryGetValue(ball.Id, out var label) ? label : $"#{ball.Id}",
|
||||
IsFailed = ball.Judgement == BallJudgement.NG
|
||||
}).ToArray();
|
||||
|
||||
_logger.LogInformation(
|
||||
"[BGA-Debug] publish overlay count={Count}, first={First}",
|
||||
items.Length,
|
||||
items.Length == 0
|
||||
? "none"
|
||||
: $"id={items[0].Id},xy=({items[0].CenterX:F1},{items[0].CenterY:F1}),d={items[0].Diameter:F1}");
|
||||
|
||||
_overlayService.PublishBgaBalls(items);
|
||||
}
|
||||
|
||||
/// <summary>生成仿真 BGA 球数据(6 行 × 9 列 = 54 个球)</summary>
|
||||
private static BGABall[] GenerateSimulatedBgaBalls()
|
||||
{
|
||||
var rand = new Random(42);
|
||||
var balls = new List<BGABall>();
|
||||
|
||||
int cols = 9, rows = 6;
|
||||
double spacingX = 1280.0 / (cols + 1);
|
||||
double spacingY = 1024.0 / (rows + 1);
|
||||
|
||||
for (int r = 0; r < rows; r++)
|
||||
for (int c = 0; c < cols; c++)
|
||||
{
|
||||
double baseDiam = 60 + rand.NextDouble() * 40;
|
||||
balls.Add(new BGABall
|
||||
{
|
||||
Id = r * cols + c,
|
||||
X = (c + 1) * spacingX + rand.NextDouble() * 10 - 5,
|
||||
Y = (r + 1) * spacingY + rand.NextDouble() * 10 - 5,
|
||||
Diameter = Math.Round(baseDiam, 1),
|
||||
Gray = (ushort)(30000 + rand.NextDouble() * 20000),
|
||||
Confidence = 0.7 + rand.NextDouble() * 0.28,
|
||||
Judgement = BallJudgement.Pending,
|
||||
Reason = string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
return balls.ToArray();
|
||||
}
|
||||
|
||||
private void ExecuteGeneratePool()
|
||||
|
||||
Reference in New Issue
Block a user