VppCalibrationPage.xaml.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. using System;
  2. using System.Collections.ObjectModel;
  3. using System.Windows;
  4. using System.Windows.Controls;
  5. using System.Windows.Media;
  6. using System.Windows.Media.Imaging;
  7. using Microsoft.Win32;
  8. namespace Plugins.Vpp.Engines
  9. {
  10. /// <summary>标定点位行</summary>
  11. public class CalibrationPointItem
  12. {
  13. public int Index { get; set; }
  14. public double RobotX { get; set; }
  15. public double RobotY { get; set; }
  16. public double PixelX { get; set; }
  17. public double PixelY { get; set; }
  18. }
  19. /// <summary>
  20. /// VisionPro 引擎标定窗体:
  21. /// - 九点标定:像素 → 机器人 仿射变换(robotX=a·px+b·py+c,robotY=d·px+e·py+f,最小二乘);
  22. /// - 旋转中心:像素坐标圆拟合(Kasa 最小二乘)。
  23. /// 自包含实现,不依赖外部数学库;仿射系数随结果输出,供宿主换算使用。
  24. /// </summary>
  25. public partial class VppCalibrationPage : UserControl
  26. {
  27. private bool _isRotationCenter;
  28. public ObservableCollection<CalibrationPointItem> Points { get; }
  29. = new ObservableCollection<CalibrationPointItem>();
  30. public VppCalibrationPage()
  31. {
  32. InitializeComponent();
  33. DataContext = this;
  34. ResetPoints(9);
  35. }
  36. private void ResetPoints(int count)
  37. {
  38. Points.Clear();
  39. for (int i = 0; i < count; i++)
  40. Points.Add(new CalibrationPointItem { Index = i + 1 });
  41. }
  42. private void NinePoint_Checked(object sender, RoutedEventArgs e) { _isRotationCenter = false; ResetPoints(9); }
  43. private void RotationCenter_Checked(object sender, RoutedEventArgs e) { _isRotationCenter = true; ResetPoints(4); }
  44. private void AddRow_Click(object sender, RoutedEventArgs e)
  45. => Points.Add(new CalibrationPointItem { Index = Points.Count + 1 });
  46. private void ClearPoints_Click(object sender, RoutedEventArgs e)
  47. => ResetPoints(_isRotationCenter ? 4 : 9);
  48. private void LoadImage_Click(object sender, RoutedEventArgs e)
  49. {
  50. var dlg = new OpenFileDialog { Filter = "图片文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff" };
  51. if (dlg.ShowDialog() != true) return;
  52. var bmp = new BitmapImage();
  53. bmp.BeginInit();
  54. bmp.CacheOption = BitmapCacheOption.OnLoad;
  55. bmp.UriSource = new Uri(dlg.FileName);
  56. bmp.EndInit();
  57. bmp.Freeze();
  58. CalibImage.Source = bmp;
  59. ImagePlaceholder.Visibility = Visibility.Collapsed;
  60. }
  61. private void Calculate_Click(object sender, RoutedEventArgs e)
  62. {
  63. try
  64. {
  65. ResultText.Text = _isRotationCenter ? CalculateRotationCenter() : CalculateNinePoint();
  66. }
  67. catch (Exception ex)
  68. {
  69. ResultText.Text = "标定计算失败:" + ex.Message;
  70. }
  71. }
  72. /// <summary>九点标定:像素 → 机器人 仿射最小二乘</summary>
  73. private string CalculateNinePoint()
  74. {
  75. int n = 0;
  76. // 正规方程累加:M = Σ[px² pxpy px; pxpy py² py; px py 1],bX = Σ[px·rx py·rx rx],bY 同理
  77. double m11 = 0, m12 = 0, m13 = 0, m22 = 0, m23 = 0, m33 = 0;
  78. double bx1 = 0, bx2 = 0, bx3 = 0, by1 = 0, by2 = 0, by3 = 0;
  79. foreach (var p in Points)
  80. {
  81. if (p.PixelX == 0 && p.PixelY == 0) continue;
  82. n++;
  83. m11 += p.PixelX * p.PixelX; m12 += p.PixelX * p.PixelY; m13 += p.PixelX;
  84. m22 += p.PixelY * p.PixelY; m23 += p.PixelY;
  85. m33 += 1;
  86. bx1 += p.PixelX * p.RobotX; bx2 += p.PixelY * p.RobotX; bx3 += p.RobotX;
  87. by1 += p.PixelX * p.RobotY; by2 += p.PixelY * p.RobotY; by3 += p.RobotY;
  88. }
  89. if (n < 3) throw new InvalidOperationException("有效标定点不足 3 个");
  90. // [a b c] = M⁻¹ · bX(robotX = a·px + b·py + c)
  91. var abc = Solve3(m11, m12, m13, m12, m22, m23, m13, m23, m33, bx1, bx2, bx3);
  92. // [d e f] = M⁻¹ · bY(robotY = d·px + e·py + f)
  93. var def = Solve3(m11, m12, m13, m12, m22, m23, m13, m23, m33, by1, by2, by3);
  94. // 复验误差(最大偏差)
  95. double maxErr = 0;
  96. foreach (var p in Points)
  97. {
  98. if (p.PixelX == 0 && p.PixelY == 0) continue;
  99. var rx = abc[0] * p.PixelX + abc[1] * p.PixelY + abc[2];
  100. var ry = def[0] * p.PixelX + def[1] * p.PixelY + def[2];
  101. maxErr = Math.Max(maxErr, Math.Sqrt(Math.Pow(rx - p.RobotX, 2) + Math.Pow(ry - p.RobotY, 2)));
  102. }
  103. return $"九点标定完成({n} 点)\r\n" +
  104. $"像素→机器人 仿射系数:\r\n" +
  105. $"robotX = {abc[0]:F6}·px + {abc[1]:F6}·py + {abc[2]:F3}\r\n" +
  106. $"robotY = {def[0]:F6}·px + {def[1]:F6}·py + {def[2]:F3}\r\n" +
  107. $"最大重投影误差:{maxErr:F4} mm";
  108. }
  109. /// <summary>3×3 线性方程组高斯消元求解</summary>
  110. private static double[] Solve3(
  111. double m11, double m12, double m13,
  112. double m21, double m22, double m23,
  113. double m31, double m32, double m33,
  114. double b1, double b2, double b3)
  115. {
  116. double det = m11 * (m22 * m33 - m23 * m32)
  117. - m12 * (m21 * m33 - m23 * m31)
  118. + m13 * (m21 * m32 - m22 * m31);
  119. if (Math.Abs(det) < 1e-12)
  120. throw new InvalidOperationException("标定点位分布退化(共线),无法求解");
  121. return new[]
  122. {
  123. (b1 * (m22 * m33 - m23 * m32) - m12 * (b1 * m33 - m23 * b3) + m13 * (b1 * m32 - m22 * b3)) / det,
  124. (m11 * (b2 * m33 - m23 * b3) - b1 * (m21 * m33 - m23 * m31) + m13 * (m21 * b3 - b2 * m31)) / det,
  125. (m11 * (m22 * b3 - b2 * m32) - m12 * (m21 * b3 - b2 * m31) + b1 * (m21 * m32 - m22 * m31)) / det,
  126. };
  127. }
  128. /// <summary>旋转中心:像素坐标圆拟合(Kasa 最小二乘)</summary>
  129. private string CalculateRotationCenter()
  130. {
  131. int n = 0;
  132. double sx = 0, sy = 0, sxx = 0, syy = 0, sxy = 0, sxz = 0, syz = 0, sz = 0;
  133. foreach (var p in Points)
  134. {
  135. if (p.PixelX == 0 && p.PixelY == 0) continue;
  136. double z = p.PixelX * p.PixelX + p.PixelY * p.PixelY;
  137. n++;
  138. sx += p.PixelX; sy += p.PixelY;
  139. sxx += p.PixelX * p.PixelX; syy += p.PixelY * p.PixelY; sxy += p.PixelX * p.PixelY;
  140. sxz += p.PixelX * z; syz += p.PixelY * z; sz += z;
  141. }
  142. if (n < 3) throw new InvalidOperationException("有效标定点不足 3 个");
  143. // [sxx sxy; sxy syy][a; b] = [sxz; syz],圆心=(a/2, b/2),r=√(c+a²+b²)
  144. double det = sxx * syy - sxy * sxy;
  145. if (Math.Abs(det) < 1e-9) throw new InvalidOperationException("点位共线,无法拟合圆");
  146. double a = (sxz * syy - syz * sxy) / det;
  147. double b = (sxx * syz - sxy * sxz) / det;
  148. double c = (sz - a * sx - b * sy) / n;
  149. double r = Math.Sqrt(Math.Max(0, c + a * a + b * b));
  150. return $"旋转中心标定完成({n} 点)\r\n" +
  151. $"旋转中心(像素):({a / 2:F2}, {b / 2:F2})\r\n" +
  152. $"旋转半径:{r:F2} px";
  153. }
  154. }
  155. }