← All articles · May 4, 2026 · pixel.hjlabs.in · Powered by image2cpp

Monochrome vs Grayscale vs Color Bitmap: Memory Tradeoffs

Choosing a bitmap format is not just an aesthetic decision — it's a memory budget. A 320x240 image is 9.6KB as 1-bit mono, 38KB as 4-bit grayscale, 154KB as 16-bit RGB565, and 230KB as 24-bit RGB. This post lays out the math, the visual tradeoffs, and the right pick per microcontroller class.

The five formats you'll actually meet

FormatBits / pixelColorsUsed by
1-bit mono12 (on/off)SSD1306 OLED, e-paper B/W
2-bit grayscale24 graysSome Sharp memory LCDs
4-bit grayscale416 graysSSD1325 OLED, some e-paper
8-bit grayscale / palette8256 (gray or palette)SSD1322 OLED, Lvgl indexed
RGB5651665,536ILI9341, ST7735, ST7789 TFTs
RGB8882416.7MHDMI / desktop output (rare on MCUs)
ARGB88883216.7M + alphaLVGL true-color w/ alpha, sprites

Memory math: pick your resolution

Bytes-per-image = (width * height * bits_per_pixel) / 8

Resolution1-bit mono4-bit grayRGB565RGB888
128x64 (small OLED)1,0244,09616,38424,576
128x160 (ST7735)2,56010,24040,96061,440
240x240 (round LCD)7,20028,800115,200172,800
320x240 (ILI9341)9,60038,400153,600230,400
800x480 (7.5" e-paper)48,000192,000768,0001,152,000

What fits on your MCU

MCUFree flash for imagesLargest practical image
ATtiny85 (8KB flash)~5KB128x64 mono (1KB)
Arduino Uno (32KB)~25KB128x160 RGB565 (40KB) doesn't fit; 128x128 mono (2KB) trivial
Mega 2560 (256KB)~240KB320x240 RGB565 (154KB) fits
RP2040 (2MB)~1.8MBmultiple 320x240 RGB565
ESP32 (4MB+)~3MBessentially unlimited

1-bit mono: the workhorse

For text, line art, logos, and UI on OLEDs and e-paper, mono is the right answer. 1KB-per-128x64 means dozens of icons fit even on an ATtiny. The visual quality is excellent for line work but degrades for photos — which is what dithering solves.

Dithering: making mono look photographic

Dithering converts grayscale to a black-and-white pattern that averages to the original gray. The eye blurs the dots into smooth tones. Three algorithms, in order of quality:

For OLED splash screens and e-paper artwork, Floyd–Steinberg gives the best photo reproduction at 1-bit cost.

4-bit grayscale: the e-paper sweet spot

16 levels of gray is enough for fairly convincing photos — this is what Kindle uses. SSD1325, SSD1327 OLEDs and some advanced e-paper panels accept 4-bit input. 4 bits per pixel means 4x the memory of mono, but no dithering noise.

RGB565: the color compromise

16-bit color is the universal MCU format because it's a clean 2-byte fit and matches what TFT controllers (ILI9341, ST7735, ST7789) accept natively. The breakdown: 5 bits red, 6 bits green, 5 bits blue.

// pack RGB888 to RGB565
uint16_t rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);

Why 6 bits for green? Human vision is twice as sensitive to green wavelengths. Spending the extra bit there gives perceptually better color than 5-5-5.

RGB888 vs RGB565: visual difference

For photos, the difference is small. RGB565 has banding visible in smooth gradients (sky, skin tones) but for icons, UI, and most product imagery, you cannot tell. The 50% memory savings pays for itself.

Indexed palette (8-bit)

For UI with a limited palette (game graphics, retro UI, infographics), indexed color is brutally efficient: 8 bits per pixel, 256 entries in a palette table of 256x2 = 512 bytes. A 320x240 image is 76,800 bytes — half RGB565 size with comparable visual quality if your palette is well-chosen.

const uint16_t palette[256] PROGMEM = {0x0000, 0xFFFF, 0xF800, /* ... */};
const uint8_t image[320*240] PROGMEM = {/* indices into palette */};

// at draw time
uint8_t idx = pgm_read_byte(&image[y*320 + x]);
uint16_t color = pgm_read_word(&palette[idx]);

LVGL supports CF_INDEXED_8BIT natively. Image2cpp does not currently emit indexed output — if you need it, post-process the converter output or use LVGL's online converter.

Alpha: when do you need it?

For sprite blitting with transparency on a TFT, you have two options:

  1. Color key — pick a "magic" RGB565 color (e.g. 0xF81F magenta), skip those pixels. Zero memory cost, no per-pixel alpha.
  2. ARGB8888 with alpha — 4 bytes per pixel. Doubles RGB565 memory cost. Use only when you need anti-aliased sprite edges.

Most embedded UI uses color-key. Most desktop UI uses true alpha. The difference shows on edges of sprites — color-key has a 1-pixel "halo".

Picking the right format: the decision tree

  1. Is your display 1-bit (OLED mono, B/W e-paper)? → 1-bit mono, dither photos.
  2. Color display, AVR/Uno target, <32KB flash? → small RGB565 sprites only, or use mono with text overlay.
  3. Color display, ESP32/RP2040? → RGB565 unless you need transparency.
  4. UI with limited colors (icons, infographics)? → consider indexed 8-bit (50% RGB565 savings).
  5. Sprites with anti-aliased edges? → ARGB8888 if memory permits, else color-key RGB565.

Conversion in image2cpp

The relevant settings in pixel.hjlabs.in/converter:

Bottom line

The right format is the smallest one that looks acceptable. Always start at 1-bit, dither, see if you can live with it. Move up only when you must. Memory is the most precious resource on a microcontroller. image2cpp shows live previews of every format — iterate visually, not by spec sheet.

Read also: PROGMEM image storage on AVR and our Arduino OLED tutorial. For project marketing graphics, og.hjlabs.in generates favicons and social previews.

Try image2cpp now — free, in browser

Drop in a PNG, JPG or BMP. Get a paste-ready C++ array for Arduino, ESP32 or RP2040. 100% client-side. Learn more about image2cpp or jump straight to the tool.

Open the converter →

More from the blog