2025-09-13 13:59:54 +08:00
|
|
|
using AForge.Video;
|
|
|
|
|
using AForge.Video.DirectShow;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Drawing;
|
|
|
|
|
using System.Drawing.Imaging;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Windows.Media.Imaging;
|
|
|
|
|
using System.Windows.Threading;
|
|
|
|
|
|
|
|
|
|
namespace Ink_Canvas.Helpers
|
|
|
|
|
{
|
|
|
|
|
public class CameraService : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private VideoCaptureDevice _videoSource;
|
|
|
|
|
private bool _isCapturing;
|
|
|
|
|
private Bitmap _currentFrame;
|
|
|
|
|
private readonly object _frameLock = new object();
|
|
|
|
|
private Dispatcher _dispatcher;
|
|
|
|
|
|
2025-10-02 02:11:54 +08:00
|
|
|
// 新增属性
|
|
|
|
|
private int _rotationAngle = 0; // 0=0度,1=90度,2=180度,3=270度
|
|
|
|
|
private int _resolutionWidth = 640;
|
|
|
|
|
private int _resolutionHeight = 480;
|
|
|
|
|
|
2025-09-13 13:59:54 +08:00
|
|
|
public event EventHandler<Bitmap> FrameReceived;
|
|
|
|
|
public event EventHandler<string> ErrorOccurred;
|
|
|
|
|
|
|
|
|
|
public bool IsCapturing => _isCapturing;
|
|
|
|
|
public List<FilterInfo> AvailableCameras { get; private set; }
|
|
|
|
|
public FilterInfo CurrentCamera { get; private set; }
|
|
|
|
|
|
2025-10-02 02:11:54 +08:00
|
|
|
// 新增属性
|
2025-10-03 17:08:46 +08:00
|
|
|
public int RotationAngle
|
|
|
|
|
{
|
|
|
|
|
get => _rotationAngle;
|
|
|
|
|
set => _rotationAngle = Math.Max(0, Math.Min(3, value));
|
2025-10-02 02:11:54 +08:00
|
|
|
}
|
2025-10-03 17:08:46 +08:00
|
|
|
|
|
|
|
|
public int ResolutionWidth
|
|
|
|
|
{
|
|
|
|
|
get => _resolutionWidth;
|
2026-02-23 13:40:19 +08:00
|
|
|
set => _resolutionWidth = Math.Max(320, Math.Min(3840, value));
|
2025-10-02 02:11:54 +08:00
|
|
|
}
|
2025-10-03 17:08:46 +08:00
|
|
|
|
|
|
|
|
public int ResolutionHeight
|
|
|
|
|
{
|
|
|
|
|
get => _resolutionHeight;
|
2026-02-23 13:40:19 +08:00
|
|
|
set => _resolutionHeight = Math.Max(240, Math.Min(2160, value));
|
2025-10-02 02:11:54 +08:00
|
|
|
}
|
|
|
|
|
|
2025-09-13 13:59:54 +08:00
|
|
|
public CameraService()
|
|
|
|
|
{
|
|
|
|
|
_dispatcher = Dispatcher.CurrentDispatcher;
|
|
|
|
|
AvailableCameras = new List<FilterInfo>();
|
|
|
|
|
RefreshCameraList();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 02:11:54 +08:00
|
|
|
public CameraService(int rotationAngle, int resolutionWidth, int resolutionHeight)
|
|
|
|
|
{
|
|
|
|
|
_dispatcher = Dispatcher.CurrentDispatcher;
|
|
|
|
|
AvailableCameras = new List<FilterInfo>();
|
|
|
|
|
_rotationAngle = rotationAngle;
|
|
|
|
|
_resolutionWidth = resolutionWidth;
|
|
|
|
|
_resolutionHeight = resolutionHeight;
|
|
|
|
|
RefreshCameraList();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 13:59:54 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 刷新可用摄像头列表
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void RefreshCameraList()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
AvailableCameras.Clear();
|
|
|
|
|
var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
|
2025-10-03 17:08:46 +08:00
|
|
|
|
2025-09-13 13:59:54 +08:00
|
|
|
foreach (FilterInfo device in videoDevices)
|
|
|
|
|
{
|
|
|
|
|
AvailableCameras.Add(device);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.WriteLogToFile($"刷新摄像头列表失败: {ex.Message}", LogHelper.LogType.Error);
|
|
|
|
|
ErrorOccurred?.Invoke(this, $"刷新摄像头列表失败: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 开始摄像头预览
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="cameraIndex">摄像头索引</param>
|
|
|
|
|
public bool StartPreview(int cameraIndex = 0)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (AvailableCameras.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
RefreshCameraList();
|
|
|
|
|
if (AvailableCameras.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
ErrorOccurred?.Invoke(this, "未找到可用的摄像头设备");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
|
|
|
|
|
{
|
|
|
|
|
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 停止当前预览
|
|
|
|
|
StopPreview();
|
|
|
|
|
|
|
|
|
|
CurrentCamera = AvailableCameras[cameraIndex];
|
|
|
|
|
_videoSource = new VideoCaptureDevice(CurrentCamera.MonikerString);
|
|
|
|
|
|
|
|
|
|
// 设置视频源事件处理
|
|
|
|
|
_videoSource.NewFrame += VideoSource_NewFrame;
|
|
|
|
|
|
|
|
|
|
// 启动视频源
|
|
|
|
|
_videoSource.Start();
|
|
|
|
|
|
|
|
|
|
_isCapturing = true;
|
|
|
|
|
LogHelper.WriteLogToFile($"开始摄像头预览: {CurrentCamera.Name}");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.WriteLogToFile($"启动摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
|
|
|
|
|
ErrorOccurred?.Invoke(this, $"启动摄像头预览失败: {ex.Message}");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 停止摄像头预览
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void StopPreview()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (_videoSource != null && _videoSource.IsRunning)
|
|
|
|
|
{
|
|
|
|
|
_videoSource.SignalToStop();
|
|
|
|
|
_videoSource.WaitForStop();
|
|
|
|
|
_videoSource.NewFrame -= VideoSource_NewFrame;
|
|
|
|
|
_videoSource = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_isCapturing = false;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.WriteLogToFile($"停止摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 切换到指定摄像头
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="cameraIndex">摄像头索引</param>
|
|
|
|
|
public bool SwitchCamera(int cameraIndex)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
|
|
|
|
|
{
|
|
|
|
|
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return StartPreview(cameraIndex);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.WriteLogToFile($"切换摄像头失败: {ex.Message}", LogHelper.LogType.Error);
|
|
|
|
|
ErrorOccurred?.Invoke(this, $"切换摄像头失败: {ex.Message}");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-09-13 14:16:11 +08:00
|
|
|
/// 获取当前帧的BitmapSource(WPF格式),直接返回可用的WPF位图
|
2025-09-13 13:59:54 +08:00
|
|
|
/// </summary>
|
|
|
|
|
public BitmapSource GetCurrentFrameAsBitmapSource()
|
|
|
|
|
{
|
|
|
|
|
lock (_frameLock)
|
|
|
|
|
{
|
|
|
|
|
if (_currentFrame == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-09-13 14:16:11 +08:00
|
|
|
// 验证位图有效性
|
2025-09-13 14:21:07 +08:00
|
|
|
if (_currentFrame.Width <= 0 || _currentFrame.Height <= 0)
|
2025-09-13 14:16:11 +08:00
|
|
|
return null;
|
2025-09-13 13:59:54 +08:00
|
|
|
|
2025-09-13 14:16:11 +08:00
|
|
|
// 使用更安全的方法转换位图
|
|
|
|
|
var bitmapData = _currentFrame.LockBits(
|
2025-09-13 14:21:07 +08:00
|
|
|
new Rectangle(0, 0, _currentFrame.Width, _currentFrame.Height),
|
2025-09-13 14:16:11 +08:00
|
|
|
ImageLockMode.ReadOnly,
|
|
|
|
|
_currentFrame.PixelFormat);
|
2025-09-13 13:59:54 +08:00
|
|
|
|
2025-09-13 14:16:11 +08:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// 根据像素格式选择合适的WPF像素格式
|
|
|
|
|
System.Windows.Media.PixelFormat wpfPixelFormat;
|
|
|
|
|
switch (_currentFrame.PixelFormat)
|
|
|
|
|
{
|
|
|
|
|
case PixelFormat.Format24bppRgb:
|
|
|
|
|
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
|
|
|
|
|
break;
|
|
|
|
|
case PixelFormat.Format32bppArgb:
|
|
|
|
|
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgra32;
|
|
|
|
|
break;
|
|
|
|
|
case PixelFormat.Format32bppRgb:
|
|
|
|
|
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr32;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 21:38:03 +08:00
|
|
|
var bitmapSource = BitmapSource.Create(
|
2025-09-13 14:16:11 +08:00
|
|
|
bitmapData.Width,
|
|
|
|
|
bitmapData.Height,
|
|
|
|
|
_currentFrame.HorizontalResolution,
|
|
|
|
|
_currentFrame.VerticalResolution,
|
|
|
|
|
wpfPixelFormat,
|
|
|
|
|
null,
|
|
|
|
|
bitmapData.Scan0,
|
|
|
|
|
bitmapData.Stride * bitmapData.Height,
|
|
|
|
|
bitmapData.Stride);
|
|
|
|
|
|
|
|
|
|
bitmapSource.Freeze();
|
|
|
|
|
return bitmapSource;
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_currentFrame.UnlockBits(bitmapData);
|
2025-09-13 13:59:54 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.WriteLogToFile($"转换帧为BitmapSource失败: {ex.Message}", LogHelper.LogType.Error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 14:16:11 +08:00
|
|
|
|
2025-09-13 13:59:54 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 视频源新帧事件处理
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
lock (_frameLock)
|
|
|
|
|
{
|
|
|
|
|
// 释放之前的帧
|
|
|
|
|
_currentFrame?.Dispose();
|
2025-10-03 17:08:46 +08:00
|
|
|
|
2025-09-13 14:16:11 +08:00
|
|
|
// 创建新的位图,避免Clone的问题
|
|
|
|
|
var sourceFrame = eventArgs.Frame;
|
2025-10-03 17:08:46 +08:00
|
|
|
|
2025-09-13 14:16:11 +08:00
|
|
|
if (sourceFrame != null)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var width = sourceFrame.Width;
|
|
|
|
|
var height = sourceFrame.Height;
|
2025-10-03 17:08:46 +08:00
|
|
|
|
|
|
|
|
if (width > 0 && height > 0)
|
|
|
|
|
{
|
|
|
|
|
// 应用旋转
|
|
|
|
|
Bitmap rotatedFrame = ApplyRotation(sourceFrame);
|
|
|
|
|
|
2025-11-15 20:48:04 +08:00
|
|
|
int targetWidth = _resolutionWidth;
|
|
|
|
|
int targetHeight = _resolutionHeight;
|
2025-12-20 13:56:46 +08:00
|
|
|
|
|
|
|
|
if (_rotationAngle == 1 || _rotationAngle == 3)
|
2025-11-15 20:48:04 +08:00
|
|
|
{
|
|
|
|
|
targetWidth = _resolutionHeight;
|
|
|
|
|
targetHeight = _resolutionWidth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight);
|
2025-10-03 17:08:46 +08:00
|
|
|
|
|
|
|
|
rotatedFrame?.Dispose();
|
|
|
|
|
}
|
2025-09-13 14:16:11 +08:00
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_currentFrame = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception frameEx)
|
|
|
|
|
{
|
2025-09-13 14:21:07 +08:00
|
|
|
LogHelper.WriteLogToFile($"处理源帧失败: {frameEx.Message}", LogHelper.LogType.Error);
|
2025-09-13 14:16:11 +08:00
|
|
|
_currentFrame = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_currentFrame = null;
|
|
|
|
|
}
|
2025-09-13 13:59:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在UI线程中触发事件
|
|
|
|
|
_dispatcher.BeginInvoke(new Action(() =>
|
|
|
|
|
{
|
|
|
|
|
FrameReceived?.Invoke(this, _currentFrame);
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.WriteLogToFile($"处理新帧失败: {ex.Message}", LogHelper.LogType.Error);
|
|
|
|
|
ErrorOccurred?.Invoke(this, $"处理新帧失败: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 获取摄像头名称列表
|
|
|
|
|
/// </summary>
|
|
|
|
|
public List<string> GetCameraNames()
|
|
|
|
|
{
|
|
|
|
|
return AvailableCameras.Select(camera => camera.Name).ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 检查是否有可用摄像头
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool HasAvailableCameras()
|
|
|
|
|
{
|
|
|
|
|
if (AvailableCameras.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
RefreshCameraList();
|
|
|
|
|
}
|
|
|
|
|
return AvailableCameras.Count > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 02:11:54 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 应用旋转到图像
|
|
|
|
|
/// </summary>
|
|
|
|
|
private Bitmap ApplyRotation(Bitmap source)
|
|
|
|
|
{
|
|
|
|
|
if (_rotationAngle == 0)
|
|
|
|
|
return new Bitmap(source);
|
|
|
|
|
|
|
|
|
|
var rotationType = RotateFlipType.RotateNoneFlipNone;
|
|
|
|
|
switch (_rotationAngle)
|
|
|
|
|
{
|
|
|
|
|
case 1: rotationType = RotateFlipType.Rotate90FlipNone; break;
|
|
|
|
|
case 2: rotationType = RotateFlipType.Rotate180FlipNone; break;
|
|
|
|
|
case 3: rotationType = RotateFlipType.Rotate270FlipNone; break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rotated = new Bitmap(source);
|
|
|
|
|
rotated.RotateFlip(rotationType);
|
|
|
|
|
return rotated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-15 20:48:04 +08:00
|
|
|
/// 调整图像大小
|
|
|
|
|
/// </summary>
|
|
|
|
|
private Bitmap ResizeImageWithAspectRatio(Bitmap source, int targetWidth, int targetHeight)
|
|
|
|
|
{
|
|
|
|
|
if (source.Width == targetWidth && source.Height == targetHeight)
|
|
|
|
|
return new Bitmap(source);
|
|
|
|
|
|
|
|
|
|
double scaleX = (double)targetWidth / source.Width;
|
|
|
|
|
double scaleY = (double)targetHeight / source.Height;
|
|
|
|
|
double scale = Math.Min(scaleX, scaleY);
|
|
|
|
|
|
|
|
|
|
// 计算实际尺寸
|
|
|
|
|
int actualWidth = (int)(source.Width * scale);
|
|
|
|
|
int actualHeight = (int)(source.Height * scale);
|
|
|
|
|
|
|
|
|
|
var resized = new Bitmap(actualWidth, actualHeight, PixelFormat.Format24bppRgb);
|
|
|
|
|
using (var graphics = Graphics.FromImage(resized))
|
|
|
|
|
{
|
|
|
|
|
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
|
|
|
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
|
|
|
|
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
|
|
|
|
|
graphics.DrawImage(source, 0, 0, actualWidth, actualHeight);
|
|
|
|
|
}
|
|
|
|
|
return resized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-02 02:11:54 +08:00
|
|
|
/// 调整图像大小
|
|
|
|
|
/// </summary>
|
|
|
|
|
private Bitmap ResizeImage(Bitmap source, int width, int height)
|
|
|
|
|
{
|
|
|
|
|
if (source.Width == width && source.Height == height)
|
|
|
|
|
return new Bitmap(source);
|
|
|
|
|
|
|
|
|
|
var resized = new Bitmap(width, height, PixelFormat.Format24bppRgb);
|
|
|
|
|
using (var graphics = Graphics.FromImage(resized))
|
|
|
|
|
{
|
|
|
|
|
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
|
|
|
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
|
|
|
|
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
|
|
|
|
|
graphics.DrawImage(source, 0, 0, width, height);
|
|
|
|
|
}
|
|
|
|
|
return resized;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 13:59:54 +08:00
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
StopPreview();
|
2025-10-03 17:08:46 +08:00
|
|
|
|
2025-09-13 13:59:54 +08:00
|
|
|
lock (_frameLock)
|
|
|
|
|
{
|
|
|
|
|
_currentFrame?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|