WellDetector.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. using System;
  2. using System.Collections.Generic;
  3. namespace IvfTl.AutoFocus.Imaging
  4. {
  5. /// <summary>well 圆检测结果。</summary>
  6. public class WellCircle
  7. {
  8. public double Cx, Cy;
  9. public double Radius;
  10. public double FillRatio; // 连通域面积 / 外接圆面积,越接近1越圆
  11. public bool Found;
  12. public double OffsetX, OffsetY;
  13. public double OffsetXPct, OffsetYPct;
  14. public bool Complete; // well 完整在画面内
  15. public double AreaPct; // 连通域占全图比例
  16. public double Aspect, BoxFill, RConsist; // 调试:宽高比/外接框填充/半径一致性
  17. public string RejectReason = "";
  18. public override string ToString()
  19. => Found
  20. ? $"圆心({Cx:F0},{Cy:F0}) R={Radius:F0} 偏移X={OffsetXPct:F1}% Y={OffsetYPct:F1}% 完整={Complete} 圆度={FillRatio:F2} 面积={AreaPct:F1}%"
  21. : "未检出well圆";
  22. }
  23. /// <summary>
  24. /// well 大圆检测(纯C#,无OpenCV)。
  25. /// well 在背光下是一片中灰亮盘。方法:Otsu阈值 → 最大连通域(BFS) →
  26. /// 由连通域的质心/面积/外接框算圆心半径,并用圆度+最小尺寸过滤,拒绝噪声小亮斑。
  27. /// 关键改进:用"最大连通域"而非"全部亮像素质心",避免分散反光/小亮斑把圆心带偏。
  28. /// 【移植说明 M2-01】逐字搬自 autofocustool/Imaging/WellDetector.cs,仅改命名空间;
  29. /// 判据常数(MinAreaPct/aspect/boxFill/rConsist/rFrac 阈值)一字不动(03 §1)。
  30. /// </summary>
  31. public static class WellDetector
  32. {
  33. /// <summary>检出阈值:连通域至少占全图比例、最小半径(下采样像素)、最低圆度。</summary>
  34. public static double MinAreaPct = 4.0;
  35. public static double MinFillRatio = 0.55;
  36. public static WellCircle Detect(byte[] bgr24, int width, int height, int downsample = 4)
  37. {
  38. int dw = width / downsample, dh = height / downsample;
  39. var gray = new byte[dw * dh];
  40. int stride = width * 3;
  41. int[] hist = new int[256];
  42. for (int y = 0; y < dh; y++)
  43. {
  44. int sy = y * downsample;
  45. for (int x = 0; x < dw; x++)
  46. {
  47. int p = sy * stride + (x * downsample) * 3;
  48. byte b = bgr24[p], g = bgr24[p + 1], r = bgr24[p + 2];
  49. byte gg = (byte)((b * 29 + g * 150 + r * 77) >> 8);
  50. gray[y * dw + x] = gg;
  51. hist[gg]++;
  52. }
  53. }
  54. int th = Otsu(hist, dw * dh);
  55. var res = new WellCircle();
  56. // 最大连通域(BFS, 4邻域)
  57. var label = new int[dw * dh];
  58. int bestCnt = 0, bestX0 = 0, bestY0 = 0, bestX1 = 0, bestY1 = 0;
  59. long bestSx = 0, bestSy = 0;
  60. var queue = new Queue<int>();
  61. for (int idx = 0; idx < dw * dh; idx++)
  62. {
  63. if (gray[idx] < th || label[idx] != 0) continue;
  64. // BFS
  65. int cnt = 0; long sx = 0, sy = 0;
  66. int minx = dw, miny = dh, maxx = 0, maxy = 0;
  67. label[idx] = 1; queue.Enqueue(idx);
  68. while (queue.Count > 0)
  69. {
  70. int q = queue.Dequeue();
  71. int qx = q % dw, qy = q / dw;
  72. cnt++; sx += qx; sy += qy;
  73. if (qx < minx) minx = qx; if (qx > maxx) maxx = qx;
  74. if (qy < miny) miny = qy; if (qy > maxy) maxy = qy;
  75. if (qx > 0 && gray[q - 1] >= th && label[q - 1] == 0) { label[q - 1] = 1; queue.Enqueue(q - 1); }
  76. if (qx < dw - 1 && gray[q + 1] >= th && label[q + 1] == 0) { label[q + 1] = 1; queue.Enqueue(q + 1); }
  77. if (qy > 0 && gray[q - dw] >= th && label[q - dw] == 0) { label[q - dw] = 1; queue.Enqueue(q - dw); }
  78. if (qy < dh - 1 && gray[q + dw] >= th && label[q + dw] == 0) { label[q + dw] = 1; queue.Enqueue(q + dw); }
  79. }
  80. if (cnt > bestCnt) { bestCnt = cnt; bestSx = sx; bestSy = sy; bestX0 = minx; bestY0 = miny; bestX1 = maxx; bestY1 = maxy; }
  81. }
  82. res.AreaPct = 100.0 * bestCnt / (dw * dh);
  83. if (bestCnt < dw * dh * MinAreaPct / 100.0) { res.Found = false; return res; }
  84. double cxd = (double)bestSx / bestCnt, cyd = (double)bestSy / bestCnt;
  85. int bw = bestX1 - bestX0 + 1, bh = bestY1 - bestY0 + 1;
  86. // 半径:外接框平均半宽 与 面积反推半径
  87. double rBox = (bw + bh) / 4.0;
  88. double rArea = Math.Sqrt(bestCnt / Math.PI);
  89. double rd = (rBox + rArea) / 2;
  90. // ── 排除斜纹反光带的双判据 ──
  91. // 1) 外接框宽高比:真well≈1(圆),反光带细长偏离1
  92. double aspect = (double)Math.Max(bw, bh) / Math.Max(1, Math.Min(bw, bh));
  93. // 2) 圆度:连通域面积 / 外接框面积。实心圆≈π/4≈0.785;反光带稀疏远小于此
  94. double boxFill = (double)bestCnt / (bw * bh);
  95. // 3) 面积半径与外接框半径一致性:真圆两者接近,反光带差很多
  96. double rConsist = Math.Min(rArea, rBox) / Math.Max(rArea, rBox);
  97. res.FillRatio = boxFill; // 用外接框填充率作圆度指标
  98. res.Aspect = aspect; res.BoxFill = boxFill; res.RConsist = rConsist;
  99. // 半径合理范围:真well半径约占画面宽15~26%(实测~490/2592=19%)。
  100. // 斜纹反光带半径常达34%(~870),用上限拦掉;过小的也排除。
  101. double rFrac = rd * downsample / width;
  102. // 真well判据:宽高比<1.5(够方) + 外接框填充>0.6(够实心) + 半径一致>0.65 + 半径占比在[0.10,0.28]
  103. if (aspect > 1.5 || boxFill < 0.6 || rConsist < 0.65 || rFrac > 0.28 || rFrac < 0.10)
  104. {
  105. res.Found = false;
  106. res.RejectReason = $"aspect={aspect:F2} boxFill={boxFill:F2} rConsist={rConsist:F2} rFrac={rFrac:F2}";
  107. return res;
  108. }
  109. res.Cx = cxd * downsample;
  110. res.Cy = cyd * downsample;
  111. res.Radius = rd * downsample;
  112. res.Found = true;
  113. res.OffsetX = res.Cx - width / 2.0;
  114. res.OffsetY = res.Cy - height / 2.0;
  115. res.OffsetXPct = res.OffsetX / width * 100;
  116. res.OffsetYPct = res.OffsetY / height * 100;
  117. // 完整性:外接框是否触画面边(下采样坐标)
  118. res.Complete = bestX0 > 1 && bestY0 > 1 && bestX1 < dw - 2 && bestY1 < dh - 2;
  119. return res;
  120. }
  121. private static int Otsu(int[] hist, int total)
  122. {
  123. long sum = 0;
  124. for (int i = 0; i < 256; i++) sum += (long)i * hist[i];
  125. long sumB = 0; int wB = 0; double maxVar = 0; int th = 0;
  126. for (int t = 0; t < 256; t++)
  127. {
  128. wB += hist[t];
  129. if (wB == 0) continue;
  130. int wF = total - wB;
  131. if (wF == 0) break;
  132. sumB += (long)t * hist[t];
  133. double mB = (double)sumB / wB;
  134. double mF = (double)(sum - sumB) / wF;
  135. double between = (double)wB * wF * (mB - mF) * (mB - mF);
  136. if (between > maxVar) { maxVar = between; th = t; }
  137. }
  138. return th;
  139. }
  140. }
  141. }