Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 10개 있습니다.)
VC++: 121. DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++)
; https://www.sysnet.pe.kr/2/0/11385

.NET Framework: 705. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드
; https://www.sysnet.pe.kr/2/0/11400

.NET Framework: 706. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력
; https://www.sysnet.pe.kr/2/0/11401

.NET Framework: 712. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력 + OpenCV
; https://www.sysnet.pe.kr/2/0/11407

.NET Framework: 713. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력 + OpenCV (2)
; https://www.sysnet.pe.kr/2/0/11408

.NET Framework: 913. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 라이브러리
; https://www.sysnet.pe.kr/2/0/12238

.NET Framework: 1123. C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법
; https://www.sysnet.pe.kr/2/0/12889

.NET Framework: 1126. C# - snagit처럼 화면 캡처를 연속으로 수행해 동영상 제작
; https://www.sysnet.pe.kr/2/0/12895

.NET Framework: 1128. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리
; https://www.sysnet.pe.kr/2/0/12897

.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)
; https://www.sysnet.pe.kr/2/0/12963




C# - snagit처럼 화면 캡처를 연속으로 수행해 동영상 제작

화면 캡처 프로그램인 snagit을 써보시면,

snagit
; https://www.techsmith.com/store/snagit

원하는 범위에서 캡처를 동영상으로 만드는 기능도 제공합니다. 마침(?) 오래전의 질문도 있으니,

c# 으로 화면캡쳐해서 동영상으로 만드는앱을 만들고싶습니다
; https://www.sysnet.pe.kr/3/0/5037

이참에 지난번에 만들어 둔 코드에,

C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법
; https://www.sysnet.pe.kr/2/0/12889

화면 녹화 기능을 넣어보겠습니다. ^^ 사실, 이에 대한 구현은 아래의 2개 내용을 합치기만 하면 됩니다.

C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법
; https://www.sysnet.pe.kr/2/0/12889

C# - OpenCvSharp.VideoWriter에 BMP 파일을 1초씩 출력하는 예제
; https://www.sysnet.pe.kr/2/0/12485

"C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법" 글에서 이미 화면 캡처 이미지를 Mat으로 반환받았으니, "C# - OpenCvSharp.VideoWriter에 BMP 파일을 1초씩 출력하는 예제"에서 소개한 VideoWriter로는 단순히 원하는 fps에 맞게 쓰기 작업을 해주면 되는 것입니다.

즉, 대충 다음과 같은 식으로 하면 구색은 갖춥니다. ^^

private void CaptureLoopMat(DXGIManager manager)
{
    int fps = 30;

    using (Mat src = new Mat(manager.Height, manager.Width, MatType.CV_8UC4))
    using (VideoWriter writer = new VideoWriter("C:\\temp\\test.avi", FourCC.XVID, fps, src.Size(), true))
    {
        while (_exitCapture == false)
        {
            long started = sw.ElapsedMilliseconds;

            if (manager.Capture(src.Data, 1000) == true)
            {
                using (Mat dst = src.CvtColor(ColorConversionCodes.BGRA2BGR))
                {
                    writer.Write(dst);
                }
            }
        }
    }
}

그런데 실제로 위의 코드를 실행해 보면, 당연히 avi 파일에 지정한 30fps, 즉 초당 30개의 이미지를 써야 동영상 재생 프로그램이 현실 시간에 따른 속도로 화면을 보여줄 텐데, 위의 코드에서는 manager.Capture가 반환하는 속도에 의존해 초당 쓰게 될 이미지의 수가 결정되므로 정상적인 속도로 화면 레코딩이 되지 않습니다.




이 문제를 해결하기 위해 우선 Capture의 동작을 이해해야 합니다. 지난 글에도 설명했지만, (Capture 내에서 사용하는) AcquireNextFrame은 캡처 대상이 되는 모니터의 화면에 변화가 있어야 그 이미지를 반환합니다.

따라서, 단순히 CaptureLoopMat에서와 같은 코드를 작성하면 화면 변경이 거의 없는 경우에는 30fps가 요구하는 이미지 수를 못 채울 것이고, Youtube와 같은 사이트를 방문해 동영상을 재생하면 30fps에 가까운 이미지가 VideoWriter에 추가될 수 있습니다. 혹은, 30fps보다 더 빠르게 Capture가 된다면 재생 시에 슬로우 모션이 나옵니다.

결국, Capture 루프에서 몇 장의 이미지가 나오든지에 상관없이 우리는 fps에 맞는 프레임을 VideoWriter에 써야 합니다.

그런데, fps가 참 재미있습니다. 가령 30fps라고 하면 1초에 30장의 이미지를 써야 하는데요, 그렇다면 1000/30 = 33.3333... 과 같이 정수로 딱 떨어지지 않는 시간이 나옵니다. 그래서 만약 33ms마다 1장씩 VideoWriter에 쓰게 된다면 0.3.... 의 시간이 절삭되고, 그것이 누적되면서 나중에는 초 단위까지 영향을 줄 수 있습니다.

제가 영상 관련은 해본 적이 없어서 이런 소수점 이하의 처리를 현업에서는 어떻게 하고 있는지 모르겠습니다. ^^; 그래서 그냥 제 맘대로 만들었습니다. 일단, Capture한 영상을 VideoWriter에 곧바로 쓰지 않고 FPS에 맞는 분량으로, 예를 들어 30fps라면 무조건 1초마다 30장의 이미지를 쓰도록 중간 처리를 하는 FpsWriter를 두었습니다.

private void CaptureLoopMat(DXGIManager manager)
{
    int fps = 30;

    Stopwatch sw = new Stopwatch();
    sw.Start();

    OpenCvSharp.Size frameSize = new OpenCvSharp.Size(manager.Width, manager.Height);
    using (Mat frame = new Mat(frameSize, MatType.CV_8UC4))
    using (VideoWriter writer = new VideoWriter("C:\\temp\\test.avi", FourCC.XVID, fps, frameSize, true))
    using (FpsWriter matBuffer = new FpsWriter(writer, MatType.CV_8UC3))
    {
        while (_exitCapture == false)
        {
            long started = sw.ElapsedMilliseconds;

            if (manager.Capture(frame.Data, 1000) == true)
            {
                Mat current = matBuffer.GetMat();
                Cv2.CvtColor(frame, current, ColorConversionCodes.BGRA2BGR);
                matBuffer.Write(current);
            }

            int elapsed = (int)(sw.ElapsedMilliseconds - started);
            matBuffer.TrySleep(elapsed);
        }
    }
}

FpsWriter.Write에서는 캡처 루프에 영향을 주지 않도록 BufferBlock<T>를 이용해 이미지 처리 스레드를 별도로 두어 전달하는 식입니다.

public void Write(Mat current)
{
    _lastCaptured = new FrameItem { TimeStamp = timeGetTime(), Mat = current };
    _queue.SendAsync(_lastCaptured); // BufferBlock에 담기만 하고 메서드를 벗어남.
}

실제적인 처리는 비동기 처리를 위한 루프에서 담당하고,

private async Task ProcessFrame()
{
    // ...[생략]...

    while (true)
    {
        FrameItem frame = await _queue.ReceiveAsync();
        if (frame.TimeStamp == 0)
        {
            break;
        }

        // ...[생략]...

        AddFrame(...[생략]...);
    }
}

AddFrame 내에서는 1초마다, 그동안 쌓인 이미지 데이터를 VideoWriter에 쓰는 작업을 합니다.

internal void AddFrame(Queue<FrameItem> frameQueue, ref FrameItem lastFrameOfPreviousTime,
    FrameItem frame, ref uint started)
{
    frameQueue.Enqueue(frame);

    // ...[생략]...

    uint diff = frame.TimeStamp - started;
    if (diff >= 1000) // 쌓인 프레임의 처음과 마지막의 프레임 시간 차이가 1초 넘으면, Queue에 쌓여 있던 프레임을 파일에 fps에 맞게 기록 
    {
        uint seconds = (uint)(diff / 1000);
        MakeFramesPerTime(frameQueue, ref lastFrameOfPreviousTime, started, seconds, 1000);
        started += (seconds * 1000);
    }
}

그리고 MakeFramesPerTime은 몇 장의 이미지가 들어왔든지에 상관없이 (fps가 30인 경우) 1초에 30장의 이미지를 VideoWriter에 씁니다. 가령, 1초에 10장의 이미지가 캡처가 되었다면, 33ms마다 그 시점과 가장 가까운 이미지를 선택해 VideoWriter에 씁니다. 예를 들어, 다음과 같은 식으로 캡처 이미지는 불규칙하게 떴을 것이고,

유형 1)
선택 이미지:                *
캡처 이미지:     |   | |||| |   |    |   |||||
fps 시점:                    |

유형 2)
선택 이미지:                        *
캡처 이미지:     |                  |
fps 시점:                      |

유형 3)
선택 이미지:     *
캡처 이미지:     |               
fps 시점:                      |                   |                   |

33ms 구간이 되는 시점이 되면 유형 1의 경우 뒷 이미지가, 유형 2는 앞 이미지가 더 가깝게 캡처가 되었기 때문에 그것을 쓰게 했습니다. 유형 3은 구간 내에 캡처 이미지가 1개뿐인데, 대략 99ms 동안 연속으로 캡처를 못했기 때문에 처음 1개의 이미지를 계속해서 VideoWriter에 출력을 합니다. 그러니까, 이런 상황이 되면 동영상 재생 시 화면이 끊기는 듯한 움직임을 보이게 됩니다.

그나저나, 다른 프로그램들도 저렇게 동영상 처리를 하는지는 모르겠습니다. (혹시, 정식 fps 처리를 알고 계신 분은 덧글 부탁드립니다. ^^)




그런데, 저런 식으로 만들다 보니 의외의 문제가 하나 있는데 바로 메모리 사용량입니다. 1920 * 1080 * RGB 캡처 이미지 한 장이 6MB 정도 됩니다. 30fps로 캡처가 된다고 가정하면 1초 분량의 이미지를 쓰기 위해 Queue에 보관한 Frame의 용량이 180MB 정도 됩니다. 즉, 1초마다 180MB의 메모리가 끊임없이 할당/해제를 반복하는 것입니다.

그래서 그 부분에 대한 처리를 DisposableObjectPool을 사용해,

C# - DefaultObjectPool의 IDisposable 개체에 대한 풀링 문제
; https://www.sysnet.pe.kr/2/0/12894

부하를 조금이라도 낮춰봤습니다. 딱히 그 외에 더 설명할 것이 없군요, ^^ (소스 코드가 포함된) 첨부 파일을 다운로드/빌드/실행할 수 있습니다. (윈도우에 포커스가 있는 상태에서) Ctrl + C를 누르면 캡처를 시작하고 Ctrl + X가 눌릴 때까지 동영상으로 c:\temp\test.avi 파일에 저장합니다.




참고로, fps가 소숫점을 가질 수 있으므로 이런 경우에는 구현이 더 복잡해집니다. 이에 관해서는 다음의 글을 참고하세요.

23.976프레임과 24프레임
; https://blog.naver.com/soundpf/220510307023




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/31/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-01-05 11시45분
[나그네] 선생님의 호기심에 박수를 보냅니다. ^^
제가 가지고 있던 여러가지 욕망(?)들이 가끔이 실현이 되는것들을 보고는 그 열정에 존경심이 우러나옵니다.
[guest]

1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13667정성태7/7/20246622닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)파일 다운로드1
13666정성태7/7/20247700Linux: 74. C++ - Vsock 예제 (Hyper-V Socket 연동)파일 다운로드1
13665정성태7/6/20247880Linux: 73. Linux 측의 socat을 이용한 Hyper-V 호스트와의 vsock 테스트파일 다운로드1
13663정성태7/5/20247474닷넷: 2272. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)의 VMID Wildcards 유형파일 다운로드1
13662정성태7/4/20247491닷넷: 2271. C# - WSL 2 VM의 VM ID를 알아내는 방법 - Host Compute System API파일 다운로드1
13661정성태7/3/20247413Linux: 72. g++ - 다른 버전의 GLIBC로 소스코드 빌드
13660정성태7/3/20247522오류 유형: 912. Visual C++ - Linux 프로젝트 빌드 오류
13659정성태7/1/20247858개발 환경 구성: 715. Windows - WSL 2 환경의 Docker Desktop 네트워크
13658정성태6/28/20248235개발 환경 구성: 714. WSL 2 인스턴스와 호스트 측의 Hyper-V에 운영 중인 VM과 네트워크 연결을 하는 방법 - 두 번째 이야기
13657정성태6/27/20247911닷넷: 2270. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)을 위한 EndPoint 사용자 정의
13656정성태6/27/20248083Windows: 264. WSL 2 VM의 swap 파일 위치
13655정성태6/24/20247850닷넷: 2269. C# - Win32 Resource 포맷 해석파일 다운로드1
13654정성태6/24/20247787오류 유형: 911. shutdown - The entered computer name is not valid or remote shutdown is not supported on the target computer.
13653정성태6/22/20247937닷넷: 2268. C# 코드에서 MAKEINTREOURCE 매크로 처리
13652정성태6/21/20249246닷넷: 2267. C# - Linux 환경에서 (Reflection 없이) DLL AssemblyFileVersion 구하는 방법파일 다운로드2
13651정성태6/19/20248487닷넷: 2266. C# - (Reflection 없이) DLL AssemblyFileVersion 구하는 방법파일 다운로드1
13650정성태6/18/20248410개발 환경 구성: 713. "WSL --debug-shell"로 살펴보는 WSL 2 VM의 리눅스 환경
13649정성태6/18/20247957오류 유형: 910. windbg - !py 확장 명령어 실행 시 "failed to find python interpreter" (2)
13648정성태6/17/20248278오류 유형: 909. C# - DynamicMethod 사용 시 System.TypeAccessException
13647정성태6/16/20249344개발 환경 구성: 712. Windows - WSL 2의 네트워크 통신 방법 - 세 번째 이야기 (같은 IP를 공유하는 WSL 2 인스턴스) [1]
13646정성태6/14/20247758오류 유형: 908. Process Explorer - "Error configuring dump resources: The system cannot find the file specified."
13645정성태6/13/20248197개발 환경 구성: 711. Visual Studio로 개발 시 기본 등록하는 dev tag 이미지로 Docker Desktop k8s에서 실행하는 방법
13644정성태6/12/20248868닷넷: 2265. C# - System.Text.Json의 기본적인 (한글 등에서의) escape 처리 [1]
13643정성태6/12/20248315오류 유형: 907. MySqlConnector 사용 시 System.IO.FileLoadException 오류
13642정성태6/11/20248198스크립트: 65. 파이썬 - asgi 버전(2, 3)에 따라 달라지는 uvicorn 호스팅
13641정성태6/11/20248673Linux: 71. Ubuntu 20.04를 22.04로 업데이트
1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...