Optimize BMP Images for GLCD Bitmaps (Monochrome & Color)

BMP to GLCD Bitmap: Best Practices and Common PitfallsEmbedded displays such as graphic LCDs (GLCDs) are common in appliances, instruments, and hobbyist projects. These modules often accept bitmaps in a compact, device-specific format rather than standard image files like BMP. Converting BMP images into GLCD-ready bitmaps involves understanding monochrome and color formats, pixel ordering, byte packing, and display memory organization. This article explains the best practices for conversion, practical workflows, common pitfalls, and tips for debugging and optimization.


1. Understand your GLCD hardware and firmware expectations

Before converting images, read the GLCD’s datasheet and any display driver or library documentation. Key details to confirm:

  • Display resolution — width and height in pixels (e.g., 128×64).
  • Color depth — monochrome (1 bpp), grayscale (2–4 bpp), or color (typically 16-bit RGB565).
  • Byte and bit order — whether the display expects pixels packed vertically or horizontally, and whether the most significant bit (MSB) corresponds to the top/left pixel.
  • Page organization — many GLCDs use paged memory (e.g., pages of 8 vertical pixels) which affects how bytes map to pixels.
  • Pixel origin and scan direction — where (0,0) is located and whether lines are scanned left-to-right/top-to-bottom.
  • Required file or array format — some libraries expect C arrays, others require binary files or specialized headers.

Confirming these determines the conversion rules; ignoring them causes images to appear rotated, inverted, shifted, or garbled.


2. Choose the right source image and pre-process it

Start with a high-contrast source image sized appropriately for the display. Pre-processing reduces artifacts and simplifies conversion.

  • Resize to display resolution before conversion to avoid unexpected resampling. Use nearest-neighbor for pixel-art or Lanczos/Bicubic for photographic content, depending on the desired result.
  • For monochrome GLCDs, convert to grayscale then apply thresholding or dithering:
    • Thresholding creates crisp black-and-white and is suitable for logos/text. Choose thresholds manually or use Otsu’s method for automatic selection.
    • Dithering (Floyd–Steinberg, ordered) preserves tonal detail by patterning black/white pixels; useful for photos on 1 bpp displays.
  • For grayscale or limited color depth displays, quantize the image to the target bit-depth and optionally apply an error-diffusion dither to preserve gradients.
  • Clean up artifacts: sharpen edges for text, remove isolated pixels, and ensure stroke widths remain legible at the final size.

Example workflow with ImageMagick:

# Resize, convert to 1-bit with Floyd–Steinberg dithering convert input.bmp -resize 128x64! -colorspace Gray -dither FloydSteinberg -monochrome output.bmp 

3. Match GLCD memory layout: packing bits into bytes

The most frequent source of wrong output is mismatched bit/byte packing. GLCDs commonly use one of two schemes:

  • Horizontal byte orientation: each byte represents 8 horizontal pixels in a row. Example for an 8-pixel-wide group:
    • bit7 -> left pixel, bit0 -> right pixel (or vice versa depending on MSB/LSB convention).
  • Vertical byte orientation (paged displays): each byte contains 8 vertical pixels in a column (common for controllers like KS0108, ST7920 in some modes, PCD8544). Displays are often divided into pages (rows of 8 pixels), and you send data page by page.

Know both the bit order (which bit corresponds to top/left) and byte order (LSB-first or MSB-first). When implementing conversion, provide both options or test each orientation.

Example pseudocode (vertical packing, page height = 8):

for (page = 0; page < (height/8); ++page) {   for (x = 0; x < width; ++x) {     byte = 0;     for (bit = 0; bit < 8; ++bit) {       y = page*8 + bit;       if (pixel(x,y) is black) byte |= (1 << bit); // bit0 -> top     }     output_byte(byte);   } } 

4. Endianness and bit significance

Two separate concerns:

  • Byte endianness (order of bytes in multi-byte values) usually matters only for multi-byte color values (RGB565) and file formats; in GLCD bitmaps, you’re typically writing single bytes sequentially.
  • Bit significance determines which bit maps to which pixel. If MSB corresponds to the leftmost/topmost pixel, set bits accordingly. If using LSB-first packing, reverse the bit positions when building bytes.

Testing both conventions quickly reveals a swapped or mirrored image, and correcting the bit shift or reversing byte order resolves it.


5. Color conversions: RGB888 → RGB565, palettes, and gamma

For color GLCDs using 16-bit RGB565:

  • Convert from truecolor (24-bit) to RGB565 by reducing channel bits: R5, G6, B5. Use integer math: R5 = (R8 * 31 + 127) / 255, G6 = (G8 * 63 + 127) / 255, B5 = (B8 * 31 + 127) / 255.
  • Pack bits: value = (R5 << 11) | (G6 << 5) | B5.
  • Consider gamma correction or perceptual color quantization when downsampling images to reduce banding.
  • For palette-based displays, build a palette that matches the display’s color table and remap pixels using nearest color or median-cut quantization.

Example C macro for packing:

#define RGB565(r,g,b) (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)) 

6. File and code formats for embedding

Decide how the bitmap will be stored and delivered to the target:

  • C arrays: Most firmware projects embed bitmaps as const uint8_t/uint16_t arrays. Include width/height metadata and align rows/pages as the display expects.
  • Binary blobs: Useful when uploading raw framebuffer data directly to the display or to an SD card.
  • Image headers: Some drivers expect headers (e.g., for PCD8544/BMP with custom headers). Follow library examples.

Example C array (monochrome, horizontal bytes):

const uint8_t my_bitmap[] = {   0xFF, 0x00, 0xAA, // ... }; const uint16_t my_bitmap_width = 128; const uint16_t my_bitmap_height = 64; 

7. Tooling and automation

Use existing tools and scripts to avoid errors:

  • ImageMagick for simple conversions and batch processing.
  • Python with Pillow for fine-grained control (thresholds, dithering, packing).
  • Specialized converters (LCD Image Converter, GLCD Image Converter tools) that support multiple controllers and export formats.

Example Python (Pillow) snippet to pack vertical bytes:

from PIL import Image img = Image.open('output.bmp').convert('1')  # 1-bit w, h = img.size data = [] for page in range(h//8):     for x in range(w):         byte = 0         for bit in range(8):             y = page*8 + bit             if img.getpixel((x,y)) == 0:  # black pixel                 byte |= (1 << bit)         data.append(byte) with open('glcd.bin','wb') as f:     f.write(bytes(data)) 

8. Common pitfalls and how to fix them

  • Image appears shifted, wrapped, or cropped:
    • Check image dimensions and whether the display expects column-major vs row-major ordering.
  • Image is inverted (black↔white):
    • Invert bits or use the inverse threshold; some controllers use 1 = off.
  • Image is mirrored horizontally or vertically:
    • Flip the image during preprocessing or reverse byte/bit orders when packing.
  • Strange vertical bands or offset every 8 pixels:
    • Likely wrong page height or using horizontal packing when the display expects vertical pages (or vice versa).
  • Poor contrast or unreadable text:
    • Increase contrast, use sharper fonts, or avoid dithering for text.
  • Color banding after RGB565 conversion:
    • Apply dithering, gamma correction, or use higher-quality quantization before packing.

9. Testing and debugging tips

  • Start with simple test patterns: a checkerboard, vertical/horizontal lines, and a coordinate grid. These expose byte/bit order and page issues quickly.
  • Use serial/log output of packed bytes for small images to inspect bit patterns.
  • If using a library, test the simplest supported image format first (often a 1-bit bitmap) and confirm that library examples work unchanged.
  • Keep a reversible conversion script so you can re-create source images from packed data to verify correctness.

10. Performance and memory considerations

  • Flash and RAM are limited on embedded systems. Store large bitmaps in flash/PROGMEM and stream data to display rather than decompressing in RAM when possible.
  • Compress bitmaps (RLE, LZ4, custom schemes) if space is tight — but balance CPU cost of decompression vs. storage savings.
  • Use efficient loops and minimize per-pixel function calls in the packing stage. Precompute masks or use bitwise operations rather than conditionals for speed.

11. Example end-to-end workflow (summary)

  1. Read GLCD datasheet: resolution, page layout, bit-order.
  2. Prepare source image: crop/resize to resolution, adjust contrast, choose threshold/dither.
  3. Convert color depth: RGB888 → RGB565 or grayscale → 1 bpp with chosen dithering.
  4. Pack pixels into bytes matching the controller’s orientation and bit significance.
  5. Export as array or binary blob and test on hardware with simple patterns first.
  6. Iterate: tweak thresholding, dithering, or packing until the display looks correct.

12. Quick reference checklist

  • Resolution matches display.
  • Bit/byte packing matches controller (horizontal vs vertical, MSB/LSB).
  • Color depth reduced correctly (RGB565 or 1 bpp) with appropriate dithering.
  • Origin and scan direction aligned with expectations.
  • Storage format (C array, binary) matches firmware.
  • Test patterns used to validate mapping.

Converting BMP to GLCD bitmaps is largely about aligning assumptions: resolution, pixel ordering, and format. With a few small test images and knowledge of the display’s memory layout, you can automate accurate conversions and avoid the common pitfalls described above.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *