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

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

아래는 winform 코드입니다.

아래는 wpf 코드입니다.

이상입니다.

1 Like

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

4 Likes

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

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

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

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

2 Likes

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 Like

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 Likes

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 Likes