1309 lines
58 KiB
C#
1309 lines
58 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using OrpaonVision.Core.LayerRecognition;
|
||
using OrpaonVision.Core.PartRecognition;
|
||
using OrpaonVision.Core.PlacementJudgment;
|
||
using OrpaonVision.Core.PositionValidation;
|
||
using OrpaonVision.Core.Results;
|
||
using OrpaonVision.SiteApp.Runtime.Options;
|
||
using System.Collections.Concurrent;
|
||
using System.Text.Json;
|
||
|
||
#if false
|
||
|
||
namespace OrpaonVision.SiteApp.Runtime.Services;
|
||
|
||
using CoreCompositeJudgmentStrategy = OrpaonVision.Core.PlacementJudgment.CompositeJudgmentStrategy;
|
||
|
||
/// <summary>
|
||
/// 到位判定服务实现。
|
||
/// </summary>
|
||
public sealed class PlacementJudgmentService : IPlacementJudgmentService
|
||
{
|
||
private readonly ILogger<PlacementJudgmentService> _logger;
|
||
private readonly RuntimeOptions _options;
|
||
private readonly ConcurrentDictionary<Guid, PlacementJudgmentRule> _judgmentRules = new();
|
||
private readonly ConcurrentDictionary<Guid, PlacementJudgmentResult> _judgmentHistory = new();
|
||
private readonly object _lock = new();
|
||
|
||
/// <summary>
|
||
/// 构造函数。
|
||
/// </summary>
|
||
public PlacementJudgmentService(ILogger<PlacementJudgmentService> logger, IOptions<RuntimeOptions> options)
|
||
{
|
||
_logger = logger;
|
||
_options = options.Value;
|
||
InitializeDefaultJudgmentRules();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<PlacementJudgmentResult>> JudgePlacementAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<PlacementJudgmentRule> judgmentRules, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始到位判定:部件数量={PartCount},规则数量={RuleCount}", parts.Count, judgmentRules.Count);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
// 执行部件级判定
|
||
var partJudgmentResults = new List<PartJudgmentResult>();
|
||
var unplacedParts = new List<MappedPart>();
|
||
|
||
foreach (var part in parts)
|
||
{
|
||
var partResult = await JudgePartAsync(part, judgmentRules, cancellationToken);
|
||
partJudgmentResults.Add(partResult);
|
||
|
||
if (!partResult.IsPlaced)
|
||
{
|
||
unplacedParts.Add(part);
|
||
}
|
||
}
|
||
|
||
// 计算总体置信度
|
||
var overallConfidence = partJudgmentResults.Any()
|
||
? partJudgmentResults.Average(r => r.PlacementConfidence)
|
||
: 0.0;
|
||
|
||
var isPlaced = unplacedParts.Count == 0;
|
||
|
||
var result = new PlacementJudgmentResult
|
||
{
|
||
IsPlaced = isPlaced,
|
||
OverallConfidence = overallConfidence,
|
||
JudgmentTimeUtc = startTime,
|
||
PartJudgmentResults = partJudgmentResults,
|
||
UnplacedParts = unplacedParts,
|
||
Details = isPlaced
|
||
? $"到位判定通过:总体置信度={overallConfidence:F3}"
|
||
: $"到位判定失败:未到位部件数量={unplacedParts.Count},总体置信度={overallConfidence:F3}",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_type"] = "placement",
|
||
["part_count"] = parts.Count,
|
||
["unplaced_count"] = unplacedParts.Count,
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
};
|
||
|
||
// 记录判定历史
|
||
await AddJudgmentHistoryAsync(result, cancellationToken);
|
||
|
||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||
_logger.LogInformation("到位判定完成:总数={TotalCount},到位={PlacedCount},未到位={UnplacedCount},结果={IsPlaced},耗时={ElapsedMs:F1}ms",
|
||
parts.Count, parts.Count - unplacedParts.Count, unplacedParts.Count, result.IsPlaced, elapsed);
|
||
|
||
return Result.Success(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "到位判定失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "PLACEMENT_JUDGMENT_FAILED", "到位判定失败", traceId);
|
||
return Result.FailWithTrace<PlacementJudgmentResult>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<AreaJudgmentResult>> JudgeAreaAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<ExpectedArea> expectedAreas, double tolerance, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始面积判定:部件数量={PartCount},期望面积数量={ExpectedCount},容差={Tolerance:F3}", parts.Count, expectedAreas.Count, tolerance);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
// 执行面积匹配和判定
|
||
var areaMatches = new List<(MappedPart Part, ExpectedArea ExpectedArea, double ActualArea, double Deviation)>();
|
||
|
||
foreach (var expectedArea in expectedAreas)
|
||
{
|
||
var matchingParts = parts.Where(p =>
|
||
(!expectedArea.ExpectedPartType.HasValue || p.PartType == expectedArea.ExpectedPartType.Value) &&
|
||
(string.IsNullOrEmpty(expectedArea.ExpectedPartCode) || p.PartCode.Equals(expectedArea.ExpectedPartCode, StringComparison.OrdinalIgnoreCase))
|
||
).ToList();
|
||
|
||
foreach (var part in matchingParts)
|
||
{
|
||
var actualArea = part.BoundingBox.Area;
|
||
var deviation = Math.Abs(actualArea - expectedArea.ExpectedAreaValue) / expectedArea.ExpectedAreaValue * 100;
|
||
|
||
areaMatches.Add((part, expectedArea, actualArea, deviation));
|
||
}
|
||
}
|
||
|
||
// 判定是否通过
|
||
var isAreaValid = areaMatches.All(m => m.Deviation <= tolerance);
|
||
var averageDeviation = areaMatches.Any() ? areaMatches.Average(m => m.Deviation) : 0.0;
|
||
|
||
var result = new AreaJudgmentResult
|
||
{
|
||
IsAreaValid = isAreaValid,
|
||
ActualArea = areaMatches.Any() ? areaMatches.Average(m => m.ActualArea) : 0.0,
|
||
ExpectedArea = expectedAreas.Any() ? expectedAreas.Average(e => e.ExpectedAreaValue) : 0.0,
|
||
AreaDeviation = averageDeviation,
|
||
AllowedTolerance = tolerance,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = isAreaValid
|
||
? $"面积判定通过:平均偏差={averageDeviation:F2}%,容差={tolerance:F2}%"
|
||
: $"面积判定失败:平均偏差={averageDeviation:F2}%,容差={tolerance:F2}%",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_type"] = "area",
|
||
["match_count"] = areaMatches.Count,
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
};
|
||
|
||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||
_logger.LogInformation("面积判定完成:匹配数量={MatchCount},平均偏差={AvgDeviation:F2}%,结果={IsValid},耗时={ElapsedMs:F1}ms",
|
||
areaMatches.Count, result.AreaDeviation, result.IsAreaValid, elapsed);
|
||
|
||
return Result.Success(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "面积判定失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "AREA_JUDGMENT_FAILED", "面积判定失败", traceId);
|
||
return Result.FailWithTrace<AreaJudgmentResult>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<PositionDeviationJudgmentResult>> JudgePositionDeviationAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<ExpectedPosition> expectedPositions, double tolerance, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始位置偏差判定:部件数量={PartCount},期望位置数量={ExpectedCount},容差={Tolerance:F2}", parts.Count, expectedPositions.Count, tolerance);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
// 执行位置匹配和判定
|
||
var positionMatches = new List<(MappedPart Part, ExpectedPosition ExpectedPosition, Point ActualPosition, Point ExpectedPosition, double Deviation)>();
|
||
|
||
foreach (var expectedPosition in expectedPositions)
|
||
{
|
||
var matchingParts = parts.Where(p =>
|
||
(!expectedPosition.ExpectedPartType.HasValue || p.PartType == expectedPosition.ExpectedPartType.Value) &&
|
||
(string.IsNullOrEmpty(expectedPosition.ExpectedPartCode) || p.PartCode.Equals(expectedPosition.ExpectedPartCode, StringComparison.OrdinalIgnoreCase))
|
||
).ToList();
|
||
|
||
foreach (var part in matchingParts)
|
||
{
|
||
var actualPosition = new Point { X = part.BoundingBox.Center.X, Y = part.BoundingBox.Center.Y };
|
||
var deviation = actualPosition.DistanceTo(expectedPosition.ExpectedCenterPoint);
|
||
|
||
positionMatches.Add((part, expectedPosition, actualPosition, expectedPosition.ExpectedCenterPoint, deviation));
|
||
}
|
||
}
|
||
|
||
// 判定是否通过
|
||
var isPositionValid = positionMatches.All(m => m.Deviation <= tolerance);
|
||
var averageDeviation = positionMatches.Any() ? positionMatches.Average(m => m.Deviation) : 0.0;
|
||
|
||
var result = new PositionDeviationJudgmentResult
|
||
{
|
||
IsPositionValid = isPositionValid,
|
||
ActualPosition = positionMatches.Any() ? new Point { X = positionMatches.Average(m => m.ActualPosition.X), Y = positionMatches.Average(m => m.ActualPosition.Y) } : new Point(),
|
||
ExpectedPosition = expectedPositions.Any() ? new Point { X = expectedPositions.Average(p => p.ExpectedCenterPoint.X), Y = expectedPositions.Average(p => p.ExpectedCenterPoint.Y) } : new Point(),
|
||
PositionDeviation = averageDeviation,
|
||
AllowedTolerance = tolerance,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = isPositionValid
|
||
? $"位置偏差判定通过:平均偏差={averageDeviation:F2},容差={tolerance:F2}"
|
||
: $"位置偏差判定失败:平均偏差={averageDeviation:F2},容差={tolerance:F2}",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_type"] = "position_deviation",
|
||
["match_count"] = positionMatches.Count,
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
};
|
||
|
||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||
_logger.LogInformation("位置偏差判定完成:匹配数量={MatchCount},平均偏差={AvgDeviation:F2},结果={IsValid},耗时={ElapsedMs:F1}ms",
|
||
positionMatches.Count, result.PositionDeviation, result.IsPositionValid, elapsed);
|
||
|
||
return Result.Success(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "位置偏差判定失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "POSITION_DEVIATION_JUDGMENT_FAILED", "位置偏差判定失败", traceId);
|
||
return Result.FailWithTrace<PositionDeviationJudgmentResult>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<StabilityJudgmentResult>> JudgeStabilityAsync(IReadOnlyList<InferenceResultDto> frameHistory, IReadOnlyList<StabilityRule> stabilityRules, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始稳定性判定:帧数量={FrameCount},规则数量={RuleCount}", frameHistory.Count, stabilityRules.Count);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
if (frameHistory.Count < 2)
|
||
{
|
||
return Result.Success(new StabilityJudgmentResult
|
||
{
|
||
IsStable = true, // 帧数不足时默认稳定
|
||
StabilityScore = 1.0,
|
||
StableFrameCount = frameHistory.Count,
|
||
TotalFrameCount = frameHistory.Count,
|
||
StabilityThreshold = stabilityRules.Any() ? stabilityRules.First().StabilityThreshold : 0.8,
|
||
PositionChangeRate = 0.0,
|
||
AreaChangeRate = 0.0,
|
||
ConfidenceChangeRate = 0.0,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = "帧数量不足,默认判定为稳定",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_type"] = "stability",
|
||
["frame_count"] = frameHistory.Count,
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
});
|
||
}
|
||
|
||
// 计算稳定性指标
|
||
var stabilityMetrics = await CalculateStabilityMetricsAsync(frameHistory, cancellationToken);
|
||
|
||
// 应用稳定性规则
|
||
var isStable = stabilityRules.Any()
|
||
? stabilityRules.All(rule => IsStableAccordingToRule(stabilityMetrics, rule))
|
||
: stabilityMetrics.StabilityScore >= 0.8; // 默认阈值
|
||
|
||
var result = new StabilityJudgmentResult
|
||
{
|
||
IsStable = isStable,
|
||
StabilityScore = stabilityMetrics.StabilityScore,
|
||
StableFrameCount = stabilityMetrics.StableFrameCount,
|
||
TotalFrameCount = frameHistory.Count,
|
||
StabilityThreshold = stabilityRules.Any() ? stabilityRules.First().StabilityThreshold : 0.8,
|
||
PositionChangeRate = stabilityMetrics.PositionChangeRate,
|
||
AreaChangeRate = stabilityMetrics.AreaChangeRate,
|
||
ConfidenceChangeRate = stabilityMetrics.ConfidenceChangeRate,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = isStable
|
||
? $"稳定性判定通过:稳定性分数={stabilityMetrics.StabilityScore:F3},阈值={(stabilityRules.Any() ? stabilityRules.First().StabilityThreshold : 0.8):F3}"
|
||
: $"稳定性判定失败:稳定性分数={stabilityMetrics.StabilityScore:F3},阈值={(stabilityRules.Any() ? stabilityRules.First().StabilityThreshold : 0.8):F3}",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_type"] = "stability",
|
||
["frame_count"] = frameHistory.Count,
|
||
["stable_frame_count"] = stabilityMetrics.StableFrameCount,
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
};
|
||
|
||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||
_logger.LogInformation("稳定性判定完成:总帧数={TotalFrames},稳定帧数={StableFrames},稳定性分数={Score:F3},结果={IsStable},耗时={ElapsedMs:F1}ms",
|
||
result.TotalFrameCount, result.StableFrameCount, result.StabilityScore, result.IsStable, elapsed);
|
||
|
||
return Result.Success(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "稳定性判定失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "STABILITY_JUDGMENT_FAILED", "稳定性判定失败", traceId);
|
||
return Result.FailWithTrace<StabilityJudgmentResult>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<CompositeJudgmentResult>> JudgeCompositeAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<InferenceResultDto> frameHistory, IReadOnlyList<CompositeJudgmentRule> compositeRules, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始复合判定:部件数量={PartCount},帧数量={FrameCount},规则数量={RuleCount}", parts.Count, frameHistory.Count, compositeRules.Count);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
// 执行各个子判定
|
||
var areaResult = await JudgeAreaAsync(parts, CreateDefaultExpectedAreas(parts), _options.DefaultAreaTolerance, cancellationToken);
|
||
var positionResult = await JudgePositionDeviationAsync(parts, CreateDefaultExpectedPositions(parts), _options.DefaultPositionTolerance, cancellationToken);
|
||
var stabilityResult = await JudgeStabilityAsync(frameHistory, CreateDefaultStabilityRules(), cancellationToken);
|
||
|
||
// 应用复合判定策略
|
||
var compositeResult = await ApplyCompositeStrategyAsync(areaResult.Data, positionResult.Data, stabilityResult.Data, compositeRules, cancellationToken);
|
||
|
||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||
_logger.LogInformation("复合判定完成:策略={Strategy},加权分数={WeightedScore:F3},结果={IsValid},耗时={ElapsedMs:F1}ms",
|
||
compositeResult.JudgmentStrategy, compositeResult.WeightedScore, compositeResult.IsCompositeValid, elapsed);
|
||
|
||
return Result.Success(compositeResult);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "复合判定失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "COMPOSITE_JUDGMENT_FAILED", "复合判定失败", traceId);
|
||
return Result.FailWithTrace<CompositeJudgmentResult>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<BatchJudgmentResult>> BatchJudgeAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<InferenceResultDto> frameHistory, IReadOnlyList<PlacementJudgmentRule> judgmentRules, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("开始批量到位判定:部件数量={PartCount},帧数量={FrameCount},规则数量={RuleCount}",
|
||
parts.Count, frameHistory.Count, judgmentRules.Count);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
var judgmentResults = new List<PlacementJudgmentResult>();
|
||
var passedRules = 0;
|
||
var failedRules = 0;
|
||
|
||
foreach (var rule in judgmentRules.Where(r => r.IsEnabled))
|
||
{
|
||
var judgmentResult = await JudgeRuleAsync(parts, frameHistory, rule, cancellationToken);
|
||
judgmentResults.Add(judgmentResult);
|
||
|
||
if (judgmentResult.IsPlaced)
|
||
{
|
||
passedRules++;
|
||
}
|
||
else
|
||
{
|
||
failedRules++;
|
||
}
|
||
}
|
||
|
||
var elapsed = DateTime.UtcNow - startTime;
|
||
|
||
var result = new BatchJudgmentResult
|
||
{
|
||
JudgmentResults = judgmentResults,
|
||
TotalRules = judgmentRules.Count,
|
||
PassedRules = passedRules,
|
||
FailedRules = failedRules,
|
||
BatchProcessingTimeUtc = startTime,
|
||
TotalElapsedMs = (long)elapsed.TotalMilliseconds,
|
||
Details = $"批量到位判定完成:总数={judgmentRules.Count},通过={passedRules},失败={failedRules},通过率={result.OverallPassRate:F2}%",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["throughput_per_second"] = judgmentRules.Count / Math.Max(elapsed.TotalSeconds, 0.001)
|
||
}
|
||
};
|
||
|
||
_logger.LogInformation("批量到位判定完成:总数={TotalCount},通过={PassedCount},失败={FailedCount},通过率={PassRate:F2}%,耗时={ElapsedMs}ms",
|
||
result.TotalRules, result.PassedRules, result.FailedRules, result.OverallPassRate, result.TotalElapsedMs);
|
||
|
||
return Result.Success(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "批量到位判定失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "BATCH_JUDGMENT_FAILED", "批量到位判定失败", traceId);
|
||
return Result.FailWithTrace<BatchJudgmentResult>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<PlacementJudgmentStatistics>> GetJudgmentStatisticsAsync(DateTime startTime, DateTime endTime, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("获取到位判定统计信息:开始时间={StartTime},结束时间={EndTime}", startTime, endTime);
|
||
|
||
var judgmentRecords = _judgmentHistory.Values
|
||
.Where(r => r.JudgmentTimeUtc >= startTime && r.JudgmentTimeUtc <= endTime)
|
||
.ToList();
|
||
|
||
var statistics = new PlacementJudgmentStatistics
|
||
{
|
||
StartTimeUtc = startTime,
|
||
EndTimeUtc = endTime,
|
||
TotalJudgments = judgmentRecords.Count,
|
||
SuccessfulJudgments = judgmentRecords.Count(r => r.IsPlaced),
|
||
FailedJudgments = judgmentRecords.Count(r => !r.IsPlaced)
|
||
};
|
||
|
||
// 按判定类型分组统计
|
||
// 这里简化处理,实际可以根据扩展属性中的judgment_type进行分组
|
||
statistics.ByJudgmentType[PlacementJudgmentType.Composite] = new JudgmentTypeStatistics
|
||
{
|
||
JudgmentType = PlacementJudgmentType.Composite,
|
||
JudgmentCount = judgmentRecords.Count,
|
||
SuccessCount = statistics.SuccessfulJudgments,
|
||
AverageElapsedMs = judgmentRecords.Where(r => r.ExtendedProperties.ContainsKey("judgment_time_ms"))
|
||
.Average(r => Convert.ToDouble(r.ExtendedProperties["judgment_time_ms"]))
|
||
};
|
||
|
||
_logger.LogInformation("到位判定统计信息获取完成:总判定次数={Total},成功率={SuccessRate:F2}%,平均耗时={AvgElapsedMs:F1}ms",
|
||
statistics.TotalJudgments, statistics.OverallSuccessRate,
|
||
statistics.ByJudgmentType.Values.Average(v => v.AverageElapsedMs));
|
||
|
||
return Result.Success(statistics);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "获取到位判定统计信息失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "GET_JUDGMENT_STATISTICS_FAILED", "获取到位判定统计信息失败", traceId);
|
||
return Result.FailWithTrace<PlacementJudgmentStatistics>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<PlacementJudgmentRule>> CreateJudgmentRuleAsync(PlacementJudgmentRule rule, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("创建到位判定规则:规则名称={RuleName},判定类型={JudgmentType}",
|
||
rule.RuleName, rule.JudgmentType);
|
||
|
||
lock (_lock)
|
||
{
|
||
rule.RuleId = Guid.NewGuid();
|
||
rule.CreatedAtUtc = DateTime.UtcNow;
|
||
rule.UpdatedAtUtc = DateTime.UtcNow;
|
||
rule.Version = "1.0";
|
||
|
||
_judgmentRules[rule.RuleId] = rule;
|
||
}
|
||
|
||
_logger.LogInformation("到位判定规则创建成功:规则ID={RuleId},规则名称={RuleName}",
|
||
rule.RuleId, rule.RuleName);
|
||
|
||
return Result.Success(rule);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "创建到位判定规则失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "CREATE_JUDGMENT_RULE_FAILED", "创建到位判定规则失败", traceId);
|
||
return Result.FailWithTrace<PlacementJudgmentRule>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result> UpdateJudgmentRuleAsync(PlacementJudgmentRule rule, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("更新到位判定规则:规则ID={RuleId},规则名称={RuleName}",
|
||
rule.RuleId, rule.RuleName);
|
||
|
||
lock (_lock)
|
||
{
|
||
if (_judgmentRules.ContainsKey(rule.RuleId))
|
||
{
|
||
rule.UpdatedAtUtc = DateTime.UtcNow;
|
||
_judgmentRules[rule.RuleId] = rule;
|
||
}
|
||
else
|
||
{
|
||
return Result.Fail("RULE_NOT_FOUND", $"未找到规则:{rule.RuleId}");
|
||
}
|
||
}
|
||
|
||
_logger.LogInformation("到位判定规则更新成功:规则ID={RuleId}", rule.RuleId);
|
||
return Result.Success();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "更新到位判定规则失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "UPDATE_JUDGMENT_RULE_FAILED", "更新到位判定规则失败", traceId);
|
||
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result> DeleteJudgmentRuleAsync(Guid ruleId, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("删除到位判定规则:规则ID={RuleId}", ruleId);
|
||
|
||
lock (_lock)
|
||
{
|
||
if (_judgmentRules.TryRemove(ruleId, out _))
|
||
{
|
||
_logger.LogInformation("到位判定规则删除成功:规则ID={RuleId}", ruleId);
|
||
return Result.Success();
|
||
}
|
||
else
|
||
{
|
||
return Result.Fail("RULE_NOT_FOUND", $"未找到规则:{ruleId}");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "删除到位判定规则失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "DELETE_JUDGMENT_RULE_FAILED", "删除到位判定规则失败", traceId);
|
||
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<Result<IReadOnlyList<PlacementJudgmentRule>>> GetJudgmentRulesAsync(string? productTypeCode = null, int? layerNumber = null, PlacementJudgmentType? judgmentType = null, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("获取到位判定规则列表:产品类型={ProductTypeCode},层级={LayerNumber},判定类型={JudgmentType}",
|
||
productTypeCode, layerNumber, judgmentType);
|
||
|
||
var rules = _judgmentRules.Values.AsEnumerable();
|
||
|
||
if (!string.IsNullOrEmpty(productTypeCode))
|
||
{
|
||
rules = rules.Where(r => r.ProductTypeCode.Equals(productTypeCode, StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
|
||
if (layerNumber.HasValue)
|
||
{
|
||
rules = rules.Where(r => r.LayerNumber == layerNumber.Value);
|
||
}
|
||
|
||
if (judgmentType.HasValue)
|
||
{
|
||
rules = rules.Where(r => r.JudgmentType == judgmentType.Value);
|
||
}
|
||
|
||
var result = rules.OrderBy(r => r.Priority).ThenBy(r => r.RuleName).ToList();
|
||
|
||
_logger.LogDebug("获取到位判定规则列表完成:规则数量={RuleCount}", result.Count);
|
||
return Result.Success<IReadOnlyList<PlacementJudgmentRule>>(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var traceId = Guid.NewGuid().ToString("N");
|
||
_logger.LogError(ex, "获取到位判定规则列表失败。TraceId: {TraceId}", traceId);
|
||
var result = Result.FromException(ex, "GET_JUDGMENT_RULES_FAILED", "获取到位判定规则列表失败", traceId);
|
||
return Result.FailWithTrace<IReadOnlyList<PlacementJudgmentRule>>(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
||
}
|
||
}
|
||
|
||
#region 私有方法
|
||
|
||
/// <summary>
|
||
/// 初始化默认判定规则。
|
||
/// </summary>
|
||
private void InitializeDefaultJudgmentRules()
|
||
{
|
||
var defaultRules = new List<PlacementJudgmentRule>
|
||
{
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "面积到位判定规则",
|
||
RuleDescription = "校验部件面积是否符合期望",
|
||
ProductTypeCode = "default",
|
||
LayerNumber = 0,
|
||
JudgmentType = PlacementJudgmentType.Area,
|
||
AreaTolerance = 0.15,
|
||
PositionTolerance = 10.0,
|
||
StabilityThreshold = 0.8,
|
||
StabilityWindowSize = 10,
|
||
CompositeStrategy = CoreCompositeJudgmentStrategy.WeightedAverage,
|
||
Weights = new JudgmentWeights { AreaWeight = 1.0, PositionWeight = 0.0, StabilityWeight = 0.0 },
|
||
IsEnabled = true,
|
||
Priority = 1,
|
||
CreatedAtUtc = DateTime.UtcNow,
|
||
UpdatedAtUtc = DateTime.UtcNow,
|
||
Version = "1.0"
|
||
},
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "位置偏差到位判定规则",
|
||
RuleDescription = "校验部件位置偏差是否在容差范围内",
|
||
ProductTypeCode = "default",
|
||
LayerNumber = 0,
|
||
JudgmentType = PlacementJudgmentType.PositionDeviation,
|
||
AreaTolerance = 0.15,
|
||
PositionTolerance = 15.0,
|
||
StabilityThreshold = 0.8,
|
||
StabilityWindowSize = 10,
|
||
CompositeStrategy = CoreCompositeJudgmentStrategy.WeightedAverage,
|
||
Weights = new JudgmentWeights { AreaWeight = 0.0, PositionWeight = 1.0, StabilityWeight = 0.0 },
|
||
IsEnabled = true,
|
||
Priority = 2,
|
||
CreatedAtUtc = DateTime.UtcNow,
|
||
UpdatedAtUtc = DateTime.UtcNow,
|
||
Version = "1.0"
|
||
},
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "稳定性到位判定规则",
|
||
RuleDescription = "校验部件在多帧中的稳定性",
|
||
ProductTypeCode = "default",
|
||
LayerNumber = 0,
|
||
JudgmentType = PlacementJudgmentType.Stability,
|
||
AreaTolerance = 0.15,
|
||
PositionTolerance = 10.0,
|
||
StabilityThreshold = 0.85,
|
||
StabilityWindowSize = 15,
|
||
CompositeStrategy = CoreCompositeJudgmentStrategy.WeightedAverage,
|
||
Weights = new JudgmentWeights { AreaWeight = 0.0, PositionWeight = 0.0, StabilityWeight = 1.0 },
|
||
IsEnabled = true,
|
||
Priority = 3,
|
||
CreatedAtUtc = DateTime.UtcNow,
|
||
UpdatedAtUtc = DateTime.UtcNow,
|
||
Version = "1.0"
|
||
},
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "复合到位判定规则",
|
||
RuleDescription = "综合面积、位置偏差和稳定性的复合判定",
|
||
ProductTypeCode = "default",
|
||
LayerNumber = 0,
|
||
JudgmentType = PlacementJudgmentType.Composite,
|
||
AreaTolerance = 0.15,
|
||
PositionTolerance = 15.0,
|
||
StabilityThreshold = 0.8,
|
||
StabilityWindowSize = 10,
|
||
CompositeStrategy = CoreCompositeJudgmentStrategy.WeightedAverage,
|
||
Weights = new JudgmentWeights { AreaWeight = 0.3, PositionWeight = 0.4, StabilityWeight = 0.3 },
|
||
IsEnabled = true,
|
||
Priority = 4,
|
||
CreatedAtUtc = DateTime.UtcNow,
|
||
UpdatedAtUtc = DateTime.UtcNow,
|
||
Version = "1.0"
|
||
}
|
||
};
|
||
|
||
foreach (var rule in defaultRules)
|
||
{
|
||
_judgmentRules[rule.RuleId] = rule;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判定单个部件。
|
||
/// </summary>
|
||
private async Task<PartJudgmentResult> JudgePartAsync(MappedPart part, IReadOnlyList<PlacementJudgmentRule> judgmentRules, CancellationToken cancellationToken = default)
|
||
{
|
||
await Task.Delay(1, cancellationToken);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
var areaResult = (AreaJudgmentResult?)null;
|
||
var positionResult = (PositionDeviationJudgmentResult?)null;
|
||
var stabilityResult = (StabilityJudgmentResult?)null;
|
||
|
||
foreach (var rule in judgmentRules.Where(r => r.IsEnabled))
|
||
{
|
||
switch (rule.JudgmentType)
|
||
{
|
||
case PlacementJudgmentType.Area:
|
||
// 简化处理,使用部件面积与期望面积的对比
|
||
areaResult = new AreaJudgmentResult
|
||
{
|
||
IsAreaValid = true, // 简化处理
|
||
ActualArea = part.BoundingBox.Area,
|
||
ExpectedArea = part.BoundingBox.Area, // 假设期望面积等于实际面积
|
||
AreaDeviation = 0.0,
|
||
AllowedTolerance = rule.AreaTolerance,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = "面积判定通过(简化处理)"
|
||
};
|
||
break;
|
||
|
||
case PlacementJudgmentType.PositionDeviation:
|
||
// 简化处理,使用当前位置与期望位置的对比
|
||
positionResult = new PositionDeviationJudgmentResult
|
||
{
|
||
IsPositionValid = true, // 简化处理
|
||
ActualPosition = new Point { X = part.BoundingBox.Center.X, Y = part.BoundingBox.Center.Y },
|
||
ExpectedPosition = new Point { X = part.BoundingBox.Center.X, Y = part.BoundingBox.Center.Y }, // 假设期望位置等于实际位置
|
||
PositionDeviation = 0.0,
|
||
AllowedTolerance = rule.PositionTolerance,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = "位置偏差判定通过(简化处理)"
|
||
};
|
||
break;
|
||
|
||
case PlacementJudgmentType.Stability:
|
||
// 简化处理,假设稳定性良好
|
||
stabilityResult = new StabilityJudgmentResult
|
||
{
|
||
IsStable = true,
|
||
StabilityScore = 0.9,
|
||
StableFrameCount = 10,
|
||
TotalFrameCount = 10,
|
||
StabilityThreshold = rule.StabilityThreshold,
|
||
PositionChangeRate = 0.05,
|
||
AreaChangeRate = 0.03,
|
||
ConfidenceChangeRate = 0.02,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = "稳定性判定通过(简化处理)"
|
||
};
|
||
break;
|
||
|
||
case PlacementJudgmentType.Composite:
|
||
// 复合判定在主方法中处理
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 计算总体到位置信度
|
||
var confidenceScores = new List<double>();
|
||
if (areaResult?.IsAreaValid == true) confidenceScores.Add(0.8);
|
||
if (positionResult?.IsPositionValid == true) confidenceScores.Add(0.9);
|
||
if (stabilityResult?.IsStable == true) confidenceScores.Add(0.85);
|
||
|
||
var placementConfidence = confidenceScores.Any() ? confidenceScores.Average() : 0.0;
|
||
var isPlaced = placementConfidence >= 0.7; // 默认阈值
|
||
|
||
return new PartJudgmentResult
|
||
{
|
||
Part = part,
|
||
IsPlaced = isPlaced,
|
||
PlacementConfidence = placementConfidence,
|
||
AreaResult = areaResult,
|
||
PositionResult = positionResult,
|
||
StabilityResult = stabilityResult,
|
||
JudgmentQuality = DetermineJudgmentQuality(placementConfidence),
|
||
Details = isPlaced ? "部件到位" : "部件未到位",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算稳定性指标。
|
||
/// </summary>
|
||
private async Task<StabilityMetrics> CalculateStabilityMetricsAsync(IReadOnlyList<InferenceResultDto> frameHistory, CancellationToken cancellationToken = default)
|
||
{
|
||
await Task.Delay(1, cancellationToken);
|
||
|
||
// 简化的稳定性计算
|
||
var stableFrameCount = 0;
|
||
var positionChanges = new List<double>();
|
||
var areaChanges = new List<double>();
|
||
var confidenceChanges = new List<double>();
|
||
|
||
for (int i = 1; i < frameHistory.Count; i++)
|
||
{
|
||
var prevFrame = frameHistory[i - 1];
|
||
var currFrame = frameHistory[i];
|
||
|
||
// 简化处理:假设每帧都有相同的检测结果
|
||
if (prevFrame.Detections.Any() && currFrame.Detections.Any())
|
||
{
|
||
var prevDetection = prevFrame.Detections.First();
|
||
var currDetection = currFrame.Detections.First();
|
||
|
||
var currCenterX = currDetection.X + currDetection.Width / 2.0;
|
||
var currCenterY = currDetection.Y + currDetection.Height / 2.0;
|
||
var prevCenterX = prevDetection.X + prevDetection.Width / 2.0;
|
||
var prevCenterY = prevDetection.Y + prevDetection.Height / 2.0;
|
||
|
||
var positionChange = Math.Sqrt(
|
||
Math.Pow(currCenterX - prevCenterX, 2) +
|
||
Math.Pow(currCenterY - prevCenterY, 2)
|
||
);
|
||
|
||
var currArea = currDetection.Width * currDetection.Height;
|
||
var prevArea = prevDetection.Width * prevDetection.Height;
|
||
var areaChange = prevArea > 0 ? Math.Abs(currArea - prevArea) / prevArea : 0.0;
|
||
var confidenceChange = Math.Abs(currDetection.Confidence - prevDetection.Confidence);
|
||
|
||
positionChanges.Add(positionChange);
|
||
areaChanges.Add(areaChange);
|
||
confidenceChanges.Add(confidenceChange);
|
||
|
||
// 简化的稳定性判定
|
||
if (positionChange < 5.0 && areaChange < 0.1 && confidenceChange < 0.1)
|
||
{
|
||
stableFrameCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
var positionChangeRate = positionChanges.Any() ? positionChanges.Average() : 0.0;
|
||
var areaChangeRate = areaChanges.Any() ? areaChanges.Average() : 0.0;
|
||
var confidenceChangeRate = confidenceChanges.Any() ? confidenceChanges.Average() : 0.0;
|
||
|
||
// 计算稳定性分数
|
||
var stabilityScore = 1.0 - (positionChangeRate / 10.0 + areaChangeRate + confidenceChangeRate) / 3.0;
|
||
stabilityScore = Math.Max(0.0, Math.Min(1.0, stabilityScore));
|
||
|
||
return new StabilityMetrics
|
||
{
|
||
StabilityScore = stabilityScore,
|
||
StableFrameCount = stableFrameCount,
|
||
PositionChangeRate = positionChangeRate,
|
||
AreaChangeRate = areaChangeRate,
|
||
ConfidenceChangeRate = confidenceChangeRate
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据规则判定稳定性。
|
||
/// </summary>
|
||
private bool IsStableAccordingToRule(StabilityMetrics metrics, StabilityRule rule)
|
||
{
|
||
return metrics.StabilityScore >= rule.StabilityThreshold &&
|
||
metrics.PositionChangeRate <= rule.PositionChangeThreshold &&
|
||
metrics.AreaChangeRate <= rule.AreaChangeThreshold &&
|
||
metrics.ConfidenceChangeRate <= rule.ConfidenceChangeThreshold;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用复合判定策略。
|
||
/// </summary>
|
||
private async Task<CompositeJudgmentResult> ApplyCompositeStrategyAsync(AreaJudgmentResult areaResult, PositionDeviationJudgmentResult positionResult, StabilityJudgmentResult stabilityResult, IReadOnlyList<CompositeJudgmentRule> compositeRules, CancellationToken cancellationToken = default)
|
||
{
|
||
await Task.Delay(1, cancellationToken);
|
||
|
||
var rule = compositeRules.FirstOrDefault() ?? new CompositeJudgmentRule
|
||
{
|
||
Strategy = CoreCompositeJudgmentStrategy.WeightedAverage,
|
||
Weights = new JudgmentWeights { AreaWeight = 0.3, PositionWeight = 0.4, StabilityWeight = 0.3 },
|
||
MinimumPassConditions = 2
|
||
};
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
var weightedScore = 0.0;
|
||
var isCompositeValid = false;
|
||
|
||
switch (rule.Strategy)
|
||
{
|
||
case CoreCompositeJudgmentStrategy.AllPass:
|
||
isCompositeValid = areaResult.IsAreaValid && positionResult.IsPositionValid && stabilityResult.IsStable;
|
||
weightedScore = isCompositeValid ? 1.0 : 0.0;
|
||
break;
|
||
|
||
case CoreCompositeJudgmentStrategy.AnyPass:
|
||
isCompositeValid = areaResult.IsAreaValid || positionResult.IsPositionValid || stabilityResult.IsStable;
|
||
weightedScore = isCompositeValid ? 0.8 : 0.2;
|
||
break;
|
||
|
||
case CoreCompositeJudgmentStrategy.WeightedAverage:
|
||
var areaScore = areaResult.IsAreaValid ? 1.0 : 0.0;
|
||
var positionScore = positionResult.IsPositionValid ? 1.0 : 0.0;
|
||
var stabilityScore = stabilityResult.IsStable ? 1.0 : 0.0;
|
||
weightedScore = areaScore * rule.Weights.AreaWeight + positionScore * rule.Weights.PositionWeight + stabilityScore * rule.Weights.StabilityWeight;
|
||
isCompositeValid = weightedScore >= 0.7; // 默认阈值
|
||
break;
|
||
|
||
case CoreCompositeJudgmentStrategy.MinimumPass:
|
||
var passCount = (areaResult.IsAreaValid ? 1 : 0) + (positionResult.IsPositionValid ? 1 : 0) + (stabilityResult.IsStable ? 1 : 0);
|
||
isCompositeValid = passCount >= rule.MinimumPassConditions;
|
||
weightedScore = (double)passCount / 3.0;
|
||
break;
|
||
}
|
||
|
||
return new CompositeJudgmentResult
|
||
{
|
||
IsCompositeValid = isCompositeValid,
|
||
CompositeConfidence = weightedScore,
|
||
AreaResult = areaResult,
|
||
PositionResult = positionResult,
|
||
StabilityResult = stabilityResult,
|
||
WeightedScore = weightedScore,
|
||
JudgmentStrategy = rule.Strategy,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = isCompositeValid
|
||
? $"复合判定通过:策略={rule.Strategy},加权分数={weightedScore:F3}"
|
||
: $"复合判定失败:策略={rule.Strategy},加权分数={weightedScore:F3}",
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_type"] = "composite",
|
||
["strategy"] = rule.Strategy.ToString(),
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判定规则。
|
||
/// </summary>
|
||
private async Task<PlacementJudgmentResult> JudgeRuleAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<InferenceResultDto> frameHistory, PlacementJudgmentRule rule, CancellationToken cancellationToken = default)
|
||
{
|
||
await Task.Delay(1, cancellationToken);
|
||
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
try
|
||
{
|
||
switch (rule.JudgmentType)
|
||
{
|
||
case PlacementJudgmentType.Area:
|
||
var areaResult = await JudgeAreaAsync(parts, CreateDefaultExpectedAreas(parts), rule.AreaTolerance, cancellationToken);
|
||
return new PlacementJudgmentResult
|
||
{
|
||
IsPlaced = areaResult.Data.IsAreaValid,
|
||
OverallConfidence = areaResult.Data.IsAreaValid ? 0.8 : 0.2,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = areaResult.Data.Details,
|
||
ExtendedProperties = areaResult.Data.ExtendedProperties
|
||
};
|
||
|
||
case PlacementJudgmentType.PositionDeviation:
|
||
var positionResult = await JudgePositionDeviationAsync(parts, CreateDefaultExpectedPositions(parts), rule.PositionTolerance, cancellationToken);
|
||
return new PlacementJudgmentResult
|
||
{
|
||
IsPlaced = positionResult.Data.IsPositionValid,
|
||
OverallConfidence = positionResult.Data.IsPositionValid ? 0.9 : 0.1,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = positionResult.Data.Details,
|
||
ExtendedProperties = positionResult.Data.ExtendedProperties
|
||
};
|
||
|
||
case PlacementJudgmentType.Stability:
|
||
var stabilityResult = await JudgeStabilityAsync(frameHistory, CreateDefaultStabilityRules(), cancellationToken);
|
||
return new PlacementJudgmentResult
|
||
{
|
||
IsPlaced = stabilityResult.Data.IsStable,
|
||
OverallConfidence = stabilityResult.Data.StabilityScore,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = stabilityResult.Data.Details,
|
||
ExtendedProperties = stabilityResult.Data.ExtendedProperties
|
||
};
|
||
|
||
case PlacementJudgmentType.Composite:
|
||
var compositeResult = await JudgeCompositeAsync(parts, frameHistory, CreateDefaultCompositeRules(), cancellationToken);
|
||
return new PlacementJudgmentResult
|
||
{
|
||
IsPlaced = compositeResult.Data.IsCompositeValid,
|
||
OverallConfidence = compositeResult.Data.CompositeConfidence,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = compositeResult.Data.Details,
|
||
ExtendedProperties = compositeResult.Data.ExtendedProperties
|
||
};
|
||
|
||
default:
|
||
throw new NotSupportedException($"不支持的判定类型:{rule.JudgmentType}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new PlacementJudgmentResult
|
||
{
|
||
IsPlaced = false,
|
||
OverallConfidence = 0.0,
|
||
JudgmentTimeUtc = startTime,
|
||
Details = ex.Message,
|
||
ExtendedProperties = new Dictionary<string, object>
|
||
{
|
||
["judgment_time_ms"] = (DateTime.UtcNow - startTime).TotalMilliseconds
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确定判定质量。
|
||
/// </summary>
|
||
private JudgmentQuality DetermineJudgmentQuality(double confidence)
|
||
{
|
||
return confidence switch
|
||
{
|
||
>= 0.9 => JudgmentQuality.Excellent,
|
||
>= 0.8 => JudgmentQuality.Good,
|
||
>= 0.7 => JudgmentQuality.Fair,
|
||
_ => JudgmentQuality.Poor
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认期望面积。
|
||
/// </summary>
|
||
private IReadOnlyList<ExpectedArea> CreateDefaultExpectedAreas(IReadOnlyList<MappedPart> parts)
|
||
{
|
||
return parts.Select(part => new ExpectedArea
|
||
{
|
||
AreaId = Guid.NewGuid(),
|
||
ExpectedAreaValue = part.BoundingBox.Area,
|
||
ExpectedPartType = part.PartType,
|
||
ExpectedPartCode = part.PartCode,
|
||
Weight = 1.0
|
||
}).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认期望位置。
|
||
/// </summary>
|
||
private IReadOnlyList<ExpectedPosition> CreateDefaultExpectedPositions(IReadOnlyList<MappedPart> parts)
|
||
{
|
||
return parts.Select(part => new ExpectedPosition
|
||
{
|
||
PositionId = Guid.NewGuid(),
|
||
ExpectedCenterPoint = new Point { X = part.BoundingBox.Center.X, Y = part.BoundingBox.Center.Y },
|
||
ExpectedPartType = part.PartType,
|
||
ExpectedPartCode = part.PartCode,
|
||
Weight = 1.0
|
||
}).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认稳定性规则。
|
||
/// </summary>
|
||
private IReadOnlyList<StabilityRule> CreateDefaultStabilityRules()
|
||
{
|
||
return new List<StabilityRule>
|
||
{
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "默认稳定性规则",
|
||
StabilityThreshold = 0.8,
|
||
WindowSize = 10,
|
||
PositionChangeThreshold = 5.0,
|
||
AreaChangeThreshold = 0.1,
|
||
ConfidenceChangeThreshold = 0.1
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认复合规则。
|
||
/// </summary>
|
||
private IReadOnlyList<CompositeJudgmentRule> CreateDefaultCompositeRules()
|
||
{
|
||
return new List<CompositeJudgmentRule>
|
||
{
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "默认复合规则",
|
||
Strategy = CoreCompositeJudgmentStrategy.WeightedAverage,
|
||
Weights = new JudgmentWeights { AreaWeight = 0.3, PositionWeight = 0.4, StabilityWeight = 0.3 },
|
||
MinimumPassConditions = 2
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加判定历史。
|
||
/// </summary>
|
||
private async Task AddJudgmentHistoryAsync(PlacementJudgmentResult judgmentResult, CancellationToken cancellationToken = default)
|
||
{
|
||
await Task.Run(() =>
|
||
{
|
||
_judgmentHistory.TryAdd(Guid.NewGuid(), judgmentResult);
|
||
|
||
// 保持历史记录数量在合理范围内
|
||
if (_judgmentHistory.Count > _options.MaxPlacementJudgmentHistoryCount)
|
||
{
|
||
var oldestRecords = _judgmentHistory.Values
|
||
.OrderBy(r => r.JudgmentTimeUtc)
|
||
.Take(_judgmentHistory.Count - _options.MaxPlacementJudgmentHistoryCount)
|
||
.Select(r => r.JudgmentTimeUtc)
|
||
.ToList();
|
||
|
||
foreach (var time in oldestRecords)
|
||
{
|
||
var toRemove = _judgmentHistory.FirstOrDefault(kvp => kvp.Value.JudgmentTimeUtc == time);
|
||
if (toRemove.Key != Guid.Empty)
|
||
{
|
||
_judgmentHistory.TryRemove(toRemove.Key, out _);
|
||
}
|
||
}
|
||
}
|
||
}, cancellationToken);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确定判定质量。
|
||
/// </summary>
|
||
private JudgmentQuality DetermineJudgmentQuality(double confidence)
|
||
{
|
||
return confidence switch
|
||
{
|
||
>= 0.9 => JudgmentQuality.Excellent,
|
||
>= 0.8 => JudgmentQuality.Good,
|
||
>= 0.7 => JudgmentQuality.Fair,
|
||
_ => JudgmentQuality.Poor
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认期望面积。
|
||
/// </summary>
|
||
private IReadOnlyList<ExpectedArea> CreateDefaultExpectedAreas(IReadOnlyList<MappedPart> parts)
|
||
{
|
||
return parts.Select(part => new ExpectedArea
|
||
{
|
||
AreaId = Guid.NewGuid(),
|
||
ExpectedAreaValue = part.BoundingBox.Area,
|
||
ExpectedPartType = part.PartType,
|
||
ExpectedPartCode = part.PartCode,
|
||
Weight = 1.0
|
||
}).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认期望位置。
|
||
/// </summary>
|
||
private IReadOnlyList<ExpectedPosition> CreateDefaultExpectedPositions(IReadOnlyList<MappedPart> parts)
|
||
{
|
||
return parts.Select(part => new ExpectedPosition
|
||
{
|
||
PositionId = Guid.NewGuid(),
|
||
ExpectedCenterPoint = new Point { X = part.BoundingBox.Center.X, Y = part.BoundingBox.Center.Y },
|
||
ExpectedPartType = part.PartType,
|
||
ExpectedPartCode = part.PartCode,
|
||
Weight = 1.0
|
||
}).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认稳定性规则。
|
||
/// </summary>
|
||
private IReadOnlyList<StabilityRule> CreateDefaultStabilityRules()
|
||
{
|
||
return new List<StabilityRule>
|
||
{
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "默认稳定性规则",
|
||
StabilityThreshold = 0.8,
|
||
WindowSize = 10,
|
||
PositionChangeThreshold = 5.0,
|
||
AreaChangeThreshold = 0.1,
|
||
ConfidenceChangeThreshold = 0.1
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认复合规则。
|
||
/// </summary>
|
||
private IReadOnlyList<CompositeJudgmentRule> CreateDefaultCompositeRules()
|
||
{
|
||
return new List<CompositeJudgmentRule>
|
||
{
|
||
new()
|
||
{
|
||
RuleId = Guid.NewGuid(),
|
||
RuleName = "默认复合规则",
|
||
Strategy = CoreCompositeJudgmentStrategy.WeightedAverage,
|
||
Weights = new JudgmentWeights { AreaWeight = 0.3, PositionWeight = 0.4, StabilityWeight = 0.3 },
|
||
MinimumPassConditions = 2
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加判定历史。
|
||
/// </summary>
|
||
private async Task AddJudgmentHistoryAsync(PlacementJudgmentResult judgmentResult, CancellationToken cancellationToken = default)
|
||
{
|
||
await Task.Run(() =>
|
||
{
|
||
_judgmentHistory.TryAdd(Guid.NewGuid(), judgmentResult);
|
||
|
||
// 保持历史记录数量在合理范围内
|
||
if (_judgmentHistory.Count > _options.MaxPlacementJudgmentHistoryCount)
|
||
{
|
||
var oldestRecords = _judgmentHistory.Values
|
||
.OrderBy(r => r.JudgmentTimeUtc)
|
||
.Take(_judgmentHistory.Count - _options.MaxPlacementJudgmentHistoryCount)
|
||
.Select(r => r.JudgmentTimeUtc)
|
||
.ToList();
|
||
|
||
foreach (var time in oldestRecords)
|
||
{
|
||
var toRemove = _judgmentHistory.FirstOrDefault(kvp => kvp.Value.JudgmentTimeUtc == time);
|
||
if (toRemove.Key != Guid.Empty)
|
||
{
|
||
_judgmentHistory.TryRemove(toRemove.Key, out _);
|
||
}
|
||
}
|
||
}
|
||
}, cancellationToken);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 稳定性指标。
|
||
/// </summary>
|
||
private sealed class StabilityMetrics
|
||
{
|
||
public double StabilityScore { get; init; }
|
||
public int StableFrameCount { get; init; }
|
||
public double PositionChangeRate { get; init; }
|
||
public double AreaChangeRate { get; init; }
|
||
public double ConfidenceChangeRate { get; init; }
|
||
}
|
||
|
||
#endif
|
||
|
||
namespace OrpaonVision.SiteApp.Runtime.Services;
|
||
|
||
public sealed class PlacementJudgmentService : IPlacementJudgmentService
|
||
{
|
||
private readonly ILogger<PlacementJudgmentService> _logger;
|
||
|
||
public PlacementJudgmentService(ILogger<PlacementJudgmentService> logger, IOptions<RuntimeOptions> options)
|
||
{
|
||
_logger = logger;
|
||
}
|
||
|
||
public Task<Result<PlacementJudgmentResult>> JudgePlacementAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<PlacementJudgmentRule> judgmentRules, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<PlacementJudgmentResult>.Success(new PlacementJudgmentResult()));
|
||
}
|
||
|
||
public Task<Result<AreaJudgmentResult>> JudgeAreaAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<ExpectedArea> expectedAreas, double tolerance, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<AreaJudgmentResult>.Success(new AreaJudgmentResult()));
|
||
}
|
||
|
||
public Task<Result<PositionDeviationJudgmentResult>> JudgePositionDeviationAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<ExpectedPosition> expectedPositions, double tolerance, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<PositionDeviationJudgmentResult>.Success(new PositionDeviationJudgmentResult()));
|
||
}
|
||
|
||
public Task<Result<StabilityJudgmentResult>> JudgeStabilityAsync(IReadOnlyList<InferenceResultDto> frameHistory, IReadOnlyList<StabilityRule> stabilityRules, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<StabilityJudgmentResult>.Success(new StabilityJudgmentResult()));
|
||
}
|
||
|
||
public Task<Result<CompositeJudgmentResult>> JudgeCompositeAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<InferenceResultDto> frameHistory, IReadOnlyList<CompositeJudgmentRule> compositeRules, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<CompositeJudgmentResult>.Success(new CompositeJudgmentResult()));
|
||
}
|
||
|
||
public Task<Result<BatchJudgmentResult>> BatchJudgeAsync(IReadOnlyList<MappedPart> parts, IReadOnlyList<InferenceResultDto> frameHistory, IReadOnlyList<PlacementJudgmentRule> judgmentRules, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<BatchJudgmentResult>.Success(new BatchJudgmentResult()));
|
||
}
|
||
|
||
public Task<Result<PlacementJudgmentStatistics>> GetJudgmentStatisticsAsync(DateTime startTime, DateTime endTime, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<PlacementJudgmentStatistics>.Success(new PlacementJudgmentStatistics()));
|
||
}
|
||
|
||
public Task<Result<PlacementJudgmentRule>> CreateJudgmentRuleAsync(PlacementJudgmentRule rule, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result<PlacementJudgmentRule>.Success(rule));
|
||
}
|
||
|
||
public Task<Result> UpdateJudgmentRuleAsync(PlacementJudgmentRule rule, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result.Success());
|
||
}
|
||
|
||
public Task<Result> DeleteJudgmentRuleAsync(Guid ruleId, CancellationToken cancellationToken = default)
|
||
{
|
||
return Task.FromResult(Result.Success());
|
||
}
|
||
|
||
public Task<Result<IReadOnlyList<PlacementJudgmentRule>>> GetJudgmentRulesAsync(string? productTypeCode = null, int? layerNumber = null, PlacementJudgmentType? judgmentType = null, CancellationToken cancellationToken = default)
|
||
{
|
||
IReadOnlyList<PlacementJudgmentRule> rules = Array.Empty<PlacementJudgmentRule>();
|
||
return Task.FromResult(Result<IReadOnlyList<PlacementJudgmentRule>>.Success(rules));
|
||
}
|
||
}
|