.NET 攝像頭採集

唐宋元明清2188發表於2024-08-29

本文主要介紹攝像頭(相機)如何採集資料,用於類似攝像頭本地顯示軟體,以及流媒體資料傳輸場景如傳屏、視訊會議等。

攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及github已經有很多介紹,這裡總結、確認技術選型給大家一個參考

1. AForge.NET

AForge影片庫是基於DirectShow技術開發的,提供了捕捉、處理和顯示影片流介面,以及影像豐富的影像處理功能,如濾鏡、特徵提取和物體檢測。詳見官網開源倉庫 andrewkirillov/AForge.NET(github.com)

我們下面看下AForge錄製程式碼,安裝Nuget包依賴:

1     <PackageReference Include="AForge.Video" Version="2.2.5" />
2     <PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
3     <PackageReference Include="System.Drawing.Common" Version="8.0.8" />

攝像頭顯示:

 1     private void StartButton_OnClick(object sender, RoutedEventArgs e)
 2     {
 3         // 獲取所有影片輸入裝置
 4         var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
 5         if (videoDevices.Count > 0)
 6         {
 7             // 選擇第一個影片輸入裝置
 8             var videoSource = new VideoCaptureDevice(videoDevices[0].MonikerString);
 9             // 註冊NewFrame事件處理程式
10             videoSource.NewFrame += new NewFrameEventHandler(videoSource_NewFrame);
11             // 開始攝像頭影片源
12             videoSource.Start();
13         }
14     }
15 
16     private async void videoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
17     {
18         // 獲取當前的影片幀,顯示
19         var image = ToBitmapImage(eventArgs.Frame);
20         await Dispatcher.InvokeAsync(() => { CaptureImage.Source = image; });
21     }

攝像頭錄製影片流:

 1     private async void videoSource_NewFrame1(object sender, NewFrameEventArgs eventArgs)
 2     {
 3         // 獲取當前的影片幀
 4         // 將Bitmap轉換為byte[],用於流媒體傳輸
 5         byte[] byteArray = BitmapToByteArray(eventArgs.Frame, out int stride);
 6         // 將byte[]轉換為BitmapImage,用於臨時展示
 7         BitmapImage image = ByteArrayToBitmapImage(byteArray, eventArgs.Frame.Width, eventArgs.Frame.Height, stride, eventArgs.Frame.PixelFormat);
 8         await Dispatcher.InvokeAsync(() => { CaptureImage.Source = image; });
 9     }

其中的資料轉換,這裡要把解析度stride同byte[]資料一同儲存,不然後續資料是無法處理的:

 1     // 將Bitmap轉換為byte[]
 2     public byte[] BitmapToByteArray(Bitmap bitmap, out int stride)
 3     {
 4         Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
 5         BitmapData bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, bitmap.PixelFormat);
 6         //stride是解析度水平值,如3840
 7         stride = bitmapData.Stride;
 8         int bytes = Math.Abs(bitmapData.Stride) * bitmap.Height;
 9         byte[] rgbValues = new byte[bytes];
10 
11         // 複製點陣圖資料到位元組陣列
12         Marshal.Copy(bitmapData.Scan0, rgbValues, 0, bytes);
13 
14         bitmap.UnlockBits(bitmapData);
15         return rgbValues;
16     }
17 
18     // 將byte[]轉換為BitmapImage
19     public BitmapImage ByteArrayToBitmapImage(byte[] byteArray, int width, int height, int stride, PixelFormat pixelFormat)
20     {
21         var bitmapImage = new BitmapImage();
22         using (var memoryStream = new MemoryStream())
23         {
24             var bmp = new Bitmap(width, height, stride, pixelFormat, Marshal.UnsafeAddrOfPinnedArrayElement(byteArray, 0));
25             // 儲存到MemoryStream中
26             bmp.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Bmp);
27             memoryStream.Seek(0, SeekOrigin.Begin);
28             bitmapImage.BeginInit();
29             bitmapImage.StreamSource = memoryStream;
30             bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
31             bitmapImage.EndInit();
32             bitmapImage.Freeze();
33         }
34         return bitmapImage;
35     }

詳見Demo程式碼 kybs00/AForgeNETDemo (github.com)

經驗證,延遲較大,對比Windows系統相機不夠清晰

2. WPFMediaKit

WPFMediaKit也是基於DirectShow的,它提供了一些封裝便於在WPF應用中使用媒體功能。Sascha-L/WPF-MediaKit (github.com)

使用 WPFMediaKit 要錄製攝像頭影片,需要結合 WPFMediaKit 提供的影片捕獲功能和其他庫(例如 AForge 或 FFmpeg)來實現錄製功能。

這裡引用WPFMediaKit、AForge.Video.FFMPEG倆個Nuget包,然後透過定時器捕獲當前影片幀:

 1     var bitmap = new Bitmap(videoCaptureElement.Width, videoCaptureElement.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
 2     var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),System.Drawing.Imaging.ImageLockMode.WriteOnly,bitmap.PixelFormat);
 3     try
 4     {
 5         videoCaptureElement.VideoCaptureDevice.GetCurrentVideoFrame(out IntPtr frame);
 6         System.Runtime.InteropServices.Marshal.Copy(frame, 0, bitmapData.Scan0, videoCaptureElement.Width * videoCaptureElement.Height * 3);
 7     }
 8     finally
 9     {
10         bitmap.UnlockBits(bitmapData);
11     }

這個定時器的實現比較low。延時較低、較流暢,但與Win系統相機對比也是不夠清晰

3.MediaCapture(UWP)

MediaCapture是Windows 8及以上版本的WinRT API,專為捕獲音訊、影片和照片設計。

MediaCaptuer是UWP應用的API 使用 MediaCapture 捕獲基本的照片、影片和音訊 - UWP applications | Microsoft Learn,要在WPF內使用需要引入倆個Nuget包:

1     <PackageReference Include="Microsoft.Toolkit.Wpf.UI.XamlHost" Version="6.1.2" />
2     <PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.26100.1" />

初始化MediaCapture:

1     var mediaCapture =new MediaCapture();
2     var videos = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
3     var settings = new MediaCaptureInitializationSettings()
4     {
5         VideoDeviceId = videos[0].Id,
6         StreamingCaptureMode = StreamingCaptureMode.Video,
7     };
8     await mediaCapture.InitializeAsync(settings);

分幾個場景,分別輸出Demo。錄製本地檔案,註釋已經很詳細了並不重複解釋,直接看程式碼:

 1     private MediaCapture _mediaCapture;
 2     private InMemoryRandomAccessStream _randomAccessStream;
 3     private async void StartButton_OnClick(object sender, RoutedEventArgs e)
 4     {
 5         // 1. 初始化 MediaCapture 物件
 6         var mediaCapture = _mediaCapture = new MediaCapture();
 7         var videos = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
 8         var settings = new MediaCaptureInitializationSettings()
 9         {
10             VideoDeviceId = videos[0].Id,
11             StreamingCaptureMode = StreamingCaptureMode.Video,
12         };
13         await mediaCapture.InitializeAsync(settings);
14 
15         // 2. 設定要錄製的資料流
16         var randomAccessStream = _randomAccessStream = new InMemoryRandomAccessStream();
17         // 3. 配置錄製的影片設定
18         var mediaEncodingProfile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.Auto);
19         // 4. 開始錄製
20         await mediaCapture.StartRecordToStreamAsync(mediaEncodingProfile, randomAccessStream);
21     }
22 
23     private async void StopButton_OnClick(object sender, RoutedEventArgs e)
24     {
25         // 停止錄製
26         await _mediaCapture.StopRecordAsync();
27         // 處理錄製後的資料,儲存至"C:\Users\XXX\Videos\RecordedVideo.mp4"
28         var storageFolder = Windows.Storage.KnownFolders.VideosLibrary;
29         var file = await storageFolder.CreateFileAsync("RecordedVideo.mp4", Windows.Storage.CreationCollisionOption.GenerateUniqueName);
30         using var fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
31         await RandomAccessStream.CopyAndCloseAsync(_randomAccessStream.GetInputStreamAt(0), fileStream.GetOutputStreamAt(0));
32         _randomAccessStream.Dispose();
33     }

攝像頭顯示,透過UWP-WindowsXamlHost承載畫面(置頂):

 1     private async void StartButton_OnClick(object sender, RoutedEventArgs e)
 2     {
 3         _mediaCapture = new MediaCapture();
 4         var videos = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
 5         var settings = new MediaCaptureInitializationSettings()
 6         {
 7             VideoDeviceId = videos[0].Id,
 8             StreamingCaptureMode = StreamingCaptureMode.Video,
 9         };
10         await _mediaCapture.InitializeAsync(settings);
11         //顯示WindowsXamlHost
12         VideoViewHost.Visibility = Visibility.Visible;
13         //繫結畫面源
14         _captureElement.Source = _mediaCapture;
15         await _mediaCapture.StartPreviewAsync();
16     }

MediaCapture是UWP平臺的實現方案,直接給CaptureElement賦值繫結畫面源。直接用CaptureElement渲染速度很快,這個實現邏輯同windows系統相機是一樣的

另外,使用MediaCapture也可以捕獲畫面幀事件,用於流媒體資料捕獲收集:

1 // 配置影片幀讀取器
2 var frameSource = mediaCapture.FrameSources.Values.FirstOrDefault(source => source.Info.MediaStreamType == MediaStreamType.VideoRecord);
3 _frameReader = await mediaCapture.CreateFrameReaderAsync(frameSource, MediaEncodingSubtypes.Argb32);
4 _frameReader.FrameArrived += FrameReader_FrameArrived;
5 await _frameReader.StartAsync();

如下方所示,監聽FrameArrived,使用Windows.UI.Xaml.Media.Imaging.BitmapImage渲染展示(僅用於展示,延遲很高):

 1     private async void FrameReader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
 2     {
 3         var frame = sender.TryAcquireLatestFrame();
 4         if (frame != null)
 5         {
 6             var bitmap = frame.VideoMediaFrame?.SoftwareBitmap;
 7             if (bitmap != null)
 8             {
 9                 // 在這裡對每一幀進行處理
10                 await Dispatcher.InvokeAsync(async () =>
11                 {
12                     var bitmapImage = await ConvertSoftwareBitmapToBitmapImageAsync(bitmap);
13                     _captureImage.Source = bitmapImage;
14                 });
15             }
16         }
17     }

如需要將SoftwareBitmap轉為buffer位元組資料,可以按如下處理:

 1     public async Task<byte[]> SoftwareBitmapToByteArrayAsync(SoftwareBitmap softwareBitmap)
 2     {
 3         // 使用InMemoryRandomAccessStream來儲存影像資料
 4         using var stream = new InMemoryRandomAccessStream();
 5         // 建立點陣圖編碼器
 6         var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
 7         // 轉換為BGRA8格式,如果當前格式不同
 8         var bitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
 9         encoder.SetSoftwareBitmap(bitmap);
10         await encoder.FlushAsync();
11         bitmap.Dispose();
12 
13         // 讀取位元組資料
14         using var reader = new DataReader(stream.GetInputStreamAt(0));
15         byte[] byteArray = new byte[stream.Size];
16         await reader.LoadAsync((uint)stream.Size);
17         reader.ReadBytes(byteArray);
18 
19         return byteArray;
20     }

以上,MediaCapture能實現攝像頭顯示及錄製相關功能。MediaCapture程式碼Demo詳見Github:kybs00/CameraCaptureDemo: 攝像頭預覽、捕獲DEMO (github.com)

這是我個人電腦本地監聽幀轉Image顯示的延遲,差不多180ms:

3.其它

OpenCvSharp是OpenCV在C#環境中的包裝,提供跨平臺的計算機視覺和影像處理功能 shimat/opencvsharp: OpenCV wrapper for .NET (github.com)2K影片較為流暢,4K影片延遲較低但顯示效果較差

其它的如EmguCV是另一個基於OpenCV的C#包裝元件庫,具有與OpenCVSharp相同的強大功能 emgucv/emgucv: Emgu CV (github.com)

DirectShow.NET,提供對DirectShow API的託管包裝,使得在.NET框架中可以直接使用DirectShow的強大功能來進行影片捕獲和處理 pauldotknopf/DirectShow.NET。DirectShow本身效能較好,但DirectShow.NET作為託管包裝,效能會受一定影響。延遲效果待驗證

此處就不一一例寫Demo了

我們看看效能資料,4K屏裝置+4K攝像頭,透過本地攝像頭預覽顯示。借用組內小夥伴建凱大佬對各方案的延時統計資料:

驗證了下MediaCaptre的延時與系統相機差不多。透過工作管理員我們可以看到,系統相機CPU佔用3%,但GPU是15%。

使用了硬體加速,效能方面很不錯,所以攝像頭採集推薦MediaCaptre方案

值得一提的是,公司大屏HDMI採集卡訊號Hdmi Record即6911龍訊韌體,採用MediaCapture採集畫面會穩定很多,減少了黑屏、粉屏的機率。從這點也說明Windows系統相機的原生實現方案,相容性更好

另外,這裡驗證的方案都是針對4K攝像頭,如果是8K攝像頭,其效能要求更高了,後面單獨介紹

參考列表:

使用 MediaCapture 捕獲基本的照片、影片和音訊 - UWP applications | Microsoft Learn

[C#] 使用Accord.Net,實現相機畫面採集,影片儲存及裁剪影片區域,利用WriteableBitmap高效渲染 - 孤獨成派 - 部落格園 (cnblogs.com)

.net中捕獲攝像頭影片的方式及對比(How to Capture Camera Video via .Net) - Wuya - 部落格園 (cnblogs.com)

C# 利用AForge進行攝像頭資訊採集 - 老碼識途呀 - 部落格園 (cnblogs.com)

WPF攝像頭使用(WPFMediaKit) - 深秋無痕 - 部落格園 (cnblogs.com)

C#使用OpenCvSharp4庫讀取電腦攝像頭資料並實時顯示_c#顯示攝像頭畫面-CSDN部落格

SharpCaptureDemo: C#,vb, .net採集攝像頭,桌面螢幕,麥克風話筒聲音,攝像頭畫面,並且支援混音,也支援同時採集錄製。效率高,底層採用了directshow技術,穩定性強,相容性好。 (gitee.com)

相關文章