You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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:

  1. 将项目依赖从Xamarin.Forms替换为Microsoft.Maui.Controls
  2. 调整命名空间(如使用Microsoft.Maui.Controls.Compatibility.CameraView兼容包)
  3. 修复API差异(如相机权限请求、帧回调方法签名)
  4. 确保组件适配AndroidX(旧Xamarin组件需升级到AndroidX版本)

迁移后,可通过组件的FrameCaptured事件获取帧数据,后续处理逻辑与方案一一致。

关键注意事项

  • 权限配置:在各平台配置文件中添加相机权限(如Android的android.permission.CAMERA,iOS的NSCameraUsageDescription)
  • 性能优化:
    • 在后台线程执行模型推理,避免阻塞UI线程
    • 对帧进行降采样,减少模型输入尺寸以提升推理速度
    • 启用硬件加速(如ONNX Runtime开启GPU推理)
  • 坐标转换:相机帧尺寸与UI控件尺寸可能不同,需按比例转换bounding box坐标,确保绘制位置准确

内容的提问来源于stack exchange,提问作者CaptainTaro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 13:25:33