임베디드 시스템에서 이미지를 다루는 것은 항상 제약과의 싸움입니다.

특히 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의 기술적 깊이를 뒷받침합니다. 단순한 실험이 아니라, 알고리즘적으로 신중하게 설계된 포맷입니다.

한계와 고려사항

  1. 아카이브용 아님: nibnib은 디코더와 긴밀히 통합된 임베디드용 포맷입니다. 범용 아카이브에는 적합하지 않습니다.
  2. RGB 565 한정: 16비트 색상만 지원합니다. HDR이나 넓은 색역은 다루지 않습니다.
  3. 인코딩 속도: Floyd-Steinberg 디더링과 팔레트 탐색으로 인코딩이 느립니다. 실시간 인코딩에는 부적합합니다.
  4. 생태계 부재: 아직 널리 채택되지 않은 독점 포맷입니다.

개발자 테이크어웨이

nibnib이 보여주는 교훈은 세 가지입니다.

1. 제약이 혁신을 만든다. 6 사이클이라는 극한의 제약이 델타 팔레트라는 독창적인 알고리즘을 낳았습니다.

2. 비대칭 설계는 임베디드의 기본이다. 인코딩은 느려도 되고, 디코딩만 빠르면 됩니다. 임베디드 시스템에서는 이 원칙이 거의 항상 적용됩니다.

3. 팔레트 델타는 그라데이션에 강하다. 절대 색상 대신 차이를 저장하면, 부드러운 영역에서 훨씬 적은 팔레트 엔트로리로 높은 품질을 달성할 수 있습니다.

RP2350뿐만 아니라, 제한된 리소스 환경에서 이미지를 다루는 모든 개발자에게 nibnib의 접근법은 참고할 만합니다.