임베디드 시스템에서 이미지를 다루는 것은 항상 제약과의 싸움입니다.
특히 RP2350 같은 마이크로컨트롤러에서는 상황이 더 극단적입니다. 이 칩으로 DVI 출력을 하려면, 픽셀당 약 6 사이클의 예산이 주어집니다. 이 안에서 해야 할 일은:
1. 픽셀 데이터 페치 (메모리 읽기)
2. 팔레트 룩업 (색상 변환)
3. 라인 버퍼에 저장 (메모리 쓰기)
총 6 사이클 — 여유가 거의 없습니다.
Raphael Finkel이 만든 nibnib 이미지 포맷은 이 극한의 제약 속에서 태어났습니다. 그리고 단순한 압축을 넘어, 알고리즘적으로 흥미로운 접근법을 제시합니다.
nibnib의 핵심 아이디어: 델타 팔레트
일반적인 팔레트 기반 이미지 포맷은 각 픽셀에 인덱스를 저장하고, 디코딩 시 팔레트에서 절대 색상을 가져옵니다.
일반 팔레트 이미지:
데이터: [3, 7, 12, 5, ...] (인덱스)
팔레트: [0xFF0000, 0x00FF00, 0x0000FF, ...]
디코딩: 인덱스 → 팔레트에서 절대 색상
문제: 부드러운 영역(그라데이션)에서 색상 간 차이가 크면
팔레트에 많은 엔트리가 필요하고, 양자화 오류가 눈에 띈다.
nibnib은 팔레트에 절대 색상이 아닌 델타(차이)를 저장합니다.
nibnib 델타 팔레트:
데이터: [3, 7, 12, 5, ...] (인덱스)
팔레트: [0xFF0000, +0x010101, -0x020100, ...] (델타)
디코딩: 이전 픽셀 × 2 + 델타 → 두 개의 RGB 565 픽셀
이 접근법의 핵심은 이전 픽셀을 기준으로 색상을 조정한다는 점입니다. 부드러운 영역에서는 인접한 픽셀의 색상이 비슷하므로, 작은 델타로 정확히 표현할 수 있습니다.
디코딩 알고리즘: 바이트 한 개에 픽셀 두 개
nibnib의 디코딩은 단순하고 결정론적입니다.
디코딩 루프 (픽셀 쌍 단위):
1. 마지막에 디코딩한 픽셀을 2배로 늘림 (left = right = prev)
2. 입력 스트림에서 바이트 한 개 읽음 → palette[index]
3. palette[index]를 2배 픽셀에 더함
4. 결과의 하위 16비트 = 왼쪽 픽셀 (RGB 565)
상위 16비트 = 오른쪽 픽셀 (RGB 565)
5. 다음 쌍으로 진행
한 바이트 → 두 픽셀 = 4bpp 압축
이 알고리즘은 마이크로컨트롤러에서 매우 효율적으로 구현할 수 있습니다.
RP2350에서의 사이클 추정:
prev_pixel << 1 → 1 사이클
load_byte(input) → 1 사이클
load_palette(index) → 2 사이클 (L1 캐시)
add + split → 1 사이클
store_to_line_buffer → 1 사이클
총: 약 6 사이클 / 픽셀 쌍
파일 포맷 구조
┌──────────────┐
│ Width (2B) │ 이미지 너비 (16-bit)
├──────────────┤
│ Height (2B) │ 이미지 높이 (16-bit)
├──────────────┤
│ Palette │ 256 × 32-bit 델타 엔트리
│ (8192 bytes) │
├──────────────┤
│ Row 0 │ compressed data (stride = 4바이트 정렬)
├──────────────┤
│ Row 1 │
├──────────────┤
│ ... │
└──────────────┘
로우 스트라이드는 4바이트의 배수로 반올림됩니다. 이는 RP2350의 DMA 전송과 호환되기 위한 설계 결정입니다.
인코딩: Floyd-Steinberg 디더링
디코딩은 단순하지만, 인코딩은 복잡합니다. nibnib은 비대칭 설계를 채택했습니다.
인코딩: 느림 (고품질 팔레트 탐색 + Floyd-Steinberg 디더링)
디코딩: 빠름 (단순 델타 룩업)
임베디드 환경에서는 디코딩 속도가 훨씬 중요하다.
인코딩은 한 번만 하고, 디코딩은 매 프레임 반복된다.
Floyd-Steinberg 디더링은 양자화 오류를 인접 픽셀에 분산시켜, 부드러운 그라데이션을 더 자연스럽게 표현합니다.
Floyd-Steinberg 오류 분산:
현재 픽셀 [X] → 다음 픽셀에 오류 분배:
[X] 7/16
↓
┌───┬───┬───┐
│ │ 3/16│ 5/16│
└───┴───┴───┘
1/16 3/16
오류가 주변 픽셀에 분산되어, 양자화 오류가 눈에 덜 띈다.
저자 배경 — Raphael Finkel
nibnib의 저자는 Raphael Finkel로, 컴퓨터 과학 분야에서 잘 알려진 인물입니다.
- TeX의 picture 환경 창안자
- Microsoft Research에서 근무
- 여러 알고리즘 및 컴파일러 관련 논문 저자
이 배경은 nibnib의 기술적 깊이를 뒷받침합니다. 단순한 실험이 아니라, 알고리즘적으로 신중하게 설계된 포맷입니다.
한계와 고려사항
- 아카이브용 아님: nibnib은 디코더와 긴밀히 통합된 임베디드용 포맷입니다. 범용 아카이브에는 적합하지 않습니다.
- RGB 565 한정: 16비트 색상만 지원합니다. HDR이나 넓은 색역은 다루지 않습니다.
- 인코딩 속도: Floyd-Steinberg 디더링과 팔레트 탐색으로 인코딩이 느립니다. 실시간 인코딩에는 부적합합니다.
- 생태계 부재: 아직 널리 채택되지 않은 독점 포맷입니다.
개발자 테이크어웨이
nibnib이 보여주는 교훈은 세 가지입니다.
1. 제약이 혁신을 만든다. 6 사이클이라는 극한의 제약이 델타 팔레트라는 독창적인 알고리즘을 낳았습니다.
2. 비대칭 설계는 임베디드의 기본이다. 인코딩은 느려도 되고, 디코딩만 빠르면 됩니다. 임베디드 시스템에서는 이 원칙이 거의 항상 적용됩니다.
3. 팔레트 델타는 그라데이션에 강하다. 절대 색상 대신 차이를 저장하면, 부드러운 영역에서 훨씬 적은 팔레트 엔트로리로 높은 품질을 달성할 수 있습니다.
RP2350뿐만 아니라, 제한된 리소스 환경에서 이미지를 다루는 모든 개발자에게 nibnib의 접근법은 참고할 만합니다.