CheckerboardDetectionService.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. using TeamAAS_VP.Models;
  2. using OpenCvSharp;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using TeamAAS_VP.Resources.Languages;
  7. namespace TeamAAS_VP.Services
  8. {
  9. /// <summary>
  10. /// 棋盘格自动检测服务
  11. /// </summary>
  12. public class CheckerboardDetectionService
  13. {
  14. /// <summary>
  15. /// 自动检测棋盘格(整图)
  16. /// </summary>
  17. /// <param name="image">输入图像</param>
  18. /// <returns>棋盘格信息</returns>
  19. public ChessboardInfo DetectChessboard(Mat image)
  20. {
  21. var info = new ChessboardInfo { IsDetected = false };
  22. if (image == null || image.Empty())
  23. return info;
  24. try
  25. {
  26. // 转换为灰度图
  27. Mat gray = new Mat();
  28. if (image.Channels() == 3)
  29. {
  30. Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
  31. }
  32. else
  33. {
  34. gray = image.Clone();
  35. }
  36. // 尝试不同的棋盘格尺寸
  37. var possibleSizes = GeneratePossibleSizes(3, 15);
  38. foreach (var size in possibleSizes)
  39. {
  40. Point2f[] corners;
  41. bool found = Cv2.FindChessboardCornersSB(
  42. gray,
  43. size,
  44. out corners,
  45. ChessboardFlags.None);
  46. if (found && corners != null && corners.Length > 0)
  47. {
  48. // 亚像素精度优化
  49. var criteria = new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 30, 0.001);
  50. Cv2.CornerSubPix(gray, corners, new Size(11, 11), new Size(-1, -1), criteria);
  51. // 计算置信度
  52. double confidence = CalculateConfidence(gray, corners, size);
  53. // 只接受高置信度的检测结果
  54. if (confidence > 0.7)
  55. {
  56. info.IsDetected = true;
  57. info.CornersWidth = size.Width;
  58. info.CornersHeight = size.Height;
  59. info.Corners = corners;
  60. info.Confidence = confidence;
  61. info.BoundingRect = CalculateBoundingRect(corners);
  62. break;
  63. }
  64. }
  65. }
  66. gray?.Dispose();
  67. }
  68. catch (Exception ex)
  69. {
  70. Console.WriteLine(string.Format(Lang.棋盘格检测错误0,ex.Message));
  71. }
  72. return info;
  73. }
  74. /// <summary>
  75. /// 在指定旋转的 ROI 区域中检测棋盘格(支持旋转矩形)
  76. /// roi: ROI 的像素矩形(外接矩形)
  77. /// angleDegrees: ROI 相对于图像的旋转角度(度)
  78. /// </summary>
  79. public ChessboardInfo DetectChessboard(Mat image, OpenCvSharp.Rect roi, double angleDegrees)
  80. {
  81. var info = new ChessboardInfo { IsDetected = false };
  82. if (image == null || image.Empty())
  83. return info;
  84. try
  85. {
  86. // 提取旋转裁剪图像
  87. Point2f cropOrigin, rotationCenter;
  88. using (var crop = ExtractRotatedRoi(image, roi, angleDegrees, out cropOrigin, out rotationCenter))
  89. {
  90. if (crop == null || crop.Empty())
  91. return info;
  92. // 在裁剪图像上检测(整图检测函数会尝试多种规格)
  93. var subInfo = DetectChessboard(crop);
  94. if (subInfo == null || !subInfo.IsDetected)
  95. return info;
  96. // 将裁剪图像中的角点映射回原图坐标
  97. var mapped = MapCornersFromRotatedToOriginal(subInfo.Corners, cropOrigin, rotationCenter, angleDegrees);
  98. info.IsDetected = true;
  99. info.Corners = mapped;
  100. info.CornersWidth = subInfo.CornersWidth;
  101. info.CornersHeight = subInfo.CornersHeight;
  102. info.Confidence = subInfo.Confidence;
  103. // 重新计算边界
  104. info.BoundingRect = CalculateBoundingRect(info.Corners);
  105. }
  106. }
  107. catch (Exception ex)
  108. {
  109. Console.WriteLine(string.Format(Lang.旋转ROI检测错误0,ex.Message));
  110. }
  111. return info;
  112. }
  113. /// <summary>
  114. /// 从原图中提取旋转后的ROI(将图像绕ROI中心反向旋转后裁剪)
  115. /// 返回裁剪图像,并通过out参数返回裁剪在旋转后图像中的原点和旋转中心
  116. /// </summary>
  117. public Mat ExtractRotatedRoi(Mat src, OpenCvSharp.Rect roi, double angleDegrees, out Point2f cropOrigin, out Point2f rotationCenter)
  118. {
  119. cropOrigin = new Point2f(0, 0);
  120. rotationCenter = new Point2f(0, 0);
  121. if (src == null || src.Empty())
  122. return null;
  123. if (roi.Width <= 0 || roi.Height <= 0)
  124. return null;
  125. // compute center of ROI in source image coords
  126. float cx = roi.X + roi.Width / 2.0f;
  127. float cy = roi.Y + roi.Height / 2.0f;
  128. rotationCenter = new Point2f(cx, cy);
  129. // rotate entire image by -angle around center so that the rotated ROI becomes axis-aligned
  130. Mat rotated = new Mat();
  131. var M = Cv2.GetRotationMatrix2D(new Point2f(cx, cy), (float)-angleDegrees, 1.0);
  132. Cv2.WarpAffine(src, rotated, M, new Size(src.Width, src.Height), InterpolationFlags.Linear, BorderTypes.Constant, Scalar.All(0));
  133. // after rotation the ROI center remains at (cx,cy); crop axis-aligned rectangle with same width/height
  134. int x = (int)Math.Round(cx - roi.Width / 2.0);
  135. int y = (int)Math.Round(cy - roi.Height / 2.0);
  136. // clamp
  137. x = Math.Max(0, Math.Min(x, rotated.Width - 1));
  138. y = Math.Max(0, Math.Min(y, rotated.Height - 1));
  139. int w = Math.Min(roi.Width, rotated.Width - x);
  140. int h = Math.Min(roi.Height, rotated.Height - y);
  141. if (w <= 0 || h <= 0)
  142. {
  143. rotated.Dispose();
  144. return null;
  145. }
  146. cropOrigin = new Point2f(x, y);
  147. var crop = new Mat(rotated, new OpenCvSharp.Rect(x, y, w, h)).Clone();
  148. rotated.Dispose();
  149. return crop;
  150. }
  151. /// <summary>
  152. /// 将在旋转后裁剪图像中检测到的角点映射回原图像坐标
  153. /// </summary>
  154. public Point2f[] MapCornersFromRotatedToOriginal(Point2f[] cornersInCrop, Point2f cropOrigin, Point2f rotationCenter, double angleDegrees)
  155. {
  156. if (cornersInCrop == null || cornersInCrop.Length == 0)
  157. return cornersInCrop;
  158. var mapped = new Point2f[cornersInCrop.Length];
  159. double angleRad = angleDegrees * Math.PI / 180.0;
  160. double cos = Math.Cos(angleRad);
  161. double sin = Math.Sin(angleRad);
  162. for (int i = 0; i < cornersInCrop.Length; i++)
  163. {
  164. // position in rotated image (absolute)
  165. double rx = cropOrigin.X + cornersInCrop[i].X;
  166. double ry = cropOrigin.Y + cornersInCrop[i].Y;
  167. // translate to rotation center
  168. double dx = rx - rotationCenter.X;
  169. double dy = ry - rotationCenter.Y;
  170. // rotate back by +angle to original image coordinates
  171. double ox = cos * dx - sin * dy + rotationCenter.X;
  172. double oy = sin * dx + cos * dy + rotationCenter.Y;
  173. mapped[i] = new Point2f((float)ox, (float)oy);
  174. }
  175. return mapped;
  176. }
  177. /// <summary>
  178. /// 生成可能的棋盘格尺寸
  179. /// </summary>
  180. private List<Size> GeneratePossibleSizes(int minSize, int maxSize)
  181. {
  182. var sizes = new List<Size>();
  183. // 常见的棋盘格尺寸
  184. var commonSizes = new[]
  185. {
  186. new Size(9, 6), // 标准标定板
  187. new Size(8, 6),
  188. new Size(10, 7),
  189. new Size(11, 8),
  190. new Size(9, 7),
  191. new Size(7, 5),
  192. new Size(6, 4),
  193. new Size(8, 5),
  194. };
  195. sizes.AddRange(commonSizes);
  196. // 添加其他可能的尺寸
  197. for (int w = minSize; w <= maxSize; w++)
  198. {
  199. for (int h = minSize; h <= maxSize; h++)
  200. {
  201. var size = new Size(w, h);
  202. if (!sizes.Any(s => s.Width == w && s.Height == h))
  203. {
  204. sizes.Add(size);
  205. }
  206. }
  207. }
  208. return sizes;
  209. }
  210. /// <summary>
  211. /// 计算检测置信度
  212. /// </summary>
  213. private double CalculateConfidence(Mat gray, Point2f[] corners, Size patternSize)
  214. {
  215. if (corners == null || corners.Length == 0)
  216. return 0;
  217. double confidence = 0;
  218. try
  219. {
  220. // 1. 检查角点的规律性 (通过计算角点间距的标准差)
  221. var distances = new List<double>();
  222. for (int i = 0; i < corners.Length - 1; i++)
  223. {
  224. double dist = Math.Sqrt(
  225. Math.Pow(corners[i + 1].X - corners[i].X, 2) +
  226. Math.Pow(corners[i + 1].Y - corners[i].Y, 2));
  227. distances.Add(dist);
  228. }
  229. if (distances.Count > 0)
  230. {
  231. double avgDist = distances.Average();
  232. double variance = distances.Sum(d => Math.Pow(d - avgDist, 2)) / distances.Count;
  233. double stdDev = Math.Sqrt(variance);
  234. // 标准差越小,规律性越好
  235. double regularityScore = Math.Max(0, 1 - (stdDev / avgDist));
  236. confidence += regularityScore * 0.5;
  237. }
  238. // 2. 检查角点数量是否完全匹配
  239. int expectedCorners = patternSize.Width * patternSize.Height;
  240. if (corners.Length == expectedCorners)
  241. {
  242. confidence += 0.3;
  243. }
  244. // 3. 检查角点是否在图像范围内
  245. int validCorners = corners.Count(c =>
  246. c.X >= 0 && c.X < gray.Width &&
  247. c.Y >= 0 && c.Y < gray.Height);
  248. confidence += (validCorners / (double)corners.Length) * 0.2;
  249. }
  250. catch
  251. {
  252. confidence = 0.5; // 默认中等置信度
  253. }
  254. return Math.Min(1.0, confidence);
  255. }
  256. /// <summary>
  257. /// 计算边界矩形
  258. /// </summary>
  259. private OpenCvSharp.Rect CalculateBoundingRect(Point2f[] corners)
  260. {
  261. if (corners == null || corners.Length == 0)
  262. return new OpenCvSharp.Rect();
  263. float minX = corners.Min(c => c.X);
  264. float minY = corners.Min(c => c.Y);
  265. float maxX = corners.Max(c => c.X);
  266. float maxY = corners.Max(c => c.Y);
  267. return new OpenCvSharp.Rect(
  268. (int)minX,
  269. (int)minY,
  270. (int)(maxX - minX),
  271. (int)(maxY - minY));
  272. }
  273. /// <summary>
  274. /// 在图像上绘制棋盘格角点
  275. /// </summary>
  276. public void DrawChessboard(Mat image, ChessboardInfo info)
  277. {
  278. if (image == null || !info.IsDetected || info.Corners == null)
  279. return;
  280. try
  281. {
  282. var patternSize = new Size(info.CornersWidth, info.CornersHeight);
  283. Cv2.DrawChessboardCorners(image, patternSize, info.Corners, true);
  284. // 绘制边界矩形
  285. Cv2.Rectangle(image, info.BoundingRect, new Scalar(0, 255, 0), 2);
  286. // 显示信息
  287. string text = $"{info.CornersWidth}x{info.CornersHeight} ({info.Confidence:F2})";
  288. Cv2.PutText(image, text, new Point(10, 30),
  289. HersheyFonts.HersheySimplex, 0.8, new Scalar(0, 255, 0), 2);
  290. }
  291. catch (Exception ex)
  292. {
  293. Console.WriteLine(string.Format(Lang.绘制棋盘格错误0,ex.Message));
  294. }
  295. }
  296. }
  297. }