using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Win32;
namespace Plugins.Vpp.Engines
{
/// 标定点位行
public class CalibrationPointItem
{
public int Index { get; set; }
public double RobotX { get; set; }
public double RobotY { get; set; }
public double PixelX { get; set; }
public double PixelY { get; set; }
}
///
/// VisionPro 引擎标定窗体:
/// - 九点标定:像素 → 机器人 仿射变换(robotX=a·px+b·py+c,robotY=d·px+e·py+f,最小二乘);
/// - 旋转中心:像素坐标圆拟合(Kasa 最小二乘)。
/// 自包含实现,不依赖外部数学库;仿射系数随结果输出,供宿主换算使用。
///
public partial class VppCalibrationPage : UserControl
{
private bool _isRotationCenter;
public ObservableCollection Points { get; }
= new ObservableCollection();
public VppCalibrationPage()
{
InitializeComponent();
DataContext = this;
ResetPoints(9);
}
private void ResetPoints(int count)
{
Points.Clear();
for (int i = 0; i < count; i++)
Points.Add(new CalibrationPointItem { Index = i + 1 });
}
private void NinePoint_Checked(object sender, RoutedEventArgs e) { _isRotationCenter = false; ResetPoints(9); }
private void RotationCenter_Checked(object sender, RoutedEventArgs e) { _isRotationCenter = true; ResetPoints(4); }
private void AddRow_Click(object sender, RoutedEventArgs e)
=> Points.Add(new CalibrationPointItem { Index = Points.Count + 1 });
private void ClearPoints_Click(object sender, RoutedEventArgs e)
=> ResetPoints(_isRotationCenter ? 4 : 9);
private void LoadImage_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog { Filter = "图片文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff" };
if (dlg.ShowDialog() != true) return;
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.UriSource = new Uri(dlg.FileName);
bmp.EndInit();
bmp.Freeze();
CalibImage.Source = bmp;
ImagePlaceholder.Visibility = Visibility.Collapsed;
}
private void Calculate_Click(object sender, RoutedEventArgs e)
{
try
{
ResultText.Text = _isRotationCenter ? CalculateRotationCenter() : CalculateNinePoint();
}
catch (Exception ex)
{
ResultText.Text = "标定计算失败:" + ex.Message;
}
}
/// 九点标定:像素 → 机器人 仿射最小二乘
private string CalculateNinePoint()
{
int n = 0;
// 正规方程累加:M = Σ[px² pxpy px; pxpy py² py; px py 1],bX = Σ[px·rx py·rx rx],bY 同理
double m11 = 0, m12 = 0, m13 = 0, m22 = 0, m23 = 0, m33 = 0;
double bx1 = 0, bx2 = 0, bx3 = 0, by1 = 0, by2 = 0, by3 = 0;
foreach (var p in Points)
{
if (p.PixelX == 0 && p.PixelY == 0) continue;
n++;
m11 += p.PixelX * p.PixelX; m12 += p.PixelX * p.PixelY; m13 += p.PixelX;
m22 += p.PixelY * p.PixelY; m23 += p.PixelY;
m33 += 1;
bx1 += p.PixelX * p.RobotX; bx2 += p.PixelY * p.RobotX; bx3 += p.RobotX;
by1 += p.PixelX * p.RobotY; by2 += p.PixelY * p.RobotY; by3 += p.RobotY;
}
if (n < 3) throw new InvalidOperationException("有效标定点不足 3 个");
// [a b c] = M⁻¹ · bX(robotX = a·px + b·py + c)
var abc = Solve3(m11, m12, m13, m12, m22, m23, m13, m23, m33, bx1, bx2, bx3);
// [d e f] = M⁻¹ · bY(robotY = d·px + e·py + f)
var def = Solve3(m11, m12, m13, m12, m22, m23, m13, m23, m33, by1, by2, by3);
// 复验误差(最大偏差)
double maxErr = 0;
foreach (var p in Points)
{
if (p.PixelX == 0 && p.PixelY == 0) continue;
var rx = abc[0] * p.PixelX + abc[1] * p.PixelY + abc[2];
var ry = def[0] * p.PixelX + def[1] * p.PixelY + def[2];
maxErr = Math.Max(maxErr, Math.Sqrt(Math.Pow(rx - p.RobotX, 2) + Math.Pow(ry - p.RobotY, 2)));
}
return $"九点标定完成({n} 点)\r\n" +
$"像素→机器人 仿射系数:\r\n" +
$"robotX = {abc[0]:F6}·px + {abc[1]:F6}·py + {abc[2]:F3}\r\n" +
$"robotY = {def[0]:F6}·px + {def[1]:F6}·py + {def[2]:F3}\r\n" +
$"最大重投影误差:{maxErr:F4} mm";
}
/// 3×3 线性方程组高斯消元求解
private static double[] Solve3(
double m11, double m12, double m13,
double m21, double m22, double m23,
double m31, double m32, double m33,
double b1, double b2, double b3)
{
double det = m11 * (m22 * m33 - m23 * m32)
- m12 * (m21 * m33 - m23 * m31)
+ m13 * (m21 * m32 - m22 * m31);
if (Math.Abs(det) < 1e-12)
throw new InvalidOperationException("标定点位分布退化(共线),无法求解");
return new[]
{
(b1 * (m22 * m33 - m23 * m32) - m12 * (b1 * m33 - m23 * b3) + m13 * (b1 * m32 - m22 * b3)) / det,
(m11 * (b2 * m33 - m23 * b3) - b1 * (m21 * m33 - m23 * m31) + m13 * (m21 * b3 - b2 * m31)) / det,
(m11 * (m22 * b3 - b2 * m32) - m12 * (m21 * b3 - b2 * m31) + b1 * (m21 * m32 - m22 * m31)) / det,
};
}
/// 旋转中心:像素坐标圆拟合(Kasa 最小二乘)
private string CalculateRotationCenter()
{
int n = 0;
double sx = 0, sy = 0, sxx = 0, syy = 0, sxy = 0, sxz = 0, syz = 0, sz = 0;
foreach (var p in Points)
{
if (p.PixelX == 0 && p.PixelY == 0) continue;
double z = p.PixelX * p.PixelX + p.PixelY * p.PixelY;
n++;
sx += p.PixelX; sy += p.PixelY;
sxx += p.PixelX * p.PixelX; syy += p.PixelY * p.PixelY; sxy += p.PixelX * p.PixelY;
sxz += p.PixelX * z; syz += p.PixelY * z; sz += z;
}
if (n < 3) throw new InvalidOperationException("有效标定点不足 3 个");
// [sxx sxy; sxy syy][a; b] = [sxz; syz],圆心=(a/2, b/2),r=√(c+a²+b²)
double det = sxx * syy - sxy * sxy;
if (Math.Abs(det) < 1e-9) throw new InvalidOperationException("点位共线,无法拟合圆");
double a = (sxz * syy - syz * sxy) / det;
double b = (sxx * syz - sxy * sxz) / det;
double c = (sz - a * sx - b * sy) / n;
double r = Math.Sqrt(Math.Max(0, c + a * a + b * b));
return $"旋转中心标定完成({n} 点)\r\n" +
$"旋转中心(像素):({a / 2:F2}, {b / 2:F2})\r\n" +
$"旋转半径:{r:F2} px";
}
}
}