Merge branch 'Develop/XP' into turbo-002-cnc
This commit is contained in:
@@ -195,7 +195,6 @@ public class BaslerCameraController : ICameraController
|
||||
}
|
||||
|
||||
_camera.ExecuteSoftwareTrigger();
|
||||
_logger.Debug("Software trigger executed.");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
@@ -0,0 +1,8 @@
|
||||
using XP.Camera.Calibration.Resources;
|
||||
|
||||
namespace XP.Camera.Calibration;
|
||||
|
||||
public class CalibrationLocalizedStrings
|
||||
{
|
||||
public CalibrationResources Resources { get; } = new CalibrationResources();
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// ============================================================================
|
||||
// 文件名: CalibrationProcessor.cs
|
||||
// 描述: 标定处理器,实现图像坐标系到世界坐标系的转换
|
||||
// 功能:
|
||||
// - 基于多点标定计算透视变换矩阵(支持4点及以上)
|
||||
// - 像素坐标到世界坐标的转换
|
||||
// - 标定数据的保存和加载(JSON格式)
|
||||
// - 从CSV文件导入标定点数据
|
||||
// 算法: 使用DLT(Direct Linear Transformation)方法求解单应性矩阵
|
||||
// ============================================================================
|
||||
|
||||
using Emgu.CV;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace XP.Camera.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// 九点标定处理器
|
||||
/// </summary>
|
||||
public class CalibrationProcessor
|
||||
{
|
||||
public class CalibrationPoint
|
||||
{
|
||||
public double PixelX { get; set; }
|
||||
public double PixelY { get; set; }
|
||||
public double WorldX { get; set; }
|
||||
public double WorldY { get; set; }
|
||||
}
|
||||
|
||||
public class CalibrationData
|
||||
{
|
||||
public List<CalibrationPoint> Points { get; set; } = new List<CalibrationPoint>();
|
||||
public double[] TransformMatrix { get; set; } = new double[9];
|
||||
public DateTime CalibrationTime { get; set; }
|
||||
}
|
||||
|
||||
private Matrix<double>? _transformMatrix;
|
||||
|
||||
/// <summary>
|
||||
/// 执行九点标定
|
||||
/// </summary>
|
||||
public bool Calibrate(List<CalibrationPoint> points)
|
||||
{
|
||||
if (points.Count < 4)
|
||||
return false;
|
||||
|
||||
int n = points.Count;
|
||||
var A = new Matrix<double>(2 * n, 8);
|
||||
var b = new Matrix<double>(2 * n, 1);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double u = points[i].PixelX;
|
||||
double v = points[i].PixelY;
|
||||
double x = points[i].WorldX;
|
||||
double y = points[i].WorldY;
|
||||
|
||||
A[2 * i, 0] = u;
|
||||
A[2 * i, 1] = v;
|
||||
A[2 * i, 2] = 1;
|
||||
A[2 * i, 3] = 0;
|
||||
A[2 * i, 4] = 0;
|
||||
A[2 * i, 5] = 0;
|
||||
A[2 * i, 6] = -x * u;
|
||||
A[2 * i, 7] = -x * v;
|
||||
b[2 * i, 0] = x;
|
||||
|
||||
A[2 * i + 1, 0] = 0;
|
||||
A[2 * i + 1, 1] = 0;
|
||||
A[2 * i + 1, 2] = 0;
|
||||
A[2 * i + 1, 3] = u;
|
||||
A[2 * i + 1, 4] = v;
|
||||
A[2 * i + 1, 5] = 1;
|
||||
A[2 * i + 1, 6] = -y * u;
|
||||
A[2 * i + 1, 7] = -y * v;
|
||||
b[2 * i + 1, 0] = y;
|
||||
}
|
||||
|
||||
var h = new Matrix<double>(8, 1);
|
||||
CvInvoke.Solve(A, b, h, Emgu.CV.CvEnum.DecompMethod.Svd);
|
||||
|
||||
_transformMatrix = new Matrix<double>(3, 3);
|
||||
_transformMatrix[0, 0] = h[0, 0];
|
||||
_transformMatrix[0, 1] = h[1, 0];
|
||||
_transformMatrix[0, 2] = h[2, 0];
|
||||
_transformMatrix[1, 0] = h[3, 0];
|
||||
_transformMatrix[1, 1] = h[4, 0];
|
||||
_transformMatrix[1, 2] = h[5, 0];
|
||||
_transformMatrix[2, 0] = h[6, 0];
|
||||
_transformMatrix[2, 1] = h[7, 0];
|
||||
_transformMatrix[2, 2] = 1.0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 像素坐标转世界坐标
|
||||
/// </summary>
|
||||
public System.Drawing.PointF PixelToWorld(System.Drawing.PointF pixel)
|
||||
{
|
||||
if (_transformMatrix == null)
|
||||
return pixel;
|
||||
|
||||
double u = pixel.X;
|
||||
double v = pixel.Y;
|
||||
|
||||
double w = _transformMatrix[2, 0] * u + _transformMatrix[2, 1] * v + _transformMatrix[2, 2];
|
||||
double x = (_transformMatrix[0, 0] * u + _transformMatrix[0, 1] * v + _transformMatrix[0, 2]) / w;
|
||||
double y = (_transformMatrix[1, 0] * u + _transformMatrix[1, 1] * v + _transformMatrix[1, 2]) / w;
|
||||
|
||||
return new System.Drawing.PointF((float)x, (float)y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存标定文件
|
||||
/// </summary>
|
||||
public void SaveCalibration(string filePath, List<CalibrationPoint> points)
|
||||
{
|
||||
var data = new CalibrationData
|
||||
{
|
||||
Points = points,
|
||||
TransformMatrix = new double[9],
|
||||
CalibrationTime = DateTime.Now
|
||||
};
|
||||
|
||||
if (_transformMatrix != null)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
data.TransformMatrix[i * 3 + j] = _transformMatrix[i, j];
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载标定文件
|
||||
/// </summary>
|
||||
public bool LoadCalibration(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var data = JsonSerializer.Deserialize<CalibrationData>(json);
|
||||
|
||||
if (data == null || data.TransformMatrix == null || data.TransformMatrix.Length != 9)
|
||||
return false;
|
||||
|
||||
_transformMatrix = new Matrix<double>(3, 3);
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
_transformMatrix[i, j] = data.TransformMatrix[i * 3 + j];
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从CSV文件加载标定点
|
||||
/// </summary>
|
||||
public List<CalibrationPoint> LoadPointsFromCsv(string filePath)
|
||||
{
|
||||
var points = new List<CalibrationPoint>();
|
||||
if (!File.Exists(filePath))
|
||||
return points;
|
||||
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
if (i == 0 && (lines[i].Contains("PixelX") || lines[i].Contains("像素"))) continue;
|
||||
|
||||
var parts = lines[i].Split(',');
|
||||
if (parts.Length >= 4)
|
||||
{
|
||||
if (double.TryParse(parts[0].Trim(), out double px) &&
|
||||
double.TryParse(parts[1].Trim(), out double py) &&
|
||||
double.TryParse(parts[2].Trim(), out double wx) &&
|
||||
double.TryParse(parts[3].Trim(), out double wy))
|
||||
{
|
||||
points.Add(new CalibrationPoint
|
||||
{
|
||||
PixelX = px,
|
||||
PixelY = py,
|
||||
WorldX = wx,
|
||||
WorldY = wy
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// ============================================================================
|
||||
// 文件名: ChessboardCalibrator.cs
|
||||
// 描述: 棋盘格标定器,实现基于棋盘格的相机内参标定
|
||||
// 功能:
|
||||
// - 从多张棋盘格图像中检测角点
|
||||
// - 计算相机内参矩阵和畸变系数
|
||||
// - 图像去畸变处理
|
||||
// - 计算重投影误差评估标定质量
|
||||
// - 标定结果的保存和加载(JSON格式)
|
||||
// 算法: 使用 Zhang's 标定方法进行相机标定
|
||||
// ============================================================================
|
||||
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.CvEnum;
|
||||
using Emgu.CV.Structure;
|
||||
using Emgu.CV.Util;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace XP.Camera.Calibration;
|
||||
|
||||
public class ChessboardCalibrator
|
||||
{
|
||||
public class CalibrationResult
|
||||
{
|
||||
public double[][] CameraMatrix { get; set; } = new double[3][];
|
||||
public double[] DistortionCoeffs { get; set; } = Array.Empty<double>();
|
||||
public double ReprojectionError { get; set; }
|
||||
public DateTime CalibrationTime { get; set; }
|
||||
}
|
||||
|
||||
private Mat? _cameraMatrix;
|
||||
private Mat? _distCoeffs;
|
||||
private double _reprojectionError;
|
||||
private List<double> _perImageErrors = new List<double>();
|
||||
|
||||
public double ReprojectionError => _reprojectionError;
|
||||
public List<double> PerImageErrors => _perImageErrors;
|
||||
|
||||
// 进度报告委托
|
||||
public delegate void ProgressReportHandler(int current, int total, string message);
|
||||
|
||||
public event ProgressReportHandler? ProgressChanged;
|
||||
|
||||
public bool CalibrateFromImages(List<string> imagePaths, int boardWidth, int boardHeight, float squareSize, out string errorMsg)
|
||||
{
|
||||
errorMsg = "";
|
||||
var objectPoints = new VectorOfVectorOfPoint3D32F();
|
||||
var imagePoints = new VectorOfVectorOfPointF();
|
||||
var imageSize = new System.Drawing.Size();
|
||||
|
||||
var objp = new MCvPoint3D32f[boardWidth * boardHeight];
|
||||
for (int i = 0; i < boardHeight; i++)
|
||||
for (int j = 0; j < boardWidth; j++)
|
||||
objp[i * boardWidth + j] = new MCvPoint3D32f(j * squareSize, i * squareSize, 0);
|
||||
|
||||
int validImages = 0;
|
||||
int totalImages = imagePaths.Count;
|
||||
|
||||
// 第一阶段:检测角点
|
||||
for (int idx = 0; idx < totalImages; idx++)
|
||||
{
|
||||
var path = imagePaths[idx];
|
||||
ProgressChanged?.Invoke(idx + 1, totalImages * 2, $"检测角点 ({idx + 1}/{totalImages})");
|
||||
|
||||
var img = CvInvoke.Imread(path, ImreadModes.Grayscale);
|
||||
if (img.IsEmpty) continue;
|
||||
|
||||
imageSize = img.Size;
|
||||
var corners = new VectorOfPointF();
|
||||
|
||||
if (CvInvoke.FindChessboardCorners(img, new System.Drawing.Size(boardWidth, boardHeight), corners))
|
||||
{
|
||||
// 亚像素级角点精化
|
||||
CvInvoke.CornerSubPix(img, corners, new System.Drawing.Size(5, 5), new System.Drawing.Size(-1, -1),
|
||||
new MCvTermCriteria(30, 0.001));
|
||||
|
||||
objectPoints.Push(new VectorOfPoint3D32F(objp));
|
||||
imagePoints.Push(corners);
|
||||
validImages++;
|
||||
}
|
||||
img.Dispose();
|
||||
}
|
||||
|
||||
if (validImages < 3)
|
||||
{
|
||||
errorMsg = $"有效图像不足,需要至少3张,当前{validImages}张";
|
||||
ProgressChanged?.Invoke(0, 100, "标定失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 第二阶段:执行标定
|
||||
ProgressChanged?.Invoke(totalImages, totalImages * 2, "执行相机标定...");
|
||||
|
||||
_cameraMatrix = new Mat();
|
||||
_distCoeffs = new Mat();
|
||||
var rvecs = new VectorOfMat();
|
||||
var tvecs = new VectorOfMat();
|
||||
|
||||
_reprojectionError = CvInvoke.CalibrateCamera(objectPoints, imagePoints, imageSize, _cameraMatrix, _distCoeffs,
|
||||
rvecs, tvecs, CalibType.Default, new MCvTermCriteria(30, 1e-6));
|
||||
|
||||
// 第三阶段:计算每张图像的重投影误差
|
||||
_perImageErrors.Clear();
|
||||
for (int i = 0; i < objectPoints.Size; i++)
|
||||
{
|
||||
ProgressChanged?.Invoke(totalImages + i + 1, totalImages * 2, $"计算重投影误差 ({i + 1}/{objectPoints.Size})");
|
||||
|
||||
var projectedPoints = new VectorOfPointF();
|
||||
CvInvoke.ProjectPoints(objectPoints[i], rvecs[i], tvecs[i], _cameraMatrix, _distCoeffs, projectedPoints);
|
||||
|
||||
double error = 0;
|
||||
for (int j = 0; j < projectedPoints.Size; j++)
|
||||
{
|
||||
var dx = imagePoints[i][j].X - projectedPoints[j].X;
|
||||
var dy = imagePoints[i][j].Y - projectedPoints[j].Y;
|
||||
error += Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
_perImageErrors.Add(error / projectedPoints.Size);
|
||||
}
|
||||
|
||||
ProgressChanged?.Invoke(totalImages * 2, totalImages * 2, "标定完成");
|
||||
return true;
|
||||
}
|
||||
|
||||
public Mat UndistortImage(Mat inputImage)
|
||||
{
|
||||
if (_cameraMatrix == null || _distCoeffs == null)
|
||||
return inputImage;
|
||||
|
||||
var output = new Mat();
|
||||
CvInvoke.Undistort(inputImage, output, _cameraMatrix, _distCoeffs);
|
||||
return output;
|
||||
}
|
||||
|
||||
public Mat? DrawChessboardCorners(string imagePath, int boardWidth, int boardHeight)
|
||||
{
|
||||
var img = CvInvoke.Imread(imagePath);
|
||||
if (img.IsEmpty) return null;
|
||||
|
||||
var gray = new Mat();
|
||||
CvInvoke.CvtColor(img, gray, ColorConversion.Bgr2Gray);
|
||||
var corners = new VectorOfPointF();
|
||||
|
||||
if (CvInvoke.FindChessboardCorners(gray, new System.Drawing.Size(boardWidth, boardHeight), corners))
|
||||
{
|
||||
CvInvoke.CornerSubPix(gray, corners, new System.Drawing.Size(11, 11), new System.Drawing.Size(-1, -1),
|
||||
new MCvTermCriteria(30, 0.001));
|
||||
CvInvoke.DrawChessboardCorners(img, new System.Drawing.Size(boardWidth, boardHeight), corners, true);
|
||||
}
|
||||
|
||||
gray.Dispose();
|
||||
return img;
|
||||
}
|
||||
|
||||
public void SaveCalibration(string filePath)
|
||||
{
|
||||
if (_cameraMatrix == null || _distCoeffs == null)
|
||||
throw new InvalidOperationException("请先执行标定");
|
||||
|
||||
var result = new CalibrationResult
|
||||
{
|
||||
CameraMatrix = new double[3][],
|
||||
DistortionCoeffs = new double[_distCoeffs.Rows],
|
||||
ReprojectionError = _reprojectionError,
|
||||
CalibrationTime = DateTime.Now
|
||||
};
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
result.CameraMatrix[i] = new double[3];
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
double[] data = new double[1];
|
||||
Marshal.Copy(_cameraMatrix.DataPointer + (i * 3 + j) * 8, data, 0, 1);
|
||||
result.CameraMatrix[i][j] = data[0];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _distCoeffs.Rows; i++)
|
||||
{
|
||||
double[] data = new double[1];
|
||||
Marshal.Copy(_distCoeffs.DataPointer + i * 8, data, 0, 1);
|
||||
result.DistortionCoeffs[i] = data[0];
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
|
||||
public bool LoadCalibration(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var result = JsonSerializer.Deserialize<CalibrationResult>(json);
|
||||
|
||||
if (result == null) return false;
|
||||
|
||||
_reprojectionError = result.ReprojectionError;
|
||||
|
||||
_cameraMatrix = new Mat(3, 3, DepthType.Cv64F, 1);
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
Marshal.Copy(new[] { result.CameraMatrix[i][j] }, 0, _cameraMatrix.DataPointer + (i * 3 + j) * 8, 1);
|
||||
|
||||
_distCoeffs = new Mat(result.DistortionCoeffs.Length, 1, DepthType.Cv64F, 1);
|
||||
for (int i = 0; i < result.DistortionCoeffs.Length; i++)
|
||||
Marshal.Copy(new[] { result.DistortionCoeffs[i] }, 0, _distCoeffs.DataPointer + i * 8, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<UserControl x:Class="XP.Camera.Calibration.Controls.CalibrationControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="850"
|
||||
d:DesignWidth="1400">
|
||||
<UserControl.Resources>
|
||||
<cal:CalibrationLocalizedStrings x:Key="LocalizedStrings" />
|
||||
<SolidColorBrush x:Key="PrimaryColor" Color="#F5F5F5" />
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#0078D4" />
|
||||
<SolidColorBrush x:Key="BackgroundColor" Color="#FAFAFA" />
|
||||
<SolidColorBrush x:Key="SidebarColor" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#E1E1E1" />
|
||||
<SolidColorBrush x:Key="TextColor" Color="#333333" />
|
||||
<SolidColorBrush x:Key="TextSecondaryColor" Color="#666666" />
|
||||
|
||||
<Style x:Key="ToolbarButtonStyle" TargetType="Button">
|
||||
<Setter Property="Width" Value="90" />
|
||||
<Setter Property="Height" Value="70" />
|
||||
<Setter Property="Margin" Value="0,0,8,0" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColor}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="3">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ContentPresenter Content="{TemplateBinding Tag}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,4,0,4" />
|
||||
<TextBlock Text="{TemplateBinding Content}"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#E5F3FF" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentColor}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#CCE8FF" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Background="{StaticResource PrimaryColor}" BorderBrush="{StaticResource BorderColor}"
|
||||
BorderThickness="0,0,0,1" Padding="15,10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoadImage}"
|
||||
Command="{Binding LoadImageCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="ImageOutline" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoadCsv}"
|
||||
Command="{Binding LoadCsvCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="FileDelimited" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationExecute}"
|
||||
Command="{Binding CalibrateCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="Crosshairs" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationSave}"
|
||||
Command="{Binding SaveCalibrationCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="ContentSave" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoad}"
|
||||
Command="{Binding LoadCalibrationCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="FolderOpen" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<CheckBox Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationShowWorld}"
|
||||
VerticalAlignment="Center" FontFamily="Segoe UI"
|
||||
IsChecked="{Binding ShowWorldCoordinates}"
|
||||
Margin="10,0,0,0" FontSize="13" Foreground="{StaticResource TextColor}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="400" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" BorderBrush="{StaticResource BorderColor}" BorderThickness="0,0,1,0"
|
||||
Background="{StaticResource SidebarColor}">
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationPointList}"
|
||||
FontSize="15" FontWeight="SemiBold" FontFamily="Segoe UI"
|
||||
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||
|
||||
<DataGrid Grid.Row="1" AutoGenerateColumns="False" CanUserAddRows="True"
|
||||
ItemsSource="{Binding CalibrationPoints}"
|
||||
HeadersVisibility="Column" GridLinesVisibility="All"
|
||||
FontFamily="Segoe UI"
|
||||
BorderBrush="{StaticResource BorderColor}" BorderThickness="1">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationPixelX}"
|
||||
Binding="{Binding PixelX}" FontFamily="Segoe UI" Width="*" />
|
||||
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationPixelY}"
|
||||
Binding="{Binding PixelY}" FontFamily="Segoe UI" Width="*" />
|
||||
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationWorldX}"
|
||||
Binding="{Binding WorldX}" FontFamily="Segoe UI" Width="*" />
|
||||
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationWorldY}"
|
||||
Binding="{Binding WorldY}" FontFamily="Segoe UI" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Column="1" Background="{StaticResource BackgroundColor}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,12,8" />
|
||||
|
||||
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
|
||||
Margin="12,0,12,12" Padding="12" MinHeight="80">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<TextBlock TextWrapping="Wrap" FontSize="12"
|
||||
Text="{Binding StatusText}" FontFamily="Segoe UI"
|
||||
Foreground="{StaticResource TextColor}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using XP.Camera.Calibration.ViewModels;
|
||||
using WpfBrushes = System.Windows.Media.Brushes;
|
||||
using WpfColor = System.Windows.Media.Color;
|
||||
|
||||
namespace XP.Camera.Calibration.Controls;
|
||||
|
||||
public partial class CalibrationControl : UserControl
|
||||
{
|
||||
private CalibrationViewModel? _viewModel;
|
||||
|
||||
public CalibrationControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += CalibrationControl_Loaded;
|
||||
}
|
||||
|
||||
private void CalibrationControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is CalibrationViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
|
||||
_viewModel.ImageLoadedRequested += (s, e) =>
|
||||
{
|
||||
imageCanvas.ReferenceImage = _viewModel.ImageSource;
|
||||
imageCanvas.RoiCanvas.Children.Clear();
|
||||
};
|
||||
|
||||
imageCanvas.CanvasRightMouseUp += ImageCanvas_RightMouseUp;
|
||||
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
|
||||
}
|
||||
}
|
||||
|
||||
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (_viewModel?.CurrentImage == null) return;
|
||||
|
||||
double zoom = e.Delta > 0 ? 1.1 : 0.9;
|
||||
imageCanvas.ZoomScale *= zoom;
|
||||
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
|
||||
}
|
||||
|
||||
private void ImageCanvas_RightMouseUp(object? sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_viewModel?.CurrentImage == null) return;
|
||||
|
||||
var pos = e.GetPosition(imageCanvas.RoiCanvas);
|
||||
float imageX = (float)pos.X;
|
||||
float imageY = (float)pos.Y;
|
||||
|
||||
if (imageX >= 0 && imageX < _viewModel.CurrentImage.Width &&
|
||||
imageY >= 0 && imageY < _viewModel.CurrentImage.Height)
|
||||
{
|
||||
var pixelPoint = new PointF(imageX, imageY);
|
||||
var worldPoint = _viewModel.ConvertPixelToWorld(pixelPoint);
|
||||
|
||||
_viewModel.StatusText = $"像素坐标: ({imageX:F2}, {imageY:F2})\n世界坐标: ({worldPoint.X:F2}, {worldPoint.Y:F2})";
|
||||
|
||||
DrawMarkerOnCanvas(imageX, imageY, worldPoint);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMarkerOnCanvas(float imageX, float imageY, PointF worldPoint)
|
||||
{
|
||||
imageCanvas.RoiCanvas.Children.Clear();
|
||||
|
||||
var ellipse = new System.Windows.Shapes.Ellipse
|
||||
{
|
||||
Width = 10, Height = 10,
|
||||
Stroke = WpfBrushes.Red, StrokeThickness = 2,
|
||||
Fill = WpfBrushes.Transparent
|
||||
};
|
||||
Canvas.SetLeft(ellipse, imageX - 5);
|
||||
Canvas.SetTop(ellipse, imageY - 5);
|
||||
imageCanvas.RoiCanvas.Children.Add(ellipse);
|
||||
|
||||
var pixelText = new TextBlock
|
||||
{
|
||||
Text = $"P:({imageX:F0},{imageY:F0})",
|
||||
Foreground = WpfBrushes.Red, FontSize = 12,
|
||||
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
|
||||
};
|
||||
Canvas.SetLeft(pixelText, imageX + 10);
|
||||
Canvas.SetTop(pixelText, imageY - 20);
|
||||
imageCanvas.RoiCanvas.Children.Add(pixelText);
|
||||
|
||||
if (_viewModel?.ShowWorldCoordinates == true)
|
||||
{
|
||||
var worldText = new TextBlock
|
||||
{
|
||||
Text = $"W:({worldPoint.X:F2},{worldPoint.Y:F2})",
|
||||
Foreground = WpfBrushes.Blue, FontSize = 12,
|
||||
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
|
||||
};
|
||||
Canvas.SetLeft(worldText, imageX + 10);
|
||||
Canvas.SetTop(worldText, imageY + 5);
|
||||
imageCanvas.RoiCanvas.Children.Add(worldText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<UserControl x:Class="XP.Camera.Calibration.Controls.ChessboardCalibrationControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="900"
|
||||
d:DesignWidth="1600">
|
||||
<UserControl.Resources>
|
||||
<cal:CalibrationLocalizedStrings x:Key="LocalizedStrings" />
|
||||
<SolidColorBrush x:Key="PrimaryColor" Color="#F5F5F5" />
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#0078D4" />
|
||||
<SolidColorBrush x:Key="BackgroundColor" Color="#FAFAFA" />
|
||||
<SolidColorBrush x:Key="SidebarColor" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#E1E1E1" />
|
||||
<SolidColorBrush x:Key="TextColor" Color="#333333" />
|
||||
<SolidColorBrush x:Key="TextSecondaryColor" Color="#666666" />
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
|
||||
<Style x:Key="ToolbarButtonStyle" TargetType="Button">
|
||||
<Setter Property="Width" Value="90" />
|
||||
<Setter Property="Height" Value="70" />
|
||||
<Setter Property="Margin" Value="0,0,8,0" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColor}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="3">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ContentPresenter Content="{TemplateBinding Tag}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,4,0,4" />
|
||||
<TextBlock Text="{TemplateBinding Content}"
|
||||
FontSize="12" FontFamily="Segoe UI"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#E5F3FF" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentColor}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#CCE8FF" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Background="{StaticResource PrimaryColor}" BorderBrush="{StaticResource BorderColor}"
|
||||
BorderThickness="0,0,0,1" Padding="15,10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardAddImages}" Command="{Binding AddImagesCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="ImageMultiple" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardClearImages}" Command="{Binding ClearImagesCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="DeleteSweep" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardCalibrate}" Command="{Binding CalibrateCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="GridLarge" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardSave}" Command="{Binding SaveCalibrationCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="ContentSave" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardLoad}" Command="{Binding LoadCalibrationCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="FolderOpen" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardUndistort}" Command="{Binding UndistortImageCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="ImageEdit" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="400" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" BorderBrush="{StaticResource BorderColor}" BorderThickness="0,0,1,0"
|
||||
Background="{StaticResource SidebarColor}">
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,16">
|
||||
<TextBlock Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardParameters}" FontFamily="Segoe UI" FontSize="15" FontWeight="SemiBold"
|
||||
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardWidth}" FontFamily="Segoe UI" VerticalAlignment="Center" Margin="0,0,8,8" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ChessboardWidth}" FontFamily="Segoe UI" Height="28" Margin="0,0,0,8" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardHeight}" FontFamily="Segoe UI" VerticalAlignment="Center" Margin="0,0,8,8" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ChessboardHeight}" FontFamily="Segoe UI" Height="28" Margin="0,0,0,8" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardSquareSize}" FontFamily="Segoe UI" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SquareSize}" FontFamily="Segoe UI" Height="28" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardImageList}" FontFamily="Segoe UI" FontSize="15" FontWeight="SemiBold"
|
||||
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||
<ListBox Grid.Row="2" ItemsSource="{Binding ImageFileNames}" SelectedIndex="{Binding SelectedImageIndex}"
|
||||
FontFamily="Segoe UI" BorderBrush="{StaticResource BorderColor}" BorderThickness="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Column="1" Background="{StaticResource BackgroundColor}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="350" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,8,8" />
|
||||
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
|
||||
Margin="12,0,8,12" Padding="12" Height="70">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="{Binding ProgressText}" FontSize="12" FontFamily="Segoe UI"
|
||||
Margin="0,0,0,6" Foreground="{StaticResource TextColor}"
|
||||
Visibility="{Binding IsCalibrating, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||
<ProgressBar Grid.Row="1" Height="24" Value="{Binding ProgressValue}" Maximum="100"
|
||||
Visibility="{Binding IsCalibrating, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||
<TextBlock Grid.Row="0" Grid.RowSpan="2" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardStatusReady}" FontFamily="Segoe UI" FontSize="12"
|
||||
VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryColor}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsCalibrating}" Value="False">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Column="1" Background="White" BorderBrush="{StaticResource BorderColor}"
|
||||
BorderThickness="1,0,0,0" Padding="12" Margin="0,12,12,12">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardStatusInfo}" FontFamily="Segoe UI" FontSize="15" FontWeight="SemiBold"
|
||||
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<TextBlock TextWrapping="Wrap" FontSize="12" FontFamily="Segoe UI"
|
||||
Text="{Binding StatusText}" Foreground="{StaticResource TextColor}" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using XP.Camera.Calibration.ViewModels;
|
||||
|
||||
namespace XP.Camera.Calibration.Controls;
|
||||
|
||||
public partial class ChessboardCalibrationControl : UserControl
|
||||
{
|
||||
private ChessboardCalibrationViewModel? _viewModel;
|
||||
|
||||
public ChessboardCalibrationControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += ChessboardCalibrationControl_Loaded;
|
||||
}
|
||||
|
||||
private void ChessboardCalibrationControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is ChessboardCalibrationViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
|
||||
_viewModel.ImageLoadedRequested += (s, e) =>
|
||||
{
|
||||
imageCanvas.ReferenceImage = _viewModel.ImageSource;
|
||||
};
|
||||
|
||||
_viewModel.ImageClearedRequested += (s, e) =>
|
||||
{
|
||||
imageCanvas.ReferenceImage = null;
|
||||
};
|
||||
|
||||
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
|
||||
}
|
||||
}
|
||||
|
||||
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (_viewModel?.ImageSource == null) return;
|
||||
|
||||
double zoom = e.Delta > 0 ? 1.1 : 0.9;
|
||||
imageCanvas.ZoomScale *= zoom;
|
||||
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<UserControl x:Class="XP.Camera.Calibration.Controls.ImageCanvasControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="600" d:DesignWidth="800" x:Name="imageCanvasControl">
|
||||
<Border ClipToBounds="True" RenderOptions.BitmapScalingMode="NearestNeighbor">
|
||||
<Viewbox>
|
||||
<AdornerDecorator x:Name="adorner" MouseWheel="Adorner_MouseWheel">
|
||||
<AdornerDecorator.RenderTransform>
|
||||
<TransformGroup>
|
||||
<TranslateTransform X="{Binding PanningOffsetX, ElementName=imageCanvasControl}"
|
||||
Y="{Binding PanningOffsetY, ElementName=imageCanvasControl}" />
|
||||
<ScaleTransform ScaleX="{Binding ZoomScale, ElementName=imageCanvasControl}"
|
||||
ScaleY="{Binding ZoomScale, ElementName=imageCanvasControl}"
|
||||
CenterX="{Binding ZoomCenter.X, ElementName=imageCanvasControl}"
|
||||
CenterY="{Binding ZoomCenter.Y, ElementName=imageCanvasControl}" />
|
||||
</TransformGroup>
|
||||
</AdornerDecorator.RenderTransform>
|
||||
<Grid PreviewMouseMove="Canvas_MouseMove"
|
||||
PreviewMouseLeftButtonUp="Canvas_MouseLeftButtonUp"
|
||||
PreviewMouseRightButtonUp="Canvas_MouseRightButtonUp"
|
||||
MouseEnter="Canvas_MouseEnter"
|
||||
PreviewMouseLeftButtonDown="Canvas_MouseLeftButtonDown"
|
||||
PreviewMouseRightButtonDown="Canvas_MouseRightButtonDown">
|
||||
<ContentPresenter Content="{Binding RoiCanvas, ElementName=imageCanvasControl}"
|
||||
SizeChanged="ContentPresenter_SizeChanged" />
|
||||
</Grid>
|
||||
</AdornerDecorator>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,229 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XP.Camera.Calibration.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 图像画布控件 - 提供图像显示、缩放、平移功能
|
||||
/// </summary>
|
||||
public partial class ImageCanvasControl : UserControl
|
||||
{
|
||||
private Point mouseDownPoint = new Point();
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty ZoomScaleProperty =
|
||||
DependencyProperty.Register("ZoomScale", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
|
||||
|
||||
public static readonly DependencyProperty ZoomCenterProperty =
|
||||
DependencyProperty.Register("ZoomCenter", typeof(Point), typeof(ImageCanvasControl), new PropertyMetadata(new Point()));
|
||||
|
||||
public static readonly DependencyProperty PanningOffsetXProperty =
|
||||
DependencyProperty.Register("PanningOffsetX", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
|
||||
|
||||
public static readonly DependencyProperty PanningOffsetYProperty =
|
||||
DependencyProperty.Register("PanningOffsetY", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
|
||||
|
||||
public static readonly DependencyProperty ReferenceImageProperty =
|
||||
DependencyProperty.Register("ReferenceImage", typeof(BitmapSource), typeof(ImageCanvasControl),
|
||||
new UIPropertyMetadata(null, ReferenceImageChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageScaleFactorProperty =
|
||||
DependencyProperty.Register("ImageScaleFactor", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
|
||||
|
||||
public static readonly DependencyProperty MaxImageWidthProperty =
|
||||
DependencyProperty.Register("MaxImageWidth", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty MaxImageHeightProperty =
|
||||
DependencyProperty.Register("MaxImageHeight", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty EnablePanningProperty =
|
||||
DependencyProperty.Register("EnablePanning", typeof(bool), typeof(ImageCanvasControl), new PropertyMetadata(true));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public double ZoomScale
|
||||
{
|
||||
get => (double)GetValue(ZoomScaleProperty);
|
||||
set => SetValue(ZoomScaleProperty, value);
|
||||
}
|
||||
|
||||
public Point ZoomCenter
|
||||
{
|
||||
get => (Point)GetValue(ZoomCenterProperty);
|
||||
set => SetValue(ZoomCenterProperty, value);
|
||||
}
|
||||
|
||||
public double PanningOffsetX
|
||||
{
|
||||
get => (double)GetValue(PanningOffsetXProperty);
|
||||
set => SetValue(PanningOffsetXProperty, value);
|
||||
}
|
||||
|
||||
public double PanningOffsetY
|
||||
{
|
||||
get => (double)GetValue(PanningOffsetYProperty);
|
||||
set => SetValue(PanningOffsetYProperty, value);
|
||||
}
|
||||
|
||||
public BitmapSource? ReferenceImage
|
||||
{
|
||||
get => (BitmapSource?)GetValue(ReferenceImageProperty);
|
||||
set => SetValue(ReferenceImageProperty, value);
|
||||
}
|
||||
|
||||
public double ImageScaleFactor
|
||||
{
|
||||
get => (double)GetValue(ImageScaleFactorProperty);
|
||||
set => SetValue(ImageScaleFactorProperty, value);
|
||||
}
|
||||
|
||||
public int MaxImageWidth
|
||||
{
|
||||
get => (int)GetValue(MaxImageWidthProperty);
|
||||
set => SetValue(MaxImageWidthProperty, value);
|
||||
}
|
||||
|
||||
public int MaxImageHeight
|
||||
{
|
||||
get => (int)GetValue(MaxImageHeightProperty);
|
||||
set => SetValue(MaxImageHeightProperty, value);
|
||||
}
|
||||
|
||||
public bool EnablePanning
|
||||
{
|
||||
get => (bool)GetValue(EnablePanningProperty);
|
||||
set => SetValue(EnablePanningProperty, value);
|
||||
}
|
||||
|
||||
private Canvas roiCanvas = new Canvas();
|
||||
public Canvas RoiCanvas
|
||||
{
|
||||
get => roiCanvas;
|
||||
set => roiCanvas = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseUp;
|
||||
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseDown;
|
||||
public event EventHandler<MouseButtonEventArgs>? CanvasLeftMouseDown;
|
||||
public event EventHandler<MouseEventArgs>? CanvasMouseMove;
|
||||
public event EventHandler<MouseWheelEventArgs>? CanvasMouseWheel;
|
||||
|
||||
#endregion
|
||||
|
||||
public ImageCanvasControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private static void ReferenceImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
(d as ImageCanvasControl)?.OnReferenceImageChanged(e.NewValue as BitmapSource);
|
||||
}
|
||||
|
||||
private void OnReferenceImageChanged(BitmapSource? bitmapSource)
|
||||
{
|
||||
if (bitmapSource != null)
|
||||
{
|
||||
ImageBrush brush = new ImageBrush { ImageSource = bitmapSource, Stretch = Stretch.Uniform };
|
||||
RoiCanvas.Background = brush;
|
||||
RoiCanvas.Height = bitmapSource.Height;
|
||||
RoiCanvas.Width = bitmapSource.Width;
|
||||
}
|
||||
else
|
||||
{
|
||||
RoiCanvas.Height = MaxImageHeight > 0 ? MaxImageHeight : 600;
|
||||
RoiCanvas.Width = MaxImageWidth > 0 ? MaxImageWidth : 800;
|
||||
RoiCanvas.Background = Brushes.LightGray;
|
||||
}
|
||||
}
|
||||
|
||||
private double CalculateScaleFactor()
|
||||
{
|
||||
if (ActualWidth <= 0) return 1;
|
||||
double scaleFactor = Math.Max(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
|
||||
if (scaleFactor < 0)
|
||||
scaleFactor = Math.Min(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
|
||||
return scaleFactor;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void Canvas_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
mouseDownPoint = e.GetPosition(RoiCanvas);
|
||||
}
|
||||
|
||||
private void Canvas_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
CanvasMouseMove?.Invoke(sender, e);
|
||||
if (EnablePanning && e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
Point mousePoint = e.GetPosition(RoiCanvas);
|
||||
double mouseMoveLength = Point.Subtract(mousePoint, mouseDownPoint).Length;
|
||||
if (mouseMoveLength > (10 * CalculateScaleFactor()) / ZoomScale)
|
||||
{
|
||||
PanningOffsetX += mousePoint.X - mouseDownPoint.X;
|
||||
PanningOffsetY += mousePoint.Y - mouseDownPoint.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { }
|
||||
|
||||
private void Canvas_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CanvasRightMouseUp?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
mouseDownPoint = e.GetPosition(RoiCanvas);
|
||||
CanvasLeftMouseDown?.Invoke(sender, e);
|
||||
if (EnablePanning && e.ClickCount == 2)
|
||||
{
|
||||
ResetView();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CanvasRightMouseDown?.Invoke(sender, e);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Adorner_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
CanvasMouseWheel?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void ContentPresenter_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ImageScaleFactor = CalculateScaleFactor();
|
||||
}
|
||||
|
||||
private void ResetView()
|
||||
{
|
||||
ZoomScale = 1.0;
|
||||
PanningOffsetX = 0.0;
|
||||
PanningOffsetY = 0.0;
|
||||
ZoomCenter = new Point(0, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.Win32;
|
||||
using System.Windows;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
|
||||
namespace XP.Camera.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// 默认对话框服务实现,使用 HexMessageBox 自定义消息框。
|
||||
/// 外部项目可直接使用,无需额外依赖。
|
||||
/// </summary>
|
||||
public class DefaultCalibrationDialogService : ICalibrationDialogService
|
||||
{
|
||||
public void ShowMessage(string message, string title)
|
||||
{
|
||||
HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
public void ShowError(string message, string title)
|
||||
{
|
||||
HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
public void ShowInfo(string message, string title)
|
||||
{
|
||||
HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
public bool ShowConfirm(string message, string title)
|
||||
{
|
||||
return HexMessageBox.Show(message, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
|
||||
}
|
||||
|
||||
public string? ShowOpenFileDialog(string filter)
|
||||
{
|
||||
var dialog = new OpenFileDialog { Filter = filter };
|
||||
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||
}
|
||||
|
||||
public string[]? ShowOpenMultipleFilesDialog(string filter)
|
||||
{
|
||||
var dialog = new OpenFileDialog { Filter = filter, Multiselect = true };
|
||||
return dialog.ShowDialog() == true ? dialog.FileNames : null;
|
||||
}
|
||||
|
||||
public string? ShowSaveFileDialog(string filter, string? defaultFileName = null)
|
||||
{
|
||||
var dialog = new SaveFileDialog { Filter = filter, FileName = defaultFileName ?? string.Empty };
|
||||
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace XP.Camera.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// 对话框服务接口,用于标定模块的文件选择和消息提示。
|
||||
/// </summary>
|
||||
public interface ICalibrationDialogService
|
||||
{
|
||||
void ShowMessage(string message, string title);
|
||||
void ShowError(string message, string title);
|
||||
void ShowInfo(string message, string title);
|
||||
bool ShowConfirm(string message, string title);
|
||||
string? ShowOpenFileDialog(string filter);
|
||||
string[]? ShowOpenMultipleFilesDialog(string filter);
|
||||
string? ShowSaveFileDialog(string filter, string? defaultFileName = null);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码由工具生成。
|
||||
// 如果重新生成代码,将丢失对此文件所做的更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace XP.Camera.Calibration.Resources {
|
||||
using System;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class CalibrationResources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal CalibrationResources() { }
|
||||
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XP.Camera.Calibration.Resources.CalibrationResources", typeof(CalibrationResources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get { return resourceCulture; }
|
||||
set { resourceCulture = value; }
|
||||
}
|
||||
|
||||
// 九点标定
|
||||
public static string CalibrationToolTitle => ResourceManager.GetString("CalibrationToolTitle", resourceCulture) ?? "";
|
||||
public static string CalibrationLoadImage => ResourceManager.GetString("CalibrationLoadImage", resourceCulture) ?? "";
|
||||
public static string CalibrationLoadCsv => ResourceManager.GetString("CalibrationLoadCsv", resourceCulture) ?? "";
|
||||
public static string CalibrationExecute => ResourceManager.GetString("CalibrationExecute", resourceCulture) ?? "";
|
||||
public static string CalibrationSave => ResourceManager.GetString("CalibrationSave", resourceCulture) ?? "";
|
||||
public static string CalibrationLoad => ResourceManager.GetString("CalibrationLoad", resourceCulture) ?? "";
|
||||
public static string CalibrationShowWorld => ResourceManager.GetString("CalibrationShowWorld", resourceCulture) ?? "";
|
||||
public static string CalibrationPointList => ResourceManager.GetString("CalibrationPointList", resourceCulture) ?? "";
|
||||
public static string CalibrationPixelX => ResourceManager.GetString("CalibrationPixelX", resourceCulture) ?? "";
|
||||
public static string CalibrationPixelY => ResourceManager.GetString("CalibrationPixelY", resourceCulture) ?? "";
|
||||
public static string CalibrationWorldX => ResourceManager.GetString("CalibrationWorldX", resourceCulture) ?? "";
|
||||
public static string CalibrationWorldY => ResourceManager.GetString("CalibrationWorldY", resourceCulture) ?? "";
|
||||
public static string CalibrationStatusReady => ResourceManager.GetString("CalibrationStatusReady", resourceCulture) ?? "";
|
||||
public static string CalibrationStatusImageLoaded => ResourceManager.GetString("CalibrationStatusImageLoaded", resourceCulture) ?? "";
|
||||
public static string CalibrationStatusCsvLoaded => ResourceManager.GetString("CalibrationStatusCsvLoaded", resourceCulture) ?? "";
|
||||
public static string CalibrationStatusSuccess => ResourceManager.GetString("CalibrationStatusSuccess", resourceCulture) ?? "";
|
||||
public static string CalibrationStatusFailed => ResourceManager.GetString("CalibrationStatusFailed", resourceCulture) ?? "";
|
||||
public static string CalibrationStatusSaved => ResourceManager.GetString("CalibrationStatusSaved", resourceCulture) ?? "";
|
||||
public static string CalibrationStatusLoaded => ResourceManager.GetString("CalibrationStatusLoaded", resourceCulture) ?? "";
|
||||
public static string CalibrationCoordinates => ResourceManager.GetString("CalibrationCoordinates", resourceCulture) ?? "";
|
||||
public static string CalibrationErrorMinPoints => ResourceManager.GetString("CalibrationErrorMinPoints", resourceCulture) ?? "";
|
||||
public static string CalibrationSuccessTitle => ResourceManager.GetString("CalibrationSuccessTitle", resourceCulture) ?? "";
|
||||
public static string CalibrationSuccessMessage => ResourceManager.GetString("CalibrationSuccessMessage", resourceCulture) ?? "";
|
||||
public static string CalibrationSaveSuccess => ResourceManager.GetString("CalibrationSaveSuccess", resourceCulture) ?? "";
|
||||
public static string CalibrationLoadSuccess => ResourceManager.GetString("CalibrationLoadSuccess", resourceCulture) ?? "";
|
||||
public static string CalibrationLoadFailed => ResourceManager.GetString("CalibrationLoadFailed", resourceCulture) ?? "";
|
||||
|
||||
// 棋盘格标定
|
||||
public static string ChessboardToolTitle => ResourceManager.GetString("ChessboardToolTitle", resourceCulture) ?? "";
|
||||
public static string ChessboardAddImages => ResourceManager.GetString("ChessboardAddImages", resourceCulture) ?? "";
|
||||
public static string ChessboardClearImages => ResourceManager.GetString("ChessboardClearImages", resourceCulture) ?? "";
|
||||
public static string ChessboardCalibrate => ResourceManager.GetString("ChessboardCalibrate", resourceCulture) ?? "";
|
||||
public static string ChessboardSave => ResourceManager.GetString("ChessboardSave", resourceCulture) ?? "";
|
||||
public static string ChessboardLoad => ResourceManager.GetString("ChessboardLoad", resourceCulture) ?? "";
|
||||
public static string ChessboardUndistort => ResourceManager.GetString("ChessboardUndistort", resourceCulture) ?? "";
|
||||
public static string ChessboardParameters => ResourceManager.GetString("ChessboardParameters", resourceCulture) ?? "";
|
||||
public static string ChessboardWidth => ResourceManager.GetString("ChessboardWidth", resourceCulture) ?? "";
|
||||
public static string ChessboardHeight => ResourceManager.GetString("ChessboardHeight", resourceCulture) ?? "";
|
||||
public static string ChessboardSquareSize => ResourceManager.GetString("ChessboardSquareSize", resourceCulture) ?? "";
|
||||
public static string ChessboardImageList => ResourceManager.GetString("ChessboardImageList", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusInfo => ResourceManager.GetString("ChessboardStatusInfo", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusReady => ResourceManager.GetString("ChessboardStatusReady", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusAdded => ResourceManager.GetString("ChessboardStatusAdded", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusCleared => ResourceManager.GetString("ChessboardStatusCleared", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusCalibrating => ResourceManager.GetString("ChessboardStatusCalibrating", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusSuccess => ResourceManager.GetString("ChessboardStatusSuccess", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusFailed => ResourceManager.GetString("ChessboardStatusFailed", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusSaved => ResourceManager.GetString("ChessboardStatusSaved", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusLoaded => ResourceManager.GetString("ChessboardStatusLoaded", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusUndistorted => ResourceManager.GetString("ChessboardStatusUndistorted", resourceCulture) ?? "";
|
||||
public static string ChessboardStatusImageError => ResourceManager.GetString("ChessboardStatusImageError", resourceCulture) ?? "";
|
||||
public static string ChessboardProgressPreparing => ResourceManager.GetString("ChessboardProgressPreparing", resourceCulture) ?? "";
|
||||
public static string ChessboardProgressDetecting => ResourceManager.GetString("ChessboardProgressDetecting", resourceCulture) ?? "";
|
||||
public static string ChessboardProgressCalibrating => ResourceManager.GetString("ChessboardProgressCalibrating", resourceCulture) ?? "";
|
||||
public static string ChessboardProgressCalculating => ResourceManager.GetString("ChessboardProgressCalculating", resourceCulture) ?? "";
|
||||
public static string ChessboardProgressComplete => ResourceManager.GetString("ChessboardProgressComplete", resourceCulture) ?? "";
|
||||
public static string ChessboardProgressFailed => ResourceManager.GetString("ChessboardProgressFailed", resourceCulture) ?? "";
|
||||
public static string ChessboardErrorMinImages => ResourceManager.GetString("ChessboardErrorMinImages", resourceCulture) ?? "";
|
||||
public static string ChessboardErrorInsufficientValid => ResourceManager.GetString("ChessboardErrorInsufficientValid", resourceCulture) ?? "";
|
||||
public static string ChessboardSaveSuccess => ResourceManager.GetString("ChessboardSaveSuccess", resourceCulture) ?? "";
|
||||
public static string ChessboardLoadSuccess => ResourceManager.GetString("ChessboardLoadSuccess", resourceCulture) ?? "";
|
||||
public static string ChessboardCalibrationComplete => ResourceManager.GetString("ChessboardCalibrationComplete", resourceCulture) ?? "";
|
||||
public static string ChessboardImageError => ResourceManager.GetString("ChessboardImageError", resourceCulture) ?? "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<!-- Nine-Point Calibration -->
|
||||
<data name="CalibrationToolTitle" xml:space="preserve">
|
||||
<value>Nine-Point Calibration Tool</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadImage" xml:space="preserve">
|
||||
<value>Load Image</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadCsv" xml:space="preserve">
|
||||
<value>Load from CSV</value>
|
||||
</data>
|
||||
<data name="CalibrationExecute" xml:space="preserve">
|
||||
<value>Calibrate</value>
|
||||
</data>
|
||||
<data name="CalibrationSave" xml:space="preserve">
|
||||
<value>Save Calibration</value>
|
||||
</data>
|
||||
<data name="CalibrationLoad" xml:space="preserve">
|
||||
<value>Load Calibration</value>
|
||||
</data>
|
||||
<data name="CalibrationShowWorld" xml:space="preserve">
|
||||
<value>Show World Coordinates</value>
|
||||
</data>
|
||||
<data name="CalibrationPointList" xml:space="preserve">
|
||||
<value>Calibration Points</value>
|
||||
</data>
|
||||
<data name="CalibrationPixelX" xml:space="preserve">
|
||||
<value>Pixel X</value>
|
||||
</data>
|
||||
<data name="CalibrationPixelY" xml:space="preserve">
|
||||
<value>Pixel Y</value>
|
||||
</data>
|
||||
<data name="CalibrationWorldX" xml:space="preserve">
|
||||
<value>World X</value>
|
||||
</data>
|
||||
<data name="CalibrationWorldY" xml:space="preserve">
|
||||
<value>World Y</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusReady" xml:space="preserve">
|
||||
<value>Ready</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusImageLoaded" xml:space="preserve">
|
||||
<value>Status: Image loaded
|
||||
{0}
|
||||
Right-click on image to view coordinate conversion</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusCsvLoaded" xml:space="preserve">
|
||||
<value>Status: Loaded {0} calibration points from CSV
|
||||
{1}</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusSuccess" xml:space="preserve">
|
||||
<value>Status: Calibration successful! Using {0} points</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusFailed" xml:space="preserve">
|
||||
<value>Status: Calibration failed</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusSaved" xml:space="preserve">
|
||||
<value>Status: Calibration saved to
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusLoaded" xml:space="preserve">
|
||||
<value>Status: Calibration loaded from
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="CalibrationCoordinates" xml:space="preserve">
|
||||
<value>Pixel coordinates: ({0:F2}, {1:F2})
|
||||
World coordinates: ({2:F2}, {3:F2})</value>
|
||||
</data>
|
||||
<data name="CalibrationErrorMinPoints" xml:space="preserve">
|
||||
<value>At least 4 calibration points required!</value>
|
||||
</data>
|
||||
<data name="CalibrationSuccessTitle" xml:space="preserve">
|
||||
<value>Success</value>
|
||||
</data>
|
||||
<data name="CalibrationSuccessMessage" xml:space="preserve">
|
||||
<value>Calibration completed!</value>
|
||||
</data>
|
||||
<data name="CalibrationSaveSuccess" xml:space="preserve">
|
||||
<value>Save successful!</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadSuccess" xml:space="preserve">
|
||||
<value>Load successful!</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadFailed" xml:space="preserve">
|
||||
<value>Load failed!</value>
|
||||
</data>
|
||||
<!-- Chessboard Calibration -->
|
||||
<data name="ChessboardToolTitle" xml:space="preserve">
|
||||
<value>Chessboard Calibration Tool</value>
|
||||
</data>
|
||||
<data name="ChessboardAddImages" xml:space="preserve">
|
||||
<value>Add Images</value>
|
||||
</data>
|
||||
<data name="ChessboardClearImages" xml:space="preserve">
|
||||
<value>Clear List</value>
|
||||
</data>
|
||||
<data name="ChessboardCalibrate" xml:space="preserve">
|
||||
<value>Calibrate</value>
|
||||
</data>
|
||||
<data name="ChessboardSave" xml:space="preserve">
|
||||
<value>Save Calibration</value>
|
||||
</data>
|
||||
<data name="ChessboardLoad" xml:space="preserve">
|
||||
<value>Load Calibration</value>
|
||||
</data>
|
||||
<data name="ChessboardUndistort" xml:space="preserve">
|
||||
<value>Undistort Image</value>
|
||||
</data>
|
||||
<data name="ChessboardParameters" xml:space="preserve">
|
||||
<value>Chessboard Parameters</value>
|
||||
</data>
|
||||
<data name="ChessboardWidth" xml:space="preserve">
|
||||
<value>Inner Corners Width:</value>
|
||||
</data>
|
||||
<data name="ChessboardHeight" xml:space="preserve">
|
||||
<value>Inner Corners Height:</value>
|
||||
</data>
|
||||
<data name="ChessboardSquareSize" xml:space="preserve">
|
||||
<value>Square Size (mm):</value>
|
||||
</data>
|
||||
<data name="ChessboardImageList" xml:space="preserve">
|
||||
<value>Calibration Images</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusInfo" xml:space="preserve">
|
||||
<value>Status Information</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusReady" xml:space="preserve">
|
||||
<value>Ready</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusAdded" xml:space="preserve">
|
||||
<value>Added {0} images</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusCleared" xml:space="preserve">
|
||||
<value>Image list cleared</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusCalibrating" xml:space="preserve">
|
||||
<value>Calibrating, please wait...</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusSuccess" xml:space="preserve">
|
||||
<value>Calibration successful!
|
||||
Overall reprojection error: {0:F4} pixels
|
||||
|
||||
{1}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusFailed" xml:space="preserve">
|
||||
<value>Calibration failed: {0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusSaved" xml:space="preserve">
|
||||
<value>Calibration saved:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusLoaded" xml:space="preserve">
|
||||
<value>Calibration loaded:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusUndistorted" xml:space="preserve">
|
||||
<value>Image undistorted:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusImageError" xml:space="preserve">
|
||||
<value>Image {0}
|
||||
Reprojection error: {1:F4} pixels</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressPreparing" xml:space="preserve">
|
||||
<value>Preparing calibration...</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressDetecting" xml:space="preserve">
|
||||
<value>Detecting corners ({0}/{1})</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressCalibrating" xml:space="preserve">
|
||||
<value>Performing camera calibration...</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressCalculating" xml:space="preserve">
|
||||
<value>Calculating reprojection errors ({0}/{1})</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressComplete" xml:space="preserve">
|
||||
<value>Calibration complete</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressFailed" xml:space="preserve">
|
||||
<value>Calibration failed</value>
|
||||
</data>
|
||||
<data name="ChessboardErrorMinImages" xml:space="preserve">
|
||||
<value>At least 3 images required!</value>
|
||||
</data>
|
||||
<data name="ChessboardErrorInsufficientValid" xml:space="preserve">
|
||||
<value>Insufficient valid images, need at least 3, current {0}</value>
|
||||
</data>
|
||||
<data name="ChessboardSaveSuccess" xml:space="preserve">
|
||||
<value>Save successful!</value>
|
||||
</data>
|
||||
<data name="ChessboardLoadSuccess" xml:space="preserve">
|
||||
<value>Load successful!</value>
|
||||
</data>
|
||||
<data name="ChessboardCalibrationComplete" xml:space="preserve">
|
||||
<value>Calibration completed!</value>
|
||||
</data>
|
||||
<data name="ChessboardImageError" xml:space="preserve">
|
||||
<value>Image{0}: {1:F4} pixels</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,259 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<!-- 九点标定 -->
|
||||
<data name="CalibrationToolTitle" xml:space="preserve">
|
||||
<value>九点标定工具</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadImage" xml:space="preserve">
|
||||
<value>加载图像</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadCsv" xml:space="preserve">
|
||||
<value>从CSV加载</value>
|
||||
</data>
|
||||
<data name="CalibrationExecute" xml:space="preserve">
|
||||
<value>执行标定</value>
|
||||
</data>
|
||||
<data name="CalibrationSave" xml:space="preserve">
|
||||
<value>保存标定</value>
|
||||
</data>
|
||||
<data name="CalibrationLoad" xml:space="preserve">
|
||||
<value>加载标定</value>
|
||||
</data>
|
||||
<data name="CalibrationShowWorld" xml:space="preserve">
|
||||
<value>显示世界坐标</value>
|
||||
</data>
|
||||
<data name="CalibrationPointList" xml:space="preserve">
|
||||
<value>标定点列表</value>
|
||||
</data>
|
||||
<data name="CalibrationPixelX" xml:space="preserve">
|
||||
<value>像素X</value>
|
||||
</data>
|
||||
<data name="CalibrationPixelY" xml:space="preserve">
|
||||
<value>像素Y</value>
|
||||
</data>
|
||||
<data name="CalibrationWorldX" xml:space="preserve">
|
||||
<value>世界X</value>
|
||||
</data>
|
||||
<data name="CalibrationWorldY" xml:space="preserve">
|
||||
<value>世界Y</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusReady" xml:space="preserve">
|
||||
<value>就绪</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusImageLoaded" xml:space="preserve">
|
||||
<value>状态:图像已加载
|
||||
{0}
|
||||
右键点击图像查看坐标转换</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusCsvLoaded" xml:space="preserve">
|
||||
<value>状态:已从CSV加载 {0} 个标定点
|
||||
{1}</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusSuccess" xml:space="preserve">
|
||||
<value>状态:标定成功!使用 {0} 个点</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusFailed" xml:space="preserve">
|
||||
<value>状态:标定失败</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusSaved" xml:space="preserve">
|
||||
<value>状态:标定文件已保存到
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="CalibrationStatusLoaded" xml:space="preserve">
|
||||
<value>状态:标定文件已加载
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="CalibrationCoordinates" xml:space="preserve">
|
||||
<value>像素坐标: ({0:F2}, {1:F2})
|
||||
世界坐标: ({2:F2}, {3:F2})</value>
|
||||
</data>
|
||||
<data name="CalibrationErrorMinPoints" xml:space="preserve">
|
||||
<value>至少需要4个标定点!</value>
|
||||
</data>
|
||||
<data name="CalibrationSuccessTitle" xml:space="preserve">
|
||||
<value>成功</value>
|
||||
</data>
|
||||
<data name="CalibrationSuccessMessage" xml:space="preserve">
|
||||
<value>标定完成!</value>
|
||||
</data>
|
||||
<data name="CalibrationSaveSuccess" xml:space="preserve">
|
||||
<value>保存成功!</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadSuccess" xml:space="preserve">
|
||||
<value>加载成功!</value>
|
||||
</data>
|
||||
<data name="CalibrationLoadFailed" xml:space="preserve">
|
||||
<value>加载失败!</value>
|
||||
</data>
|
||||
<!-- 棋盘格标定 -->
|
||||
<data name="ChessboardToolTitle" xml:space="preserve">
|
||||
<value>棋盘格畸变校正工具</value>
|
||||
</data>
|
||||
<data name="ChessboardAddImages" xml:space="preserve">
|
||||
<value>添加图像</value>
|
||||
</data>
|
||||
<data name="ChessboardClearImages" xml:space="preserve">
|
||||
<value>清空列表</value>
|
||||
</data>
|
||||
<data name="ChessboardCalibrate" xml:space="preserve">
|
||||
<value>执行标定</value>
|
||||
</data>
|
||||
<data name="ChessboardSave" xml:space="preserve">
|
||||
<value>保存标定</value>
|
||||
</data>
|
||||
<data name="ChessboardLoad" xml:space="preserve">
|
||||
<value>加载标定</value>
|
||||
</data>
|
||||
<data name="ChessboardUndistort" xml:space="preserve">
|
||||
<value>校正图像</value>
|
||||
</data>
|
||||
<data name="ChessboardParameters" xml:space="preserve">
|
||||
<value>棋盘格参数</value>
|
||||
</data>
|
||||
<data name="ChessboardWidth" xml:space="preserve">
|
||||
<value>内角点宽度:</value>
|
||||
</data>
|
||||
<data name="ChessboardHeight" xml:space="preserve">
|
||||
<value>内角点高度:</value>
|
||||
</data>
|
||||
<data name="ChessboardSquareSize" xml:space="preserve">
|
||||
<value>方格尺寸(mm):</value>
|
||||
</data>
|
||||
<data name="ChessboardImageList" xml:space="preserve">
|
||||
<value>标定图像列表</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusInfo" xml:space="preserve">
|
||||
<value>状态信息</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusReady" xml:space="preserve">
|
||||
<value>就绪</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusAdded" xml:space="preserve">
|
||||
<value>已添加 {0} 张图像</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusCleared" xml:space="preserve">
|
||||
<value>已清空图像列表</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusCalibrating" xml:space="preserve">
|
||||
<value>正在标定,请稍候...</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusSuccess" xml:space="preserve">
|
||||
<value>标定成功!
|
||||
总体重投影误差: {0:F4} 像素
|
||||
|
||||
{1}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusFailed" xml:space="preserve">
|
||||
<value>标定失败: {0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusSaved" xml:space="preserve">
|
||||
<value>标定已保存:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusLoaded" xml:space="preserve">
|
||||
<value>标定已加载:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusUndistorted" xml:space="preserve">
|
||||
<value>已校正图像:
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="ChessboardStatusImageError" xml:space="preserve">
|
||||
<value>图像 {0}
|
||||
重投影误差: {1:F4} 像素</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressPreparing" xml:space="preserve">
|
||||
<value>准备标定...</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressDetecting" xml:space="preserve">
|
||||
<value>检测角点 ({0}/{1})</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressCalibrating" xml:space="preserve">
|
||||
<value>执行相机标定...</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressCalculating" xml:space="preserve">
|
||||
<value>计算重投影误差 ({0}/{1})</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressComplete" xml:space="preserve">
|
||||
<value>标定完成</value>
|
||||
</data>
|
||||
<data name="ChessboardProgressFailed" xml:space="preserve">
|
||||
<value>标定失败</value>
|
||||
</data>
|
||||
<data name="ChessboardErrorMinImages" xml:space="preserve">
|
||||
<value>至少需要3张图像!</value>
|
||||
</data>
|
||||
<data name="ChessboardErrorInsufficientValid" xml:space="preserve">
|
||||
<value>有效图像不足,需要至少3张,当前{0}张</value>
|
||||
</data>
|
||||
<data name="ChessboardSaveSuccess" xml:space="preserve">
|
||||
<value>保存成功!</value>
|
||||
</data>
|
||||
<data name="ChessboardLoadSuccess" xml:space="preserve">
|
||||
<value>加载成功!</value>
|
||||
</data>
|
||||
<data name="ChessboardCalibrationComplete" xml:space="preserve">
|
||||
<value>标定完成!</value>
|
||||
</data>
|
||||
<data name="ChessboardImageError" xml:space="preserve">
|
||||
<value>图像{0}: {1:F4} 像素</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,158 @@
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Serilog;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Res = XP.Camera.Calibration.Resources.CalibrationResources;
|
||||
|
||||
namespace XP.Camera.Calibration.ViewModels;
|
||||
|
||||
public class CalibrationViewModel : BindableBase
|
||||
{
|
||||
private readonly ICalibrationDialogService _dialogService;
|
||||
private readonly CalibrationProcessor _calibrator = new();
|
||||
private Image<Bgr, byte>? _currentImage;
|
||||
private static readonly ILogger _logger = Log.ForContext<CalibrationViewModel>();
|
||||
private BitmapSource? _imageSource;
|
||||
private string _statusText = Res.CalibrationStatusReady;
|
||||
private bool _showWorldCoordinates;
|
||||
|
||||
public CalibrationViewModel(ICalibrationDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
CalibrationPoints = new ObservableCollection<CalibrationProcessor.CalibrationPoint>();
|
||||
|
||||
LoadImageCommand = new DelegateCommand(LoadImage);
|
||||
LoadCsvCommand = new DelegateCommand(LoadCsv);
|
||||
CalibrateCommand = new DelegateCommand(Calibrate, CanCalibrate)
|
||||
.ObservesProperty(() => CalibrationPoints.Count);
|
||||
SaveCalibrationCommand = new DelegateCommand(SaveCalibration);
|
||||
LoadCalibrationCommand = new DelegateCommand(LoadCalibration);
|
||||
}
|
||||
|
||||
public ObservableCollection<CalibrationProcessor.CalibrationPoint> CalibrationPoints { get; }
|
||||
|
||||
public BitmapSource? ImageSource
|
||||
{
|
||||
get => _imageSource;
|
||||
set => SetProperty(ref _imageSource, value);
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
public bool ShowWorldCoordinates
|
||||
{
|
||||
get => _showWorldCoordinates;
|
||||
set => SetProperty(ref _showWorldCoordinates, value);
|
||||
}
|
||||
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand LoadCsvCommand { get; }
|
||||
public DelegateCommand CalibrateCommand { get; }
|
||||
public DelegateCommand SaveCalibrationCommand { get; }
|
||||
public DelegateCommand LoadCalibrationCommand { get; }
|
||||
|
||||
private void LoadImage()
|
||||
{
|
||||
_logger.Information("Loading image file");
|
||||
var fileName = _dialogService.ShowOpenFileDialog("图像文件|*.jpg;*.png;*.bmp;*.tif");
|
||||
if (fileName == null) return;
|
||||
|
||||
_currentImage = new Image<Bgr, byte>(fileName);
|
||||
ImageSource = MatToBitmapSource(_currentImage.Mat);
|
||||
StatusText = string.Format(Res.CalibrationStatusImageLoaded, fileName);
|
||||
RaiseEvent(ImageLoadedRequested);
|
||||
}
|
||||
|
||||
private void LoadCsv()
|
||||
{
|
||||
var fileName = _dialogService.ShowOpenFileDialog("CSV文件|*.csv|所有文件|*.*");
|
||||
if (fileName == null) return;
|
||||
|
||||
var points = _calibrator.LoadPointsFromCsv(fileName);
|
||||
CalibrationPoints.Clear();
|
||||
foreach (var pt in points)
|
||||
CalibrationPoints.Add(pt);
|
||||
|
||||
StatusText = string.Format(Res.CalibrationStatusCsvLoaded, CalibrationPoints.Count, fileName);
|
||||
}
|
||||
|
||||
private bool CanCalibrate() => CalibrationPoints.Count >= 4;
|
||||
|
||||
private void Calibrate()
|
||||
{
|
||||
if (CalibrationPoints.Count < 4)
|
||||
{
|
||||
_dialogService.ShowError(Res.CalibrationErrorMinPoints, Res.CalibrationSuccessTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_calibrator.Calibrate(new List<CalibrationProcessor.CalibrationPoint>(CalibrationPoints)))
|
||||
{
|
||||
StatusText = string.Format(Res.CalibrationStatusSuccess, CalibrationPoints.Count);
|
||||
_dialogService.ShowInfo(Res.CalibrationSuccessMessage, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = Res.CalibrationStatusFailed;
|
||||
_dialogService.ShowError(Res.CalibrationStatusFailed, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCalibration()
|
||||
{
|
||||
var fileName = _dialogService.ShowSaveFileDialog("标定文件|*.json", "calibration.json");
|
||||
if (fileName == null) return;
|
||||
|
||||
_calibrator.SaveCalibration(fileName, new List<CalibrationProcessor.CalibrationPoint>(CalibrationPoints));
|
||||
StatusText = string.Format(Res.CalibrationStatusSaved, fileName);
|
||||
_dialogService.ShowInfo(Res.CalibrationSaveSuccess, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
|
||||
private void LoadCalibration()
|
||||
{
|
||||
var fileName = _dialogService.ShowOpenFileDialog("标定文件|*.json");
|
||||
if (fileName == null) return;
|
||||
|
||||
if (_calibrator.LoadCalibration(fileName))
|
||||
{
|
||||
StatusText = string.Format(Res.CalibrationStatusLoaded, fileName);
|
||||
_dialogService.ShowInfo(Res.CalibrationLoadSuccess, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowError(Res.CalibrationLoadFailed, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
}
|
||||
|
||||
public PointF ConvertPixelToWorld(PointF pixel) => _calibrator.PixelToWorld(pixel);
|
||||
|
||||
public Image<Bgr, byte>? CurrentImage => _currentImage;
|
||||
|
||||
public event EventHandler? ImageLoadedRequested;
|
||||
|
||||
private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
private static BitmapSource MatToBitmapSource(Mat mat)
|
||||
{
|
||||
using var bitmap = mat.ToBitmap();
|
||||
var hBitmap = bitmap.GetHbitmap();
|
||||
try
|
||||
{
|
||||
return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
|
||||
hBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteObject(hBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
|
||||
private static extern bool DeleteObject(IntPtr hObject);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using Emgu.CV;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Res = XP.Camera.Calibration.Resources.CalibrationResources;
|
||||
|
||||
namespace XP.Camera.Calibration.ViewModels;
|
||||
|
||||
public class ChessboardCalibrationViewModel : BindableBase
|
||||
{
|
||||
private readonly ICalibrationDialogService _dialogService;
|
||||
private readonly ChessboardCalibrator _calibrator = new();
|
||||
private readonly ObservableCollection<string> _imagePaths = new();
|
||||
|
||||
private BitmapSource? _imageSource;
|
||||
private string _statusText = Res.ChessboardStatusReady;
|
||||
private int _chessboardWidth = 11;
|
||||
private int _chessboardHeight = 8;
|
||||
private float _squareSize = 15;
|
||||
private int _selectedImageIndex = -1;
|
||||
private bool _isCalibrating = false;
|
||||
private double _progressValue = 0;
|
||||
private string _progressText = "";
|
||||
|
||||
public ChessboardCalibrationViewModel(ICalibrationDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
ImageFileNames = new ObservableCollection<string>();
|
||||
|
||||
AddImagesCommand = new DelegateCommand(AddImages);
|
||||
ClearImagesCommand = new DelegateCommand(ClearImages, CanClearImages)
|
||||
.ObservesProperty(() => ImageFileNames.Count);
|
||||
CalibrateCommand = new DelegateCommand(async () => await CalibrateAsync(), CanCalibrate)
|
||||
.ObservesProperty(() => ImageFileNames.Count)
|
||||
.ObservesProperty(() => IsCalibrating);
|
||||
SaveCalibrationCommand = new DelegateCommand(SaveCalibration);
|
||||
LoadCalibrationCommand = new DelegateCommand(LoadCalibration);
|
||||
UndistortImageCommand = new DelegateCommand(UndistortImage);
|
||||
|
||||
_calibrator.ProgressChanged += OnCalibrationProgressChanged;
|
||||
}
|
||||
|
||||
public ObservableCollection<string> ImageFileNames { get; }
|
||||
public BitmapSource? ImageSource { get => _imageSource; set => SetProperty(ref _imageSource, value); }
|
||||
public string StatusText { get => _statusText; set => SetProperty(ref _statusText, value); }
|
||||
public int ChessboardWidth { get => _chessboardWidth; set => SetProperty(ref _chessboardWidth, value); }
|
||||
public int ChessboardHeight { get => _chessboardHeight; set => SetProperty(ref _chessboardHeight, value); }
|
||||
public float SquareSize { get => _squareSize; set => SetProperty(ref _squareSize, value); }
|
||||
|
||||
public int SelectedImageIndex
|
||||
{
|
||||
get => _selectedImageIndex;
|
||||
set { if (SetProperty(ref _selectedImageIndex, value) && value >= 0) LoadSelectedImage(value); }
|
||||
}
|
||||
|
||||
public bool IsCalibrating { get => _isCalibrating; set => SetProperty(ref _isCalibrating, value); }
|
||||
public double ProgressValue { get => _progressValue; set => SetProperty(ref _progressValue, value); }
|
||||
public string ProgressText { get => _progressText; set => SetProperty(ref _progressText, value); }
|
||||
|
||||
public DelegateCommand AddImagesCommand { get; }
|
||||
public DelegateCommand ClearImagesCommand { get; }
|
||||
public DelegateCommand CalibrateCommand { get; }
|
||||
public DelegateCommand SaveCalibrationCommand { get; }
|
||||
public DelegateCommand LoadCalibrationCommand { get; }
|
||||
public DelegateCommand UndistortImageCommand { get; }
|
||||
|
||||
private void AddImages()
|
||||
{
|
||||
var fileNames = _dialogService.ShowOpenMultipleFilesDialog("图像文件|*.jpg;*.png;*.bmp;*.tif");
|
||||
if (fileNames == null) return;
|
||||
foreach (var file in fileNames)
|
||||
{
|
||||
_imagePaths.Add(file);
|
||||
ImageFileNames.Add(Path.GetFileName(file));
|
||||
}
|
||||
StatusText = string.Format(Res.ChessboardStatusAdded, _imagePaths.Count);
|
||||
}
|
||||
|
||||
private bool CanClearImages() => ImageFileNames.Count > 0;
|
||||
|
||||
private void ClearImages()
|
||||
{
|
||||
_imagePaths.Clear();
|
||||
ImageFileNames.Clear();
|
||||
ImageSource = null;
|
||||
StatusText = Res.ChessboardStatusCleared;
|
||||
RaiseEvent(ImageClearedRequested);
|
||||
}
|
||||
|
||||
private bool CanCalibrate() => ImageFileNames.Count >= 3 && !IsCalibrating;
|
||||
|
||||
private void OnCalibrationProgressChanged(int current, int total, string message)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ProgressValue = (double)current / total * 100;
|
||||
if (message.Contains("检测角点"))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(message, @"\((\d+)/(\d+)\)");
|
||||
ProgressText = match.Success
|
||||
? string.Format(Res.ChessboardProgressDetecting, match.Groups[1].Value, match.Groups[2].Value)
|
||||
: message;
|
||||
}
|
||||
else if (message.Contains("执行相机标定"))
|
||||
ProgressText = Res.ChessboardProgressCalibrating;
|
||||
else if (message.Contains("计算重投影误差"))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(message, @"\((\d+)/(\d+)\)");
|
||||
ProgressText = match.Success
|
||||
? string.Format(Res.ChessboardProgressCalculating, match.Groups[1].Value, match.Groups[2].Value)
|
||||
: message;
|
||||
}
|
||||
else if (message.Contains("标定完成"))
|
||||
ProgressText = Res.ChessboardProgressComplete;
|
||||
else if (message.Contains("标定失败"))
|
||||
ProgressText = Res.ChessboardProgressFailed;
|
||||
else
|
||||
ProgressText = message;
|
||||
});
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task CalibrateAsync()
|
||||
{
|
||||
if (_imagePaths.Count < 3)
|
||||
{
|
||||
_dialogService.ShowError(Res.ChessboardErrorMinImages, Res.ChessboardCalibrationComplete);
|
||||
return;
|
||||
}
|
||||
|
||||
IsCalibrating = true;
|
||||
ProgressValue = 0;
|
||||
ProgressText = Res.ChessboardProgressPreparing;
|
||||
StatusText = Res.ChessboardStatusCalibrating;
|
||||
|
||||
try
|
||||
{
|
||||
await System.Threading.Tasks.Task.Run(() =>
|
||||
{
|
||||
if (_calibrator.CalibrateFromImages(new List<string>(_imagePaths), ChessboardWidth, ChessboardHeight, SquareSize, out string error))
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var imageErrors = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < _calibrator.PerImageErrors.Count; i++)
|
||||
imageErrors.AppendLine(string.Format(Res.ChessboardImageError, i + 1, _calibrator.PerImageErrors[i]));
|
||||
StatusText = string.Format(Res.ChessboardStatusSuccess, _calibrator.ReprojectionError, imageErrors.ToString());
|
||||
_dialogService.ShowInfo(Res.ChessboardCalibrationComplete, Res.ChessboardSaveSuccess);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText = string.Format(Res.ChessboardStatusFailed, error);
|
||||
_dialogService.ShowError(error, Res.ChessboardCalibrationComplete);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsCalibrating = false;
|
||||
ProgressValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCalibration()
|
||||
{
|
||||
var fileName = _dialogService.ShowSaveFileDialog("标定文件|*.json", "camera_calibration.json");
|
||||
if (fileName == null) return;
|
||||
try
|
||||
{
|
||||
_calibrator.SaveCalibration(fileName);
|
||||
StatusText = string.Format(Res.ChessboardStatusSaved, fileName);
|
||||
_dialogService.ShowInfo(Res.ChessboardSaveSuccess, Res.ChessboardCalibrationComplete);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowError($"保存失败: {ex.Message}", Res.ChessboardCalibrationComplete);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCalibration()
|
||||
{
|
||||
var fileName = _dialogService.ShowOpenFileDialog("标定文件|*.json");
|
||||
if (fileName == null) return;
|
||||
if (_calibrator.LoadCalibration(fileName))
|
||||
{
|
||||
StatusText = string.Format(Res.ChessboardStatusLoaded, fileName);
|
||||
_dialogService.ShowInfo(Res.ChessboardLoadSuccess, Res.ChessboardCalibrationComplete);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowError(Res.CalibrationLoadFailed, Res.ChessboardCalibrationComplete);
|
||||
}
|
||||
}
|
||||
|
||||
private void UndistortImage()
|
||||
{
|
||||
var fileName = _dialogService.ShowOpenFileDialog("图像文件|*.jpg;*.png;*.bmp;*.tif");
|
||||
if (fileName == null) return;
|
||||
using var image = CvInvoke.Imread(fileName);
|
||||
var undistorted = _calibrator.UndistortImage(image);
|
||||
ImageSource = MatToBitmapSource(undistorted);
|
||||
StatusText = string.Format(Res.ChessboardStatusUndistorted, Path.GetFileName(fileName));
|
||||
RaiseEvent(ImageLoadedRequested);
|
||||
}
|
||||
|
||||
private void LoadSelectedImage(int index)
|
||||
{
|
||||
if (index < 0 || index >= _imagePaths.Count) return;
|
||||
var img = _calibrator.DrawChessboardCorners(_imagePaths[index], ChessboardWidth, ChessboardHeight);
|
||||
if (img != null)
|
||||
{
|
||||
ImageSource = MatToBitmapSource(img);
|
||||
RaiseEvent(ImageLoadedRequested);
|
||||
}
|
||||
if (_calibrator.PerImageErrors.Count > index)
|
||||
StatusText = string.Format(Res.ChessboardStatusImageError, index + 1, _calibrator.PerImageErrors[index]);
|
||||
}
|
||||
|
||||
public event EventHandler? ImageLoadedRequested;
|
||||
public event EventHandler? ImageClearedRequested;
|
||||
|
||||
private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
private static BitmapSource MatToBitmapSource(Mat mat)
|
||||
{
|
||||
using var bitmap = mat.ToBitmap();
|
||||
var hBitmap = bitmap.GetHbitmap();
|
||||
try
|
||||
{
|
||||
return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
|
||||
hBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteObject(hBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
|
||||
private static extern bool DeleteObject(IntPtr hObject);
|
||||
}
|
||||
+97
-124
@@ -1,6 +1,6 @@
|
||||
# XP.Camera 使用说明
|
||||
|
||||
基于 .NET 8 WPF 的工业相机控制类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
|
||||
基于 .NET 8 WPF 的工业相机控制与标定类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
|
||||
|
||||
## 环境要求
|
||||
|
||||
@@ -12,16 +12,36 @@
|
||||
|
||||
```
|
||||
XP.Camera/
|
||||
├── ICameraController.cs # 控制器接口 + 工厂接口
|
||||
├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
|
||||
├── BaslerCameraController.cs # Basler 实现
|
||||
├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
|
||||
├── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
|
||||
├── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换工具
|
||||
├── Core/ # 相机核心抽象
|
||||
│ ├── ICameraController.cs # 控制器接口 + 工厂接口
|
||||
│ ├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
|
||||
│ ├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
|
||||
│ └── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
|
||||
├── Basler/ # Basler 品牌实现
|
||||
│ └── BaslerCameraController.cs # Basler pylon SDK 实现
|
||||
├── Converters/ # 数据转换工具
|
||||
│ └── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换
|
||||
├── Calibration/ # 相机标定模块
|
||||
│ ├── CalibrationProcessor.cs # 九点标定(DLT 单应性矩阵,像素→世界坐标)
|
||||
│ ├── ChessboardCalibrator.cs # 棋盘格标定(Zhang's 方法,内参 + 畸变校正)
|
||||
│ ├── IDialogService.cs # ICalibrationDialogService 接口
|
||||
│ ├── DefaultCalibrationDialogService.cs # 默认实现(标准 WPF MessageBox)
|
||||
│ ├── CalibrationLocalizedStrings.cs # XAML 本地化绑定辅助
|
||||
│ ├── Controls/ # 标定 UI 控件(UserControl)
|
||||
│ │ ├── CalibrationControl.xaml/.cs # 九点标定界面
|
||||
│ │ ├── ChessboardCalibrationControl.xaml/.cs # 棋盘格标定界面
|
||||
│ │ └── ImageCanvasControl.xaml/.cs # 图像画布(缩放/平移)
|
||||
│ ├── ViewModels/ # 标定视图模型
|
||||
│ │ ├── CalibrationViewModel.cs
|
||||
│ │ └── ChessboardCalibrationViewModel.cs
|
||||
│ └── Resources/ # 本地化资源
|
||||
│ ├── CalibrationResources.resx # 中文(默认)
|
||||
│ ├── CalibrationResources.en-US.resx # 英文
|
||||
│ └── CalibrationResources.Designer.cs
|
||||
└── XP.Camera.csproj
|
||||
```
|
||||
|
||||
所有类型统一在 `XP.Camera` 命名空间下。
|
||||
所有相机核心类型在 `XP.Camera` 命名空间下,标定模块在 `XP.Camera.Calibration` 命名空间下。
|
||||
|
||||
## 项目引用
|
||||
|
||||
@@ -48,27 +68,12 @@ Console.WriteLine($"已连接: {info.ModelName} (SN: {info.SerialNumber})");
|
||||
在 Prism / DI 容器中注册:
|
||||
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
var config = AppConfig.Load();
|
||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||
new CameraFactory().CreateController(config.CameraType));
|
||||
```
|
||||
|
||||
ViewModel 中注入使用:
|
||||
|
||||
```csharp
|
||||
public class MyViewModel
|
||||
{
|
||||
private readonly ICameraController _camera;
|
||||
|
||||
public MyViewModel(ICameraController camera)
|
||||
{
|
||||
_camera = camera;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
相机品牌通过配置文件 `config.json` 指定:
|
||||
|
||||
```json
|
||||
@@ -82,60 +87,85 @@ public class MyViewModel
|
||||
```csharp
|
||||
_camera.ImageGrabbed += (s, e) =>
|
||||
{
|
||||
// PixelConverter 返回已 Freeze 的 BitmapSource,可跨线程传递
|
||||
var bitmap = PixelConverter.ToBitmapSource(
|
||||
e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
CameraImageSource = bitmap;
|
||||
});
|
||||
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
|
||||
};
|
||||
```
|
||||
|
||||
XAML 绑定:
|
||||
|
||||
```xml
|
||||
<Image Source="{Binding CameraImageSource}" Stretch="Uniform" />
|
||||
```
|
||||
|
||||
### 4. 软件触发采集流程
|
||||
|
||||
```csharp
|
||||
camera.Open();
|
||||
camera.SetExposureTime(10000); // 10ms
|
||||
camera.StartGrabbing();
|
||||
|
||||
// 每次需要采集时调用(结果通过 ImageGrabbed 事件返回)
|
||||
camera.ExecuteSoftwareTrigger();
|
||||
|
||||
camera.StopGrabbing();
|
||||
camera.Close();
|
||||
```
|
||||
|
||||
### 5. 实时连续采集(链式触发)
|
||||
### 5. 使用标定模块
|
||||
|
||||
收到上一帧后立即触发下一帧,自动适配任何帧率:
|
||||
标定模块完全自包含,可独立使用,无需外部依赖。
|
||||
|
||||
#### 棋盘格标定(相机内参 + 畸变校正)
|
||||
|
||||
```csharp
|
||||
private volatile bool _liveViewRunning;
|
||||
using XP.Camera.Calibration;
|
||||
using XP.Camera.Calibration.ViewModels;
|
||||
using XP.Camera.Calibration.Controls;
|
||||
|
||||
_camera.ImageGrabbed += (s, e) =>
|
||||
// 使用默认对话框服务(标准 WPF MessageBox)
|
||||
var dialogService = new DefaultCalibrationDialogService();
|
||||
var viewModel = new ChessboardCalibrationViewModel(dialogService);
|
||||
|
||||
var window = new Window
|
||||
{
|
||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
|
||||
|
||||
if (_liveViewRunning)
|
||||
_camera.ExecuteSoftwareTrigger(); // 链式触发下一帧
|
||||
Title = "棋盘格标定",
|
||||
Width = 1600, Height = 900,
|
||||
Content = new ChessboardCalibrationControl { DataContext = viewModel }
|
||||
};
|
||||
window.ShowDialog();
|
||||
```
|
||||
|
||||
// 启动实时
|
||||
_camera.StartGrabbing();
|
||||
_liveViewRunning = true;
|
||||
_camera.ExecuteSoftwareTrigger(); // 触发第一帧
|
||||
#### 九点标定(像素→世界坐标)
|
||||
|
||||
// 停止实时
|
||||
_liveViewRunning = false;
|
||||
```csharp
|
||||
var dialogService = new DefaultCalibrationDialogService();
|
||||
var viewModel = new CalibrationViewModel(dialogService);
|
||||
|
||||
var window = new Window
|
||||
{
|
||||
Title = "九点标定",
|
||||
Width = 1400, Height = 850,
|
||||
Content = new CalibrationControl { DataContext = viewModel }
|
||||
};
|
||||
window.ShowDialog();
|
||||
```
|
||||
|
||||
#### 纯算法调用(不使用 UI)
|
||||
|
||||
```csharp
|
||||
// 棋盘格标定
|
||||
var calibrator = new ChessboardCalibrator();
|
||||
calibrator.CalibrateFromImages(imagePaths, boardWidth: 11, boardHeight: 8, squareSize: 15f, out string error);
|
||||
calibrator.SaveCalibration("camera_calibration.json");
|
||||
|
||||
// 九点标定
|
||||
var processor = new CalibrationProcessor();
|
||||
processor.Calibrate(points);
|
||||
var worldPoint = processor.PixelToWorld(new PointF(100, 200));
|
||||
```
|
||||
|
||||
#### 自定义对话框服务
|
||||
|
||||
如需自定义弹框样式,实现 `ICalibrationDialogService` 接口即可:
|
||||
|
||||
```csharp
|
||||
public class MyDialogService : ICalibrationDialogService
|
||||
{
|
||||
// 实现所有接口方法,使用自定义 UI 组件...
|
||||
}
|
||||
```
|
||||
|
||||
## 核心接口
|
||||
@@ -149,94 +179,37 @@ _liveViewRunning = false;
|
||||
| `StartGrabbing()` | 以软件触发模式启动采集 |
|
||||
| `ExecuteSoftwareTrigger()` | 触发一帧采集 |
|
||||
| `StopGrabbing()` | 停止采集 |
|
||||
| `Get/SetExposureTime` | 曝光时间(微秒) |
|
||||
| `Get/SetGain` | 增益值 |
|
||||
| `Get/SetWidth/Height` | 图像尺寸 |
|
||||
| `Get/SetPixelFormat` | 像素格式(Mono8 / BGR8 / BGRA8) |
|
||||
|
||||
### 参数读写
|
||||
### 事件
|
||||
|
||||
| 方法 | 说明 |
|
||||
| 事件 | 说明 |
|
||||
|------|------|
|
||||
| `Get/SetExposureTime(double)` | 曝光时间(微秒) |
|
||||
| `Get/SetGain(double)` | 增益值 |
|
||||
| `Get/SetWidth(int)` | 图像宽度(自动校正到有效值) |
|
||||
| `Get/SetHeight(int)` | 图像高度(自动校正到有效值) |
|
||||
| `Get/SetPixelFormat(string)` | 像素格式(Mono8 / BGR8 / BGRA8) |
|
||||
| `ImageGrabbed` | 成功采集一帧图像 |
|
||||
| `GrabError` | 图像采集失败 |
|
||||
| `ConnectionLost` | 相机连接意外断开 |
|
||||
|
||||
### ICameraFactory
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `CreateController(string cameraType)` | 根据品牌名创建控制器 |
|
||||
|
||||
当前支持的 `cameraType` 值:`"Basler"`
|
||||
|
||||
## 事件
|
||||
|
||||
| 事件 | 说明 | 触发线程 |
|
||||
|------|------|----------|
|
||||
| `ImageGrabbed` | 成功采集一帧图像 | StreamGrabber 回调线程 |
|
||||
| `GrabError` | 图像采集失败 | StreamGrabber 回调线程 |
|
||||
| `ConnectionLost` | 相机连接意外断开 | pylon SDK 事件线程 |
|
||||
|
||||
> 所有事件均在非 UI 线程触发。更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
|
||||
> `PixelConverter.ToBitmapSource()` 返回的 BitmapSource 已调用 `Freeze()`,可直接跨线程传递。
|
||||
> 所有事件均在非 UI 线程触发,更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
|
||||
|
||||
## 异常处理
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
camera.Open();
|
||||
}
|
||||
catch (DeviceNotFoundException)
|
||||
{
|
||||
// 无可用相机设备
|
||||
}
|
||||
catch (CameraException ex)
|
||||
{
|
||||
// 其他相机错误,ex.InnerException 包含原始 SDK 异常
|
||||
}
|
||||
```
|
||||
|
||||
| 异常类型 | 场景 |
|
||||
|---------|------|
|
||||
| `DeviceNotFoundException` | 无可用相机 |
|
||||
| `ConnectionLostException` | 相机物理断开 |
|
||||
| `CameraException` | SDK 操作失败(基类) |
|
||||
| `InvalidOperationException` | 未连接时访问参数,未采集时触发 |
|
||||
| `TimeoutException` | 软件触发等待超时 |
|
||||
|
||||
## 扩展其他品牌相机
|
||||
|
||||
1. 实现 `ICameraController` 接口:
|
||||
|
||||
```csharp
|
||||
public class HikvisionCameraController : ICameraController
|
||||
{
|
||||
// 实现所有接口方法...
|
||||
}
|
||||
```
|
||||
|
||||
2. 在 `CameraFactory.cs` 中注册:
|
||||
|
||||
```csharp
|
||||
public ICameraController CreateController(string cameraType)
|
||||
{
|
||||
return cameraType switch
|
||||
{
|
||||
"Basler" => new BaslerCameraController(),
|
||||
"Hikvision" => new HikvisionCameraController(),
|
||||
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
3. 配置文件切换品牌即可,业务代码无需修改。
|
||||
1. 在 `Basler/` 同级创建新文件夹,实现 `ICameraController` 接口
|
||||
2. 在 `CameraFactory.cs` 中注册新品牌
|
||||
3. 配置文件切换品牌即可,业务代码无需修改
|
||||
|
||||
## 线程安全
|
||||
|
||||
- 所有公共方法(Open / Close / StartGrabbing / StopGrabbing / ExecuteSoftwareTrigger / 参数读写)均线程安全
|
||||
- 所有公共方法均线程安全
|
||||
- 事件回调不持有内部锁,不会导致死锁
|
||||
- `Open()` / `Close()` 幂等,重复调用安全
|
||||
|
||||
## 日志
|
||||
|
||||
使用 Serilog 静态 API(`Log.ForContext<T>()`),与宿主应用共享同一个日志管道。宿主应用只需在启动时配置 `Log.Logger` 即可。
|
||||
|
||||
@@ -13,7 +13,24 @@
|
||||
<Reference Include="Basler.Pylon">
|
||||
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
|
||||
</Reference>
|
||||
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
||||
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.10.0.5680" />
|
||||
<PackageReference Include="Emgu.CV.Bitmap" Version="4.10.0.5680" />
|
||||
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" />
|
||||
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
|
||||
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<ProjectReference Include="..\XP.Common\XP.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Calibration\Resources\CalibrationResources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>CalibrationResources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Calibration\Resources\CalibrationResources.en-US.resx">
|
||||
<DependentUpon>CalibrationResources.resx</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<Window x:Class="XP.Common.GeneralForm.Views.HexMessageBox"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Height="240" Width="400"
|
||||
MinHeight="240" MinWidth="400"
|
||||
MaxHeight="240" MaxWidth="400"
|
||||
WindowStartupLocation="Manual"
|
||||
Topmost="True"
|
||||
WindowStyle="None"
|
||||
x:Name="csdMessageBox"
|
||||
Background="White">
|
||||
|
||||
<Window.Resources>
|
||||
<Style x:Key="MenuButton" TargetType="Button">
|
||||
<Setter Property="MinWidth" Value="80" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Background" Value="#0078D7" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="2" Padding="10,5">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#005A9E" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#004275" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border BorderBrush="#CCCCCC" BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image Grid.Column="0" Source="{Binding Icon, ElementName=csdMessageBox}"
|
||||
Width="34" Height="33" Margin="0,0,10,0" VerticalAlignment="Top" />
|
||||
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" MaxHeight="140">
|
||||
<TextBlock Text="{Binding MessageText, ElementName=csdMessageBox}"
|
||||
TextWrapping="Wrap" FontSize="14" VerticalAlignment="Center" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<Rectangle Grid.Row="1" Fill="#CCCCCC" Height="1" />
|
||||
|
||||
<Grid Grid.Row="2" Margin="10">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding MessageBoxButton, ElementName=csdMessageBox}" Value="{x:Static MessageBoxButton.YesNo}">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<Button Content="Yes" Style="{StaticResource MenuButton}" Margin="0,0,10,0" Click="Yes_Click" />
|
||||
<Button Content="No" Style="{StaticResource MenuButton}" Click="No_Click" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding MessageBoxButton, ElementName=csdMessageBox}" Value="{x:Static MessageBoxButton.OKCancel}">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<Button Content="OK" Style="{StaticResource MenuButton}" Margin="0,0,10,0" Click="OK_Click" />
|
||||
<Button Content="Cancel" Style="{StaticResource MenuButton}" Click="Cancel_Click" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel HorizontalAlignment="Right">
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding MessageBoxButton, ElementName=csdMessageBox}" Value="{x:Static MessageBoxButton.OK}">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<Button Content="OK" Style="{StaticResource MenuButton}" Click="OK_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XP.Common.GeneralForm.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义消息框,可替代标准 MessageBox
|
||||
/// </summary>
|
||||
public partial class HexMessageBox : Window
|
||||
{
|
||||
public MessageBoxResult Result { get; set; }
|
||||
|
||||
public string MessageText
|
||||
{
|
||||
get => (string)GetValue(MessageTextProperty);
|
||||
set => SetValue(MessageTextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MessageTextProperty =
|
||||
DependencyProperty.Register("MessageText", typeof(string), typeof(HexMessageBox), new PropertyMetadata(""));
|
||||
|
||||
public new ImageSource Icon
|
||||
{
|
||||
get => (ImageSource)GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public new static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register("Icon", typeof(ImageSource), typeof(HexMessageBox), new PropertyMetadata(null));
|
||||
|
||||
public MessageBoxButton MessageBoxButton
|
||||
{
|
||||
get => (MessageBoxButton)GetValue(MessageBoxButtonProperty);
|
||||
set => SetValue(MessageBoxButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MessageBoxButtonProperty =
|
||||
DependencyProperty.Register("MessageBoxButton", typeof(MessageBoxButton), typeof(HexMessageBox), new PropertyMetadata(MessageBoxButton.OK));
|
||||
|
||||
public HexMessageBox(string messageText, MessageBoxButton messageBoxButton, MessageBoxImage image)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Left = (SystemParameters.PrimaryScreenWidth - Width) / 2;
|
||||
Top = (SystemParameters.PrimaryScreenHeight - Height) / 2;
|
||||
|
||||
MessageText = messageText;
|
||||
IntPtr iconHandle = SystemIcons.Information.Handle;
|
||||
|
||||
switch (image)
|
||||
{
|
||||
case MessageBoxImage.Error:
|
||||
iconHandle = SystemIcons.Error.Handle;
|
||||
break;
|
||||
case MessageBoxImage.Question:
|
||||
iconHandle = SystemIcons.Question.Handle;
|
||||
break;
|
||||
case MessageBoxImage.Warning:
|
||||
iconHandle = SystemIcons.Warning.Handle;
|
||||
break;
|
||||
case MessageBoxImage.Information:
|
||||
iconHandle = SystemIcons.Information.Handle;
|
||||
break;
|
||||
}
|
||||
|
||||
Icon = Imaging.CreateBitmapSourceFromHIcon(iconHandle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
|
||||
Icon.Freeze();
|
||||
|
||||
MessageBoxButton = messageBoxButton;
|
||||
Result = MessageBoxResult.None;
|
||||
Topmost = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示模态对话框并返回结果
|
||||
/// </summary>
|
||||
public static MessageBoxResult Show(string messageText, MessageBoxButton messageBoxButton, MessageBoxImage image)
|
||||
{
|
||||
return Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var window = new HexMessageBox(messageText, messageBoxButton, image);
|
||||
window.ShowDialog();
|
||||
return window.Result;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示非模态消息
|
||||
/// </summary>
|
||||
public static HexMessageBox ShowMessage(string messageText, MessageBoxImage image)
|
||||
{
|
||||
HexMessageBox? messageBox = null;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
messageBox = new HexMessageBox(messageText, MessageBoxButton.OK, image);
|
||||
messageBox.Show();
|
||||
});
|
||||
return messageBox!;
|
||||
}
|
||||
|
||||
private void Yes_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.Yes; Close(); }
|
||||
private void No_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.No; Close(); }
|
||||
private void OK_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.OK; Close(); }
|
||||
private void Cancel_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.Cancel; Close(); }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Configuration;
|
||||
using System.Configuration;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Dump.Configs;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Prism.Mvvm;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Prism.Ioc;
|
||||
using Prism.Ioc;
|
||||
using Prism.Modularity;
|
||||
using System.Resources;
|
||||
using XP.Common.Localization;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Scan.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记 INI Key 名称(可选,默认使用属性名)| Marks the INI key name (optional, defaults to property name)
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class IniKeyAttribute : Attribute
|
||||
{
|
||||
public string KeyName { get; }
|
||||
|
||||
public IniKeyAttribute(string keyName)
|
||||
{
|
||||
KeyName = keyName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Scan.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记 INI Section 名称 | Marks the INI section name
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class IniSectionAttribute : Attribute
|
||||
{
|
||||
public string SectionName { get; }
|
||||
|
||||
public IniSectionAttribute(string sectionName)
|
||||
{
|
||||
SectionName = sectionName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
# XP.Scan 扫描配置文件设计文档
|
||||
|
||||
## 1. 需求概述
|
||||
|
||||
在平面CT扫描采集过程中,需要将当前扫描的所有参数信息(项目信息、射线源参数、探测器参数、运动控制参数、扫描配置、校正配置)打包为一个配置对象,最终序列化为 INI 格式文件传递给重构电脑。
|
||||
|
||||
INI 文件结构与立式CT采集配置兼容,包含 6 个 Section:
|
||||
- `[Project_Information]` — 项目基本信息
|
||||
- `[XRay]` — 射线源参数
|
||||
- `[Detector]` — 探测器参数
|
||||
- `[Move_Control]` — 运动控制轴位置
|
||||
- `[Scan_Config]` — 扫描配置
|
||||
- `[Correction_Config]` — 校正参数
|
||||
|
||||
## 2. 设计方案对比
|
||||
|
||||
| 方案 | 优点 | 缺点 | 推荐 |
|
||||
|------|------|------|------|
|
||||
| A. 单个大类 `ScanConfig` | 简单直接 | 字段太多,职责不清 | ✗ |
|
||||
| B. 分组模型 + 序列化服务 | 职责清晰,可测试,可扩展 | 类稍多 | ✓ |
|
||||
| C. Dictionary + 手写序列化 | 灵活 | 无类型安全,易出错 | ✗ |
|
||||
|
||||
**选择方案 B**:用分组模型类映射 INI 的每个 Section,加一个序列化服务负责读写 INI 文件。
|
||||
|
||||
## 3. 数据模型设计
|
||||
|
||||
### 3.1 整体结构
|
||||
|
||||
```
|
||||
ScanConfigData (顶层聚合)
|
||||
├── ProjectInfo → [Project_Information]
|
||||
├── XRayConfig → [XRay]
|
||||
├── DetectorConfig → [Detector]
|
||||
├── MoveControlConfig → [Move_Control]
|
||||
├── ScanSettings → [Scan_Config]
|
||||
└── CorrectionConfig → [Correction_Config]
|
||||
```
|
||||
|
||||
### 3.2 类定义
|
||||
|
||||
```csharp
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描配置数据(顶层聚合,对应完整 INI 文件)
|
||||
/// </summary>
|
||||
public class ScanConfigData
|
||||
{
|
||||
public ProjectInfo ProjectInfo { get; set; } = new();
|
||||
public XRayConfig XRay { get; set; } = new();
|
||||
public DetectorConfig Detector { get; set; } = new();
|
||||
public MoveControlConfig MoveControl { get; set; } = new();
|
||||
public ScanSettings ScanSettings { get; set; } = new();
|
||||
public CorrectionConfig Correction { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### [Project_Information]
|
||||
|
||||
```csharp
|
||||
public class ProjectInfo
|
||||
{
|
||||
/// <summary>图像保存路径</summary>
|
||||
public string FileSave { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>滤波片1</summary>
|
||||
public string Filter1 { get; set; } = "None";
|
||||
|
||||
/// <summary>滤波片2</summary>
|
||||
public string Filter2 { get; set; } = "None";
|
||||
|
||||
/// <summary>项目名称</summary>
|
||||
public string Project { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>样品编号</summary>
|
||||
public string SampleNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>扫描模式名称</summary>
|
||||
public string ScanMode { get; set; } = "QuickScan";
|
||||
}
|
||||
```
|
||||
|
||||
#### [XRay]
|
||||
|
||||
```csharp
|
||||
public class XRayConfig
|
||||
{
|
||||
/// <summary>管电流 (μA)</summary>
|
||||
public int Current_uA { get; set; }
|
||||
|
||||
/// <summary>焦点尺寸</summary>
|
||||
public string Focus { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>管电压 (kV)</summary>
|
||||
public int Voltage_kV { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### [Detector]
|
||||
|
||||
```csharp
|
||||
public class DetectorConfig
|
||||
{
|
||||
/// <summary>帧合并数</summary>
|
||||
public int Det_Avg_Frames { get; set; } = 1;
|
||||
|
||||
/// <summary>Binning 模式</summary>
|
||||
public string Det_Binning { get; set; } = "1*1";
|
||||
|
||||
/// <summary>帧率</summary>
|
||||
public int Det_Frame_rate { get; set; } = 2;
|
||||
|
||||
/// <summary>增益 (PGA)</summary>
|
||||
public int Det_PGA { get; set; } = 6;
|
||||
|
||||
// ROI 参数
|
||||
public int Image_ROI_Height { get; set; }
|
||||
public int Image_ROI_Width { get; set; }
|
||||
public int Image_ROI_xStart { get; set; }
|
||||
public int Image_ROI_xEnd { get; set; }
|
||||
public int Image_ROI_yStart { get; set; }
|
||||
public int Image_ROI_yEnd { get; set; }
|
||||
public int Image_ROI_zStart { get; set; }
|
||||
public int Image_ROI_zEnd { get; set; }
|
||||
|
||||
// 图像尺寸
|
||||
public int Image_Size_Height { get; set; }
|
||||
public int Image_Size_Width { get; set; }
|
||||
|
||||
// 物理尺寸 (mm)
|
||||
public double Physical_Size_X { get; set; }
|
||||
public double Physical_Size_Y { get; set; }
|
||||
|
||||
// 像素尺寸 (mm)
|
||||
public double Pixel_X { get; set; }
|
||||
public double Pixel_Y { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### [Move_Control]
|
||||
|
||||
```csharp
|
||||
public class MoveControlConfig
|
||||
{
|
||||
/// <summary>探测器 X 位置 (mm)</summary>
|
||||
public double DetX { get; set; }
|
||||
|
||||
/// <summary>探测器 Y 位置 (mm)</summary>
|
||||
public double DetY { get; set; }
|
||||
|
||||
/// <summary>探测器 Z 位置 (mm)</summary>
|
||||
public double DetZ { get; set; }
|
||||
|
||||
/// <summary>旋转台角度 (°)</summary>
|
||||
public double Rotation { get; set; }
|
||||
|
||||
/// <summary>样品台 X 位置 (mm) — 即 SOD</summary>
|
||||
public double X { get; set; }
|
||||
|
||||
/// <summary>射线源 Z 位置 (mm)</summary>
|
||||
public double XRAYZ { get; set; }
|
||||
|
||||
/// <summary>样品台 Y 位置 (mm)</summary>
|
||||
public double Y { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### [Scan_Config]
|
||||
|
||||
```csharp
|
||||
public class ScanSettings
|
||||
{
|
||||
/// <summary>采集张数</summary>
|
||||
public int AcquiresNums { get; set; }
|
||||
|
||||
/// <summary>旋转角度 (°)</summary>
|
||||
public double RotateDegree { get; set; }
|
||||
|
||||
/// <summary>扫描模式描述</summary>
|
||||
public string ScanMode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>SDD — 射线源到探测器距离 (mm)</summary>
|
||||
public double SDD { get; set; }
|
||||
|
||||
/// <summary>SOD — 射线源到样品距离 (mm)</summary>
|
||||
public double SOD { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### [Correction_Config]
|
||||
|
||||
```csharp
|
||||
public class CorrectionConfig
|
||||
{
|
||||
/// <summary>探测器水平偏移 (mm)</summary>
|
||||
public double Detector_Horizontal_Offset { get; set; }
|
||||
|
||||
/// <summary>探测器旋转偏移 (°)</summary>
|
||||
public double Detector_Rotation_Offset { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## 4. INI 序列化服务设计
|
||||
|
||||
### 4.1 接口
|
||||
|
||||
```csharp
|
||||
namespace XP.Scan.Services
|
||||
{
|
||||
public interface IScanConfigSerializer
|
||||
{
|
||||
/// <summary>将配置数据序列化为 INI 格式字符串</summary>
|
||||
string Serialize(ScanConfigData config);
|
||||
|
||||
/// <summary>将配置数据写入 INI 文件</summary>
|
||||
void SaveToFile(ScanConfigData config, string filePath);
|
||||
|
||||
/// <summary>从 INI 文件读取配置数据</summary>
|
||||
ScanConfigData LoadFromFile(string filePath);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 实现方案
|
||||
|
||||
不引入第三方 INI 库,手写轻量级序列化,原因:
|
||||
- INI 结构简单固定(6 个 Section,字段已知)
|
||||
- 避免额外 NuGet 依赖
|
||||
- 完全可控,格式与立式CT兼容
|
||||
|
||||
核心思路:用 `[IniSection("Section_Name")]` 和 `[IniKey("Key_Name")]` 特性标注模型属性,序列化时通过反射自动生成 INI 内容。
|
||||
|
||||
### 4.3 特性定义
|
||||
|
||||
```csharp
|
||||
/// <summary>标记 INI Section 名称</summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class IniSectionAttribute : Attribute
|
||||
{
|
||||
public string SectionName { get; }
|
||||
public IniSectionAttribute(string sectionName) => SectionName = sectionName;
|
||||
}
|
||||
|
||||
/// <summary>标记 INI Key 名称(可选,默认用属性名)</summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class IniKeyAttribute : Attribute
|
||||
{
|
||||
public string KeyName { get; }
|
||||
public IniKeyAttribute(string keyName) => KeyName = keyName;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 模型标注示例
|
||||
|
||||
```csharp
|
||||
[IniSection("Project_Information")]
|
||||
public class ProjectInfo
|
||||
{
|
||||
[IniKey("fileSave")]
|
||||
public string FileSave { get; set; } = string.Empty;
|
||||
|
||||
[IniKey("filter1")]
|
||||
public string Filter1 { get; set; } = "None";
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
[IniSection("XRay")]
|
||||
public class XRayConfig
|
||||
{
|
||||
[IniKey("Current_uA")]
|
||||
public int Current_uA { get; set; }
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 序列化输出示例
|
||||
|
||||
```ini
|
||||
[Project_Information]
|
||||
fileSave=D:\HexagonCTData\Test_2026-04-21\Image
|
||||
filter1=Cu 0.2mm
|
||||
filter2=None
|
||||
Project=Test
|
||||
SampleNo=
|
||||
ScanMode=QuickScan
|
||||
|
||||
[XRay]
|
||||
Current_uA=1000
|
||||
Focus=450um
|
||||
Voltage_kV=450
|
||||
|
||||
[Detector]
|
||||
Det_Avg_Frames=1
|
||||
Det_Binning=1*1
|
||||
...
|
||||
```
|
||||
|
||||
## 5. 文件结构规划
|
||||
|
||||
```
|
||||
XP.Scan/
|
||||
├── Models/
|
||||
│ ├── ScanConfigData.cs # 顶层聚合类
|
||||
│ ├── ProjectInfo.cs # [Project_Information]
|
||||
│ ├── XRayConfig.cs # [XRay]
|
||||
│ ├── DetectorConfig.cs # [Detector]
|
||||
│ ├── MoveControlConfig.cs # [Move_Control]
|
||||
│ ├── ScanSettings.cs # [Scan_Config]
|
||||
│ └── CorrectionConfig.cs # [Correction_Config]
|
||||
│
|
||||
├── Attributes/
|
||||
│ ├── IniSectionAttribute.cs # Section 特性
|
||||
│ └── IniKeyAttribute.cs # Key 特性
|
||||
│
|
||||
├── Services/
|
||||
│ ├── IScanConfigSerializer.cs # 序列化接口
|
||||
│ └── ScanConfigSerializer.cs # 序列化实现(反射 + 手写 INI)
|
||||
│
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 6. 使用流程
|
||||
|
||||
```
|
||||
1. 扫描开始前,从各硬件服务收集参数 → 填充 ScanConfigData
|
||||
|
||||
var config = new ScanConfigData();
|
||||
config.XRay.Voltage_kV = raySourceService.CurrentVoltage;
|
||||
config.XRay.Current_uA = raySourceService.CurrentCurrent;
|
||||
config.Detector.Det_Avg_Frames = detectorService.AvgFrames;
|
||||
config.MoveControl.X = motionService.GetPosition(AxisId.StageX);
|
||||
config.ScanSettings.AcquiresNums = acquisitionCount;
|
||||
// ...
|
||||
|
||||
2. 序列化为 INI 文件
|
||||
|
||||
var serializer = new ScanConfigSerializer();
|
||||
serializer.SaveToFile(config, @"D:\HexagonCTData\Test\ScanConfig.ini");
|
||||
|
||||
3. 传递给重构电脑(文件拷贝或网络传输)
|
||||
```
|
||||
|
||||
## 7. 设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 数据模型 | 分组类(每个 Section 一个类) | 职责清晰,属性有类型安全 |
|
||||
| 序列化方式 | 自定义特性 + 反射 | 轻量,无第三方依赖,格式完全可控 |
|
||||
| INI Key 映射 | `[IniKey]` 特性 | 属性名可以用 C# 命名规范,INI Key 保持与立式CT兼容 |
|
||||
| 数值格式 | `InvariantCulture` | 避免不同系统区域设置导致小数点格式不一致 |
|
||||
| 文件编码 | UTF-8 无 BOM | 兼容性最好 |
|
||||
|
||||
## 8. 扩展性
|
||||
|
||||
- 新增 Section:创建新模型类 + 标注 `[IniSection]` + 在 `ScanConfigData` 中添加属性
|
||||
- 新增字段:在对应模型类中添加属性 + 标注 `[IniKey]`
|
||||
- 反序列化:`LoadFromFile` 支持从 INI 文件回读配置(用于加载历史扫描参数)
|
||||
- 验证:可在模型类中添加 `Validate()` 方法,检查参数范围合法性
|
||||
|
||||
---
|
||||
|
||||
**版本:** 1.0
|
||||
**最后更新:** 2026-04-21
|
||||
@@ -0,0 +1,20 @@
|
||||
using XP.Scan.Attributes;
|
||||
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 校正配置 | Correction configuration
|
||||
/// 对应 INI [Correction_Config] Section
|
||||
/// </summary>
|
||||
[IniSection("Correction_Config")]
|
||||
public class CorrectionConfig
|
||||
{
|
||||
/// <summary>探测器水平偏移 (mm) | Detector horizontal offset (mm)</summary>
|
||||
[IniKey("Detector_Horizontal_Offset")]
|
||||
public double DetectorHorizontalOffset { get; set; }
|
||||
|
||||
/// <summary>探测器旋转偏移 (°) | Detector rotation offset (°)</summary>
|
||||
[IniKey("Detector_Rotation_Offset")]
|
||||
public double DetectorRotationOffset { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using XP.Scan.Attributes;
|
||||
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 探测器配置 | Detector configuration
|
||||
/// 对应 INI [Detector] Section
|
||||
/// </summary>
|
||||
[IniSection("Detector")]
|
||||
public class DetectorConfig
|
||||
{
|
||||
/// <summary>帧合并数 | Average frames</summary>
|
||||
[IniKey("Det_Avg_Frames")]
|
||||
public int DetAvgFrames { get; set; } = 1;
|
||||
|
||||
/// <summary>Binning 模式 | Binning mode</summary>
|
||||
[IniKey("Det_Binning")]
|
||||
public string DetBinning { get; set; } = "1*1";
|
||||
|
||||
/// <summary>帧率 | Frame rate</summary>
|
||||
[IniKey("Det_Frame_rate")]
|
||||
public int DetFrameRate { get; set; } = 2;
|
||||
|
||||
/// <summary>增益 (PGA) | Gain (PGA)</summary>
|
||||
[IniKey("Det_PGA")]
|
||||
public int DetPGA { get; set; } = 6;
|
||||
|
||||
/// <summary>ROI 高度 | ROI height</summary>
|
||||
[IniKey("Image_ROI_Height")]
|
||||
public int ImageROIHeight { get; set; }
|
||||
|
||||
/// <summary>ROI 宽度 | ROI width</summary>
|
||||
[IniKey("Image_ROI_Width")]
|
||||
public int ImageROIWidth { get; set; }
|
||||
|
||||
/// <summary>ROI X 起始 | ROI X start</summary>
|
||||
[IniKey("Image_ROI_xStart")]
|
||||
public int ImageROIxStart { get; set; }
|
||||
|
||||
/// <summary>ROI X 结束 | ROI X end</summary>
|
||||
[IniKey("Image_ROI_xEnd")]
|
||||
public int ImageROIxEnd { get; set; }
|
||||
|
||||
/// <summary>ROI Y 起始 | ROI Y start</summary>
|
||||
[IniKey("Image_ROI_yStart")]
|
||||
public int ImageROIyStart { get; set; }
|
||||
|
||||
/// <summary>ROI Y 结束 | ROI Y end</summary>
|
||||
[IniKey("Image_ROI_yEnd")]
|
||||
public int ImageROIyEnd { get; set; }
|
||||
|
||||
/// <summary>ROI Z 起始 | ROI Z start</summary>
|
||||
[IniKey("Image_ROI_zStart")]
|
||||
public int ImageROIzStart { get; set; }
|
||||
|
||||
/// <summary>ROI Z 结束 | ROI Z end</summary>
|
||||
[IniKey("Image_ROI_zEnd")]
|
||||
public int ImageROIzEnd { get; set; }
|
||||
|
||||
/// <summary>图像高度 | Image height</summary>
|
||||
[IniKey("Image_Size_Height")]
|
||||
public int ImageSizeHeight { get; set; }
|
||||
|
||||
/// <summary>图像宽度 | Image width</summary>
|
||||
[IniKey("Image_Size_Width")]
|
||||
public int ImageSizeWidth { get; set; }
|
||||
|
||||
/// <summary>物理尺寸 X (mm) | Physical size X (mm)</summary>
|
||||
[IniKey("Physical_Size_X")]
|
||||
public double PhysicalSizeX { get; set; }
|
||||
|
||||
/// <summary>物理尺寸 Y (mm) | Physical size Y (mm)</summary>
|
||||
[IniKey("Physical_Size_Y")]
|
||||
public double PhysicalSizeY { get; set; }
|
||||
|
||||
/// <summary>像素尺寸 X (mm) | Pixel size X (mm)</summary>
|
||||
[IniKey("Pixel_X")]
|
||||
public double PixelX { get; set; }
|
||||
|
||||
/// <summary>像素尺寸 Y (mm) | Pixel size Y (mm)</summary>
|
||||
[IniKey("Pixel_Y")]
|
||||
public double PixelY { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using XP.Scan.Attributes;
|
||||
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 运动控制配置 | Motion control configuration
|
||||
/// 对应 INI [Move_Control] Section
|
||||
/// </summary>
|
||||
[IniSection("Move_Control")]
|
||||
public class MoveControlConfig
|
||||
{
|
||||
/// <summary>探测器 X 位置 (mm) | Detector X position (mm)</summary>
|
||||
[IniKey("DetX")]
|
||||
public double DetX { get; set; }
|
||||
|
||||
/// <summary>探测器 Y 位置 (mm) | Detector Y position (mm)</summary>
|
||||
[IniKey("DetY")]
|
||||
public double DetY { get; set; }
|
||||
|
||||
/// <summary>探测器 Z 位置 (mm) | Detector Z position (mm)</summary>
|
||||
[IniKey("DetZ")]
|
||||
public double DetZ { get; set; }
|
||||
|
||||
/// <summary>旋转台角度 (°) | Rotation angle (°)</summary>
|
||||
[IniKey("Rotation")]
|
||||
public double Rotation { get; set; }
|
||||
|
||||
/// <summary>样品台 X 位置 (mm),即 SOD | Stage X position (mm), i.e. SOD</summary>
|
||||
[IniKey("X")]
|
||||
public double X { get; set; }
|
||||
|
||||
/// <summary>射线源 Z 位置 (mm) | X-Ray source Z position (mm)</summary>
|
||||
[IniKey("XRAYZ")]
|
||||
public double XRAYZ { get; set; }
|
||||
|
||||
/// <summary>样品台 Y 位置 (mm) | Stage Y position (mm)</summary>
|
||||
[IniKey("Y")]
|
||||
public double Y { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using XP.Scan.Attributes;
|
||||
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 项目信息 | Project information
|
||||
/// 对应 INI [Project_Information] Section
|
||||
/// </summary>
|
||||
[IniSection("Project_Information")]
|
||||
public class ProjectInfo
|
||||
{
|
||||
/// <summary>图像保存路径 | Image save path</summary>
|
||||
[IniKey("fileSave")]
|
||||
public string FileSave { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>滤波片1 | Filter 1</summary>
|
||||
[IniKey("filter1")]
|
||||
public string Filter1 { get; set; } = "None";
|
||||
|
||||
/// <summary>滤波片2 | Filter 2</summary>
|
||||
[IniKey("filter2")]
|
||||
public string Filter2 { get; set; } = "None";
|
||||
|
||||
/// <summary>项目名称 | Project name</summary>
|
||||
[IniKey("Project")]
|
||||
public string Project { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>样品编号 | Sample number</summary>
|
||||
[IniKey("SampleNo")]
|
||||
public string SampleNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>扫描模式名称 | Scan mode name</summary>
|
||||
[IniKey("ScanMode")]
|
||||
public string ScanMode { get; set; } = "QuickScan";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描配置数据(顶层聚合,对应完整 INI 文件)
|
||||
/// Scan configuration data (top-level aggregate, corresponds to complete INI file)
|
||||
/// </summary>
|
||||
public class ScanConfigData
|
||||
{
|
||||
/// <summary>项目信息 → [Project_Information]</summary>
|
||||
public ProjectInfo ProjectInfo { get; set; } = new();
|
||||
|
||||
/// <summary>射线源配置 → [XRay]</summary>
|
||||
public XRayConfig XRay { get; set; } = new();
|
||||
|
||||
/// <summary>探测器配置 → [Detector]</summary>
|
||||
public DetectorConfig Detector { get; set; } = new();
|
||||
|
||||
/// <summary>运动控制配置 → [Move_Control]</summary>
|
||||
public MoveControlConfig MoveControl { get; set; } = new();
|
||||
|
||||
/// <summary>扫描配置 → [Scan_Config]</summary>
|
||||
public ScanSettings ScanSettings { get; set; } = new();
|
||||
|
||||
/// <summary>校正配置 → [Correction_Config]</summary>
|
||||
public CorrectionConfig Correction { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using XP.Scan.Attributes;
|
||||
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描配置 | Scan configuration
|
||||
/// 对应 INI [Scan_Config] Section
|
||||
/// </summary>
|
||||
[IniSection("Scan_Config")]
|
||||
public class ScanSettings
|
||||
{
|
||||
/// <summary>采集张数 | Number of acquisitions</summary>
|
||||
[IniKey("AcquiresNums")]
|
||||
public int AcquiresNums { get; set; }
|
||||
|
||||
/// <summary>旋转角度 (°) | Rotation degree (°)</summary>
|
||||
[IniKey("RotateDegree")]
|
||||
public double RotateDegree { get; set; }
|
||||
|
||||
/// <summary>扫描模式描述 | Scan mode description</summary>
|
||||
[IniKey("ScanMode")]
|
||||
public string ScanMode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>SDD — 射线源到探测器距离 (mm) | Source to detector distance (mm)</summary>
|
||||
[IniKey("SDD")]
|
||||
public double SDD { get; set; }
|
||||
|
||||
/// <summary>SOD — 射线源到样品距离 (mm) | Source to object distance (mm)</summary>
|
||||
[IniKey("SOD")]
|
||||
public double SOD { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using XP.Scan.Attributes;
|
||||
|
||||
namespace XP.Scan.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 射线源配置 | X-Ray source configuration
|
||||
/// 对应 INI [XRay] Section
|
||||
/// </summary>
|
||||
[IniSection("XRay")]
|
||||
public class XRayConfig
|
||||
{
|
||||
/// <summary>管电流 (μA) | Tube current (μA)</summary>
|
||||
[IniKey("Current_uA")]
|
||||
public int CurrentUA { get; set; }
|
||||
|
||||
/// <summary>焦点尺寸 | Focus size</summary>
|
||||
[IniKey("Focus")]
|
||||
public string Focus { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>管电压 (kV) | Tube voltage (kV)</summary>
|
||||
[IniKey("Voltage_kV")]
|
||||
public int VoltageKV { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using XP.Scan.Models;
|
||||
|
||||
namespace XP.Scan.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描配置 INI 序列化接口 | Scan config INI serialization interface
|
||||
/// </summary>
|
||||
public interface IScanConfigSerializer
|
||||
{
|
||||
/// <summary>将配置数据序列化为 INI 格式字符串 | Serialize config to INI string</summary>
|
||||
string Serialize(ScanConfigData config);
|
||||
|
||||
/// <summary>将配置数据写入 INI 文件 | Save config to INI file</summary>
|
||||
void SaveToFile(ScanConfigData config, string filePath);
|
||||
|
||||
/// <summary>从 INI 文件读取配置数据 | Load config from INI file</summary>
|
||||
ScanConfigData LoadFromFile(string filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using XP.Scan.Attributes;
|
||||
using XP.Scan.Models;
|
||||
|
||||
namespace XP.Scan.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描配置 INI 序列化实现 | Scan config INI serialization implementation
|
||||
/// 通过反射 + 自定义特性自动生成/解析 INI 内容
|
||||
/// </summary>
|
||||
public class ScanConfigSerializer : IScanConfigSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 将配置数据序列化为 INI 格式字符串
|
||||
/// </summary>
|
||||
public string Serialize(ScanConfigData config)
|
||||
{
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var sections = GetSectionObjects(config);
|
||||
|
||||
foreach (var (sectionName, sectionObj) in sections)
|
||||
{
|
||||
sb.AppendLine($"[{sectionName}]");
|
||||
SerializeSection(sb, sectionObj);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将配置数据写入 INI 文件
|
||||
/// </summary>
|
||||
public void SaveToFile(ScanConfigData config, string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath));
|
||||
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var content = Serialize(config);
|
||||
File.WriteAllText(filePath, content, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 INI 文件读取配置数据
|
||||
/// </summary>
|
||||
public ScanConfigData LoadFromFile(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"INI file not found: {filePath}", filePath);
|
||||
|
||||
var lines = File.ReadAllLines(filePath, Encoding.UTF8);
|
||||
var iniData = ParseIniLines(lines);
|
||||
|
||||
var config = new ScanConfigData();
|
||||
DeserializeSection(iniData, config.ProjectInfo);
|
||||
DeserializeSection(iniData, config.XRay);
|
||||
DeserializeSection(iniData, config.Detector);
|
||||
DeserializeSection(iniData, config.MoveControl);
|
||||
DeserializeSection(iniData, config.ScanSettings);
|
||||
DeserializeSection(iniData, config.Correction);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
#region 序列化辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 获取 ScanConfigData 中所有标注了 [IniSection] 的子对象
|
||||
/// </summary>
|
||||
private List<(string SectionName, object SectionObj)> GetSectionObjects(ScanConfigData config)
|
||||
{
|
||||
var result = new List<(string, object)>();
|
||||
|
||||
foreach (var prop in typeof(ScanConfigData).GetProperties())
|
||||
{
|
||||
var obj = prop.GetValue(config);
|
||||
if (obj == null) continue;
|
||||
|
||||
var sectionAttr = obj.GetType().GetCustomAttribute<IniSectionAttribute>();
|
||||
if (sectionAttr != null)
|
||||
{
|
||||
result.Add((sectionAttr.SectionName, obj));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化单个 Section 的所有属性为 key=value 行
|
||||
/// </summary>
|
||||
private void SerializeSection(StringBuilder sb, object sectionObj)
|
||||
{
|
||||
foreach (var prop in sectionObj.GetType().GetProperties())
|
||||
{
|
||||
var keyAttr = prop.GetCustomAttribute<IniKeyAttribute>();
|
||||
var keyName = keyAttr?.KeyName ?? prop.Name;
|
||||
var value = prop.GetValue(sectionObj);
|
||||
var valueStr = FormatValue(value);
|
||||
|
||||
sb.AppendLine($"{keyName}={valueStr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化属性值为 INI 字符串(使用 InvariantCulture)
|
||||
/// </summary>
|
||||
private string FormatValue(object? value)
|
||||
{
|
||||
if (value == null) return string.Empty;
|
||||
|
||||
return value switch
|
||||
{
|
||||
double d => d.ToString(CultureInfo.InvariantCulture),
|
||||
float f => f.ToString(CultureInfo.InvariantCulture),
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 反序列化辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 解析 INI 文件行为 Section → Key/Value 字典
|
||||
/// </summary>
|
||||
private Dictionary<string, Dictionary<string, string>> ParseIniLines(string[] lines)
|
||||
{
|
||||
var result = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||
string currentSection = string.Empty;
|
||||
|
||||
foreach (var rawLine in lines)
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
|
||||
// 跳过空行和注释
|
||||
if (string.IsNullOrEmpty(line) || line.StartsWith(";") || line.StartsWith("#"))
|
||||
continue;
|
||||
|
||||
// Section 头
|
||||
if (line.StartsWith("[") && line.EndsWith("]"))
|
||||
{
|
||||
currentSection = line.Substring(1, line.Length - 2).Trim();
|
||||
if (!result.ContainsKey(currentSection))
|
||||
{
|
||||
result[currentSection] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key=Value
|
||||
var eqIndex = line.IndexOf('=');
|
||||
if (eqIndex > 0 && !string.IsNullOrEmpty(currentSection))
|
||||
{
|
||||
var key = line.Substring(0, eqIndex).Trim();
|
||||
var val = line.Substring(eqIndex + 1).Trim();
|
||||
result[currentSection][key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 INI 数据反序列化到模型对象
|
||||
/// </summary>
|
||||
private void DeserializeSection(Dictionary<string, Dictionary<string, string>> iniData, object sectionObj)
|
||||
{
|
||||
var sectionAttr = sectionObj.GetType().GetCustomAttribute<IniSectionAttribute>();
|
||||
if (sectionAttr == null) return;
|
||||
|
||||
if (!iniData.TryGetValue(sectionAttr.SectionName, out var sectionData))
|
||||
return;
|
||||
|
||||
foreach (var prop in sectionObj.GetType().GetProperties())
|
||||
{
|
||||
var keyAttr = prop.GetCustomAttribute<IniKeyAttribute>();
|
||||
var keyName = keyAttr?.KeyName ?? prop.Name;
|
||||
|
||||
if (!sectionData.TryGetValue(keyName, out var valueStr))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var convertedValue = ConvertValue(valueStr, prop.PropertyType);
|
||||
if (convertedValue != null)
|
||||
{
|
||||
prop.SetValue(sectionObj, convertedValue);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 转换失败时保留默认值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字符串值转换为目标类型
|
||||
/// </summary>
|
||||
private object? ConvertValue(string value, Type targetType)
|
||||
{
|
||||
if (targetType == typeof(string))
|
||||
return value;
|
||||
|
||||
if (targetType == typeof(int))
|
||||
return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var i) ? i : null;
|
||||
|
||||
if (targetType == typeof(double))
|
||||
return double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : null;
|
||||
|
||||
if (targetType == typeof(float))
|
||||
return float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var f) ? f : null;
|
||||
|
||||
if (targetType == typeof(bool))
|
||||
return bool.TryParse(value, out var b) ? b : null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+73
-8
@@ -13,6 +13,7 @@ using XP.Camera;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Database.Implementations;
|
||||
using XP.Common.Database.Interfaces;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.Dump.Configs;
|
||||
using XP.Common.Dump.Implementations;
|
||||
using XP.Common.Dump.Interfaces;
|
||||
@@ -150,7 +151,23 @@ namespace XplorePlane
|
||||
Log.Error(ex, "射线源资源释放失败");
|
||||
}
|
||||
|
||||
// 释放相机服务资源
|
||||
// 先停止导航相机实时采集,再释放资源,避免回调死锁
|
||||
try
|
||||
{
|
||||
var bootstrapper = AppBootstrapper.Instance;
|
||||
if (bootstrapper != null)
|
||||
{
|
||||
var cameraVm = bootstrapper.Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||
cameraVm?.Dispose();
|
||||
Log.Information("导航相机 ViewModel 已释放");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "导航相机 ViewModel 释放失败");
|
||||
}
|
||||
|
||||
// 释放导航相机服务资源
|
||||
try
|
||||
{
|
||||
var bootstrapper = AppBootstrapper.Instance;
|
||||
@@ -158,12 +175,12 @@ namespace XplorePlane
|
||||
{
|
||||
var cameraService = bootstrapper.Container.Resolve<ICameraService>();
|
||||
cameraService?.Dispose();
|
||||
Log.Information("相机服务资源已释放");
|
||||
Log.Information("导航相机服务资源已释放");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "相机服务资源释放失败");
|
||||
Log.Error(ex, "导航相机服务资源释放失败");
|
||||
}
|
||||
|
||||
// 释放SQLite数据库资源 | Release SQLite database resources
|
||||
@@ -232,19 +249,67 @@ namespace XplorePlane
|
||||
|
||||
private bool _modulesInitialized = false;
|
||||
|
||||
private string? _cameraError;
|
||||
|
||||
protected override Window CreateShell()
|
||||
{
|
||||
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
|
||||
// 默认 Prism 顺序是 CreateShell → InitializeModules,
|
||||
// 但 MainWindow 中嵌入的硬件控件会在 XAML 解析时触发 ViewModelLocator,
|
||||
// 此时模块尚未加载,导致依赖解析失败
|
||||
if (!_modulesInitialized)
|
||||
{
|
||||
base.InitializeModules();
|
||||
_modulesInitialized = true;
|
||||
}
|
||||
|
||||
return Container.Resolve<MainWindow>();
|
||||
var shell = Container.Resolve<MainWindow>();
|
||||
|
||||
// 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
|
||||
shell.Loaded += (s, e) =>
|
||||
{
|
||||
TryConnectCamera();
|
||||
|
||||
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
|
||||
try
|
||||
{
|
||||
var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||
cameraVm.OnCameraReady();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "通知相机 ViewModel 失败");
|
||||
}
|
||||
|
||||
if (_cameraError != null)
|
||||
{
|
||||
HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
};
|
||||
|
||||
return shell;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上检索并连接导航相机。
|
||||
/// pylon SDK 要求在主线程(STA)上操作,不能放到后台线程。
|
||||
/// </summary>
|
||||
private void TryConnectCamera()
|
||||
{
|
||||
var camera = Container.Resolve<ICameraController>();
|
||||
|
||||
try
|
||||
{
|
||||
var info = camera.Open();
|
||||
Log.Information("导航相机已连接: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber);
|
||||
}
|
||||
catch (DeviceNotFoundException)
|
||||
{
|
||||
Log.Warning("未检测到导航相机");
|
||||
_cameraError = "未检测到导航相机,请检查连接后重启软件。";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "导航相机自动连接失败: {Message}", ex.Message);
|
||||
_cameraError = $"导航相机连接失败: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -329,7 +394,7 @@ namespace XplorePlane
|
||||
containerRegistry.RegisterForNavigation<CncPageView>();
|
||||
containerRegistry.RegisterForNavigation<MatrixPageView>();
|
||||
|
||||
// ── 相机服务(单例)──
|
||||
// ── 导航相机服务(单例)──
|
||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||
new CameraFactory().CreateController("Basler"));
|
||||
|
||||
@@ -28,11 +28,11 @@ namespace XplorePlane.Services
|
||||
var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0);
|
||||
int width = formatted.PixelWidth;
|
||||
int height = formatted.PixelHeight;
|
||||
int stride = width;
|
||||
byte[] pixels = new byte[height * stride];
|
||||
formatted.CopyPixels(pixels, stride, 0);
|
||||
|
||||
var image = new Image<Gray, byte>(width, height);
|
||||
int stride = image.Bytes.Length / height;
|
||||
var pixels = new byte[height * stride];
|
||||
formatted.CopyPixels(pixels, stride, 0);
|
||||
image.Bytes = pixels;
|
||||
return image;
|
||||
}
|
||||
@@ -40,7 +40,19 @@ namespace XplorePlane.Services
|
||||
public static Image<Gray, byte> ToEmguCVFromPixels(byte[] pixels, int width, int height)
|
||||
{
|
||||
var image = new Image<Gray, byte>(width, height);
|
||||
int required = image.Bytes.Length;
|
||||
if (pixels.Length == required)
|
||||
{
|
||||
image.Bytes = pixels;
|
||||
}
|
||||
else
|
||||
{
|
||||
int stride = required / height;
|
||||
var padded = new byte[required];
|
||||
for (int row = 0; row < height; row++)
|
||||
Buffer.BlockCopy(pixels, row * width, padded, row * stride, width);
|
||||
image.Bytes = padded;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -50,8 +62,8 @@ namespace XplorePlane.Services
|
||||
|
||||
int width = emguImage.Width;
|
||||
int height = emguImage.Height;
|
||||
int stride = width;
|
||||
byte[] pixels = emguImage.Bytes;
|
||||
int stride = pixels.Length / height;
|
||||
|
||||
return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
|
||||
}
|
||||
|
||||
@@ -176,6 +176,30 @@ namespace XplorePlane.ViewModels
|
||||
ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected);
|
||||
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
|
||||
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
|
||||
|
||||
CameraStatusText = "正在检索相机...";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相机连接完成后由外部调用,启动实时预览。
|
||||
/// </summary>
|
||||
public void OnCameraReady()
|
||||
{
|
||||
if (!_camera.IsConnected)
|
||||
{
|
||||
CameraStatusText = "未检测到相机";
|
||||
return;
|
||||
}
|
||||
|
||||
_camera.ImageGrabbed += OnCameraImageGrabbed;
|
||||
_camera.GrabError += OnCameraGrabError;
|
||||
_camera.ConnectionLost += OnCameraConnectionLost;
|
||||
|
||||
IsCameraConnected = true;
|
||||
CameraStatusText = "已连接";
|
||||
RefreshCameraParams();
|
||||
StartGrab();
|
||||
IsLiveViewEnabled = true;
|
||||
}
|
||||
|
||||
#region Camera Methods
|
||||
@@ -326,14 +350,17 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
|
||||
app.Dispatcher.Invoke(() =>
|
||||
app.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
if (!_disposed)
|
||||
CameraImageSource = bitmap;
|
||||
});
|
||||
|
||||
@@ -344,6 +371,7 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_disposed)
|
||||
_logger.Error(ex, "Failed to process camera image");
|
||||
}
|
||||
}
|
||||
@@ -354,8 +382,9 @@ namespace XplorePlane.ViewModels
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
|
||||
app.Dispatcher.Invoke(() =>
|
||||
app.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
if (!_disposed)
|
||||
CameraStatusText = $"采集错误: {e.ErrorDescription}";
|
||||
});
|
||||
}
|
||||
@@ -366,8 +395,9 @@ namespace XplorePlane.ViewModels
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
|
||||
app.Dispatcher.Invoke(() =>
|
||||
app.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
if (_disposed) return;
|
||||
IsCameraConnected = false;
|
||||
IsCameraGrabbing = false;
|
||||
CameraStatusText = "连接已断开";
|
||||
@@ -382,12 +412,19 @@ namespace XplorePlane.ViewModels
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_liveViewRunning = false;
|
||||
|
||||
try { _camera.Dispose(); }
|
||||
catch (Exception ex) { _logger.Error(ex, "Error disposing camera"); }
|
||||
// 先取消事件订阅,防止回调继续触发
|
||||
_camera.ImageGrabbed -= OnCameraImageGrabbed;
|
||||
_camera.GrabError -= OnCameraGrabError;
|
||||
_camera.ConnectionLost -= OnCameraConnectionLost;
|
||||
|
||||
_disposed = true;
|
||||
// 停止采集后再关闭连接
|
||||
try { if (_camera.IsGrabbing) _camera.StopGrabbing(); } catch { }
|
||||
try { _camera.Close(); } catch { }
|
||||
|
||||
_logger.Information("NavigationPropertyPanelViewModel disposed");
|
||||
}
|
||||
|
||||
#endregion IDisposable
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
@@ -56,50 +55,5 @@
|
||||
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Border>
|
||||
|
||||
<!-- 控制按钮栏 -->
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Background="#F0F0F0"
|
||||
BorderBrush="#DDDDDD"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="4">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button
|
||||
Command="{Binding ConnectCameraCommand}"
|
||||
Width="60" Height="26"
|
||||
Margin="2"
|
||||
Background="#4CAF50"
|
||||
Foreground="#000000"
|
||||
Content="连接" />
|
||||
<Button
|
||||
Command="{Binding DisconnectCameraCommand}"
|
||||
Width="60" Height="26"
|
||||
Margin="2"
|
||||
Background="#F44336"
|
||||
Foreground="#000000"
|
||||
Content="断开" />
|
||||
<Button
|
||||
Command="{Binding StartGrabCommand}"
|
||||
Width="60" Height="26"
|
||||
Margin="2"
|
||||
Background="#2196F3"
|
||||
Foreground="#000000"
|
||||
Content="采集" />
|
||||
<Button
|
||||
Command="{Binding StopGrabCommand}"
|
||||
Width="60" Height="26"
|
||||
Margin="2"
|
||||
Background="#FF9800"
|
||||
Foreground="#000000"
|
||||
Content="停止" />
|
||||
<CheckBox
|
||||
IsChecked="{Binding IsLiveViewEnabled}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="6,0,0,0"
|
||||
Foreground="#333333"
|
||||
Content="实时" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -28,8 +28,16 @@ namespace XplorePlane.Views
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 双击相机图像时,计算并显示点击位置的像素坐标。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: 后续需要将点击的像素坐标通过 CalibrationProcessor 转换为世界坐标,
|
||||
/// 再传给运动机构执行定位。
|
||||
/// </remarks>
|
||||
private void ImgCamera_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ClickCount != 2) return;
|
||||
if (_viewModel?.CameraImageSource == null) return;
|
||||
|
||||
var image = (Image)sender;
|
||||
|
||||
Reference in New Issue
Block a user