TURBO-569:更新工程结构;将导航相机标定和校准功能迁移到XP.Camera类
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user