环境: 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
}
}
