fix(viewport): 修复流水线处理16位图像后直方图退化为0-255的问题

- PipelinePreviewUpdatedPayload 新增 ResultImage16 字段携带16位结果图
- MainViewportService 增加 CurrentHistogramSource16 配对属性,引用配对防止过期数据
- SetManualImage/SetLiveProcessedImage 增加16位伴随图重载
- ViewportPanelViewModel 暴露 HistogramSource16 并在状态同步时更新
- ViewportPanelView 直方图刷新优先使用16位配对源统计全动态范围
This commit is contained in:
wei.lw.li
2026-07-21 09:27:49 +08:00
parent e39ca8c2b3
commit d025c8922b
6 changed files with 94 additions and 10 deletions
+7 -1
View File
@@ -28,16 +28,22 @@ namespace XplorePlane.Events
public sealed class PipelinePreviewUpdatedPayload
{
public PipelinePreviewUpdatedPayload(BitmapSource image, string statusMessage, bool isLivePreview = false)
public PipelinePreviewUpdatedPayload(BitmapSource image, string statusMessage, bool isLivePreview = false, BitmapSource? resultImage16 = null)
{
Image = image;
StatusMessage = statusMessage;
IsLivePreview = isLivePreview;
ResultImage16 = resultImage16;
}
public BitmapSource Image { get; }
public string StatusMessage { get; }
public bool IsLivePreview { get; }
/// <summary>
/// 与显示图配对的 16 位结果图(Gray16),供直方图等需要全动态范围的场景使用;可为 null。
/// </summary>
public BitmapSource? ResultImage16 { get; }
}
/// <summary>
@@ -23,6 +23,13 @@ namespace XplorePlane.Services.Main.Viewport
/// </summary>
ImageSource? LatestResultImage16 { get; set; }
/// <summary>
/// 与当前显示图配对的 16 位直方图数据源(Gray16 BitmapSource)。
/// 仅当当前显示图正是其配对的归一化 8 位图时返回非 null,供直方图控件按全动态范围统计。
/// 显示图切换为原图 / 实时帧等其它来源后自动失配返回 null。
/// </summary>
ImageSource? CurrentHistogramSource16 { get; }
event EventHandler StateChanged;
void SetRealtimeDisplayEnabled(bool isEnabled);
@@ -33,8 +40,19 @@ namespace XplorePlane.Services.Main.Viewport
void SetManualImage(ImageSource image, string filePath);
/// <summary>
/// 设置手动图像,并附带与之配对的 16 位直方图数据源。
/// 用于流水线处理结果:显示图为归一化 8 位图,直方图使用 16 位伴随图统计全动态范围。
/// </summary>
void SetManualImage(ImageSource image, string filePath, ImageSource? histogramSource16);
void SetLiveProcessedImage(ImageSource image, string label);
/// <summary>
/// 设置实时处理结果图像,并附带与之配对的 16 位直方图数据源。
/// </summary>
void SetLiveProcessedImage(ImageSource image, string label, ImageSource? histogramSource16);
void ClearLiveProcessedImage();
/// <summary>
@@ -129,6 +147,11 @@ namespace XplorePlane.Services.Main.Viewport
private string _latestManualInfo = "未加载手动图像";
private ImageSource? _latestResultImage16;
// 与显示图配对的 16 位直方图源。_histogramSource16Owner 记录该 16 位源所对应的 8 位显示图,
// 仅当当前显示图 == owner 时,CurrentHistogramSource16 才返回该 16 位源,从而避免过期数据。
private ImageSource? _histogramSource16;
private ImageSource? _histogramSource16Owner;
public MainViewportService(ILoggerService logger)
{
_logger = logger?.ForModule<MainViewportService>() ?? throw new ArgumentNullException(nameof(logger));
@@ -229,6 +252,20 @@ namespace XplorePlane.Services.Main.Viewport
set { lock (_syncRoot) { _latestResultImage16 = value; } }
}
public ImageSource? CurrentHistogramSource16
{
get
{
lock (_syncRoot)
{
// 仅当配对的 8 位显示图仍是当前显示图时,16 位源才有效
return ReferenceEquals(_histogramSource16Owner, _currentDisplayImage)
? _histogramSource16
: null;
}
}
}
public event EventHandler StateChanged;
public void SetRealtimeDisplayEnabled(bool isEnabled)
@@ -360,6 +397,9 @@ namespace XplorePlane.Services.Main.Viewport
}
public void SetManualImage(ImageSource image, string filePath)
=> SetManualImage(image, filePath, null);
public void SetManualImage(ImageSource image, string filePath, ImageSource? histogramSource16)
{
if (image == null)
return;
@@ -381,13 +421,17 @@ namespace XplorePlane.Services.Main.Viewport
_currentSourceMode = MainViewportSourceMode.ManualImage;
_currentDisplayImage = _latestManualImage;
_currentDisplayInfo = _latestManualInfo;
SetHistogramSource16_NoLock(histogramSource16, _currentDisplayImage);
}
_logger.Info("[图像链路] MainViewportService.SetManualImage:已更新图像 {FileName},触发 StateChanged", fileName);
_logger.Info("[图像链路] MainViewportService.SetManualImage:已更新图像 {FileName}has16bit={Has16bit}触发 StateChanged", fileName, histogramSource16 != null);
RaiseStateChanged();
}
public void SetLiveProcessedImage(ImageSource image, string label)
=> SetLiveProcessedImage(image, label, null);
public void SetLiveProcessedImage(ImageSource image, string label, ImageSource? histogramSource16)
{
if (image == null)
return;
@@ -405,6 +449,12 @@ namespace XplorePlane.Services.Main.Viewport
{
_currentDisplayImage = _latestLiveProcessedImage;
_currentDisplayInfo = _latestLiveProcessedInfo;
SetHistogramSource16_NoLock(histogramSource16, _currentDisplayImage);
}
else
{
// 未启用实时显示时,结果图不上屏,清除配对避免误用
SetHistogramSource16_NoLock(null, null);
}
}
@@ -554,6 +604,16 @@ namespace XplorePlane.Services.Main.Viewport
RaiseStateChanged();
}
/// <summary>
/// 设置与显示图配对的 16 位直方图源。owner 为该 16 位源对应的 8 位显示图;
/// 传入 null 表示清除配对(当前显示图无对应 16 位数据)。
/// </summary>
private void SetHistogramSource16_NoLock(ImageSource? histogramSource16, ImageSource? owner)
{
_histogramSource16 = histogramSource16;
_histogramSource16Owner = histogramSource16 != null ? owner : null;
}
private void ApplyLiveDetectorDisplay_NoLock()
{
if (_isLiveProcessedImageActive && _latestLiveProcessedImage != null)
@@ -567,7 +567,7 @@ namespace XplorePlane.ViewModels.ImageProcessing
_logger.Info("[LivePipelineDiag] ExecutePipelineAsync completed: isLiveExecution={IsLiveExecution}, output={OutputSize}",
isLiveExecution,
DescribeBitmap(execResult.Image));
PublishPipelinePreviewUpdated(execResult.Image, StatusMessage, isLiveExecution);
PublishPipelinePreviewUpdated(execResult.Image, StatusMessage, isLiveExecution, execResult.ResultImage16);
}
catch (OperationCanceledException)
{
@@ -748,7 +748,7 @@ namespace XplorePlane.ViewModels.ImageProcessing
.Publish(new ManualImageLoadedPayload(bitmap, filePath));
}
private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage, bool isLivePreview = false)
private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage, bool isLivePreview = false, BitmapSource resultImage16 = null)
{
if (bitmap == null)
{
@@ -756,12 +756,13 @@ namespace XplorePlane.ViewModels.ImageProcessing
return;
}
_logger.Info("[LivePipelineDiag] PublishPipelinePreviewUpdated: isLivePreview={IsLivePreview}, image={ImageSize}, statusMessage={Msg}",
_logger.Info("[LivePipelineDiag] PublishPipelinePreviewUpdated: isLivePreview={IsLivePreview}, image={ImageSize}, has16bit={Has16bit}, statusMessage={Msg}",
isLivePreview,
DescribeBitmap(bitmap),
resultImage16 != null,
statusMessage);
_eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
.Publish(new PipelinePreviewUpdatedPayload(bitmap, statusMessage, isLivePreview));
.Publish(new PipelinePreviewUpdatedPayload(bitmap, statusMessage, isLivePreview, resultImage16));
}
private bool ShouldLogLiveSourceSkip()
+2 -2
View File
@@ -1873,12 +1873,12 @@ namespace XplorePlane.ViewModels.Main
if (payload.IsLivePreview && _mainViewportService.IsRealtimeDisplayEnabled)
{
_logger.Info("[LivePipelineDiag] Routing preview to live viewport display.");
_mainViewportService.SetLiveProcessedImage(payload.Image, payload.StatusMessage);
_mainViewportService.SetLiveProcessedImage(payload.Image, payload.StatusMessage, payload.ResultImage16);
}
else
{
_logger.Info("[LivePipelineDiag] Routing preview to manual image display.");
_mainViewportService.SetManualImage(payload.Image, string.Empty);
_mainViewportService.SetManualImage(payload.Image, string.Empty, payload.ResultImage16);
}
}
@@ -36,6 +36,7 @@ namespace XplorePlane.ViewModels.Main
private string _infoOverlayText = string.Empty;
private ImageSource _imageSource;
private ImageSource _histogramSource16;
private string _imageInfo = "等待探测器图像...";
private MeasurementToolMode _currentMeasurementMode = MeasurementToolMode.None;
private Point? _measurePoint1;
@@ -104,6 +105,16 @@ namespace XplorePlane.ViewModels.Main
set => SetProperty(ref _imageSource, value);
}
/// <summary>
/// 与当前显示图配对的 16 位直方图数据源(Gray16);无配对时为 null。
/// 直方图控件优先使用它按全动态范围(0-65535)统计。
/// </summary>
public ImageSource HistogramSource16
{
get => _histogramSource16;
private set => SetProperty(ref _histogramSource16, value);
}
public string ImageInfo
{
get => _imageInfo;
@@ -298,6 +309,8 @@ namespace XplorePlane.ViewModels.Main
private void UpdateFromState(bool updateInfo)
{
// 先更新 16 位配对源,再更新显示图;View 监听 ImageSource 变化刷新直方图时可读到最新配对
HistogramSource16 = _mainViewportService.CurrentHistogramSource16;
ImageSource = _mainViewportService.CurrentDisplayImage;
// Task 5.8: Sync IsRealtimeEnabled from service
@@ -1304,6 +1304,7 @@ namespace XplorePlane.Views.Main
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewportPanelViewModel.ImageSource)
|| e.PropertyName == nameof(ViewportPanelViewModel.HistogramSource16)
|| e.PropertyName == nameof(ViewportPanelViewModel.IsHistogramVisible))
{
UpdateHistogramFromCurrentImage();
@@ -1321,7 +1322,10 @@ namespace XplorePlane.Views.Main
if (!vm.IsHistogramVisible)
return;
if (vm.ImageSource is not BitmapSource bitmap)
// 优先使用与显示图配对的 16 位数据源,保证 X 轴按全动态范围(0-65535)统计;
// 无配对时回退到当前显示图(通常为 8 位)。
var histogramImage = vm.HistogramSource16 as BitmapSource ?? vm.ImageSource as BitmapSource;
if (histogramImage == null)
{
HistogramOverlay.Clear();
return;
@@ -1329,7 +1333,7 @@ namespace XplorePlane.Views.Main
try
{
var (pixels, width, height, bitDepth) = CopyHistogramPixels(bitmap);
var (pixels, width, height, bitDepth) = CopyHistogramPixels(histogramImage);
HistogramOverlay.UpdateImage(pixels, width, height, bitDepth);
}
catch (Exception ex)