WPF: 双摄像头拍照程序

环境: VS2022+.Net Framework4.8
NuGet包:OpenCvSharp4 4.13.0.20260627
OpenCvSharp4.Extensions 4.13.0.20260627
OpenCvSharp4.runtime.win 4.13.0.20260627
OpenCvSharp4.Windows 4.13.0.20260627
OpenCvSharp4.WpfExtensions 4.13.0.20260627

程序名:WpfA_Camera

一、功能概述

  • 程序功能包括:初始化摄像头、单次拍照、双摄同时拍照、存盘到指定目录,并显示预览缩略图。


二、坑与解决办法

1、 摄像头打开失败,但设备管理器能看到

  • 索引问题:
    OpenCV 的索引可能与 Windows 的设备顺序不一致。尝试把 0 和 1 交换,或者循环 0~9 找到能打开的索引。

  • 后端问题:
    VideoCaptureAPIs.DSHOW 和 VideoCaptureAPIs.MSMF 兼容性不同,可以切换尝试。

  • 被占用:
    如果其他程序(QQ)占用摄像头,先关闭它们。

2、 拍照得到的不是最新画面

  • 这是 OpenCV 常见问题,摄像头有内部缓冲区,Read() 可能返回旧帧。
    解决办法是在正式 Read() 之前,连续读取并丢弃 3~5 帧。可采用这个策略。

3、 分辨率为默认值

  • Set() 调用顺序必须在 Read() 之前;另外某些摄像头不支持 4000×3000 这类高分辨率,会自动回退到默认值。
    可以先设为 1920x1080 测试,确认后再逐步提高。

4、 保存的图像是全黑

  • 通常是首帧尚未就绪。
    同样通过"丢弃前几帧"解决。如果双摄同时读取时出现黑屏,可以改为串行读取:先读完相机 1,再读相机 2。

5、 提示找不到 OpenCvSharpExtern.dll

  • 检查 bin\Debug\net47\ 或 bin\Release\net47\ 目录下是否存在 OpenCvSharpExtern.dll 和相关的 opencv_videoio_ffmpeg*.dll。如果没有,说明 OpenCvSharp4.runtime.win 没有正确安装或没有复制到输出目录。

三、 程序

1、项目结构

  • WpfA_Camera/
    ├── App.xaml
    ├── App.xaml.cs
    ├── MainWindow.xaml
    └── MainWindow.xaml.cs

2、MainWindow.xaml

<Window x:Class="WpfA_Camera.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfA_Camera"
        Title="双摄像头拍照系统" 
        Height="720" Width="1080"
        WindowStartupLocation="CenterScreen"
        Closing="Window_Closing">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!-- 顶部:路径设置 -->
        <Border Grid.Row="0" BorderBrush="Gray" BorderThickness="1" Padding="8" Margin="0,0,0,8">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Text="保存目录:" VerticalAlignment="Center" Margin="0,0,8,0"/>
                <TextBox Grid.Column="1" x:Name="TxtSaveFolder" 
                         Text="D:\CameraCapture" 
                         VerticalContentAlignment="Center" Height="28"/>
                <Button Grid.Column="2" x:Name="BtnBrowse" Content="浏览..." 
                        Width="80" Height="28" Margin="8,0,8,0" Click="BtnBrowse_Click"/>
                <Button Grid.Column="3" x:Name="BtnOpenFolder" Content="打开目录" 
                        Width="80" Height="28" Click="BtnOpenFolder_Click"/>
            </Grid>
        </Border>

        <!-- 中部:双摄预览 -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Border Grid.Column="0" BorderBrush="Gray" BorderThickness="1" Margin="0,0,4,0">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <TextBlock Grid.Row="0" Text="摄像头 1" FontWeight="Bold" 
                               HorizontalAlignment="Center" Margin="4"/>
                    <Image Grid.Row="1" x:Name="ImgCamera1" 
                           Stretch="Uniform" Margin="4"/>
                </Grid>
            </Border>

            <Border Grid.Column="1" BorderBrush="Gray" BorderThickness="1" Margin="4,0,0,0">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <TextBlock Grid.Row="0" Text="摄像头 2" FontWeight="Bold" 
                               HorizontalAlignment="Center" Margin="4"/>
                    <Image Grid.Row="1" x:Name="ImgCamera2" 
                           Stretch="Uniform" Margin="4"/>
                </Grid>
            </Border>
        </Grid>

        <!-- 状态栏 -->
        <Border Grid.Row="2" BorderBrush="Gray" BorderThickness="1" 
                Padding="8" Margin="0,8,0,8">
            <TextBlock x:Name="TxtStatus" Text="就绪" VerticalAlignment="Center"/>
        </Border>

        <!-- 底部:操作按钮 -->
        <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center">
            <Button x:Name="BtnInit" Content="初始化摄像头" 
                    Width="120" Height="40" Margin="5" Click="BtnInit_Click"/>
            <Button x:Name="BtnCaptureBoth" Content="双摄拍照并保存" 
                    Width="140" Height="40" Margin="5" Click="BtnCaptureBoth_Click" 
                    Background="LightGreen"/>
            <Button x:Name="BtnCapture1" Content="仅拍摄像头1" 
                    Width="120" Height="40" Margin="5" Click="BtnCapture1_Click"/>
            <Button x:Name="BtnCapture2" Content="仅拍摄像头2" 
                    Width="120" Height="40" Margin="5" Click="BtnCapture2_Click"/>
            <Button x:Name="BtnRelease" Content="释放摄像头" 
                    Width="120" Height="40" Margin="5" Click="BtnRelease_Click"/>
        </StackPanel>
    </Grid>
</Window>

3、MainWindow.xaml.cs

using Microsoft.Win32;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
// 使用别名限定 OpenCvSharp 的类型
using Cv2 = OpenCvSharp.Cv2;
using Mat = OpenCvSharp.Mat;
using VideoCapture = OpenCvSharp.VideoCapture;
using VideoCaptureAPIs = OpenCvSharp.VideoCaptureAPIs;
using VideoCaptureProperties = OpenCvSharp.VideoCaptureProperties;


namespace WpfA_Camera
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>

     public partial class MainWindow : System.Windows.Window
    {
        // 两个摄像头对象
        private VideoCapture _camera1;
        private VideoCapture _camera2;

        // 摄像头是否已初始化
        private bool _isInitialized = false;

        // 默认保存路径
        private string _saveFolder = @"D:\CameraCapture";

        // 期望的分辨率(可根据实际摄像头支持情况调整)
        private const int CAM_WIDTH = 1920;
        private const int CAM_HEIGHT = 1080;

        public MainWindow()
        {
            InitializeComponent();
            TxtSaveFolder.Text = _saveFolder;
        }

        #region 初始化 / 释放摄像头

        /// <summary>
        /// 初始化两个摄像头
        /// </summary>
        private void BtnInit_Click(object sender, RoutedEventArgs e)
        {
            InitCameras();
        }

        private void InitCameras()
        {
            try
            {
                // 先释放旧对象
                ReleaseCameras();

                // 使用 DSHOW 后端,稳定性较好
                // 若摄像头索引与预期相反,可将 0/1 对调
                _camera1 = new VideoCapture(0, VideoCaptureAPIs.DSHOW);
                _camera2 = new VideoCapture(1, VideoCaptureAPIs.DSHOW);

                if (!_camera1.IsOpened())
                {
                    SetStatus("摄像头 1 打开失败,请检查设备索引或连接。");
                    _camera1?.Dispose();
                    _camera1 = null;
                    return;
                }

                if (!_camera2.IsOpened())
                {
                    SetStatus("摄像头 2 打开失败,请检查设备索引或连接。");
                    _camera2?.Dispose();
                    _camera2 = null;
                    return;
                }

                // 设置分辨率(必须在读取帧之前设置)
                _camera1.Set(VideoCaptureProperties.FrameWidth, CAM_WIDTH);
                _camera1.Set(VideoCaptureProperties.FrameHeight, CAM_HEIGHT);
                // 尽量减少缓冲区,避免读取到旧帧
                _camera1.Set(VideoCaptureProperties.BufferSize, 1);

                _camera2.Set(VideoCaptureProperties.FrameWidth, CAM_WIDTH);
                _camera2.Set(VideoCaptureProperties.FrameHeight, CAM_HEIGHT);
                _camera2.Set(VideoCaptureProperties.BufferSize, 1);

                _isInitialized = true;

                // 初始化后读取一次,用于显示预览缩略图(丢弃前几帧让参数生效)
                WarmUpAndPreview();

                SetStatus($"摄像头初始化成功。分辨率:{CAM_WIDTH}x{CAM_HEIGHT}");
            }
            catch (Exception ex)
            {
                SetStatus($"摄像头初始化异常:{ex.Message}");
            }
        }

        /// <summary>
        /// 丢弃前几帧,并显示一张预览图
        /// </summary>
        private void WarmUpAndPreview()
        {
            using (Mat frame1 = new Mat())
            using (Mat frame2 = new Mat())
            {
                // 连续读取几帧,丢弃前几帧以让摄像头的曝光/白平衡稳定
                for (int i = 0; i < 5; i++)
                {
                    _camera1?.Read(frame1);
                    _camera2?.Read(frame2);
                }

                if (frame1 != null && !frame1.Empty())
                    ImgCamera1.Source = frame1.ToBitmapSource();

                if (frame2 != null && !frame2.Empty())
                    ImgCamera2.Source = frame2.ToBitmapSource();
            }
        }

        /// <summary>
        /// 释放摄像头资源
        /// </summary>
        private void ReleaseCameras()
        {
            try
            {
                if (_camera1 != null)
                {
                    if (_camera1.IsOpened()) _camera1.Release();
                    _camera1.Dispose();
                    _camera1 = null;
                }

                if (_camera2 != null)
                {
                    if (_camera2.IsOpened()) _camera2.Release();
                    _camera2.Dispose();
                    _camera2 = null;
                }
            }
            catch (Exception ex)
            {
                SetStatus($"释放摄像头时出现异常:{ex.Message}");
            }
            finally
            {
                _isInitialized = false;
            }
        }

        private void BtnRelease_Click(object sender, RoutedEventArgs e)
        {
            ReleaseCameras();
            ImgCamera1.Source = null;
            ImgCamera2.Source = null;
            SetStatus("摄像头已释放。");
        }

        #endregion

        #region 拍照

        /// <summary>
        /// 双摄同时拍照并保存
        /// </summary>
        private void BtnCaptureBoth_Click(object sender, RoutedEventArgs e)
        {
            if (!CheckReady()) return;

            string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
            string folder = EnsureSaveFolder();

            string file1 = Path.Combine(folder, $"cam1_{timestamp}.jpg");
            string file2 = Path.Combine(folder, $"cam2_{timestamp}.jpg");

            bool ok1 = CaptureOne(_camera1, file1, ImgCamera1);
            bool ok2 = CaptureOne(_camera2, file2, ImgCamera2);

            if (ok1 && ok2)
                SetStatus($"双摄拍照成功:\n  {file1}\n  {file2}");
            else
                SetStatus($"拍照部分失败。cam1={ok1}, cam2={ok2}");
        }

        /// <summary>
        /// 仅拍摄像头 1
        /// </summary>
        private void BtnCapture1_Click(object sender, RoutedEventArgs e)
        {
            if (!CheckReady()) return;

            string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
            string file1 = Path.Combine(EnsureSaveFolder(), $"cam1_{timestamp}.jpg");

            if (CaptureOne(_camera1, file1, ImgCamera1))
                SetStatus($"摄像头1 拍照成功:{file1}");
            else
                SetStatus("摄像头1 拍照失败。");
        }

        /// <summary>
        /// 仅拍摄像头 2
        /// </summary>
        private void BtnCapture2_Click(object sender, RoutedEventArgs e)
        {
            if (!CheckReady()) return;

            string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
            string file2 = Path.Combine(EnsureSaveFolder(), $"cam2_{timestamp}.jpg");

            if (CaptureOne(_camera2, file2, ImgCamera2))
                SetStatus($"摄像头2 拍照成功:{file2}");
            else
                SetStatus("摄像头2 拍照失败。");
        }

        /// <summary>
        /// 从指定摄像头抓取一帧并保存,同时更新界面预览
        /// </summary>
        /// <param name="cam">摄像头对象</param>
        /// <param name="filePath">保存路径</param>
        /// <param name="imgControl">界面预览控件</param>
        /// <returns>是否成功</returns>
        private bool CaptureOne(VideoCapture cam, string filePath, System.Windows.Controls.Image imgControl)
        {
            if (cam == null || !cam.IsOpened()) return false;

            try
            {
                // 丢弃前几帧,避免读到旧帧(解决“拍照不是最新画面”的问题)
                using (Mat discard = new Mat())
                {
                    for (int i = 0; i < 3; i++)
                        cam.Read(discard);
                }

                using (Mat frame = new Mat())
                {
                    bool ok = cam.Read(frame);

                    if (!ok || frame.Empty())
                        return false;

                    // 保存到磁盘
                    Cv2.ImWrite(filePath, frame);

                    // 更新界面预览
                    imgControl.Source = frame.ToBitmapSource();

                    return true;
                }
            }
            catch (Exception ex)
            {
                SetStatus($"拍照异常:{ex.Message}");
                return false;
            }
        }

        #endregion

        #region 辅助方法

        /// <summary>
        /// 检查摄像头是否已就绪
        /// </summary>
        private bool CheckReady()
        {
            if (!_isInitialized || _camera1 == null || _camera2 == null)
            {
                MessageBox.Show("请先点击【初始化摄像头】。", "提示",
                    MessageBoxButton.OK, MessageBoxImage.Warning);
                return false;
            }
            return true;
        }

        /// <summary>
        /// 确保保存目录存在
        /// </summary>
        private string EnsureSaveFolder()
        {
            string folder = TxtSaveFolder.Text?.Trim();
            if (string.IsNullOrEmpty(folder))
            {
                folder = _saveFolder;
                TxtSaveFolder.Text = folder;
            }

            if (!Directory.Exists(folder))
                Directory.CreateDirectory(folder);

            return folder;
        }

        /// <summary>
        /// 更新状态栏
        /// </summary>
        private void SetStatus(string text)
        {
            TxtStatus.Text = text;
        }

        /// <summary>
        /// 浏览保存目录
        /// </summary>
        /// <summary>
        /// 浏览保存目录(纯 WPF 实现,不依赖 WinForms)
        /// </summary>
        private void BtnBrowse_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new Microsoft.Win32.OpenFileDialog
            {
                Title = "进入目标文件夹后,点【打开】即可选中该目录",
                CheckFileExists = false,
                CheckPathExists = true,
                FileName = "选择当前文件夹",
                Filter = "所有文件 (*.*)|*.*",
                InitialDirectory = TxtSaveFolder.Text
            };

            if (dialog.ShowDialog() == true)
            {
                string folder = System.IO.Path.GetDirectoryName(dialog.FileName);
                if (!string.IsNullOrEmpty(folder))
                {
                    TxtSaveFolder.Text = folder;
                }
            }
        }

        /// <summary>
        /// 在资源管理器中打开保存目录
        /// </summary>
        private void BtnOpenFolder_Click(object sender, RoutedEventArgs e)
        {
            string folder = EnsureSaveFolder();
            try
            {
                Process.Start("explorer.exe", folder);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"打开目录失败:{ex.Message}");
            }
        }

        /// <summary>
        /// 窗口关闭时释放资源
        /// </summary>
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            ReleaseCameras();
        }

        #endregion
    }
}

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容