using System;
using System.Collections.Generic;
namespace AutoFocusTool.Imaging
{
/// well 圆检测结果。
public class WellCircle
{
public double Cx, Cy;
public double Radius;
public double FillRatio; // 连通域面积 / 外接圆面积,越接近1越圆
public bool Found;
public double OffsetX, OffsetY;
public double OffsetXPct, OffsetYPct;
public bool Complete; // well 完整在画面内
public double AreaPct; // 连通域占全图比例
public double Aspect, BoxFill, RConsist; // 调试:宽高比/外接框填充/半径一致性
public string RejectReason = "";
public override string ToString()
=> Found
? $"圆心({Cx:F0},{Cy:F0}) R={Radius:F0} 偏移X={OffsetXPct:F1}% Y={OffsetYPct:F1}% 完整={Complete} 圆度={FillRatio:F2} 面积={AreaPct:F1}%"
: "未检出well圆";
}
///
/// well 大圆检测(纯C#,无OpenCV)。
/// well 在背光下是一片中灰亮盘。方法:Otsu阈值 → 最大连通域(BFS) →
/// 由连通域的质心/面积/外接框算圆心半径,并用圆度+最小尺寸过滤,拒绝噪声小亮斑。
/// 关键改进:用"最大连通域"而非"全部亮像素质心",避免分散反光/小亮斑把圆心带偏。
///
public static class WellDetector
{
/// 检出阈值:连通域至少占全图比例、最小半径(下采样像素)、最低圆度。
public static double MinAreaPct = 4.0;
public static double MinFillRatio = 0.55;
public static WellCircle Detect(byte[] bgr24, int width, int height, int downsample = 4)
{
int dw = width / downsample, dh = height / downsample;
var gray = new byte[dw * dh];
int stride = width * 3;
int[] hist = new int[256];
for (int y = 0; y < dh; y++)
{
int sy = y * downsample;
for (int x = 0; x < dw; x++)
{
int p = sy * stride + (x * downsample) * 3;
byte b = bgr24[p], g = bgr24[p + 1], r = bgr24[p + 2];
byte gg = (byte)((b * 29 + g * 150 + r * 77) >> 8);
gray[y * dw + x] = gg;
hist[gg]++;
}
}
int th = Otsu(hist, dw * dh);
var res = new WellCircle();
// 最大连通域(BFS, 4邻域)
var label = new int[dw * dh];
int bestCnt = 0, bestX0 = 0, bestY0 = 0, bestX1 = 0, bestY1 = 0;
long bestSx = 0, bestSy = 0;
var queue = new Queue();
for (int idx = 0; idx < dw * dh; idx++)
{
if (gray[idx] < th || label[idx] != 0) continue;
// BFS
int cnt = 0; long sx = 0, sy = 0;
int minx = dw, miny = dh, maxx = 0, maxy = 0;
label[idx] = 1; queue.Enqueue(idx);
while (queue.Count > 0)
{
int q = queue.Dequeue();
int qx = q % dw, qy = q / dw;
cnt++; sx += qx; sy += qy;
if (qx < minx) minx = qx; if (qx > maxx) maxx = qx;
if (qy < miny) miny = qy; if (qy > maxy) maxy = qy;
if (qx > 0 && gray[q - 1] >= th && label[q - 1] == 0) { label[q - 1] = 1; queue.Enqueue(q - 1); }
if (qx < dw - 1 && gray[q + 1] >= th && label[q + 1] == 0) { label[q + 1] = 1; queue.Enqueue(q + 1); }
if (qy > 0 && gray[q - dw] >= th && label[q - dw] == 0) { label[q - dw] = 1; queue.Enqueue(q - dw); }
if (qy < dh - 1 && gray[q + dw] >= th && label[q + dw] == 0) { label[q + dw] = 1; queue.Enqueue(q + dw); }
}
if (cnt > bestCnt) { bestCnt = cnt; bestSx = sx; bestSy = sy; bestX0 = minx; bestY0 = miny; bestX1 = maxx; bestY1 = maxy; }
}
res.AreaPct = 100.0 * bestCnt / (dw * dh);
if (bestCnt < dw * dh * MinAreaPct / 100.0) { res.Found = false; return res; }
double cxd = (double)bestSx / bestCnt, cyd = (double)bestSy / bestCnt;
int bw = bestX1 - bestX0 + 1, bh = bestY1 - bestY0 + 1;
// 半径:外接框平均半宽 与 面积反推半径
double rBox = (bw + bh) / 4.0;
double rArea = Math.Sqrt(bestCnt / Math.PI);
double rd = (rBox + rArea) / 2;
// ── 排除斜纹反光带的双判据 ──
// 1) 外接框宽高比:真well≈1(圆),反光带细长偏离1
double aspect = (double)Math.Max(bw, bh) / Math.Max(1, Math.Min(bw, bh));
// 2) 圆度:连通域面积 / 外接框面积。实心圆≈π/4≈0.785;反光带稀疏远小于此
double boxFill = (double)bestCnt / (bw * bh);
// 3) 面积半径与外接框半径一致性:真圆两者接近,反光带差很多
double rConsist = Math.Min(rArea, rBox) / Math.Max(rArea, rBox);
res.FillRatio = boxFill; // 用外接框填充率作圆度指标
res.Aspect = aspect; res.BoxFill = boxFill; res.RConsist = rConsist;
// 半径合理范围:真well半径约占画面宽15~26%(实测~490/2592=19%)。
// 斜纹反光带半径常达34%(~870),用上限拦掉;过小的也排除。
double rFrac = rd * downsample / width;
// 真well判据:宽高比<1.5(够方) + 外接框填充>0.6(够实心) + 半径一致>0.65 + 半径占比在[0.10,0.28]
if (aspect > 1.5 || boxFill < 0.6 || rConsist < 0.65 || rFrac > 0.28 || rFrac < 0.10)
{
res.Found = false;
res.RejectReason = $"aspect={aspect:F2} boxFill={boxFill:F2} rConsist={rConsist:F2} rFrac={rFrac:F2}";
return res;
}
res.Cx = cxd * downsample;
res.Cy = cyd * downsample;
res.Radius = rd * downsample;
res.Found = true;
res.OffsetX = res.Cx - width / 2.0;
res.OffsetY = res.Cy - height / 2.0;
res.OffsetXPct = res.OffsetX / width * 100;
res.OffsetYPct = res.OffsetY / height * 100;
// 完整性:外接框是否触画面边(下采样坐标)
res.Complete = bestX0 > 1 && bestY0 > 1 && bestX1 < dw - 2 && bestY1 < dh - 2;
return res;
}
private static int Otsu(int[] hist, int total)
{
long sum = 0;
for (int i = 0; i < 256; i++) sum += (long)i * hist[i];
long sumB = 0; int wB = 0; double maxVar = 0; int th = 0;
for (int t = 0; t < 256; t++)
{
wB += hist[t];
if (wB == 0) continue;
int wF = total - wB;
if (wF == 0) break;
sumB += (long)t * hist[t];
double mB = (double)sumB / wB;
double mF = (double)(sum - sumB) / wF;
double between = (double)wB * wF * (mB - mF) * (mB - mF);
if (between > maxVar) { maxVar = between; th = t; }
}
return th;
}
}
}