Files
XplorePlane/XplorePlane/Services/Inspection/Task/PoolBallAnalysisService.cs
T

156 lines
6.9 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using XP.ImageProcessing.Processors;
using XplorePlane.Models.ImageProcessing;
using XplorePlane.Models.Inspection.Tasks;
using XplorePlane.Services.ImageProcessing;
using XplorePlane.Services.Pipeline;
namespace XplorePlane.Services.Inspection.Tasks
{
public sealed record PoolBallAnalysisResult(
IReadOnlyList<BGABall> Balls,
BitmapSource ProcessedImage,
string Engine);
/// <summary>
/// 检测池/CNC 共用的 Ball 分析入口。
/// PipelineSnapshot 是唯一参数来源;没有流水线输出 BgaBalls 时直接失败,禁止切换到另一套识别算法。
/// </summary>
public interface IPoolBallAnalysisService
{
Task<PoolBallAnalysisResult> AnalyzeAsync(
BitmapSource image,
PipelineModel pipeline,
CancellationToken cancellationToken = default);
}
public sealed class PoolBallAnalysisService : IPoolBallAnalysisService
{
private readonly IPipelineExecutionService _pipeline;
private readonly IImageProcessingService _imageProcessing;
public PoolBallAnalysisService(
IPipelineExecutionService pipeline,
IImageProcessingService imageProcessing)
{
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
_imageProcessing = imageProcessing ?? throw new ArgumentNullException(nameof(imageProcessing));
}
public async Task<PoolBallAnalysisResult> AnalyzeAsync(
BitmapSource image,
PipelineModel pipeline,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(image);
ArgumentNullException.ThrowIfNull(pipeline);
using var source16 = ImageConverter.ToEmguCV16(image);
var result = await _pipeline.ExecutePipelineAsync16(
BuildNodes(pipeline), source16, cancellationToken: cancellationToken);
var balls = ReadPipelineBalls(result.LastStepOutputData);
if (balls.Count == 0)
throw new InvalidOperationException(
"Pipeline 未输出 BgaBalls。请确认流水线包含启用的 BgaVoidRate 算子,并检查其参数。");
return new PoolBallAnalysisResult(balls, result.Image, "Pipeline");
}
private IReadOnlyList<PipelineNodeViewModel> BuildNodes(PipelineModel model)
{
var nodes = new List<PipelineNodeViewModel>();
foreach (var item in model.Nodes?.OrderBy(n => n.Order) ?? Enumerable.Empty<PipelineNodeModel>())
{
var displayName = _imageProcessing.GetProcessorDisplayName(item.OperatorKey) ?? item.OperatorKey;
var node = new PipelineNodeViewModel(item.OperatorKey, displayName)
{
Order = item.Order,
IsEnabled = item.IsEnabled
};
var definitions = _imageProcessing.GetProcessorParameters(item.OperatorKey);
if (definitions == null) continue;
foreach (var definition in definitions)
{
var parameter = new ProcessorParameterVM(definition);
if (item.Parameters != null && item.Parameters.TryGetValue(definition.Name, out var saved))
parameter.Value = ConvertSavedValue(saved, definition.ValueType);
node.Parameters.Add(parameter);
}
nodes.Add(node);
}
return nodes;
}
private static IReadOnlyList<BGABall> ReadPipelineBalls(IReadOnlyDictionary<string, object>? output)
{
if (output == null || !output.TryGetValue("BgaBalls", out var value) || value is not IEnumerable items)
return Array.Empty<BGABall>();
var balls = new List<BGABall>();
foreach (var item in items)
{
if (item is BgaBallInfo raw)
balls.Add(BgaDetectionEngineService.ToBGABall(raw));
else if (item is BGABall ball)
balls.Add(ball);
}
return balls;
}
private static object ConvertSavedValue(object savedValue, Type targetType)
{
if (savedValue is not JsonElement json) return savedValue;
if (targetType == typeof(int) && json.TryGetInt32(out var intValue)) return intValue;
if (targetType == typeof(double) && json.TryGetDouble(out var doubleValue)) return doubleValue;
if (targetType == typeof(bool) && (json.ValueKind == JsonValueKind.True || json.ValueKind == JsonValueKind.False))
return json.GetBoolean();
if (targetType == typeof(string)) return json.GetString() ?? string.Empty;
return json.ToString();
}
}
public static class PoolBallMatcher
{
/// <summary>把 Block 图像内检测结果绑定到向导生成的全局 Ball 模板。</summary>
public static IReadOnlyList<BGABall> MatchBlock(
InspectionBlock block,
IReadOnlyList<BGABall> detected,
int imageWidth,
int imageHeight)
{
if (block.Balls.Count == 0) return detected;
var used = new HashSet<int>();
var result = new List<BGABall>(block.Balls.Count);
foreach (var expected in block.Balls.OrderBy(b => b.Y).ThenBy(b => b.X))
{
var ex = block.Width <= 0 ? 0.5 : (expected.X - (block.CenterX - block.Width / 2)) / block.Width;
var ey = block.Height <= 0 ? 0.5 : (expected.Y - (block.CenterY - block.Height / 2)) / block.Height;
var candidate = detected
.Select((ball, index) => new { ball, index })
.Where(x => !used.Contains(x.index))
.OrderBy(x => Distance(ex, ey, imageWidth <= 0 ? 0.5 : x.ball.X / imageWidth,
imageHeight <= 0 ? 0.5 : x.ball.Y / imageHeight))
.FirstOrDefault();
if (candidate == null || Distance(ex, ey, imageWidth <= 0 ? 0.5 : candidate.ball.X / imageWidth,
imageHeight <= 0 ? 0.5 : candidate.ball.Y / imageHeight) > 0.25)
{
result.Add(expected with { Judgement = BallJudgement.Missing, Reason = "未检测到对应焊球" });
continue;
}
used.Add(candidate.index);
result.Add(candidate.ball with { Id = expected.Id, X = expected.X, Y = expected.Y });
}
return result;
}
private static double Distance(double x1, double y1, double x2, double y2)
=> Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2));
}
}