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;
}
}
}