MainWindow.Scan.cs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Media;
  9. using System.Windows.Shapes;
  10. using AutoFocusTool.Imaging;
  11. namespace AutoFocusTool
  12. {
  13. // Z序列扫描选层 + 清晰度曲线 + ROI叠加
  14. public partial class MainWindow
  15. {
  16. // ── Z 扫描:逐层移动→等稳→抓帧→算分,画曲线,取全局峰 ──
  17. private void BtnZScan_Click(object sender, RoutedEventArgs e)
  18. {
  19. LogAction($"点击 Z扫描 起点={TxtScanStart.Text} 层距={TxtScanStep.Text} 层数={TxtScanCount.Text}");
  20. if (_camera == null || _motor == null) { Log("需要相机+串口都连接。"); return; }
  21. if (_busy) { Log("串口忙。"); return; }
  22. int start = ParseInt2(TxtScanStart.Text, 0);
  23. int step = ParseInt2(TxtScanStep.Text, 128);
  24. int count = ParseInt2(TxtScanCount.Text, 11);
  25. int motorDelay = ParseInt2(TxtMotorDelay.Text, 1500); // UI线程先读好
  26. if (count < 2) { Log("层数至少2。"); return; }
  27. _scanCts = new CancellationTokenSource();
  28. var token = _scanCts.Token;
  29. Task.Run(() =>
  30. {
  31. _busy = true;
  32. _curve.Clear();
  33. try
  34. {
  35. if (_motor != null) _motor.MotorDelayMs = motorDelay;
  36. Log($"=== Z扫描开始: 起点{start} 层距{step} 层数{count} ===");
  37. _motor.OpenLED();
  38. Thread.Sleep(200);
  39. for (int i = 0; i < count; i++)
  40. {
  41. if (token.IsCancellationRequested) { Log("扫描已停止。"); break; }
  42. int z = start + step * i;
  43. // 移动到该层(开环,VerticalMoveTo 内含 motorDelay 等待)
  44. if (!_motor.VerticalMoveTo(z))
  45. Log($"层{i + 1}: Z移动到{z} 失败(继续)");
  46. // P0-2: 移动后相机缓冲区可能是移动前的旧帧,丢弃第1帧用第2帧
  47. if (_camera.GrabRgb() != 0) { Log($"层{i + 1}: 抓帧失败"); continue; }
  48. int gr = _camera.GrabRgb();
  49. if (gr != 0) { Log($"层{i + 1}: 抓帧失败"); continue; }
  50. byte[] buf = _camera.GetSourceBuffer();
  51. // ROI统一:先检well圆用圆内0.95r ROI(与精对焦一致),检不出降级中央40%
  52. var circle = WellDetector.Detect(buf, _camWidth, _camHeight);
  53. var roi = circle.Found
  54. ? new System.Drawing.Rectangle(
  55. Math.Max(0, (int)(circle.Cx - circle.Radius * 0.95)),
  56. Math.Max(0, (int)(circle.Cy - circle.Radius * 0.95)),
  57. (int)(circle.Radius * 1.9), (int)(circle.Radius * 1.9))
  58. : CenterRoi(_camWidth, _camHeight, 0.4);
  59. double score = Sharpness.Compute(buf, _camWidth, _camHeight, roi);
  60. _curve.Add((z, score));
  61. Log($"层{i + 1}/{count}: Z={z} 清晰度={score:F1}{(circle.Found ? "" : "(全图降级:未检出圆)")}");
  62. // 实时刷新画面+曲线
  63. var bmp = ImageConverter.ToBitmapSource(buf, _camWidth, _camHeight);
  64. Dispatcher.Invoke(() =>
  65. {
  66. ImgPreview.Source = bmp;
  67. TxtScore.Text = $"清晰度: {score:F1}";
  68. TxtZ.Text = $"Z: {z}";
  69. DrawCurve();
  70. });
  71. }
  72. _motor.CloseLED();
  73. // 选层:全局峰 + 单峰/全糊判据
  74. AnalyzeCurve();
  75. }
  76. catch (Exception ex) { Log($"扫描异常: {ex.Message}"); }
  77. finally { _busy = false; }
  78. }, token);
  79. }
  80. private void BtnStop_Click(object sender, RoutedEventArgs e)
  81. {
  82. LogAction("点击 停止扫描");
  83. _scanCts?.Cancel();
  84. _liveCts?.Cancel();
  85. _calibCts?.Cancel();
  86. Log("已请求停止。");
  87. }
  88. /// <summary>分析曲线:取全局峰,判断是否"全糊"。</summary>
  89. private void AnalyzeCurve()
  90. {
  91. if (_curve.Count == 0) { Log("无数据。"); return; }
  92. var scores = _curve.Select(c => c.score).ToList();
  93. double max = scores.Max();
  94. double mean = scores.Average();
  95. int peakIdx = scores.IndexOf(max);
  96. int peakZ = _curve[peakIdx].z;
  97. // 全糊判据:峰值/均值接近1 且 曲线平 → 没有明显焦点
  98. double ratio = mean > 1e-6 ? max / mean : 1;
  99. bool allBlur = ratio < 1.15; // 经验阈值,真机标定
  100. if (allBlur)
  101. {
  102. Log($"⚠ 整组偏糊:最高分层{peakIdx + 1}(Z={peakZ}) 分={max:F1},但 max/mean={ratio:F2} 偏低,可能无真实焦点(需扩范围搜索或空孔)。");
  103. }
  104. else
  105. {
  106. Log($"★ 最清晰层 = 第{peakIdx + 1}层 Z={peakZ} 分={max:F1}(max/mean={ratio:F2})");
  107. // 移动到最清晰层
  108. Task.Run(() =>
  109. {
  110. _busy = true;
  111. try { _motor.VerticalMoveTo(peakZ); Log($"已移动到最清晰层 Z={peakZ}"); }
  112. finally { _busy = false; }
  113. RefreshPositions();
  114. });
  115. }
  116. Dispatcher.Invoke(() => DrawCurve(peakIdx));
  117. }
  118. // ── 曲线绘制 ──
  119. private void DrawCurve(int peakIdx = -1)
  120. {
  121. var canvas = CurveCanvas;
  122. canvas.Children.Clear();
  123. if (_curve.Count < 2) return;
  124. double w = canvas.ActualWidth, h = canvas.ActualHeight;
  125. if (w < 10 || h < 10) return;
  126. double min = _curve.Min(c => c.score);
  127. double max = _curve.Max(c => c.score);
  128. double range = max - min;
  129. if (range < 1e-6) range = 1;
  130. double dx = w / (_curve.Count - 1);
  131. var poly = new Polyline { Stroke = Brushes.Aqua, StrokeThickness = 2 };
  132. for (int i = 0; i < _curve.Count; i++)
  133. {
  134. double x = i * dx;
  135. double y = h - 10 - (_curve[i].score - min) / range * (h - 20);
  136. poly.Points.Add(new Point(x, y));
  137. var dot = new Ellipse { Width = 6, Height = 6, Fill = (i == peakIdx) ? Brushes.Orange : Brushes.Aqua };
  138. Canvas.SetLeft(dot, x - 3); Canvas.SetTop(dot, y - 3);
  139. canvas.Children.Add(dot);
  140. }
  141. canvas.Children.Add(poly);
  142. if (peakIdx >= 0)
  143. {
  144. double px = peakIdx * dx;
  145. var line = new Line { X1 = px, Y1 = 0, X2 = px, Y2 = h, Stroke = Brushes.Orange, StrokeThickness = 1, StrokeDashArray = new DoubleCollection { 3, 3 } };
  146. canvas.Children.Add(line);
  147. }
  148. }
  149. // ── ROI 叠加框 ──
  150. private void DrawRoiOverlay(System.Drawing.Rectangle roi)
  151. {
  152. OverlayCanvas.Children.Clear();
  153. double iw = ImgPreview.ActualWidth, ih = ImgPreview.ActualHeight;
  154. if (iw < 10 || ih < 10) return;
  155. // 图像按 Uniform 缩放,ROI 同比例映射
  156. double sx = iw / _camWidth, sy = ih / _camHeight;
  157. double s = Math.Min(sx, sy);
  158. double offX = (iw - _camWidth * s) / 2, offY = (ih - _camHeight * s) / 2;
  159. var rect = new Rectangle
  160. {
  161. Stroke = Brushes.Lime,
  162. StrokeThickness = 2,
  163. Width = roi.Width * s,
  164. Height = roi.Height * s,
  165. StrokeDashArray = new DoubleCollection { 4, 2 }
  166. };
  167. Canvas.SetLeft(rect, offX + roi.X * s);
  168. Canvas.SetTop(rect, offY + roi.Y * s);
  169. OverlayCanvas.Children.Add(rect);
  170. }
  171. }
  172. }