C# - OpenCvSharp을 이용한 동영상(avi, mp4, ...) 처리
OpenCvSharp을 이용해,
OpenCV 응용 프로그램을 C#으로 구현 - OpenCvSharp
; https://www.sysnet.pe.kr/2/0/11402
동영상 처리하는 것도 다음과 같이 간단합니다.
[Cpp] Capturing Video
; https://github.com/shimat/opencvsharp/wiki/%5BCpp%5D-Capturing-Video
using OpenCvSharp;
using System;
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            VideoCapture capture = new VideoCapture("c:\\temp\\test.avi");
            int sleepTime = (int)Math.Round(1000 / capture.Fps);
            using (Window window = new Window("capture"))
            using (Mat image = new Mat()) // Frame image buffer
            {
                // When the movie playback reaches end, Mat.data becomes NULL.
                while (true)
                {
                    capture.Read(image); // same as cvQueryFrame
                    if (image.Empty())
                        break;
                    window.ShowImage(image);
                    Cv2.WaitKey(sleepTime);
                }
            }
        }
    }
}
참고로 Cv2.WaitKey를 Thread.Sleep으로 바꾸면 동영상 출력이 안됩니다. (아마도 Cv2.WaitKey 내부 로직에는 OpenCvSharp.Window의 메시지 처리 등도 포함이 된 것 같습니다.)
그런데, Windows Forms로 올려서 OpenCvSharp.Window 없이 처리하려면 어떻게 해야 할까요? 이를 위해 다음과 같은 식으로 처리를 해봤는데요.
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        protected override void OnPaintBackground(PaintEventArgs e)
        {
         //  base.OnPaintBackground(e);
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            var g = e.Graphics;
            
            while (true)
            {
                if (_q.TryTake(out Bitmap item) == true)
                {
                    g.DrawImage(item, new PointF(0, 0));
                    item.Dispose();
                }
                else
                {
                    break;
                }
            }
        }
        BlockingCollection<Bitmap> _q = new BlockingCollection<Bitmap>();
        private Thread camera;
        private void CaptureCameraCallback()
        {
            VideoCapture capture = new VideoCapture("c:\\temp\\test.avi");
            if (capture.IsOpened() == false)
            {
                return;
            }
            int fps = (int)capture.Fps;
            int expectedProcessTimePerFrame = 1000 / fps;
            Stopwatch st = new Stopwatch();
            st.Start();
            using (Mat image = new Mat())
            {
                while (true)
                {
                    long started = st.ElapsedMilliseconds;
                    capture.Read(image);
                    if (image.Empty() == true)
                    {
                        break;
                    }
                    _q.Add(BitmapConverter.ToBitmap(image));
                    this.Invoke((Action)(() => this.Invalidate()));
                    int elapsed = (int)(st.ElapsedMilliseconds - started);
                    int delay = expectedProcessTimePerFrame - elapsed;
                    if (delay > 0)
                    {
                        Thread.Sleep(delay);
                    }
                }
            }
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            camera = new Thread(CaptureCameraCallback);
            camera.IsBackground = true;
            camera.Start();
        }
    }
}
이전의 OpenCvSharp.Window를 이용했던 예제와 비교해 성능이 심각하게 하락했습니다. 예를 들어 1280 * 720 해상도의 24 fps 동영상도 화면이 뚝뚝 끊기면서 나오고 CPU 사용량도 늘었습니다. 단순히 OpenCvSharp.Window를 출력할 때는 CPU가 거의 0에 가깝게 소비하고 있었는데 위와 같이 바꾸니까 항상 7 ~ 12% 이상을 소비합니다. (4Core 기준)
물론 OpenCvSharp.Window를 사용할 때와는 다르게 ToBitmap의 호출로 인한 버퍼 복사에 대한 부하가 있겠지만 가장 병목이 되는 지점은 Graphics.DrawImage였습니다. DrawImage 호출에 20ms ~ 50ms까지 부하가 나오므로 24fps를 위한 최소 기준인 41ms를 심심치 않게 초과하는 것입니다.
Graphics.DrawImage에 대해 검색해 봐도 비슷한 의견을 많이 볼 수 있습니다.
Is Graphics.DrawImage too slow for bigger images?
; https://stackoverflow.com/questions/11020710/is-graphics-drawimage-too-slow-for-bigger-images
그러니까, 혹시나 GDI로 영상 처리를 한다면 단일 프레임은 그나마 상관없겠지만 다중 프레임을 처리해야 하는 상황이라면 이런 성능 차이를 고려해 OpenCvSharp.Window로 처리하는 것이 권장됩니다.
(
첨부한 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]