Sfoglia il codice sorgente

feat(calib): 水平全行程粗扫定位 + Z固定大窗口粗对焦 + 扩精对焦半幅

- CoarseFocus 改固定中心90000±30000步距2000
- 新增HCoarseLocate全行程粗扫(0~70000)命中完整圆即停+局部密扫
- 精对焦半幅6000步距500, 所有电机移动经ClampH/ClampZ限位

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 1 settimana fa
parent
commit
144f3db5cc
1 ha cambiato i file con 84 aggiunte e 30 eliminazioni
  1. 84 30
      Calib/CalibrationEngine.cs

+ 84 - 30
Calib/CalibrationEngine.cs

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