|
|
@@ -150,8 +150,7 @@ namespace AutoFocusTool.Calib
|
|
|
_cam.SetExposure(CenterScanExposure);
|
|
|
for (int i = 0; i < steps; i++)
|
|
|
{
|
|
|
- int hp = center - range + step * i;
|
|
|
- if (hp < 0) continue;
|
|
|
+ int hp = ClampH(center - range + step * i);
|
|
|
_motor.HorizontalMoveTo(hp, actualDelay);
|
|
|
var b = Grab();
|
|
|
var c = WellDetector.Detect(b, W, H);
|
|
|
@@ -170,6 +169,57 @@ namespace AutoFocusTool.Calib
|
|
|
return (bestHPos, best);
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 水平全行程粗扫定位 + 局部密扫居中。不依赖 EEPROM 水平位置准确:
|
|
|
+ /// ① 从 HCoarseStart 到 HCoarseEnd 按 HCoarseStep 扫,命中完整圆即停,记录该位置;
|
|
|
+ /// ② 以命中点为中心做局部密扫(ScanForCenter)优化 Y 居中;
|
|
|
+ /// ③ 全程未命中完整圆则取扫描中 |Y偏移| 最小且检出的位置;仍无返回 (-1,null)。
|
|
|
+ /// </summary>
|
|
|
+ (int bestHPos, WellCircle bestCircle) HCoarseLocate(int well)
|
|
|
+ {
|
|
|
+ _cam.SetExposure(CenterScanExposure);
|
|
|
+
|
|
|
+ int hitHPos = -1; WellCircle hitCircle = null;
|
|
|
+ int fallbackHPos = -1; WellCircle fallbackCircle = null;
|
|
|
+ double fallbackScore = double.MaxValue;
|
|
|
+
|
|
|
+ // ① 全行程粗扫,命中完整圆即停
|
|
|
+ for (int hp = HCoarseStart; hp <= HCoarseEnd; hp += HCoarseStep)
|
|
|
+ {
|
|
|
+ int p = ClampH(hp);
|
|
|
+ _motor.HorizontalMoveTo(p, ScanDelayMs);
|
|
|
+ var b = Grab();
|
|
|
+ var c = WellDetector.Detect(b, W, H);
|
|
|
+ DebugSave?.Invoke(b, $"hcoarse_w{well}_hp{p}");
|
|
|
+ Log?.Invoke(c.Found
|
|
|
+ ? $" 粗扫水平{p}: Y偏移={c.OffsetYPct:F1}% 完整={c.Complete}"
|
|
|
+ : $" 粗扫水平{p}: 未检出圆");
|
|
|
+ OnStep?.Invoke($"水平粗扫 hp={p}", c, null);
|
|
|
+ if (c.Found)
|
|
|
+ {
|
|
|
+ double score = Math.Abs(c.OffsetYPct) + (c.Complete ? 0 : 100);
|
|
|
+ if (score < fallbackScore) { fallbackScore = score; fallbackHPos = p; fallbackCircle = c; }
|
|
|
+ if (c.Complete) { hitHPos = p; hitCircle = c; break; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 命中完整圆 → 局部密扫;否则用检出最优的降级点
|
|
|
+ int center = hitHPos >= 0 ? hitHPos : fallbackHPos;
|
|
|
+ if (center < 0)
|
|
|
+ {
|
|
|
+ Log?.Invoke($"[well{well}] ✗ 水平全行程未检出任何圆");
|
|
|
+ return (-1, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ② 以命中/降级点为中心局部密扫居中(范围取粗扫步距量级)
|
|
|
+ int fineRange = HCoarseStep;
|
|
|
+ var fine = ScanForCenter(well, center, fineRange, FineScanSteps, 800);
|
|
|
+ if (fine.bestCircle != null) return (fine.bestHPos, fine.bestCircle);
|
|
|
+
|
|
|
+ // ③ 局部密扫没检出 → 回退到粗扫命中/降级结果
|
|
|
+ return (center, hitCircle ?? fallbackCircle);
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 标定单个 well。eepromHPos=该well的EEPROM水平位置(扫描中心),eepromZ=Z焦准零点。
|
|
|
/// 返回标定结果。
|
|
|
@@ -179,7 +229,7 @@ namespace AutoFocusTool.Calib
|
|
|
var r = new WellCalib { Well = well };
|
|
|
|
|
|
// 先到 EEPROM 水平位置 + 中低曝光
|
|
|
- if (!RetryMove(() => _motor.HorizontalMoveTo(eepromHPos, ScanDelayMs), $"well{well}初始水平"))
|
|
|
+ if (!RetryMove(() => _motor.HorizontalMoveTo(ClampH(eepromHPos), ScanDelayMs), $"well{well}初始水平"))
|
|
|
{
|
|
|
Log?.Invoke($"[well{well}] ✗ 电机移动到初始位置失败(已重试),跳过该well");
|
|
|
return new WellCalib { Well = well, Note = "电机移动失败" };
|
|
|
@@ -189,23 +239,25 @@ namespace AutoFocusTool.Calib
|
|
|
// ── ① 粗对焦:先让 well 清晰,否则画面模糊根本找不到圆(采纳用户洞察:
|
|
|
// 对焦与居中耦合,焦不对→画面糊→检不出圆→无法居中。故先粗对焦)──
|
|
|
Log?.Invoke($"[well{well}] ①粗对焦(让well清晰可检)...");
|
|
|
- int coarseZ = CoarseFocus(well, eepromZ, ZHalf, CoarseFocusLayers);
|
|
|
- _motor.VerticalMoveTo(coarseZ, ScanDelayMs);
|
|
|
+ int coarseZ = CoarseFocus(well);
|
|
|
+ _motor.VerticalMoveTo(ClampZ(coarseZ), ScanDelayMs);
|
|
|
Log?.Invoke($"[well{well}] → 粗对焦Z={coarseZ}");
|
|
|
|
|
|
// ── ② 旋转居中(此时画面清晰,圆可稳定检出;只优化Y偏移)──
|
|
|
// 转动培养皿让well在画面里上下(Y)移动;X的~5%固定偏移是相机/转盘硬件偏移,旋转修不了。
|
|
|
- Log?.Invoke($"[well{well}] ②旋转居中(优化Y偏移)...");
|
|
|
- var coarse = ScanForCenter(well, eepromHPos, HScanRange, HScanSteps);
|
|
|
- int fineRange = Math.Max(300, 2 * HScanRange / Math.Max(1, HScanSteps - 1));
|
|
|
- var fine = ScanForCenter(well, coarse.bestHPos, fineRange, FineScanSteps, 800); // 细扫用800ms长延时确保检测准确
|
|
|
-
|
|
|
- int bestHPos = fine.bestCircle != null ? fine.bestHPos : coarse.bestHPos;
|
|
|
- WellCircle bestCircle = fine.bestCircle ?? coarse.bestCircle;
|
|
|
+ Log?.Invoke($"[well{well}] ②水平全行程定位+居中...");
|
|
|
+ var located = HCoarseLocate(well);
|
|
|
+ if (located.bestHPos < 0)
|
|
|
+ {
|
|
|
+ Log?.Invoke($"[well{well}] ✗ 水平全行程未找到圆,跳过该well");
|
|
|
+ return new WellCalib { Well = well, Note = "水平全程未检出圆" };
|
|
|
+ }
|
|
|
+ int bestHPos = located.bestHPos;
|
|
|
+ WellCircle bestCircle = located.bestCircle;
|
|
|
r.HorizontalPulse = bestHPos;
|
|
|
r.CircleFound = bestCircle != null;
|
|
|
r.CenterOffsetPct = bestCircle?.OffsetYPct ?? 0;
|
|
|
- if (!RetryMove(() => _motor.HorizontalMoveTo(bestHPos, ScanDelayMs), $"well{well}居中水平"))
|
|
|
+ if (!RetryMove(() => _motor.HorizontalMoveTo(ClampH(bestHPos), ScanDelayMs), $"well{well}居中水平"))
|
|
|
{
|
|
|
Log?.Invoke($"[well{well}] ✗ 电机移动到居中位置失败(已重试),跳过该well");
|
|
|
return new WellCalib { Well = well, Note = "居中后电机移动失败" };
|
|
|
@@ -250,19 +302,19 @@ namespace AutoFocusTool.Calib
|
|
|
if (!circle.Found)
|
|
|
Log?.Invoke($"[well{well}] ⚠ 精对焦未检出well圆,对焦ROI降级为中央40%(非全图)");
|
|
|
|
|
|
- int fineZHalf = Math.Max(200, ZHalf / 3); // 精对焦围绕粗焦点小范围
|
|
|
- int zstep = ZLayers > 1 ? 2 * fineZHalf / (ZLayers - 1) : 1;
|
|
|
+ int zstep = FineZStep > 0 ? FineZStep : 500;
|
|
|
+ int fineLayers = 2 * FineZHalf / zstep + 1; // 半幅6000步距500 → 25层
|
|
|
var curve = new List<(int z, double s)>();
|
|
|
- for (int i = 0; i < ZLayers; i++)
|
|
|
+ for (int i = 0; i < fineLayers; i++)
|
|
|
{
|
|
|
- int z = Math.Max(0, coarseZ - fineZHalf) + zstep * i;
|
|
|
- _motor.VerticalMoveTo(z, ScanDelayMs);
|
|
|
+ int z = coarseZ - FineZHalf + zstep * i;
|
|
|
+ _motor.VerticalMoveTo(ClampZ(z), ScanDelayMs);
|
|
|
// P0-5: 每次移动后丢弃旧帧
|
|
|
Grab(); // 丢弃第一帧
|
|
|
var b = Grab(); // 使用第二帧
|
|
|
double sc = Sharpness.Compute(b, W, H, roi);
|
|
|
curve.Add((z, sc));
|
|
|
- OnStep?.Invoke($"精对焦 {i + 1}/{ZLayers} Z={z} 分={sc:F4}", circle, null);
|
|
|
+ OnStep?.Invoke($"精对焦 {i + 1}/{fineLayers} Z={z} 分={sc:F4}", circle, null);
|
|
|
}
|
|
|
|
|
|
// P1-3: 峰值平滑与插值 - 提升对焦精度
|
|
|
@@ -306,7 +358,7 @@ namespace AutoFocusTool.Calib
|
|
|
r.Note += "对焦峰弱;";
|
|
|
}
|
|
|
|
|
|
- _motor.VerticalMoveTo(r.FocusZ, ScanDelayMs);
|
|
|
+ _motor.VerticalMoveTo(ClampZ(r.FocusZ), ScanDelayMs);
|
|
|
Log?.Invoke($"[well{well}] → 最清晰Z={r.FocusZ} 峰值={max:F4} max/mean={r.PeakRatio:F2} " +
|
|
|
$"{(r.PeakRatio < 1.2 ? "(弱峰/可能空well)" : "✓有焦点")}");
|
|
|
|
|
|
@@ -315,29 +367,31 @@ namespace AutoFocusTool.Calib
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 粗对焦:围绕 centerZ±half 扫 layers 层,使用中央ROI清晰度找焦点。
|
|
|
- /// 目的是先把画面调清楚,让后续居中能稳定检出圆(对焦与居中耦合)。
|
|
|
- /// P0-6修复:使用中央40% ROI避免被背景边缘带偏。
|
|
|
+ /// 粗对焦:固定中心 ZCoarseCenter±ZCoarseHalf 按 ZCoarseStep 扫描,中央40% ROI 找焦点。
|
|
|
+ /// 实测所有 well 焦面集中在 60000~120000,故用固定大窗口,不依赖 EEPROM 零点。
|
|
|
/// </summary>
|
|
|
- int CoarseFocus(int well, int centerZ, int half, int layers)
|
|
|
+ int CoarseFocus(int well)
|
|
|
{
|
|
|
- int zstep = layers > 1 ? 2 * half / (layers - 1) : 1;
|
|
|
- int bestZ = centerZ; double bestS = -1;
|
|
|
+ int lo = ZCoarseCenter - ZCoarseHalf;
|
|
|
+ int hi = ZCoarseCenter + ZCoarseHalf;
|
|
|
+ int bestZ = ZCoarseCenter; double bestS = -1;
|
|
|
|
|
|
// P0-6: 粗对焦使用中央40%区域ROI,避免背景干扰
|
|
|
var centerROI = CenterRoi40();
|
|
|
|
|
|
- for (int i = 0; i < layers; i++)
|
|
|
+ int layers = 0;
|
|
|
+ for (int z = lo; z <= hi; z += ZCoarseStep)
|
|
|
{
|
|
|
- int z = Math.Max(0, centerZ - half) + zstep * i;
|
|
|
- _motor.VerticalMoveTo(z, ScanDelayMs);
|
|
|
+ layers++;
|
|
|
+ _motor.VerticalMoveTo(ClampZ(z), ScanDelayMs);
|
|
|
// P0-5: 丢弃旧帧
|
|
|
Grab();
|
|
|
var b = Grab();
|
|
|
double sc = Sharpness.Compute(b, W, H, centerROI); // 中央ROI(避免背景带偏)
|
|
|
if (sc > bestS) { bestS = sc; bestZ = z; }
|
|
|
- OnStep?.Invoke($"粗对焦 {i + 1}/{layers} Z={z}", null, null);
|
|
|
+ OnStep?.Invoke($"粗对焦 Z={z} (区间{lo}~{hi})", null, null);
|
|
|
}
|
|
|
+ Log?.Invoke($"[well{well}] 粗对焦扫{layers}层 区间[{lo},{hi}] 步距{ZCoarseStep}");
|
|
|
return bestZ;
|
|
|
}
|
|
|
}
|