会议白板之摄像头采集
本文经原作者授权以原创方式二次分享,欢迎转载、分享。
原文作者:唐宋元明清
原文地址:https://www.cnblogs.com/kybs0/p/18385599
会议白板之摄像头采集
本文主要介绍摄像头(相机)如何采集数据,用于类似摄像头本地显示软件,以及流媒体数据传输场景如传屏、视讯会议等。
摄像头采集有多种方案,如AForge.NET
、WPFMediaKit
、OpenCvSharp
、EmguCv
、DirectShow.NET
、MediaCaptre(UWP)
,网上一些文章以及github
已经有很多介绍,这里总结、确认技术选型给大家一个参考。
1. AForge.NET:
AForge
视频库是基于DirectShow
技术开发的,提供了捕捉、处理和显示视频流接口,以及图像丰富的图像处理功能,如滤镜、特征提取和物体检测。详见官网开源仓库 andrewkirillov/AForge.NET(github.com)[1]
我们下面看下AForge
录制代码,安装Nuget
包依赖:
<PackageReference Include="AForge.Video" Version="2.2.5" />
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
<PackageReference Include="System.Drawing.Common" Version="8.0.8" />
摄像头图像显示:
private void StartButton_OnClick(object sender, RoutedEventArgs e)
{
// 获取所有视频输入设备
var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
if (videoDevices.Count > 0)
{
// 选择第一个视频输入设备
var videoSource = new VideoCaptureDevice(videoDevices[0].MonikerString);
// 注册NewFrame事件处理程序
videoSource.NewFrame += new NewFrameEventHandler(videoSource_NewFrame);
// 开始摄像头视频源
videoSource.Start();
}
}
private async void videoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
// 获取当前的视频帧,显示
var image = ToBitmapImage(eventArgs.Frame);
await Dispatcher.InvokeAsync(() => { CaptureImage.Source = image; });
}
摄像头录制视频流:
private async void videoSource_NewFrame1(object sender, NewFrameEventArgs eventArgs)
{
// 获取当前的视频帧
// 将Bitmap转换为byte[],用于流媒体传输
byte[] byteArray = BitmapToByteArray(eventArgs.Frame, out int stride);
// 将byte[]转换为BitmapImage,用于临时展示
BitmapImage image = ByteArrayToBitmapImage(byteArray, eventArgs.Frame.Width, eventArgs.Frame.Height, stride, eventArgs.Frame.PixelFormat);
await Dispatcher.InvokeAsync(() => { CaptureImage.Source = image; });
}
其中的数据转换,这里要把分辨率stride
同byte[]
数据一同储存,不然后续数据是无法处理的:
// 将Bitmap转换为byte[]
public byte[] BitmapToByteArray(Bitmap bitmap, out int stride)
{
Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
BitmapData bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, bitmap.PixelFormat);
//stride是分辨率水平值,如3840
stride = bitmapData.Stride;
int bytes = Math.Abs(bitmapData.Stride) * bitmap.Height;
byte[] rgbValues = new byte[bytes];
// 复制位图数据到字节数组
Marshal.Copy(bitmapData.Scan0, rgbValues, 0, bytes);
bitmap.UnlockBits(bitmapData);
return rgbValues;
}
// 将byte[]转换为BitmapImage
public BitmapImage ByteArrayToBitmapImage(byte[] byteArray, int width, int height, int stride, PixelFormat pixelFormat)
{
var bitmapImage = new BitmapImage();
using (var memoryStream = new MemoryStream())
{
var bmp = new Bitmap(width, height, stride, pixelFormat, Marshal.UnsafeAddrOfPinnedArrayElement(byteArray, 0));
// 保存到MemoryStream中
bmp.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Bmp);
memoryStream.Seek(0, SeekOrigin.Begin);
bitmapImage.BeginInit();
bitmapImage.StreamSource = memoryStream;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
}
return bitmapImage;
}
详见Demo
代码 kybs00/AForgeNETDemo (github.com)[2]
经验证,延迟较大,对比Windows
系统相机不够清晰
测试延时,大家可以用摄像头对着屏幕,屏幕显示摄像头画面以及在线秒表计时器。在线推荐大家用这个在线秒表 - 在线计时器 - 在线记时器 (bmcx.com)[3] 或者 在线秒表计时器 (lddgo.net)[4],放大页面比例会清晰点
2. WPFMediaKit
WPFMediaKit
也是基于DirectShow
的,它提供了一些封装便于在WPF
应用中使用媒体功能。Sascha-L/WPF-MediaKit (github.com)[5]
使用 WPFMediaKit
要录制摄像头视频,需要结合 WPFMediaKit
提供的视频捕获功能和其他库(例如 AForge
或 FFmpeg
)来实现录制功能。
这里引用WPFMediaKit
、AForge.Video.FFMPEG
俩个Nuget
包,然后通过定时器捕获当前视频帧:
var bitmap = new Bitmap(videoCaptureElement.Width, videoCaptureElement.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),System.Drawing.Imaging.ImageLockMode.WriteOnly,bitmap.PixelFormat);
try
{
videoCaptureElement.VideoCaptureDevice.GetCurrentVideoFrame(out IntPtr frame);
System.Runtime.InteropServices.Marshal.Copy(frame, 0, bitmapData.Scan0, videoCaptureElement.Width * videoCaptureElement.Height * 3);
}
finally
{
bitmap.UnlockBits(bitmapData);
}
这个定时器的实现比较low
。延时较低、较流畅,但与Win
系统相机对比也是不够清晰
3.MediaCapture(UWP)
MediaCapture
是Windows 8
及以上版本的WinRT API
,专为捕获音频、视频和照片设计。
MediaCaptuer
是UWP
应用的API
使用 MediaCapture
捕获基本的照片、视频和音频 - UWP applications
| Microsoft Learn
,要在WPF
内使用需要引入俩个Nuget
包:
<PackageReference Include="Microsoft.Toolkit.Wpf.UI.XamlHost" Version="6.1.2" />
<PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.26100.1" />
初始化MediaCapture
:
var mediaCapture =new MediaCapture();
var videos = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
var settings = new MediaCaptureInitializationSettings()
{
VideoDeviceId = videos[0].Id,
StreamingCaptureMode = StreamingCaptureMode.Video,
};
await mediaCapture.InitializeAsync(settings);
分几个场景,分别输出Demo
。录制本地文件,注释已经很详细了并不重复解释,直接看代码:
private MediaCapture _mediaCapture;
private InMemoryRandomAccessStream _randomAccessStream;
private async void StartButton_OnClick(object sender, RoutedEventArgs e)
{
// 1. 初始化 MediaCapture 对象
var mediaCapture = _mediaCapture = new MediaCapture();
var videos = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
var settings = new MediaCaptureInitializationSettings()
{
VideoDeviceId = videos[0].Id,
StreamingCaptureMode = StreamingCaptureMode.Video,
};
await mediaCapture.InitializeAsync(settings);
// 2. 设置要录制的数据流
var randomAccessStream = _randomAccessStream = new InMemoryRandomAccessStream();
// 3. 配置录制的视频设置
var mediaEncodingProfile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.Auto);
// 4. 开始录制
await mediaCapture.StartRecordToStreamAsync(mediaEncodingProfile, randomAccessStream);
}
private async void StopButton_OnClick(object sender, RoutedEventArgs e)
{
// 停止录制
await _mediaCapture.StopRecordAsync();
// 处理录制后的数据,保存至"C:\Users\XXX\Videos\RecordedVideo.mp4"
var storageFolder = Windows.Storage.KnownFolders.VideosLibrary;
var file = await storageFolder.CreateFileAsync("RecordedVideo.mp4", Windows.Storage.CreationCollisionOption.GenerateUniqueName);
using var fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
await RandomAccessStream.CopyAndCloseAsync(_randomAccessStream.GetInputStreamAt(0), fileStream.GetOutputStreamAt(0));
_randomAccessStream.Dispose();
}
摄像头显示,通过UWP-WindowsXamlHost
承载画面(置顶):
private async void StartButton_OnClick(object sender, RoutedEventArgs e)
{
_mediaCapture = new MediaCapture();
var videos = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
var settings = new MediaCaptureInitializationSettings()
{
VideoDeviceId = videos[0].Id,
StreamingCaptureMode = StreamingCaptureMode.Video,
};
await _mediaCapture.InitializeAsync(settings);
//显示WindowsXamlHost
VideoViewHost.Visibility = Visibility.Visible;
//绑定画面源
_captureElement.Source = _mediaCapture;
await _mediaCapture.StartPreviewAsync();
}
MediaCapture
是UWP
平台的实现方案,直接给CaptureElement
赋值绑定画面源。直接用CaptureElement
渲染速度很快,这个实现逻辑同windows
系统相机是一样的
另外,使用MediaCapture
也可以捕获画面帧事件,用于流媒体数据捕获收集:
// 配置视频帧读取器
var frameSource = mediaCapture.FrameSources.Values.FirstOrDefault(source => source.Info.MediaStreamType == MediaStreamType.VideoRecord);
_frameReader = await mediaCapture.CreateFrameReaderAsync(frameSource, MediaEncodingSubtypes.Argb32);
_frameReader.FrameArrived += FrameReader_FrameArrived;
await _frameReader.StartAsync();
如下方所示,监听FrameArrived
,使用Windows.UI.Xaml.Media.Imaging.BitmapImage
渲染展示(仅用于展示,延迟很高):
private async void FrameReader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
var frame = sender.TryAcquireLatestFrame();
if (frame != null)
{
var bitmap = frame.VideoMediaFrame?.SoftwareBitmap;
if (bitmap != null)
{
// 在这里对每一帧进行处理
await Dispatcher.InvokeAsync(async () =>
{
var bitmapImage = await ConvertSoftwareBitmapToBitmapImageAsync(bitmap);
_captureImage.Source = bitmapImage;
});
}
}
}
如需要将SoftwareBitmap
转为buffer
字节数据,可以按如下处理:
public async Task<byte[]> SoftwareBitmapToByteArrayAsync(SoftwareBitmap softwareBitmap)
{
// 使用InMemoryRandomAccessStream来存储图像数据
using var stream = new InMemoryRandomAccessStream();
// 创建位图编码器
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
// 转换为BGRA8格式,如果当前格式不同
var bitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
encoder.SetSoftwareBitmap(bitmap);
await encoder.FlushAsync();
bitmap.Dispose();
// 读取字节数据
using var reader = new DataReader(stream.GetInputStreamAt(0));
byte[] byteArray = new byte[stream.Size];
await reader.LoadAsync((uint)stream.Size);
reader.ReadBytes(byteArray);
return byteArray;
}
以上,MediaCapture
能实现摄像头显示及录制相关功能。MediaCapture
代码Demo
详见Github:kybs00/CameraCaptureDemo: 摄像头预览、捕获DEMO (github.com)[6]
这是我个人电脑本地监听帧转Image
显示的延迟,差不多180ms
:
3.其它
OpenCvSharp
是OpenCV
在C#
环境中的包装,提供跨平台的计算机视觉和图像处理功能 shimat/opencvsharp: OpenCV wrapper for .NET (github.com)[7]。2K
视频较为流畅,4K
视频延迟较低但显示效果较差
其它的如EmguCV
是另一个基于OpenCV
的C#
包装组件库,具有与OpenCVSharp
相同的强大功能 emgucv/emgucv: Emgu CV (github.com)[8]
DirectShow.NET
,提供对DirectShow API
的托管包装,使得在.NET
框架中可以直接使用DirectShow
的强大功能来进行视频捕获和处理 pauldotknopf/DirectShow.NET[9]。DirectShow
本身性能较好,但DirectShow.NET
作为托管包装,性能会受一定影响。延迟效果待验证
此处就不一一例写Demo
了
我们看看性能数据,4K
屏设备+4K
摄像头,通过本地摄像头预览显示。对各方案的延时统计数据:
验证了下MediaCaptre
的延时与系统相机差不多。通过任务管理器我们可以看到,系统相机CPU
占用3%
,但GPU
是15%
。
使用了硬件加速,性能方面很不错,所以摄像头采集推荐MediaCaptre
方案
值得一提的是,公司大屏HDMI
采集卡信号Hdmi Record
即6911龙讯
固件,采用MediaCapture
采集画面会稳定很多,减少了黑屏、粉屏的概率。从这点也说明Windows
系统相机的原生实现方案,兼容性更好
另外,这里验证的方案都是针对4K
摄像头,如果是8K
摄像头,其性能要求更高了,后面单独介绍
andrewkirillov/AForge.NET(github.com): https://github.com/andrewkirillov/AForge.NET
[2]kybs00/AForgeNETDemo (github.com): https://github.com/kybs00/AForgeNETDemo
[3]在线秒表 - 在线计时器 - 在线记时器 (bmcx.com): https://miaobiao.bmcx.com/miaobiao/?ivk_sa=1024320u
[4]在线秒表计时器 (lddgo.net): https://www.lddgo.net/common/stopwatch#:~:text=%E5%9C%A8%E7%BA%BF%E8%A1%A8%E7%A7%92%E8%AE%A1%E6%97%B6
[5]Sascha-L/WPF-MediaKit (github.com): https://github.com/Sascha-L/WPF-MediaKit
[6]kybs00/CameraCaptureDemo: 摄像头预览、捕获DEMO (github.com): https://github.com/kybs00/CameraCaptureDemo
[7]shimat/opencvsharp: OpenCV wrapper for .NET (github.com): https://github.com/shimat/opencvsharp
[8]emgucv/emgucv: Emgu CV (github.com): https://github.com/emgucv/emgucv
[9]pauldotknopf/DirectShow.NET: https://github.com/pauldotknopf/DirectShow.NET