#0020 浮动图像处理工具箱调研

This commit is contained in:
zhengxuan.zhang
2026-03-14 23:21:19 +08:00
parent e89767d976
commit 7c7d0b6a20
29 changed files with 1311 additions and 4 deletions
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using XplorePlane.ViewModels;
namespace XplorePlane.Services
{
public record PipelineProgress(int CurrentStep, int TotalSteps, string CurrentOperator);
public interface IPipelineExecutionService
{
Task<BitmapSource> ExecutePipelineAsync(
IEnumerable<PipelineNodeViewModel> nodes,
BitmapSource source,
IProgress<PipelineProgress> progress = null,
CancellationToken cancellationToken = default);
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services
{
public interface IPipelinePersistenceService
{
Task SaveAsync(PipelineModel pipeline, string filePath);
Task<PipelineModel> LoadAsync(string filePath);
Task<IReadOnlyList<PipelineModel>> LoadAllAsync(string directory);
}
}
@@ -0,0 +1,24 @@
using System;
namespace XplorePlane.Services
{
public class PipelineExecutionException : Exception
{
public int FailedNodeOrder { get; }
public string FailedOperatorKey { get; }
public PipelineExecutionException(string message, int failedNodeOrder, string failedOperatorKey)
: base(message)
{
FailedNodeOrder = failedNodeOrder;
FailedOperatorKey = failedOperatorKey;
}
public PipelineExecutionException(string message, int failedNodeOrder, string failedOperatorKey, Exception innerException)
: base(message, innerException)
{
FailedNodeOrder = failedNodeOrder;
FailedOperatorKey = failedOperatorKey;
}
}
}
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using XplorePlane.ViewModels;
namespace XplorePlane.Services
{
public class PipelineExecutionService : IPipelineExecutionService
{
private const int PreviewMaxHeight = 1080;
private const int UhdThreshold = 3840;
private readonly IImageProcessingService _imageProcessingService;
public PipelineExecutionService(IImageProcessingService imageProcessingService)
{
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
}
public async Task<BitmapSource> ExecutePipelineAsync(
IEnumerable<PipelineNodeViewModel> nodes,
BitmapSource source,
IProgress<PipelineProgress> progress = null,
CancellationToken cancellationToken = default)
{
if (source == null) throw new ArgumentNullException(nameof(source));
var enabledNodes = (nodes ?? Enumerable.Empty<PipelineNodeViewModel>())
.Where(n => n.IsEnabled)
.OrderBy(n => n.Order)
.ToList();
if (enabledNodes.Count == 0)
return source;
// 大图像预览缩放
var current = ScaleForPreview(source);
int total = enabledNodes.Count;
for (int step = 0; step < total; step++)
{
cancellationToken.ThrowIfCancellationRequested();
var node = enabledNodes[step];
var parameters = node.Parameters
.Where(p => p.IsValueValid)
.ToDictionary(p => p.Name, p => p.Value);
try
{
current = await _imageProcessingService.ProcessImageAsync(
current, node.OperatorKey, parameters, null, cancellationToken);
if (current == null)
throw new PipelineExecutionException(
$"算子 '{node.OperatorKey}' 返回了空图像",
node.Order, node.OperatorKey);
}
catch (OperationCanceledException)
{
throw;
}
catch (PipelineExecutionException)
{
throw;
}
catch (Exception ex)
{
throw new PipelineExecutionException(
$"算子 '{node.DisplayName}' 执行失败:{ex.Message}",
node.Order, node.OperatorKey, ex);
}
progress?.Report(new PipelineProgress(step + 1, total, node.DisplayName));
}
if (!current.IsFrozen)
current.Freeze();
return current;
}
private static BitmapSource ScaleForPreview(BitmapSource source)
{
if (source.PixelWidth <= UhdThreshold && source.PixelHeight <= UhdThreshold)
return source;
double scale = (double)PreviewMaxHeight / source.PixelHeight;
if (source.PixelWidth * scale > UhdThreshold)
scale = (double)UhdThreshold / source.PixelWidth;
var scaled = new TransformedBitmap(source, new ScaleTransform(scale, scale));
scaled.Freeze();
return scaled;
}
}
}
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services
{
public class PipelinePersistenceService : IPipelinePersistenceService
{
private readonly IImageProcessingService _imageProcessingService;
private readonly string _baseDirectory;
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
public PipelinePersistenceService(IImageProcessingService imageProcessingService, string baseDirectory = null)
{
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
_baseDirectory = baseDirectory ?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"XplorePlane", "Pipelines");
}
public async Task SaveAsync(PipelineModel pipeline, string filePath)
{
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
ValidatePath(filePath);
pipeline.UpdatedAt = DateTime.UtcNow;
var json = JsonSerializer.Serialize(pipeline, JsonOptions);
await File.WriteAllTextAsync(filePath, json);
}
public async Task<PipelineModel> LoadAsync(string filePath)
{
ValidatePath(filePath);
if (!File.Exists(filePath))
throw new FileNotFoundException("流水线文件不存在", filePath);
var json = await File.ReadAllTextAsync(filePath);
var model = JsonSerializer.Deserialize<PipelineModel>(json, JsonOptions)
?? throw new InvalidDataException("流水线文件格式无效");
// 白名单验证:过滤未注册的算子键
var available = _imageProcessingService.GetAvailableProcessors().ToHashSet(StringComparer.OrdinalIgnoreCase);
model.Nodes = model.Nodes
.Where(n => available.Contains(n.OperatorKey))
.OrderBy(n => n.Order)
.ToList();
// 重新编号确保 Order 连续
for (int i = 0; i < model.Nodes.Count; i++)
model.Nodes[i].Order = i;
return model;
}
public async Task<IReadOnlyList<PipelineModel>> LoadAllAsync(string directory)
{
if (string.IsNullOrWhiteSpace(directory))
directory = _baseDirectory;
ValidateDirectory(directory);
if (!Directory.Exists(directory))
return Array.Empty<PipelineModel>();
var files = Directory.GetFiles(directory, "*.pipeline.json");
var results = new List<PipelineModel>();
foreach (var file in files)
{
try
{
var model = await LoadAsync(file);
results.Add(model);
}
catch
{
// 跳过无效文件
}
}
return results;
}
private void ValidatePath(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("文件路径不能为空", nameof(filePath));
if (filePath.Contains(".."))
throw new UnauthorizedAccessException($"不允许路径遍历:{filePath}");
}
private void ValidateDirectory(string directory)
{
if (directory.Contains(".."))
throw new UnauthorizedAccessException($"不允许路径遍历:{directory}");
}
}
}