성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[tree soap] 아차! f는 기억이 나는데, m은 ㅜㅜ 감사합니다!!! ^...
[정성태] 'm'은 decimal 타입의 숫자에 붙는 접미사입니다. ...
[정성태] https://lxr.sourceforge.io/ http...
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
글쓰기
제목
이름
암호
전자우편
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# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩</h1> <p> 기본적인 디코딩 절차를 마련했으니,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12956'>https://www.sysnet.pe.kr/2/0/12956</a> </pre> <br /> 이제 packet과 frame의 기본 정보를 출력해볼까요?<br /> <br /> 우선, 비디오의 경우 가장 중요한 것은 프레임이 갖고 있는 이미지의 width와 height입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > switch (mediaType) { case AVMediaType.AVMEDIA_TYPE_VIDEO: // 비디오 frame 처리 Console.Write($"V {frame->width} x {frame->height}"); Console.WriteLine(); break; case AVMediaType.AVMEDIA_TYPE_AUDIO: // 오디오 frame 처리 break; } </pre> <br /> 그리고 이미지의 포맷일 텐데요, ffmpeg.AutoGen에서 format은 int 타입이기 때문에 다음과 같이 형변환을 하면 좀 더 사용자에게 친숙한 문자열을 얻을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Console.Write($", {(AVPixelFormat)frame->format}"); </pre> <br /> 여기까지 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12947'>예제 동영상으로 준비했던 것</a>을 대상으로 실행해 보면 다음과 같은 출력이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > V 1920 x 1080, AV_PIX_FMT_YUV420P V 1920 x 1080, AV_PIX_FMT_YUV420P ...[생략]... </pre> <br /> AV_PIX_FMT_YUV420P를 이제 좀 더 깊게 들어가 볼까요? ^^ 소스 코드 내 주석을 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples) AV_PIX_FMT_YUV420P = 0, </pre> <br /> YUV420은 아래 문서의 설명에 따라,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Recommended 8-Bit YUV Formats for Video Rendering ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering'>https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering</a> </pre> <br /> [그림 출처: <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering'>Recommended 8-Bit YUV Formats for Video Rendering</a>]<br /> <img alt='yuv-sampling-grids.png' src='/SysWebRes/bbs/yuv-sampling-grids.png' /><br /> <br /> Luma 채널(Y) 값 4개에 대해 Chroma 채널(U(Cb), V(Cr)) 1개가 할당된 방식입니다.<br /> <br /> 즉, 화면에 다음과 같이 픽셀 8개가 있을 때,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > x x x x x x x x </pre> <br /> 1바이트의 Y 값은 각각의 픽셀마다 값을 가지고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > y y y y y y y y </pre> <br /> 그다음 개별적으로 1바이트의 u, 1바이트의 v 값이 4개의 점에 대해 공유하는 방식입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > y y y y u,v u,v y y y y </pre> <br /> 따라서 개별 pixel에 대한 u, v 값이 아닌 4개의 점에 대한 공유 u, v를 가지기 때문에 약간의 손실이 발생합니다. 반면, YUV444인 경우라면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples) AV_PIX_FMT_YUV444P = 5, </pre> <br /> 각각의 픽셀에 대해 u, v가 동일하게 유지하는 방식으로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > (y,u,v) (y,u,v) (y,u,v) (y,u,v) (y,u,v) (y,u,v) (y,u,v) (y,u,v) </pre> <br /> 손실이 없는 방식입니다.<br /> <br /> 일단 YUV에 대한 의미는 그렇고, 그다음 "P" 접미사에 대한 의미는 planar를 의미하며 없으면 packed 방식을 의미합니다. planar인 경우, y, u, v에 대한 데이터를 따로 저장하는데요, ffmpeg에서는 frame의 data 포인터 배열에 각각 다음과 같이 나눠서 저장을 하게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > data[0] = y 데이터 포인터 data[1] = u 데이터 포인터 data[2] = v 데이터 포인터 </pre> <br /> 따라서 AV_PIX_FMT_YUV420P라고 하면, <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 예제 동영상의 경우 width == 1920, height == 1080이므로, data[0] = 1920 * 1080 바이트의 y 데이터를 가리키는 포인터 data[1] = (1920 / 2) * (1080 / 2) 바이트의 u 데이터를 가리키는 포인터 data[2] = (1920 / 2) * (1080 / 2) 바이트의 v 데이터를 가리키는 포인터 </pre> <br /> 라고 해석이 됩니다. 그리고 AVFrame은 y, u, v의 데이터를 담고 있는 data[0], data[1], data[2]의 각각의 stride 크기를 linesize에 담고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > linesize[0]: data[0]이 담고 있는 데이터가 하나의 라인에 해당하는 바이트 수 linesize[1]: data[1]이 담고 있는 데이터가 하나의 라인에 해당하는 바이트 수 linesize[2]: data[2]이 담고 있는 데이터가 하나의 라인에 해당하는 바이트 수 </pre> <br /> 즉 예제 데이터의 경우에는 linesize[0] == 1920, linesize[1] == linesize[2] == 960 값을 담게 됩니다.<br /> <br /> 혹시 평면 형식의 데이터가 연속으로 y, u, v가 할당돼 있는 구조일까요? 디버그로 포인터를 출력해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > byte* yData = frame->data[0]; byte* uData = frame->data[1]; byte* vData = frame->data[2]; // uData == 0x00000231cb4146c0 // vData == 0x00000231cb8c0f00 (v - u = 4,900,928) // yData == 0x00000231cc89b080 (y - v = 16,621,952) </pre> <br /> 메모리 순서 상으로 u, v, y이고, uData와 vData의 offset을 계산했을 때, 예상되는 uData의 크기가 (1920/2) * (1080/2) = 518,400 바이트인 것을 감안하면 딱히 y, u, v 모두 연속적인 공간에 할당된 것으로 보이지는 않습니다. (이상하군요, ^^; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering#420-formats-12-bits-per-pixel'>4:2:0 Formats, 12 Bits per Pixel</a> 글을 보면 연속 공간에 할당한 것으로 나오는데... 게다가 메모리 순서도 y, v, u 또는 y, v, u라고 나와야 하는데, 포인터 상으로는 u, v, y로 나오고 있습니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 만약 P가 없다면 (planar가 아닌) packed 형식으로 데이터가 일렬로 저장된 것을 의미합니다. 예를 들어, AV_PIX_FMT_YUYV422라고 하면, <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // // Summary: // <span style='color: blue; font-weight: bold'>packed</span> YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr AV_PIX_FMT_YUYV422 = 1, </pre> <br /> 도움말에도 나오는 것처럼, (이전처럼 8개의 픽셀을 가정하면) 다음과 같이 표현하게 되고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > (y u y v) (y u y v) (y u y v) (y u y v) </pre> <br /> 이 데이터를 그대로 data[0]에 포함하게 될 듯합니다. 하지만 이것을 테스트할 수 있는 방법이 없습니다. 왜냐하면 아래의 글에서 설명했듯이,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ffmpeg.exe를 이용해 planar 포맷의 데이터를 packed 형식으로 변환하는 방법? ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12957'>https://www.sysnet.pe.kr/2/0/12957</a> </pre> <br /> packed 포맷으로 저장하는 동영상 파일을 만들 수 없기 때문입니다. <br /> <br /> 하지만, libx264 코덱의 경우 "yuv420p yuvj420p yuv422p yuvj422p yuv444p yuvj444p nv12 nv16 nv21 yuv420p10le yuv422p10le yuv444p10le nv20le gray gray10le" 같은 다양한 포맷을 지원하기 때문에 frame을 디코딩할 때 감안해야 할 것입니다.<br /> <br /> 즉, yuv444p인 경우라면 linesize의 [0], [1], [2]에는 (1920 x 1080 동영상인 경우) 모두 1920 값을 가지게 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 마지막으로, YUV 데이터를 RGB 포맷으로 바꿔 윈도우 화면 상에 출력하고 싶을 텐데요, 이게 좀 문제입니다. yuv를 rgb로 해석하는 방법이 설명하는 곳마다 모두 제각각입니다. ^^;<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;' > // Recommended 8-Bit YUV Formats for Video Rendering // ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering'>https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering</a> // c++ - How to convert RGB -> YUV -> RGB (both ways) // ; <a target='tab' href='https://jike.in/?qa=712652/c-how-to-convert-rgb-yuv-rgb-both-ways'>https://jike.in/?qa=712652/c-how-to-convert-rgb-yuv-rgb-both-ways</a> // C - yuv422와 yuv420파일을 RGB Foramt으로 변환하기 // <a target='tab' href='https://junsk1016.github.io/c/YUV422&420-to-Raw/'>https://junsk1016.github.io/c/YUV422&420-to-Raw/</a> C = Y - 16 D = U - 128 E = V - 128 R = clip( round( 1.164383 * C + 1.596027 * E ) ) G = clip( round( 1.164383 * C - (0.391762 * D) - (0.812968 * E) ) ) B = clip( round( 1.164383 * C + 2.017232 * D ) ) </pre> <br /> 제가 가진 "<a target='tab' href='http://www.yes24.com/Product/Goods/24310863'>Visual C++ 영상 처리 프로그래밍</a>" 책에서는 이렇게 제시합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > R = Y + 1.4075 * (V - 128) G = Y - 0.3455 * (U - 128) - 0.7169 * (V - 128) B = Y + 1.7790 * (U - 128) </pre> <br /> 마이크로소프트의 경우 Y 값에서 16을 뺀 것도 위의 식에서는 없습니다. 게다가 각각의 요소에 곱하는 실수도 값이 다릅니다. 그리고 이렇게 해석하는 글도 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 3-2.이미지 출력 // ; <a target='tab' href='http://soen.kr/lecture/library/FFmpeg/3-2.htm'>http://soen.kr/lecture/library/FFmpeg/3-2.htm</a> r = int(Y + 1.3707 * (V - 128)); g = int(Y - 0.6980 * (U - 128) - 0.3376 * (V - 128)); b = int(Y + 1.7324 * (U - 128)); </pre> <br /> 찾아보면 또 얼마나 다른 값들이 나올지... ^^; <br /> <br /> 이런 다양한 방식에 혼란스럽다면, 그냥 ffmpeg에 맡기는 것도 좋겠습니다. 이를 위해 SwsContext를 사용하는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // YUV <-> RGB by ffmpeg // ; <a target='tab' href='https://gist.github.com/nakaly/11eb992ebd134ee08b75e4c67afb5703'>https://gist.github.com/nakaly/11eb992ebd134ee08b75e4c67afb5703</a> // Convert from YUV to RGB struct SwsContext *sws_ctx = NULL; sws_ctx = sws_getContext(inlink->w, inlink->h, inlink->format, outlink->w, outlink->h, AV_PIX_FMT_RGB24, SWS_BILINEAR,NULL,NULL,NULL); uint8_t *prgb24 = calloc(3 * inlink->w * inlink->h, sizeof(uint8_t)); uint8_t *rgb24[1] = { prgb24 }; int rgb24_stride[1] = { 3 * inlink->w }; sws_scale(sws_ctx, in->data, in->linesize, 0, inlink->h, rgb24, rgb24_stride); </pre> <br /> 사실 현실적으로 보면 당연히 sws_scale을 이용해 처리하는 것이 좋습니다. 왜냐하면 <a target='tab' href='https://www.sysnet.pe.kr/2/0/11423'>성능상으로도 C#에서 루프로 처리하는 것보다 C/C++ 내부에서 최적화한 코드를 이용한 것이 더 좋을 것</a>이기 때문입니다.<br /> <br /> <hr style='width: 50%' /><br /> <a name='yuv2rgb'></a> <br /> 그래도 여기서는 공부 차원에서 하는 것이기 때문에 sws_scale을 사용하지 않고 3가지 yuv-to-rgb 코드를 테스트해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > switch (mediaType) { case AVMediaType.AVMEDIA_TYPE_VIDEO: byte* yData = frame->data[0]; byte* uData = frame->data[1]; byte* vData = frame->data[2]; int yStride = frame->linesize[0]; int uStride = frame->linesize[1]; int vStride = frame->linesize[2]; byte Y, U, V; int r, g, b; if ((AVPixelFormat)frame->format == AVPixelFormat.AV_PIX_FMT_YUV420P && frame->coded_picture_number == 300) { Bitmap bitmap = new Bitmap(frame->width, frame->height, PixelFormat.Format24bppRgb); for (int y = 0; y < frame->height; y++) { for (int x = 0; x < frame->width; x++) { Y = yData[yStride * y + x]; U = uData[uStride * (y / 2) + x / 2]; V = vData[vStride * (y / 2) + x / 2]; <span style='color: blue; font-weight: bold'>...[YUV 2 RGB]...(Y, U, V, out r, out g, out b);</span> Color pixel = Color.FromArgb(0, r, g, b); bitmap.SetPixel(x, y, pixel); } } bitmap.Save(@"d:\temp\test.bmp"); } break; case AVMediaType.AVMEDIA_TYPE_AUDIO: // 오디오 frame 처리 break; } </pre> <br /> 즉, 위의 ...[YUV 2 RGB]... 영역만 다음의 3가지 메서드로 테스트해보고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // Recommended 8-Bit YUV Formats for Video Rendering // <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering'>https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering</a> private static void YUV2RGB_ByMS(int y, int u, int v, out int r, out int g, out int b) { y -= 16; u -= 128; v -= 128; r = (int)(1.164383 * y + 1.596027 * v); g = (int)(1.164383 * y - 0.391762 * u - 0.812968 * v); b = (int)(1.164383 * y + 2.017232 * u); r = Math.Max(0, Math.Min(255, r)); g = Math.Max(0, Math.Min(255, g)); b = Math.Max(0, Math.Min(255, b)); } // 3-2.이미지 출력 // ; http://soen.kr/lecture/library/FFmpeg/3-2.htm private static void YUV2RGB_BySoen(byte y, byte u, byte v, out int r, out int g, out int b) { r = (int)(y + 1.3707 * (v - 128)); g = (int)(y - 0.6980 * (u - 128) - 0.3376 * (v - 128)); b = (int)(y + 1.7324 * (u - 128)); r = Math.Max(0, Math.Min(255, r)); g = Math.Max(0, Math.Min(255, g)); b = Math.Max(0, Math.Min(255, b)); } // Visual C++ 영상 처리 프로그래밍 // http://www.yes24.com/Product/Goods/24310863 private static void YUV2RGB_ByBook(byte y, byte u, byte v, out int r, out int g, out int b) { r = (int)(y + 1.4075 * (v - 128)); g = (int)(y - 0.3455 * (u - 128) - 0.7169 * (v - 128)); b = (int)(y + 1.7790 * (u - 128)); r = Math.Max(0, Math.Min(255, r)); g = Math.Max(0, Math.Min(255, g)); b = Math.Max(0, Math.Min(255, b)); } </pre> <br /> 출력된 이미지를 보면, 이렇게 나옵니다.<br /> <br /> (사진은 <a target='tab' href='https://www.youtube.com/watch?v=f-sxdm4QIvc'>유튜브 영상 "디에이드"의 "안다은" 님</a>이고 사용을 허락받고 올립니다.)<br /> <img onclick='toggle_img(this)' class='imgView' alt='decode_yuv_to_rgb_1.png' src='/SysWebRes/bbs/decode_yuv_to_rgb_1.png' /><br /> <br /> 보는 바와 같이, 어떤 yuv2rgb를 사용해도 영상의 차이점을 잘 모르겠습니다. ^^;<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1897&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
8698
(왼쪽의 숫자를 입력해야 합니다.)