CalibrationService.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /*
  2. PSEUDOCODE / 设计计划(详尽):
  3. 1. 目标:为 CalibrationService 增加完整中文注释(XML 文档注释 + 行内注释),保持原有逻辑不变。
  4. 2. 对象/职责:
  5. - 管理校准项集合(内存):_calibrations(线程安全访问)
  6. - 提供读取、保存、添加/更新、删除、批量加载/保存以及像素到位置信息转换的功能
  7. - 与文件系统交互:Calibration 列表文件与每个 Calibration 的单独文件、ToolBlock 文件(.vpp)
  8. 3. 方法处理:
  9. - GetAllCalibrations: 返回只读集合的快照(加锁,防止并发修改)
  10. - GetCalibration: 按 Id 查找校准项(加锁)
  11. - AddOrUpdateCalibration:
  12. a. 验空
  13. b. 在集合中查找是否存在:存在 -> 替换;不存在 -> 追加并设置 Index
  14. c. 更新 DateTime
  15. d. 确保文件夹存在,写入单个校准 cfg 文件
  16. e. 更新并写入 CalibrationList.cfg(Id->Name 映射)
  17. f. 保存或加载 ToolBlock(.vpp),如果不存在则尝试使用模板创建
  18. g. 返回 calib
  19. - RemoveCalibration:
  20. a. 在集合中找到并移除(加锁)
  21. b. 更新 CalibrationList.cfg
  22. c. 重新按 Index 排序并保存每个校准文件
  23. d. 返回是否成功移除
  24. - LoadAll:
  25. a. 确保 CalibrationPath 存在
  26. b. 清空集合并创建空索引文件(如果索引文件不存在)
  27. c. 读取索引列表,遍历每个条目:加载单个 cfg、尝试加载对应 .vpp、加入集合
  28. d. 单个文件异常时忽略,继续加载其他文件
  29. - SaveAll:
  30. a. 遍历集合,确保对应目录存在,写入每个 cfg,若有 ToolBlock 则保存 .vpp
  31. b. 写入索引文件
  32. - ConvertPixelToPosition(主转换逻辑):
  33. a. 用 calib 的仿射矩阵将像素坐标(X,Y,1)映射到机器人坐标
  34. b. 根据 calib.CameraMount 类型选择不同后处理:
  35. - MobileJ4 / MobileDown_XYPlatform:将像素->mm 点做基于当前机器人位置与旋转的变换
  36. - 其他:若提供 robotCoord,则使用 ToolCoord 计算工具坐标(对 Schneider 角度符号处理)
  37. c. 返回 (IsSucceed, X, Y, U)
  38. 4. 注释策略:
  39. - 为类与公有方法添加 XML 注释(中文)
  40. - 为复杂代码段添加行内注释(中文),解释数学与坐标变换逻辑
  41. - 保持所有原始逻辑、签名与行为不变
  42. */
  43. using Cognex.VisionPro;
  44. using Cognex.VisionPro.ToolBlock;
  45. using MathNet.Numerics.LinearAlgebra;
  46. using System;
  47. using System.Collections.Generic;
  48. using System.Collections.ObjectModel;
  49. using System.IO;
  50. using System.Linq;
  51. using System.Windows.Forms;
  52. using TeamAAS_VP.Core;
  53. using TeamAAS_VP.Enums;
  54. using TeamAAS_VP.Interfaces;
  55. using TeamAAS_VP.Models.Calibration;
  56. using TeamAAS_VP.Resources.Languages;
  57. namespace TeamAAS_VP.Services
  58. {
  59. /// <summary>
  60. /// Calibration 管理服务,负责校准文件的读取与保存以及像素到机器人位姿的转换。
  61. /// 线程安全:对内部集合的读/写通过 <see cref="_sync"/> 锁进行保护。
  62. /// 注意:构造函数不自动加载数据,调用者需要在合适时机调用 <see cref="LoadAll"/>。
  63. /// </summary>
  64. public class CalibrationService : ICalibrationService
  65. {
  66. /// <summary>
  67. /// 内部路径常量定义(相对路径)
  68. /// </summary>
  69. private static class Paths
  70. {
  71. public static readonly string CalibrationPath = "..//Calibration";
  72. public static string CalibrationListFilePath => Path.Combine(CalibrationPath ?? string.Empty, "CalibrationList.cfg");
  73. }
  74. // 同步锁对象,保护 _calibrations 的并发访问
  75. private readonly object _sync = new object();
  76. // 内存中的校准集合(用于 UI 绑定/管理)
  77. private ObservableCollection<CalibrationInfo> _calibrations = new ObservableCollection<CalibrationInfo>();
  78. /// <summary>
  79. /// 构造函数。注意:不在构造中自动加载校准,调用者应根据需要调用 <see cref="LoadAll"/>。
  80. /// </summary>
  81. public CalibrationService()
  82. {
  83. // 不在构造中自动加载,调用者可以选择 LoadAll
  84. }
  85. /// <summary>
  86. /// 获取所有校准项的只读快照。
  87. /// 返回一个集合快照以避免外部对内部集合的直接修改。
  88. /// </summary>
  89. /// <returns>只读的 <see cref="CalibrationInfo"/> 集合</returns>
  90. public IReadOnlyCollection<CalibrationInfo> GetAllCalibrations()
  91. {
  92. lock (_sync)
  93. {
  94. // 返回当前集合的一个独立只读副本,避免并发问题
  95. return _calibrations.ToList().AsReadOnly();
  96. }
  97. }
  98. /// <summary>
  99. /// 根据 Id 获取单个校准信息。
  100. /// </summary>
  101. /// <param name="id">校准项的唯一标识</param>
  102. /// <returns>找到则返回 <see cref="CalibrationInfo"/>,否则返回 null</returns>
  103. public CalibrationInfo GetCalibration(Guid id)
  104. {
  105. lock (_sync)
  106. {
  107. return _calibrations.FirstOrDefault(c => c.Id == id);
  108. }
  109. }
  110. /// <summary>
  111. /// 添加或更新校准项:
  112. /// - 若 Id 存在则替换,否则追加并设置 Index
  113. /// - 写入单个校准 cfg 文件并更新索引文件
  114. /// - 保存或加载 ToolBlock(.vpp),若不存在则尝试从模板创建
  115. /// </summary>
  116. /// <param name="calib">要添加或更新的校准信息</param>
  117. /// <returns>保存后的 <see cref="CalibrationInfo"/>(可能为传入对象或修改后的对象);参数为 null 则返回 null</returns>
  118. public CalibrationInfo AddOrUpdateCalibration(CalibrationInfo calib)
  119. {
  120. if (calib == null) return null;
  121. lock (_sync)
  122. {
  123. var exist = _calibrations.FirstOrDefault(c => c.Id == calib.Id);
  124. if (exist != null)
  125. {
  126. // 替换已存在项(保持集合长度与 Index 不变)
  127. var idx = _calibrations.IndexOf(exist);
  128. _calibrations[idx] = calib;
  129. }
  130. else
  131. {
  132. // 新增项,Index 设为当前数量 + 1
  133. calib.Index = _calibrations.Count + 1;
  134. _calibrations.Add(calib);
  135. }
  136. }
  137. // 更新时间戳
  138. calib.DateTime = DateTime.Now;
  139. // 确保校准保存目录存在
  140. string folder = Path.Combine(Paths.CalibrationPath, calib.Name);
  141. string calibFile = Path.Combine(folder, calib.Name + ".cfg");
  142. if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
  143. // 写入单个校准文件(JSON)
  144. FileHelper.WriteJsonFile(calib, calibFile);
  145. // 更新索引列表(Id -> Name)并写入文件
  146. var list = new Dictionary<Guid, string>();
  147. lock (_sync)
  148. {
  149. foreach (var item in _calibrations)
  150. {
  151. list[item.Id] = item.Name;
  152. }
  153. }
  154. FileHelper.WriteJsonFile(list, Paths.CalibrationListFilePath);
  155. // 保存或加载 ToolBlock(.vpp)
  156. string prcPath = Path.Combine(folder, calib.Name + ".vpp");
  157. if (calib.ToolBlock != null)
  158. {
  159. // 已有 ToolBlock,直接保存
  160. VisionProFileHelper.SaveObjectToFileSafe(calib.ToolBlock, prcPath);
  161. }
  162. else
  163. {
  164. // 优先从当前目录加载已有 .vpp 文件,否则尝试从模板加载并保存
  165. if (File.Exists(prcPath))
  166. {
  167. calib.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
  168. }
  169. else if (File.Exists("..//Vision Template//Calibration.vpp"))
  170. {
  171. calib.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>("..//Vision Template//Calibration.vpp");
  172. VisionProFileHelper.SaveObjectToFileSafe(calib.ToolBlock, prcPath);
  173. }
  174. }
  175. return calib;
  176. }
  177. /// <summary>
  178. /// 根据 Id 移除校准项:
  179. /// - 从内存集合移除
  180. /// - 更新索引文件
  181. /// - 重新为剩余项排序 Index 并保存每个 cfg
  182. /// 注:不自动删除物理文件(除非有明确需求)
  183. /// </summary>
  184. /// <param name="id">要删除的校准项 Id</param>
  185. /// <returns>是否成功移除</returns>
  186. public bool RemoveCalibration(Guid id)
  187. {
  188. bool result = false;
  189. CalibrationInfo removed = null;
  190. lock (_sync)
  191. {
  192. removed = _calibrations.FirstOrDefault(c => c.Id == id);
  193. if (removed != null) result = _calibrations.Remove(removed);
  194. }
  195. // 更新索引文件(Id -> Name)
  196. var list = new Dictionary<Guid, string>();
  197. lock (_sync)
  198. {
  199. foreach (var item in _calibrations)
  200. {
  201. list[item.Id] = item.Name;
  202. }
  203. }
  204. FileHelper.WriteJsonFile(list, Paths.CalibrationListFilePath);
  205. // 删除后重新按 Index 排序并保存每个校准文件
  206. var calibs = _calibrations.OrderBy(c => c.Index).ToList();
  207. int index = 1;
  208. foreach (var item in calibs)
  209. {
  210. item.Index = index;
  211. // 保存更新后的 cfg 文件
  212. string folder = Path.Combine(Paths.CalibrationPath, item.Name);
  213. string calibFile = Path.Combine(folder, item.Name + ".cfg");
  214. FileHelper.WriteJsonFile(item, calibFile);
  215. index++;
  216. }
  217. // 用排序后的集合替换内存集合(注意:替换时未加锁是因为已在外部加锁或逻辑保证)
  218. _calibrations = new ObservableCollection<CalibrationInfo>(calibs);
  219. // 可选:删除物理文件和 toolblock(不自动删除)
  220. return result;
  221. }
  222. /// <summary>
  223. /// 加载所有校准数据:
  224. /// - 确保 CalibrationPath 存在
  225. /// - 读取索引文件,遍历项并加载各自的 cfg 与 .vpp(如果存在)
  226. /// - 忽略单个文件加载异常,继续加载其他文件
  227. /// </summary>
  228. public void LoadAll()
  229. {
  230. lock (_sync)
  231. {
  232. if (!Directory.Exists(Paths.CalibrationPath)) Directory.CreateDirectory(Paths.CalibrationPath);
  233. _calibrations.Clear();
  234. if (!File.Exists(Paths.CalibrationListFilePath))
  235. {
  236. // 若索引文件不存在,写入空索引并返回
  237. FileHelper.WriteJsonFile(new Dictionary<Guid, string>(), Paths.CalibrationListFilePath);
  238. return;
  239. }
  240. var list = FileHelper.ReadJsonFile<Dictionary<Guid, string>>(Paths.CalibrationListFilePath);
  241. if (list == null) return;
  242. foreach (var item in list)
  243. {
  244. try
  245. {
  246. // 使用 FilePath.CalibrationPath(项目全局路径)构建单个 cfg 路径
  247. string filePath = Path.Combine(FilePath.CalibrationPath, item.Value, item.Value + ".cfg");
  248. if (File.Exists(filePath))
  249. {
  250. var calib = FileHelper.ReadJsonFile<CalibrationInfo>(filePath);
  251. // 尝试加载对应的 .vpp ToolBlock
  252. string prcPath = Path.Combine(FilePath.CalibrationPath, calib.Name, calib.Name + ".vpp");
  253. if (File.Exists(prcPath))
  254. {
  255. calib.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
  256. }
  257. _calibrations.Add(calib);
  258. }
  259. }
  260. catch
  261. {
  262. // 忽略单个文件错误,继续加载其他文件
  263. }
  264. }
  265. }
  266. }
  267. /// <summary>
  268. /// 将内存中的所有校准保存到文件:
  269. /// - 为每个校准创建目录并写入 cfg
  270. /// - 若有 ToolBlock,则保存 .vpp
  271. /// - 写入索引文件
  272. /// </summary>
  273. public void SaveAll()
  274. {
  275. lock (_sync)
  276. {
  277. var list = new Dictionary<Guid, string>();
  278. foreach (var item in _calibrations)
  279. {
  280. list[item.Id] = item.Name;
  281. string folder = Path.Combine(FilePath.CalibrationPath, item.Name);
  282. if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
  283. string filePath = Path.Combine(folder, item.Name + ".cfg");
  284. FileHelper.WriteJsonFile(item, filePath);
  285. if (item.ToolBlock != null)
  286. {
  287. string prcPath = Path.Combine(folder, item.Name + ".vpp");
  288. VisionProFileHelper.SaveObjectToFileSafe(item.ToolBlock, prcPath);
  289. }
  290. }
  291. FileHelper.WriteJsonFile(list, Paths.CalibrationListFilePath);
  292. }
  293. }
  294. /// <summary>
  295. /// 校准转换:将像素坐标转换为机器人位置坐标(X,Y)及角度 U。
  296. /// 逻辑摘要:
  297. /// 1. 使用 calib.AffineTransformationMaterial 构建 3x3 仿射矩阵,将像素 [X,Y,1] 映射到机器人坐标系(单位:mm/像素转换后)。
  298. /// 2. 根据摄像机安装方式(CameraMount)进行不同的后处理:
  299. /// - MobileJ4: 采用基于 MarkPoint 的旋转矩阵,将像素直接转换的点旋转并平移到机器人当前 TOOL0 下的位置
  300. /// - MobileDown_XYPlatform: 类似 MobileJ4,但当前实现中角度置为 0(不旋转)
  301. /// - 其他情况:如果提供 robotCoord,则通过 ToolCoord 计算工具坐标(考虑机器人品牌对角度符号的影响)
  302. /// 3. 对于某些 CameraMount,输入角度需要取反(例如 FixedDown、MobileJ2、MobileJ4),以匹配机器人坐标系习惯
  303. /// </summary>
  304. /// <param name="pixelCoord">像素坐标及方向:(X, Y, angle)。angle 表示图像中测量的角度(度)</param>
  305. /// <param name="robotCoord">机器人当前位姿数组,通常为 [X, Y, U],可为 null(视场景而定)</param>
  306. /// <param name="calib">要使用的校准数据</param>
  307. /// <param name="robotBrand">机器人品牌,用于处理角度符号等差异</param>
  308. /// <returns>
  309. /// 返回元组 (IsSucceed, X, Y, U):
  310. /// - IsSucceed: 总是 true(当前实现未对数学运算做失败判定),调用者可根据需要扩展错误处理
  311. /// - X, Y: 转换后的机器人坐标(单位同 calib 所用单位)
  312. /// - U: 角度(经过可能的符号调整)
  313. /// </returns>
  314. public (bool IsSucceed,double X, double Y, double U) ConvertPixelToPosition((double X, double Y, double angle) pixelCoord, double[] robotCoord, CalibrationInfo calib, RobotBrand robotBrand = RobotBrand.Default)
  315. {
  316. double angle = pixelCoord.angle;
  317. // 从校准数据获取仿射变换矩阵(3x3)
  318. Matrix<double> matrix = Matrix<double>.Build.DenseOfArray(calib.AffineTransformationMaterial);
  319. // 某些安装方式需要取反角度以匹配机器人坐标系定义
  320. if (calib.CameraMount == CameraMount.FixedDown || calib.CameraMount == CameraMount.MobileJ2 || calib.CameraMount == CameraMount.MobileJ4)
  321. {
  322. angle *= -1;
  323. }
  324. // 将像素坐标通过仿射矩阵转换到机器人坐标(rpos 是长度 3 的向量:[X, Y, w],通常 w 为 1)
  325. var rpos = matrix * Vector<double>.Build.Dense(new double[] { pixelCoord.X, pixelCoord.Y, 1 });
  326. // 针对不同的 CameraMount 做进一步坐标变换
  327. if (calib.CameraMount == Enums.CameraMount.MobileJ4)
  328. {
  329. // 当 CameraMount 为 MobileJ4 时:
  330. // - rpos 的前两维表示像素->mm 转换后的点 (Robot_X, Robot_Y)
  331. // - 需要基于机器人当前 TOOL0 坐标以及 MarkPoint 的角度进行旋转和平移
  332. double Robot_X = rpos[0];
  333. double Robot_Y = rpos[1];
  334. // 机器人当前 TOOL0 下的位姿
  335. double curpos_x = robotCoord[0];
  336. double curpos_y = robotCoord[1];
  337. double curpos_u = robotCoord[2];
  338. // 旋转角(弧度) = (curpos_u - calib.MarkPoint.U) * PI / 180
  339. double angle1 = Math.PI * (curpos_u - calib.MarkPoint.U) / 180;
  340. // 2x2 旋转矩阵
  341. Matrix<double> rotationMatrix = Matrix<double>.Build.DenseOfArray(new double[,]
  342. {
  343. { Math.Cos(angle1), -Math.Sin(angle1) },
  344. { Math.Sin(angle1), Math.Cos(angle1) }
  345. });
  346. // 将相对向量 p3 旋转并平移到当前工具坐标系下
  347. Vector<double> p3 = Vector<double>.Build.Dense(new double[] { Robot_X - calib.CalibPoints[0].Robot.X, Robot_Y - calib.CalibPoints[0].Robot.Y });
  348. Vector<double> phere = Vector<double>.Build.Dense(new double[] { curpos_x, curpos_y });
  349. var cc = rotationMatrix * p3 + phere;
  350. rpos = Vector<double>.Build.Dense(new double[] { cc[0], cc[1] });
  351. }
  352. else if (calib.CameraMount == Enums.CameraMount.MobileDown_XYPlatform)
  353. {
  354. // MobileDown_XYPlatform 模式下,当前实现与 MobileJ4 类似,但角度暂时设为 0(不旋转)
  355. double Robot_X = rpos[0];
  356. double Robot_Y = rpos[1];
  357. double curpos_x = robotCoord[0];
  358. double curpos_y = robotCoord[1];
  359. double curpos_u = robotCoord[2];
  360. // 角度暂用 0(如有需要可基于 curpos_u - calib.MarkPoint.U 做旋转)
  361. double angle1 = 0;
  362. Matrix<double> rotationMatrix = Matrix<double>.Build.DenseOfArray(new double[,]
  363. {
  364. { Math.Cos(angle1), -Math.Sin(angle1) },
  365. { Math.Sin(angle1), Math.Cos(angle1) }
  366. });
  367. Vector<double> p3 = Vector<double>.Build.Dense(new double[] { Robot_X - calib.CalibPoints[0].Robot.X, Robot_Y - calib.CalibPoints[0].Robot.Y });
  368. Vector<double> phere = Vector<double>.Build.Dense(new double[] { curpos_x, curpos_y });
  369. var cc = rotationMatrix * p3 + phere;
  370. rpos = Vector<double>.Build.Dense(new double[] { cc[0], cc[1] }); // Mark 点在 tool0 下的坐标
  371. }
  372. else
  373. {
  374. // 其他 CameraMount:如果提供了 robotCoord,则将仿射变换得到的点转换为工具坐标
  375. if (robotCoord != null)
  376. {
  377. double curposX = robotCoord[0];
  378. double curposY = robotCoord[1];
  379. double curposU = robotCoord[2];
  380. // 使用 ToolCoord 计算工具坐标(ToolCoord 内封装了从机器人基坐标到工具坐标的数学)
  381. ToolCoord tool = new ToolCoord();
  382. // Schneider 机器人角度符号可能与其他品牌相反,做兼容处理
  383. if (robotBrand == RobotBrand.Schneider)
  384. {
  385. curposU *= -1;
  386. }
  387. // 计算工具坐标并将 rpos 替换为工具坐标的 X,Y
  388. tool.ComputeTool(curposX, curposY, curposU, rpos[0], rpos[1]);
  389. rpos = Vector<double>.Build.Dense(new double[] { tool.X, tool.Y });
  390. }
  391. }
  392. // 返回最终结果,IsSucceed 在当前实现中总为 true(可根据需要扩展)
  393. return (true, rpos[0], rpos[1], angle);
  394. }
  395. /// <summary>
  396. /// 通过 calibId 获取校准并执行像素->位置转换。
  397. /// </summary>
  398. /// <param name="pixelCoord">像素坐标及角度 (X, Y, angle)</param>
  399. /// <param name="robotCoord">机器人当前位姿数组 [X, Y, U]</param>
  400. /// <param name="calibId">校准项 Id</param>
  401. /// <param name="robotBrand">机器人品牌(可选)</param>
  402. /// <returns>返回 (IsSucceed, X, Y, U)</returns>
  403. public (bool IsSucceed, double X, double Y, double U) ConvertPixelToPosition((double X, double Y, double angle) pixelCoord, double[] robotCoord, Guid calibId, RobotBrand robotBrand = RobotBrand.Default)
  404. {
  405. var calib = GetCalibration(calibId);
  406. if (calib == null)
  407. {
  408. // 若未找到校准数据,返回失败标志
  409. return (false, 0, 0, 0);
  410. }
  411. var result = ConvertPixelToPosition(pixelCoord, robotCoord, calib, robotBrand);
  412. return result;
  413. }
  414. /// <summary>
  415. /// Dispose 时保存所有校准数据并抑制终结化。
  416. /// </summary>
  417. public void Dispose()
  418. {
  419. SaveAll();
  420. GC.SuppressFinalize(this);
  421. }
  422. }
  423. }