156 lines
6.9 KiB
C#
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));
|
|
}
|
|
}
|