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]

... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12279정성태7/29/202012638개발 환경 구성: 499. 닷넷에서 접근해보는 InterSystems의 Cache 데이터베이스파일 다운로드1
12278정성태7/23/20209834VS.NET IDE: 149. ("Binary was not built with debug information" 상태로) 소스 코드 디버깅이 안되는 경우
12277정성태7/23/202011402개발 환경 구성: 498. DEVPATH 환경 변수의 사용 예 - .NET Reflector의 (PDB 연결이 없는) DLL의 소스 코드 디버깅
12276정성태7/23/202010739.NET Framework: 930. 개발자를 위한 닷넷 어셈블리 바인딩 - DEVPATH 환경 변수
12275정성태7/22/202013182개발 환경 구성: 497. 닷넷에서 접근해보는 InterSystems의 IRIS Data Platform 데이터베이스파일 다운로드1
12274정성태7/21/202012585개발 환경 구성: 496. Azure - Blob Storage Account의 Location 이전 방법 [1]파일 다운로드1
12273정성태7/18/202014365개발 환경 구성: 495. Azure - Location이 다른 웹/DB 서버의 경우 발생하는 성능 하락
12272정성태7/16/20209158.NET Framework: 929. (StrongName의 버전 구분이 필요 없는) .NET Core 어셈블리 바인딩 규칙 [2]파일 다운로드1
12271정성태7/16/202011279.NET Framework: 928. .NET Framework의 Strong-named 어셈블리 바인딩 (2) - 런타임에 바인딩 리디렉션파일 다운로드1
12270정성태7/16/202012063오류 유형: 633. SSL_CTX_use_certificate_file - error:140AB18F:SSL routines:SSL_CTX_use_certificate:ee key too small
12269정성태7/16/20209435오류 유형: 632. .NET Core 웹 응용 프로그램 - The process was terminated due to an unhandled exception.
12268정성태7/15/202011294오류 유형: 631. .NET Core 웹 응용 프로그램 오류 - HTTP Error 500.35 - ANCM Multiple In-Process Applications in same Process
12267정성태7/15/202012864.NET Framework: 927. C# - 윈도우 프로그램에서 Credential Manager를 이용한 보안 정보 저장파일 다운로드1
12266정성태7/14/202010579오류 유형: 630. 사용자 계정을 지정해 CreateService API로 서비스를 등록한 경우 "Error 1069: The service did not start due to a logon failure." 오류발생
12265정성태7/10/20209686오류 유형: 629. Visual Studio - 웹 애플리케이션 실행 시 "Unable to connect to web server 'IIS Express'." 오류 발생
12264정성태7/9/202018800오류 유형: 628. docker: Error response from daemon: Conflict. The container name "..." is already in use by container "...".
12261정성태7/9/202011827VS.NET IDE: 148. 윈도우 10에서 .NET Core 응용 프로그램을 리눅스 환경에서 실행하는 2가지 방법 - docker, WSL 2 [5]
12260정성태7/8/202010054.NET Framework: 926. C# - ETW를 이용한 ThreadPool 스레드 감시파일 다운로드1
12259정성태7/8/20209639오류 유형: 627. nvlddmkm.sys의 BAD_POOL_HEADER BSOD 문제 [1]
12258정성태7/8/202012811기타: 77. DataDog APM 간략 소개
12257정성태7/7/20209797.NET Framework: 925. C# - ETW를 이용한 Monitor Enter/Exit 감시파일 다운로드1
12256정성태7/7/202010219.NET Framework: 924. C# - Reflection으로 변경할 수 없는 readonly 정적 필드 [4]
12255정성태7/6/202010730.NET Framework: 923. C# - ETW(Event Tracing for Windows)를 이용한 Finalizer 실행 감시파일 다운로드1
12254정성태7/2/202010502오류 유형: 626. git - REMOTE HOST IDENTIFICATION HAS CHANGED!
12253정성태7/2/202011762.NET Framework: 922. C# - .NET ThreadPool의 Local/Global Queue파일 다운로드1
12252정성태7/2/202013654.NET Framework: 921. C# - I/O 스레드를 사용한 비동기 소켓 서버/클라이언트파일 다운로드2
... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...