已合并 PR 25: 合并最新图像库,打通加载图像和算子流程图拖拽功能

![image.png](http://cntao-ap-v83/HMQ-Solution/7ff128fd-5cc6-4feb-9529-2a03b2895662/_apis/git/repositories/e2c5485f-4369-4ed9-9fb9-d087ca4e04b6/pullRequests/25/attachments/image.png)
This commit is contained in:
ZHANG Zhengxuan
2026-04-20 13:35:23 +08:00
31 changed files with 939 additions and 197 deletions
+39 -4
View File
@@ -15,6 +15,7 @@
using Emgu.CV;
using Emgu.CV.Structure;
using Emgu.CV.Util;
using System.Globalization;
namespace XP.ImageProcessing.Core;
@@ -164,11 +165,45 @@ public abstract class ImageProcessorBase
/// </summary>
public T GetParameter<T>(string name)
{
if (Parameters.ContainsKey(name))
{
return (T)Convert.ChangeType(Parameters[name].Value, typeof(T))!;
}
if (!Parameters.ContainsKey(name))
throw new ArgumentException($"参数 {name} 不存在");
var parameter = Parameters[name];
try
{
if (parameter.Value is T typedValue)
return typedValue;
if (parameter.Value is string textValue)
{
var normalizedText = NormalizeText(textValue);
if (typeof(T) == typeof(string))
return (T)(object)textValue;
if (typeof(T) == typeof(int) && int.TryParse(normalizedText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
return (T)(object)intValue;
if (typeof(T) == typeof(double) && double.TryParse(normalizedText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue))
return (T)(object)doubleValue;
if (typeof(T) == typeof(bool) && bool.TryParse(normalizedText, out var boolValue))
return (T)(object)boolValue;
}
return (T)Convert.ChangeType(parameter.Value, typeof(T), CultureInfo.InvariantCulture)!;
}
catch (Exception ex)
{
throw new ArgumentException(
$"参数 {name} 的值 '{parameter.Value}' 无法转换为 {typeof(T).Name}",
ex);
}
}
private static string NormalizeText(string value)
{
return value.Trim().TrimEnd('、', '', ',', '。', '.', ';', '', ':', '');
}
/// <summary>
@@ -70,7 +70,7 @@ public class SuperResolutionProcessor : ImageProcessorBase
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
{
string model = GetParameter<string>("Model");
int scale = int.Parse(GetParameter<string>("Scale"));
int scale = GetParameter<int>("Scale");
// 查找模型文件
string modelPath = FindModelFile(model, scale);
@@ -1,4 +1,8 @@
using Moq;
using Prism.Events;
using System;
using System.IO;
using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services;
@@ -29,7 +33,7 @@ namespace XplorePlane.Tests.Pipeline
}
private PipelineEditorViewModel CreateVm() =>
new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, _mockLogger.Object);
new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object);
// ── 6.1 AddOperatorCommand ────────────────────────────────────
@@ -44,6 +48,18 @@ namespace XplorePlane.Tests.Pipeline
Assert.Equal(0, vm.PipelineNodes[0].Order);
}
[Fact]
public void AddOperator_KnownKey_SetsIconPath()
{
_mockImageSvc.Setup(s => s.GetAvailableProcessors()).Returns(new[] { "ShockFilter" });
var vm = CreateVm();
vm.AddOperatorCommand.Execute("ShockFilter");
Assert.Single(vm.PipelineNodes);
Assert.Equal("⚡", vm.PipelineNodes[0].IconPath);
}
[Fact]
public void AddOperator_InvalidKey_NodeNotAdded()
{
@@ -92,6 +108,36 @@ namespace XplorePlane.Tests.Pipeline
Assert.Equal(i, vm.PipelineNodes[i].Order);
}
[Fact]
public void LoadImageFromFile_SetsSourceImage()
{
var vm = CreateVm();
var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".png");
try
{
var bitmap = TestHelpers.CreateTestBitmap(8, 8);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using (var stream = File.Create(tempPath))
{
encoder.Save(stream);
}
vm.LoadImageFromFile(tempPath);
Assert.NotNull(vm.SourceImage);
Assert.NotNull(vm.PreviewImage);
Assert.Contains(Path.GetFileName(tempPath), vm.StatusMessage);
}
finally
{
if (File.Exists(tempPath))
File.Delete(tempPath);
}
}
// ── 6.2 RemoveOperatorCommand ─────────────────────────────────
[Fact]
@@ -2,6 +2,7 @@ using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using System;
using System.Collections.Generic;
using System.IO;
@@ -31,7 +32,7 @@ namespace XplorePlane.Tests.Pipeline
var mockPersistSvc = new Mock<IPipelinePersistenceService>();
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(mockLogger.Object);
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, mockLogger.Object);
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, new EventAggregator(), mockLogger.Object);
}
/// <summary>
@@ -0,0 +1,28 @@
using Moq;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services;
using Xunit;
namespace XplorePlane.Tests.Services
{
public class ImageProcessingServiceTests
{
[Fact]
public void DiscoverProcessors_LoadsKnownProcessors()
{
var logger = new Mock<ILoggerService>();
logger.Setup(l => l.ForModule<ImageProcessingService>()).Returns(logger.Object);
using var service = new ImageProcessingService(logger.Object);
var processors = service.GetAvailableProcessors();
Assert.Contains("GaussianBlur", processors);
Assert.Contains("ShockFilter", processors);
Assert.Contains("BandPassFilter", processors);
Assert.Contains("Division", processors);
Assert.Contains("Contour", processors);
Assert.True(processors.Count >= 20, $"Expected many discovered processors, got {processors.Count}.");
}
}
}
-1
View File
@@ -268,7 +268,6 @@ namespace XplorePlane
// 注册视图和视图模型
containerRegistry.RegisterForNavigation<MainWindow>();
containerRegistry.RegisterForNavigation<MainWindowB>();
containerRegistry.Register<MainViewModel>();
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
@@ -0,0 +1,22 @@
using Prism.Events;
using System.Windows.Media.Imaging;
namespace XplorePlane.Events
{
public sealed class ManualImageLoadedEvent : PubSubEvent<ManualImageLoadedPayload>
{
}
public sealed class ManualImageLoadedPayload
{
public ManualImageLoadedPayload(BitmapSource image, string filePath)
{
Image = image;
FilePath = filePath;
}
public BitmapSource Image { get; }
public string FilePath { get; }
public string FileName => System.IO.Path.GetFileName(FilePath);
}
}
@@ -0,0 +1,21 @@
using Prism.Events;
using System.Windows.Media.Imaging;
namespace XplorePlane.Events
{
public sealed class PipelinePreviewUpdatedEvent : PubSubEvent<PipelinePreviewUpdatedPayload>
{
}
public sealed class PipelinePreviewUpdatedPayload
{
public PipelinePreviewUpdatedPayload(BitmapSource image, string statusMessage)
{
Image = image;
StatusMessage = statusMessage;
}
public BitmapSource Image { get; }
public string StatusMessage { get; }
}
}
+3
View File
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("XplorePlane.Tests")]
@@ -1,12 +1,15 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces;
using XP.ImageProcessing.Core;
using XP.ImageProcessing.Processors;
namespace XplorePlane.Services
{
@@ -18,40 +21,180 @@ namespace XplorePlane.Services
public ImageProcessingService(ILoggerService logger)
{
_logger = logger?.ForModule<ImageProcessingService>() ?? throw new ArgumentNullException(nameof(logger));
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>();
RegisterBuiltInProcessors();
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>(StringComparer.OrdinalIgnoreCase);
DiscoverProcessors();
}
private void RegisterBuiltInProcessors()
private void DiscoverProcessors()
{
// 8-bit processors
_processorRegistry["GaussianBlur"] = new GaussianBlurProcessor();
_processorRegistry["Threshold"] = new ThresholdProcessor();
_processorRegistry["Division"] = new DivisionProcessor();
_processorRegistry["Contrast"] = new ContrastProcessor();
_processorRegistry["Gamma"] = new GammaProcessor();
_processorRegistry["Morphology"] = new MorphologyProcessor();
_processorRegistry["Contour"] = new ContourProcessor();
_processorRegistry["ShockFilter"] = new ShockFilterProcessor();
_processorRegistry["BandPassFilter"] = new BandPassFilterProcessor();
var assemblies = LoadCandidateAssemblies().ToList();
var discoveredCount = 0;
_logger.Info("Registered {Count} built-in image processors", _processorRegistry.Count);
foreach (var assembly in assemblies)
{
foreach (var processorType in GetProcessorTypes(assembly))
{
if (!TryCreateProcessor(processorType, out var processor))
continue;
var key = GetProcessorKey(processorType);
RegisterProcessorInternal(key, processor, discovered: true);
discoveredCount++;
}
}
public IReadOnlyList<string> GetAvailableProcessors() => new List<string>(_processorRegistry.Keys).AsReadOnly();
_logger.Info(
"Discovered {ProcessorCount} image processors from {AssemblyCount} assemblies",
discoveredCount,
assemblies.Count);
}
private IReadOnlyList<Assembly> LoadCandidateAssemblies()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.ToList();
var loadedNames = new HashSet<string>(
assemblies.Select(a => a.GetName().Name ?? string.Empty),
StringComparer.OrdinalIgnoreCase);
foreach (var path in EnumerateProcessorAssemblyPaths())
{
var assemblyName = Path.GetFileNameWithoutExtension(path);
if (string.IsNullOrWhiteSpace(assemblyName) || loadedNames.Contains(assemblyName))
continue;
try
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(path));
loadedNames.Add(assembly.GetName().Name ?? assemblyName);
assemblies.Add(assembly);
_logger.Debug("Loaded processor assembly from {Path}", path);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to load processor assembly from {Path}", path);
}
}
return assemblies;
}
private static IEnumerable<string> EnumerateProcessorAssemblyPaths()
{
var baseDirectory = AppContext.BaseDirectory;
var candidateDirectories = new[]
{
baseDirectory,
Path.Combine(baseDirectory, "Processors"),
Path.Combine(baseDirectory, "Plugins"),
Path.Combine(baseDirectory, "Libs", "ImageProcessing")
};
var paths = new List<string>();
foreach (var directory in candidateDirectories.Distinct(StringComparer.OrdinalIgnoreCase))
{
if (!Directory.Exists(directory))
continue;
if (string.Equals(directory, baseDirectory, StringComparison.OrdinalIgnoreCase))
{
var processorDll = Path.Combine(directory, "XP.ImageProcessing.Processors.dll");
if (File.Exists(processorDll))
paths.Add(processorDll);
}
else
{
paths.AddRange(Directory.GetFiles(directory, "*.dll", SearchOption.TopDirectoryOnly));
}
}
return paths.Distinct(StringComparer.OrdinalIgnoreCase);
}
private static IEnumerable<Type> GetProcessorTypes(Assembly assembly)
{
try
{
return assembly
.GetTypes()
.Where(type => type is { IsClass: true, IsAbstract: false }
&& typeof(ImageProcessorBase).IsAssignableFrom(type)
&& type.GetConstructor(Type.EmptyTypes) != null);
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types
.Where(type => type != null
&& type.IsClass
&& !type.IsAbstract
&& typeof(ImageProcessorBase).IsAssignableFrom(type)
&& type.GetConstructor(Type.EmptyTypes) != null)!;
}
}
private static string GetProcessorKey(Type processorType)
{
const string suffix = "Processor";
return processorType.Name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)
? processorType.Name[..^suffix.Length]
: processorType.Name;
}
private bool TryCreateProcessor(Type processorType, out ImageProcessorBase processor)
{
try
{
processor = (ImageProcessorBase)Activator.CreateInstance(processorType)!;
return true;
}
catch (Exception ex)
{
processor = null;
_logger.Error(ex, "Failed to create processor instance for {ProcessorType}", processorType.FullName);
return false;
}
}
private void RegisterProcessorInternal(string name, ImageProcessorBase processor, bool discovered)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Processor name cannot be empty", nameof(name));
if (processor == null)
throw new ArgumentNullException(nameof(processor));
_processorRegistry.AddOrUpdate(
name,
processor,
(_, _) => processor);
if (discovered)
_logger.Debug("Discovered processor: {ProcessorName}", name);
else
_logger.Info("Registered processor: {ProcessorName}", name);
}
public IReadOnlyList<string> GetAvailableProcessors()
{
return _processorRegistry.Keys
.OrderBy(key => ProcessorUiMetadata.GetCategoryOrder(ProcessorUiMetadata.Get(key).Category))
.ThenBy(key => GetProcessorDisplayName(key), StringComparer.CurrentCultureIgnoreCase)
.ToList()
.AsReadOnly();
}
public void RegisterProcessor(string name, ImageProcessorBase processor)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Processor name cannot be empty", nameof(name));
if (processor == null) throw new ArgumentNullException(nameof(processor));
_processorRegistry[name] = processor;
_logger.Info("Registered processor: {ProcessorName}", name);
RegisterProcessorInternal(name, processor, discovered: false);
}
public IReadOnlyList<ProcessorParameter> GetProcessorParameters(string processorName)
{
if (_processorRegistry.TryGetValue(processorName, out var processor))
return processor.GetParameters().AsReadOnly();
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
}
@@ -59,13 +202,15 @@ namespace XplorePlane.Services
{
if (_processorRegistry.TryGetValue(processorName, out var processor))
return processor;
throw new ArgumentException($"Processor not registered or is 16-bit only: {processorName}", nameof(processorName));
}
public string GetProcessorDisplayName(string processorName)
{
if (_processorRegistry.TryGetValue(processorName, out var p))
return string.IsNullOrWhiteSpace(p.Name) ? processorName : p.Name;
if (_processorRegistry.TryGetValue(processorName, out var processor))
return string.IsNullOrWhiteSpace(processor.Name) ? processorName : processor.Name;
return processorName;
}
@@ -81,7 +226,6 @@ namespace XplorePlane.Services
if (!_processorRegistry.TryGetValue(processorName, out var processor))
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
// Extract pixels on the UI thread (BitmapSource / FormatConvertedBitmap are DependencyObjects)
var rawPixels = ImageConverter.ExtractGray8Pixels(source, out int imgWidth, out int imgHeight);
return await Task.Run(() =>
@@ -103,7 +247,7 @@ namespace XplorePlane.Services
progress?.Report(0.9);
var result = ImageConverter.ToBitmapSource(processedEmgu);
result.Freeze(); // must freeze before crossing thread boundary
result.Freeze();
progress?.Report(1.0);
return result;
@@ -131,6 +275,7 @@ namespace XplorePlane.Services
if (processor is IDisposable disposable)
disposable.Dispose();
}
_processorRegistry.Clear();
}
}
@@ -0,0 +1,161 @@
using System;
namespace XplorePlane.Services
{
internal static class ProcessorUiMetadata
{
private static readonly (string Category, string CategoryIcon, int Order)[] CategoryDefinitions =
{
("滤波与平滑", "🌀", 0),
("图像增强", "✨", 1),
("图像变换", "🔁", 2),
("数学运算", "➗", 3),
("形态学处理", "⬚", 4),
("边缘检测", "📐", 5),
("检测分析", "🔎", 6),
("其他", "⚙", 99),
};
internal static (string Category, string CategoryIcon, string OperatorIcon) Get(string operatorKey)
{
var category = GetCategory(operatorKey);
return (category, GetCategoryIcon(category), GetOperatorIcon(operatorKey, category));
}
internal static string GetCategory(string operatorKey)
{
if (string.IsNullOrWhiteSpace(operatorKey))
return "其他";
if (ContainsAny(operatorKey, "Blur", "Filter", "Shock"))
return "滤波与平滑";
if (ContainsAny(operatorKey, "Contrast", "Gamma", "Retinex", "Histogram", "Sharpen", "Layer",
"SubPixel", "SuperResolution", "HDR", "Effect", "PseudoColor", "Color"))
return "图像增强";
if (ContainsAny(operatorKey, "Mirror", "Rotate", "Grayscale", "Threshold"))
return "图像变换";
if (ContainsAny(operatorKey, "Division", "Multiplication", "Difference", "Integral", "Or"))
return "数学运算";
if (ContainsAny(operatorKey, "Morphology"))
return "形态学处理";
if (ContainsAny(operatorKey, "Edge"))
return "边缘检测";
if (ContainsAny(operatorKey, "Measurement", "Detection", "Contour", "FillRate", "Void", "Line", "PointToLine", "Ellipse", "Bga"))
return "检测分析";
return "其他";
}
internal static int GetCategoryOrder(string category)
{
foreach (var definition in CategoryDefinitions)
{
if (string.Equals(definition.Category, category, StringComparison.Ordinal))
return definition.Order;
}
return 99;
}
internal static string GetCategoryIcon(string category)
{
foreach (var definition in CategoryDefinitions)
{
if (string.Equals(definition.Category, category, StringComparison.Ordinal))
return definition.CategoryIcon;
}
return "⚙";
}
internal static string GetOperatorIcon(string operatorKey) => GetOperatorIcon(operatorKey, GetCategory(operatorKey));
private static string GetOperatorIcon(string operatorKey, string category)
{
if (string.IsNullOrWhiteSpace(operatorKey))
return GetCategoryIcon(category);
if (ContainsAny(operatorKey, "Shock"))
return "⚡";
if (ContainsAny(operatorKey, "BandPass"))
return "📶";
if (ContainsAny(operatorKey, "GaussianBlur", "MeanFilter", "MedianFilter", "BilateralFilter", "LowPassFilter", "HighPassFilter"))
return "🌀";
if (ContainsAny(operatorKey, "Contrast"))
return "🌗";
if (ContainsAny(operatorKey, "Gamma"))
return "γ";
if (ContainsAny(operatorKey, "Retinex"))
return "🎛";
if (ContainsAny(operatorKey, "Histogram"))
return "📊";
if (ContainsAny(operatorKey, "Sharpen"))
return "✦";
if (ContainsAny(operatorKey, "SubPixel", "SuperResolution"))
return "🔬";
if (ContainsAny(operatorKey, "HDR"))
return "💡";
if (ContainsAny(operatorKey, "PseudoColor"))
return "🎨";
if (ContainsAny(operatorKey, "FilmEffect"))
return "🎞";
if (ContainsAny(operatorKey, "ColorLayer"))
return "🧪";
if (ContainsAny(operatorKey, "Mirror"))
return "↔";
if (ContainsAny(operatorKey, "Rotate"))
return "⟳";
if (ContainsAny(operatorKey, "Grayscale"))
return "◻";
if (ContainsAny(operatorKey, "Threshold"))
return "▣";
if (ContainsAny(operatorKey, "Division"))
return "➗";
if (ContainsAny(operatorKey, "Multiplication"))
return "✕";
if (ContainsAny(operatorKey, "Difference"))
return "Δ";
if (ContainsAny(operatorKey, "Integral"))
return "∫";
if (ContainsAny(operatorKey, "Or"))
return "";
if (ContainsAny(operatorKey, "Morphology"))
return "⬚";
if (ContainsAny(operatorKey, "Sobel", "Kirsch", "HorizontalEdge"))
return "📐";
if (ContainsAny(operatorKey, "Contour"))
return "✏";
if (ContainsAny(operatorKey, "Measurement"))
return "📏";
if (ContainsAny(operatorKey, "FillRate"))
return "🧮";
if (ContainsAny(operatorKey, "Void"))
return "⚪";
if (ContainsAny(operatorKey, "Ellipse"))
return "⭕";
if (ContainsAny(operatorKey, "PointToLine"))
return "📍";
if (ContainsAny(operatorKey, "Edge"))
return "📐";
return GetCategoryIcon(category);
}
private static bool ContainsAny(string value, params string[] terms)
{
foreach (var term in terms)
{
if (value.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0)
return true;
}
return false;
}
}
}
@@ -46,6 +46,19 @@ namespace XplorePlane.Services
cancellationToken.ThrowIfCancellationRequested();
var node = enabledNodes[step];
var invalidParameters = node.Parameters
.Where(p => !p.IsValueValid)
.Select(p => p.DisplayName)
.ToList();
if (invalidParameters.Count > 0)
{
throw new PipelineExecutionException(
$"算子 '{node.DisplayName}' 存在无效参数:{string.Join("", invalidParameters)}",
node.Order,
node.OperatorKey);
}
var parameters = node.Parameters
.Where(p => p.IsValueValid)
.ToDictionary(p => p.Name, p => p.Value);
@@ -37,21 +37,7 @@ namespace XplorePlane.ViewModels
private readonly IImageProcessingService _imageProcessingService;
private string _searchText = string.Empty;
// 算子 Key -> (分类名, 分类图标, 算子图标) 映射
private static readonly Dictionary<string, (string Category, string CatIcon, string OpIcon)> CategoryMap = new()
{
["GaussianBlur"] = ("滤波与平滑", "🔵", "🌀"),
["GaussianBlur16"] = ("滤波与平滑", "🔵", "🌀"),
["BandPassFilter"] = ("滤波与平滑", "🔵", "📶"),
["ShockFilter"] = ("滤波与平滑", "🔵", "⚡"),
["Contrast"] = ("增强与校正", "🟡", "🔆"),
["Gamma"] = ("增强与校正", "🟡", "🌗"),
["FlatFieldCorrection16"] = ("增强与校正", "🟡", "📐"),
["Threshold"] = ("分割与阈值", "🟢", "📊"),
["Division"] = ("分割与阈值", "🟢", "➗"),
["Morphology"] = ("形态学与轮廓", "🔴", "🔲"),
["Contour"] = ("形态学与轮廓", "🔴", "✏️"),
};
// UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致
public OperatorToolboxViewModel(IImageProcessingService imageProcessingService)
{
@@ -82,10 +68,8 @@ namespace XplorePlane.ViewModels
foreach (var key in _imageProcessingService.GetAvailableProcessors())
{
var displayName = _imageProcessingService.GetProcessorDisplayName(key);
var (category, catIcon, opIcon) = CategoryMap.TryGetValue(key, out var info)
? info
: ("其他", "⚙", "⚙");
AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, opIcon, category, catIcon));
var (category, categoryIcon, operatorIcon) = ProcessorUiMetadata.Get(key);
AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, operatorIcon, category, categoryIcon));
}
ApplyFilter();
}
@@ -122,9 +106,12 @@ namespace XplorePlane.ViewModels
private static int GetCategoryOrder(string category) => category switch
{
"滤波与平滑" => 0,
"增强与校正" => 1,
"分割与阈值" => 2,
"形态学与轮廓" => 3,
"图像增强" => 1,
"图像变换" => 2,
"数学运算" => 3,
"形态学处理" => 4,
"边缘检测" => 5,
"检测分析" => 6,
_ => 99
};
}
@@ -1,4 +1,5 @@
using Microsoft.Win32;
using Prism.Events;
using Prism.Commands;
using Prism.Mvvm;
using System;
@@ -9,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services;
@@ -22,6 +24,7 @@ namespace XplorePlane.ViewModels
private readonly IImageProcessingService _imageProcessingService;
private readonly IPipelineExecutionService _executionService;
private readonly IPipelinePersistenceService _persistenceService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private PipelineNodeViewModel _selectedNode;
@@ -40,11 +43,13 @@ namespace XplorePlane.ViewModels
IImageProcessingService imageProcessingService,
IPipelineExecutionService executionService,
IPipelinePersistenceService persistenceService,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
@@ -60,9 +65,13 @@ namespace XplorePlane.ViewModels
SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync());
DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync());
LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync());
LoadImageCommand = new DelegateCommand(LoadImage);
OpenToolboxCommand = new DelegateCommand(OpenToolbox);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Subscribe(OnManualImageLoaded);
}
// ── State Properties ──────────────────────────────────────────
@@ -88,6 +97,7 @@ namespace XplorePlane.ViewModels
if (SetProperty(ref _sourceImage, value))
{
ExecutePipelineCommand.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(DisplayImage));
TriggerDebouncedExecution();
}
}
@@ -96,8 +106,14 @@ namespace XplorePlane.ViewModels
public BitmapSource PreviewImage
{
get => _previewImage;
set => SetProperty(ref _previewImage, value);
set
{
if (SetProperty(ref _previewImage, value))
RaisePropertyChanged(nameof(DisplayImage));
}
}
public BitmapSource DisplayImage => PreviewImage ?? SourceImage;
public string PipelineName
{
@@ -142,6 +158,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand SaveAsPipelineCommand { get; }
public DelegateCommand DeletePipelineCommand { get; }
public DelegateCommand LoadPipelineCommand { get; }
public DelegateCommand LoadImageCommand { get; }
public DelegateCommand OpenToolboxCommand { get; }
@@ -183,7 +200,8 @@ namespace XplorePlane.ViewModels
}
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
var node = new PipelineNodeViewModel(operatorKey, displayName)
var icon = ProcessorUiMetadata.GetOperatorIcon(operatorKey);
var node = new PipelineNodeViewModel(operatorKey, displayName, icon)
{
Order = PipelineNodes.Count
};
@@ -296,6 +314,7 @@ namespace XplorePlane.ViewModels
PreviewImage = result;
StatusMessage = "流水线执行完成";
PublishPipelinePreviewUpdated(result, StatusMessage);
}
catch (OperationCanceledException)
{
@@ -315,6 +334,84 @@ namespace XplorePlane.ViewModels
}
}
private void LoadImage()
{
var dialog = new OpenFileDialog
{
Title = "加载图像",
Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*"
};
if (dialog.ShowDialog() != true)
return;
try
{
LoadImageFromFile(dialog.FileName);
}
catch (Exception ex)
{
StatusMessage = $"加载图像失败:{ex.Message}";
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
}
}
internal void LoadImageFromFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("图像路径不能为空", nameof(filePath));
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
SourceImage = bitmap;
PreviewImage = bitmap;
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}";
PublishManualImageLoaded(bitmap, filePath);
}
internal void LoadImageFromBitmap(BitmapSource bitmap, string filePath, bool runPipeline = true)
{
if (bitmap == null)
throw new ArgumentNullException(nameof(bitmap));
SourceImage = bitmap;
PreviewImage = bitmap;
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}";
PublishManualImageLoaded(bitmap, filePath);
if (runPipeline)
TriggerDebouncedExecution();
}
private void PublishManualImageLoaded(BitmapSource bitmap, string filePath)
{
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Publish(new ManualImageLoadedPayload(bitmap, filePath));
}
private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage)
{
if (bitmap == null) return;
_eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
.Publish(new PipelinePreviewUpdatedPayload(bitmap, statusMessage));
}
private void OnManualImageLoaded(ManualImageLoadedPayload payload)
{
if (payload?.Image == null) return;
if (ReferenceEquals(SourceImage, payload.Image)) return;
SourceImage = payload.Image;
PreviewImage = payload.Image;
StatusMessage = $"已加载图像:{payload.FileName}";
}
private void CancelExecution()
{
_executionCts?.Cancel();
@@ -442,7 +539,8 @@ namespace XplorePlane.ViewModels
{
var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey)
?? nodeModel.OperatorKey;
var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName)
var icon = ProcessorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey);
var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon)
{
Order = nodeModel.Order,
IsEnabled = nodeModel.IsEnabled
@@ -1,5 +1,7 @@
using Prism.Mvvm;
using Prism.Mvvm;
using System;
using System.Globalization;
using System.Linq;
using XP.ImageProcessing.Core;
namespace XplorePlane.ViewModels
@@ -16,7 +18,8 @@ namespace XplorePlane.ViewModels
_value = parameter.Value;
MinValue = parameter.MinValue;
MaxValue = parameter.MaxValue;
ParameterType = parameter.ValueType?.Name?.ToLower() switch
Options = parameter.Options;
ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch
{
"int32" or "int" => "int",
"double" => "double",
@@ -30,6 +33,7 @@ namespace XplorePlane.ViewModels
public string DisplayName { get; }
public object MinValue { get; }
public object MaxValue { get; }
public string[]? Options { get; }
public string ParameterType { get; }
public bool IsValueValid
@@ -43,28 +47,160 @@ namespace XplorePlane.ViewModels
get => _value;
set
{
if (SetProperty(ref _value, value))
ValidateValue(value);
var normalizedValue = NormalizeValue(value);
if (SetProperty(ref _value, normalizedValue))
ValidateValue(normalizedValue);
}
}
private void ValidateValue(object value)
{
if (value == null || MinValue == null || MaxValue == null)
if (value == null)
{
IsValueValid = true;
IsValueValid = false;
return;
}
if (ParameterType == "int")
{
IsValueValid = TryConvertToInt(value, out var intValue) && IsWithinRange(intValue);
return;
}
if (ParameterType == "double")
{
IsValueValid = TryConvertToDouble(value, out var doubleValue) && IsWithinRange(doubleValue);
return;
}
if (ParameterType == "bool")
{
IsValueValid = TryConvertToBool(value, out _);
return;
}
if (Options is { Length: > 0 })
{
var stringValue = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
IsValueValid = Options.Contains(stringValue, StringComparer.OrdinalIgnoreCase);
return;
}
IsValueValid = true;
}
private object NormalizeValue(object value)
{
if (value == null)
return value;
if (ParameterType == "int" && TryConvertToInt(value, out var intValue))
return intValue;
if (ParameterType == "double" && TryConvertToDouble(value, out var doubleValue))
return doubleValue;
if (ParameterType == "bool" && TryConvertToBool(value, out var boolValue))
return boolValue;
return value;
}
private bool IsWithinRange(double value)
{
if (MinValue != null && TryConvertToDouble(MinValue, out var minValue) && value < minValue)
return false;
if (MaxValue != null && TryConvertToDouble(MaxValue, out var maxValue) && value > maxValue)
return false;
return true;
}
private static string NormalizeNumericText(string value)
{
return value.Trim().TrimEnd('、', '', ',', '。', '.', ';', '', ':', '');
}
private static bool TryConvertToInt(object value, out int result)
{
switch (value)
{
case int intValue:
result = intValue;
return true;
case string stringValue:
stringValue = NormalizeNumericText(stringValue);
return int.TryParse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)
|| int.TryParse(stringValue, NumberStyles.Integer, CultureInfo.CurrentCulture, out result);
default:
try
{
double dVal = Convert.ToDouble(value);
double dMin = Convert.ToDouble(MinValue);
double dMax = Convert.ToDouble(MaxValue);
IsValueValid = dVal >= dMin && dVal <= dMax;
result = Convert.ToInt32(value, CultureInfo.InvariantCulture);
return true;
}
catch
{
IsValueValid = true; // éžæ•°å€¼ç±»åž‹ä¸åšèŒƒå›´æ ¡éª?
result = default;
return false;
}
}
}
private static bool TryConvertToDouble(object value, out double result)
{
switch (value)
{
case double doubleValue:
result = doubleValue;
return true;
case float floatValue:
result = floatValue;
return true;
case int intValue:
result = intValue;
return true;
case long longValue:
result = longValue;
return true;
case string stringValue:
stringValue = NormalizeNumericText(stringValue);
return double.TryParse(stringValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result)
|| double.TryParse(stringValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out result);
default:
try
{
result = Convert.ToDouble(value, CultureInfo.InvariantCulture);
return true;
}
catch
{
result = default;
return false;
}
}
}
private static bool TryConvertToBool(object value, out bool result)
{
switch (value)
{
case bool boolValue:
result = boolValue;
return true;
case string stringValue:
return bool.TryParse(stringValue.Trim(), out result);
default:
try
{
result = Convert.ToBoolean(value, CultureInfo.InvariantCulture);
return true;
}
catch
{
result = default;
return false;
}
}
}
}
+39 -1
View File
@@ -1,11 +1,15 @@
using Prism.Commands;
using Prism.Events;
using Prism.Ioc;
using Prism.Mvvm;
using Microsoft.Win32;
using System;
using System.Collections.ObjectModel;
using System.Configuration;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
using XplorePlane.Events;
using XP.Common.Logging.Interfaces;
using XP.Common.PdfViewer.Interfaces;
using XP.Hardware.MotionControl.Abstractions;
@@ -16,6 +20,7 @@ namespace XplorePlane.ViewModels
{
private readonly ILoggerService _logger;
private readonly IContainerProvider _containerProvider;
private readonly IEventAggregator _eventAggregator;
private string _licenseInfo = "当前时间";
public string LicenseInfo
@@ -36,6 +41,7 @@ namespace XplorePlane.ViewModels
// 窗口打开命令
public DelegateCommand OpenImageProcessingCommand { get; }
public DelegateCommand LoadImageCommand { get; }
public DelegateCommand OpenPipelineEditorCommand { get; }
public DelegateCommand OpenCncEditorCommand { get; }
public DelegateCommand OpenMatrixEditorCommand { get; }
@@ -64,10 +70,11 @@ namespace XplorePlane.ViewModels
private Window _toolboxWindow;
private Window _raySourceConfigWindow;
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider)
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator)
{
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
NavigationTree = new ObservableCollection<object>();
@@ -81,6 +88,7 @@ namespace XplorePlane.ViewModels
// 窗口打开命令
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器"));
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排"));
@@ -260,6 +268,36 @@ namespace XplorePlane.ViewModels
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "射线源配置");
}
private void ExecuteLoadImage()
{
var dialog = new OpenFileDialog
{
Title = "加载图像",
Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*"
};
if (dialog.ShowDialog() != true)
return;
try
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(dialog.FileName, UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName));
}
catch (Exception ex)
{
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
MessageBox.Show($"加载图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExecuteWarmUp()
{
var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机",
@@ -8,6 +8,7 @@ using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XP.Hardware.Detector.Abstractions.Events;
using XplorePlane.Events;
namespace XplorePlane.ViewModels
{
@@ -39,6 +40,10 @@ namespace XplorePlane.ViewModels
eventAggregator.GetEvent<ImageCapturedEvent>()
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
}
private void OnImageCaptured(ImageCapturedEventArgs args)
@@ -75,6 +80,22 @@ namespace XplorePlane.ViewModels
}
}
private void OnManualImageLoaded(ManualImageLoadedPayload payload)
{
if (payload?.Image == null) return;
ImageSource = payload.Image;
ImageInfo = $"手动加载: {payload.FileName}";
}
private void OnPipelinePreviewUpdated(PipelinePreviewUpdatedPayload payload)
{
if (payload?.Image == null) return;
ImageSource = payload.Image;
ImageInfo = payload.StatusMessage;
}
/// <summary>
/// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器)
/// </summary>
@@ -3,7 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="库版本信息"
Width="850"
Width="400"
Height="600"
ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterOwner">
@@ -4,11 +4,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
d:DesignHeight="700"
d:DesignWidth="350"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<UserControl.Resources>
@@ -48,13 +46,14 @@
<Grid.RowDefinitions>
<!-- Row 0: 工具栏 -->
<RowDefinition Height="Auto" />
<!-- Row 1: 流水线节点列表 -->
<RowDefinition Height="3*" />
<!-- Row 2: 分隔线 -->
<!-- Row 2: 流水线节点列表 -->
<RowDefinition Height="2*" MinHeight="180" />
<!-- Row 3: 分隔线 -->
<RowDefinition Height="Auto" />
<!-- Row 3: 参数面板 -->
<!-- Row 4: 参数面板 -->
<RowDefinition Height="2*" MinHeight="80" />
<!-- Row 4: 状态栏 -->
<!-- Row 5: 状态栏 -->
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
@@ -90,6 +89,14 @@
Content="加载"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载流水线" />
<!--
<Button
Width="64"
Command="{Binding LoadImageCommand}"
Content="加载图像"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载输入图像" />
-->
<Button
Command="{Binding ExecutePipelineCommand}"
Content="▶"
@@ -109,6 +116,9 @@
</Grid>
</Border>
<!-- 流水线节点列表(拖拽目标) -->
<ListBox
x:Name="PipelineListBox"
@@ -164,7 +174,7 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="13"
Text="" />
Text="{Binding IconPath}" />
</Border>
<!-- 算子名称 -->
@@ -1,3 +1,4 @@
using System;
using Prism.Ioc;
using System.Windows;
using System.Windows.Controls;
@@ -22,6 +23,18 @@ namespace XplorePlane.Views
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (DataContext is not PipelineEditorViewModel)
{
try
{
DataContext = ContainerLocator.Current?.Resolve<PipelineEditorViewModel>();
}
catch (Exception ex)
{
_logger?.Error(ex, "PipelineEditorViewModel 解析失败");
}
}
_logger?.Info("PipelineEditorView DataContext 类型={Type}",
DataContext?.GetType().Name);
@@ -50,7 +63,7 @@ namespace XplorePlane.Views
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
{
_logger?.Warn("Drop 事件触发但数据中 {Format}", OperatorToolboxView.DragFormat);
_logger?.Warn("Drop 事件触发但数据中没有 {Format}", OperatorToolboxView.DragFormat);
return;
}
@@ -1,19 +1,46 @@
<Window x:Class="XplorePlane.Views.PipelineEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
xmlns:views="clr-namespace:XplorePlane.Views"
Title="流水线编辑器"
Width="700" Height="750"
Width="1200"
Height="750"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False">
<Grid>
<Grid Background="#F3F3F3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="240" MinWidth="200" />
<ColumnDefinition Width="*" MinWidth="400" />
<ColumnDefinition Width="250" MinWidth="250" />
</Grid.ColumnDefinitions>
<views:OperatorToolboxView Grid.Column="0" />
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="#E0E0E0" />
<views:PipelineEditorView Grid.Column="2" />
<Border Grid.Column="0"
Margin="8"
Background="White"
BorderBrush="#D0D0D0"
BorderThickness="1"
CornerRadius="4">
<views:OperatorToolboxView />
</Border>
<Border Grid.Column="1"
Margin="8,8,4,8"
Background="White"
BorderBrush="#D0D0D0"
BorderThickness="1"
CornerRadius="4">
<roi:PolygonRoiCanvas ImageSource="{Binding DisplayImage}"
Background="White" />
</Border>
<Border Grid.Column="2"
Margin="4,8,8,8"
Background="White"
BorderBrush="#D0D0D0"
BorderThickness="1"
CornerRadius="4">
<views:PipelineEditorView />
</Border>
</Grid>
</Window>
@@ -1,4 +1,7 @@
using Prism.Ioc;
using System;
using System.Windows;
using XplorePlane.ViewModels;
namespace XplorePlane.Views
{
@@ -7,6 +10,15 @@ namespace XplorePlane.Views
public PipelineEditorWindow()
{
InitializeComponent();
try
{
DataContext = ContainerLocator.Current?.Resolve<PipelineEditorViewModel>();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
}
}
}
+10 -5
View File
@@ -136,7 +136,11 @@
</StackPanel>
</telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="快捷工具">
<telerik:RadRibbonButton
Command="{Binding LoadImageCommand}"
Size="Large"
SmallImage="/Assets/Icons/open.png"
Text="加载图像" />
<!-- 快捷工具: 上下两列,带文字 -->
<StackPanel>
<telerik:RadRibbonButton
@@ -319,7 +323,7 @@
telerik:ScreenTip.Title="模块"
Size="Medium"
SmallImage="/Assets/Icons/Module.png"
Text="模块" />
Text="检测模块" />
<telerik:RadRibbonButton
telerik:ScreenTip.Title="全部保存"
Size="Medium"
@@ -331,12 +335,12 @@
telerik:ScreenTip.Title="消息"
Size="Medium"
SmallImage="/Assets/Icons/message.png"
Text="消息" />
Text="消息弹窗" />
<telerik:RadRibbonButton
telerik:ScreenTip.Title="等待"
Size="Medium"
SmallImage="/Assets/Icons/wait.png"
Text="等待" />
Text="插入等待" />
</StackPanel>
</telerik:RadRibbonGroup>
@@ -384,7 +388,7 @@
Size="Large"
SmallImage="/Assets/Icons/spiral.png" />
</telerik:RadRibbonGroup>
<!--
<telerik:RadRibbonGroup Header="图像处理">
<telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" />
@@ -395,6 +399,7 @@
SmallImage="/Assets/Icons/workflow.png"
Text="流水线编辑器" />
</telerik:RadRibbonGroup>
-->
</telerik:RadRibbonTab>
<telerik:RadRibbonTab Header="关于">
<telerik:RadRibbonGroup Header="关于">
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="400"
d:DesignWidth="600"
@@ -22,14 +23,10 @@
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
</Border>
<!-- 图像显示区域,支持滚动 -->
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Image Source="{Binding ImageSource}"
Stretch="None"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="NearestNeighbor" />
</ScrollViewer>
<!-- 图像显示区域,支持滚动、缩放和ROI -->
<roi:PolygonRoiCanvas Grid.Row="1"
ImageSource="{Binding ImageSource}"
Background="White" />
<!-- 图像信息栏 -->
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
+4 -2
View File
@@ -14,6 +14,7 @@
<ItemGroup>
<Page Remove="MainWindow.xaml" />
<Page Remove="Views\Main\MainWindowB.xaml" />
</ItemGroup>
<!-- NuGet 包引用 -->
@@ -144,6 +145,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
</None>
<Compile Remove="Views\Main\MainWindowB.xaml.cs" />
<Resource Include="XplorerPlane.ico" />
@@ -164,7 +166,7 @@
<ProjectReference Include="..\XP.Camera\XP.Camera.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="xcopy $(SolutionDir)ExternalLibraries\*.* $(TargetDir) /d /y" />
<Exec Command="xcopy $(SolutionDir)ExternalLibraries\Models $(TargetDir)Models\ /d /y /i" />
<Exec Command="xcopy &quot;$(MSBuildProjectDirectory)\..\ExternalLibraries\*.*&quot; &quot;$(TargetDir)&quot; /d /y" />
<Exec Command="xcopy &quot;$(MSBuildProjectDirectory)\..\ExternalLibraries\Models&quot; &quot;$(TargetDir)Models\&quot; /d /y /i" />
</Target>
</Project>
-78
View File
@@ -1,78 +0,0 @@
---------------------------------------------------------------
__ __ _ _____ _
\ \ / / | | | __ \| |
\ V / _ __ | | ___ _ __ ___| |__) | | __ _ _ __ ___
> < | '_ \| |/ _ \| '__/ _ \ ___/| |/ _` | '_ \ / _ \
/ . \| |_) | | (_) | | | __/ | | | (_| | | | | __/
/_/ \_\ .__/|_|\___/|_| \___|_| |_|\__,_|_| |_|\___|
| |
|_|
---------------------------------------------------------------
2026.3.14
----------------------
1、主页面的布局与拆分 √
2、硬件层射线源的集成 √
3、图像层集成,包括复刻一个示例界面,优化界面布局及算子中文 √
4、浮动图像处理工具箱调研 √
5、修复图像工具箱拖拽事件,流水线列表没有生成对应的控件 √
2026.3.16
----------------------
1、优化图像处理窗体的页面布局,简洁清晰 √
2、新增打开图像工具箱(修复DataContext问题) √
3、对主界面B方案进行优化 √
2026.3.17
----------------------
1、对界面设计进行优化 ,增加了扫描模式,移除了探测器设置,增加底部工具栏 √
2026.3.18
----------------------
1、全局数据结构的考虑与设计(多个窗体可以调用公共的数据,如射线源状态,探测器状态,运动位置,图像等) √
2、将计划窗体默认隐藏,只有CNC状态下展开 √
2026.3.20
----------------------
1、软件主界面设计讨论,暂定初稿,给出效果图设计 √
2、日志该用XP.Common库和多语言的学习 √
2026.3.26
----------------------
1、各窗体间数据流的传递,全局数据结构的设计(包括一个基本的说明文档)√
2、将telerik 升级到 2024.1.408.310;调整界面和主题;引入 硬件层依赖 √
3、图像算子流程文件,保存文件后缀 .imw, image process workflow 缩写 √
4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写
5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式) √
PrismBootstrapper 的执行顺序是:RegisterTypes() → ConfigureModuleCatalog() → InitializeModules() → CreateShell()
2026.3.27
----------------------
CNC及矩阵功能的设计与实现,包含以下功能:
1、CNC功能设计与实现,包含以下功能:
a. CNC状态的定义和管理
b. CNC界面设计与实现
c. CNC相关数据的传递和处理
2、CNC相关的编排工具,如插入节点,插入位置,图像模块,等
TO-DO
----------------------