WellSpacing.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. using System;
  2. using System.IO;
  3. using System.Threading;
  4. using AutoFocusTool.Serial;
  5. using AutoFocusTool.Imaging;
  6. using SerialCamera = AutoFocusTool.Camera.Camera;
  7. namespace AutoFocusTool
  8. {
  9. /// <summary>
  10. /// 只读诊断工具:逐个 well 移动到其 EEPROM 水平位置 → 抓帧 → 跑 WellDetector → 存图。
  11. /// 用于验证“每个 well 移动过去后相机实际看到的是不是对应的孔、偏差多少”。
  12. /// 不做对焦/不改标定,只在固定 Z(传入或默认)和固定曝光下拍一张。
  13. ///
  14. /// 用法:WellSpacing.exe [COM口=COM11] [相机index=2] [曝光=80] [Z=93000]
  15. /// 存图目录:calib_result\well_check_yyyyMMdd_HHmmss\
  16. /// </summary>
  17. internal class WellSpacing
  18. {
  19. [STAThread]
  20. static int Main(string[] args)
  21. {
  22. Console.OutputEncoding = System.Text.Encoding.UTF8;
  23. if (args.Length > 0 && args[0] == "zcurve")
  24. return ZCurve(args);
  25. string port = args.Length > 0 ? args[0] : "COM11";
  26. int camIdx = args.Length > 1 && int.TryParse(args[1], out int ci) ? ci : 2;
  27. int exposure = args.Length > 2 && int.TryParse(args[2], out int ex) ? ex : 80;
  28. int zPos = args.Length > 3 && int.TryParse(args[3], out int z) ? z : 93000;
  29. int camW = 2592, camH = 1944;
  30. void L(string m) => Console.WriteLine($"{DateTime.Now:HH:mm:ss} {m}");
  31. string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
  32. string outDir = $@"C:\claudeFile\TL\AutoFocusTool\TestData\well_check_{stamp}";
  33. Directory.CreateDirectory(outDir);
  34. L($"========== well 实拍检测 {port} cam#{camIdx} 曝光{exposure} Z{zPos} ==========");
  35. L($"存图目录: {outDir}");
  36. var motor = new HouseMotor(port) { Log = null };
  37. if (!motor.Open()) { L($"✗ 打开 {port} 失败(串口被占用?)"); return 1; }
  38. motor.MotorDelayMs = 1500;
  39. int sn = motor.ShakeHands();
  40. L($"握手 houseSn = {sn}");
  41. var cam = new SerialCamera(camIdx, camW, camH, exposure);
  42. int init = cam.Init();
  43. if (init != 0) { L($"✗ 相机#{camIdx} 初始化失败 code={init}"); motor.Close(); return 2; }
  44. cam.SetOpMode(0);
  45. cam.SetExposure(exposure);
  46. motor.OpenLED();
  47. Thread.Sleep(300);
  48. // 先到统一对焦 Z(所有 well 用同一 Z,只看水平是否对到不同孔)
  49. motor.VerticalMoveTo(zPos, 1500);
  50. try
  51. {
  52. for (int w = 1; w <= 16; w++)
  53. {
  54. int hpos = motor.ReadWellHorizontalPos(w);
  55. if (hpos < 0) { L($"well{w,2}: 读EEPROM位置失败,跳过"); continue; }
  56. motor.HorizontalMoveTo(hpos, 1500);
  57. Thread.Sleep(200);
  58. cam.GrabRgb(); // 丢弃第一帧
  59. Thread.Sleep(100);
  60. cam.GrabRgb();
  61. byte[] buf = cam.GetSourceBuffer();
  62. var c = WellDetector.Detect(buf, camW, camH);
  63. string tag = c.Found
  64. ? $"圆心({c.Cx:F0},{c.Cy:F0}) R={c.Radius:F0} 偏移X={c.OffsetXPct:F1}% Y={c.OffsetYPct:F1}% 完整={c.Complete}"
  65. : $"未检出圆[{c.RejectReason}]";
  66. L($"well{w,2}: EEPROM水平={hpos,7} {tag}");
  67. string detTag = c.Found ? (c.Complete ? "OK" : "partial") : "NOCIRCLE";
  68. string path = Path.Combine(outDir, $"well{w:D2}_hp{hpos}_{detTag}.bmp");
  69. ImageConverter.SaveBmp(buf, camW, camH, path);
  70. }
  71. }
  72. finally
  73. {
  74. motor.CloseLED();
  75. cam.Dispose();
  76. motor.Close();
  77. }
  78. L($"========== 完成,图片在: {outDir} ==========");
  79. return 0;
  80. }
  81. /// <summary>
  82. /// Z清晰度曲线扫描:移到指定well的EEPROM水平位置,从zLo到zHi逐层抓帧算清晰度,
  83. /// 同时记录灰度均值(验证亮度归一化是否带偏),每层存图。
  84. /// 用法:WellSpacing.exe zcurve <COM> <相机idx> <well> [zLo=20000] [zHi=120000] [zStep=2000] [曝光=60]
  85. /// </summary>
  86. static int ZCurve(string[] a)
  87. {
  88. string port = a.Length > 1 ? a[1] : "COM11";
  89. int camIdx = a.Length > 2 && int.TryParse(a[2], out int ci) ? ci : 2;
  90. int well = a.Length > 3 && int.TryParse(a[3], out int wv) ? wv : 1;
  91. int zLo = a.Length > 4 && int.TryParse(a[4], out int v4) ? v4 : 20000;
  92. int zHi = a.Length > 5 && int.TryParse(a[5], out int v5) ? v5 : 120000;
  93. int zStep = a.Length > 6 && int.TryParse(a[6], out int v6) ? v6 : 2000;
  94. int exposure = a.Length > 7 && int.TryParse(a[7], out int v7) ? v7 : 60;
  95. int camW = 2592, camH = 1944;
  96. void L(string m) => Console.WriteLine($"{DateTime.Now:HH:mm:ss} {m}");
  97. string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
  98. string outDir = $@"C:\claudeFile\TL\AutoFocusTool\TestData\zcurve_w{well}_{stamp}";
  99. Directory.CreateDirectory(outDir);
  100. L($"==== Z清晰度曲线 well{well} {port} cam#{camIdx} Z[{zLo},{zHi}] step{zStep} 曝光{exposure} ====");
  101. L($"存图: {outDir}");
  102. var motor = new HouseMotor(port) { Log = null };
  103. if (!motor.Open()) { L($"✗ 打开 {port} 失败"); return 1; }
  104. motor.MotorDelayMs = 1500;
  105. int sn = motor.ShakeHands();
  106. L($"握手 houseSn={sn}");
  107. int hpos = motor.ReadWellHorizontalPos(well);
  108. if (hpos < 0) { L("✗ 读well水平位置失败"); motor.Close(); return 1; }
  109. var cam = new SerialCamera(camIdx, camW, camH, exposure);
  110. if (cam.Init() != 0) { L("✗ 相机初始化失败"); motor.Close(); return 2; }
  111. cam.SetOpMode(0); cam.SetExposure(exposure);
  112. motor.OpenLED(); Thread.Sleep(300);
  113. motor.HorizontalMoveTo(hpos, 1500);
  114. L($"已移到 well{well} 水平={hpos}");
  115. double bestScore = -1; int bestZ = zLo;
  116. try
  117. {
  118. for (int z = zLo; z <= zHi; z += zStep)
  119. {
  120. motor.VerticalMoveTo(z, 1500);
  121. Thread.Sleep(150);
  122. cam.GrabRgb(); Thread.Sleep(80); cam.GrabRgb();
  123. byte[] buf = cam.GetSourceBuffer();
  124. int roiW = (int)(camW * 0.4), roiH = (int)(camH * 0.4);
  125. var roi = new System.Drawing.Rectangle((camW - roiW) / 2, (camH - roiH) / 2, roiW, roiH);
  126. double score = Sharpness.Compute(buf, camW, camH, roi);
  127. double mean = GrayMean(buf, camW, camH, roi);
  128. L($"z={z,6} 归一化分={score:F5} 亮度均值={mean:F1}");
  129. if (score > bestScore) { bestScore = score; bestZ = z; }
  130. ImageConverter.SaveBmp(buf, camW, camH,
  131. Path.Combine(outDir, $"z{z}_s{score:F4}.bmp"));
  132. }
  133. }
  134. finally { motor.CloseLED(); cam.Dispose(); motor.Close(); }
  135. L($"==== 峰值 z={bestZ} 分={bestScore:F5} 图在 {outDir} ====");
  136. return 0;
  137. }
  138. /// <summary>中央ROI灰度均值(BGR→灰度,行优先)。</summary>
  139. static double GrayMean(byte[] bgr, int W, int H, System.Drawing.Rectangle roi)
  140. {
  141. long sum = 0; int n = 0; int stride = W * 3;
  142. for (int y = roi.Y; y < roi.Y + roi.Height && y < H; y++)
  143. for (int x = roi.X; x < roi.X + roi.Width && x < W; x++)
  144. {
  145. int p = y * stride + x * 3;
  146. sum += (bgr[p] * 29 + bgr[p + 1] * 150 + bgr[p + 2] * 77) >> 8;
  147. n++;
  148. }
  149. return n > 0 ? (double)sum / n : 0;
  150. }
  151. }
  152. }