修复标记属性图像匹配和显示
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user