修复标记属性图像匹配和显示

This commit is contained in:
zhengxuan.zhang
2026-08-12 10:57:01 +08:00
parent ab5c606cd5
commit 85169cbaff
@@ -3,25 +3,30 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
using System.Windows.Media.Imaging;
using Prism.Ioc;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Prism.Ioc;
using Serilog;
using XplorePlane.Models;
using XplorePlane.Services.Inspection.Documentation;
namespace XplorePlane.Views.Inspection.Documentation
{
public partial class MarkPropertiesWindow : Window
{
private readonly InspectionMarkRecord _mark;
public partial class MarkPropertiesWindow : Window
{
private static readonly ILogger Log = Serilog.Log.ForContext<MarkPropertiesWindow>();
private readonly InspectionMarkRecord _mark;
private readonly InspectionMapDocument? _map;
public MarkPropertiesWindow(InspectionMarkRecord mark, InspectionMapDocument? map = null)
{
_mark = mark ?? throw new ArgumentNullException(nameof(mark));
_map = map;
InitializeComponent();
PopulateFields();
LoadPreviewImage();
InitializeComponent();
Log.Information("Mark properties opened: markId={MarkId}, type={Type}, cncRunId={CncRunId}, cncNodeId={CncNodeId}, linkedImage={LinkedImage}, snapshotImage={SnapshotImage}, mapId={MapId}",
_mark.MarkId, _mark.Type, _mark.CncRunId, _mark.CncNodeId, _mark.LinkedImageRelativePath, _mark.SnapshotImageRelativePath, _map?.MapId);
PopulateFields();
LoadPreviewImage();
}
private void OnSaveClick(object sender, RoutedEventArgs e)
@@ -85,12 +90,18 @@ namespace XplorePlane.Views.Inspection.Documentation
{
var result = FindCncResultImagePath();
if (!string.IsNullOrWhiteSpace(result))
{
Log.Information("CNC mark image selected: nodeId={NodeId}, resultImage={ResultImage}", _mark.CncNodeId, result);
return result;
}
}
return !string.IsNullOrWhiteSpace(_mark.LinkedImageRelativePath)
var fallback = !string.IsNullOrWhiteSpace(_mark.LinkedImageRelativePath)
? _mark.LinkedImageRelativePath
: _mark.SnapshotImageRelativePath;
Log.Warning("CNC result image not found; using mark fallback: nodeId={NodeId}, linkedImage={LinkedImage}, snapshotImage={SnapshotImage}, fallback={Fallback}",
_mark.CncNodeId, _mark.LinkedImageRelativePath, _mark.SnapshotImageRelativePath, fallback);
return fallback;
}
private string FindCncResultImagePath()
@@ -99,21 +110,59 @@ namespace XplorePlane.Views.Inspection.Documentation
{
var snapshot = ResolveImagePath(_mark.SnapshotImageRelativePath);
var dataRoot = snapshot;
if (string.IsNullOrWhiteSpace(dataRoot) && _map != null)
{
var store = AppBootstrapper.Instance?.Container.Resolve<IInspectionMapStore>();
dataRoot = store?.GetRunRootDirectory(_map);
Log.Debug("Snapshot path is empty; using map run root to locate CNC results: runRoot={RunRoot}", dataRoot);
}
while (!string.IsNullOrWhiteSpace(dataRoot) &&
!string.Equals(new DirectoryInfo(dataRoot).Name, "XPData", StringComparison.OrdinalIgnoreCase))
dataRoot = Directory.GetParent(dataRoot)?.FullName;
if (string.IsNullOrWhiteSpace(dataRoot) || !Directory.Exists(dataRoot))
{
Log.Warning("Cannot resolve XPData root for CNC mark image: snapshot={Snapshot}, resolvedSnapshot={ResolvedSnapshot}",
_mark.SnapshotImageRelativePath, snapshot);
return string.Empty;
}
var nodeId = _mark.CncNodeId.Value.ToString("D");
var manifests = Directory.EnumerateFiles(
Path.Combine(dataRoot, "Results"), "manifest.json", SearchOption.AllDirectories);
var resultsRoot = Path.Combine(dataRoot, "Results");
var cncResultsRoot = Path.Combine(resultsRoot, "CNCResults");
var snapshotRunDirectory = GetSnapshotRunDirectory(snapshot);
var roots = _mark.CncRunId.HasValue
? new[] { cncResultsRoot, resultsRoot }
: new[] { snapshotRunDirectory ?? string.Empty };
roots = roots
.Where(Directory.Exists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var manifests = roots
.SelectMany(root => Directory.EnumerateFiles(root, "manifest.json", SearchOption.AllDirectories))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderByDescending(File.GetLastWriteTimeUtc)
.ToArray();
Log.Information("Searching CNC result images: runId={RunId}, nodeId={NodeId}, resolvedSnapshot={ResolvedSnapshot}, snapshotRunDirectory={SnapshotRunDirectory}, dataRoot={DataRoot}, roots={Roots}, manifestCount={ManifestCount}",
_mark.CncRunId, nodeId, snapshot, snapshotRunDirectory, dataRoot, string.Join(";", roots), manifests.Length);
foreach (var manifestPath in manifests.OrderByDescending(File.GetLastWriteTimeUtc))
{
using var document = JsonDocument.Parse(File.ReadAllText(manifestPath));
if (_mark.CncRunId.HasValue &&
document.RootElement.TryGetProperty("Run", out var run) &&
run.TryGetProperty("RunId", out var runId) &&
!string.Equals(runId.GetString(), _mark.CncRunId.Value.ToString("D"), StringComparison.OrdinalIgnoreCase))
{
Log.Debug("Skipping CNC manifest from another run: expectedRunId={ExpectedRunId}, manifestRunId={ManifestRunId}, manifest={ManifestPath}",
_mark.CncRunId, runId.GetString(), manifestPath);
continue;
}
if (!document.RootElement.TryGetProperty("Nodes", out var nodes) ||
nodes.ValueKind != JsonValueKind.Array)
{
Log.Debug("CNC manifest has no Nodes array: manifest={ManifestPath}", manifestPath);
continue;
}
foreach (var node in nodes.EnumerateArray())
{
@@ -126,25 +175,50 @@ namespace XplorePlane.Views.Inspection.Documentation
var relative = imagePath.GetString();
if (string.IsNullOrWhiteSpace(relative))
continue;
var absolute = Path.Combine(dataRoot, relative.Replace('/', Path.DirectorySeparatorChar));
if (File.Exists(absolute))
var normalized = relative.Replace('/', Path.DirectorySeparatorChar);
var manifestDirectory = Path.GetDirectoryName(manifestPath) ?? string.Empty;
var candidates = new[]
{
Path.Combine(cncResultsRoot, normalized),
Path.Combine(resultsRoot, normalized),
Path.Combine(dataRoot, normalized),
Path.Combine(manifestDirectory, normalized)
}.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
var absolute = candidates.FirstOrDefault(File.Exists);
Log.Information("CNC node matched in manifest: nodeId={NodeId}, manifest={ManifestPath}, resultRelative={ResultRelative}, candidates={Candidates}, selected={Selected}",
nodeId, manifestPath, relative, string.Join(";", candidates), absolute ?? "<none>");
if (!string.IsNullOrWhiteSpace(absolute))
return absolute;
}
}
}
catch
catch (Exception ex)
{
// 属性窗口仍可回退到标记快照。
Log.Error(ex, "Failed to find CNC result image: nodeId={NodeId}, snapshotImage={SnapshotImage}",
_mark.CncNodeId, _mark.SnapshotImageRelativePath);
}
return string.Empty;
}
private static string? GetSnapshotRunDirectory(string snapshotPath)
{
if (string.IsNullOrWhiteSpace(snapshotPath) || !File.Exists(snapshotPath))
return null;
var directory = new DirectoryInfo(Path.GetDirectoryName(snapshotPath)!);
// snapshot.bmp -> mark folder -> snapshots -> marks -> CNC run folder
return directory.Parent?.Parent?.Parent?.FullName;
}
private void LoadPreviewImage()
{
var preferred = GetPreferredImagePath();
var path = ResolveImagePath(preferred);
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
Log.Information("Loading mark preview image: markId={MarkId}, preferred={Preferred}, resolved={Resolved}, exists={Exists}",
_mark.MarkId, preferred, path, !string.IsNullOrWhiteSpace(path) && File.Exists(path));
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
PreviewPlaceholder.Visibility = Visibility.Visible;
PreviewImage.Source = null;
@@ -157,19 +231,72 @@ namespace XplorePlane.Views.Inspection.Documentation
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = new Uri(path, UriKind.Absolute);
image.EndInit();
image.Freeze();
PreviewImage.Source = image;
PreviewPlaceholder.Visibility = Visibility.Collapsed;
MarkFileText.Text = path;
}
catch
{
PreviewPlaceholder.Visibility = Visibility.Visible;
image.EndInit();
image.Freeze();
var displayImage = EnhancePreviewContrast(image);
PreviewImage.Source = displayImage;
PreviewPlaceholder.Visibility = Visibility.Collapsed;
MarkFileText.Text = path;
Log.Information("Mark preview image loaded: markId={MarkId}, path={Path}, sourcePixelWidth={PixelWidth}, sourcePixelHeight={PixelHeight}, sourceFormat={Format}, displayFormat={DisplayFormat}",
_mark.MarkId, path, image.PixelWidth, image.PixelHeight, image.Format, displayImage.Format);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to load mark preview image: markId={MarkId}, path={Path}", _mark.MarkId, path);
PreviewPlaceholder.Visibility = Visibility.Visible;
PreviewImage.Source = null;
}
}
}
private static BitmapSource EnhancePreviewContrast(BitmapSource source)
{
var bgra = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
var stride = bgra.PixelWidth * 4;
var pixels = new byte[stride * bgra.PixelHeight];
bgra.CopyPixels(pixels, stride, 0);
var min = 255d;
var max = 0d;
for (var i = 0; i < pixels.Length; i += 4)
{
var b = pixels[i];
var g = pixels[i + 1];
var r = pixels[i + 2];
if (g > r * 1.25 && g > b * 1.25)
continue;
var luminance = 0.114 * b + 0.587 * g + 0.299 * r;
min = Math.Min(min, luminance);
max = Math.Max(max, luminance);
}
if (max - min < 2)
return source;
var scale = 255d / (max - min);
for (var i = 0; i < pixels.Length; i += 4)
{
var b = pixels[i];
var g = pixels[i + 1];
var r = pixels[i + 2];
if (g > r * 1.25 && g > b * 1.25)
continue;
var luminance = 0.114 * b + 0.587 * g + 0.299 * r;
var target = Math.Clamp((luminance - min) * scale, 0, 255);
var ratio = luminance < 1 ? 0 : target / luminance;
pixels[i] = (byte)Math.Clamp(b * ratio, 0, 255);
pixels[i + 1] = (byte)Math.Clamp(g * ratio, 0, 255);
pixels[i + 2] = (byte)Math.Clamp(r * ratio, 0, 255);
}
var result = BitmapSource.Create(bgra.PixelWidth, bgra.PixelHeight, bgra.DpiX, bgra.DpiY,
PixelFormats.Bgra32, null, pixels, stride);
result.Freeze();
Log.Debug("Enhanced mark preview contrast: sourceFormat={SourceFormat}, minLuminance={MinLuminance}, maxLuminance={MaxLuminance}",
source.Format, min, max);
return result;
}
private string ResolveImagePath(string path)
{
@@ -192,7 +319,10 @@ namespace XplorePlane.Views.Inspection.Documentation
var runRoot = store.GetRunRootDirectory(_map);
var runRelative = Path.Combine(runRoot, normalized);
if (File.Exists(runRelative))
{
Log.Debug("Resolved mark image relative to run root: input={Input}, resolved={Resolved}", path, runRelative);
return runRelative;
}
// CNC 结果资产可能保存为相对于 XPData 根目录的 Results/... 路径。
if (normalized.StartsWith("Results" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
@@ -203,14 +333,19 @@ namespace XplorePlane.Views.Inspection.Documentation
dataRoot = Directory.GetParent(dataRoot)?.FullName;
var dataRelative = dataRoot == null ? string.Empty : Path.Combine(dataRoot, normalized);
if (!string.IsNullOrWhiteSpace(dataRelative) && File.Exists(dataRelative))
{
Log.Debug("Resolved mark image relative to XPData root: input={Input}, resolved={Resolved}", path, dataRelative);
return dataRelative;
}
}
Log.Debug("Mark image path did not resolve to an existing file: input={Input}, runCandidate={RunCandidate}", path, runRelative);
return runRelative;
}
catch
{
return path;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to resolve mark image path: input={Input}", path);
return path;
}
}
}