MainWindow.Scan.cs 8.1 KB

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