rtsp 카메라 영상을 비트맵으로 ImageBrush.ImageSource 에 넣어 출력하는 코드입니다.
winform에서는 버퍼링 없이 돌아가던 코드가 wpf로 옮겨오니 뚝뚝 끊기는 형상을 보입니다.
아래는 winform 코드입니다.
아래는 wpf 코드입니다.
이상입니다.
rtsp 카메라 영상을 비트맵으로 ImageBrush.ImageSource 에 넣어 출력하는 코드입니다.
winform에서는 버퍼링 없이 돌아가던 코드가 wpf로 옮겨오니 뚝뚝 끊기는 형상을 보입니다.
아래는 winform 코드입니다.
아래는 wpf 코드입니다.
이상입니다.
윈폼 코드와 WPF 코드의 차이점이 WPF 코드에선 Invoke 를 통해 호출하네요.
실시간 영상출력시 Invoke를 사용하면 일반적으로 초당 30프레임을 표출할 수 없습니다.(10 프레임 이하 정도는 가능할지 모르겠습니다)
콜백함수가 rtsp 라이브러리 내부의 쓰레드를 탈건데 어떤 방식으로든 Invoke를 하지않고 바로 렌더링을 해야할것 같습니다.
@스노우맨 님께서 답변해주신 대로 현재 코드는 lock
과 Invoke
가 무분별하게 사용되어 느리게 동작할 수 밖에 없도록 구현되어 있습니다.
추가적으로 비트맵 변환 과정에서 불필요한 CPU 부하가 발생하고 LOH 할당에 의한 Gen2 GC 유발 및 메모리 누수 가능성 등 여러가지 문제가 있네요.
원본 코드를 텍스트로 올려주시면 답변에 도움이 될 것 같습니다.
그리고 Form1_captureProc
함수가 주 스레드에서 호출되는 것인지 별도 스레스를 통해 호출된 것인지 여부도 확인해주시면 좋을 것 같네요.
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 이벤트 핸들러같이 보이는데 사이즈 조절이나 화면갱신시 마지막 영상 데이터를 그려주게하고요.
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
은 멀티 스레드에서 사용하기 어려운 문제점이 있습니다. 해결 방법 필요 시 설명 드리도록 하겠습니다.
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));
}
// 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;
}