사진으로 동영상 만드는 앱을 만들다 보니 이해할 수 없는 코드들을 너무 많이 만나게 되었다.
CVPixcelBuffer, CGBitmapContext 등등..
그리고 2016.5.11 현재 내가 만든 앱은 기능적으로는 정상 작동하지만 동영상에 사용되는 각각의 프레임이 검은 화면으로 나오는 상황에 처했으며 이를 해결할 수 있는 능력이 나에게는 없다.
그래서 이러한 이미지 처리를 이해하고 그 후 해결해야겠다는 생각이 들었다.
그러다가 다음과 같은 좋은 글을 발견하고 이를 간략하게 번역하여 기록하고자 한다.
출처: https://www.raywenderlich.com/69855/image-processing-in-ios-part-1-raw-bitmap-modification
spooky image filter라는 샘플 앱을 이용하여 설명을 할 예정이며 다음과 같은 메소드를 사용할 것이다.
- Raw bitmap modification
- Using the Core Graphics Library
- Using the Core Image Library
- Using the 3rd-party GPUImage library
What’s an Image?
이미지란 pixel의 집합이다. 각 pixel은 특정 색상을 가지고 있는 점이며 이 점들이 2차원 배열로 나열된 것이 바로 이미지이다.
How are Colors Represented in Bytes?
여러가지 방법이 있지만 가장 보편적인 방법은 32비트 RGBA로 표현하는 것이다. 32비트는 4바이트이고 RGB는 다들 알다시피 레드, 그린, 블루 3원색이다. 마지막 A는 알파 채널인데 투명도를 의미한다. 이 투명도가 의미있으려면 당연하게도 이미지 뒤에 뭔가 색이 존재하여야 할 것이다.
개인적으로 만드는 앱에 pixelBufferFromCGImage라는 좀 유명한 메소드가 사용되는데(공식 메소드는 아님) 이 메소드 내에서 색상 관련하여 호출되는CVPixelBufferCreate 메소드의 PixelFormatType 파라미터에서 kCVPixelFormatType_32ARGB라는 상수를 사용하는데 바로 이것을 의미함을 이해할 수 있다.
Color Spaces
위 RGB메소드가 컬러 공간에 대한 한 예이다. 컬러 공간이라고 하면 막상 단어가 좀 생소하여 이해가 잘 안 되는데 아래 그림을 보면 어느정도 이해가 될 것이다.
왼쪽 그림은 RGB로 표현된 컬러 공간이고 오른쪽은 HSV로 표현된 컬러 공간을 의미한다.
HSV는 다음과 같다.
- Hue as “Color”
- Saturation as “How full is this color”
- Value, as the “Brightness” - 밝기는 RGB 평균의 높고 낮은 정도라고 생각하면 된다.
이 역시 내 개인 앱 pixelBufferFromCGImage 메소드에서 사용되는데 CGContextCreate 메소드의 6번째 인자를 위해 호출되는 CGColorSpaceCreateDeviceRGB()를 이해하는 좋은 자료가 되었다.
Coordinate Systems
좌표계이다. 픽셀의 위치를 결정할 때 사용되는데 이미지 박스를 볼 때 0,0이 왼쪽 위에 있는 좌표계와 0,0이 왼쪽 아래에 위치한 좌표계 두 가지가 있으며 XCode에서는 둘 다 지원한다고 한다.
일반적으로UIImage나 UIView는 왼쪽 위가 0,0이고 Core Image나 Core Graphics는 왼쪽 아래가 0,0이다.
간혹 앱 개발 했는데 그림이 위아래로 뒤집어져있다면 좌표계를 의심해보아야 한다.
Image Compression
이미지의 기본 사이즈는 생각보다 크다.
예를 들어 8백만 화소짜리 이미지라면 800만 픽셀에 픽셀당 4바이트가 필요하므로 32메가바이트의 저장공간이 필요하다는 계산이 나온다.
JPG(JPEG), PNG 자체가 다 이미지 압축 포맷 이름이다.
CGContextCreate 메소드의 5번째 인자인 bytesPerRow에 값을 넣을 때 일반적으로 4 * (image.width)를 사용하는데 이 이유가 다 픽셀당 4바이트를 사용하기 때문이다. 앱을 개발하며 궁금한 것들이 이렇게 이해가 되니 더 재미있어 진다.
Looking at Pixels
이제부터 코드 분석이다. 샘플 코드는 여기서 다운!
샘플 코드를 실행해보면 앱이 실행되자마자 꼬마 귀신 이미지가 세팅되고 XCode의 output 창에도 아래 그림과 같은 재미있는 출력물이 나온다.
처음 나오는 꼬마 귀신 이미지에서 귀신 외부의 색깔은 brightness가 0이다. 즉 검은색이나 다름없다. 하지만 알파 채널 값 역시 0이라서 완전 투명하기 때문이 검은색으로 보이지는 않는다.
XCode에서 imageView의 백그라운드 컬러를 레드로 바꾸면 귀신 이미지 외부도 빨갛게 나오는 것을 볼 수 있을 것이다.
이제 코드를 보자.
이렇게 앱이 시작하자마자 꼬마 귀신 이미지를 보여주는 것은 ViewController.m파일의 setupWithImage 메소드와 logPixelsOfImage 메소드 때문이다.
// 1. CGImageRef inputCGImage = [image CGImage]; NSUInteger width = CGImageGetWidth(inputCGImage); NSUInteger height = CGImageGetHeight(inputCGImage); // 2. NSUInteger bytesPerPixel = 4; NSUInteger bytesPerRow = bytesPerPixel * width; NSUInteger bitsPerComponent = 8; UInt32 * pixels; pixels = (UInt32 *) calloc(height * width, sizeof(UInt32)); // 3. CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(pixels, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); // 4. CGContextDrawImage(context, CGRectMake(0, 0, width, height), inputCGImage); // 5. Cleanup CGColorSpaceRelease(colorSpace); CGContextRelease(context);
주석별로 살펴 보면 원문은 다음과 같이 설명하고 있다.
- Section 1: Convert the UIImage to a CGImage object, which is needed for the Core Graphics calls. Also, get the image’s width and height.
Core Graphics에서 사용하려면 UIImage를 CGImage로 변환해야 한다는 내용 - Section 2: For the 32-bit RGBA color space you’re working in, you hardcode the parameters bytesPerPixel and bitsPerComponent, then calculate bytesPerRow of the image. Finally, you allocate an array pixels to store the pixel data.
위에서 살펴보았던 내용이다. bytePerPixel을 왜 4로 설정하는지, bytePerRow를 왜 이렇게 계산하는지는 이제 이해할 수 있을 것이다.
허나 bitsPerComponent를 왜 8로 지정하는지는 이해하지 못 하겠다. 정확히는 Component자체가 뭐하는 놈인지 모르겠다. 아무튼 iOS의 경우 대개 8로 지정하면 되는데 그 근거는 아래 링크의 Supported Pixel Formats 표를 참조하자.
https://developer.apple.com/library/ios/documentation/GraphicsImaging... - Section 3: Create an RGB CGColorSpace and a CGBitmapContext, passing in the pixels pointer as the buffer to store the pixel data this context holds. You’ll explore Core Graphics in more depth in a section below.
ColorSpace 생성하고 이를 CGBitmapContextCreate에 인자로 넣는다. - Section 4: Draw the input image into the context. This populates pixels with the pixel data of image in the format you specified when creating context.
Context에 CGImage와 똑같은 그림을 그린다. - Section 5: Cleanup colorSpace and context.
// 1. #define Mask8(x) ( (x) & 0xFF ) #define R(x) ( Mask8(x) ) #define G(x) ( Mask8(x >> 8 ) ) #define B(x) ( Mask8(x >> 16) ) NSLog(@"Brightness of image:"); // 2. UInt32 * currentPixel = pixels; for (NSUInteger j = 0; j < height; j++) { for (NSUInteger i = 0; i < width; i++) { // 3. UInt32 color = *currentPixel; printf("%3.0f ", (R(color)+G(color)+B(color))/3.0); // 4. currentPixel++; } printf("\n"); }
이 코드에도 처음에는 간단하게 생각하고 넘어가려 했었는데 다시 보니 깊은 내용이 있다.
우선 픽셀 개수만큼 텍스트로 찍어대면 엄청나게 거대한 출력물이 나올 것이다. 그러므로 이 코드는 픽셀 개수를 단순화 시키는 로직이 들어있다. 내가 아직 내공이 깊지 않아서 정확하게 이해는 할 수 없으나 느낌적인 느낌으로 대충 알 것 같다.
아무튼 단순화시키기 위해서 bit연산을 했고 8bit씩 shift해서 픽셀의 개수를 줄였다.
뭐 얼만큼 줄인 것인지 확인하려면 실제 이미지의 width, height와 이 출력물의 width, height 비율을 따져보면 알 수 있을 것이다.
그리고 이 방법이 썩 괜찮은 방법이라고 생각된다. 중간에 이미지가 제대로, 내가 원하는 대로 그려지고 있는지 확인이 필요한데 output 출력창을 통해서 이미지 형태로 확인이 가능하니 별도의 imageView 등을 이용하지 않아도 되는 좋은 방법이라 생각된다.
물론 지금의 나는 swift로 pixelbuffer를 이용해야 하기에 이 방법을 적용시킬 줄 모르겠다. ㅋ
원래 출처의 내용은 이보다 더 긴데, 내가 나름 바빠서 더 이상의 설명은 기약할 수 없는 다음 기회로 미루겠다.