이미지 프로세싱이란 넓은 의미에서 화소 데이터를 처리하는 과정을 의미한다. 이미지 프로세싱은 벡터 래스터라이징을 거쳐 생성한 장면 이미지를 새로 가공하거나 JPEG, PNG 형식 등 미리 가공된 이미지 자원으로부터 이미지 화소 정보를 해석하여 화면에 출력하는 과정을 포함한다. 또한 영상 처리 분야에서의 영상 가공, 이를테면 이미지를 추출하고 변환, 다른 이미지와 합성하는 작업 역시 이미지 프로세싱의 범주에 포함된다. 정리하면 이미지 프로세싱은 이미지 리소스의 출처가 무엇이든 간에 이미지 데이터를 인코딩(encoding), 디코딩(decoding)하여 그 형태를 변환하거나 후처리(post-processing)를 통해 어떤 효과를 적용하는 작업으로 간주한다.
한편, UI 렌더링 엔진에서는 이미지 프로세싱을 전담하기 위해 이미지 렌더러(Image-Renderer)를 구성할 수 있다. 이는 이미지 소스(파일)로부터 데이터를 읽어와 비트맵 데이터를 생성하고 필요에 따라 스케일링(Scaling), 회전, 색상 변환 그리고 블러(Blur)와 같은 이미지 필터(filter) 기능을 수행한다. 따라서 UI 렌더링 엔진에서 이미지 프로세싱은 3장에서 살펴본 벡터 그래픽스와 더불어 UI 이미지 리소스를 화면에 출력하는 핵심 기능에 해당한다. 이번 장에서는 캔버스 엔진에서 이미지를 출력하기 위한 주요 기능 및 동작 과정을 이해하고 이미지 프로세싱을 수행하는 이미지 렌더러의 특징을 살펴보도록 하자.
1. 학습 목표
이번 장을 통해 다음 사항을 학습해 보자.
2. 이미지 포맷
2.1 이미지 포맷 종류
이미지를 활용하기에 앞서 JPEG, PNG와 같이 현존하는 이미지 포맷을 이용한다면 기능 효율성을 높일 수 있다. 그렇다면 이미지 포맷으로 어떤 것들이 있을까? 다음 목록은 UI 엔진에서 지원할 수 있는 대표적인 이미지 포맷의 종류와 특징을 설명한다.
이 외에도 더 많은 이미지 포맷이 존재하지만, 현재 업계에서 주로 사용하는 포맷은 위 목록에 나열되어 있다. 이 중에서도 PNG는 UI 리소스로서 가장 많이 활용되는 포맷이고 JPEG은 앱의 콘텐츠로 많이 활용되므로 UI 엔진에서 이 두 포맷을 지원하는 것은 필수이다. 더욱 최적화된 UI 엔진을 설계하는 데 있어서 필요에 따라 자체적인 이미지 포맷을 새로 설계하거나 기존 포맷을 변형/확장할 수도 있지만, 이 경우 범용성이 떨어지기 때문에 주의해야 한다. 자체적인 새로운 포맷을 추가하였다면 JPG, PNG와 같은 기존 포맷으로부터 새로운 포맷으로 변형할 수 있는 툴을 함께 제공하여 앱 개발자가 새로운 포맷의 이미지를 쉽게 생성할 수 있도록 해야 한다. 포토샵, 일러스트레이터, 애프터 이펙트 등 대표 이미지 제작 툴에서 새로운 포맷의 파일을 바로 추출할 수 있는 추출기(exporter)를 제공하는 것도 방법이다. 새로 만든 포맷을 범용적인 포맷이 될 수 있도록 오픈소스로 프로젝트로 공개하는 것도 좋은 방침이다. 물론 기존 포맷 대비 특장점이 있지 않은 한 새로운 포맷을 지원하는 일은 신중해야 한다.
YCbCr 색상 공간 주로 예전의 디지털 TV를 위해 사용된 색상 공간 중 하나이며 mpeg 모델에서 사용되는 모델이기도 하다. 줄여서 YUV로 불리기도 한다. 휘도, 즉 밝기 성분 Y와 색상 성분 Cb, Cr을 통해 최종 색상을 표현한다. 사람의 눈이 색상보다는 밝기의 차이에 민감하다는 점을 고려해서 휘도에 더욱 큰 스펙트럼의 수치를 저장하고 색상은 RGB 모델보다 적은 Cb, Cr 두 요소로만 기재한다. 추가 설명하자면 영상 시스템에서 YUV는 크로마 서브샘플링(Chroma Subsampling) 기법을 이용하여 명도를 제외한 색차 정보만 줄여서 영상을 인코딩한다. 샘플링 정보는 J:a:b와 같이 표기한다. 이를테면, 4:2:2는 4x2영역의 픽셀에 대해서 각 루마(Y)를 적용하되, 첫 번째 행 네 픽셀에 크로마 두 개(a=2), 두 번째 행 4픽셀에 대해서 크로마 두 개(b=2)를 적용한다. 4:2:0이라면, 4x2영역의 픽셀에 대해서 각 루마(Y)를 적용하되, 첫 번째 행 네 픽셀에 크로마 두 개(a=2), 두 번째 행 네 픽셀에 크로마 0개(b=0)를 적용한다. 4:4:4의 경우는 샘플링 효과가 없다. 크로마 서브샘플링 (위키피디아) 다음 코드는 YCbCr과 RGB 색상 모델 간의 변환 식을 구현한다. //YCbCr from RGB Y = 16 + 1/256 * (65.738 * R + 129.057 * G + 25.064 * B); Cb = 128 + 1/256 * (-37.945 * R - 74.494 * G + 112.439 * B); Cr = 128 + 1/256 * (112.439 - 94.154 * G - 18.258 * B); //RGB from YCbCr R = Min(255, Y + 1.40200 * (Cr - 128)); G = Min(255, Y - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128)); B = Min(255, Y + 1.77200 * (Cb - 128)); |
2.2 이미지 로더
//준비된 서브 이미지 로더 해시 Hash imageLoaders UIImageLoader.FindImageLoader(file): /* 파일 확장명을 참고하여 서브 이미지 로더를 결정한다. 여기서 extension은 png 값을 갖는다고 가정한다. */ String extension = getExtension(file) if (extension == “png”) return imageLoaders.get(“png”) else if (extension == “jpg”) return imageLoaders.get(“jpg”) else if (...) ... /* 파일 확장명으로부터 후보 이미지 로더를 결정하지 못한 경우, 준비된 로더로부터 불러오기를 한 번씩 시도해 본다. */ foreach(imageLoaders, loader) if (loader.open(file) == success) return loader
MIME (Multipurpose Internet Mail Extensions)
|
그림 5: 스레드 풀 (Thread-Pool) 기반 이미지 로더
img = UIImage(): //앱은 지연 로딩 기능을 활성화하여 이미지를 불러오도록 요청한다. .open(“car.png”, async=true) //비동기로 이미지를 불러온다면, 이미지 불러오는 작업이 끝났다는 신호도 필요하다. .EventLoadDone += f
3. PNG 로더
PNG 파일은 파일 시그니처와 다수의 데이터 조각(Chunk)으로 구성된다. 파일 시그니처는 파일의 맨처음 8바이트 영역을 차지한다. 이곳에는 137 80 78 71 13 10 26 10 값을 기록하는데 만약 파일을 오픈한 후 처음 8바이트의 값이 위와 동일하다면 해당 파일을 PNG 포맷으로 간주할 수 있다. 앞서 코드 1의 18번째 라인처럼 파일의 확장명이 png가 아니라면 파일을 오픈하고 위 시그니처를 비교함으로써 해당 파일이 png인지 아닌지를 판단할 수 있다.
signature: 137 80 78 71 13 10 26 10
/* 8비트 요소로 구성한 CRC 테이블 */ unsigned long crc_table[256]; /* CRC 테이블 초기화 여부 기록 */ int crc_table_computed = 0; /* 빠른 CRC를 위한 테이블 구축 */ void make_crc_table(void) { unsigned long c; int n, k; for (n = 0; n < 256; n++) { c = (unsigned long) n; for (k = 0; k < 8; k++) { if (c & 1) c = 0xedb88320L ^ (c >> 1); else c = c >> 1; } crc_table[n] = c; } crc_table_computed = 1; } /* CRC 값 계산 수행. 첫 번째 인자 crc의 필드는 모두 1로 초기화 되어있어야 한다. */ unsigned long update_crc(unsigned long crc, unsigned char *buf, int len) { unsigned long c = crc; int n; if (!crc_table_computed) make_crc_table(); for (n = 0; n < len; n++) { c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); } return c; } /* buf[0 ~ len -1]에 대한 CRC 값 반환.
crc() 함수의 최종 반환 값은 1의 보수에 해당한다. */ unsigned long crc(unsigned char *buf, int len) { return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL; }
데이터 조각 유형은 총 18개로 다양하지만 실제로는 다음 네 유형이 핵심이다.
3.1 PNG 인코딩
PNG는 여러 절차를 걸쳐 인코딩을 수행한다. 다음은 인코딩 절차를 순차적으로 설명한다.
a. 패스 추출(Pass Extraction): 인터레이스드(Interlaced) 기능을 위해 수행하며 아담(Adam) 7 알고리즘을 이용하여 일곱 단계의 축소된 이미지를 생성한다. 참고로 인터레이스드 기능은 이미지를 점진적으로 보여주기 위한 기능이며 디코딩을 완료한 부분 이미지를 먼저 보여줌으로써 저 사양의 시스템에서도 사용자에게 즉각적인 이미지 출력 반응을 보여주는 효과를 제공한다.
PNG에서 인터레이스드 기능은 선택 사항일 뿐 많이 활용되지 않는 편이다. 만약 인터레이스드 기능을 활용하지 않는다면 패스 추출 단계 역시 수행할 필요 없다.
b. 스캔라인 직렬화(Scanline Serialization): 패스 추출 단계를 거친 후 준비된 각 부분 이미지 조각에 대해서 이미지 행 단위(Scanline)로 데이터를 분리한다. 만약 패스 추출을 수행하지 않는다면, 원본 이미지의 행 단위로 데이터를 분리한다.
c. 필터링(Filtering): 보다 효율적인 압축을 위해 사전 필터링을 수행한다. PNG는 하나의 필터 방법을 표준으로 사용하며 이 필터 방법 내에는 다수의 필터 타입이 존재한다. 전 단계에서 준비한 각 스캔라인에 대해서 필터 타입을 개별적으로 선택 적용하기 때문에 각 스캔라인에 필터 타입 정보가 추가된다. 더욱 자세한 내용은 4.3.3절을 확인하자.
d. 압축(Compression): 필터링을 적용한 스캔라인 데이터를 압축한다. 압축은 정의된 방식 중 하나를 지정하며 일반적으로 PNG 표준 압축 방식인 DEFLATE(압축 해제: INFLATE)를 적용한다. 압축 시에는 연속된 스캔라인 데이터를 입력으로 받고 결과물로는 무손실인 zlib 압축 데이터스트림을 데이터로 생성한다. 더욱 자세한 내용은 4.3.3절의 압축 내용을 참고하자.
e. 조각화(Chunking): 압축된 데이터 스트림을 다수의 IDAT 조각으로 분리한다. 각 데이터 조각은 PNG 파일의 시그니처 영역 뒤에 일렬로 연결되어 저장되므로 각 IDAT 조각의 데이터를 순서대로 연결하면 하나의 zlib 데이터가 완성된다. 염두에 둬야 할 사항은 IDAT 조각이 순서대로 연결되어 있을지라도 데이터 조각 간의 경계는 임의의 바이트에 해당한다는 점이다. 달리 말하면 스캔라인 또는 픽셀 기준으로 데이터를 분리하지 않기 때문에 하나의 데이터 조각이라도 손실된다면 데이터를 복원할 수 없다.
3.2 IHDR
IHDR(Image Header)은 이미지 헤더로서 이미지의 속성 정보를 보유한다. 헤더의 데이터 크기는 13바이트이고 이 데이터는 이미지 크기, 색상 깊이, 색상 타입, 압축과 필터링 방식 등의 정보를 포함한다.
이미지 타입 |
색상 타입 (Color Type) |
가능한 색상 깊이 (Bit Depth) |
설명 |
그레이스케일 (Grayscale) |
0 |
1, 2, 4, 8, 16 |
그레이스케일(흑백) 색상 |
트루컬러 (Truecolor) |
2 |
8, 16 |
RGB 색상 |
인덱스칼라 (Indexed-color) |
3 |
1, 2, 4, 8 |
인덱싱 방식. PLTE 데이터가 반드시 있어야 한다. |
그레이스케일 + 알파 |
4 |
8, 16 |
투명 속성을 지닌 그레이스케일 |
트루컬러 + 알파 |
6 |
8, 16 |
RGBA 색상 |
Length: 13, Chunk type: IHDR, #16진수 값은 73 72 68 82 Chunk data: { Width: 800, Height: 600, Bit depth: 8, Color type: 6, Compression method: 0, Filter method: 0, Interlace method: 0 }, CRC
3.3 IDAT
IDAT(Image Data)는 실제 이미지 데이터를 보유한다. 이미지 데이터는 필터링과 압축을 거친 결과물이며 직렬화(Serialization)를 통해 연속된 바이트를 구성한다. 하나의 IDAT 데이터 최대 크기는 65,536바이트이므로 가공된 데이터의 크기가 이보다 클 경우 IDAT 조각을 추가하여 하나의 스트림(Stream)을 구축한다. IDAT의 타입 값은 73 68 65 84 이다.
IDAT에 적용하는 기본 필터 0번을 살펴보면 필터 타입이 총 다섯 개가 있다. 필터 타입은 기본적으로 휴리스틱(Heuristic) 방식을 통해 결정하며 다섯 개의 필터 함수를 모두 수행하고 결과를 비교한 후 최적의 타입을 선택한다. 필터 함수는 이미지 픽셀의 각 행(스캔라인)마다 개별적으로 적용하는 편이 압축률에서 효과적이다. 필터는 깊이 정보 및 색상 타입에 의존하지 않으며 이미지 픽셀 단위가 아닌 바이트 단위로 계산을 수행하는 점을 염두에 둔다.
필터 함수 | 필터 역함수 | ||
Filt(x) = Orig(x) | Recon(x) = Filt(x) | ||
Filt(x) = Orig(x) - Orig(a) | Recon(x) = Filt(x) + Recon(a) | ||
Filt(x) = Orig(x) - Orig(b) | Recon(x) = Filt(x) + Recon(c) | ||
Filt(x) = Orig(x) - floor((Orig(a) + Orig(b)) / 2) | Recon(x) = Filt(x) + floor(((Recon(a)+Recon(b)) / 2) | ||
Filt(x) = Orig(x) - PaethPredictor(Orig(a), Orig(b), Orig(c)) | Recon(x) = Filt(x) + PaethPredictor(Recon(a), Recon(b), Recon(c)) |
이해를 돕기 위해 트루컬러(24비트) 이미지에서 픽셀(0F2865)의 x를 기준으로 a, b, c를 도식화하면 다음 그림과 같다.
RGBA 색상의 경우 알파 채널은 별도로 분리하여 RGB와 동일한 방식으로 데이터를 가공한다. 경험적으로 트루컬러 및 그레이스케일 색상에서는 대체로 다섯 개의 필터 타입 모두 효과적이다. 만약 하나의 필터만 사용해야 할 경우에는 Paeth 필터가 효과적이다.
p = a + b - c pa = abs(p - a) pb = abs(p - b) pc = abs(p - c) if pa <= pb and pa <= then Pr = a else if pb <= pc then Pr = b else Pr = c return Pr
필터를 적용하면 적용한 필터 타입이 무엇인지 알아야 하므로 이미지 데이터의 첫 번째 열 1바이트에 이를 기록한다.
04 0F32AE 962AE2 429D72 098FD3 0AEF21 058271 028E11 083123 ... (Paeth) 02 0F32AE 962AE2 429D72 098FD3 0AEF21 058271 028E11 083123 ... (Up)
데이터 압축은 기본적으로 DEFLATE 알고리즘(해제는 INFLATE)을 이용한다. DEFLATE는 많이 알려진 zip, gzip에서 사용되는 압축 포맷이자 알고리즘이다. 이는 기본적으로 LZ77 알고리즘을 통해 데이터를 압축한 후 중복되는 데이터를 더욱 최소화하기 위해 허프만 부호화 알고리즘으로 한 번 더 압축하는데 타 압축 방식보다 압축률이 높고 속도도 빠른 편에 속해 대중적으로 활용되고 있다.
LZ77 압축 알고리즘 LZ1으로 불리기도 한다. 비손실 압축 알고리즘으로서 1977년 아브람 렘펠의 의해 처음 발표되었다. LZW, LZSS, LZMA 등 다양한 변형 알고리즘이 존재하며 LZ77은 DEFLATE뿐만 아니라 GIF, ZIP의 압축에도 활용되고 있다. LZ77 압축의 핵심 원리는 단순하다. 주어진 입력값 중 반복되는 데이터가 존재할 시 데이터의 오프셋과 그 길이만을 입력하여 데이터의 양을 줄이는 방식이다. 가령, ‘ABCDEFGHIJKABCDEFG1234’ 라는 입력 데이터가 있으면 입력값 후반의 ABCDEFG는 중복 데이터에 해당하므로 이를 ‘ABCDEFGHIJK{11,7}1234’ 와 같이 변경하여 데이터의 크기를 줄인다. 실제로 인코딩 결과물은 오프셋과 길이뿐만 아니라 바로 다음에 올 데이터(코드)까지 같이 기재하는데 위 예제의 경우 인코딩 결과물은 {11, 7, c(1)}에 해당한다. 중복되는지 판단하기 위한 데이터 검색 영역을 일정 크기 단위로 분할할 수도 있다. LZ77에서는 이를 슬라이딩 윈도우(Sliding Window) 또는 히스토리 테이블(History Table)라고도 한다. 이러한 개념을 도입하면 입력 데이터를 슬라이딩 윈도우 영역 내에서만 중복되는지 판단할 수 있다. 슬라이딩 윈도우 범위 내에서 이미 인코딩을 끝낸 영역은 검색 버퍼(Search Buffer)로 지정하고 아직 인코딩을 끝내지 않은 영역을 Look-Ahead Buffer라고 한다. 슬라이딩 윈도우의 크기는 압축 속도와 압축률에 영향을 미치기 때문에 상황에 맞게 그 값을 조정할 수도 있다. LZ77에서 중복 영역의 데이터 길이는 최대 256바이트로 제한하며 이를 인코딩 시 최대 3바이트까지 줄일 수 있다. |
허프만 부호화 (Huffman Coding) 1951년 허프만 박사에 의해 고안된 알고리즘이며 무손실 압축 기법으로서 데이터의 사용 빈도수에 따라 데이터에 다른 길이의 부호를 사용하는 알고리즘이다. 이 알고리즘의 핵심은 가장 많이 사용되는 데이터일수록 더 적은 수의 비트를 활용한다는 점에 있다. 이는 이 알고리즘에서 압축 효과를 높이는 가장 중요한 핵심 원리이다. 가령, “abcd_abb_abbbc_ccddd” 라는 입력 데이터가 있을 때 사용 빈도수를 확인해보면 b는 6, a는 3인 점을 알 수 있다. 여기에 허프만 알고리즘을 적용한다면 b는 2비트인 11, a는 3비트인 100으로 치환(부호화)할 수 있다. 그리고 원래의 데이터와 치환된 값을 허프만 테이블에 따로 기입하고 디코딩 시 인코딩된 비트값이 원래의 어떤 값으로 치환될지 확인한다. 치환될 값을 결정하는 방식은 입력 데이터의 출현 빈도수에 따르며 이진 트리를 구성하여 확인할 수 있다. 즉, 입력 데이터를 트리로 구축하고 특정 데이터에 접근하기 위해 트리를 탐색하는 절차(좌: 0, 우: 1)가 바로 치환될 값이다. 100의 값을 가지는 a 입력 데이터는 실제로 트리의 루트 노드로부터 우(1)->좌(0)->좌(0)의 방향으로 깊이 탐색하는 과정과 동일하다. 이러한 절차에 의해 치환 값은 중복되거나 다른 치환 값의 부분값이 되지 않아 결괏값을 해석할 때 문제가 되지 않는다. 허프만 부호화의 알고리즘을 정리하면 다음과 같은 절차를 수행한다. 1. 모든 입력 데이터를 출현 빈도수로 정렬하여 목록 구축 2. 하나의 데이터가 남을 때까지 아래 단계 반복 a. 목록에서 빈도수가 가장 낮은 두 개의 데이터를 선택하고 목록에서 제거 b. 선택한 두 데이터로부터 하나의 새로운 (접두부호) 데이터를 생성하고 선택한 두 개의 데이터를 새 데이터의 이진 트리 자식으로서 좌, 우에 추가. 이때 새로운 데이터의 출현 빈도수는 두 자식 데이터의 출현 빈도수의 합으로 결정 c. b에서 생성된 새로운 데이터를 출현 빈도 기준으로 정렬하여 목록에 추가 위 과정을 도식화하면 다음과 같다. |
3.4 IEND
IEND(Image End)는 데이터의 마지막 조각에 해당하며 PNG 파일 데이터의 끝부분임을 명시한다. 실제 데이터가 존재하지 않음으로 길이는 0이고 타입 값은 73 69 68 68이다.
4. 이미지 스케일링
이미지 렌더러는 이미지 로더에서 생성한 비트맵 데이터를 원본으로 전달받고 이를 시스템에서 출력 가능한 포맷으로 변환하거나 추가적인 이미지 프로세싱을 수행한다. 예를 들면, 요청한 크기에 맞게 원본 이미지의 크기를 조정하거나 회전, 색상 변환, 다른 이미지와 합성 등의 작업을 수행할 수 있다. 이미지 로더가 여러 이미지 소스로부터 실제 가용한 이미지 데이터를 생성하는 과정을 수행한다면 이미지 렌더러는 원본 이미지의 비트맵 데이터를 토대로 렌더링 엔진에서 가용한 후처리(Post-Processing) 작업을 수행한다고 볼 수 있다.
4.1 포인트 샘플링
- 최단 입점 보간
/* * Point Sampling(Nearest Neighbor) 스케일링 구현 * @p src: NativeBuffer * @p dst: NativeBuffer */ UIImageRenderer.nearestNeighborScale(src, dst): //가로, 세로 스케일 요소를 구한다. xScale = src.width / dst.width yScale = src.height / dst.height //버퍼 메모리 접근 Pixel srcBitmap[] = src.map() Pixel dstBitmap[] = dst.map() /* 포인트 샘플링을 수행한다. 스케일된 픽셀 위치로부터 원본 이미지 픽셀 위치를 찾는다. 접근하는 위치가 메모리 버퍼 영역을 벗어나는 검사는 생략한다. */ for (y = 0; y < dst.height; ++y) for (x = 0; x < dst.width; ++x) sx = xScale * x sy = yScale * y destBitmap[y * dst.scanlineSize + x] = srcBitmap[sy * src.scanlineSize + sx]
4.2 슈퍼샘플링
슈퍼샘플링은 포인트 샘플링 대비 고급 스케일링 기법에 해당한다. 원본 이미지로부터 하나의 픽셀을 선택하고 스케일된 공간으로 복사하는 포인트 샘플링보다 슈퍼샘플링은 원본 이미지로부터 인접한 복수의 픽셀을 선택 후 하나로 합성하여 스케일된 공간으로 복사한다. 따라서 슈퍼샘플링은 스케일링으로 인해 색상의 손실이나 도트가 두드러져 보이는 현상을 줄여준다. 일반적으로 슈퍼샘플링은 포인트 샘플링 대비 시각적으로 부드럽고 깨끗한 질감을 만들어내므로 많이 활용되는 샘플링 방식에 해당한다.
슈퍼샘플링의 효과는 이미지 안에 존재하는 경계선에도 그대로 적용된다. 이미지 안의 경계선에 도트가 두드러지는 앨리어싱 현상(일명 계단 현상)은 시각적으로 이미지 품질을 저해하는데 이를 개선하기 위해 앤티에일리어싱 목적으로 슈퍼샘플링을 이용할 수 있다. 이는 출력하고자 하는 해상도보다 더 큰 해상도의 이미지를 준비한 후 슈퍼샘플링을 통해 원래 출력하고자 했던 크기로 이미지 해상도를 축소하는 과정을 수행한다. 이러한 앤티에일리어싱 기법은 슈퍼샘플링 앤티에일리어싱(SSAA) 또는 Full Scene Anti-Aliasing(FSAA)이라고도 부르며 다운 샘플링(Down Sampling)이라고도 한다.
- 이중선형 보간
/* * Super Sampling(Bilinear Interpolation Filter) 스케일링 구현 * @p src: 원본 이미지 데이터 * @p dst: 스케일을 적용한 결과 이미지 데이터 */ UIImageRenderer.bilinearScale(src, dst): //가로, 세로 스케일 요소를 구한다. xScale = src.width / dst.width yScale = src.height / dst.height
//버퍼 메모리 접근 Pixel srcBitmap[] = src.map() Pixel destBitmap[] = dst.map() /* Bilinear Interpolation 샘플링을 수행한다. 스케일된 픽셀 위치로부터 원본 이미지
픽셀을 찾은 후, 인접한 세 개의 픽셀과 색상을 혼합한다. */ for (y = 0; y < dst.height; ++y) for (x = 0; x < dst.width; ++x) dx = xScale * x dy = yScale * y sx1 = round(dx) sy1 = round(dy) sx2 = sx + 1 sy2 = sy + 1 //Index Overflow에 주의 if (sx2 == src.width) sx2 = sx1 if (sy2 == src.height) sy2 = sy1 //소수점 이하 값 xFraction = dx - sx1 yFraction = dy - sy1 s1 = srcBitmap[sy1 * src.scanlineSize + sx1] s2 = srcBitmap[sy1 * src.scanlineSize + sx2] s3 = srcBitmap[sy2 * src.scanlineSize + sx1] s4 = srcBitmap[sy2 * src.scanlineSize + sx2] //D1 = ((xS2 - xD1) / (xS2 - xS1)) * S1 + ((xD1 - xS1) / (xS2 - xS1)) * S2 t = ((sx2 - dx) / (sx2 - sx)) t2 = ((dx - sx1) / (sx2 - sx)) alpha = t * ((s1 >> 24) & 0xff) + t2 * ((s2 >> 24) & 0xff) red = t * ((s1 >> 16) & 0xff) + t2 * ((s2 >> 16) & 0xff) green = t * ((s1 >> 8) & 0xff) + t2 * ((s2 >> 8) & 0xff) blue = t * (s1 & 0xff) + t2 * (s2 & 0xff) d1 = (alpha << 24) | (red << 16) | (green << 8) | (blue) t = ((sx4 - dx) / (sx4 - sx3)) t2 = ((dx - sx3) / (sx4 - sx3))
//D2 = ((xS4 - xD2) / (xS4 - xS3)) * S3 + ((xD2 - xS3) / (xS4 - xS3)) * S4 alpha = t * ((s3 >> 24) & 0xff) + t2 * ((s4 >> 24) & 0xff) red = t * ((s3 >> 16) & 0xff) + t2 * ((s4 >> 16) & 0xff) green = t * ((s3 >> 8) & 0xff) + t2 * ((s4 >> 8) & 0xff) blue = t * (s3 & 0xff) + t2 * (s4 & 0xff) d2 = (alpha << 24) | (red << 16) | (green << 8) | (blue) //D = ((yD2 - yD) / (yD2 - yD1)) * D1 + ((yD - yD1) / (yD2 - yD1)) * D2 t = ((sy2 - dy) / (sy2 - sy1)) t2 = ((dy - sy1) / (sy2 - sy1)) alpha = t * ((d1 >> 24) & 0xff) + t2 * ((d2 >> 24) & 0xff) red = t * ((d1 >> 16) & 0xff) + t2 * ((d2 >> 16) & 0xff) green = t * ((d1 >> 8) & 0xff) + t2 * ((d2 >> 8) & 0xff) blue = t * (d1 & 0xff) + t2 * (d2 & 0xff) d = (alpha << 24) | (red << 16) | (green << 8) | (blue) destBitmap[y * dst.scanlineSize + x] = d
슈퍼샘플링에는 이중선형 보간 외에 삼선형(Trilinear) 및 이방성(Anisotropic) 보간 방식도 존재한다. 이들 보간법은 이중선형 대비 더 좋은 이미지 품질을 제공하지만, 더 많은 리소스와 계산량을 요구한다. 일반적인 UI 시스템에서는 이중선형 보간식으로도 만족할만한 수준의 이미지 품질을 제공한다.
5. 텍스처 매핑
텍스처 매핑(Texture Mapping)은 이미지를 기하 형태로 출력한다. 원래는 단어 뜻처럼 재질을 씌우는 개념으로 볼 수 있는데 전통적인 3D 그래픽스에서는 텍스처 매핑 기술을 이용해 폴리곤(Polygon)에 이미지를 매핑함으로써 메시(Mesh)나 도형에 재질감을 부여한다. UI 엔진에서 텍스처 매핑은 출력하고자 하는 이미지가 직사각형이 아닌 기하 형태로 출력할 수 있게 도와주므로 특수 효과 등의 목적으로 활용할 수 있다. 특히 UI 엔진에서 텍스처 매핑은 이미지를 회전하거나 쉬어(Sheer) 변환 등을 수행할 때 유용하다. 가령 UI가 z축을 중심으로 회전을 수행한다면 해당 UI 이미지는 직사각형에서 마름모 형태로 변환될 것이다. 이 경우 마름모 형태의 폴리곤에 이미지를 매핑한다면 원하는 결과물을 표현할 수 있다.
텍스처 매핑의 핵심 정보는 폴리곤을 구성하고 폴리곤의 각 꼭짓점에 매핑될 텍스처 좌표를 지정하는 것에 기반한다. OpenGL 및 Direct3D와 같은 대중적인 3D 출력 시스템에서는 텍스처 매핑 기능을 위해 폴리곤의 각 꼭짓점에 매칭될 텍스처 좌표 UV 값을 입력받는다. 이후 렌더링 엔진은 각 꼭짓점에 입력된 텍스처 좌표를 참고하여 폴리곤에 채워질 이미지 픽셀을 계산한다. 텍스처 좌푯값 UV는 맵핑할 이미지 공간을 정규화한 값으로써 0~1 사이의 값을 받는다. 따라서 이 값은 이미지의 크기에 상관없이 항상 균일하다.
/* Quadron은 사각형 도형을 출력하기 위한 인터페이스이다. Quadron의 정점(꼭짓점)은 시계 방향(Clock-Wise)으로 구성된다. */ quad = UIQuadron(): //Quadron의 네 정점 좌표 {x, y, z}를 지정한다. 인덱스는 정점 순서를 가리킨다. .coord[0] = {200, 0, 0} .coord[1] = {400, 50, 0} .coord[2] = {400, 100, 0} .coord[3] = {200, 150, 0} //Quadron의 텍스처 좌표를 지정한다. .uv[0] = {0, 0} //이미지 좌측 상단 .uv[1] = {1, 0} //이미지 우측 상단 .uv[2] = {1, 1} //이미지 우측 하단 .uv[3] = {0, 1} //이미지 좌측 하단 //매핑할 텍스처 리소스 .path = “texture.png”
//코드 8과 동일하게 Quadron의 정점과 텍스처 좌표를 설정한다. quad = UIQuadron() ... //매핑할 텍스처로 버튼 컨트롤을 준비한다. button = UIButton() ... //path보다는 source가 더 유연한 인터페이스로 보인다. quad.source = button
회전처럼 범용적인 기능은 더욱더 쉬운 인터페이스를 제공함으로써 앱 개발자가 UI 효과를 쉽고 빠르게 구현할 수 있도록 도와줄 수 있다. 이를 위해 사용자는 정점 좌표를 지정하는 대신 회전 각도를 지정할 수 있을 것이다. 코드 10의 경우 버튼을 회전하지만 엔진 내부적으로는 버튼을 소스로 텍스처 매핑을 수행하는 것과 동일하다.
/* rotate()는 x, y, z축 회전 각도를 파라미터로 정의한다.
예제의 경우 버튼을 z축 중심으로 45도 회전한다. */ button.rotate(0, 0, 45)
사실 단순 회전 효과를 구현하기 위해서는 회전한 UI 객체의 경계선 위치만 결정하면 된다. 이 경우 오일러(Euler) 각에 대한 회전 행렬을 구현하는 것으로도 충분하다. 이를 위해 x, y, z 각 축에 대한 회전 행렬을 결합하여 최종 변환 행렬을 구축할 수 있다. 이미지를 구성하는 각 픽셀은 이미지 중심을 원점으로 3D 벡터를 구하여 이를 행렬과 곱해주면 회전된 위치의 픽셀을 구할 수 있다. 회전을 구현하는 로직은 3.4.5절에서도 언급하므로 여기서는 생략한다.
위의 구현 방식은 이미지를 구성하는 모든 픽셀을 대상으로 변환을 수행한다. 구현 방식은 직관적이지만 회전 외의 변환을 수행해야 한다면 이 방식은 효율적이지 않은 단점이 존재한다.
5.2 원근법 구현
원근법(Perspective)을 구현하기 위해서는 z 좌푯값을 어떻게 다뤄야 할지 고려해야 한다. 전통적인 3D 시스템을 모방하자면 월드 공간 내에 여러 UI 객체가 공존할 수 있다. 이들은 사용자가 설정한 전역적인 시야 절두체(Frustum) 정보를 공유하고 이를 기반으로 투영 변환을 수행한다. 다른 방안으로는 UI 객체마다 독립적인 투영식을 수행한다. 이 방법에서는 객체 자신의 영역을 독립적인 뷰포트(Viewport)로 정의할 수 있고 이 뷰포트 영역 내에 독립적인 투영 공간을 구축하고 이를 토대로 z 축에 대한 투영 변환을 수행한다. 이 방식은 OpenGL, Direct3D 환경의 전통적인 모델-뷰-투영 변환과 달리 객체마다 전역적인 뷰 변환(카메라 변환)이 존재하지 않는 UI 시스템의 특성에 기인한다.
/* 투영 정보를 설정한다. 초점의 중심 위치와 소실점까지 거리를 지정 */ obj.perspect(focalX, focalY, distance)
투영된 정점의 최종 위치는 원래 객체의 정점 위치로부터 소실점까지의 거리를 토대로 판단할 수 있다. 다시 말해서 초점 위치로부터 각 정점을 향하는 방향 벡터를 구하고 각 정점의 z 값을 distance로 나눈 값을 벡터 길이로써 활용한다.
그림 27: 정점의 투영 변환 수행식
5.3 매핑 알고리즘
코드 12는 그림 30의 핵심을 구현한다.
/* Span[8]의 px[5](여섯 번째 픽셀)를 구해보자.
먼저 px[5]에 대응하는 vTex의 픽셀 위치를 찾기 위해 비율을 계산한다. */ pxPos = 6 / (spans[8].coord[1] - spans[8].coord[0])
/* Span[8]에 매핑할 텍스처 공간의 벡터를 구한다. */ vTex = spans[8].uv[1] - spans[8].uv[0]
/* vTex의 방향 벡터를 구한다. */ vTexNorm = vTex.normalize()
/* 마지막으로 vTex에서 pxPos에 위치하는 텍스처 좌표를 찾는다. */ texCoord = vTexNorm * pxPos
5.4 앤티에일리어싱
Span 기반 매핑 알고리즘을 이용하여 폴리곤을 출력했다면 도형 외곽선의 품질을 고려해야 한다. 외곽선 품질은 앤티에일리어싱(Anti-Aliasing, 줄여서 AA) 기법을 통해 개선할 수 있으며 환경에 따라 여러 앤티에일리어싱 기법의 하나를 선택할 수 있다.
대표적인 슈퍼샘플링 AA는 방법은 단순하지만, 퀄리티 개선 효과는 탁월하다. 원리는 우리가 출력하고자 하는 이미지보다 N 배 큰 해상도의 이미지를 생성한 후 원래 해상도로 이미지를 축소하는 것이다. 이때 4.2절에서 기술한 보간법으로 다운 샘플링을 수행하여 도형 외곽라인의 계단 현상을 자연스럽게 완화할 수 있다. N은 샘플링 개수를 의미하며 네 개의 샘플링이면 품질을 충분히 개선할 수 있다. 구현 방법은 단순하지만, N 배 해상도의 이미지를 생성해야 하므로 메모리 및 프로세싱의 추가 부담이 존재한다.
슈퍼샘플링 AA는 화면 영역 전체를 대상으로 샘플링 작업을 수행하므로 외곽선의 계단 현상뿐만 아니라 다운 스케일링 시 발생하는 색상의 손실을 해소하고 텍스처 도트가 두드러지게 보이는 현상도 개선할 수 있다. 슈퍼샘플링 AA는 샘플링을 적용하지 않은 전체 화면을 대상으로 적용할 때 보다 효율적이다.
슈퍼 샘플링 AA의 부하를 회피하는 방편으로 멀티샘플링 AA를 고려할 수 있다. 이미지 전 영역을 대상으로 수행하는 슈퍼샘플링 AA와 달리 멀티샘플링 AA는 도형의 외곽선만 샘플링을 수행한다. 이 경우 샘플링 수행 범위를 대폭 축소할 수 있음으로 프로세싱 부담도 그만큼 줄어든다. 다만 멀티샘플링 특성상 도형의 외곽선을 인지할 수 있는 전제 조건이 필요하다. OpenGL 및 Direct3D와 같은 전통 그래픽스 출력 시스템에서는 폴리곤 단위로 래스터 작업을 수행하므로 렌더링 파이프라인 과정에서 도형의 지오메트리 정보를 취급하는 것이 가능하다.
멀티샘플링 AA의 구현 시 한 픽셀의 색상을 결정하기 위해 샘플링 필터를 적용할 수 있다. 여기서 샘플링 필터는 샘플링할 픽셀의 개수 및 위치 정보를 구현한다. 네 점의 샘플링 필터라면 회전한 그리드 형태로 구성할 수 있으며 샘플링한 네 점 위치의 픽셀 값을 보간하여 최종 픽셀 색상을 결정할 수 있다. 샘플링 필터의 잘 분산된 샘플링 위치 정보는 하나의 픽셀을 지나는 도형의 외곽선으로부터 균형 있는 색상 보간이 가능하도록 도와준다.
6. 이미지 합성
렌더링 엔진에서는 생성한 개별 UI 이미지를 합성하는 작업을 수행해야 한다. 이를 이미지 합성 내지 컴퍼지션(Composition) 또는 블렌딩(Blending)이라고도 하는데 이는 두 장 이상의 이미지를 같은 영역에 출력하기 위한 작업의 일환이다. 이미지 합성의 핵심은 합성하는 두 이미지의 픽셀값과 합성식에 있다. 간단한 예로 알파 블랜딩에서 배경 이미지 위에 반투명한(Opacity=50%) 한 이미지를 출력한다면 두 이미지의 색상 농도를 반으로 감소한 후 이를 합산하여 그린다.
6.1 알파 블랜딩
/* * 알파 블렌딩 구현 * 블렌딩 식: (DstRGB * (1 - SrcA)) + (SrcRGB * SrcA) * @p src: Pixel * @p dst: Pixel */ blend(src, dst): out = Pixel(): .r = dst.r * ((255 - src.a) / 255) + (src.r * (src.a/255)) .g = dst.g * ((255 - src.a) / 255) + (src.g * (src.a/255)) .b = dst.b * ((255 - src.b) / 255) + (src.b * (src.a/255)) .a = (255 - src.a) + src.a return out
코드 13은 두 색상을 합성하는 알파 블랜딩 함수를 구현한다. 소스(src)와 대상(dst) 색상을 입력받고 지정된 수식에 의거 색상을 합성한다. 여기서 사용된 블렌딩 수식은 (Src Color / Src Alpha) + (Dst Color * (1 - Src Alpha))이다. 이때 수식에서 색상 범위는 0~1로 가정한다. 수식에서 소스 이미지의 투명도가 20%에 해당한다면 대상 이미지의 투명도는 80%를 적용하며 Out = Src * 0.2 + Dst * 0.8로 이해할 수 있다. 실제 채널당 색상 값은 0~255구간을 가지므로 0~1은 0~255로 매핑되어야 한다.
코드 13 comp()는 렌더링 엔진에서 도형 또는 이미지를 그릴 때 호출할 수 있다. 다음 코드는 3.5.1절의 사각형 그리는 로직에 위 comp()를 적용한 주요 코드를 보여준다.
/* * 사각형 그리는 작업 수행 * @p buffer: NativeBuffer * @p rect: Geometry * @p clipper: Geometry * @p fill: UIFill * @p comp: UICompositor */ UIVectorRenderer.drawRect(buffer, rect, clipper, fill, comp, ...):
... //src는 사각형 색상, dst는 사각형이 그려질 대상에 해당 src = fill.color for (y = 0; y < rect.h; ++y) for (x = 0; x < rect.w; ++x) dst = bitmap[y * scanlineSize + x] /* comp()는 src와 dst에 알파블랜딩을 적용한 값을 반환한다. */ bitmap[y * scanlineSize + x] = comp(src, dst)
다음은 UI 객체에 블렌딩 옵션을 적용하기 위한 API 호출 예시이다.
//대상 객체 dst = UICircle(): .position = 200, 100 .radius = 100 .fill = UIRGBA(233, 30, 99, 255) //소스 객체 src = UIRect(): .geometry = {100, 200, 200, 200} .fill = UIRGBA(33, 150, 243, 127)
.compMethod = UIComposition.AlphaBlend //알파 블렌딩 지정
/* Compositor 인터페이스. 입력과 출력을 정의 */ Interface UICompositor:
/*
* @p src: Pixel
* @p dst: Pixel
*/ Pixel comp(src, dst) /* Compositor 인터페이스를 기반으로 AddBlend 구현 */ UIAddBlend implements UICompositor: override comp(src, dst): ... return out /* Compositor 인터페이스를 기반으로 AlphaBlend 구현 */ UIAlphaBlend implements UICompositor: override comp(src, dst): ... return out /* 그 외 브랜딩 옵션 구현 */
...
/* UI 객체에 AlphaBlend 지정 */ src = UIRect():
/* compMethod()에 익명(Anonymous)의 UIAlphaBlend 객체를 전달 */ .compMethod = UIAlphaBlend ...
/* UIObject는 블랜딩 옵션으로 UCompositor 객체를 전달받는다.
* @p comp: UICompositor
*/ UIObject.compMethod(comp): .comp = comp ... /* 이후 객체가 render()를 수행할 시 comp를 래스터라이저로 전달한다. 이 comp는 래스터 단계에서 호출된다. 이후 코드는 14와 동일하다. */ UIRect.render(...): ... UIVectorRenderer.drawRect(..., .comp, ...)
6.2 마스킹
마스킹은 입력 데이터로서 소스(Source)와 마스킹(Masking) 두 개의 이미지를 요구하며 마스킹 이미지의 픽셀 데이터는 소스 이미지 픽셀 데이터와 함께 마스킹 함수로 전달되어 합성된다. 일반적으로 마스킹 이미지가 지닌 데이터를 알파 값으로 간주하는데 달리 말하면 마스킹 이미지의 픽셀 데이터는 알파 값으로서 소스 이미지에 적용할 수 있다. 이때 두 데이터 간 동일 위치에 있는 픽셀끼리 마스킹 연산을 수행하고 그 결과(Output)를 저장한다.
디자인 관점과 달리 마스킹 동작은 비용이 다소 비싼 작업에 해당한다. 원형 아이콘을 예로 들자면 하나의 아이콘을 위해 마스킹 이미지를 별도로 준비해야 한다. 만약 마스킹 이미지가 따로 구비되어 있지 않다면 렌더링 과정을 거쳐 마스킹 이미지를 동적으로 생성해야 한다. 이후 준비된 마스크 이미지는 소스 이미지와 마스킹 합성 작업을 거치고 최종 아이콘을 생성할 수 있다. 최악의 경우 마스킹이 필요 없는 아이콘 대비 두 배 이상의 메모리 사용과 데이터 처리 비용이 든다. 만약 디자인 단계에서 미리 마스킹 작업을 처리할 수 있다면 보다 최적화된 UI 앱 개발에 도움이 된다.
6.3 필터
그림 42: 이미지 필터 효과
일반적인 필터 방안은 출력 준비가 끝난 비트맵을 입력값으로 받아 필터 함수를 통해 비트맵을 변조한 후 출력될 수 있도록 한다. 이때 필터 함수는 내장 함수 또는 사용자 커스텀 함수일 수 있으며 요청으로 복수의 필터가 연속으로 수행될 수도 있다. 더 나은 성능을 위해서는 필터 입력으로 사용할 비트맵을 생성함과 동시에 필터를 적용하고 렌더링 단계를 최소화할 수 있다.
/* UserCustomFilter는 UIFilter 인터페이스를 구현한다.*/ UserCustomFilter implements UIFilter:
/*
* 필터 수행 함수
* @p in: Pixel
* @p coord: Point
*/ override func(in, coord, ...): Pixel out /* 여기서 필터 동작을 수행하는 로직을 작성한다. in으로 받은 픽셀 데이터를 변조하여 out에 기록한다. 구체적인 로직은 애니메이션 & 이펙트에서 다룬다. */ ... return out
content = UIImage(): /* UI객체에 사용자 정의 필터를 추가하는 것은 물론 내장 필터를 추가할 수도 있다. */ .filters += UserCustomFilter .filters += UIBlurFilter
/* UIImage는 이미지를 출력하는 UIObject의 파생 타입이다.
* @p buffer: nativeBuffer */ UIImage.render(buffer, ...): ... /* 필터를 적용한 중간 결과물을 저장할 버퍼 생성 */ tmp = NativeBuffer(...) /* 출력할 이미지 데이터를 대상 버퍼(buffer)가 아닌 임시 버퍼(tmp)에 그린다. */ UIVectorRenderer.drawImage(temp, ...) /* 객체에 지정된 필터 목록(.filters())을 참조하여 필터 프로세싱을 수행한다. UIFilterProcess.proc()은 filters()에 지정된 필터 목록을 순차적으로 수행해 주는 역할을 한다고 가정하며
입력 데이터 tmp으로부터 결과물을 buffer에 기록한다. */ UIFilterProcessor.proc(tmp, .filters, ..., buffer) ...
이를 토대로 안드로이드는 렌더스크립트 개념을 확장한 필터스크립트 기능을 제공한다. 이는 필터 기능에 집중한 인터페이스와 기능을 제공함으로써 사용자가 필터 함수를 더욱더 쉽게 구현할 수 있도록 도와준다.
#pragma version(1) #pragma rs java_package_name(com.example.rssample) #pragma rs_fp_relaxed /* 필터 함수. 23번 줄의 forEach_invert()에 의해 호출 */ uchar4 __attribute__((kernel)) invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; } //렌더스크립트 초기화 mRS = RenderScript.create(this); mInAllocation = Allocation.createFromBitmap(mRS, bm, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); mOutAllocation = Allocation.createFromBitmap(mRS, bm, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); mScript = new ScriptC_mono(mRS, getResource(), R.raw.mono); //렌더스크립트 실행 mScript.forEach_invert(mInAllocation, mOutAllocation);
7. 최적화 전략
7.1 이미지 캐싱
UI 렌더링 엔진에서 이미지 프로세싱은 기능상 많은 비중을 차지한다. 만약 벡터 래스터라이징을 통해 생성한 이미지나 이미지 스케일링을 통해 가공한 다른 해상도 이미지를 캐싱을 통해 재사용할 수 있다면 보다 성능 효율을 높일 수 있다. 여기서 이미지 캐싱(Caching)은 이미지만 전담한 캐싱 기능을 말하는데 렌더링 엔진 내부에서는 이미지 캐시 관리자(Image Cache Manager)를 구현하여 비트맵 데이터를 관리하고 엔진 정책에 따라 비트맵 데이터를 재사용하거나 폐기할 수 있다.
그림 46: 이미지 캐시 관리자 운용
첫 번째 고려사항으로 이미지 캐시 관리자가 캐싱할 이미지 대상은 다음 목록과 같이 간추려 볼 수 있다.
//해당 프로세스에서 사용할 이미지 캐시 최대 크기 지정 (예시: 50MB) UICanvas.cacheSize(50000000)
/* * UIImage.render() 전 수행되는 이미지 준비 단계 */ UIImage.prepare(...) ... //Cache ID 생성. 이미지 경로와 출력할 이미지 가로, 세로 크기 조합 key = .path + .width.toString + "x" + .height.toString //기존에 캐싱된 데이터인지 확인 dstBuffer = ImageCacheMgr.get(key) /* 캐싱되어 있지 않으므로 이미지 데이터를 새로 만들어서 추가한다. 만약 dstBuffer가 존재한다면 데이터를 새로 만들지 않고 재활용한다. */ if (!dstBuffer) dstBuffer = NativeBuffer(UICanvas.getSurface, .width, .height, RGBA32, IO_WRITE + IO_READ) //srcBuffer로부터 dstBuffer에 이미지 스케일링 수행 UIImageRenderer.bilinearScale(.srcBuffer, dstBuffer) //재사용을 위해 dstBuffer를 캐싱 ImageCacheMgr.add(key, dstBuffer) ... /* * 새 캐시 데이터 추가 * @p key: String * @p data: NativeBuffer */ UIImageCacheMgr.add(key, data) size = data.size //새로 캐싱할 이미지 크기 //캐시 크기 초과 if (size > .maxCacheSize) return false .cacheSize += size //누적 캐시 크기 갱신 //초과한 캐시 크기만큼 캐시 메모리 확보 while(.cacheSize - .maxCacheSize > 0)
/* 캐시 목록에서 첫 번째 (가장 오래된) 데이터를 가져와서 삭제한다. Pair는 키와 데이터 쌍을 담는 자료구조이다. */ Pair it = .cacheList.get(0) NativeBuffer tmp = it.data .cacheSize -= tmp.size .cacheList.remove(0) //캐시 공간이 확보되었으므로 cacheHash에 새 데이터 추가 Pair it(key, data) .cacheList.append(it) /* * 기존 캐시 데이터 반환 * @p key: String */ UIImageCacheMgr.get(key) Pair it found = false //캐시 목록에서 현재 키와 일치하는 데이터를 찾는다. for (i = 0; i < .cacheList.length; i++) it = .cacheList.get(i) //데이터를 찾은 경우 캐시 목록에서 제거 if (it.key == key) .cacheList.remove(i) found = true break if (!found) return //존재하지 않는 캐시 데이터 //데이터를 리스트의 최상단으로 옮긴 후 캐시 데이터 반환 .cacheList.append(it) return it.data
8. 정리하기
이미지 프로세싱은 UI 렌더링 엔진의 핵심 기능으로서 이미지 가공을 수행한다. 이번 장에서 우리는 렌더링 엔진이 이미지를 출력하기 위해 파일로부터 이미지 데이터를 불러오는 과정을 확인하였다. JPEG, PNG, WEBP, GIF 등 포맷별 특징을 살펴보았고 이들의 데이터 구조 및 인코딩 방식이 다르기 때문에 렌더링 엔진이 전달받은 이미지 데이터를 특수 알고리즘을 이용하여 디코딩하고 이를 출력 가능한 형태로 변형함을 확인할 수 있었다.
추가로 렌더링 엔진은 이미지 파일 뿐만 아니라 벡터 래스터를 통해 완성된 도형 내지 폰트 글리프 역시 비트맵 형태로서 이미지 데이터를 가공한다는 사실을 알 수 있었다. 그리고 이미지 프로세싱을 통해 이미지 데이터의 해상도를 변경하거나 색상 합성, 필요에 따라 이미지를 후처리하여 필터 효과 등을 적용하는 방법도 살펴보았다.
나아가 3차원 변환 효과를 위해 원근법 구현 및 텍스처 매핑 기술을 확인해 보았고 이미지 프로세싱 과정에서 발생하는 이미지 품질 손실을 보완하기 위해 앤티에일리어싱 및 샘플링 보간 등 이미지 품질 개선 기법도 함께 살펴보았다.
마지막으로 이미지 캐싱 관리자를 통해 이미지 데이터를 재사용하는 방법을 이용하고 이미지 프로세싱의 작업 부담을 해소할 방법을 살펴보았다.