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]

... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1856정성태2/15/201521264.NET Framework: 493. TypeRef 메타테이블에 등록되는 타입의 조건파일 다운로드1
1855정성태2/10/201520803개발 환경 구성: 256. WebDAV Redirector - Sysinternals 폴더 연결 시 "The network path was not found" 오류 해결 방법
1854정성태2/10/201521800Windows: 104. 폴더는 삭제할 수 없지만, 그 하위 폴더/파일은 생성/삭제/변경하는 보안 설정
1853정성태2/6/201552060웹: 29. 여신금융협회 웹 사이트의 "Netscape 6.0은 지원하지 않습니다." 오류 메시지 [5]
1852정성태2/5/201522486.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미파일 다운로드1
1851정성태2/5/201523401VC++: 88. 하룻밤의 꿈 - 인텔 하스웰의 TSX Instruction 지원 [2]
1850정성태2/4/201544264Windows: 103. 작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미 [4]
1849정성태2/4/201524189기타: 51. DropBox의 CPU 100% 현상 [1]파일 다운로드1
1848정성태2/4/201519432.NET Framework: 491. 닷넷 Generic 타입의 메타 데이터 토큰 값 알아내는 방법 [2]
1847정성태2/3/201522742기타: 50. C# - 윈도우에서 dropbox 동기화 폴더 경로 및 종료하는 방법
1846정성태2/2/201532016Windows: 102. 제어판의 프로그램 추가/삭제 항목을 수동으로 실행하고 싶다면? [1]
1845정성태1/26/201532900Windows: 101. 제어판의 "Windows 자격 증명 관리(Manage your credentials)"를 금지시키는 방법
1844정성태1/26/201530856오류 유형: 269. USB 메모리의 용량이 비정상적으로 보여진다면? [7]
1843정성태1/24/201521917VC++: 87. 무시할 수 없는 Visual C++ 런타임 함수 성능
1842정성태1/23/201544426개발 환경 구성: 255. 노트북 키보드에 없는 BREAK 키를 다른 키로 대체하는 방법
1841정성태1/21/201519401오류 유형: 268. Win32 핸들 관련 CLR4 보안 오류 사례
1840정성태1/8/201527618오류 유형: 267. Visual Studio - CodeLens 사용 시 CPU 100% 현상
1839정성태1/5/201520531디버깅 기술: 69. windbg 분석 사례 - cpu 100% 현상 (2)
1838정성태1/4/201540236기타: 49. 윈도우 내레이터(Narrator) 기능 끄는 방법(윈도우에 파란색의 굵은 테두리 선이 나타난다면?) [4]
1837정성태1/4/201526353디버깅 기술: 68. windbg 분석 사례 - 메모리 부족 [1]
1836정성태1/4/201526357디버깅 기술: 67. windbg - 덤프 파일과 handle 정보
1835정성태1/3/201526853개발 환경 구성: 254. SQL 서버 역시 SSL 3.0/TLS 1.0만을 지원하는 듯!
1834정성태1/3/201551481개발 환경 구성: 253. TLS 1.2를 적용한 IIS 웹 사이트 구성
1833정성태1/3/201527560.NET Framework: 490. System.Data.SqlClient는 SSL 3.0/TLS 1.0만 지원하는 듯! [3]
1832정성태1/2/201520649오류 유형: 266. Azure에 응용 프로그램 게시 중 로그인 오류
1831정성태1/1/201528531디버깅 기술: 66. windbg 분석 사례 - cpu 100% 현상 (1) [1]
... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...