Просмотр исходного кода

重构相机图像清晰度分析架构并增强功能

- 拆分并重构原CameraFocusAnalyzer为多模块,核心算法移至FocusAnalysisEngine,参数/结果/报告等独立建模
- 新增FocusMethod、AnalysisMode枚举,支持多算法和多模式分析
- 相机基类统一为CameraBase,相关相机类继承关系同步调整
- 新增WPF控件FocusAnalyzerControl,支持图像显示、ROI绘制、清晰度曲线、参数设置、建议与报告导出
- 分析引擎支持多种清晰度算法、棋盘格对比度分析、质量评分、统计分析(均值、方差、趋势、峰值、自相关等)
- 修正OxyPlot DataPoint命名冲突,完善csproj资源编译项
- 大幅提升了分析功能的模块化、可扩展性和UI交互体验
孝锋 徐 10 месяцев назад
Родитель
Сommit
79fb36e599

+ 333 - 0
TeamAAS-VM/Controls/FocusAnalyzerControl.xaml

@@ -0,0 +1,333 @@
+<UserControl x:Class="TeamAAS_VP.Controls.FocusAnalyzerControl"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:TeamAAS_VP.Controls"
+             xmlns:oxy="http://oxyplot.org/wpf"
+             mc:Ignorable="d"
+             d:DesignHeight="600"
+             d:DesignWidth="800">
+    <UserControl.Resources>
+        <Style x:Key="CardStyle"
+               TargetType="Border">
+            <Setter Property="Background"
+                    Value="#FFFFFF" />
+            <Setter Property="CornerRadius"
+                    Value="4" />
+            <Setter Property="BorderBrush"
+                    Value="#E0E0E0" />
+            <Setter Property="BorderThickness"
+                    Value="1" />
+            <Setter Property="Padding"
+                    Value="8" />
+            <Setter Property="Margin"
+                    Value="4" />
+        </Style>
+
+        <Style x:Key="ValueTextStyle"
+               TargetType="TextBlock">
+            <Setter Property="FontSize"
+                    Value="14" />
+            <Setter Property="FontWeight"
+                    Value="SemiBold" />
+            <Setter Property="Margin"
+                    Value="0,2,0,4" />
+        </Style>
+    </UserControl.Resources>
+
+    <Grid>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="*" />
+            <ColumnDefinition Width="300" />
+        </Grid.ColumnDefinitions>
+
+        <!-- 左侧:图像显示和图表 -->
+        <Grid Grid.Column="0">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="*" />
+                <RowDefinition Height="200" />
+            </Grid.RowDefinitions>
+
+            <!-- 状态栏 -->
+            <Border Grid.Row="0"
+                    Style="{StaticResource CardStyle}">
+                <StackPanel Orientation="Horizontal">
+                    <TextBlock Text="状态: "
+                               FontWeight="Bold" />
+                    <TextBlock x:Name="StatusText"
+                               Text="就绪"
+                               Margin="5,0,0,0" />
+                    <TextBlock x:Name="FrameCountText"
+                               Text="帧数: 0"
+                               Margin="20,0,0,0" />
+                    <TextBlock x:Name="BestSharpnessText"
+                               Text="最佳: 0.00"
+                               Margin="20,0,0,0" />
+
+                    <Button x:Name="StartButton"
+                            Content="开始分析"
+                            Command="{Binding StartCommand}"
+                            Margin="20,0,5,0"
+                            Padding="10,4" />
+                    <Button x:Name="StopButton"
+                            Content="停止分析"
+                            Command="{Binding StopCommand}"
+                            Margin="5,0,0,0"
+                            Padding="10,4" />
+                </StackPanel>
+            </Border>
+
+            <!-- 图像显示 -->
+            <Border Grid.Row="1"
+                    Style="{StaticResource CardStyle}"
+                    Margin="0,4,0,4">
+                <Grid>
+                    <Image x:Name="ImageDisplay"
+                           Source="{Binding CurrentImage}"
+                           Stretch="Uniform" />
+
+                    <!-- ROI绘制层 -->
+                    <Canvas x:Name="RoiCanvas"
+                            IsHitTestVisible="False">
+                        <Rectangle x:Name="RoiRectangle"
+                                   Stroke="Red"
+                                   StrokeThickness="2"
+                                   StrokeDashArray="4,4"
+                                   Visibility="Collapsed" />
+                    </Canvas>
+                </Grid>
+            </Border>
+
+            <!-- 曲线图表 -->
+            <Border Grid.Row="2"
+                    Style="{StaticResource CardStyle}">
+                <oxy:PlotView x:Name="SharpnessPlot"
+                              Model="{Binding PlotModel}" />
+            </Border>
+        </Grid>
+
+        <!-- 右侧:参数和结果显示 -->
+        <Grid Grid.Column="1"
+              Margin="4,0,0,0">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="*" />
+            </Grid.RowDefinitions>
+
+            <!-- 参数设置 -->
+            <Border Grid.Row="0"
+                    Style="{StaticResource CardStyle}">
+                <StackPanel>
+                    <TextBlock Text="分析参数"
+                               FontWeight="Bold"
+                               Margin="0,0,0,8" />
+
+                    <Grid>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                        </Grid.RowDefinitions>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="80" />
+                            <ColumnDefinition Width="*" />
+                        </Grid.ColumnDefinitions>
+
+                        <TextBlock Grid.Row="0"
+                                   Grid.Column="0"
+                                   Text="算法:"
+                                   VerticalAlignment="Center" />
+                        <ComboBox Grid.Row="0"
+                                  Grid.Column="1"
+                                  ItemsSource="{Binding FocusMethods}"
+                                  SelectedItem="{Binding Parameters.FocusMethod}"
+                                  Margin="0,0,0,4" />
+
+                        <TextBlock Grid.Row="1"
+                                   Grid.Column="0"
+                                   Text="模式:"
+                                   VerticalAlignment="Center" />
+                        <ComboBox Grid.Row="1"
+                                  Grid.Column="1"
+                                  ItemsSource="{Binding AnalysisModes}"
+                                  SelectedItem="{Binding Parameters.AnalysisMode}"
+                                  Margin="0,0,0,4" />
+
+                        <TextBlock Grid.Row="2"
+                                   Grid.Column="0"
+                                   Text="亮度:"
+                                   VerticalAlignment="Center" />
+                        <Slider Grid.Row="2"
+                                Grid.Column="1"
+                                Minimum="0"
+                                Maximum="255"
+                                Value="{Binding Parameters.TargetBrightness}"
+                                Margin="0,0,0,4" />
+
+                        <CheckBox Grid.Row="3"
+                                  Grid.Column="0"
+                                  Grid.ColumnSpan="2"
+                                  Content="实时分析"
+                                  IsChecked="{Binding Parameters.EnableRealtimeAnalysis}"
+                                  Margin="0,4,0,0" />
+                    </Grid>
+
+                    <Button Content="恢复默认"
+                            Command="{Binding ResetCommand}"
+                            Margin="0,8,0,0"
+                            Padding="8,4" />
+                </StackPanel>
+            </Border>
+
+            <!-- 清晰度结果 -->
+            <Border Grid.Row="1"
+                    Style="{StaticResource CardStyle}"
+                    Margin="0,4,0,0">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="*" />
+                        <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
+
+                    <StackPanel Grid.Column="0">
+                        <TextBlock Text="当前清晰度"
+                                   FontSize="11"
+                                   Foreground="#666" />
+                        <TextBlock Text="{Binding CurrentResult.Sharpness, StringFormat=F2}"
+                                   Style="{StaticResource ValueTextStyle}"
+                                   Foreground="#007ACC" />
+                        <ProgressBar Value="{Binding CurrentResult.Sharpness}"
+                                     Maximum="50"
+                                     Height="6"
+                                     Margin="0,2,4,0" />
+                    </StackPanel>
+
+                    <StackPanel Grid.Column="1">
+                        <TextBlock Text="质量评分"
+                                   FontSize="11"
+                                   Foreground="#666" />
+                        <TextBlock Text="{Binding CurrentResult.QualityScore, StringFormat=F1}"
+                                   Style="{StaticResource ValueTextStyle}"
+                                   FontSize="18" />
+                        <TextBlock Text="/100"
+                                   FontSize="11"
+                                   Foreground="#999" />
+                    </StackPanel>
+                </Grid>
+            </Border>
+
+            <!-- 棋盘格分析 -->
+            <Border Grid.Row="2"
+                    Style="{StaticResource CardStyle}"
+                    Margin="0,4,0,0">
+                <StackPanel>
+                    <TextBlock Text="棋盘格分析"
+                               FontWeight="Bold"
+                               Margin="0,0,0,8" />
+
+                    <Grid>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                        </Grid.RowDefinitions>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="*" />
+                            <ColumnDefinition Width="*" />
+                        </Grid.ColumnDefinitions>
+
+                        <TextBlock Grid.Row="0"
+                                   Grid.Column="0"
+                                   Text="状态:"
+                                   FontSize="11"
+                                   Foreground="#666" />
+                        <TextBlock Grid.Row="0"
+                                   Grid.Column="1"
+                                   Text="黑色均值:"
+                                   FontSize="11"
+                                   Foreground="#666" />
+
+                        <TextBlock Grid.Row="1"
+                                   Grid.Column="0"
+                                   Text="{Binding CurrentResult.Checkerboard.Detected, 
+                                          Converter={StaticResource BoolToTextConverter},
+                                          ConverterParameter='已检测|未检测'}"
+                                   Style="{StaticResource ValueTextStyle}" />
+
+                        <TextBlock Grid.Row="1"
+                                   Grid.Column="1"
+                                   Text="{Binding CurrentResult.Checkerboard.BlackMean, StringFormat=F1}"
+                                   Style="{StaticResource ValueTextStyle}" />
+
+                        <TextBlock Grid.Row="2"
+                                   Grid.Column="0"
+                                   Text="对比度:"
+                                   FontSize="11"
+                                   Foreground="#666" />
+                        <TextBlock Grid.Row="2"
+                                   Grid.Column="1"
+                                   Text="白色均值:"
+                                   FontSize="11"
+                                   Foreground="#666" />
+
+                        <TextBlock Grid.Row="3"
+                                   Grid.Column="0"
+                                   Text="{Binding CurrentResult.Checkerboard.Contrast, StringFormat=F1}"
+                                   Style="{StaticResource ValueTextStyle}" />
+
+                        <TextBlock Grid.Row="3"
+                                   Grid.Column="1"
+                                   Text="{Binding CurrentResult.Checkerboard.WhiteMean, StringFormat=F1}"
+                                   Style="{StaticResource ValueTextStyle}" />
+                    </Grid>
+
+                    <TextBlock Text="建议亮度:"
+                               FontSize="11"
+                               Foreground="#666"
+                               Margin="0,8,0,0" />
+                    <TextBlock Text="{Binding CurrentResult.Checkerboard.OptimalBrightness}"
+                               Style="{StaticResource ValueTextStyle}"
+                               Foreground="#4CAF50" />
+                </StackPanel>
+            </Border>
+
+            <!-- 建议列表 -->
+            <Border Grid.Row="3"
+                    Style="{StaticResource CardStyle}"
+                    Margin="0,4,0,0">
+                <StackPanel>
+                    <TextBlock Text="调整建议"
+                               FontWeight="Bold"
+                               Margin="0,0,0,8" />
+
+                    <ScrollViewer VerticalScrollBarVisibility="Auto"
+                                  MaxHeight="200">
+                        <ItemsControl ItemsSource="{Binding CurrentResult.Suggestions}">
+                            <ItemsControl.ItemTemplate>
+                                <DataTemplate>
+                                    <Border Background="#F5F5F5"
+                                            CornerRadius="3"
+                                            Padding="6"
+                                            Margin="0,0,0,4">
+                                        <TextBlock Text="{Binding}"
+                                                   TextWrapping="Wrap"
+                                                   FontSize="11" />
+                                    </Border>
+                                </DataTemplate>
+                            </ItemsControl.ItemTemplate>
+                        </ItemsControl>
+                    </ScrollViewer>
+
+                    <Button Content="导出报告"
+                            Command="{Binding ExportCommand}"
+                            Margin="0,8,0,0"
+                            Padding="8,4" />
+                </StackPanel>
+            </Border>
+        </Grid>
+    </Grid>
+</UserControl>

+ 568 - 0
TeamAAS-VM/Controls/FocusAnalyzerControl.xaml.cs

@@ -0,0 +1,568 @@
+using OpenCvSharp;
+using OpenCvSharp.WpfExtensions;
+using OxyPlot;
+using OxyPlot.Axes;
+using OxyPlot.Series;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using TeamAAS_VP.Core;
+using TeamAAS_VP.Enums;
+using TeamAAS_VP.Models;
+using OpenCvSharp.Extensions;
+using Rect = OpenCvSharp.Rect;
+using DataPoint = TeamAAS_VP.Models.DataPoint;
+
+namespace TeamAAS_VP.Controls
+{
+    /// <summary>
+    /// FocusAnalyzerControl.xaml 的交互逻辑
+    /// </summary>
+    public partial class FocusAnalyzerControl : UserControl, INotifyPropertyChanged
+    {
+        // 命令
+        public ICommand StartCommand { get; set; }
+        public ICommand StopCommand { get; set; }
+        public ICommand ResetCommand { get; set; }
+        public ICommand ExportCommand { get; set; }
+
+        // 可绑定属性
+        private ImageSource _currentImage;
+        private AnalysisParameters _parameters;
+        private AnalysisResult _currentResult;
+        private AnalysisReport _finalReport;
+        private bool _isAnalyzing;
+        private Rect? _currentRoi;
+        private PlotModel _plotModel;
+
+        // 内部状态
+        private List<DataPoint> _historyData = new List<DataPoint>();
+        private int _frameCounter = 0;
+        private double _bestSharpness = 0;
+        private DateTime _startTime;
+
+        // 枚举值列表(用于ComboBox绑定)
+        public Array FocusMethods => Enum.GetValues(typeof(FocusMethod));
+        public Array AnalysisModes => Enum.GetValues(typeof(AnalysisMode));
+
+        public ImageSource CurrentImage
+        {
+            get => _currentImage;
+            set => SetField(ref _currentImage, value);
+        }
+
+        public AnalysisParameters Parameters
+        {
+            get => _parameters;
+            set => SetField(ref _parameters, value);
+        }
+
+        public AnalysisResult CurrentResult
+        {
+            get => _currentResult;
+            set => SetField(ref _currentResult, value);
+        }
+
+        public AnalysisReport FinalReport
+        {
+            get => _finalReport;
+            set => SetField(ref _finalReport, value);
+        }
+
+        public bool IsAnalyzing
+        {
+            get => _isAnalyzing;
+            set => SetField(ref _isAnalyzing, value);
+        }
+
+        public Rect? CurrentRoi
+        {
+            get => _currentRoi;
+            set => SetField(ref _currentRoi, value);
+        }
+
+        public PlotModel PlotModel
+        {
+            get => _plotModel;
+            set => SetField(ref _plotModel, value);
+        }
+
+
+        public FocusAnalyzerControl()
+        {
+            InitializeComponent();
+            InitializeCommands();
+            InitializeProperties();
+            InitializePlot();
+
+            DataContext = this;
+        }
+        private void InitializeCommands()
+        {
+            StartCommand = new RelayCommand(
+                execute: StartAnalysis,
+                canExecute: () => !IsAnalyzing);
+
+            StopCommand = new RelayCommand(
+                execute: StopAnalysis,
+                canExecute: () => IsAnalyzing);
+
+            ResetCommand = new RelayCommand(
+                execute: ResetAnalysis);
+
+            ExportCommand = new RelayCommand(
+                execute: ExportReport);
+        }
+
+        private void InitializeProperties()
+        {
+            Parameters = new AnalysisParameters();
+            CurrentResult = new AnalysisResult();
+            FinalReport = new AnalysisReport();
+
+            // 设置默认ROI
+            CurrentRoi = null;
+        }
+
+        private void InitializePlot()
+        {
+            PlotModel = new PlotModel
+            {
+                Title = "清晰度变化曲线",
+                TitleFontSize = 12,
+                PlotMargins = new OxyThickness(40, 10, 10, 40)
+            };
+
+            // X轴
+            PlotModel.Axes.Add(new LinearAxis
+            {
+                Position = AxisPosition.Bottom,
+                Title = "帧序列",
+                MajorGridlineStyle = LineStyle.Solid,
+                MinorGridlineStyle = LineStyle.Dot,
+                MajorGridlineColor = OxyColor.FromArgb(40, 0, 0, 0),
+                MinorGridlineColor = OxyColor.FromArgb(20, 0, 0, 0)
+            });
+
+            // Y轴
+            PlotModel.Axes.Add(new LinearAxis
+            {
+                Position = AxisPosition.Left,
+                Title = "清晰度",
+                MajorGridlineStyle = LineStyle.Solid,
+                MinorGridlineStyle = LineStyle.Dot,
+                MajorGridlineColor = OxyColor.FromArgb(40, 0, 0, 0),
+                MinorGridlineColor = OxyColor.FromArgb(20, 0, 0, 0)
+            });
+
+            // 清晰度曲线
+            var sharpnessSeries = new LineSeries
+            {
+                Title = "清晰度",
+                Color = OxyColors.DodgerBlue,
+                StrokeThickness = 2,
+                MarkerType = MarkerType.Circle,
+                MarkerSize = 3,
+                MarkerFill = OxyColors.DodgerBlue,
+                MarkerStroke = OxyColors.White,
+                MarkerStrokeThickness = 1
+            };
+
+            PlotModel.Series.Add(sharpnessSeries);
+        }
+
+        /// <summary>
+        /// 开始分析(由外部调用)
+        /// </summary>
+        public void StartAnalysis()
+        {
+            try
+            {
+                IsAnalyzing = true;
+                _frameCounter = 0;
+                _bestSharpness = 0;
+                _historyData.Clear();
+                _startTime = DateTime.Now;
+
+                // 清空图表
+                var series = PlotModel.Series[0] as LineSeries;
+                series?.Points.Clear();
+
+                // 初始化最终报告
+                FinalReport = new AnalysisReport
+                {
+                    Parameters = Parameters,
+                    StartTime = _startTime
+                };
+
+                // 更新UI
+                UpdateStatus("分析中...");
+                CommandManager.InvalidateRequerySuggested();
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"开始分析失败: {ex.Message}", "错误",
+                    MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+
+        /// <summary>
+        /// 停止分析(由外部调用)
+        /// </summary>
+        public void StopAnalysis()
+        {
+            try
+            {
+                IsAnalyzing = false;
+
+                // 完成报告
+                FinalReport.EndTime = DateTime.Now;
+                FinalReport.FinalResult = CurrentResult;
+                FinalReport.HistoryData = new List<DataPoint>(_historyData);
+
+                // 更新UI
+                UpdateStatus("分析完成");
+                CommandManager.InvalidateRequerySuggested();
+
+                // 触发分析完成事件
+                OnAnalysisCompleted();
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"停止分析失败: {ex.Message}", "错误",
+                    MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+
+        /// <summary>
+        /// 重置分析
+        /// </summary>
+        public void ResetAnalysis()
+        {
+            Parameters.ResetToDefaults();
+            CurrentResult = new AnalysisResult();
+            FinalReport = new AnalysisReport();
+
+            _historyData.Clear();
+            _frameCounter = 0;
+            _bestSharpness = 0;
+
+            // 清空图表
+            var series = PlotModel.Series[0] as LineSeries;
+            series?.Points.Clear();
+            PlotModel.InvalidatePlot(true);
+
+            // 清空图像
+            CurrentImage = null;
+
+            UpdateStatus("已重置");
+        }
+
+        /// <summary>
+        /// 导出报告
+        /// </summary>
+        private void ExportReport()
+        {
+            try
+            {
+                var saveDialog = new Microsoft.Win32.SaveFileDialog
+                {
+                    Filter = "文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*",
+                    FileName = $"相机分析报告_{DateTime.Now:yyyyMMdd_HHmmss}.txt"
+                };
+
+                if (saveDialog.ShowDialog() == true)
+                {
+                    var report = FinalReport.GenerateTextReport();
+                    System.IO.File.WriteAllText(saveDialog.FileName, report);
+
+                    MessageBox.Show($"报告已保存到:{saveDialog.FileName}", "成功",
+                        MessageBoxButton.OK, MessageBoxImage.Information);
+                }
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"导出失败:{ex.Message}", "错误",
+                    MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+
+        /// <summary>
+        /// 处理新图像帧(由外部调用)
+        /// </summary>
+        public void ProcessImageFrame(Mat frame)
+        {
+            if (!IsAnalyzing || frame == null || frame.Empty())
+                return;
+
+            try
+            {
+                // 更新UI图像
+                Application.Current.Dispatcher.Invoke(() =>
+                {
+                    CurrentImage = BitmapSourceConverter.ToBitmapSource(frame);
+                });
+
+                // 执行分析
+                PerformAnalysis(frame);
+                _frameCounter++;
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"处理图像帧失败: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// 执行单次图像分析
+        /// </summary>
+        public void PerformAnalysis(Mat frame)
+        {
+            if (frame == null || frame.Empty())
+                return;
+
+            try
+            {
+                // 1. 计算清晰度
+                double sharpness = FocusAnalysisEngine.CalculateImageSharpness(
+                    frame, Parameters.FocusMethod, CurrentRoi);
+
+                // 2. 分析棋盘格
+                CheckerboardResult checkerboard;
+                if (Parameters.AnalysisMode == AnalysisMode.AutoCheckerboard)
+                {
+                    checkerboard = FocusAnalysisEngine.AnalyzeCheckerboardContrast(frame, CurrentRoi);
+                }
+                else
+                {
+                    checkerboard = new CheckerboardResult();
+                }
+
+                // 3. 计算质量评分
+                double qualityScore = FocusAnalysisEngine.CalculateQualityScore(sharpness, checkerboard);
+
+                // 4. 生成建议
+                var suggestions = FocusAnalysisEngine.GenerateSuggestions(
+                    sharpness, checkerboard,
+                    Parameters.SharpnessThreshold,
+                    Parameters.ContrastThreshold);
+
+                // 5. 更新最佳清晰度
+                if (sharpness > _bestSharpness)
+                {
+                    _bestSharpness = sharpness;
+                }
+
+                // 6. 更新结果
+                Application.Current.Dispatcher.Invoke(() =>
+                {
+                    CurrentResult.Sharpness = sharpness;
+                    CurrentResult.Checkerboard = checkerboard;
+                    CurrentResult.QualityScore = qualityScore;
+                    CurrentResult.FrameCount = _frameCounter + 1;
+                    CurrentResult.BestSharpness = _bestSharpness;
+                    CurrentResult.AnalysisTime = DateTime.Now;
+                    CurrentResult.Suggestions = new ObservableCollection<string>(suggestions);
+
+                    // 更新历史数据
+                    var dataPoint = new DataPoint(_frameCounter, sharpness,
+                        checkerboard.Detected ? checkerboard.Contrast : 0);
+                    _historyData.Add(dataPoint);
+
+                    // 更新图表
+                    UpdatePlot(dataPoint);
+
+                    // 更新状态文本
+                    UpdateStatusText();
+                });
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"图像分析失败: {ex.Message}");
+            }
+        }
+
+        private void UpdatePlot(DataPoint dataPoint)
+        {
+            var series = PlotModel.Series[0] as LineSeries;
+            if (series != null)
+            {
+                series.Points.Add(new OxyPlot.DataPoint(dataPoint.FrameIndex, dataPoint.Sharpness));
+
+                // 限制显示的点数(最多200个点)
+                if (series.Points.Count > 200)
+                {
+                    series.Points.RemoveAt(0);
+                }
+
+                PlotModel.InvalidatePlot(true);
+            }
+        }
+
+        private void UpdateStatusText()
+        {
+            StatusText.Text = $"分析中... 第 {_frameCounter + 1} 帧";
+            FrameCountText.Text = $"帧数: {_frameCounter + 1}";
+            BestSharpnessText.Text = $"最佳: {_bestSharpness:F2}";
+        }
+
+        private void UpdateStatus(string status)
+        {
+            StatusText.Text = status;
+        }
+
+        // ROI绘制相关
+        private bool _isDrawingRoi = false;
+        private System.Windows.Point _roiStartPoint;
+
+        private void ImageDisplay_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+        {
+            if (CurrentImage == null) return;
+
+            _roiStartPoint = e.GetPosition(ImageDisplay);
+            _isDrawingRoi = true;
+
+            RoiRectangle.Visibility = Visibility.Visible;
+            Canvas.SetLeft(RoiRectangle, _roiStartPoint.X);
+            Canvas.SetTop(RoiRectangle, _roiStartPoint.Y);
+            RoiRectangle.Width = 0;
+            RoiRectangle.Height = 0;
+        }
+
+        private void ImageDisplay_MouseMove(object sender, MouseEventArgs e)
+        {
+            if (!_isDrawingRoi) return;
+
+            var currentPoint = e.GetPosition(ImageDisplay);
+
+            double left = Math.Min(_roiStartPoint.X, currentPoint.X);
+            double top = Math.Min(_roiStartPoint.Y, currentPoint.Y);
+            double width = Math.Abs(currentPoint.X - _roiStartPoint.X);
+            double height = Math.Abs(currentPoint.Y - _roiStartPoint.Y);
+
+            Canvas.SetLeft(RoiRectangle, left);
+            Canvas.SetTop(RoiRectangle, top);
+            RoiRectangle.Width = width;
+            RoiRectangle.Height = height;
+        }
+
+        private void ImageDisplay_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+        {
+            if (!_isDrawingRoi) return;
+
+            _isDrawingRoi = false;
+            var endPoint = e.GetPosition(ImageDisplay);
+
+            // 转换为图像坐标
+            if (CurrentImage is BitmapSource bitmapSource)
+            {
+                double scaleX = bitmapSource.PixelWidth / ImageDisplay.ActualWidth;
+                double scaleY = bitmapSource.PixelHeight / ImageDisplay.ActualHeight;
+
+                int x = (int)(Math.Min(_roiStartPoint.X, endPoint.X) * scaleX);
+                int y = (int)(Math.Min(_roiStartPoint.Y, endPoint.Y) * scaleY);
+                int width = (int)(Math.Abs(endPoint.X - _roiStartPoint.X) * scaleX);
+                int height = (int)(Math.Abs(endPoint.Y - _roiStartPoint.Y) * scaleY);
+
+                // 确保ROI在图像范围内
+                x = Math.Max(0, Math.Min(x, bitmapSource.PixelWidth - 1));
+                y = Math.Max(0, Math.Min(y, bitmapSource.PixelHeight - 1));
+                width = Math.Max(1, Math.Min(width, bitmapSource.PixelWidth - x));
+                height = Math.Max(1, Math.Min(height, bitmapSource.PixelHeight - y));
+
+                CurrentRoi = new Rect(x, y, width, height);
+            }
+        }
+
+        private void ImageDisplay_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
+        {
+            // 清除ROI
+            CurrentRoi = null;
+            RoiRectangle.Visibility = Visibility.Collapsed;
+        }
+
+        // INotifyPropertyChanged 实现
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+
+        protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+        {
+            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
+            field = value;
+            OnPropertyChanged(propertyName);
+            return true;
+        }
+
+        // 事件
+        public event EventHandler AnalysisCompleted;
+
+        protected virtual void OnAnalysisCompleted()
+        {
+            AnalysisCompleted?.Invoke(this, EventArgs.Empty);
+        }
+    }
+
+    /// <summary>
+    /// RelayCommand实现
+    /// </summary>
+    public class RelayCommand : ICommand
+    {
+        private readonly Action _execute;
+        private readonly Func<bool> _canExecute;
+
+        public event EventHandler CanExecuteChanged
+        {
+            add => CommandManager.RequerySuggested += value;
+            remove => CommandManager.RequerySuggested -= value;
+        }
+
+        public RelayCommand(Action execute, Func<bool> canExecute = null)
+        {
+            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
+            _canExecute = canExecute;
+        }
+
+        public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
+
+        public void Execute(object parameter) => _execute();
+    }
+
+    /// <summary>
+    /// 布尔值到文本转换器
+    /// </summary>
+    public class BoolToTextConverter : System.Windows.Data.IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            if (value is bool boolValue && parameter is string param)
+            {
+                var options = param.Split('|');
+                return boolValue ? options[0] : options[1];
+            }
+            return string.Empty;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 0 - 678
TeamAAS-VM/Core/CameraFocusAnalyzer.cs

@@ -1,678 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using OpenCvSharp;
-
-namespace TeamAAS_VP.Core
-{
-    public class CameraFocusAnalyzer
-    {
-        // 清晰度评估方法枚举
-        public enum FocusMethod
-        {
-            Tenengrad,      // Tenengrad梯度法(推荐)
-            Laplacian,      // 拉普拉斯方差法
-            Brenner,        // Brenner梯度法
-            GrayVariance,   // 灰度方差法
-            SMD             // 灰度差分法
-        }
-
-        /// <summary>
-        /// 计算图像清晰度(多种方法)
-        /// </summary>
-        public static double CalculateImageSharpness(Mat image, FocusMethod method = FocusMethod.Tenengrad, Rect? roi = null)
-        {
-            if (image == null || image.Empty())
-                throw new ArgumentException("输入图像无效");
-
-            // 转换为灰度图
-            Mat gray = new Mat();
-            if (image.Channels() == 3)
-                Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
-            else
-                gray = image.Clone();
-
-            // 应用ROI
-            if (roi.HasValue)
-            {
-                gray = new Mat(gray, roi.Value);
-            }
-
-            double sharpness = 0;
-
-            switch (method)
-            {
-                case FocusMethod.Tenengrad:
-                    sharpness = CalculateTenengrad(gray);
-                    break;
-                case FocusMethod.Laplacian:
-                    sharpness = CalculateLaplacianVariance(gray);
-                    break;
-                case FocusMethod.Brenner:
-                    sharpness = CalculateBrenner(gray);
-                    break;
-                case FocusMethod.GrayVariance:
-                    sharpness = CalculateGrayVariance(gray);
-                    break;
-                case FocusMethod.SMD:
-                    sharpness = CalculateSMD(gray);
-                    break;
-            }
-
-            gray.Dispose();
-            return sharpness;
-        }
-
-        /// <summary>
-        /// Tenengrad梯度法(对噪声敏感度较低)
-        /// </summary>
-        private static double CalculateTenengrad(Mat gray)
-        {
-            Mat gradX = new Mat();
-            Mat gradY = new Mat();
-            Mat magnitude = new Mat();
-
-            Cv2.Sobel(gray, gradX, MatType.CV_32F, 1, 0, 3);
-            Cv2.Sobel(gray, gradY, MatType.CV_32F, 0, 1, 3);
-            Cv2.Magnitude(gradX, gradY, magnitude);
-
-            Scalar mean = Cv2.Mean(magnitude);
-
-            gradX.Dispose();
-            gradY.Dispose();
-            magnitude.Dispose();
-
-            return mean.Val0;
-        }
-
-        /// <summary>
-        /// 拉普拉斯方差法(对边缘敏感)
-        /// </summary>
-        private static double CalculateLaplacianVariance(Mat gray)
-        {
-            Mat laplacian = new Mat();
-            Cv2.Laplacian(gray, laplacian, MatType.CV_32F);
-
-            Mat mean = new Mat();
-            Mat stdDev = new Mat();
-            Cv2.MeanStdDev(laplacian, mean, stdDev);
-
-            double variance = stdDev.Get<double>(0);
-            variance = variance * variance;
-
-            laplacian.Dispose();
-            mean.Dispose();
-            stdDev.Dispose();
-
-            return variance;
-        }
-
-        /// <summary>
-        /// Brenner梯度法(简单快速)
-        /// </summary>
-        private static double CalculateBrenner(Mat gray)
-        {
-            double sum = 0;
-            int width = gray.Cols;
-            int height = gray.Rows;
-
-            for (int y = 0; y < height; y++)
-            {
-                for (int x = 0; x < width - 2; x++)
-                {
-                    int diff = gray.Get<byte>(y, x + 2) - gray.Get<byte>(y, x);
-                    sum += diff * diff;
-                }
-            }
-
-            return sum / (width * height);
-        }
-
-        /// <summary>
-        /// 灰度方差法
-        /// </summary>
-        private static double CalculateGrayVariance(Mat gray)
-        {
-            Mat mean = new Mat();
-            Mat stdDev = new Mat();
-            Cv2.MeanStdDev(gray, mean, stdDev);
-
-            double variance = stdDev.Get<double>(0);
-            variance = variance * variance;
-
-            mean.Dispose();
-            stdDev.Dispose();
-
-            return variance;
-        }
-
-        /// <summary>
-        /// 灰度差分法(SMD)
-        /// </summary>
-        private static double CalculateSMD(Mat gray)
-        {
-            double sum = 0;
-            int width = gray.Cols;
-            int height = gray.Rows;
-
-            for (int y = 0; y < height; y++)
-            {
-                for (int x = 0; x < width - 1; x++)
-                {
-                    sum += Math.Abs(gray.Get<byte>(y, x + 1) - gray.Get<byte>(y, x));
-                }
-            }
-
-            for (int y = 0; y < height - 1; y++)
-            {
-                for (int x = 0; x < width; x++)
-                {
-                    sum += Math.Abs(gray.Get<byte>(y + 1, x) - gray.Get<byte>(y, x));
-                }
-            }
-
-            return sum / (width * height);
-        }
-
-        /// <summary>
-        /// 智能棋盘格对比度分析(简化版)
-        /// </summary>
-        /// <param name="image">输入图像</param>
-        /// <param name="roi">检测区域(可选)</param>
-        /// <returns>对比度分析结果,包含黑白均值、对比度和亮度建议</returns>
-        public static CheckerboardContrastResult AnalyzeCheckerboardContrast(Mat image, Rect? roi = null)
-        {
-            var result = new CheckerboardContrastResult
-            {
-                Success = false,
-                BlackMean = 0,
-                WhiteMean = 0,
-                Contrast = 0,
-                OptimalBrightness = 128
-            };
-
-            if (image == null || image.Empty())
-                return result;
-
-            try
-            {
-                // 转换为灰度图
-                Mat gray = new Mat();
-                if (image.Channels() == 3)
-                    Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
-                else
-                    gray = image.Clone();
-
-                // 应用ROI
-                if (roi.HasValue)
-                {
-                    gray = new Mat(gray, roi.Value);
-                }
-
-                // 1. 自适应检测棋盘格角点
-                var corners = DetectCheckerboardCorners(gray);
-                if (corners == null || corners.Length < 20) // 至少需要一定数量的角点
-                {
-                    gray.Dispose();
-                    return result;
-                }
-
-                // 2. 自动分析网格结构
-                var gridInfo = AnalyzeGridStructure(corners);
-                if (!gridInfo.IsValid)
-                {
-                    gray.Dispose();
-                    return result;
-                }
-
-                // 3. 采样黑白格子像素
-                SampleCheckerboardColors(gray, corners, gridInfo,
-                    out double blackMean, out double whiteMean, out int sampleCount);
-
-                if (sampleCount > 0)
-                {
-                    result.BlackMean = blackMean;
-                    result.WhiteMean = whiteMean;
-                    result.Contrast = Math.Abs(whiteMean - blackMean);
-                    result.OptimalBrightness = CalculateOptimalBrightness(blackMean, whiteMean);
-                    result.Success = true;
-
-                    // 计算图像质量评分
-                    result.QualityScore = CalculateQualityScore(result.Contrast, blackMean, whiteMean);
-                }
-
-                gray.Dispose();
-                return result;
-            }
-            catch (Exception ex)
-            {
-                Console.WriteLine($"棋盘格分析出错: {ex.Message}");
-                return result;
-            }
-        }
-
-        /// <summary>
-        /// 自适应检测棋盘格角点
-        /// </summary>
-        private static Point2f[] DetectCheckerboardCorners(Mat gray)
-        {
-            // 首先尝试标准的棋盘格检测
-            for (int boardWidth = 5; boardWidth <= 15; boardWidth++)
-            {
-                for (int boardHeight = 5; boardHeight <= 15; boardHeight++)
-                {
-                    Size patternSize = new Size(boardWidth, boardHeight);
-                    Point2f[] corners;
-
-                    bool found = Cv2.FindChessboardCorners(gray, patternSize, out corners,
-                        ChessboardFlags.AdaptiveThresh |
-                        ChessboardFlags.NormalizeImage |
-                        ChessboardFlags.FastCheck);
-
-                    if (found && corners.Length > 0)
-                    {
-                        // 提高角点精度
-                        Mat grayFloat = new Mat();
-                        gray.ConvertTo(grayFloat, MatType.CV_32F);
-                        Cv2.CornerSubPix(grayFloat, corners, new Size(5, 5), new Size(-1, -1),
-                            new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 30, 0.1));
-                        grayFloat.Dispose();
-
-                        return corners;
-                    }
-                }
-            }
-
-            // 如果标准方法失败,尝试使用角点检测
-            return DetectCornersByHarris(gray);
-        }
-
-        /// <summary>
-        /// 使用Harris角点检测作为备选方案
-        /// </summary>
-        private static Point2f[] DetectCornersByHarris(Mat gray)
-        {
-            Mat cornersMat = new Mat();
-            Mat grayFloat = new Mat();
-            gray.ConvertTo(grayFloat, MatType.CV_32F);
-
-            // Harris角点检测
-            Cv2.CornerHarris(grayFloat, cornersMat, 2, 3, 0.04);
-
-            // 找到局部最大值
-            var corners = new List<Point2f>();
-            double maxVal = 0;
-            Cv2.MinMaxLoc(cornersMat, out _, out maxVal);
-
-            float threshold = (float)(maxVal * 0.01);
-
-            for (int y = 0; y < cornersMat.Rows; y++)
-            {
-                for (int x = 0; x < cornersMat.Cols; x++)
-                {
-                    float value = cornersMat.Get<float>(y, x);
-                    if (value > threshold)
-                    {
-                        // 检查是否是局部最大值
-                        bool isLocalMax = true;
-                        int neighborhood = 5;
-
-                        for (int dy = -neighborhood; dy <= neighborhood && isLocalMax; dy++)
-                        {
-                            for (int dx = -neighborhood; dx <= neighborhood && isLocalMax; dx++)
-                            {
-                                int ny = y + dy;
-                                int nx = x + dx;
-
-                                if (ny >= 0 && ny < cornersMat.Rows &&
-                                    nx >= 0 && nx < cornersMat.Cols &&
-                                    !(dy == 0 && dx == 0))
-                                {
-                                    if (cornersMat.Get<float>(ny, nx) > value)
-                                    {
-                                        isLocalMax = false;
-                                    }
-                                }
-                            }
-                        }
-
-                        if (isLocalMax)
-                        {
-                            corners.Add(new Point2f(x, y));
-                        }
-                    }
-                }
-            }
-
-            grayFloat.Dispose();
-            cornersMat.Dispose();
-
-            return corners.ToArray();
-        }
-
-        /// <summary>
-        /// 分析网格结构
-        /// </summary>
-        private static GridStructureInfo AnalyzeGridStructure(Point2f[] corners)
-        {
-            var info = new GridStructureInfo();
-
-            if (corners.Length < 9) // 至少3x3网格
-                return info;
-
-            // 估计网格间距
-            List<double> distances = new List<double>();
-            for (int i = 0; i < corners.Length; i++)
-            {
-                for (int j = i + 1; j < Math.Min(i + 10, corners.Length); j++) // 限制检查数量
-                {
-                    double dist = Math.Sqrt(Math.Pow(corners[j].X - corners[i].X, 2) +
-                                           Math.Pow(corners[j].Y - corners[i].Y, 2));
-                    if (dist > 5) // 忽略太近的点
-                        distances.Add(dist);
-                }
-            }
-
-            if (distances.Count == 0)
-                return info;
-
-            // 找到最常见的距离作为网格间距
-            distances.Sort();
-            double estimatedSpacing = distances[distances.Count / 2]; // 中位数
-
-            // 对角落点进行排序(按大致位置)
-            var sortedCorners = corners.OrderBy(p => p.Y).ThenBy(p => p.X).ToArray();
-
-            // 尝试分组到行和列
-            double tolerance = estimatedSpacing * 0.3;
-
-            info.IsValid = true;
-            info.EstimatedSpacing = estimatedSpacing;
-            info.Corners = sortedCorners;
-
-            return info;
-        }
-
-        /// <summary>
-        /// 采样棋盘格颜色
-        /// </summary>
-        private static void SampleCheckerboardColors(Mat gray, Point2f[] corners, GridStructureInfo gridInfo,
-            out double blackMean, out double whiteMean, out int sampleCount)
-        {
-            blackMean = 0;
-            whiteMean = 0;
-            int blackCount = 0;
-            int whiteCount = 0;
-            sampleCount = 0;
-
-            double spacing = gridInfo.EstimatedSpacing;
-            int sampleRadius = (int)(spacing * 0.3);
-
-            // 对每个角点区域采样
-            for (int i = 0; i < corners.Length; i++)
-            {
-                int x = (int)corners[i].X;
-                int y = (int)corners[i].Y;
-
-                // 采样区域
-                int x1 = Math.Max(0, x - sampleRadius);
-                int y1 = Math.Max(0, y - sampleRadius);
-                int x2 = Math.Min(gray.Cols - 1, x + sampleRadius);
-                int y2 = Math.Min(gray.Rows - 1, y + sampleRadius);
-
-                if (x2 > x1 && y2 > y1)
-                {
-                    Rect roi = new Rect(x1, y1, x2 - x1, y2 - y1);
-                    Mat sample = new Mat(gray, roi);
-                    Scalar mean = Cv2.Mean(sample);
-                    sample.Dispose();
-
-                    double value = mean.Val0;
-
-                    // 根据位置判断黑白(奇偶性)
-                    int gridX = (int)(x / spacing);
-                    int gridY = (int)(y / spacing);
-
-                    bool isBlack = (gridX + gridY) % 2 == 0;
-
-                    if (isBlack)
-                    {
-                        blackMean += value;
-                        blackCount++;
-                    }
-                    else
-                    {
-                        whiteMean += value;
-                        whiteCount++;
-                    }
-
-                    sampleCount++;
-                }
-            }
-
-            if (blackCount > 0) blackMean /= blackCount;
-            if (whiteCount > 0) whiteMean /= whiteCount;
-
-            // 确保黑色比白色暗
-            if (blackMean > whiteMean)
-            {
-                // 交换
-                double temp = blackMean;
-                blackMean = whiteMean;
-                whiteMean = temp;
-            }
-        }
-
-        /// <summary>
-        /// 计算最佳亮度
-        /// </summary>
-        private static int CalculateOptimalBrightness(double blackMean, double whiteMean)
-        {
-            double currentMid = (blackMean + whiteMean) / 2;
-            int optimal = (int)Clamp(128 + (128 - currentMid), 30, 225);
-            return optimal;
-        }
-
-        public static double Clamp(double value, double min, double max)
-        {
-            return value < min ? min : (value > max ? max : value);
-        }
-
-        /// <summary>
-        /// 计算图像质量评分
-        /// </summary>
-        private static double CalculateQualityScore(double contrast, double blackMean, double whiteMean)
-        {
-            // 对比度评分(0-100)
-            double contrastScore = Math.Min(100, contrast / 2.55);
-
-            // 亮度范围评分
-            double blackScore = Math.Max(0, 100 - Math.Abs(blackMean - 30));
-            double whiteScore = Math.Max(0, 100 - Math.Abs(whiteMean - 220));
-            double rangeScore = (blackScore + whiteScore) / 2;
-
-            // 综合评分
-            double qualityScore = contrastScore * 0.6 + rangeScore * 0.4;
-
-            return Math.Min(100, Math.Max(0, qualityScore));
-        }
-
-        /// <summary>
-        /// 一键获取图像质量报告
-        /// </summary>
-        public static ImageQualityReport GetImageQualityReport(Mat image, Rect? roi = null)
-        {
-            var report = new ImageQualityReport();
-
-            if (image == null || image.Empty())
-                return report;
-
-            // 计算清晰度
-            report.Sharpness = CalculateImageSharpness(image, FocusMethod.Tenengrad, roi);
-
-            // 分析棋盘格对比度
-            var contrastResult = AnalyzeCheckerboardContrast(image, roi);
-            if (contrastResult.Success)
-            {
-                report.HasCheckerboard = true;
-                report.BlackMean = contrastResult.BlackMean;
-                report.WhiteMean = contrastResult.WhiteMean;
-                report.Contrast = contrastResult.Contrast;
-                report.OptimalBrightness = contrastResult.OptimalBrightness;
-                report.QualityScore = contrastResult.QualityScore;
-            }
-            else
-            {
-                // 如果没有检测到棋盘格,使用图像整体对比度
-                report.HasCheckerboard = false;
-                report.Contrast = CalculateGlobalContrast(image, roi);
-                report.OptimalBrightness = EstimateOptimalBrightness(image, roi);
-                report.QualityScore = Math.Min(100, report.Contrast / 2.0);
-            }
-
-            // 给出建议
-            report.Suggestions = GenerateSuggestions(report);
-
-            return report;
-        }
-
-        /// <summary>
-        /// 计算全局对比度(无棋盘格时使用)
-        /// </summary>
-        private static double CalculateGlobalContrast(Mat image, Rect? roi = null)
-        {
-            Mat gray = new Mat();
-            if (image.Channels() == 3)
-                Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
-            else
-                gray = image.Clone();
-
-            if (roi.HasValue)
-            {
-                gray = new Mat(gray, roi.Value);
-            }
-
-            Mat mean = new Mat();
-            Mat stdDev = new Mat();
-            Cv2.MeanStdDev(gray, mean, stdDev);
-
-            double contrast = stdDev.Get<double>(0) * 2; // 标准差的两倍作为对比度估计
-
-            gray.Dispose();
-            mean.Dispose();
-            stdDev.Dispose();
-
-            return contrast;
-        }
-
-        /// <summary>
-        /// 估计最佳亮度(无棋盘格时使用)
-        /// </summary>
-        private static int EstimateOptimalBrightness(Mat image, Rect? roi = null)
-        {
-            Mat gray = new Mat();
-            if (image.Channels() == 3)
-                Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
-            else
-                gray = image.Clone();
-
-            if (roi.HasValue)
-            {
-                gray = new Mat(gray, roi.Value);
-            }
-
-            Scalar mean = Cv2.Mean(gray);
-            double avgGray = mean.Val0;
-
-            // 根据平均灰度调整
-            int optimal;
-            if (avgGray < 80)
-                optimal = (int)Math.Min(255, avgGray + 80);
-            else if (avgGray > 180)
-                optimal = (int)Math.Max(0, avgGray - 80);
-            else
-                optimal = 128;
-
-            gray.Dispose();
-            return optimal;
-        }
-
-        /// <summary>
-        /// 生成调整建议
-        /// </summary>
-        private static List<string> GenerateSuggestions(ImageQualityReport report)
-        {
-            var suggestions = new List<string>();
-
-            // 清晰度建议
-            if (report.Sharpness < 10)
-                suggestions.Add("图像模糊,请调整焦距使图像变清晰");
-            else if (report.Sharpness < 20)
-                suggestions.Add("清晰度一般,可以继续微调焦距");
-            else
-                suggestions.Add("图像清晰度良好");
-
-            // 对比度建议
-            if (report.HasCheckerboard)
-            {
-                if (report.Contrast < 50)
-                    suggestions.Add("棋盘格对比度过低,建议调整光源或相机参数");
-                else if (report.Contrast < 100)
-                    suggestions.Add("棋盘格对比度适中");
-                else
-                    suggestions.Add("棋盘格对比度很好");
-
-                suggestions.Add($"建议亮度值: {report.OptimalBrightness}");
-            }
-            else
-            {
-                if (report.Contrast < 30)
-                    suggestions.Add("图像整体对比度较低");
-                else
-                    suggestions.Add("图像对比度正常");
-            }
-
-            // 质量评分建议
-            if (report.QualityScore < 60)
-                suggestions.Add($"图像质量需要改进,当前评分: {report.QualityScore:F1}/100");
-            else if (report.QualityScore < 80)
-                suggestions.Add($"图像质量良好,当前评分: {report.QualityScore:F1}/100");
-            else
-                suggestions.Add($"图像质量优秀,当前评分: {report.QualityScore:F1}/100");
-
-            return suggestions;
-        }
-    }
-
-    // 辅助数据结构
-    public class CheckerboardContrastResult
-    {
-        public bool Success { get; set; }
-        public double BlackMean { get; set; }
-        public double WhiteMean { get; set; }
-        public double Contrast { get; set; }
-        public int OptimalBrightness { get; set; }
-        public double QualityScore { get; set; }
-    }
-
-    public class GridStructureInfo
-    {
-        public bool IsValid { get; set; }
-        public double EstimatedSpacing { get; set; }
-        public Point2f[] Corners { get; set; }
-    }
-
-    public class ImageQualityReport
-    {
-        public double Sharpness { get; set; }
-        public bool HasCheckerboard { get; set; }
-        public double BlackMean { get; set; }
-        public double WhiteMean { get; set; }
-        public double Contrast { get; set; }
-        public int OptimalBrightness { get; set; }
-        public double QualityScore { get; set; }
-        public List<string> Suggestions { get; set; } = new List<string>();
-    }
-}

+ 1 - 1
TeamAAS-VM/Core/Cameras/AHuaCamera.cs

@@ -29,7 +29,7 @@ using static MVSDK_Net.IMVDefine;
 
 namespace TeamAAS_VP.Core.Cameras
 {
-    public class AHuaCamera : CameraBaseLock, ICamera, INotifyPropertyChanged
+    public class AHuaCamera : CameraBase, ICamera, INotifyPropertyChanged
     {
         #region 字段
         private Thread m_hReceiveThread;

+ 1 - 1
TeamAAS-VM/Core/Cameras/BaslerCamera.cs

@@ -25,7 +25,7 @@ using Microsoft.Win32;
 
 namespace TeamAAS_VP.Core.Cameras
 {
-    public class BaslerCamera : CameraBaseLock, Interfaces.ICamera, INotifyPropertyChanged
+    public class BaslerCamera : CameraBase, Interfaces.ICamera, INotifyPropertyChanged
     {
         #region 字段
         private Basler.Pylon.Camera m_MyCamera;

+ 0 - 0
TeamAAS-VM/Core/Cameras/CameraBaseLock.cs → TeamAAS-VM/Core/Cameras/CameraBase.cs


+ 711 - 710
TeamAAS-VM/Core/Cameras/CognexCamera.cs

@@ -1,710 +1,711 @@
-using Cognex.VisionPro;
-using MaterialDesignThemes.Wpf;
-using Microsoft.Win32;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Diagnostics.Contracts;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using TeamAAS_VP.Enums;
-using TeamAAS_VP.Interfaces;
-using TeamAAS_VP.Models;
-using TeamAAS_VP;
-
-namespace TeamAAS_VP.Core.Cameras
-{
-    public class CognexCamera : CameraBaseLock, ICamera, INotifyPropertyChanged
-    {
-        #region 字段
-        private Thread m_hReceiveThread;
-        #endregion
-
-        #region 属性
-
-        public CameraBrand CameraBrand { get => CameraBrand.Cognex; }
-
-        public string Name { get; private set; }
-        public Guid ID { get; private set; }
-        public int Index { get; set; }
-        public string ManufacturerName { get; private set; }
-        public bool IsFindByIp { get; private set; }
-        public string ModelName { get; private set; }
-
-        public string SerialNumber { get; private set; }
-
-        public string CameraIp { get; private set; }
-
-        public CameraType CameraType { get; private set; }
-
-        ICogAcqFifo CameraAcq;
-
-        public UInt32 ImageWidth { get; private set; }
-
-        public UInt32 ImageHeight { get; private set; }
-
-        private bool _IsGrabbing;
-
-        /// <summary>
-        /// 正在采集
-        /// </summary>
-        public bool IsGrabbing
-        {
-            get { return _IsGrabbing; }
-            private set { SetProperty(ref _IsGrabbing, value); }
-        }
-
-        private ICogImage _Image;
-
-        public ICogImage Image
-        {
-            get { return _Image; }
-            private set { SetProperty(ref _Image, value); }
-        }
-
-        public bool IsConnected { get; private set; }
-
-        /// <summary>
-        /// 采集用时
-        /// </summary>
-        public TimeSpan TotalTime { get; private set; }
-
-        /// <summary>
-        /// 错误信息
-        /// </summary>
-        public string ErrorMessage { get; private set; }
-
-        private bool IsHaveCamera = false;
-        #endregion
-
-        #region 事件
-        public event Action<ICogImage, TimeSpan, string> ImageCallbackEvent;
-        public event Action<Guid, bool> CameraConnectChangedEvent;
-        #endregion
-
-        public CognexCamera(Guid id, int index, string name, string serialNumber, string cameraip, bool isFindByIp)
-        {
-            ID = id;
-            Name = serialNumber;
-            Index = index;
-            CameraIp = cameraip;
-            SerialNumber = serialNumber;
-            IsFindByIp = isFindByIp;
-            //using (CogFrameGrabbers mFrameGrabbers = new CogFrameGrabbers())//识别已安装的帧抓取器的类//帧抓取器:相机
-            //{
-            //    foreach (ICogFrameGrabber item in mFrameGrabbers)
-            //    {
-            //        if (item.SerialNumber == SerialNumber)
-            //        {
-            //            var videoFormat = item.AvailableVideoFormats[0];
-
-            //            foreach (string format in item.AvailableVideoFormats)
-            //            {
-            //                if (format == "Generic GigEVision (Mono)")
-            //                {
-            //                    videoFormat = format;
-            //                    break;
-            //                }
-
-            //                if (format != "Generic GigEVision (Bayer Color)") continue;
-            //                videoFormat = format;
-            //                break;
-            //            }
-            //            CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
-            //            true);
-            //            //CameraAcq.OwnedExposureParams.Exposure = exposure;
-            //            //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
-            //            //CameraAcq.OwnedContrastParams.Contrast = contrast;
-            //            //
-            //            CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
-            //            CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
-            //            IsHaveCamera = true;
-            //            break;
-            //        }
-
-            //    }
-            //}
-
-            IsConnected = false;
-        }
-
-        #region 方法
-
-        /// <summary>
-        /// 获取设备
-        /// </summary>
-        /// <returns></returns>
-        public static CameraInfo[] GetDevices()
-        {
-            List<CameraInfo> cameras = new List<CameraInfo>();
-            if (!CheckSoftwareInstalled().isInstalled)
-            {
-                LogHelper.WriteLogInfo("未安装Cognex VisionPro");
-                return cameras.ToArray();
-            }
-            using (CogFrameGrabbers mFrameGrabbers = new CogFrameGrabbers())//识别已安装的帧抓取器的类//帧抓取器:相机
-            {
-                int i = 0;
-                foreach (ICogFrameGrabber item in mFrameGrabbers)
-                {
-                    string[] bf1 = item.Name.Split(':');
-                    string manufacturerName=item.Name;
-                    string model = item.Name;
-                    CameraType type = CameraType.GIGE;
-                    if (bf1.Length==3)
-                    {
-                        manufacturerName = bf1[1].Trim();
-                        model = bf1[2].Trim();
-                        if (bf1[0].ToLower().Trim().Contains("gige"))
-                        {
-                            type = CameraType.GIGE;
-                        }
-                        else if (bf1[0].ToLower().Trim().Contains("usb"))
-                        {
-                            type = CameraType.USB;
-                        }
-                        else
-                        {
-                            type = CameraType.Unknown;
-                        }
-                    }
-                    if (type== CameraType.GIGE)
-                    {
-                        cameras.Add(new Models.CameraInfo()
-                        {
-                            CameraNo = i + 1,
-                            CameraName = item.Name,
-                            CameraBrand = CameraBrand.Cognex,
-                            CameraType = type,
-                            ManufacturerName = manufacturerName,
-                            Model = model,
-                            SerialNumber = item.SerialNumber,
-                            CameraIp = item.OwnedGigEAccess.CurrentIPAddress,
-                            IsFindByIp=true,
-                        });
-                    }
-                    else
-                    {
-                        cameras.Add(new Models.CameraInfo()
-                        {
-                            CameraNo = i + 1,
-                            CameraName = "",
-                            CameraBrand = CameraBrand.Cognex,
-                            CameraType = type,
-                            ManufacturerName = manufacturerName,
-                            Model = model,
-                            SerialNumber = item.SerialNumber,
-                            CameraIp = item.Name,
-                        });
-                    }
-                    i++;
-                }
-            }
-            return cameras.ToArray();
-        }
-
-        /// <summary>
-        /// 检查相机软件是否安装
-        /// </summary>
-        /// <returns></returns>
-        public static (bool isInstalled, string version) CheckSoftwareInstalled()
-        {
-            string softwareName = "Cognex VisionPro";
-            string softwareVersion = "90.0.5";
-            string[] registryPaths = new[]
-            {
-                @"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
-                @"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
-            };
-            foreach (var path in registryPaths)
-            {
-                using (RegistryKey key = Registry.LocalMachine.OpenSubKey(path))
-                {
-                    if (key == null) continue;
-
-                    foreach (string subKeyName in key.GetSubKeyNames())
-                    {
-                        using (RegistryKey subKey = key.OpenSubKey(subKeyName))
-                        {
-                            string displayName = subKey.GetValue("DisplayName")?.ToString();
-                            if (displayName != null && displayName.Contains(softwareName))
-                            {
-                                string version = subKey.GetValue("DisplayVersion")?.ToString();
-                                Version v1 = new Version(version);
-                                Version v2 = new Version(softwareVersion);
-                                if (v1 >= v2)
-                                {
-                                    return (true, version);
-                                }
-                                else
-                                {
-                                    return (false, version);
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            return (false, "");
-        }
-
-        /// <summary>
-        /// 打开相机
-        /// </summary>
-        /// <returns></returns>
-        /// <exception cref="Exception"></exception>
-        public bool OpenDevice()
-        {
-            EnterOperation();
-            try
-            {
-                IsConnected = false;
-                if (!IsHaveCamera || CameraAcq==null)
-                {
-                    using (CogFrameGrabbers mFrameGrabbers = new CogFrameGrabbers())//识别已安装的帧抓取器的类//帧抓取器:相机
-                    {
-                        foreach (ICogFrameGrabber item in mFrameGrabbers)
-                        {
-                            string[] bf1 = item.Name.Split(':');
-                            CameraType type = CameraType.GIGE;
-                            if (bf1.Length == 3)
-                            {
-                                if (bf1[0].ToLower().Trim().Contains("gige"))
-                                {
-                                    type = CameraType.GIGE;
-                                }
-                                else if (bf1[0].ToLower().Trim().Contains("usb"))
-                                {
-                                    type = CameraType.USB;
-                                }
-                                else
-                                {
-                                    type = CameraType.Unknown;
-                                }
-                            }
-                            if (type== CameraType.GIGE)
-                            {
-                                if (IsFindByIp)
-                                {
-                                    if (CameraIp== item.OwnedGigEAccess.CurrentIPAddress)
-                                    {
-                                        var videoFormat = item.AvailableVideoFormats[0];
-
-                                        foreach (string format in item.AvailableVideoFormats)
-                                        {
-                                            if (format == "Generic GigEVision (Mono)")
-                                            {
-                                                videoFormat = format;
-                                                break;
-                                            }
-
-                                            if (format != "Generic GigEVision (Bayer Color)") continue;
-                                            videoFormat = format;
-                                            break;
-                                        }
-                                        CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
-                                        true);
-                                        //CameraAcq.OwnedExposureParams.Exposure = exposure;
-                                        //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
-                                        //CameraAcq.OwnedContrastParams.Contrast = contrast;
-                                        //
-                                        CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
-                                        CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
-
-                                        string manufacturerName = item.Name;
-                                        string model = item.Name;
-                                        manufacturerName = bf1[1].Trim();
-                                        model = bf1[2].Trim();
-                                        ManufacturerName = manufacturerName;
-                                        ModelName = model;
-                                        CameraIp = item.OwnedGigEAccess.CurrentIPAddress;
-                                        IsHaveCamera = true;
-                                        break;
-                                    }
-                                }
-                                else
-                                {
-                                    if (item.SerialNumber == SerialNumber)
-                                    {
-                                        var videoFormat = item.AvailableVideoFormats[0];
-
-                                        foreach (string format in item.AvailableVideoFormats)
-                                        {
-                                            if (format == "Generic GigEVision (Mono)")
-                                            {
-                                                videoFormat = format;
-                                                break;
-                                            }
-
-                                            if (format != "Generic GigEVision (Bayer Color)") continue;
-                                            videoFormat = format;
-                                            break;
-                                        }
-                                        CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
-                                        true);
-                                        //CameraAcq.OwnedExposureParams.Exposure = exposure;
-                                        //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
-                                        //CameraAcq.OwnedContrastParams.Contrast = contrast;
-                                        //
-                                        CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
-                                        CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
-
-                                        string manufacturerName = item.Name;
-                                        string model = item.Name;
-                                        manufacturerName = bf1[1].Trim();
-                                        model = bf1[2].Trim();
-                                        ManufacturerName = manufacturerName;
-                                        ModelName = model;
-                                        CameraIp = item.OwnedGigEAccess.CurrentIPAddress;
-                                        IsHaveCamera = true;
-                                        break;
-                                    }
-                                }
-                            }
-                            else
-                            {
-                                if (item.SerialNumber == SerialNumber)
-                                {
-                                    var videoFormat = item.AvailableVideoFormats[0];
-
-                                    foreach (string format in item.AvailableVideoFormats)
-                                    {
-                                        if (format == "Generic GigEVision (Mono)")
-                                        {
-                                            videoFormat = format;
-                                            break;
-                                        }
-
-                                        if (format != "Generic GigEVision (Bayer Color)") continue;
-                                        videoFormat = format;
-                                        break;
-                                    }
-                                    CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
-                                    true);
-                                    //CameraAcq.OwnedExposureParams.Exposure = exposure;
-                                    //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
-                                    //CameraAcq.OwnedContrastParams.Contrast = contrast;
-                                    //
-                                    CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
-                                    CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
-
-                                    string manufacturerName = item.Name;
-                                    string model = item.Name;
-                                    manufacturerName = bf1[1].Trim();
-                                    model = bf1[2].Trim();
-                                    ManufacturerName = manufacturerName;
-                                    ModelName = model;
-                                    IsHaveCamera = true;
-                                    break;
-                                }
-                            }
-                        }
-                    }
-                    if (!IsHaveCamera)
-                    {
-                        return false;
-                    }
-                }
-                if (!IsConnected)
-                {
-                    CameraConnectChangedEvent?.Invoke(ID, true);
-                }
-
-                IsConnected = true;
-                return IsConnected;
-            }
-            finally
-            {
-                ExitOperation();
-            }
-        }
-
-        /// <summary>
-        /// 打开相机异步
-        /// </summary>
-        /// <returns></returns>
-        public Task<bool> OpenDeviceAsync()
-        {
-            return Task.Run(() =>
-            {
-                return OpenDevice();
-            });
-        }
-
-        /// <summary>
-        /// 关闭相机
-        /// </summary>
-        public void CloseDevice()
-        {
-            EnterOperation();
-            try
-            {
-                if (CameraAcq!=null)
-                {
-                    CameraAcq = null;
-
-                }
-                IsConnected = false;
-            }
-            finally
-            {
-                ExitOperation();
-            }
-        }
-
-        /// <summary>
-        /// 采集图像
-        /// </summary>
-        /// <returns></returns>
-        public ICogImage Grab()
-        {
-            EnterOperation();
-            try
-            {
-                Stopwatch sw = Stopwatch.StartNew();
-                for (int i = 0; i < 5; i++)
-                {
-                    try
-                    {
-                        if (CameraAcq == null)
-                        {
-                            OpenDevice();
-                        }
-
-                        CameraAcq.Flush();
-                        Image= CameraAcq.CompleteAcquireEx(new CogAcqInfo());
-                        ImageWidth = (uint)Image.Width;
-                        ImageHeight = (uint)Image.Height;
-                        TotalTime = sw.Elapsed;
-                        ErrorMessage = "";
-                        return Image;
-                    }
-                    catch (Exception ex)
-                    {
-                        ErrorMessage = ex.Message;
-                        CloseDevice();
-                        Thread.Sleep(100);
-                        OpenDevice();
-                    }
-                }
-                if (IsConnected)
-                {
-                    IsConnected = false;
-                    CameraConnectChangedEvent?.Invoke(ID, false);
-                }
-                return null;
-            }
-            finally
-            {
-                ExitOperation();
-            }
-        }
-
-        /// <summary>
-        /// 开始采集图像
-        /// </summary>
-        public void StartGrabbing()
-        {
-            lock (_stateLock)
-            {
-                if (IsGrabbing)
-                    return;
-                IsGrabbing = true;
-            }
-            m_hReceiveThread = new Thread(GetStreamThreadProc) { IsBackground = true };
-            m_hReceiveThread.Start();
-        }
-
-        /// <summary>
-        /// 停止采集图像
-        /// </summary>
-        public void StopGrabbing()
-        {
-            try
-            {
-                lock (_stateLock)
-                {
-                    IsGrabbing = false;
-                }
-                Thread.Sleep(1000);
-                if (m_hReceiveThread != null)
-                {
-                    m_hReceiveThread.Abort();
-                    m_hReceiveThread = null;
-                }
-            }
-            catch (Exception)
-            {
-
-            }
-        }
-
-        /// <summary>
-        /// 设置曝光时间
-        /// </summary>
-        /// <param name="ExposureTime"></param>
-        /// <returns></returns>
-        public bool SetExposureTime(float ExposureTime)
-        {
-            EnterOperation();
-            try
-            {
-                if (CameraAcq==null)
-                    return false;
-                CameraAcq.OwnedExposureParams.Exposure = (double)ExposureTime;
-                return true;
-            }
-            finally
-            {
-                ExitOperation();
-            }
-        }
-
-        /// <summary>
-        /// 获取曝光时间
-        /// </summary>
-        /// <returns></returns>
-        public float GetExposureTime()
-        {
-            EnterOperation();
-            try
-            {
-                if (CameraAcq == null)
-                    return 0;
-                return (float)CameraAcq.OwnedExposureParams.Exposure;
-            }
-            finally
-            {
-                ExitOperation();
-            }
-        }
-
-        /// <summary>
-        /// 设置增益
-        /// </summary>
-        /// <param name="Gain"></param>
-        /// <returns></returns>
-        public bool SetGain(float Gain)
-        {
-            EnterOperation();
-            try
-            {
-                if (CameraAcq==null)
-                    return false;
-                // 设置增益 
-                CameraAcq.OwnedBrightnessParams.Brightness = (double)Gain;
-                return true;
-            }
-            finally
-            {
-                ExitOperation();
-            }
-        }
-
-        /// <summary>
-        /// 获取增益
-        /// </summary>
-        /// <returns></returns>
-        public float GetGain()
-        {
-            EnterOperation();
-            try
-            {
-                if (CameraAcq == null)
-                    return 0;
-                return (float)(CameraAcq.OwnedBrightnessParams.Brightness);
-            }
-            finally
-            {
-                ExitOperation();
-            }
-        }
-
-        private void GetStreamThreadProc()
-        {
-            while (IsGrabbing)
-            {
-                try
-                {
-                    Grab();
-                    ImageCallbackEvent?.Invoke(Image, TotalTime, ErrorMessage);
-                }
-                catch (Exception)
-                {
-
-                }
-                Thread.Sleep(10);
-            }
-            IsGrabbing = false;
-        }
-        #endregion
-
-        #region 属性通知
-        /// <summary>
-        /// Occurs when a property value changes.
-        /// </summary>
-        public event PropertyChangedEventHandler PropertyChanged;
-
-        /// <summary>
-        /// Checks if a property already matches a desired value. Sets the property and
-        /// notifies listeners only when necessary.
        protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
-        {
-            if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
-
-            storage = value;
-            RaisePropertyChanged(propertyName);
-
-            return true;
-        }
-
-        /// <summary>
-        /// Checks if a property already matches a desired value. Sets the property and
-        /// notifies listeners only when necessary.
-        /// </summary>
-        /// <typeparam name="T">Type of the property.</typeparam>
-        /// <param name="storage">Reference to a property with both getter and setter.</param>
-        /// <param name="value">Desired value for the property.</param>
-        /// <param name="propertyName">Name of the property used to notify listeners. This
-        /// value is optional and can be provided automatically when invoked from compilers that
-        /// support CallerMemberName.</param>
-        /// <param name="onChanged">Action that is called after the property value has been changed.</param>
-        /// <returns>True if the value was changed, false if the existing value matched the
-        /// desired value.</returns>
-        protected virtual bool SetProperty<T>(ref T storage, T value, Action onChanged, [CallerMemberName] string propertyName = null)
-        {
-            if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
-
-            storage = value;
-            onChanged?.Invoke();
-            RaisePropertyChanged(propertyName);
-
-            return true;
-        }
-
-        /// <summary>
-        /// Raises this object's PropertyChanged event.
-        /// </summary>
-        /// <param name="propertyName">Name of the property used to notify listeners. This
-        /// value is optional and can be provided automatically when invoked from compilers
-        /// that support <see cref="CallerMemberNameAttribute"/>.</param>
-        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
-        {
-            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
-        }
-
-        /// <summary>
-        /// Raises this object's PropertyChanged event.
-        /// </summary>
-        /// <param name="args">The PropertyChangedEventArgs</param>
-        protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
-        {
-            PropertyChanged?.Invoke(this, args);
-        }
-        #endregion
-    }
-}
+using Cognex.VisionPro;
+using MaterialDesignThemes.Wpf;
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using TeamAAS_VP.Enums;
+using TeamAAS_VP.Interfaces;
+using TeamAAS_VP.Models;
+using TeamAAS_VP;
+
+namespace TeamAAS_VP.Core.Cameras
+{
+    public class CognexCamera : CameraBase, ICamera, INotifyPropertyChanged
+    {
+        #region 字段
+        private Thread m_hReceiveThread;
+        #endregion
+
+        #region 属性
+
+        public CameraBrand CameraBrand { get => CameraBrand.Cognex; }
+
+        public string Name { get; private set; }
+        public Guid ID { get; private set; }
+        public int Index { get; set; }
+        public string ManufacturerName { get; private set; }
+        public bool IsFindByIp { get; private set; }
+        public string ModelName { get; private set; }
+
+        public string SerialNumber { get; private set; }
+
+        public string CameraIp { get; private set; }
+
+        public CameraType CameraType { get; private set; }
+
+        ICogAcqFifo CameraAcq;
+
+        public UInt32 ImageWidth { get; private set; }
+
+        public UInt32 ImageHeight { get; private set; }
+
+        private bool _IsGrabbing;
+
+        /// <summary>
+        /// 正在采集
+        /// </summary>
+        public bool IsGrabbing
+        {
+            get { return _IsGrabbing; }
+            private set { SetProperty(ref _IsGrabbing, value); }
+        }
+
+        private ICogImage _Image;
+
+        public ICogImage Image
+        {
+            get { return _Image; }
+            private set { SetProperty(ref _Image, value); }
+        }
+
+        public bool IsConnected { get; private set; }
+
+        /// <summary>
+        /// 采集用时
+        /// </summary>
+        public TimeSpan TotalTime { get; private set; }
+
+        /// <summary>
+        /// 错误信息
+        /// </summary>
+        public string ErrorMessage { get; private set; }
+
+        private bool IsHaveCamera = false;
+        #endregion
+
+        #region 事件
+        public event Action<ICogImage, TimeSpan, string> ImageCallbackEvent;
+        public event Action<Guid, bool> CameraConnectChangedEvent;
+        #endregion
+
+        public CognexCamera(Guid id, int index, string name, string serialNumber, string cameraip, bool isFindByIp)
+        {
+            ID = id;
+            Name = serialNumber;
+            Index = index;
+            CameraIp = cameraip;
+            SerialNumber = serialNumber;
+            IsFindByIp = isFindByIp;
+            //using (CogFrameGrabbers mFrameGrabbers = new CogFrameGrabbers())//识别已安装的帧抓取器的类//帧抓取器:相机
+            //{
+            //    foreach (ICogFrameGrabber item in mFrameGrabbers)
+            //    {
+            //        if (item.SerialNumber == SerialNumber)
+            //        {
+            //            var videoFormat = item.AvailableVideoFormats[0];
+
+            //            foreach (string format in item.AvailableVideoFormats)
+            //            {
+            //                if (format == "Generic GigEVision (Mono)")
+            //                {
+            //                    videoFormat = format;
+            //                    break;
+            //                }
+
+            //                if (format != "Generic GigEVision (Bayer Color)") continue;
+            //                videoFormat = format;
+            //                break;
+            //            }
+            //            CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
+            //            true);
+            //            //CameraAcq.OwnedExposureParams.Exposure = exposure;
+            //            //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
+            //            //CameraAcq.OwnedContrastParams.Contrast = contrast;
+            //            //
+            //            CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
+            //            CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
+            //            IsHaveCamera = true;
+            //            break;
+            //        }
+
+            //    }
+            //}
+
+            IsConnected = false;
+        }
+
+        #region 方法
+
+        /// <summary>
+        /// 获取设备
+        /// </summary>
+        /// <returns></returns>
+        public static CameraInfo[] GetDevices()
+        {
+            List<CameraInfo> cameras = new List<CameraInfo>();
+            if (!CheckSoftwareInstalled().isInstalled)
+            {
+                LogHelper.WriteLogInfo("未安装Cognex VisionPro");
+                return cameras.ToArray();
+            }
+            using (CogFrameGrabbers mFrameGrabbers = new CogFrameGrabbers())//识别已安装的帧抓取器的类//帧抓取器:相机
+            {
+                int i = 0;
+                foreach (ICogFrameGrabber item in mFrameGrabbers)
+                {
+                    string[] bf1 = item.Name.Split(':');
+                    string manufacturerName=item.Name;
+                    string model = item.Name;
+                    CameraType type = CameraType.GIGE;
+                    if (bf1.Length==3)
+                    {
+                        manufacturerName = bf1[1].Trim();
+                        model = bf1[2].Trim();
+                        if (bf1[0].ToLower().Trim().Contains("gige"))
+                        {
+                            type = CameraType.GIGE;
+                        }
+                        else if (bf1[0].ToLower().Trim().Contains("usb"))
+                        {
+                            type = CameraType.USB;
+                        }
+                        else
+                        {
+                            type = CameraType.Unknown;
+                        }
+                    }
+                    if (type== CameraType.GIGE)
+                    {
+                        cameras.Add(new Models.CameraInfo()
+                        {
+                            CameraNo = i + 1,
+                            CameraName = item.Name,
+                            CameraBrand = CameraBrand.Cognex,
+                            CameraType = type,
+                            ManufacturerName = manufacturerName,
+                            Model = model,
+                            SerialNumber = item.SerialNumber,
+                            CameraIp = item.OwnedGigEAccess.CurrentIPAddress,
+                            IsFindByIp=true,
+                        });
+                    }
+                    else
+                    {
+                        cameras.Add(new Models.CameraInfo()
+                        {
+                            CameraNo = i + 1,
+                            CameraName = "",
+                            CameraBrand = CameraBrand.Cognex,
+                            CameraType = type,
+                            ManufacturerName = manufacturerName,
+                            Model = model,
+                            SerialNumber = item.SerialNumber,
+                            CameraIp = item.Name,
+                        });
+                    }
+                    i++;
+                }
+            }
+            return cameras.ToArray();
+        }
+
+        /// <summary>
+        /// 检查相机软件是否安装
+        /// </summary>
+        /// <returns></returns>
+        public static (bool isInstalled, string version) CheckSoftwareInstalled()
+        {
+            string softwareName = "Cognex VisionPro";
+            string softwareVersion = "90.0.5";
+            string[] registryPaths = new[]
+            {
+                @"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
+                @"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
+            };
+            foreach (var path in registryPaths)
+            {
+                using (RegistryKey key = Registry.LocalMachine.OpenSubKey(path))
+                {
+                    if (key == null) continue;
+
+                    foreach (string subKeyName in key.GetSubKeyNames())
+                    {
+                        using (RegistryKey subKey = key.OpenSubKey(subKeyName))
+                        {
+                            string displayName = subKey.GetValue("DisplayName")?.ToString();
+                            if (displayName != null && displayName.Contains(softwareName))
+                            {
+                                string version = subKey.GetValue("DisplayVersion")?.ToString();
+                                Version v1 = new Version(version);
+                                Version v2 = new Version(softwareVersion);
+                                if (v1 >= v2)
+                                {
+                                    return (true, version);
+                                }
+                                else
+                                {
+                                    return (false, version);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return (false, "");
+        }
+
+        /// <summary>
+        /// 打开相机
+        /// </summary>
+        /// <returns></returns>
+        /// <exception cref="Exception"></exception>
+        public bool OpenDevice()
+        {
+            EnterOperation();
+            try
+            {
+                IsConnected = false;
+                if (!IsHaveCamera || CameraAcq==null)
+                {
+                    using (CogFrameGrabbers mFrameGrabbers = new CogFrameGrabbers())//识别已安装的帧抓取器的类//帧抓取器:相机
+                    {
+                        foreach (ICogFrameGrabber item in mFrameGrabbers)
+                        {
+                            string[] bf1 = item.Name.Split(':');
+                            CameraType type = CameraType.GIGE;
+                            if (bf1.Length == 3)
+                            {
+                                if (bf1[0].ToLower().Trim().Contains("gige"))
+                                {
+                                    type = CameraType.GIGE;
+                                }
+                                else if (bf1[0].ToLower().Trim().Contains("usb"))
+                                {
+                                    type = CameraType.USB;
+                                }
+                                else
+                                {
+                                    type = CameraType.Unknown;
+                                }
+                            }
+                            if (type== CameraType.GIGE)
+                            {
+                                if (IsFindByIp)
+                                {
+                                    if (CameraIp== item.OwnedGigEAccess.CurrentIPAddress)
+                                    {
+                                        var videoFormat = item.AvailableVideoFormats[0];
+
+                                        foreach (string format in item.AvailableVideoFormats)
+                                        {
+                                            if (format == "Generic GigEVision (Mono)")
+                                            {
+                                                videoFormat = format;
+                                                break;
+                                            }
+
+                                            if (format != "Generic GigEVision (Bayer Color)") continue;
+                                            videoFormat = format;
+                                            break;
+                                        }
+                                        CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
+                                        true);
+                                        //CameraAcq.OwnedExposureParams.Exposure = exposure;
+                                        //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
+                                        //CameraAcq.OwnedContrastParams.Contrast = contrast;
+                                        //
+                                        CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
+                                        CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
+
+                                        string manufacturerName = item.Name;
+                                        string model = item.Name;
+                                        manufacturerName = bf1[1].Trim();
+                                        model = bf1[2].Trim();
+                                        ManufacturerName = manufacturerName;
+                                        ModelName = model;
+                                        CameraIp = item.OwnedGigEAccess.CurrentIPAddress;
+                                        IsHaveCamera = true;
+                                        break;
+                                    }
+                                }
+                                else
+                                {
+                                    if (item.SerialNumber == SerialNumber)
+                                    {
+                                        var videoFormat = item.AvailableVideoFormats[0];
+
+                                        foreach (string format in item.AvailableVideoFormats)
+                                        {
+                                            if (format == "Generic GigEVision (Mono)")
+                                            {
+                                                videoFormat = format;
+                                                break;
+                                            }
+
+                                            if (format != "Generic GigEVision (Bayer Color)") continue;
+                                            videoFormat = format;
+                                            break;
+                                        }
+                                        CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
+                                        true);
+                                        //CameraAcq.OwnedExposureParams.Exposure = exposure;
+                                        //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
+                                        //CameraAcq.OwnedContrastParams.Contrast = contrast;
+                                        //
+                                        CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
+                                        CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
+
+                                        string manufacturerName = item.Name;
+                                        string model = item.Name;
+                                        manufacturerName = bf1[1].Trim();
+                                        model = bf1[2].Trim();
+                                        ManufacturerName = manufacturerName;
+                                        ModelName = model;
+                                        CameraIp = item.OwnedGigEAccess.CurrentIPAddress;
+                                        IsHaveCamera = true;
+                                        break;
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                if (item.SerialNumber == SerialNumber)
+                                {
+                                    var videoFormat = item.AvailableVideoFormats[0];
+
+                                    foreach (string format in item.AvailableVideoFormats)
+                                    {
+                                        if (format == "Generic GigEVision (Mono)")
+                                        {
+                                            videoFormat = format;
+                                            break;
+                                        }
+
+                                        if (format != "Generic GigEVision (Bayer Color)") continue;
+                                        videoFormat = format;
+                                        break;
+                                    }
+                                    CameraAcq = item.CreateAcqFifo(videoFormat, CogAcqFifoPixelFormatConstants.Format8Grey, 0,
+                                    true);
+                                    //CameraAcq.OwnedExposureParams.Exposure = exposure;
+                                    //CameraAcq.OwnedBrightnessParams.Brightness = brightness;
+                                    //CameraAcq.OwnedContrastParams.Contrast = contrast;
+                                    //
+                                    CameraAcq.OwnedTriggerParams.TriggerEnabled = true;
+                                    CameraAcq.OwnedTriggerParams.TriggerModel = CogAcqTriggerModelConstants.FreeRun;
+
+                                    string manufacturerName = item.Name;
+                                    string model = item.Name;
+                                    manufacturerName = bf1[1].Trim();
+                                    model = bf1[2].Trim();
+                                    ManufacturerName = manufacturerName;
+                                    ModelName = model;
+                                    IsHaveCamera = true;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (!IsHaveCamera)
+                    {
+                        return false;
+                    }
+                }
+                if (!IsConnected)
+                {
+                    CameraConnectChangedEvent?.Invoke(ID, true);
+                }
+
+                IsConnected = true;
+                return IsConnected;
+            }
+            finally
+            {
+                ExitOperation();
+            }
+        }
+
+        /// <summary>
+        /// 打开相机异步
+        /// </summary>
+        /// <returns></returns>
+        public Task<bool> OpenDeviceAsync()
+        {
+            return Task.Run(() =>
+            {
+                return OpenDevice();
+            });
+        }
+
+        /// <summary>
+        /// 关闭相机
+        /// </summary>
+        public void CloseDevice()
+        {
+            EnterOperation();
+            try
+            {
+                if (CameraAcq!=null)
+                {
+                    CameraAcq = null;
+
+                }
+                IsConnected = false;
+            }
+            finally
+            {
+                ExitOperation();
+            }
+        }
+
+        /// <summary>
+        /// 采集图像
+        /// </summary>
+        /// <returns></returns>
+        public ICogImage Grab()
+        {
+            EnterOperation();
+            try
+            {
+                Stopwatch sw = Stopwatch.StartNew();
+                for (int i = 0; i < 5; i++)
+                {
+                    try
+                    {
+                        if (CameraAcq == null)
+                        {
+                            OpenDevice();
+                        }
+
+                        CameraAcq.Flush();
+                        Image= CameraAcq.CompleteAcquireEx(new CogAcqInfo());
+                        ImageWidth = (uint)Image.Width;
+                        ImageHeight = (uint)Image.Height;
+                        TotalTime = sw.Elapsed;
+                        ErrorMessage = "";
+                        return Image;
+                    }
+                    catch (Exception ex)
+                    {
+                        ErrorMessage = ex.Message;
+                        CloseDevice();
+                        Thread.Sleep(100);
+                        OpenDevice();
+                    }
+                }
+                if (IsConnected)
+                {
+                    IsConnected = false;
+                    CameraConnectChangedEvent?.Invoke(ID, false);
+                }
+                return null;
+            }
+            finally
+            {
+                ExitOperation();
+            }
+        }
+
+        /// <summary>
+        /// 开始采集图像
+        /// </summary>
+        public void StartGrabbing()
+        {
+            lock (_stateLock)
+            {
+                if (IsGrabbing)
+                    return;
+                IsGrabbing = true;
+            }
+            m_hReceiveThread = new Thread(GetStreamThreadProc) { IsBackground = true };
+            m_hReceiveThread.Start();
+        }
+
+        /// <summary>
+        /// 停止采集图像
+        /// </summary>
+        public void StopGrabbing()
+        {
+            try
+            {
+                lock (_stateLock)
+                {
+                    IsGrabbing = false;
+                }
+                Thread.Sleep(1000);
+                if (m_hReceiveThread != null)
+                {
+                    m_hReceiveThread.Abort();
+                    m_hReceiveThread = null;
+                }
+            }
+            catch (Exception)
+            {
+
+            }
+        }
+
+        /// <summary>
+        /// 设置曝光时间
+        /// </summary>
+        /// <param name="ExposureTime"></param>
+        /// <returns></returns>
+        public bool SetExposureTime(float ExposureTime)
+        {
+            EnterOperation();
+            try
+            {
+                if (CameraAcq==null)
+                    return false;
+                CameraAcq.OwnedExposureParams.Exposure = (double)ExposureTime;
+                return true;
+            }
+            finally
+            {
+                ExitOperation();
+            }
+        }
+
+        /// <summary>
+        /// 获取曝光时间
+        /// </summary>
+        /// <returns></returns>
+        public float GetExposureTime()
+        {
+            EnterOperation();
+            try
+            {
+                if (CameraAcq == null)
+                    return 0;
+                return (float)CameraAcq.OwnedExposureParams.Exposure;
+            }
+            finally
+            {
+                ExitOperation();
+            }
+        }
+
+        /// <summary>
+        /// 设置增益
+        /// </summary>
+        /// <param name="Gain"></param>
+        /// <returns></returns>
+        public bool SetGain(float Gain)
+        {
+            EnterOperation();
+            try
+            {
+                if (CameraAcq==null)
+                    return false;
+                // 设置增益 
+                CameraAcq.OwnedBrightnessParams.Brightness = (double)Gain;
+                return true;
+            }
+            finally
+            {
+                ExitOperation();
+            }
+        }
+
+        /// <summary>
+        /// 获取增益
+        /// </summary>
+        /// <returns></returns>
+        public float GetGain()
+        {
+            EnterOperation();
+            try
+            {
+                if (CameraAcq == null)
+                    return 0;
+                return (float)(CameraAcq.OwnedBrightnessParams.Brightness);
+            }
+            finally
+            {
+                ExitOperation();
+            }
+        }
+
+        private void GetStreamThreadProc()
+        {
+            while (IsGrabbing)
+            {
+                try
+                {
+                    Grab();
+                    ImageCallbackEvent?.Invoke(Image, TotalTime, ErrorMessage);
+                }
+                catch (Exception)
+                {
+
+                }
+                Thread.Sleep(10);
+            }
+            IsGrabbing = false;
+        }
+        #endregion
+
+        #region 属性通知
+        /// <summary>
+        /// Occurs when a property value changes.
+        /// </summary>
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        /// <summary>
+        /// Checks if a property already matches a desired value. Sets the property and
+        /// notifies listeners only when necessary.
+        protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
+        {
+            if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
+
+            storage = value;
+            RaisePropertyChanged(propertyName);
+
+            return true;
+        }
+
+        /// <summary>
+        /// Checks if a property already matches a desired value. Sets the property and
+        /// notifies listeners only when necessary.
+        /// </summary>
+        /// <typeparam name="T">Type of the property.</typeparam>
+        /// <param name="storage">Reference to a property with both getter and setter.</param>
+        /// <param name="value">Desired value for the property.</param>
+        /// <param name="propertyName">Name of the property used to notify listeners. This
+        /// value is optional and can be provided automatically when invoked from compilers that
+        /// support CallerMemberName.</param>
+        /// <param name="onChanged">Action that is called after the property value has been changed.</param>
+        /// <returns>True if the value was changed, false if the existing value matched the
+        /// desired value.</returns>
+        protected virtual bool SetProperty<T>(ref T storage, T value, Action onChanged, [CallerMemberName] string propertyName = null)
+        {
+            if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
+
+            storage = value;
+            onChanged?.Invoke();
+            RaisePropertyChanged(propertyName);
+
+            return true;
+        }
+
+        /// <summary>
+        /// Raises this object's PropertyChanged event.
+        /// </summary>
+        /// <param name="propertyName">Name of the property used to notify listeners. This
+        /// value is optional and can be provided automatically when invoked from compilers
+        /// that support <see cref="CallerMemberNameAttribute"/>.</param>
+        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
+        }
+
+        /// <summary>
+        /// Raises this object's PropertyChanged event.
+        /// </summary>
+        /// <param name="args">The PropertyChangedEventArgs</param>
+        protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
+        {
+            PropertyChanged?.Invoke(this, args);
+        }
+        #endregion
+    }
+}

+ 1 - 1
TeamAAS-VM/Core/Cameras/GalaxyCamera.cs

@@ -21,7 +21,7 @@ using TeamAAS_VP.Models;
 
 namespace TeamAAS_VP.Core.Cameras
 {
-    public class GalaxyCamera : CameraBaseLock, ICamera, INotifyPropertyChanged
+    public class GalaxyCamera : CameraBase, ICamera, INotifyPropertyChanged
     {
         #region 字段
         private Thread m_hReceiveThread;

+ 734 - 0
TeamAAS-VM/Core/FocusAnalysisEngine.cs

@@ -0,0 +1,734 @@
+using MathNet.Numerics.Statistics;
+using OpenCvSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using TeamAAS_VP.Enums;
+using TeamAAS_VP.Models;
+
+namespace TeamAAS_VP.Core
+{
+    /// <summary>
+    /// 图像清晰度分析引擎
+    /// </summary>
+    public class FocusAnalysisEngine
+    {
+        // 自定义Clamp方法
+        private static double Clamp(double value, double min, double max)
+        {
+            return value < min ? min : (value > max ? max : value);
+        }
+
+        private static int Clamp(int value, int min, int max)
+        {
+            return value < min ? min : (value > max ? max : value);
+        }
+
+        /// <summary>
+        /// 计算图像清晰度
+        /// </summary>
+        public static double CalculateImageSharpness(Mat image, FocusMethod method, Rect? roi = null)
+        {
+            if (image == null || image.Empty())
+                throw new ArgumentException("输入图像无效");
+
+            // 转换为灰度图
+            Mat gray = new Mat();
+            if (image.Channels() == 3)
+                Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
+            else
+                gray = image.Clone();
+
+            // 应用ROI
+            if (roi.HasValue)
+            {
+                gray = new Mat(gray, roi.Value);
+            }
+
+            double sharpness = 0;
+
+            switch (method)
+            {
+                case FocusMethod.Tenengrad:
+                    sharpness = CalculateTenengrad(gray);
+                    break;
+                case FocusMethod.Laplacian:
+                    sharpness = CalculateLaplacianVariance(gray);
+                    break;
+                case FocusMethod.Brenner:
+                    sharpness = CalculateBrenner(gray);
+                    break;
+                case FocusMethod.GrayVariance:
+                    sharpness = CalculateGrayVariance(gray);
+                    break;
+                case FocusMethod.SMD:
+                    sharpness = CalculateSMD(gray);
+                    break;
+            }
+
+            gray.Dispose();
+            return sharpness;
+        }
+
+        private static double CalculateTenengrad(Mat gray)
+        {
+            Mat gradX = new Mat();
+            Mat gradY = new Mat();
+            Mat magnitude = new Mat();
+
+            Cv2.Sobel(gray, gradX, MatType.CV_32F, 1, 0, 3);
+            Cv2.Sobel(gray, gradY, MatType.CV_32F, 0, 1, 3);
+            Cv2.Magnitude(gradX, gradY, magnitude);
+
+            Scalar mean = Cv2.Mean(magnitude);
+
+            gradX.Dispose();
+            gradY.Dispose();
+            magnitude.Dispose();
+
+            return mean.Val0;
+        }
+
+        private static double CalculateLaplacianVariance(Mat gray)
+        {
+            Mat laplacian = new Mat();
+            Cv2.Laplacian(gray, laplacian, MatType.CV_32F);
+
+            Mat mean = new Mat();
+            Mat stdDev = new Mat();
+            Cv2.MeanStdDev(laplacian, mean, stdDev);
+
+            double variance = stdDev.Get<double>(0);
+            variance = variance * variance;
+
+            laplacian.Dispose();
+            mean.Dispose();
+            stdDev.Dispose();
+
+            return variance;
+        }
+
+        private static double CalculateBrenner(Mat gray)
+        {
+            double sum = 0;
+            int width = gray.Cols;
+            int height = gray.Rows;
+
+            for (int y = 0; y < height; y++)
+            {
+                for (int x = 0; x < width - 2; x++)
+                {
+                    int diff = gray.Get<byte>(y, x + 2) - gray.Get<byte>(y, x);
+                    sum += diff * diff;
+                }
+            }
+
+            return sum / (width * height);
+        }
+
+        private static double CalculateGrayVariance(Mat gray)
+        {
+            Mat mean = new Mat();
+            Mat stdDev = new Mat();
+            Cv2.MeanStdDev(gray, mean, stdDev);
+
+            double variance = stdDev.Get<double>(0);
+            variance = variance * variance;
+
+            mean.Dispose();
+            stdDev.Dispose();
+
+            return variance;
+        }
+
+        private static double CalculateSMD(Mat gray)
+        {
+            double sum = 0;
+            int width = gray.Cols;
+            int height = gray.Rows;
+
+            for (int y = 0; y < height; y++)
+            {
+                for (int x = 0; x < width - 1; x++)
+                {
+                    sum += Math.Abs(gray.Get<byte>(y, x + 1) - gray.Get<byte>(y, x));
+                }
+            }
+
+            for (int y = 0; y < height - 1; y++)
+            {
+                for (int x = 0; x < width; x++)
+                {
+                    sum += Math.Abs(gray.Get<byte>(y + 1, x) - gray.Get<byte>(y, x));
+                }
+            }
+
+            return sum / (width * height);
+        }
+
+        /// <summary>
+        /// 分析棋盘格对比度
+        /// </summary>
+        public static CheckerboardResult AnalyzeCheckerboardContrast(Mat image, Rect? roi = null)
+        {
+            var result = new CheckerboardResult();
+
+            try
+            {
+                // 转换为灰度图
+                Mat gray = new Mat();
+                if (image.Channels() == 3)
+                    Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
+                else
+                    gray = image.Clone();
+
+                // 应用ROI
+                if (roi.HasValue)
+                {
+                    gray = new Mat(gray, roi.Value);
+                }
+
+                // 简化版的棋盘格检测
+                bool detected = TryDetectCheckerboard(gray, out double blackMean, out double whiteMean);
+
+                if (detected)
+                {
+                    result.Detected = true;
+                    result.BlackMean = blackMean;
+                    result.WhiteMean = whiteMean;
+                    result.Contrast = Math.Abs(whiteMean - blackMean);
+                    result.OptimalBrightness = CalculateOptimalBrightness(blackMean, whiteMean);
+                }
+
+                gray.Dispose();
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"棋盘格分析出错: {ex.Message}");
+            }
+
+            return result;
+        }
+
+        private static bool TryDetectCheckerboard(Mat gray, out double blackMean, out double whiteMean)
+        {
+            blackMean = 0;
+            whiteMean = 0;
+
+            try
+            {
+                // 使用自适应阈值进行二值化
+                Mat binary = new Mat();
+                Cv2.AdaptiveThreshold(gray, binary, 255,
+                    AdaptiveThresholdTypes.GaussianC,
+                    ThresholdTypes.Binary, 11, 2);
+
+                // 查找轮廓
+                var contours = Cv2.FindContoursAsArray(binary,
+                    RetrievalModes.External,
+                    ContourApproximationModes.ApproxSimple);
+
+                // 寻找近似矩形的轮廓(可能是棋盘格)
+                var rectangles = new List<Rect>();
+                foreach (var contour in contours)
+                {
+                    var poly = Cv2.ApproxPolyDP(contour, 0.02 * Cv2.ArcLength(contour, true), true);
+
+                    if (poly.Length == 4) // 四边形
+                    {
+                        var rect = Cv2.BoundingRect(contour);
+                        if (rect.Width > 50 && rect.Height > 50) // 忽略太小的区域
+                        {
+                            rectangles.Add(rect);
+                        }
+                    }
+                }
+
+                if (rectangles.Count >= 2)
+                {
+                    // 采样黑色和白色区域
+                    blackMean = SampleRegionMean(gray, rectangles[0]);
+                    whiteMean = SampleRegionMean(gray, rectangles.Count > 1 ? rectangles[1] : rectangles[0]);
+
+                    // 确保黑色比白色暗
+                    if (blackMean > whiteMean)
+                    {
+                        double temp = blackMean;
+                        blackMean = whiteMean;
+                        whiteMean = temp;
+                    }
+
+                    return true;
+                }
+
+                binary.Dispose();
+            }
+            catch
+            {
+                // 如果检测失败,返回false
+            }
+
+            return false;
+        }
+
+        private static double SampleRegionMean(Mat gray, Rect region)
+        {
+            var sample = new Mat(gray, region);
+            Scalar mean = Cv2.Mean(sample);
+            sample.Dispose();
+            return mean.Val0;
+        }
+
+        private static int CalculateOptimalBrightness(double blackMean, double whiteMean)
+        {
+            double currentMid = (blackMean + whiteMean) / 2;
+            return (int)Clamp(128 + (128 - currentMid), 30, 225);
+        }
+
+        /// <summary>
+        /// 计算图像质量评分
+        /// </summary>
+        public static double CalculateQualityScore(double sharpness, CheckerboardResult checkerboard)
+        {
+            // 清晰度评分(0-50分)
+            double sharpnessScore = Clamp(sharpness / 0.5, 0, 50);
+
+            // 对比度评分(0-50分)
+            double contrastScore = 0;
+            if (checkerboard.Detected)
+            {
+                contrastScore = Clamp(checkerboard.Contrast / 2, 0, 50);
+            }
+
+            return sharpnessScore + contrastScore;
+        }
+
+        /// <summary>
+        /// 生成建议
+        /// </summary>
+        public static List<string> GenerateSuggestions(double sharpness, CheckerboardResult checkerboard,
+            double sharpnessThreshold, double contrastThreshold)
+        {
+            var suggestions = new List<string>();
+
+            // 清晰度建议
+            if (sharpness < sharpnessThreshold)
+                suggestions.Add("图像模糊,请调整焦距使图像变清晰");
+            else if (sharpness < sharpnessThreshold * 2)
+                suggestions.Add("清晰度一般,可以继续微调焦距");
+            else
+                suggestions.Add("图像清晰度良好");
+
+            // 棋盘格建议
+            if (checkerboard.Detected)
+            {
+                if (checkerboard.Contrast < contrastThreshold)
+                    suggestions.Add("棋盘格对比度过低,建议调整光源");
+                else if (checkerboard.Contrast < contrastThreshold * 2)
+                    suggestions.Add("棋盘格对比度适中");
+                else
+                    suggestions.Add("棋盘格对比度很好");
+
+                suggestions.Add($"建议亮度值: {checkerboard.OptimalBrightness}");
+            }
+            else
+            {
+                suggestions.Add("未检测到棋盘格,请确保棋盘格在视野中");
+            }
+
+            return suggestions;
+        }
+
+        /// <summary>
+        /// 使用MathNet计算统计信息
+        /// </summary>
+        public static StatisticalSummary CalculateStatistics(List<double> data)
+        {
+            if (data == null || data.Count == 0)
+                return new StatisticalSummary();
+
+            // 使用MathNet.Numerics.Statistics计算
+            var stats = MathNet.Numerics.Statistics.Statistics.MeanVariance(data);
+
+            return new StatisticalSummary
+            {
+                Mean = stats.Item1,  // 均值
+                Variance = stats.Item2,  // 方差
+                StdDev = Math.Sqrt(stats.Item2),  // 标准差
+                Min = data.Min(),
+                Max = data.Max(),
+                Count = data.Count
+            };
+        }
+
+        /// <summary>
+        /// 使用MathNet计算更详细的统计信息
+        /// </summary>
+        public static StatisticalSummary CalculateDetailedStatistics(List<double> data)
+        {
+            if (data == null || data.Count == 0)
+                return new StatisticalSummary();
+
+            // 使用DescriptiveStatistics获取更多统计信息
+            var descriptiveStats = new MathNet.Numerics.Statistics.DescriptiveStatistics(data);
+
+            return new StatisticalSummary
+            {
+                Mean = descriptiveStats.Mean,
+                Variance = descriptiveStats.Variance,
+                StdDev = descriptiveStats.StandardDeviation,
+                Min = descriptiveStats.Minimum,
+                Max = descriptiveStats.Maximum,
+                Count = data.Count,
+                // 还可以添加更多统计信息
+                Skewness = descriptiveStats.Skewness,
+                Kurtosis = descriptiveStats.Kurtosis
+            };
+        }
+
+        /// <summary>
+        /// 计算移动平均
+        /// </summary>
+        public static List<double> CalculateMovingAverage(List<double> data, int windowSize)
+        {
+            if (data == null || data.Count == 0 || windowSize <= 0)
+                return new List<double>();
+
+            var result = new List<double>();
+
+            for (int i = 0; i < data.Count; i++)
+            {
+                int start = Math.Max(0, i - windowSize + 1);
+                int count = Math.Min(windowSize, i + 1);
+
+                double sum = 0;
+                for (int j = start; j <= i; j++)
+                {
+                    sum += data[j];
+                }
+                result.Add(sum / count);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// 计算指数移动平均
+        /// </summary>
+        public static List<double> CalculateExponentialMovingAverage(List<double> data, double alpha)
+        {
+            if (data == null || data.Count == 0 || alpha <= 0 || alpha > 1)
+                return new List<double>();
+
+            var result = new List<double> { data[0] };
+
+            for (int i = 1; i < data.Count; i++)
+            {
+                double ema = alpha * data[i] + (1 - alpha) * result[i - 1];
+                result.Add(ema);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// 检测峰值
+        /// </summary>
+        public static List<int> DetectPeaks(List<double> data, double threshold = 0.5)
+        {
+            var peaks = new List<int>();
+
+            if (data == null || data.Count < 3)
+                return peaks;
+
+            var stats = CalculateStatistics(data);
+            double mean = stats.Mean;
+            double stdDev = stats.StdDev;
+
+            for (int i = 1; i < data.Count - 1; i++)
+            {
+                if (data[i] > data[i - 1] && data[i] > data[i + 1])
+                {
+                    // 峰值需要超过阈值
+                    if (data[i] > mean + threshold * stdDev)
+                    {
+                        peaks.Add(i);
+                    }
+                }
+            }
+
+            return peaks;
+        }
+
+        /// <summary>
+        /// 计算趋势线(线性回归)
+        /// </summary>
+        public static (double slope, double intercept) CalculateTrendLine(List<double> data)
+        {
+            if (data == null || data.Count == 0)
+                return (0, 0);
+
+            var xData = Enumerable.Range(0, data.Count).Select(x => (double)x).ToArray();
+            var yData = data.ToArray();
+
+            // 使用MathNet进行线性回归
+            var fit = MathNet.Numerics.Fit.Line(xData, yData);
+
+            return (fit.Item2, fit.Item1);  // slope, intercept
+        }
+
+        /// <summary>
+        /// 计算自相关性
+        /// </summary>
+        public static List<double> CalculateAutocorrelation(List<double> data, int maxLag = 20)
+        {
+            if (data == null || data.Count == 0)
+                return new List<double>();
+
+            var autocorr = new List<double>();
+            var stats = CalculateStatistics(data);
+            double mean = stats.Mean;
+            double variance = stats.Variance;
+
+            if (variance == 0) return autocorr;
+
+            int n = data.Count;
+            maxLag = Math.Min(maxLag, n - 1);
+
+            for (int lag = 0; lag <= maxLag; lag++)
+            {
+                double sum = 0;
+                for (int i = 0; i < n - lag; i++)
+                {
+                    sum += (data[i] - mean) * (data[i + lag] - mean);
+                }
+                autocorr.Add(sum / ((n - lag) * variance));
+            }
+
+            return autocorr;
+        }
+    }
+
+    /// <summary>
+    /// 扩展的统计摘要类
+    /// </summary>
+    public class StatisticalSummary
+    {
+        public double Mean { get; set; }
+        public double Variance { get; set; }
+        public double StdDev { get; set; }
+        public double Min { get; set; }
+        public double Max { get; set; }
+        public int Count { get; set; }
+
+        // 扩展的统计信息
+        public double Skewness { get; set; }  // 偏度
+        public double Kurtosis { get; set; }  // 峰度
+
+        // 分位数(可选)
+        public double Median { get; set; }
+        public double Q1 { get; set; }  // 第一四分位数
+        public double Q3 { get; set; }  // 第三四分位数
+        public double IQR => Q3 - Q1;   // 四分位距
+
+        /// <summary>
+        /// 计算分位数
+        /// </summary>
+        public void CalculateQuantiles(List<double> data)
+        {
+            if (data == null || data.Count == 0)
+                return;
+
+            var sortedData = data.OrderBy(x => x).ToList();
+
+            // 中位数
+            Median = MathNet.Numerics.Statistics.Statistics.Median(sortedData);
+
+            // 四分位数
+            Q1 = MathNet.Numerics.Statistics.Statistics.Quantile(sortedData, 0.25);
+            Q3 = MathNet.Numerics.Statistics.Statistics.Quantile(sortedData, 0.75);
+        }
+
+        /// <summary>
+        /// 生成统计摘要字符串
+        /// </summary>
+        public string ToSummaryString()
+        {
+            return $@"统计摘要:
+样本数量: {Count}
+均值: {Mean:F3}
+标准差: {StdDev:F3}
+最小值: {Min:F3}
+最大值: {Max:F3}
+范围: {Max - Min:F3}
+方差: {Variance:F3}
+偏度: {Skewness:F3}
+峰度: {Kurtosis:F3}";
+        }
+
+        /// <summary>
+        /// 获取详细统计信息(包含分位数)
+        /// </summary>
+        public string ToDetailedString()
+        {
+            return $@"详细统计信息:
+样本数量: {Count}
+均值: {Mean:F3} ± {StdDev:F3}
+中位数: {Median:F3}
+第一四分位数(Q1): {Q1:F3}
+第三四分位数(Q3): {Q3:F3}
+四分位距(IQR): {IQR:F3}
+最小值: {Min:F3}
+最大值: {Max:F3}
+范围: {Max - Min:F3}
+方差: {Variance:F3}
+标准差: {StdDev:F3}
+变异系数: {(StdDev / Mean * 100):F1}%
+偏度: {Skewness:F3}
+峰度: {Kurtosis:F3}";
+        }
+    }
+
+    /// <summary>
+    /// 分析结果增强类
+    /// </summary>
+    public class EnhancedAnalysisResult : AnalysisResult
+    {
+        public StatisticalSummary SharpnessStats { get; set; }
+        public StatisticalSummary ContrastStats { get; set; }
+        public List<int> SharpnessPeaks { get; set; }
+        public double TrendSlope { get; set; }
+        public double TrendIntercept { get; set; }
+        public double AutocorrelationAtLag1 { get; set; }
+
+        public EnhancedAnalysisResult()
+        {
+            SharpnessStats = new StatisticalSummary();
+            ContrastStats = new StatisticalSummary();
+            SharpnessPeaks = new List<int>();
+        }
+    }
+
+    /// <summary>
+    /// 增强的分析引擎
+    /// </summary>
+    public static class EnhancedFocusAnalysisEngine
+    {
+        /// <summary>
+        /// 执行增强分析
+        /// </summary>
+        public static EnhancedAnalysisResult PerformEnhancedAnalysis(
+            List<DataPoint> historyData,
+            AnalysisResult baseResult)
+        {
+            var enhancedResult = new EnhancedAnalysisResult
+            {
+                // 复制基础结果
+                Sharpness = baseResult.Sharpness,
+                Checkerboard = baseResult.Checkerboard,
+                QualityScore = baseResult.QualityScore,
+                FrameCount = baseResult.FrameCount,
+                BestSharpness = baseResult.BestSharpness,
+                AnalysisTime = baseResult.AnalysisTime,
+                Suggestions = baseResult.Suggestions
+            };
+
+            if (historyData == null || historyData.Count == 0)
+                return enhancedResult;
+
+            // 提取清晰度和对比度数据
+            var sharpnessData = historyData.Select(d => d.Sharpness).ToList();
+            var contrastData = historyData
+                .Where(d => d.Contrast > 0)
+                .Select(d => d.Contrast)
+                .ToList();
+
+            // 计算统计信息
+            enhancedResult.SharpnessStats = FocusAnalysisEngine.CalculateDetailedStatistics(sharpnessData);
+            if (contrastData.Count > 0)
+            {
+                enhancedResult.ContrastStats = FocusAnalysisEngine.CalculateDetailedStatistics(contrastData);
+            }
+
+            // 计算分位数
+            enhancedResult.SharpnessStats.CalculateQuantiles(sharpnessData);
+
+            // 检测峰值
+            enhancedResult.SharpnessPeaks = FocusAnalysisEngine.DetectPeaks(sharpnessData, 1.0);
+
+            // 计算趋势线
+            var trend = FocusAnalysisEngine.CalculateTrendLine(sharpnessData);
+            enhancedResult.TrendSlope = trend.slope;
+            enhancedResult.TrendIntercept = trend.intercept;
+
+            // 计算自相关性
+            var autocorr = FocusAnalysisEngine.CalculateAutocorrelation(sharpnessData, 1);
+            if (autocorr.Count > 1)
+            {
+                enhancedResult.AutocorrelationAtLag1 = autocorr[1];
+            }
+
+            return enhancedResult;
+        }
+
+        /// <summary>
+        /// 生成增强分析报告
+        /// </summary>
+        public static string GenerateEnhancedReport(EnhancedAnalysisResult result)
+        {
+            var report = new System.Text.StringBuilder();
+
+            report.AppendLine("=== 增强分析报告 ===");
+            report.AppendLine($"分析时间: {result.AnalysisTime:yyyy-MM-dd HH:mm:ss}");
+            report.AppendLine($"总帧数: {result.FrameCount}");
+            report.AppendLine();
+
+            report.AppendLine("一、清晰度统计分析");
+            report.AppendLine(result.SharpnessStats.ToDetailedString());
+            report.AppendLine();
+
+            report.AppendLine("二、趋势分析");
+            report.AppendLine($"趋势线斜率: {result.TrendSlope:F6}");
+            report.AppendLine($"趋势线截距: {result.TrendIntercept:F3}");
+            report.AppendLine($"自相关性(lag=1): {result.AutocorrelationAtLag1:F3}");
+            report.AppendLine($"检测到峰值数量: {result.SharpnessPeaks.Count}");
+
+            if (result.SharpnessPeaks.Any())
+            {
+                report.Append("峰值位置(帧): ");
+                report.AppendLine(string.Join(", ", result.SharpnessPeaks));
+            }
+            report.AppendLine();
+
+            if (result.ContrastStats.Count > 0)
+            {
+                report.AppendLine("三、对比度统计分析");
+                report.AppendLine(result.ContrastStats.ToSummaryString());
+                report.AppendLine();
+            }
+
+            report.AppendLine("四、分析建议");
+            if (result.SharpnessPeaks.Count > 0)
+            {
+                int lastPeak = result.SharpnessPeaks.Last();
+                report.AppendLine($"发现清晰度峰值,最后峰值在第 {lastPeak + 1} 帧");
+
+                if (result.TrendSlope < -0.01)
+                    report.AppendLine("清晰度呈下降趋势,可能已过最佳对焦点");
+                else if (result.TrendSlope > 0.01)
+                    report.AppendLine("清晰度呈上升趋势,可继续当前方向调整");
+                else
+                    report.AppendLine("清晰度趋势平稳");
+            }
+
+            if (Math.Abs(result.AutocorrelationAtLag1) > 0.5)
+            {
+                report.AppendLine("数据具有较强自相关性,表明调整过程平稳");
+            }
+
+            return report.ToString();
+        }
+    }
+}

+ 27 - 0
TeamAAS-VM/Enums/AnalysisMode.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Enums
+{
+    /// <summary>
+    /// 分析模式
+    /// </summary>
+    public enum AnalysisMode
+    {
+        /// <summary>
+        /// 自动检测棋盘格
+        /// </summary>
+        AutoCheckerboard,
+        /// <summary>
+        /// 全图分析
+        /// </summary>
+        FullImage,
+        /// <summary>
+        /// 自定义ROI
+        /// </summary>
+        CustomROI
+    }
+}

+ 35 - 0
TeamAAS-VM/Enums/FocusMethod.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Enums
+{
+    /// <summary>
+    /// 清晰度计算方法
+    /// </summary>
+    public enum FocusMethod
+    {
+        /// <summary>
+        /// Tenengrad梯度法(推荐)
+        /// </summary>
+        Tenengrad,
+        /// <summary>
+        /// 拉普拉斯方差法
+        /// </summary>
+        Laplacian,
+        /// <summary>
+        /// Brenner梯度法
+        /// </summary>
+        Brenner,
+        /// <summary>
+        /// 灰度方差法
+        /// </summary>
+        GrayVariance,
+        /// <summary>
+        /// 灰度差分法
+        /// </summary>
+        SMD
+    }
+}

+ 96 - 0
TeamAAS-VM/Models/AnalysisParameters.cs

@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using TeamAAS_VP.Enums;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 输入参数
+    /// </summary>
+    public class AnalysisParameters : INotifyPropertyChanged
+    {
+        private FocusMethod _focusMethod = FocusMethod.Tenengrad;
+        private AnalysisMode _analysisMode = AnalysisMode.AutoCheckerboard;
+        private int _targetBrightness = 128;
+        private double _sharpnessThreshold = 10;
+        private double _contrastThreshold = 50;
+        private bool _enableRealtimeAnalysis = true;
+        private bool _autoAdjustBrightness = true;
+
+        public FocusMethod FocusMethod
+        {
+            get => _focusMethod;
+            set => SetField(ref _focusMethod, value);
+        }
+
+        public AnalysisMode AnalysisMode
+        {
+            get => _analysisMode;
+            set => SetField(ref _analysisMode, value);
+        }
+
+        public int TargetBrightness
+        {
+            get => _targetBrightness;
+            set => SetField(ref _targetBrightness, value);
+        }
+
+        public double SharpnessThreshold
+        {
+            get => _sharpnessThreshold;
+            set => SetField(ref _sharpnessThreshold, value);
+        }
+
+        public double ContrastThreshold
+        {
+            get => _contrastThreshold;
+            set => SetField(ref _contrastThreshold, value);
+        }
+
+        public bool EnableRealtimeAnalysis
+        {
+            get => _enableRealtimeAnalysis;
+            set => SetField(ref _enableRealtimeAnalysis, value);
+        }
+
+        public bool AutoAdjustBrightness
+        {
+            get => _autoAdjustBrightness;
+            set => SetField(ref _autoAdjustBrightness, value);
+        }
+
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+
+        protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+        {
+            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
+            field = value;
+            OnPropertyChanged(propertyName);
+            return true;
+        }
+
+        /// <summary>
+        /// 重置为默认值
+        /// </summary>
+        public void ResetToDefaults()
+        {
+            FocusMethod = FocusMethod.Tenengrad;
+            AnalysisMode = AnalysisMode.AutoCheckerboard;
+            TargetBrightness = 128;
+            SharpnessThreshold = 10;
+            ContrastThreshold = 50;
+            EnableRealtimeAnalysis = true;
+            AutoAdjustBrightness = true;
+        }
+    }
+}

+ 69 - 0
TeamAAS-VM/Models/AnalysisReport.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 最终报告
+    /// </summary>
+    public class AnalysisReport
+    {
+        public AnalysisParameters Parameters { get; set; }
+        public AnalysisResult FinalResult { get; set; }
+        public List<DataPoint> HistoryData { get; set; }
+        public DateTime StartTime { get; set; }
+        public DateTime EndTime { get; set; }
+        public TimeSpan Duration => EndTime - StartTime;
+
+        public AnalysisReport()
+        {
+            HistoryData = new List<DataPoint>();
+        }
+
+        public string GenerateTextReport()
+        {
+            return $@"工业相机图像质量分析报告
+生成时间:{EndTime:yyyy-MM-dd HH:mm:ss}
+分析时长:{Duration.TotalSeconds:F1}秒
+
+一、分析参数
+-----------
+清晰度算法:{Parameters.FocusMethod}
+分析模式:{Parameters.AnalysisMode}
+目标亮度:{Parameters.TargetBrightness}
+实时分析:{Parameters.EnableRealtimeAnalysis}
+
+二、最终结果
+-----------
+清晰度:{FinalResult.Sharpness:F2}
+最佳清晰度:{FinalResult.BestSharpness:F2}
+质量评分:{FinalResult.QualityScore:F1}/100
+分析帧数:{FinalResult.FrameCount}
+
+三、棋盘格分析
+-----------
+检测状态:{(FinalResult.Checkerboard.Detected ? "已检测" : "未检测")}
+黑色均值:{FinalResult.Checkerboard.BlackMean:F1}
+白色均值:{FinalResult.Checkerboard.WhiteMean:F1}
+对比度:{FinalResult.Checkerboard.Contrast:F1}
+建议亮度:{FinalResult.Checkerboard.OptimalBrightness}
+
+四、统计信息
+-----------
+总帧数:{HistoryData.Count}
+平均清晰度:{HistoryData.Average(d => d.Sharpness):F2}
+最大清晰度:{HistoryData.Max(d => d.Sharpness):F2}
+最小清晰度:{HistoryData.Min(d => d.Sharpness):F2}
+
+五、调整建议
+-----------
+{string.Join("\n", FinalResult.Suggestions)}
+
+------------------------
+报告结束";
+        }
+    }
+}

+ 82 - 0
TeamAAS-VM/Models/AnalysisResult.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 综合结果
+    /// </summary>
+    public class AnalysisResult : INotifyPropertyChanged
+    {
+        private double _sharpness;
+        private CheckerboardResult _checkerboard = new CheckerboardResult();
+        private double _qualityScore;
+        private int _frameCount;
+        private double _bestSharpness;
+        private DateTime _analysisTime;
+        private ObservableCollection<string> _suggestions = new ObservableCollection<string>();
+
+        public double Sharpness
+        {
+            get => _sharpness;
+            set => SetField(ref _sharpness, value);
+        }
+
+        public CheckerboardResult Checkerboard
+        {
+            get => _checkerboard;
+            set => SetField(ref _checkerboard, value);
+        }
+
+        public double QualityScore
+        {
+            get => _qualityScore;
+            set => SetField(ref _qualityScore, value);
+        }
+
+        public int FrameCount
+        {
+            get => _frameCount;
+            set => SetField(ref _frameCount, value);
+        }
+
+        public double BestSharpness
+        {
+            get => _bestSharpness;
+            set => SetField(ref _bestSharpness, value);
+        }
+
+        public DateTime AnalysisTime
+        {
+            get => _analysisTime;
+            set => SetField(ref _analysisTime, value);
+        }
+
+        public ObservableCollection<string> Suggestions
+        {
+            get => _suggestions;
+            set => SetField(ref _suggestions, value);
+        }
+
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+
+        protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+        {
+            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
+            field = value;
+            OnPropertyChanged(propertyName);
+            return true;
+        }
+    }
+}

+ 67 - 0
TeamAAS-VM/Models/CheckerboardResult.cs

@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 棋盘格分析结果
+    /// </summary>
+    public class CheckerboardResult : INotifyPropertyChanged
+    {
+        private bool _detected;
+        private double _blackMean;
+        private double _whiteMean;
+        private double _contrast;
+        private int _optimalBrightness;
+
+        public bool Detected
+        {
+            get => _detected;
+            set => SetField(ref _detected, value);
+        }
+
+        public double BlackMean
+        {
+            get => _blackMean;
+            set => SetField(ref _blackMean, value);
+        }
+
+        public double WhiteMean
+        {
+            get => _whiteMean;
+            set => SetField(ref _whiteMean, value);
+        }
+
+        public double Contrast
+        {
+            get => _contrast;
+            set => SetField(ref _contrast, value);
+        }
+
+        public int OptimalBrightness
+        {
+            get => _optimalBrightness;
+            set => SetField(ref _optimalBrightness, value);
+        }
+
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+
+        protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+        {
+            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
+            field = value;
+            OnPropertyChanged(propertyName);
+            return true;
+        }
+    }
+}

+ 27 - 0
TeamAAS-VM/Models/DataPoint.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 统计数据点
+    /// </summary>
+    public class DataPoint
+    {
+        public int FrameIndex { get; set; }
+        public double Sharpness { get; set; }
+        public double Contrast { get; set; }
+        public DateTime Timestamp { get; set; }
+
+        public DataPoint(int index, double sharpness, double contrast)
+        {
+            FrameIndex = index;
+            Sharpness = sharpness;
+            Contrast = contrast;
+            Timestamp = DateTime.Now;
+        }
+    }
+}

+ 16 - 2
TeamAAS-VM/TeamAAS-VP.csproj

@@ -510,6 +510,9 @@
     <Compile Include="Controls\FeederSystemParam.xaml.cs">
       <DependentUpon>FeederSystemParam.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Controls\FocusAnalyzerControl.xaml.cs">
+      <DependentUpon>FocusAnalyzerControl.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Controls\myNumericUpDowm.xaml.cs">
       <DependentUpon>myNumericUpDowm.xaml</DependentUpon>
     </Compile>
@@ -546,12 +549,14 @@
     <Compile Include="Core\BgCommunicate.cs" />
     <Compile Include="Core\BgModbusTcpCommunicate.cs" />
     <Compile Include="Core\BraceFoldingStrategy.cs" />
-    <Compile Include="Core\CameraFocusAnalyzer.cs" />
-    <Compile Include="Core\Cameras\CameraBaseLock.cs" />
+    <Compile Include="Core\Cameras\CameraBase.cs" />
+    <Compile Include="Core\FocusAnalysisEngine.cs" />
     <Compile Include="Core\Robots\Axis.cs" />
     <Compile Include="Core\Robots\XYZU_Robot.cs" />
     <Compile Include="Data\DatabaseInitializer.cs" />
     <Compile Include="Data\SystemDatabaseService.cs" />
+    <Compile Include="Enums\AnalysisMode.cs" />
+    <Compile Include="Enums\FocusMethod.cs" />
     <Compile Include="Enums\OutputPointMode.cs" />
     <Compile Include="Interfaces\ICalibrationService.cs" />
     <Compile Include="Interfaces\ICameraService.cs" />
@@ -562,6 +567,11 @@
     <Compile Include="Interfaces\IRemoteCommandService.cs" />
     <Compile Include="Interfaces\ISystemDatabaseService.cs" />
     <Compile Include="Models\AlarmRecord.cs" />
+    <Compile Include="Models\AnalysisParameters.cs" />
+    <Compile Include="Models\AnalysisReport.cs" />
+    <Compile Include="Models\AnalysisResult.cs" />
+    <Compile Include="Models\CheckerboardResult.cs" />
+    <Compile Include="Models\DataPoint.cs" />
     <Compile Include="Models\ProductionRecord.cs" />
     <Compile Include="Models\ProductionStatQuery.cs" />
     <Compile Include="Models\ProductionStatResult.cs" />
@@ -967,6 +977,10 @@
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
+    <Page Include="Controls\FocusAnalyzerControl.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
+    </Page>
     <Page Include="Controls\myNumericUpDowm.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>

+ 6 - 6
TeamAAS-VM/ViewModels/Statistics/ProductionStatementViewModel.cs

@@ -242,7 +242,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
                 barSeries.TrackerFormatString = $"{0}\r\n{2:dd}{Lang.日}: {4:0}";
                 foreach (var item in Items)
                 {
-                    barSeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(item.Key), (double)(item.Value[category])));
+                    barSeries.Points.Add(new OxyPlot.DataPoint(DateTimeAxis.ToDouble(item.Key), (double)(item.Value[category])));
                 }
 
                 model.Series.Add(barSeries);
@@ -259,7 +259,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
                     count += item1.Value;
                 }
 
-                Series.Points.Add(new DataPoint(DateTimeAxis.ToDouble(item.Key), (double)count));
+                Series.Points.Add(new OxyPlot.DataPoint(DateTimeAxis.ToDouble(item.Key), (double)count));
             }
             model.Series.Add(Series);
             return model;
@@ -329,7 +329,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
                 barSeries.TrackerFormatString = $"{0}\r\n{Lang.周}{2:0}: {4:0}";
                 foreach (var item in Items)
                 {
-                    barSeries.Points.Add(new DataPoint(LinearAxis.ToDouble(item.Key), (double)(item.Value[category])));
+                    barSeries.Points.Add(new OxyPlot.DataPoint(LinearAxis.ToDouble(item.Key), (double)(item.Value[category])));
                 }
 
                 model.Series.Add(barSeries);
@@ -346,7 +346,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
                     count += item1.Value;
                 }
 
-                Series.Points.Add(new DataPoint(DateTimeAxis.ToDouble(item.Key), (double)count));
+                Series.Points.Add(new OxyPlot.DataPoint(DateTimeAxis.ToDouble(item.Key), (double)count));
             }
             model.Series.Add(Series);
             return model;
@@ -416,7 +416,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
                 barSeries.TrackerFormatString = "{0}\r\n{2:0}Hours: {4:0}";
                 foreach (var item in Items)
                 {
-                    barSeries.Points.Add(new DataPoint(LinearAxis.ToDouble(item.Key), (double)(item.Value[category])));
+                    barSeries.Points.Add(new OxyPlot.DataPoint(LinearAxis.ToDouble(item.Key), (double)(item.Value[category])));
                 }
 
                 model.Series.Add(barSeries);
@@ -433,7 +433,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
                     count += item1.Value;
                 }
 
-                Series.Points.Add(new DataPoint(DateTimeAxis.ToDouble(item.Key), (double)count));
+                Series.Points.Add(new OxyPlot.DataPoint(DateTimeAxis.ToDouble(item.Key), (double)count));
             }
             model.Series.Add(Series);
             return model;