CameraFocusAnalyzer.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using OpenCvSharp;
  7. namespace TeamAAS_VP.Core
  8. {
  9. public class CameraFocusAnalyzer
  10. {
  11. // 清晰度评估方法枚举
  12. public enum FocusMethod
  13. {
  14. Tenengrad, // Tenengrad梯度法(推荐)
  15. Laplacian, // 拉普拉斯方差法
  16. Brenner, // Brenner梯度法
  17. GrayVariance, // 灰度方差法
  18. SMD // 灰度差分法
  19. }
  20. /// <summary>
  21. /// 计算图像清晰度(多种方法)
  22. /// </summary>
  23. public static double CalculateImageSharpness(Mat image, FocusMethod method = FocusMethod.Tenengrad, Rect? roi = null)
  24. {
  25. if (image == null || image.Empty())
  26. throw new ArgumentException("输入图像无效");
  27. // 转换为灰度图
  28. Mat gray = new Mat();
  29. if (image.Channels() == 3)
  30. Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
  31. else
  32. gray = image.Clone();
  33. // 应用ROI
  34. if (roi.HasValue)
  35. {
  36. gray = new Mat(gray, roi.Value);
  37. }
  38. double sharpness = 0;
  39. switch (method)
  40. {
  41. case FocusMethod.Tenengrad:
  42. sharpness = CalculateTenengrad(gray);
  43. break;
  44. case FocusMethod.Laplacian:
  45. sharpness = CalculateLaplacianVariance(gray);
  46. break;
  47. case FocusMethod.Brenner:
  48. sharpness = CalculateBrenner(gray);
  49. break;
  50. case FocusMethod.GrayVariance:
  51. sharpness = CalculateGrayVariance(gray);
  52. break;
  53. case FocusMethod.SMD:
  54. sharpness = CalculateSMD(gray);
  55. break;
  56. }
  57. gray.Dispose();
  58. return sharpness;
  59. }
  60. /// <summary>
  61. /// Tenengrad梯度法(对噪声敏感度较低)
  62. /// </summary>
  63. private static double CalculateTenengrad(Mat gray)
  64. {
  65. Mat gradX = new Mat();
  66. Mat gradY = new Mat();
  67. Mat magnitude = new Mat();
  68. Cv2.Sobel(gray, gradX, MatType.CV_32F, 1, 0, 3);
  69. Cv2.Sobel(gray, gradY, MatType.CV_32F, 0, 1, 3);
  70. Cv2.Magnitude(gradX, gradY, magnitude);
  71. Scalar mean = Cv2.Mean(magnitude);
  72. gradX.Dispose();
  73. gradY.Dispose();
  74. magnitude.Dispose();
  75. return mean.Val0;
  76. }
  77. /// <summary>
  78. /// 拉普拉斯方差法(对边缘敏感)
  79. /// </summary>
  80. private static double CalculateLaplacianVariance(Mat gray)
  81. {
  82. Mat laplacian = new Mat();
  83. Cv2.Laplacian(gray, laplacian, MatType.CV_32F);
  84. Mat mean = new Mat();
  85. Mat stdDev = new Mat();
  86. Cv2.MeanStdDev(laplacian, mean, stdDev);
  87. double variance = stdDev.Get<double>(0);
  88. variance = variance * variance;
  89. laplacian.Dispose();
  90. mean.Dispose();
  91. stdDev.Dispose();
  92. return variance;
  93. }
  94. /// <summary>
  95. /// Brenner梯度法(简单快速)
  96. /// </summary>
  97. private static double CalculateBrenner(Mat gray)
  98. {
  99. double sum = 0;
  100. int width = gray.Cols;
  101. int height = gray.Rows;
  102. for (int y = 0; y < height; y++)
  103. {
  104. for (int x = 0; x < width - 2; x++)
  105. {
  106. int diff = gray.Get<byte>(y, x + 2) - gray.Get<byte>(y, x);
  107. sum += diff * diff;
  108. }
  109. }
  110. return sum / (width * height);
  111. }
  112. /// <summary>
  113. /// 灰度方差法
  114. /// </summary>
  115. private static double CalculateGrayVariance(Mat gray)
  116. {
  117. Mat mean = new Mat();
  118. Mat stdDev = new Mat();
  119. Cv2.MeanStdDev(gray, mean, stdDev);
  120. double variance = stdDev.Get<double>(0);
  121. variance = variance * variance;
  122. mean.Dispose();
  123. stdDev.Dispose();
  124. return variance;
  125. }
  126. /// <summary>
  127. /// 灰度差分法(SMD)
  128. /// </summary>
  129. private static double CalculateSMD(Mat gray)
  130. {
  131. double sum = 0;
  132. int width = gray.Cols;
  133. int height = gray.Rows;
  134. for (int y = 0; y < height; y++)
  135. {
  136. for (int x = 0; x < width - 1; x++)
  137. {
  138. sum += Math.Abs(gray.Get<byte>(y, x + 1) - gray.Get<byte>(y, x));
  139. }
  140. }
  141. for (int y = 0; y < height - 1; y++)
  142. {
  143. for (int x = 0; x < width; x++)
  144. {
  145. sum += Math.Abs(gray.Get<byte>(y + 1, x) - gray.Get<byte>(y, x));
  146. }
  147. }
  148. return sum / (width * height);
  149. }
  150. /// <summary>
  151. /// 智能棋盘格对比度分析(简化版)
  152. /// </summary>
  153. /// <param name="image">输入图像</param>
  154. /// <param name="roi">检测区域(可选)</param>
  155. /// <returns>对比度分析结果,包含黑白均值、对比度和亮度建议</returns>
  156. public static CheckerboardContrastResult AnalyzeCheckerboardContrast(Mat image, Rect? roi = null)
  157. {
  158. var result = new CheckerboardContrastResult
  159. {
  160. Success = false,
  161. BlackMean = 0,
  162. WhiteMean = 0,
  163. Contrast = 0,
  164. OptimalBrightness = 128
  165. };
  166. if (image == null || image.Empty())
  167. return result;
  168. try
  169. {
  170. // 转换为灰度图
  171. Mat gray = new Mat();
  172. if (image.Channels() == 3)
  173. Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
  174. else
  175. gray = image.Clone();
  176. // 应用ROI
  177. if (roi.HasValue)
  178. {
  179. gray = new Mat(gray, roi.Value);
  180. }
  181. // 1. 自适应检测棋盘格角点
  182. var corners = DetectCheckerboardCorners(gray);
  183. if (corners == null || corners.Length < 20) // 至少需要一定数量的角点
  184. {
  185. gray.Dispose();
  186. return result;
  187. }
  188. // 2. 自动分析网格结构
  189. var gridInfo = AnalyzeGridStructure(corners);
  190. if (!gridInfo.IsValid)
  191. {
  192. gray.Dispose();
  193. return result;
  194. }
  195. // 3. 采样黑白格子像素
  196. SampleCheckerboardColors(gray, corners, gridInfo,
  197. out double blackMean, out double whiteMean, out int sampleCount);
  198. if (sampleCount > 0)
  199. {
  200. result.BlackMean = blackMean;
  201. result.WhiteMean = whiteMean;
  202. result.Contrast = Math.Abs(whiteMean - blackMean);
  203. result.OptimalBrightness = CalculateOptimalBrightness(blackMean, whiteMean);
  204. result.Success = true;
  205. // 计算图像质量评分
  206. result.QualityScore = CalculateQualityScore(result.Contrast, blackMean, whiteMean);
  207. }
  208. gray.Dispose();
  209. return result;
  210. }
  211. catch (Exception ex)
  212. {
  213. Console.WriteLine($"棋盘格分析出错: {ex.Message}");
  214. return result;
  215. }
  216. }
  217. /// <summary>
  218. /// 自适应检测棋盘格角点
  219. /// </summary>
  220. private static Point2f[] DetectCheckerboardCorners(Mat gray)
  221. {
  222. // 首先尝试标准的棋盘格检测
  223. for (int boardWidth = 5; boardWidth <= 15; boardWidth++)
  224. {
  225. for (int boardHeight = 5; boardHeight <= 15; boardHeight++)
  226. {
  227. Size patternSize = new Size(boardWidth, boardHeight);
  228. Point2f[] corners;
  229. bool found = Cv2.FindChessboardCorners(gray, patternSize, out corners,
  230. ChessboardFlags.AdaptiveThresh |
  231. ChessboardFlags.NormalizeImage |
  232. ChessboardFlags.FastCheck);
  233. if (found && corners.Length > 0)
  234. {
  235. // 提高角点精度
  236. Mat grayFloat = new Mat();
  237. gray.ConvertTo(grayFloat, MatType.CV_32F);
  238. Cv2.CornerSubPix(grayFloat, corners, new Size(5, 5), new Size(-1, -1),
  239. new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 30, 0.1));
  240. grayFloat.Dispose();
  241. return corners;
  242. }
  243. }
  244. }
  245. // 如果标准方法失败,尝试使用角点检测
  246. return DetectCornersByHarris(gray);
  247. }
  248. /// <summary>
  249. /// 使用Harris角点检测作为备选方案
  250. /// </summary>
  251. private static Point2f[] DetectCornersByHarris(Mat gray)
  252. {
  253. Mat cornersMat = new Mat();
  254. Mat grayFloat = new Mat();
  255. gray.ConvertTo(grayFloat, MatType.CV_32F);
  256. // Harris角点检测
  257. Cv2.CornerHarris(grayFloat, cornersMat, 2, 3, 0.04);
  258. // 找到局部最大值
  259. var corners = new List<Point2f>();
  260. double maxVal = 0;
  261. Cv2.MinMaxLoc(cornersMat, out _, out maxVal);
  262. float threshold = (float)(maxVal * 0.01);
  263. for (int y = 0; y < cornersMat.Rows; y++)
  264. {
  265. for (int x = 0; x < cornersMat.Cols; x++)
  266. {
  267. float value = cornersMat.Get<float>(y, x);
  268. if (value > threshold)
  269. {
  270. // 检查是否是局部最大值
  271. bool isLocalMax = true;
  272. int neighborhood = 5;
  273. for (int dy = -neighborhood; dy <= neighborhood && isLocalMax; dy++)
  274. {
  275. for (int dx = -neighborhood; dx <= neighborhood && isLocalMax; dx++)
  276. {
  277. int ny = y + dy;
  278. int nx = x + dx;
  279. if (ny >= 0 && ny < cornersMat.Rows &&
  280. nx >= 0 && nx < cornersMat.Cols &&
  281. !(dy == 0 && dx == 0))
  282. {
  283. if (cornersMat.Get<float>(ny, nx) > value)
  284. {
  285. isLocalMax = false;
  286. }
  287. }
  288. }
  289. }
  290. if (isLocalMax)
  291. {
  292. corners.Add(new Point2f(x, y));
  293. }
  294. }
  295. }
  296. }
  297. grayFloat.Dispose();
  298. cornersMat.Dispose();
  299. return corners.ToArray();
  300. }
  301. /// <summary>
  302. /// 分析网格结构
  303. /// </summary>
  304. private static GridStructureInfo AnalyzeGridStructure(Point2f[] corners)
  305. {
  306. var info = new GridStructureInfo();
  307. if (corners.Length < 9) // 至少3x3网格
  308. return info;
  309. // 估计网格间距
  310. List<double> distances = new List<double>();
  311. for (int i = 0; i < corners.Length; i++)
  312. {
  313. for (int j = i + 1; j < Math.Min(i + 10, corners.Length); j++) // 限制检查数量
  314. {
  315. double dist = Math.Sqrt(Math.Pow(corners[j].X - corners[i].X, 2) +
  316. Math.Pow(corners[j].Y - corners[i].Y, 2));
  317. if (dist > 5) // 忽略太近的点
  318. distances.Add(dist);
  319. }
  320. }
  321. if (distances.Count == 0)
  322. return info;
  323. // 找到最常见的距离作为网格间距
  324. distances.Sort();
  325. double estimatedSpacing = distances[distances.Count / 2]; // 中位数
  326. // 对角落点进行排序(按大致位置)
  327. var sortedCorners = corners.OrderBy(p => p.Y).ThenBy(p => p.X).ToArray();
  328. // 尝试分组到行和列
  329. double tolerance = estimatedSpacing * 0.3;
  330. info.IsValid = true;
  331. info.EstimatedSpacing = estimatedSpacing;
  332. info.Corners = sortedCorners;
  333. return info;
  334. }
  335. /// <summary>
  336. /// 采样棋盘格颜色
  337. /// </summary>
  338. private static void SampleCheckerboardColors(Mat gray, Point2f[] corners, GridStructureInfo gridInfo,
  339. out double blackMean, out double whiteMean, out int sampleCount)
  340. {
  341. blackMean = 0;
  342. whiteMean = 0;
  343. int blackCount = 0;
  344. int whiteCount = 0;
  345. sampleCount = 0;
  346. double spacing = gridInfo.EstimatedSpacing;
  347. int sampleRadius = (int)(spacing * 0.3);
  348. // 对每个角点区域采样
  349. for (int i = 0; i < corners.Length; i++)
  350. {
  351. int x = (int)corners[i].X;
  352. int y = (int)corners[i].Y;
  353. // 采样区域
  354. int x1 = Math.Max(0, x - sampleRadius);
  355. int y1 = Math.Max(0, y - sampleRadius);
  356. int x2 = Math.Min(gray.Cols - 1, x + sampleRadius);
  357. int y2 = Math.Min(gray.Rows - 1, y + sampleRadius);
  358. if (x2 > x1 && y2 > y1)
  359. {
  360. Rect roi = new Rect(x1, y1, x2 - x1, y2 - y1);
  361. Mat sample = new Mat(gray, roi);
  362. Scalar mean = Cv2.Mean(sample);
  363. sample.Dispose();
  364. double value = mean.Val0;
  365. // 根据位置判断黑白(奇偶性)
  366. int gridX = (int)(x / spacing);
  367. int gridY = (int)(y / spacing);
  368. bool isBlack = (gridX + gridY) % 2 == 0;
  369. if (isBlack)
  370. {
  371. blackMean += value;
  372. blackCount++;
  373. }
  374. else
  375. {
  376. whiteMean += value;
  377. whiteCount++;
  378. }
  379. sampleCount++;
  380. }
  381. }
  382. if (blackCount > 0) blackMean /= blackCount;
  383. if (whiteCount > 0) whiteMean /= whiteCount;
  384. // 确保黑色比白色暗
  385. if (blackMean > whiteMean)
  386. {
  387. // 交换
  388. double temp = blackMean;
  389. blackMean = whiteMean;
  390. whiteMean = temp;
  391. }
  392. }
  393. /// <summary>
  394. /// 计算最佳亮度
  395. /// </summary>
  396. private static int CalculateOptimalBrightness(double blackMean, double whiteMean)
  397. {
  398. double currentMid = (blackMean + whiteMean) / 2;
  399. int optimal = (int)Clamp(128 + (128 - currentMid), 30, 225);
  400. return optimal;
  401. }
  402. public static double Clamp(double value, double min, double max)
  403. {
  404. return value < min ? min : (value > max ? max : value);
  405. }
  406. /// <summary>
  407. /// 计算图像质量评分
  408. /// </summary>
  409. private static double CalculateQualityScore(double contrast, double blackMean, double whiteMean)
  410. {
  411. // 对比度评分(0-100)
  412. double contrastScore = Math.Min(100, contrast / 2.55);
  413. // 亮度范围评分
  414. double blackScore = Math.Max(0, 100 - Math.Abs(blackMean - 30));
  415. double whiteScore = Math.Max(0, 100 - Math.Abs(whiteMean - 220));
  416. double rangeScore = (blackScore + whiteScore) / 2;
  417. // 综合评分
  418. double qualityScore = contrastScore * 0.6 + rangeScore * 0.4;
  419. return Math.Min(100, Math.Max(0, qualityScore));
  420. }
  421. /// <summary>
  422. /// 一键获取图像质量报告
  423. /// </summary>
  424. public static ImageQualityReport GetImageQualityReport(Mat image, Rect? roi = null)
  425. {
  426. var report = new ImageQualityReport();
  427. if (image == null || image.Empty())
  428. return report;
  429. // 计算清晰度
  430. report.Sharpness = CalculateImageSharpness(image, FocusMethod.Tenengrad, roi);
  431. // 分析棋盘格对比度
  432. var contrastResult = AnalyzeCheckerboardContrast(image, roi);
  433. if (contrastResult.Success)
  434. {
  435. report.HasCheckerboard = true;
  436. report.BlackMean = contrastResult.BlackMean;
  437. report.WhiteMean = contrastResult.WhiteMean;
  438. report.Contrast = contrastResult.Contrast;
  439. report.OptimalBrightness = contrastResult.OptimalBrightness;
  440. report.QualityScore = contrastResult.QualityScore;
  441. }
  442. else
  443. {
  444. // 如果没有检测到棋盘格,使用图像整体对比度
  445. report.HasCheckerboard = false;
  446. report.Contrast = CalculateGlobalContrast(image, roi);
  447. report.OptimalBrightness = EstimateOptimalBrightness(image, roi);
  448. report.QualityScore = Math.Min(100, report.Contrast / 2.0);
  449. }
  450. // 给出建议
  451. report.Suggestions = GenerateSuggestions(report);
  452. return report;
  453. }
  454. /// <summary>
  455. /// 计算全局对比度(无棋盘格时使用)
  456. /// </summary>
  457. private static double CalculateGlobalContrast(Mat image, Rect? roi = null)
  458. {
  459. Mat gray = new Mat();
  460. if (image.Channels() == 3)
  461. Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
  462. else
  463. gray = image.Clone();
  464. if (roi.HasValue)
  465. {
  466. gray = new Mat(gray, roi.Value);
  467. }
  468. Mat mean = new Mat();
  469. Mat stdDev = new Mat();
  470. Cv2.MeanStdDev(gray, mean, stdDev);
  471. double contrast = stdDev.Get<double>(0) * 2; // 标准差的两倍作为对比度估计
  472. gray.Dispose();
  473. mean.Dispose();
  474. stdDev.Dispose();
  475. return contrast;
  476. }
  477. /// <summary>
  478. /// 估计最佳亮度(无棋盘格时使用)
  479. /// </summary>
  480. private static int EstimateOptimalBrightness(Mat image, Rect? roi = null)
  481. {
  482. Mat gray = new Mat();
  483. if (image.Channels() == 3)
  484. Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
  485. else
  486. gray = image.Clone();
  487. if (roi.HasValue)
  488. {
  489. gray = new Mat(gray, roi.Value);
  490. }
  491. Scalar mean = Cv2.Mean(gray);
  492. double avgGray = mean.Val0;
  493. // 根据平均灰度调整
  494. int optimal;
  495. if (avgGray < 80)
  496. optimal = (int)Math.Min(255, avgGray + 80);
  497. else if (avgGray > 180)
  498. optimal = (int)Math.Max(0, avgGray - 80);
  499. else
  500. optimal = 128;
  501. gray.Dispose();
  502. return optimal;
  503. }
  504. /// <summary>
  505. /// 生成调整建议
  506. /// </summary>
  507. private static List<string> GenerateSuggestions(ImageQualityReport report)
  508. {
  509. var suggestions = new List<string>();
  510. // 清晰度建议
  511. if (report.Sharpness < 10)
  512. suggestions.Add("图像模糊,请调整焦距使图像变清晰");
  513. else if (report.Sharpness < 20)
  514. suggestions.Add("清晰度一般,可以继续微调焦距");
  515. else
  516. suggestions.Add("图像清晰度良好");
  517. // 对比度建议
  518. if (report.HasCheckerboard)
  519. {
  520. if (report.Contrast < 50)
  521. suggestions.Add("棋盘格对比度过低,建议调整光源或相机参数");
  522. else if (report.Contrast < 100)
  523. suggestions.Add("棋盘格对比度适中");
  524. else
  525. suggestions.Add("棋盘格对比度很好");
  526. suggestions.Add($"建议亮度值: {report.OptimalBrightness}");
  527. }
  528. else
  529. {
  530. if (report.Contrast < 30)
  531. suggestions.Add("图像整体对比度较低");
  532. else
  533. suggestions.Add("图像对比度正常");
  534. }
  535. // 质量评分建议
  536. if (report.QualityScore < 60)
  537. suggestions.Add($"图像质量需要改进,当前评分: {report.QualityScore:F1}/100");
  538. else if (report.QualityScore < 80)
  539. suggestions.Add($"图像质量良好,当前评分: {report.QualityScore:F1}/100");
  540. else
  541. suggestions.Add($"图像质量优秀,当前评分: {report.QualityScore:F1}/100");
  542. return suggestions;
  543. }
  544. }
  545. // 辅助数据结构
  546. public class CheckerboardContrastResult
  547. {
  548. public bool Success { get; set; }
  549. public double BlackMean { get; set; }
  550. public double WhiteMean { get; set; }
  551. public double Contrast { get; set; }
  552. public int OptimalBrightness { get; set; }
  553. public double QualityScore { get; set; }
  554. }
  555. public class GridStructureInfo
  556. {
  557. public bool IsValid { get; set; }
  558. public double EstimatedSpacing { get; set; }
  559. public Point2f[] Corners { get; set; }
  560. }
  561. public class ImageQualityReport
  562. {
  563. public double Sharpness { get; set; }
  564. public bool HasCheckerboard { get; set; }
  565. public double BlackMean { get; set; }
  566. public double WhiteMean { get; set; }
  567. public double Contrast { get; set; }
  568. public int OptimalBrightness { get; set; }
  569. public double QualityScore { get; set; }
  570. public List<string> Suggestions { get; set; } = new List<string>();
  571. }
  572. }