using System; namespace AutoFocusTool.Imaging { /// /// 清晰度评价(纯 C#,无 OpenCV 依赖)。 /// 按总方案:梯度类指标(Tenengrad 主 / Laplacian 备),锁 ROI、缩图、除均值归一化。 /// 别用灰度方差/对比度(会被"暗+高反差"骗)。 /// /// 输入统一为相机的 24bpp BGR 字节数组(未翻正也可,清晰度与上下翻转无关)。 /// public static class Sharpness { /// 清晰度算法种类 public enum Metric { Tenengrad, Laplacian } /// /// 计算清晰度分数(越大越清晰,已除均值归一化,可跨帧比较)。 /// /// 相机原始 24bpp BGR 字节 /// 图宽 /// 图高 /// ROI(像素坐标,null=全图)。只在 ROI 内算分,排除背景反光。 /// 指标类型 public static double Compute(byte[] bgr24, int width, int height, System.Drawing.Rectangle? roi = null, Metric metric = Metric.Tenengrad) { // 1) 转灰度(只取 ROI 区域,行优先) var (gray, gw, gh) = ToGrayRoi(bgr24, width, height, roi); if (gw < 3 || gh < 3) return 0; // 2) 梯度算分 return metric == Metric.Laplacian ? LaplacianScore(gray, gw, gh) : TenengradScore(gray, gw, gh); } /// BGR → 8bit 灰度,裁出 ROI。返回灰度数组+宽高。 private static (byte[] gray, int w, int h) ToGrayRoi( byte[] bgr24, int width, int height, System.Drawing.Rectangle? roi) { int x0 = 0, y0 = 0, w = width, h = height; if (roi.HasValue) { var r = roi.Value; x0 = Math.Max(0, r.X); y0 = Math.Max(0, r.Y); w = Math.Min(r.Width, width - x0); h = Math.Min(r.Height, height - y0); if (w <= 0 || h <= 0) { x0 = 0; y0 = 0; w = width; h = height; } } var gray = new byte[w * h]; int stride = width * 3; for (int y = 0; y < h; y++) { int srcRow = (y0 + y) * stride; int dstRow = y * w; for (int x = 0; x < w; x++) { int p = srcRow + (x0 + x) * 3; // BGR: 0.114B + 0.587G + 0.299R byte b = bgr24[p], g = bgr24[p + 1], rr = bgr24[p + 2]; gray[dstRow + x] = (byte)((b * 29 + g * 150 + rr * 77) >> 8); } } return (gray, w, h); } /// /// Tenengrad:Sobel 梯度幅值平方和,除以像素数与均值灰度(归一化)。 /// private static double TenengradScore(byte[] g, int w, int h) { double sumSq = 0; long sumGray = 0; for (int i = 0; i < g.Length; i++) sumGray += g[i]; double mean = (double)sumGray / g.Length; if (mean < 1e-6) mean = 1e-6; for (int y = 1; y < h - 1; y++) { int row = y * w; for (int x = 1; x < w - 1; x++) { int i = row + x; // Sobel Gx int gx = (g[i - w + 1] + 2 * g[i + 1] + g[i + w + 1]) - (g[i - w - 1] + 2 * g[i - 1] + g[i + w - 1]); // Sobel Gy int gy = (g[i + w - 1] + 2 * g[i + w] + g[i + w + 1]) - (g[i - w - 1] + 2 * g[i - w] + g[i - w + 1]); sumSq += (double)gx * gx + (double)gy * gy; } } int n = (w - 2) * (h - 2); if (n <= 0) return 0; // 除以像素数(消除 ROI 尺寸影响)+ 除以均值平方(消除亮度影响) return sumSq / n / (mean * mean); } /// /// Laplacian 方差:拉普拉斯响应的方差,除均值归一化。备用指标。 /// private static double LaplacianScore(byte[] g, int w, int h) { long sumGray = 0; for (int i = 0; i < g.Length; i++) sumGray += g[i]; double mean = (double)sumGray / g.Length; if (mean < 1e-6) mean = 1e-6; int n = (w - 2) * (h - 2); if (n <= 0) return 0; double sum = 0, sumSq = 0; for (int y = 1; y < h - 1; y++) { int row = y * w; for (int x = 1; x < w - 1; x++) { int i = row + x; int lap = 4 * g[i] - g[i - 1] - g[i + 1] - g[i - w] - g[i + w]; sum += lap; sumSq += (double)lap * lap; } } double m = sum / n; double variance = sumSq / n - m * m; return variance / (mean * mean); } } }