Sharpness.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. using System;
  2. namespace IvfTl.AutoFocus.Imaging
  3. {
  4. /// <summary>
  5. /// 清晰度评价(纯 C#,无 OpenCV 依赖)。
  6. /// 按总方案:梯度类指标(Tenengrad 主 / Laplacian 备),锁 ROI、缩图、除均值归一化。
  7. /// 别用灰度方差/对比度(会被"暗+高反差"骗)。
  8. ///
  9. /// 输入统一为相机的 24bpp BGR 字节数组(未翻正也可,清晰度与上下翻转无关)。
  10. /// 【移植说明 M2-01】逐字搬自 autofocustool/Imaging/Sharpness.cs,仅改命名空间。
  11. /// ÷mean 一次的真机修复(行尾)逐字保留(03 §1)。
  12. /// </summary>
  13. public static class Sharpness
  14. {
  15. /// <summary>清晰度算法种类</summary>
  16. public enum Metric { Tenengrad, Laplacian }
  17. /// <summary>
  18. /// 计算清晰度分数(越大越清晰,已除均值归一化,可跨帧比较)。
  19. /// </summary>
  20. /// <param name="bgr24">相机原始 24bpp BGR 字节</param>
  21. /// <param name="width">图宽</param>
  22. /// <param name="height">图高</param>
  23. /// <param name="roi">ROI(像素坐标,null=全图)。只在 ROI 内算分,排除背景反光。</param>
  24. /// <param name="metric">指标类型</param>
  25. public static double Compute(byte[] bgr24, int width, int height,
  26. System.Drawing.Rectangle? roi = null, Metric metric = Metric.Tenengrad)
  27. {
  28. // 1) 转灰度(只取 ROI 区域,行优先)
  29. var (gray, gw, gh) = ToGrayRoi(bgr24, width, height, roi);
  30. if (gw < 3 || gh < 3) return 0;
  31. // 2) 梯度算分
  32. return metric == Metric.Laplacian
  33. ? LaplacianScore(gray, gw, gh)
  34. : TenengradScore(gray, gw, gh);
  35. }
  36. /// <summary>BGR → 8bit 灰度,裁出 ROI。返回灰度数组+宽高。</summary>
  37. private static (byte[] gray, int w, int h) ToGrayRoi(
  38. byte[] bgr24, int width, int height, System.Drawing.Rectangle? roi)
  39. {
  40. int x0 = 0, y0 = 0, w = width, h = height;
  41. if (roi.HasValue)
  42. {
  43. var r = roi.Value;
  44. x0 = Math.Max(0, r.X);
  45. y0 = Math.Max(0, r.Y);
  46. w = Math.Min(r.Width, width - x0);
  47. h = Math.Min(r.Height, height - y0);
  48. if (w <= 0 || h <= 0) { x0 = 0; y0 = 0; w = width; h = height; }
  49. }
  50. var gray = new byte[w * h];
  51. int stride = width * 3;
  52. for (int y = 0; y < h; y++)
  53. {
  54. int srcRow = (y0 + y) * stride;
  55. int dstRow = y * w;
  56. for (int x = 0; x < w; x++)
  57. {
  58. int p = srcRow + (x0 + x) * 3;
  59. // BGR: 0.114B + 0.587G + 0.299R
  60. byte b = bgr24[p], g = bgr24[p + 1], rr = bgr24[p + 2];
  61. gray[dstRow + x] = (byte)((b * 29 + g * 150 + rr * 77) >> 8);
  62. }
  63. }
  64. return (gray, w, h);
  65. }
  66. /// <summary>
  67. /// Tenengrad:Sobel 梯度幅值平方和,除以像素数与均值灰度(归一化)。
  68. /// </summary>
  69. private static double TenengradScore(byte[] g, int w, int h)
  70. {
  71. double sumSq = 0;
  72. long sumGray = 0;
  73. for (int i = 0; i < g.Length; i++) sumGray += g[i];
  74. double mean = (double)sumGray / g.Length;
  75. if (mean < 1e-6) mean = 1e-6;
  76. for (int y = 1; y < h - 1; y++)
  77. {
  78. int row = y * w;
  79. for (int x = 1; x < w - 1; x++)
  80. {
  81. int i = row + x;
  82. // Sobel Gx
  83. int gx = (g[i - w + 1] + 2 * g[i + 1] + g[i + w + 1])
  84. - (g[i - w - 1] + 2 * g[i - 1] + g[i + w - 1]);
  85. // Sobel Gy
  86. int gy = (g[i + w - 1] + 2 * g[i + w] + g[i + w + 1])
  87. - (g[i - w - 1] + 2 * g[i - w] + g[i - w + 1]);
  88. sumSq += (double)gx * gx + (double)gy * gy;
  89. }
  90. }
  91. int n = (w - 2) * (h - 2);
  92. if (n <= 0) return 0;
  93. // 除以像素数(消除 ROI 尺寸影响)+ 除以均值(归一化亮度,但只除一次方)。
  94. // 【真机修正 2026-06-16】原为除以 mean*mean(均值平方)。实测 4 号舱 well1 的 Z 清晰度曲线
  95. // (TestData/zcurve_w1_*)显示:随 Z 增大画面亮度从 ~53 单调升到 ~185(光学效应),
  96. // mean² 会把高 Z 的清晰帧分数严重压低,导致算法把最暗最糊的低 Z 层(z=20000)误判为
  97. // 最清晰,真实焦面(z≈92000,原始梯度峰)反而落选 → 对焦落到错误位置。
  98. // 改为除以 mean(一次方):既保留对亮度的鲁棒性,又不过度压制高亮清晰帧,
  99. // 反算验证峰正确落在 92000。
  100. return sumSq / n / mean;
  101. }
  102. /// <summary>
  103. /// Laplacian 方差:拉普拉斯响应的方差,除均值归一化。备用指标。
  104. /// </summary>
  105. private static double LaplacianScore(byte[] g, int w, int h)
  106. {
  107. long sumGray = 0;
  108. for (int i = 0; i < g.Length; i++) sumGray += g[i];
  109. double mean = (double)sumGray / g.Length;
  110. if (mean < 1e-6) mean = 1e-6;
  111. int n = (w - 2) * (h - 2);
  112. if (n <= 0) return 0;
  113. double sum = 0, sumSq = 0;
  114. for (int y = 1; y < h - 1; y++)
  115. {
  116. int row = y * w;
  117. for (int x = 1; x < w - 1; x++)
  118. {
  119. int i = row + x;
  120. int lap = 4 * g[i] - g[i - 1] - g[i + 1] - g[i - w] - g[i + w];
  121. sum += lap;
  122. sumSq += (double)lap * lap;
  123. }
  124. }
  125. double m = sum / n;
  126. double variance = sumSq / n - m * m;
  127. return variance / (mean * mean);
  128. }
  129. }
  130. }