using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using AutoFocusTool.Imaging; namespace AutoFocusTool { // Z序列扫描选层 + 清晰度曲线 + ROI叠加 public partial class MainWindow { // ── Z 扫描:逐层移动→等稳→抓帧→算分,画曲线,取全局峰 ── private void BtnZScan_Click(object sender, RoutedEventArgs e) { if (_camera == null || _motor == null) { Log("需要相机+串口都连接。"); return; } if (_busy) { Log("串口忙。"); return; } int start = ParseInt2(TxtScanStart.Text, 0); int step = ParseInt2(TxtScanStep.Text, 128); int count = ParseInt2(TxtScanCount.Text, 11); int motorDelay = ParseInt2(TxtMotorDelay.Text, 1500); // UI线程先读好 if (count < 2) { Log("层数至少2。"); return; } _scanCts = new CancellationTokenSource(); var token = _scanCts.Token; Task.Run(() => { _busy = true; _curve.Clear(); try { if (_motor != null) _motor.MotorDelayMs = motorDelay; Log($"=== Z扫描开始: 起点{start} 层距{step} 层数{count} ==="); _motor.OpenLED(); Thread.Sleep(200); for (int i = 0; i < count; i++) { if (token.IsCancellationRequested) { Log("扫描已停止。"); break; } int z = start + step * i; // 移动到该层(开环,VerticalMoveTo 内含 motorDelay 等待) if (!_motor.VerticalMoveTo(z)) Log($"层{i + 1}: Z移动到{z} 失败(继续)"); // P0-2: 移动后相机缓冲区可能是移动前的旧帧,丢弃第1帧用第2帧 if (_camera.GrabRgb() != 0) { Log($"层{i + 1}: 抓帧失败"); continue; } int gr = _camera.GrabRgb(); if (gr != 0) { Log($"层{i + 1}: 抓帧失败"); continue; } byte[] buf = _camera.GetSourceBuffer(); // ROI统一:先检well圆用圆内0.95r ROI(与精对焦一致),检不出降级中央40% var circle = WellDetector.Detect(buf, _camWidth, _camHeight); var roi = circle.Found ? new System.Drawing.Rectangle( Math.Max(0, (int)(circle.Cx - circle.Radius * 0.95)), Math.Max(0, (int)(circle.Cy - circle.Radius * 0.95)), (int)(circle.Radius * 1.9), (int)(circle.Radius * 1.9)) : CenterRoi(_camWidth, _camHeight, 0.4); double score = Sharpness.Compute(buf, _camWidth, _camHeight, roi); _curve.Add((z, score)); Log($"层{i + 1}/{count}: Z={z} 清晰度={score:F1}{(circle.Found ? "" : "(全图降级:未检出圆)")}"); // 实时刷新画面+曲线 var bmp = ImageConverter.ToBitmapSource(buf, _camWidth, _camHeight); Dispatcher.Invoke(() => { ImgPreview.Source = bmp; TxtScore.Text = $"清晰度: {score:F1}"; TxtZ.Text = $"Z: {z}"; DrawCurve(); }); } _motor.CloseLED(); // 选层:全局峰 + 单峰/全糊判据 AnalyzeCurve(); } catch (Exception ex) { Log($"扫描异常: {ex.Message}"); } finally { _busy = false; } }, token); } private void BtnStop_Click(object sender, RoutedEventArgs e) { _scanCts?.Cancel(); _liveCts?.Cancel(); _calibCts?.Cancel(); Log("已请求停止。"); } /// 分析曲线:取全局峰,判断是否"全糊"。 private void AnalyzeCurve() { if (_curve.Count == 0) { Log("无数据。"); return; } var scores = _curve.Select(c => c.score).ToList(); double max = scores.Max(); double mean = scores.Average(); int peakIdx = scores.IndexOf(max); int peakZ = _curve[peakIdx].z; // 全糊判据:峰值/均值接近1 且 曲线平 → 没有明显焦点 double ratio = mean > 1e-6 ? max / mean : 1; bool allBlur = ratio < 1.15; // 经验阈值,真机标定 if (allBlur) { Log($"⚠ 整组偏糊:最高分层{peakIdx + 1}(Z={peakZ}) 分={max:F1},但 max/mean={ratio:F2} 偏低,可能无真实焦点(需扩范围搜索或空孔)。"); } else { Log($"★ 最清晰层 = 第{peakIdx + 1}层 Z={peakZ} 分={max:F1}(max/mean={ratio:F2})"); // 移动到最清晰层 Task.Run(() => { _busy = true; try { _motor.VerticalMoveTo(peakZ); Log($"已移动到最清晰层 Z={peakZ}"); } finally { _busy = false; } RefreshPositions(); }); } Dispatcher.Invoke(() => DrawCurve(peakIdx)); } // ── 曲线绘制 ── private void DrawCurve(int peakIdx = -1) { var canvas = CurveCanvas; canvas.Children.Clear(); if (_curve.Count < 2) return; double w = canvas.ActualWidth, h = canvas.ActualHeight; if (w < 10 || h < 10) return; double min = _curve.Min(c => c.score); double max = _curve.Max(c => c.score); double range = max - min; if (range < 1e-6) range = 1; double dx = w / (_curve.Count - 1); var poly = new Polyline { Stroke = Brushes.Aqua, StrokeThickness = 2 }; for (int i = 0; i < _curve.Count; i++) { double x = i * dx; double y = h - 10 - (_curve[i].score - min) / range * (h - 20); poly.Points.Add(new Point(x, y)); var dot = new Ellipse { Width = 6, Height = 6, Fill = (i == peakIdx) ? Brushes.Orange : Brushes.Aqua }; Canvas.SetLeft(dot, x - 3); Canvas.SetTop(dot, y - 3); canvas.Children.Add(dot); } canvas.Children.Add(poly); if (peakIdx >= 0) { double px = peakIdx * dx; var line = new Line { X1 = px, Y1 = 0, X2 = px, Y2 = h, Stroke = Brushes.Orange, StrokeThickness = 1, StrokeDashArray = new DoubleCollection { 3, 3 } }; canvas.Children.Add(line); } } // ── ROI 叠加框 ── private void DrawRoiOverlay(System.Drawing.Rectangle roi) { OverlayCanvas.Children.Clear(); double iw = ImgPreview.ActualWidth, ih = ImgPreview.ActualHeight; if (iw < 10 || ih < 10) return; // 图像按 Uniform 缩放,ROI 同比例映射 double sx = iw / _camWidth, sy = ih / _camHeight; double s = Math.Min(sx, sy); double offX = (iw - _camWidth * s) / 2, offY = (ih - _camHeight * s) / 2; var rect = new Rectangle { Stroke = Brushes.Lime, StrokeThickness = 2, Width = roi.Width * s, Height = roi.Height * s, StrokeDashArray = new DoubleCollection { 4, 2 } }; Canvas.SetLeft(rect, offX + roi.X * s); Canvas.SetTop(rect, offY + roi.Y * s); OverlayCanvas.Children.Add(rect); } } }