ExposureMeter.cs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. using System;
  2. namespace IvfTl.AutoFocus.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. /// 【移植说明 M2-01】逐字搬自 autofocustool/Imaging/ExposureMeter.cs,仅改命名空间;
  22. /// MeanLo=95/MeanHi=150/SatMax=2.0 一字不动(03 §1)。
  23. /// </summary>
  24. public static class ExposureMeter
  25. {
  26. public const double MeanLo = 95, MeanHi = 150; // 目标亮度区间(参考图well内约中灰)
  27. public const double SatMax = 2.0; // 饱和像素超过2%判过曝
  28. /// <summary>在 well 圆内(取内部 ratio 半径)评价曝光。circle 为 null 则用全图中心圆。</summary>
  29. public static ExposureInfo Measure(byte[] bgr24, int width, int height, WellCircle circle, double ratio = 0.7)
  30. {
  31. double cx, cy, r;
  32. if (circle != null && circle.Found)
  33. {
  34. cx = circle.Cx; cy = circle.Cy; r = circle.Radius * ratio;
  35. }
  36. else
  37. {
  38. cx = width / 2.0; cy = height / 2.0; r = Math.Min(width, height) * 0.25;
  39. }
  40. double r2 = r * r;
  41. int x0 = Math.Max(0, (int)(cx - r)), x1 = Math.Min(width - 1, (int)(cx + r));
  42. int y0 = Math.Max(0, (int)(cy - r)), y1 = Math.Min(height - 1, (int)(cy + r));
  43. int stride = width * 3;
  44. long sum = 0, n = 0, sat = 0, dark = 0;
  45. for (int y = y0; y <= y1; y++)
  46. {
  47. double dy = y - cy;
  48. for (int x = x0; x <= x1; x++)
  49. {
  50. double dx = x - cx;
  51. if (dx * dx + dy * dy > r2) continue;
  52. int p = y * stride + x * 3;
  53. int g = (bgr24[p] * 29 + bgr24[p + 1] * 150 + bgr24[p + 2] * 77) >> 8;
  54. sum += g; n++;
  55. if (g >= 250) sat++;
  56. if (g <= 5) dark++;
  57. }
  58. }
  59. var info = new ExposureInfo();
  60. if (n == 0) { info.State = ExposureInfo.Verdict.Under; return info; }
  61. info.Mean = (double)sum / n;
  62. info.SaturatedPct = 100.0 * sat / n;
  63. info.DarkPct = 100.0 * dark / n;
  64. if (info.SaturatedPct > SatMax || info.Mean > MeanHi) info.State = ExposureInfo.Verdict.Over;
  65. else if (info.Mean < MeanLo) info.State = ExposureInfo.Verdict.Under;
  66. else info.State = ExposureInfo.Verdict.Good;
  67. return info;
  68. }
  69. /// <summary>
  70. /// 二分法找合适曝光。给定 [lo,hi] 曝光范围 和 抓帧+评价的回调,
  71. /// 返回让 well 内亮度落入目标区间的曝光值。
  72. /// grabMeasure: 输入曝光值 → 设曝光抓帧并返回 ExposureInfo。
  73. /// </summary>
  74. public static int BinarySearch(int lo, int hi, Func<int, ExposureInfo> grabMeasure,
  75. Action<string> log = null, int maxIter = 8)
  76. {
  77. int best = (lo + hi) / 2; double bestDist = double.MaxValue;
  78. double target = (MeanLo + MeanHi) / 2;
  79. for (int i = 0; i < maxIter && lo <= hi; i++)
  80. {
  81. int mid = (lo + hi) / 2;
  82. var info = grabMeasure(mid);
  83. double dist = Math.Abs(info.Mean - target);
  84. if (dist < bestDist) { bestDist = dist; best = mid; }
  85. log?.Invoke($" 二分#{i + 1} 曝光={mid} {info}");
  86. if (info.State == ExposureInfo.Verdict.Good) return mid;
  87. if (info.State == ExposureInfo.Verdict.Over) hi = mid - 1; // 太亮→降曝光
  88. else lo = mid + 1; // 太暗→升曝光
  89. }
  90. return best;
  91. }
  92. }
  93. }