rtsp 카메라 영상 이미지로 출력

rtsp 카메라 영상을 비트맵으로 ImageBrush.ImageSource 에 넣어 출력하는 코드입니다.
winform에서는 버퍼링 없이 돌아가던 코드가 wpf로 옮겨오니 뚝뚝 끊기는 형상을 보입니다.

아래는 winform 코드입니다.

아래는 wpf 코드입니다.

이상입니다.

1개의 좋아요

윈폼 코드와 WPF 코드의 차이점이 WPF 코드에선 Invoke 를 통해 호출하네요.
실시간 영상출력시 Invoke를 사용하면 일반적으로 초당 30프레임을 표출할 수 없습니다.(10 프레임 이하 정도는 가능할지 모르겠습니다)
콜백함수가 rtsp 라이브러리 내부의 쓰레드를 탈건데 어떤 방식으로든 Invoke를 하지않고 바로 렌더링을 해야할것 같습니다.

4개의 좋아요

@스노우맨 님께서 답변해주신 대로 현재 코드는 lockInvoke가 무분별하게 사용되어 느리게 동작할 수 밖에 없도록 구현되어 있습니다.

추가적으로 비트맵 변환 과정에서 불필요한 CPU 부하가 발생하고 LOH 할당에 의한 Gen2 GC 유발 및 메모리 누수 가능성 등 여러가지 문제가 있네요.

원본 코드를 텍스트로 올려주시면 답변에 도움이 될 것 같습니다.

그리고 Form1_captureProc 함수가 주 스레드에서 호출되는 것인지 별도 스레스를 통해 호출된 것인지 여부도 확인해주시면 좋을 것 같네요.

2개의 좋아요

CaptureProc 함수는 별도 스레드를 통해 호출됩니다. 별도 클래스의 생성자에서 콜백함수로 등록되었으며, 해당 콜백함수는 다른 스레드에서 BitmapData 데이터가 수신될 때마다 호출됩니다.

void CaptureProc(IntPtr bitmapBits, int bufferSize, int width, int height)
{
    Dispatcher.Invoke(() =>
    {
        BitmapData bmpData = bmpCapture.LockBits(new System.Drawing.Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppRgb);
        if (bitmapBits != IntPtr.Zero) CopyMemory(bmpData.Scan0, bitmapBits, (uint)bufferSize);
        bmpCapture.UnlockBits(bmpData);
    });
    InvalidateVisual();
}

protected override void OnRender(DrawingContext drawingContext)
{
    lock (this)
    {
        Bitmap bitmap = bmpCapture;
        MemoryStream memoryStream = new MemoryStream();
        bitmap.Save(memoryStream, ImageFormat.Png);
        memoryStream.Seek(0, SeekOrigin.Begin);

        BitmapImage bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        bitmapImage.StreamSource = memoryStream;
        bitmapImage.EndInit();

        //Application.Current.Dispatcher.Invoke(() =>
        //{
            Rect rect = new Rect(0, 0, Width, Height);

            drawingContext.DrawImage(bitmapImage, rect);
            img_Camera.ImageSource = bitmapImage;
        //});
    }
}

그런데 이렇게되면 CaptureProc의 InvalidateVisual 에서 액세스 불가 에러가 뜹니다…

이 부분에서 OnRender를 구현하는 해당 컨트롤과 img_Camera 이미지 컨트롤 두 군데 모두 업데이트가 필요한 상황인가요?

    protected override void OnRender(DrawingContext drawingContext)
    {
        lock (this)
        {
            // 비트맵 이미지 로딩 및 크기 조정
            Bitmap bitmap = bmpCapture;
            MemoryStream memoryStream = new MemoryStream();
            bitmap.Save(memoryStream, ImageFormat.Png);
            memoryStream.Seek(0, SeekOrigin.Begin);

            // 비동기적으로 BitmapImage 로드
            BitmapImage bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
            bitmapImage.StreamSource = memoryStream;
            bitmapImage.EndInit();

            img_Camera.ImageSource = bitmapImage;
        }
    }

말씀 주신대로 수정했는데 아직도 끊기는 증상이 보입니다…!

간단하게 말씀드리자면 영상을 렌더링하는 부분은 UI 쓰레드를 타면 안됩니다.
그러니까 CaptureProc 콜백함수에서 바로 눈에 보이게 그려야하고 InvalidateVisual() 을 호출해서 갱신하면 안됩니다.
OnRender가 WM_PAINT 이벤트 핸들러같이 보이는데 사이즈 조절이나 화면갱신시 마지막 영상 데이터를 그려주게하고요.

1개의 좋아요

OnRender 함수 부분은 삭제하시고 아래 코드만으로 돌려보시죠.

아래 코드가 정상 동작한다면 이후 메모리 최적화를 시도하시면 될 것 같습니다.

void CaptureProc(IntPtr bitmapBits, int bufferSize, int width, int height)
{
    var bitmapSource = BitmapSource.Create(
        width, height, 96, 96, PixelFormats.Bgr32, null,
        bitmapBits, bufferSize, bufferSize / height);
    bitmapSource.Freeze();
    Dispatcher.BeginInvoke(new Action(() => img_Camera.ImageSource = bitmapSource));
}

위 코드는 매 프레임마다 BitmapImage를 생성하며 이 때 bufferSize에 해당하는 비트맵 버퍼를 메모리를 할당하고, 일반적으로 카메라 사이즈의 비트맵은 매우 높은 확률로 LOH 할당 대상인 85,000 바이트를 초과하므로 Gen2 GC를 피하기 어렵습니다.

이는 WriteableBitmap 클래스를 사용하면 해소할 수 있으나 WritableBitmap은 멀티 스레드에서 사용하기 어려운 문제점이 있습니다. 해결 방법 필요 시 설명 드리도록 하겠습니다.

3개의 좋아요

Solution #1

void CaptureProc(IntPtr bitmapBits, int bufferSize, int width, int height)
{
    var bitmapSource = BitmapSource.Create(
        width, height, 96, 96, PixelFormats.Bgr32, null,
        bitmapBits, bufferSize, bufferSize / height);
    bitmapSource.Freeze();
    Dispatcher.BeginInvoke(new Action(() => img_Camera.ImageSource = bitmapSource));
}

Solution #2

// 2024.2.26 최초 작성
// 2024.2.27 변수 정리 및 lock 개선
private WriteableBitmap _buffer1;
private WriteableBitmap _buffer2;

private WriteableBitmap _freeBuffer;

private object _bufferLock = new object();
private IntPtr _bufferPtr;
private IntPtr _lastWritten;

private void InitializeBuffers() // 생성 시 호출
{
    const int IMAGE_WIDTH = 1920;
    const int IMAGE_HEIGHT = 1080;

    _buffer1 = new WriteableBitmap(IMAGE_WIDTH, IMAGE_HEIGHT, 96, 96, PixelFormats.Bgr32, null);
    _buffer2 = new WriteableBitmap(IMAGE_WIDTH, IMAGE_HEIGHT, 96, 96, PixelFormats.Bgr32, null);

    _freeBuffer = _buffer2;
    _freeBuffer.Lock();
    _bufferPtr = _freeBuffer.BackBuffer;
}

private void CaptureProc(IntPtr bitmapBits, int bufferSize, int width, int height)
{

    WriteableBitmap buffer = null;

    lock (_bufferLock)
    {
        if (_lastWritten == _bufferPtr) // 이전 버퍼가 아직 사용되지 않았다면 스킵
        {
            return;
        }

        CopyMemory(_bufferPtr, bitmapBits, (uint)bufferSize);

        _lastWritten = _bufferPtr;
        buffer = _freeBuffer;
    }
    Dispatcher.BeginInvoke(new Action(() => SwitchBuffer(buffer)), DispatcherPriority.Render);
}

private void SwitchBuffer(WriteableBitmap buffer)
{
    // CountFPS();

    lock (_bufferLock)
    {
        _freeBuffer = buffer == _buffer2 ? _buffer1 : _buffer2;
        _freeBuffer.Lock();
        _bufferPtr = _freeBuffer.BackBuffer;
    }

    buffer.AddDirtyRect(new Int32Rect(0, 0, buffer.PixelWidth, buffer.PixelHeight));
    buffer.Unlock();

    img_Camera.ImageSource = buffer;
}

1920*1080 이미지 디버그 모드 테스트 결과

  • Solution #1: 메모리 사용량 290MB / 154 FPS / GC 지속 발생
  • Solution #2: 메모리 사용량 175MB / 200 FPS / GC 발생 없음 (릴리스 모드 330 FPS)
9개의 좋아요