using System; namespace AutoFocusTool.Imaging { /// 曝光评价结果。 public class ExposureInfo { public double Mean; // ROI内平均灰度 public double SaturatedPct; // 饱和(>=250)像素百分比 public double DarkPct; // 死黑(<=5)像素百分比 public Verdict State; public enum Verdict { Good, Over, Under } public override string ToString() => $"均值={Mean:F0} 饱和={SaturatedPct:F1}% 死黑={DarkPct:F1}% 判定={State}"; } /// /// 曝光评价(在 well 圆内 ROI 上测)。 /// 合格判据(参考图):well内中灰、无大片饱和。 /// - 目标均值区间 [meanLo, meanHi] /// - 饱和像素比 < satMax /// 提供二分调节建议:给当前曝光返回下一个该试的曝光值。 /// public static class ExposureMeter { public const double MeanLo = 95, MeanHi = 150; // 目标亮度区间(参考图well内约中灰) public const double SatMax = 2.0; // 饱和像素超过2%判过曝 /// 在 well 圆内(取内部 ratio 半径)评价曝光。circle 为 null 则用全图中心圆。 public static ExposureInfo Measure(byte[] bgr24, int width, int height, WellCircle circle, double ratio = 0.7) { double cx, cy, r; if (circle != null && circle.Found) { cx = circle.Cx; cy = circle.Cy; r = circle.Radius * ratio; } else { cx = width / 2.0; cy = height / 2.0; r = Math.Min(width, height) * 0.25; } double r2 = r * r; int x0 = Math.Max(0, (int)(cx - r)), x1 = Math.Min(width - 1, (int)(cx + r)); int y0 = Math.Max(0, (int)(cy - r)), y1 = Math.Min(height - 1, (int)(cy + r)); int stride = width * 3; long sum = 0, n = 0, sat = 0, dark = 0; for (int y = y0; y <= y1; y++) { double dy = y - cy; for (int x = x0; x <= x1; x++) { double dx = x - cx; if (dx * dx + dy * dy > r2) continue; int p = y * stride + x * 3; int g = (bgr24[p] * 29 + bgr24[p + 1] * 150 + bgr24[p + 2] * 77) >> 8; sum += g; n++; if (g >= 250) sat++; if (g <= 5) dark++; } } var info = new ExposureInfo(); if (n == 0) { info.State = ExposureInfo.Verdict.Under; return info; } info.Mean = (double)sum / n; info.SaturatedPct = 100.0 * sat / n; info.DarkPct = 100.0 * dark / n; if (info.SaturatedPct > SatMax || info.Mean > MeanHi) info.State = ExposureInfo.Verdict.Over; else if (info.Mean < MeanLo) info.State = ExposureInfo.Verdict.Under; else info.State = ExposureInfo.Verdict.Good; return info; } /// /// 二分法找合适曝光。给定 [lo,hi] 曝光范围 和 抓帧+评价的回调, /// 返回让 well 内亮度落入目标区间的曝光值。 /// grabMeasure: 输入曝光值 → 设曝光抓帧并返回 ExposureInfo。 /// public static int BinarySearch(int lo, int hi, Func grabMeasure, Action log = null, int maxIter = 8) { int best = (lo + hi) / 2; double bestDist = double.MaxValue; double target = (MeanLo + MeanHi) / 2; for (int i = 0; i < maxIter && lo <= hi; i++) { int mid = (lo + hi) / 2; var info = grabMeasure(mid); double dist = Math.Abs(info.Mean - target); if (dist < bestDist) { bestDist = dist; best = mid; } log?.Invoke($" 二分#{i + 1} 曝光={mid} {info}"); if (info.State == ExposureInfo.Verdict.Good) return mid; if (info.State == ExposureInfo.Verdict.Over) hi = mid - 1; // 太亮→降曝光 else lo = mid + 1; // 太暗→升曝光 } return best; } } }