ExposureMeter.cs 4.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. using System;
  2. namespace AutoFocusTool.Imaging
  3. {
  4. /// <summary>曝光评价结果。</summary>
  5. public class ExposureInfo
  6. {
  7. public double Mean; // ROI内平均灰度
  8. public double SaturatedPct; // 饱和(>=250)像素百分比
  9. public double DarkPct; // 死黑(<=5)像素百分比
  10. public Verdict State;
  11. public enum Verdict { Good, Over, Under }
  12. public override string ToString()
  13. => $"均值={Mean:F0} 饱和={SaturatedPct:F1}% 死黑={DarkPct:F1}% 判定={State}";
  14. }
  15. /// <summary>
  16. /// 曝光评价(在 well 圆内 ROI 上测)。
  17. /// 合格判据(参考图):well内中灰、无大片饱和。
  18. /// - 目标均值区间 [meanLo, meanHi]
  19. /// - 饱和像素比 < satMax
  20. /// 提供二分调节建议:给当前曝光返回下一个该试的曝光值。
  21. /// </summary>
  22. public static class ExposureMeter
  23. {
  24. public const double MeanLo = 95, MeanHi = 150; // 目标亮度区间(参考图well内约中灰)
  25. public const double SatMax = 2.0; // 饱和像素超过2%判过曝
  26. /// <summary>在 well 圆内(取内部 ratio 半径)评价曝光。circle 为 null 则用全图中心圆。</summary>
  27. public static ExposureInfo Measure(byte[] bgr24, int width, int height, WellCircle circle, double ratio = 0.7)
  28. {
  29. double cx, cy, r;
  30. if (circle != null && circle.Found)
  31. {
  32. cx = circle.Cx; cy = circle.Cy; r = circle.Radius * ratio;
  33. }
  34. else
  35. {
  36. cx = width / 2.0; cy = height / 2.0; r = Math.Min(width, height) * 0.25;
  37. }
  38. double r2 = r * r;
  39. int x0 = Math.Max(0, (int)(cx - r)), x1 = Math.Min(width - 1, (int)(cx + r));
  40. int y0 = Math.Max(0, (int)(cy - r)), y1 = Math.Min(height - 1, (int)(cy + r));
  41. int stride = width * 3;
  42. long sum = 0, n = 0, sat = 0, dark = 0;
  43. for (int y = y0; y <= y1; y++)
  44. {
  45. double dy = y - cy;
  46. for (int x = x0; x <= x1; x++)
  47. {
  48. double dx = x - cx;
  49. if (dx * dx + dy * dy > r2) continue;
  50. int p = y * stride + x * 3;
  51. int g = (bgr24[p] * 29 + bgr24[p + 1] * 150 + bgr24[p + 2] * 77) >> 8;
  52. sum += g; n++;
  53. if (g >= 250) sat++;
  54. if (g <= 5) dark++;
  55. }
  56. }
  57. var info = new ExposureInfo();
  58. if (n == 0) { info.State = ExposureInfo.Verdict.Under; return info; }
  59. info.Mean = (double)sum / n;
  60. info.SaturatedPct = 100.0 * sat / n;
  61. info.DarkPct = 100.0 * dark / n;
  62. if (info.SaturatedPct > SatMax || info.Mean > MeanHi) info.State = ExposureInfo.Verdict.Over;
  63. else if (info.Mean < MeanLo) info.State = ExposureInfo.Verdict.Under;
  64. else info.State = ExposureInfo.Verdict.Good;
  65. return info;
  66. }
  67. /// <summary>
  68. /// 二分法找合适曝光。给定 [lo,hi] 曝光范围 和 抓帧+评价的回调,
  69. /// 返回让 well 内亮度落入目标区间的曝光值。
  70. /// grabMeasure: 输入曝光值 → 设曝光抓帧并返回 ExposureInfo。
  71. /// </summary>
  72. public static int BinarySearch(int lo, int hi, Func<int, ExposureInfo> grabMeasure,
  73. Action<string> log = null, int maxIter = 8)
  74. {
  75. int best = (lo + hi) / 2; double bestDist = double.MaxValue;
  76. double target = (MeanLo + MeanHi) / 2;
  77. for (int i = 0; i < maxIter && lo <= hi; i++)
  78. {
  79. int mid = (lo + hi) / 2;
  80. var info = grabMeasure(mid);
  81. double dist = Math.Abs(info.Mean - target);
  82. if (dist < bestDist) { bestDist = dist; best = mid; }
  83. log?.Invoke($" 二分#{i + 1} 曝光={mid} {info}");
  84. if (info.State == ExposureInfo.Verdict.Good) return mid;
  85. if (info.State == ExposureInfo.Verdict.Over) hi = mid - 1; // 太亮→降曝光
  86. else lo = mid + 1; // 太暗→升曝光
  87. }
  88. return best;
  89. }
  90. }
  91. }