浏览代码

feat(diag): WellSpacing诊断工具(逐well移动+拍照+检测+存图)

诊断阶段创建,用于验证每个well移到EEPROM位置后实际看到的孔与偏差。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 1 周之前
父节点
当前提交
db4a32ea60
共有 3 个文件被更改,包括 133 次插入0 次删除
  1. 1 0
      AutoFocusTool.csproj
  2. 92 0
      WellSpacing/WellSpacing.cs
  3. 40 0
      WellSpacing/WellSpacing.csproj

+ 1 - 0
AutoFocusTool.csproj

@@ -22,6 +22,7 @@
     <Compile Remove="Calibrate\**\*.cs" />
     <Compile Remove="HScan\**\*.cs" />
     <Compile Remove="ToPng\**\*.cs" />
+    <Compile Remove="WellSpacing\**\*.cs" />
     <Compile Remove="bin\**\*.cs" />
     <Compile Remove="obj\**\*.cs" />
     <Page Remove="SelfTest\**" />

+ 92 - 0
WellSpacing/WellSpacing.cs

@@ -0,0 +1,92 @@
+using System;
+using System.IO;
+using System.Threading;
+using AutoFocusTool.Serial;
+using AutoFocusTool.Imaging;
+using SerialCamera = AutoFocusTool.Camera.Camera;
+
+namespace AutoFocusTool
+{
+    /// <summary>
+    /// 只读诊断工具:逐个 well 移动到其 EEPROM 水平位置 → 抓帧 → 跑 WellDetector → 存图。
+    /// 用于验证“每个 well 移动过去后相机实际看到的是不是对应的孔、偏差多少”。
+    /// 不做对焦/不改标定,只在固定 Z(传入或默认)和固定曝光下拍一张。
+    ///
+    /// 用法:WellSpacing.exe [COM口=COM11] [相机index=2] [曝光=80] [Z=93000]
+    /// 存图目录:calib_result\well_check_yyyyMMdd_HHmmss\
+    /// </summary>
+    internal class WellSpacing
+    {
+        [STAThread]
+        static int Main(string[] args)
+        {
+            Console.OutputEncoding = System.Text.Encoding.UTF8;
+            string port = args.Length > 0 ? args[0] : "COM11";
+            int camIdx = args.Length > 1 && int.TryParse(args[1], out int ci) ? ci : 2;
+            int exposure = args.Length > 2 && int.TryParse(args[2], out int ex) ? ex : 80;
+            int zPos = args.Length > 3 && int.TryParse(args[3], out int z) ? z : 93000;
+
+            int camW = 2592, camH = 1944;
+            void L(string m) => Console.WriteLine($"{DateTime.Now:HH:mm:ss} {m}");
+
+            string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+            string outDir = $@"C:\claudeFile\TL\AutoFocusTool\calib_result\well_check_{stamp}";
+            Directory.CreateDirectory(outDir);
+
+            L($"========== well 实拍检测  {port}  cam#{camIdx}  曝光{exposure}  Z{zPos} ==========");
+            L($"存图目录: {outDir}");
+
+            var motor = new HouseMotor(port) { Log = null };
+            if (!motor.Open()) { L($"✗ 打开 {port} 失败(串口被占用?)"); return 1; }
+            motor.MotorDelayMs = 1500;
+            int sn = motor.ShakeHands();
+            L($"握手 houseSn = {sn}");
+
+            var cam = new SerialCamera(camIdx, camW, camH, exposure);
+            int init = cam.Init();
+            if (init != 0) { L($"✗ 相机#{camIdx} 初始化失败 code={init}"); motor.Close(); return 2; }
+            cam.SetOpMode(0);
+            cam.SetExposure(exposure);
+            motor.OpenLED();
+            Thread.Sleep(300);
+
+            // 先到统一对焦 Z(所有 well 用同一 Z,只看水平是否对到不同孔)
+            motor.VerticalMoveTo(zPos, 1500);
+
+            try
+            {
+                for (int w = 1; w <= 16; w++)
+                {
+                    int hpos = motor.ReadWellHorizontalPos(w);
+                    if (hpos < 0) { L($"well{w,2}: 读EEPROM位置失败,跳过"); continue; }
+
+                    motor.HorizontalMoveTo(hpos, 1500);
+                    Thread.Sleep(200);
+                    cam.GrabRgb();             // 丢弃第一帧
+                    Thread.Sleep(100);
+                    cam.GrabRgb();
+                    byte[] buf = cam.GetSourceBuffer();
+
+                    var c = WellDetector.Detect(buf, camW, camH);
+                    string tag = c.Found
+                        ? $"圆心({c.Cx:F0},{c.Cy:F0}) R={c.Radius:F0} 偏移X={c.OffsetXPct:F1}% Y={c.OffsetYPct:F1}% 完整={c.Complete}"
+                        : $"未检出圆[{c.RejectReason}]";
+                    L($"well{w,2}: EEPROM水平={hpos,7}  {tag}");
+
+                    string detTag = c.Found ? (c.Complete ? "OK" : "partial") : "NOCIRCLE";
+                    string path = Path.Combine(outDir, $"well{w:D2}_hp{hpos}_{detTag}.bmp");
+                    ImageConverter.SaveBmp(buf, camW, camH, path);
+                }
+            }
+            finally
+            {
+                motor.CloseLED();
+                cam.Dispose();
+                motor.Close();
+            }
+
+            L($"========== 完成,图片在: {outDir} ==========");
+            return 0;
+        }
+    }
+}

+ 40 - 0
WellSpacing/WellSpacing.csproj

@@ -0,0 +1,40 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0-windows</TargetFramework>
+    <Nullable>disable</Nullable>
+    <ImplicitUsings>disable</ImplicitUsings>
+    <UseWPF>true</UseWPF>
+    <Platforms>x64</Platforms>
+    <PlatformTarget>x64</PlatformTarget>
+    <AssemblyName>WellSpacing</AssemblyName>
+    <RootNamespace>AutoFocusTool</RootNamespace>
+    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="System.IO.Ports" Version="8.0.0" />
+    <PackageReference Include="System.Drawing.Common" Version="8.0.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\Serial\Protocol.cs" />
+    <Compile Include="..\Serial\SerialMotor.cs" />
+    <Compile Include="..\Serial\HouseMotor.cs" />
+    <Compile Include="..\Camera\MVCAPI.cs" />
+    <Compile Include="..\Camera\CapInfoStruct.cs" />
+    <Compile Include="..\Camera\Camera.cs" />
+    <Compile Include="..\Imaging\WellDetector.cs" />
+    <Compile Include="..\Imaging\ImageConverter.cs" />
+    <Compile Include="WellSpacing.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Include="..\DependFile\ccd\*.dll">
+      <Link>DependFile\ccd\%(Filename)%(Extension)</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>