6 minute read

디지털 이미지를 표현하는 가장 기본적인 방식 중 하나는 여러 개의 픽셀이 각자의 색상 정보를 비트의 조합으로 저장하여 구성하는 것입니다. 안드로이드에서는 android.graphics.Bitmap 클래스를 통해 이러한 구조의 디지털 이미지를 제어하고 처리할 수 있습니다.

Bitmap 클래스는 2차원 픽셀 배열로 구성된 이미지를 메모리에 올려두고, 이를 픽셀 단위로 접근하여 조작하거나 화면에 그릴 수 있게 해주는 역할을 합니다. 추가로 Android에서 Bitmap은 이미지의 크기에 따라 메모리를 굉장히 많이 차지할 수 있기 때문에 이를 주의하여 관리하는 것이 중요합니다.


Bitmap.Config

Android에서 사용하는 Bitmap은 가로 x 세로 픽셀 수로 구성된 디지털 이미지이며, 이들은 각자의 색상 값으로 구성 되어있습니다. 이때 픽셀의 색상 값을 어떤 방식으로 저장해둘까에 대한 정보는 Bitmap.Config를 통해 저장됩니다. 이는 픽셀 하나가 어떤 방식으로 메모리에 저장되는지에 대한 정보이며, Bitmap의 메모리 사용량은 Bitmap.Config에 따라 달라집니다. Config는 각 픽셀이 얼마나 많은 비트를 사용하는지와 이미지의 색상 품질과 메모리 사용량을 결정짓는 중요한 요소입니다.

  public enum Config {
        ALPHA_8(1),
        RGB_565(3),
        @Deprecated
        ARGB_4444(4),
        ARGB_8888(5),
        RGBA_F16(6),
        HARDWARE(7),
        RGBA_1010102(8);
	...
    }

아래에서부터는 Config의 요소에 대해 순차적으로 다루어보겠습니다.


ALPHA_8

ALPHA_8은 색상 정보 없이 오직 Alpha 값만 저장하는 포맷으로 각 픽셀당 8비트만 사용됩니다.

해당 포맷은 마스크 또는 Alpha 채널을 분리하여 따로 관리할 때 유용합니다. 대표적인 예시를 들자면 그림자와 같은 마스크 이미지들은 투명도만 필요한데, 이때 ALPHA_8을 사용하면 메모리양을 크기 줄일 수 있습니다. 아래 예시와 같이 투명도 50% 파란색 스타일의 원을 그렸지만, 실제로는 ALPHA_8 설정을 사용하였기 때문에, 단일 채널의 원으로 표시되는 것을 확인할 수 있습니다.

// ALPHA_8을 사용하여 Bitmap을 생성합니다.
val bitmap: Bitmap = createBitmap(500, 500, Bitmap.Config.ALPHA_8)
// 투명도 50% 파란색 스타일의 Paint를 설정합니다.
val paint: Paint = Paint().apply {
    color = Color.BLUE
    alpha = 128
}
// Canvas에 Bitmap을 추가한 뒤, 지정한 스타일의원을 그립니다.
val canvas: Canvas = Canvas(bitmap)
canvas.drawCircle(
	/* cx = */ 250f,
	/* cy = */ 250f,
	/* radius = */ 100f,
	/* paint = */ paint
)

RGB_565

RGB_565는 상대적으로 적은 메모리로 이미지를 표현할 수 있는 포맷입니다. 구성은 Red 5비트, Green 6비트, Blue 5비트를 사용하여 총 16비트가 픽셀에 저장됩니다. (이를 16비트 RGB라고 칭하기도 합니다.)

해당 포맷은 RGB 채널만 저장되고, Alpha(투명도) 값을 저장하지 않기 때문에 투명한 이미지는 표현할 수 없지만, 메모리 절약과 같은 중요한 상황에서는 유용하게 쓰입니다.

G 채널에 6비트를 부여한 이유는 사람의 눈이 빨간색과 파란색보다 초록색에 더욱 민감하므로 비트를 더 할당하여 이미지를 정확하게 표현할 수 있기 때문입니다.

https://en.wikipedia.org/wiki/High_color?utm_source=chatgpt.com

ARGB_4444 (deprecated)

ARGB_4444는 세 개의 RGB 채널과 A 채널이 각각 4비트를 보유하여 총 16비트로 저장되는 이미지 포맷입니다. 해당 포맷은 품질이 좋지 않다는 이유로 ARGB_8888 사용을 권장하며, deprecated 되었습니다.

ARGB_8888

ARGB_8888은 가장 일반적으로 사용되는 픽셀 포맷으로 Alpha, Red, Green, Blue 네 가지 색상 채널 각각 8비트(1바이트)를 차지하여 총 32비트(4바이트)가 한 픽셀에 사용됩니다.

이에 따라 색상 품질은 매우 뛰어나지만, 메모리 사용량이 크다는 단점이 존재합니다. 예를 들어 1080 x 1920 해상도의 이미지를 ARGB_8888로 저장하면 약 8MB 이상의 메모리가 소모됩니다.

ARGB_F16

ARGB_F16은 색상 데이터를 부동 소수점 형식으로 저장하는 포맷입니다. 각 채널은 16비트 부동소수점으로 저장되며, 총 64비트(8바이트)가 한 픽셀이 됩니다. 주로 정확한 색 보존이 필요한 작업이나 HDR 이미지를 처리할 때 사용됩니다.

HDR(High Dynamic Range)이란 영상이나 이미지에서 밝은 부분과 어두운 부분의 디테일을 동시에 표현할 수 있는 기술입니다. 영화,드라마,넷플릭스,게임 같은 서비스들에서 HDR 콘텐츠를 제공하고 있으며, HDR을 지원해 주는 디스플레이를 통해 사실적인 컨텐츠를 이미지를 감상할 수 있습니다

HARDWARE

Bitmap.Config.HARDWAREHardware Bitmap을 사용하기 위한 특수한 설정으로 기존의 CPU 메모리 기반 Bitmap과 달리, GPU 전용 메모리에 이미지를 저장하는 방식입니다.

일반적인 Bitmap은 RAM에 위치하며, CPU가 직접 픽셀 데이터를 읽고 쓰는 구조입니다. 하지만 최종적으로 화면에 그리기 위해서는 결국 GPU로 데이터를 복사한 후, GPU가 텍스처로 변환하여 렌더링하게 되므로 이 과정에서 성능 저하가 발생할 수 있게 됩니다.

Hardware Bitmap은 이 문제를 해결하기 위해 도입되었으며, GPU 메모리에 직접 저장되어 GPU가 바로 접근하고 그릴 수 있게 됩니다. 물론 이러한 구조 덕분에 렌더링 속도는 매우 빨라지지만, immutable 속성 제약이 따르게 됩니다. 이 때문에 픽셀 데이터를 직접 수정하거나 필터를 적용하는 경우에는 해당 포맷을 사용해서는 안 됩니다.

RGBA_1010102

RGBA_1010102은 RGB 채널 각각이 10비트와 2비트의 Alpha 채널로 구성된 포맷입니다. 이 구성은 Alpha 블렌딩이 필요 없는 HDR 컨텐츠에 적합하며 ARGB_8888보다 더 높은 품질을 가지고 있습니다.


Mutable Immutable

Android에서 Bitmap은 생성 시점에서 수정 가능 여부를 설정할 수 있습니다. 이 속성은 isMutable 플래그 필드를 통해 구분되며, Bitmap의 생성 방식에 따라 자동으로 결정되거나 명시적으로 지정할 수 있습니다.

mutable로 생성된 BitmapCanvas를 사용하여 이미지를 그리거나, setPixel() 등의 메서드를 사용하는 방식 등을 통해 픽셀 데이터를 자유롭게 수정할 수 있습니다. Bitmap.createBitmap() 시리즈의 메서드로 생성할 경우 대다수는 mutable의 속성을 가지게 됩니다. (다른 비트맵을 복사해서 생성하는 케이스는 다른 비트맵의 isMutable 플래그에 따라 달라집니다.)

Immutable Bitmap의 경우 기본적으로 수정할 수 없는 상태로 만들어집니다. 이는 편집이 불가능하다는 단점이 존재하지만, 메모리를 안전하게 관리하거나 성능을 최적화할 수 있다는 장점이 존재합니다.

BitmapFactory.decode() 메서드와 HARDWARE Config를 가지고 생성할 경우 immutable의 속성을 가질 수 있게 됩니다. 별개로 BitmapFactory.decode() 메서드는 isMutable 플래그를 true로 설정할 경우 mutable한 비트맵을 얻을 수 있게 됩니다.

public final boolean isMutable() {
    return !nativeIsImmutable(mNativePtr);
}

...

@CriticalNative
private static native boolean nativeIsImmutable(long nativePtr);

Alpha

앞서 Bitmap.Config에서 픽셀을 저장하는 여러 가지 포맷에 대해 다루어 보았으며, 그중 ARGB_8888과 같이특정 포맷은 Alpha 채널을 포함하고 있다는 사실을 알게 되었습니다.

Alpha 채널은 각 픽셀의 투명도를 나타내는 값으로, 픽셀이 얼마나 보일지를 결정합니다. 예를 들어, 우리가 투명한 이미지를 볼 때, 앞쪽 이미지가 반투명하다면 뒷배경이 함께 비추어서 보인다는 것을 확인할 수 있습니다.

즉, Alpha 채널이 포함된 픽셀은 완전히 불투명하거나 부분적으로 투명할 수도 있으며, 이러한 Alpha 채널을 사용하면 현실 세계와 같이 다양한 이미지를 만들어 낼 수 있게 됩니다. 위와 같은 투명한 픽셀을 처리하기 위해서는 블렌딩 기법을 사용해야 하며 이를 통해 투명도 표현이 가능한 이미지를 자연스럽게 화면에 표현할 수 있게 됩니다.

블렌딩

블렌딩은 두 개의 색상을 섞어서 하나의 최종 색상을 만드는 기법으로 앞의 픽셀 색상과 Alpha 값을 기준으로, 뒤에 있는 배경 픽셀과 조합하여 최종 색상을 결정하는 그래픽 연산입니다. 블렌딩의 대표적인 수식은 아래와 같습니다.

color = srcColor × srcAlpha + dstColor × (1 - srcAlpha)
// color: 최종적으로 화면에 렌더링이 될 색상
// srcColor: 앞 픽셀의 색상
// srcAlpha: 앞 픽셀의 투명도
// dstColor: 뒷 배경이 될 픽셀의 색상

예를 들어, RGB가 각각 0, 0, 255인 배경이 있고, 그 위에 RGB 255,0,0과 0.5의 alpha를 가진 도형이 있다고 가정해 보겠습니다. 배경과 도형의 색상을 블렌딩 공식에 넣었을 때, 아래와 같이 계산됩니다. 이때 0 ~ 255 로 표시된 색상 값은 0.0 ~ 1.0 사이로 정규화하여 계산을 수행합니다. ( color / 255 )

// srcColor 도형 (1.0,0.0,0.0)
// dstColor 배경 (0.0,0.0,1.0)
// 투명도 0.5
color = (1.0, 0.0, 0.0) × 0.5 + (0.0, 0.0, 1.0) × (1 - 0.5)
color = (0.5, 0.0, 0.0) + (0.0, 0.0, 0.5)
color = (0.5, 0.0, 0.5)

계산 결과 대략 RGB(128,0,128) 정도의 색상 값으로 블렌딩이 됩니다.

hasAlpha

Bitmap.hasAlpha()는 해당 Bitmap 이미지가 Alpha 채널을 포함하는지를 확인하는 플래그입니다. hasAlpha의 플래그가 true일 경우 해당 이미지는 픽셀마다 투명및 불투명 여부를 별도로 가질 수 있음을 의미합니다.

Alpha 채널은 픽셀을 그릴 때, 블렌딩 연산이 필요하게 만듭니다. Alpha 채널이 없을 경우에는 단순히 픽셀을 덮어쓰면 되지만, 존재할 경우에는 블렌딩 연산을 거쳐야 하기 때문에 추가 비용이 발생합니다. 해당 플래그는 Alpha 채널이 존재하는지를 빠르게 판단하여 블렌딩 연산 과정을 생략하고 이에 따라 렌더링 속도를 향상시킬수 있게 도와줄 수 있습니다.

public final boolean hasAlpha() {
    if (mRecycled) {
        Log.w(TAG, "Called hasAlpha() on a recycle()'d bitmap! This is undefined behavior!");
    }
    return nativeHasAlpha(mNativePtr);
}

isPremultiplied

isPremultiplied 또한 마찬가지로 Bitmap에서 Alpha 채널 및 블렌딩과 연관이 있는 필드 중 하나입니다. 앞서 Alpha 채널이 존재할 경우 블렌딩을 수행하기 위한 부가적인 연산을 요구로 해야 한다는 점을 언급하였습니다. 이때 premultiplied 상태에서는 이 연산 과정을 미리 계산해 두기 때문에 궁극적인 이미지를 처리하는 과정이 단순화됩니다.

isPremultiplied 플래그는 현재 픽셀 데이터가 premultiplied alpha 방식으로 저장되어 있는지에 대한 여부를 반환합니다. 만약 true 일 경우 픽셀 내부가 이미 A×R, A×G, A×B 형태로 저장되어 있음을 의미하며, false일 경우 픽셀이 원래 R, G, B 값을 그대로 유지하고 있음을 의미합니다.

단, isPremultiplied 플래그가 true이지만 픽셀 데이터는 연산 처리가 되어있지 않은 RGB 값이거나, 반대로 false이지만 픽셀 데이터가 premultiplied 연산이 되어있을 경우 잘못된 픽셀 결과가 출력될 수 있습니다.

물론 일반적으로 Bitmap.isPremultiplied 플래그는 대부분은 자동으로 true 플래그로 설정됩니다.

val bitmap: Bitmap = createBitmap(500, 500, Bitmap.Config.ARGB_8888)
println(bitmap.isPremultiplied)
//true 반환

이번 글에서는 Android Bitmap에 대한 소개와 일부 필드 속성에 대해 다루어보았습니다. 다음 이어지는 글에서 다른 Bitmap 속성들에 관한 내용과 간략한 내부 구조에 대한 설명으로 이어 나가겠습니다.