#0020 浮动图像处理工具箱调研
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user