CheckerboardDetectionService.cs 12 KB

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