.NET MAUI实时目标检测应用:如何获取并解析相机帧?
实现.NET MAUI实时目标检测应用的方案
核心思路
无需依赖付费库,通过原生API或开源组件获取相机实时帧,转换为AI模型兼容格式后完成推理,最后在UI层绘制检测结果。
方案一:使用.NET MAUI原生MediaCapture API(跨平台、无第三方依赖)
通过依赖服务封装各平台的相机帧捕获逻辑,直接获取原始帧数据:
步骤1:定义跨平台接口
在共享项目中创建相机帧捕获的统一接口:
public interface ICameraFrameService { event EventHandler<byte[]> FrameCaptured; Task StartCaptureAsync(); Task StopCaptureAsync(); }
步骤2:各平台原生实现
Android平台(基于CameraX/ImageReader)
[assembly: Dependency(typeof(AndroidCameraFrameService))] namespace YourApp.Droid { public class AndroidCameraFrameService : ICameraFrameService, IDisposable { private ImageReader _imageReader; private CameraCaptureSession _captureSession; private CameraDevice _cameraDevice; public event EventHandler<byte[]> FrameCaptured; public async Task StartCaptureAsync() { var cameraManager = (CameraManager)Android.App.Application.Context.GetSystemService(Context.CameraService); var cameraId = cameraManager.GetCameraIdList().First(id => cameraManager.GetCameraCharacteristics(id).Get(CameraCharacteristics.LensFacing) == LensFacing.Back); await cameraManager.OpenCameraAsync(cameraId, new CameraStateCallback(this), null); } private void OnImageAvailable(object sender, ImageReader.ImageAvailableEventArgs e) { using var image = e.ImageReader.AcquireNextImage(); if (image == null) return; // 转换YUV_420_888格式帧为RGB字节数组(示例简化,实际需处理像素格式转换) var buffer = image.Planes[0].Buffer; var bytes = new byte[buffer.Remaining()]; buffer.Get(bytes); FrameCaptured?.Invoke(this, bytes); image.Close(); } public Task StopCaptureAsync() { _captureSession?.StopRepeating(); _cameraDevice?.Close(); _imageReader?.Dispose(); return Task.CompletedTask; } public void Dispose() => StopCaptureAsync().Wait(); // 实现CameraStateCallback、CaptureSessionCallback等辅助类(略) } }
iOS平台(基于AVCaptureSession)
[assembly: Dependency(typeof(IosCameraFrameService))] namespace YourApp.iOS { public class IosCameraFrameService : ICameraFrameService, IDisposable { private AVCaptureSession _captureSession; private AVCaptureVideoDataOutput _videoOutput; public event EventHandler<byte[]> FrameCaptured; public Task StartCaptureAsync() { _captureSession = new AVCaptureSession(); _captureSession.SessionPreset = AVCaptureSession.PresetMedium; var camera = AVCaptureDevice.DefaultDeviceWithMediaType(AVMediaType.Video); var input = AVCaptureDeviceInput.FromDevice(camera); if (_captureSession.CanAddInput(input)) _captureSession.AddInput(input); _videoOutput = new AVCaptureVideoDataOutput(); _videoOutput.SetSampleBufferDelegate(new SampleBufferDelegate(this), DispatchQueue.CurrentQueue); if (_captureSession.CanAddOutput(_videoOutput)) _captureSession.AddOutput(_videoOutput); _captureSession.StartRunning(); return Task.CompletedTask; } private class SampleBufferDelegate : AVCaptureVideoDataOutputSampleBufferDelegate { private readonly IosCameraFrameService _service; public SampleBufferDelegate(IosCameraFrameService service) => _service = service; public override void DidOutputSampleBuffer(AVCaptureOutput captureOutput, CMSampleBuffer sampleBuffer, AVCaptureConnection connection) { using var imageBuffer = sampleBuffer.GetImageBuffer() as CVPixelBuffer; imageBuffer.Lock(CVPixelBufferLock.ReadOnly); var baseAddress = imageBuffer.BaseAddress; var bytesPerRow = imageBuffer.BytesPerRow; var bytes = new byte[imageBuffer.Height * bytesPerRow]; Marshal.Copy(baseAddress, bytes, 0, bytes.Length); _service.FrameCaptured?.Invoke(_service, bytes); imageBuffer.Unlock(CVPixelBufferLock.ReadOnly); } } public Task StopCaptureAsync() { _captureSession?.StopRunning(); _captureSession?.Dispose(); return Task.CompletedTask; } public void Dispose() => StopCaptureAsync().Wait(); } }
Windows平台(基于MediaCapture/FrameReader)
[assembly: Dependency(typeof(WindowsCameraFrameService))] namespace YourApp.WinUI { public class WindowsCameraFrameService : ICameraFrameService, IDisposable { private MediaCapture _mediaCapture; private MediaFrameReader _frameReader; public event EventHandler<byte[]> FrameCaptured; public async Task StartCaptureAsync() { _mediaCapture = new MediaCapture(); await _mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings { StreamingCaptureMode = StreamingCaptureMode.Video }); var frameSource = _mediaCapture.FrameSources .First(f => f.Value.Info.MediaStreamType == MediaStreamType.VideoPreview).Value; _frameReader = await _mediaCapture.CreateFrameReaderAsync(frameSource, MediaEncodingSubtypes.Bgra8); _frameReader.FrameArrived += FrameReader_FrameArrived; await _frameReader.StartAsync(); } private void FrameReader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args) { using var frame = sender.TryAcquireLatestFrame(); if (frame == null) return; using var bitmap = SoftwareBitmap.Convert(frame.VideoMediaFrame.SoftwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); var bytes = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4]; bitmap.CopyToBuffer(bytes.AsBuffer()); FrameCaptured?.Invoke(this, bytes); } public async Task StopCaptureAsync() { if (_frameReader != null) { await _frameReader.StopAsync(); _frameReader.FrameArrived -= FrameReader_FrameArrived; _frameReader.Dispose(); } _mediaCapture?.Dispose(); } public void Dispose() => StopCaptureAsync().Wait(); } }
步骤3:共享代码中处理帧与绘制结果
在MAUI页面中集成服务,完成帧处理和UI绘制:
public partial class MainPage : ContentPage { private ICameraFrameService _cameraService; public MainPage() { InitializeComponent(); _cameraService = DependencyService.Get<ICameraFrameService>(); _cameraService.FrameCaptured += OnFrameCaptured; } protected override async void OnAppearing() { base.OnAppearing(); await _cameraService.StartCaptureAsync(); } protected override async void OnDisappearing() { base.OnDisappearing(); await _cameraService.StopCaptureAsync(); } private void OnFrameCaptured(object sender, byte[] frameBytes) { // 1. 将字节数组转换为模型输入格式(缩放、归一化等) // 2. 传入目标检测模型(如ONNX Runtime加载的模型) var detectionResults = RunModelInference(frameBytes); // 3. 在UI线程绘制检测框 MainThread.BeginInvokeOnMainThread(() => DrawBoundingBoxes(detectionResults)); } private void DrawBoundingBoxes(List<DetectionResult> results) { CameraCanvas.Clear(); var scale = CameraCanvas.Width / ModelInputWidth; // 帧与控件的尺寸比例 foreach (var result in results) { var rect = new Rect( result.X * scale, result.Y * scale, result.Width * scale, result.Height * scale); // 绘制矩形框和标签 CameraCanvas.DrawRectangle(rect, Colors.Red, 2); CameraCanvas.DrawString($"{result.Label} {result.Score:F2}", rect.X, rect.Y - 20, Colors.White, 16); } } }
方案二:使用开源.NET MAUI相机组件
部分社区维护的开源MAUI组件支持实时帧捕获,无需自行封装原生代码:
- 直接提供帧回调事件,可获取Bitmap或字节数组格式的帧数据
- 内置相机预览界面,减少UI布局工作量
这类组件均为免费开源,可直接集成到项目中。
方案三:适配Xamarin CameraView到.NET MAUI
Xamarin的CameraView组件可迁移到.NET MAUI:
- 将项目依赖从
Xamarin.Forms替换为Microsoft.Maui.Controls - 调整命名空间(如使用
Microsoft.Maui.Controls.Compatibility.CameraView兼容包) - 修复API差异(如相机权限请求、帧回调方法签名)
- 确保组件适配AndroidX(旧Xamarin组件需升级到AndroidX版本)
迁移后,可通过组件的FrameCaptured事件获取帧数据,后续处理逻辑与方案一一致。
关键注意事项
- 权限配置:在各平台配置文件中添加相机权限(如Android的
android.permission.CAMERA,iOS的NSCameraUsageDescription) - 性能优化:
- 在后台线程执行模型推理,避免阻塞UI线程
- 对帧进行降采样,减少模型输入尺寸以提升推理速度
- 启用硬件加速(如ONNX Runtime开启GPU推理)
- 坐标转换:相机帧尺寸与UI控件尺寸可能不同,需按比例转换bounding box坐标,确保绘制位置准确
内容的提问来源于stack exchange,提问作者CaptainTaro
相关产品推荐
相关产品推荐

