성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - snagit처럼 화면 캡처를 연속으로 수행해 동영상 제작</h1> <p> 화면 캡처 프로그램인 snagit을 써보시면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > snagit ; <a target='tab' href='https://www.techsmith.com/store/snagit'>https://www.techsmith.com/store/snagit</a> </pre> <br /> 원하는 범위에서 캡처를 동영상으로 만드는 기능도 제공합니다. 마침(?) 오래전의 질문도 있으니,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > c# 으로 화면캡쳐해서 동영상으로 만드는앱을 만들고싶습니다 ; <a target='tab' href='https://www.sysnet.pe.kr/3/0/5037'>https://www.sysnet.pe.kr/3/0/5037</a> </pre> <br /> 이참에 지난번에 만들어 둔 코드에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12889'>https://www.sysnet.pe.kr/2/0/12889</a> </pre> <br /> 화면 녹화 기능을 넣어보겠습니다. ^^ 사실, 이에 대한 구현은 아래의 2개 내용을 합치기만 하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12889'>https://www.sysnet.pe.kr/2/0/12889</a> C# - OpenCvSharp.VideoWriter에 BMP 파일을 1초씩 출력하는 예제 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12485'>https://www.sysnet.pe.kr/2/0/12485</a> </pre> <br /> "<a target='tab' href='https://www.sysnet.pe.kr/2/0/12889'>C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법</a>" 글에서 이미 화면 캡처 이미지를 Mat으로 반환받았으니, "<a target='tab' href='https://www.sysnet.pe.kr/2/0/12485'>C# - OpenCvSharp.VideoWriter에 BMP 파일을 1초씩 출력하는 예제</a>"에서 소개한 VideoWriter로는 단순히 원하는 fps에 맞게 쓰기 작업을 해주면 되는 것입니다.<br /> <br /> 즉, 대충 다음과 같은 식으로 하면 구색은 갖춥니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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); } } } } } </pre> <br /> 그런데 실제로 위의 코드를 실행해 보면, 당연히 avi 파일에 지정한 30fps, 즉 초당 30개의 이미지를 써야 동영상 재생 프로그램이 현실 시간에 따른 속도로 화면을 보여줄 텐데, 위의 코드에서는 manager.Capture가 반환하는 속도에 의존해 초당 쓰게 될 이미지의 수가 결정되므로 정상적인 속도로 화면 레코딩이 되지 않습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이 문제를 해결하기 위해 우선 Capture의 동작을 이해해야 합니다. <a target='tab' href='https://www.sysnet.pe.kr/2/0/11385#acquire_next_frame'>지난 글에도 설명했지만, (Capture 내에서 사용하는) AcquireNextFrame</a>은 캡처 대상이 되는 모니터의 화면에 변화가 있어야 그 이미지를 반환합니다.<br /> <br /> 따라서, 단순히 CaptureLoopMat에서와 같은 코드를 작성하면 화면 변경이 거의 없는 경우에는 30fps가 요구하는 이미지 수를 못 채울 것이고, Youtube와 같은 사이트를 방문해 동영상을 재생하면 30fps에 가까운 이미지가 VideoWriter에 추가될 수 있습니다. 혹은, 30fps보다 더 빠르게 Capture가 된다면 재생 시에 슬로우 모션이 나옵니다.<br /> <br /> 결국, Capture 루프에서 몇 장의 이미지가 나오든지에 상관없이 우리는 fps에 맞는 프레임을 VideoWriter에 써야 합니다.<br /> <br /> 그런데, fps가 참 재미있습니다. 가령 30fps라고 하면 1초에 30장의 이미지를 써야 하는데요, 그렇다면 1000/30 = 33.3333... 과 같이 정수로 딱 떨어지지 않는 시간이 나옵니다. 그래서 만약 33ms마다 1장씩 VideoWriter에 쓰게 된다면 0.3.... 의 시간이 절삭되고, 그것이 누적되면서 나중에는 초 단위까지 영향을 줄 수 있습니다.<br /> <br /> 제가 영상 관련은 해본 적이 없어서 이런 소수점 이하의 처리를 현업에서는 어떻게 하고 있는지 모르겠습니다. ^^; 그래서 그냥 제 맘대로 만들었습니다. 일단, Capture한 영상을 VideoWriter에 곧바로 쓰지 않고 FPS에 맞는 분량으로, 예를 들어 30fps라면 무조건 1초마다 30장의 이미지를 쓰도록 중간 처리를 하는 FpsWriter를 두었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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)) <span style='color: blue; font-weight: bold'>using (FpsWriter matBuffer = new FpsWriter(writer, MatType.CV_8UC3))</span> { while (_exitCapture == false) { long started = sw.ElapsedMilliseconds; <span style='color: blue; font-weight: bold'>if (manager.Capture(frame.Data, 1000) == true) { Mat current = matBuffer.GetMat(); Cv2.CvtColor(frame, current, ColorConversionCodes.BGRA2BGR); matBuffer.Write(current); }</span> int elapsed = (int)(sw.ElapsedMilliseconds - started); matBuffer.TrySleep(elapsed); } } } </pre> <br /> FpsWriter.Write에서는 캡처 루프에 영향을 주지 않도록 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12845'>BufferBlock<T></a>를 이용해 이미지 처리 스레드를 별도로 두어 전달하는 식입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public void Write(Mat current) { _lastCaptured = new FrameItem { TimeStamp = timeGetTime(), Mat = current }; <span style='color: blue; font-weight: bold'>_queue.SendAsync(_lastCaptured);</span> // BufferBlock에 담기만 하고 메서드를 벗어남. } </pre> <br /> 실제적인 처리는 비동기 처리를 위한 루프에서 담당하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private async Task ProcessFrame() { // ...[생략]... while (true) { <span style='color: blue; font-weight: bold'>FrameItem frame = await _queue.ReceiveAsync();</span> if (frame.TimeStamp == 0) { break; } // ...[생략]... AddFrame(...[생략]...); } } </pre> <br /> AddFrame 내에서는 1초마다, 그동안 쌓인 이미지 데이터를 VideoWriter에 쓰는 작업을 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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); } } </pre> <br /> 그리고 MakeFramesPerTime은 몇 장의 이미지가 들어왔든지에 상관없이 (fps가 30인 경우) 1초에 30장의 이미지를 VideoWriter에 씁니다. 가령, 1초에 10장의 이미지가 캡처가 되었다면, 33ms마다 그 시점과 가장 가까운 이미지를 선택해 VideoWriter에 씁니다. 예를 들어, 다음과 같은 식으로 캡처 이미지는 불규칙하게 떴을 것이고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 유형 1) 선택 이미지: * 캡처 이미지: | | |||| <span style='color: blue; font-weight: bold'>|</span> | | ||||| fps 시점: | 유형 2) 선택 이미지: * 캡처 이미지: | <span style='color: blue; font-weight: bold'>|</span> fps 시점: | 유형 3) 선택 이미지: * 캡처 이미지: <span style='color: blue; font-weight: bold'>|</span> fps 시점: | | | </pre> <br /> 33ms 구간이 되는 시점이 되면 유형 1의 경우 뒷 이미지가, 유형 2는 앞 이미지가 더 가깝게 캡처가 되었기 때문에 그것을 쓰게 했습니다. 유형 3은 구간 내에 캡처 이미지가 1개뿐인데, 대략 99ms 동안 연속으로 캡처를 못했기 때문에 처음 1개의 이미지를 계속해서 VideoWriter에 출력을 합니다. 그러니까, 이런 상황이 되면 동영상 재생 시 화면이 끊기는 듯한 움직임을 보이게 됩니다.<br /> <br /> 그나저나, 다른 프로그램들도 저렇게 동영상 처리를 하는지는 모르겠습니다. (혹시, 정식 fps 처리를 알고 계신 분은 덧글 부탁드립니다. ^^)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 저런 식으로 만들다 보니 의외의 문제가 하나 있는데 바로 메모리 사용량입니다. 1920 * 1080 * RGB 캡처 이미지 한 장이 6MB 정도 됩니다. 30fps로 캡처가 된다고 가정하면 1초 분량의 이미지를 쓰기 위해 Queue에 보관한 Frame의 용량이 180MB 정도 됩니다. 즉, 1초마다 180MB의 메모리가 끊임없이 할당/해제를 반복하는 것입니다.<br /> <br /> 그래서 그 부분에 대한 처리를 DisposableObjectPool을 사용해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - DefaultObjectPool<T>의 IDisposable 개체에 대한 풀링 문제 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12894'>https://www.sysnet.pe.kr/2/0/12894</a> </pre> <br /> 부하를 조금이라도 낮춰봤습니다. 딱히 그 외에 더 설명할 것이 없군요, ^^ <a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1878&boardid=331301885'>(소스 코드가 포함된) 첨부 파일을 다운로드/빌드/실행할 수 있습니다.</a> (윈도우에 포커스가 있는 상태에서) Ctrl + C를 누르면 캡처를 시작하고 Ctrl + X가 눌릴 때까지 동영상으로 c:\temp\test.avi 파일에 저장합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, fps가 소숫점을 가질 수 있으므로 이런 경우에는 구현이 더 복잡해집니다. 이에 관해서는 다음의 글을 참고하세요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 23.976프레임과 24프레임 ; <a target='tab' href='https://blog.naver.com/soundpf/220510307023'>https://blog.naver.com/soundpf/220510307023</a> </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
3504
(왼쪽의 숫자를 입력해야 합니다.)