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

OpenCVSharp相机初始化后随机FPS骤降问题排查求助

WinForms + OpenCVSharp 视频录制卡顿问题排查求助

我开发的WinForms桌面应用基于OpenCVSharp实现视频录制与存储,目前用以下代码初始化相机:

VideoCapture videoCapture = VideoCapture.FromCamera(index: cameraIndex, apiPreference: VideoCaptureAPIs.DSHOW);

多数场景下运行正常,但近期客户端反馈:应用加载、相机初始化完成后,FPS会随机骤降至1-5帧,画面严重卡顿,只有重启应用才能恢复。我通过反复初始化VideoCapture能复现这个随机问题,已经花了两天排查但没进展。

换成VideoCaptureAPIs.MSMF作为apiPreference后,FPS问题基本解决,性能还明显提升,但出现了类似GitHub已知问题的新bug,现在陷入瓶颈。不确定是API的问题还是代码有疏漏,附上相关代码片段,求帮忙排查:


OpenCVHelper中创建VideoCapture的代码

public VideoCapture CreateVideoCaptureFromCamera(int cameraIndex, int width, int height, double inputFPS, int IsAutoFocus)
{
    try
    {
        VideoCapture videoCapture = VideoCapture.FromCamera(index: cameraIndex, apiPreference: VideoCaptureAPIs.DSHOW);
        videoCapture.FrameHeight = height;
        videoCapture.FrameWidth = width;
        videoCapture.Fps = inputFPS;

        if (IsAutoFocus == (int)AutoFocusStatus.On)
        {
            videoCapture.AutoFocus = true;
        }
        else
        {
            videoCapture.AutoFocus = false;
            videoCapture.Focus = 100;
        }
        videoCapture.Zoom = 0;
        videoCapture.FourCC = "MJPG";
        return videoCapture;
    }
    catch
    {
        throw;
    }
}

Presenter层创建VideoCapture并启用多线程的代码

videoCapturePresenter = recorderMainAppManager.CreateVideoCapture(
    index: camera.Index,
    resolution: resolution, //resolutions[resolutions.Count - 1],
    fps: ProfilePresenter.CameraFps,
    focus: ProfilePresenter.IsAutoFocus ?? (int)AutoFocusStatus.Off
);
captureTokenSource = new();
_captureThread = new Thread(() => CaptureFrame(videoCapturePresenter, captureTokenSource))
{
    Name = "Capture Thread",
    Priority = ThreadPriority.AboveNormal, ///This is not the cause because i added after i can reproduced the issues. 
};
_captureThread.Start();

CaptureFrame方法代码(帧捕获与UI更新)

private void CaptureFrame(VideoCapture capture, CancellationTokenSource captureToken)
{
    
    string newOverlayText = string.Empty;
    try
    {
        ShipmentOverlay shipmentOverlay = recorderMainAppManager.GetShipmentOverlay
            (
                resolution: $"{capture.FrameWidth}x{capture.FrameHeight}"
            );

        string username = $"Employee Name : {UserPresenter.Username}";
        while (!captureToken.IsCancellationRequested)
        {
            string datetime = DateTimeOffset.Now.ToString("dd/MM/yyyy HH:mm:ss");
            string overlayText = createNewOverlayString();
            using (Mat rawFrame = new())
            {
                newOverlayText = $"Date : {datetime} | {username} | {overlayText}";
                if (!capture.Read(rawFrame))
                {
                    Console.WriteLine("Error: Failed to read frame.");
                    break;
                }

                if (rawFrame.Empty())
                {
                    Console.WriteLine("Error: Empty frame.");
                    break;
                }

                using (Mat frameWithOverlay = recorderMainAppManager.DrawOverlay(frame: rawFrame, shipmentOverlay: shipmentOverlay, overlayText: newOverlayText))
                {
                    Mat clonedFrame = frameWithOverlay.Clone();
                    {
                        lock (clonedFrame)
                        {
                            var currentPictureBox = recorderMainAppView.GetPicRecordResolution();
                            var targetSize = new OpenCvSharp.Size() { Width = currentPictureBox.Width, Height = currentPictureBox.Height };
                            if (targetSize.Width > 0 && targetSize.Height > 0)
                            {
                                Mat captureMat = new();
                                Cv2.Resize(clonedFrame, captureMat, targetSize, interpolation: InterpolationFlags.Area);
                                using (Bitmap _bitmap = BitmapConverter.ToBitmap(captureMat))
                                {
                                    Bitmap clonedResizeFrame = (Bitmap)_bitmap.Clone();
                                    recorderMainAppView.UpdateCameraView(clonedResizeFrame);
                                }

                                if (_isRecording == RecordStatus.RECORD)
                                {
                                    _frameQueue.Enqueue(clonedFrame);
                                }
                                else
                                {
                                    captureMat.Dispose();
                                }
                            }
                        }
                    }
                }
            }
            _threadStopEvent.Wait(1);
            if (captureToken.IsCancellationRequested) break;
        }
    }
    catch
    {
        throw;
    }
}

UI层UpdateCameraView方法代码

public void UpdateCameraView(Bitmap img)
{
    try
    {
        if (picRecord.InvokeRequired && 
            WindowState != FormWindowState.Minimized)
        {
            using (picRecord.Image)
            {
                Invoke(() => picRecord.Image = img);
            }
        }
    }
    catch (ObjectDisposedException)
    {
        // This function throw everytime in debug mode, apparently because some video frame aren't being disposed yet
        // But frmMain is already disposed. Happened only when i exit the app.
        // For now, do nothing. But if you know how to solve this. Feel free.
        return;
    }
}

补充:帧写入线程代码(问题出现于录制前)

创建写入线程代码

videoWriterPresenter = recorderMainAppManager.CreateVideoWriter(
    capture: videoCapturePresenter,
    savedDir: outputDirectory,
    BaseLabel: data.FirstOrDefault(t => !string.IsNullOrEmpty(t)));
_threadStopEvent.Reset();
writerTokenSource = new();
_writerThread = new Thread(() => WriteFrame(writerTokenSource))
{
    Name = "Writer Thread",
    IsBackground = true
};
_writerThread.Start();

WriteFrame方法代码

private void WriteFrame(CancellationTokenSource writerToken)
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    double _delay = 1_000 / videoCapturePresenter!.Fps;
    try
    {
        while (!_threadStopEvent.Wait(0) && !writerToken.IsCancellationRequested)
        {
            if (stopwatch.Elapsed.TotalMilliseconds <= _delay)
            {
                _threadStopEvent.Wait(1);
                continue;
            }

            while (_frameQueue.Count != 0)
            {
                if (_frameQueue.TryDequeue(out Mat? frameDequeue))
                {
                    using (frameDequeue)
                    {
                        videoWriterPresenter!.Write(frameDequeue);
                    }
                    _threadStopEvent.Wait(1);
                    stopwatch.Restart();
                }
            }
        }
    }
    catch
    {
        throw;
    }
    finally
    {
        stopwatch.Stop();
    }
}

排查方向建议

  1. DSHW模式资源泄漏/竞争

    • 每次销毁VideoCapture时,必须调用Release()和Dispose(),避免相机设备资源被残留实例占用,导致新实例无法获取足够带宽引发卡顿。
    • 部分相机不支持多实例访问,若初始化失败后未正确释放,后续实例会陷入资源争抢,导致FPS骤降。
  2. CaptureFrame线程资源处理

    • 非录制状态下clonedFrame未释放:在else分支添加clonedFrame.Dispose();,避免内存泄漏引发GC频繁触发拖慢帧率。
    • 锁对象错误:lock (clonedFrame)无法起到同步作用,应改用全局静态对象(如private static readonly object _frameLock = new object();)作为锁。
    • 移除不必要的_threadStopEvent.Wait(1),让capture.Read()自然阻塞获取帧,减少额外延迟。
  3. UI更新性能优化

    • 修改UpdateCameraView的Image释放逻辑,在Invoke内部处理旧Image:
      Invoke(() => 
      {
          var oldImage = picRecord.Image;
          picRecord.Image = img;
          oldImage?.Dispose();
      });
      
    • 减少Bitmap克隆和转换的开销,考虑直接在UI线程进行图像转换,或使用更高效的Mat到PictureBox数据传输方式。
  4. MSMF模式问题规避

    • 强制指定相机硬件支持的分辨率和帧率,设置videoCapture.Set(VideoCaptureProperties.FourCC, FourCC.FromString("MJPG")),降低驱动兼容性问题。
    • 确认客户端设备已授予摄像头完全访问权限,部分系统对MSMF模式有额外权限要求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 20:40:56