Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
12 kB · 333 lines
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334#!/usr/bin/env python3"""Generate test BMP images for verifying Bitmap.cpp format support.
Creates BMP files at 480x800 (CrossPoint display in portrait orientation).Test images use patterns designed to reveal dithering artifacts: - Checkerboard: sharp edges between gray levels, dithering adds noise at boundaries - Fine lines: thin 1px lines on contrasting background, dithering smears them - Mixed blocks: small rectangles of alternating grays, dithering blurs transitions - Gradient band: smooth transition in the middle, clean grays top/bottom
Formats generated:- 1-bit: black & white (baseline, never dithered)- 2-bit: 4-level grayscale (non-standard CrossPoint extension, won't open on PC)- 4-bit: 4-color grayscale palette (standard BMP, new support)- 8-bit: 4-color grayscale palette (colorsUsed=4, should skip dithering)- 8-bit: 256-color grayscale (full palette, should be dithered)- 24-bit: RGB grayscale gradient (should be dithered)
Usage: python generate_test_bmps.py [output_dir] Default output_dir: ./test_bmps/"""
import structimport osimport sys
WIDTH = 480HEIGHT = 800
# The 4 e-ink gray levels (luminance values)GRAY_LEVELS = [0, 85, 170, 255]
def write_bmp_file_header(f, pixel_data_offset, file_size): f.write(b'BM') f.write(struct.pack('<I', file_size)) f.write(struct.pack('<HH', 0, 0)) # reserved f.write(struct.pack('<I', pixel_data_offset))
def write_bmp_dib_header(f, width, height, bpp, colors_used=0): f.write(struct.pack('<I', 40)) # DIB header size (BITMAPINFOHEADER) f.write(struct.pack('<i', width)) top_down_height = -height f.write(struct.pack('<i', top_down_height)) f.write(struct.pack('<HH', 1, bpp)) # planes, bpp f.write(struct.pack('<I', 0)) # compression (BI_RGB) f.write(struct.pack('<I', 0)) # image size (can be 0 for BI_RGB) f.write(struct.pack('<i', 0)) # X pixels per meter f.write(struct.pack('<i', 0)) # Y pixels per meter f.write(struct.pack('<I', colors_used)) f.write(struct.pack('<I', 0)) # important colors
def write_palette(f, entries): """Write palette entries as BGRA (B, G, R, 0x00).""" for gray in entries: f.write(struct.pack('BBBB', gray, gray, gray, 0))
def get_test_pattern_index(x, y, width, height): """Return palette index (0-3) for a test pattern designed to reveal dithering.
Layout (4 horizontal bands): Band 1 (top 25%): Checkerboard of all 4 gray levels in 16x16 blocks Band 2 (25-50%): Fine horizontal lines (1px) alternating gray levels Band 3 (50-75%): 4 vertical stripes, each with a nested smaller pattern Band 4 (bottom 25%): Diagonal gradient-like pattern using the 4 levels """ band = y * 4 // height
if band == 0: # Checkerboard: 16x16 blocks cycling through all 4 gray levels bx = (x // 16) % 4 by = (y // 16) % 4 return (bx + by) % 4
elif band == 1: # Fine lines: 1px horizontal lines alternating between two gray levels # Left half: alternates black/white, Right half: alternates dark/light gray if x < width // 2: return 0 if (y % 2 == 0) else 3 # black/white else: return 1 if (y % 2 == 0) else 2 # dark gray/light gray
elif band == 2: # 4 vertical stripes, each containing a smaller checkerboard at 4x4 stripe = min(x * 4 // width, 3) inner = ((x // 4) + (y // 4)) % 2 if stripe == 0: return 0 if inner else 1 # black / dark gray elif stripe == 1: return 1 if inner else 2 # dark gray / light gray elif stripe == 2: return 2 if inner else 3 # light gray / white else: return 0 if inner else 3 # black / white (max contrast)
else: # Diagonal bands using all 4 levels return ((x + y) // 20) % 4
def get_test_pattern_lum(x, y, width, height): """Return 0-255 luminance for a continuous-tone test pattern.
Same layout as index version but uses full grayscale range to show how dithering handles non-native gray values. """ band = y * 4 // height
if band == 0: # Checkerboard with intermediate grays (not native levels) bx = (x // 16) % 4 by = (y // 16) % 4 # Use values between native levels to force dithering decisions values = [0, 64, 128, 192] return values[(bx + by) % 4]
elif band == 1: # Fine lines with intermediate values if x < width // 2: return 32 if (y % 2 == 0) else 224 # near-black / near-white else: return 96 if (y % 2 == 0) else 160 # mid-grays
elif band == 2: # Smooth horizontal gradient across full width return (x * 255) // (width - 1)
else: # Diagonal bands with smooth transitions return ((x + y) * 255 // (width + height))
def generate_1bit(path): """1-bit BMP: checkerboard pattern.""" bpp = 1 palette = [0, 255] row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT
with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette)
for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): # 16x16 checkerboard val = ((x // 16) + (y // 16)) % 2 if val: row[x >> 3] |= (0x80 >> (x & 7)) f.write(row)
print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)")
def generate_2bit(path): """2-bit BMP: 4-level grayscale test pattern (non-standard, CrossPoint extension).""" bpp = 2 palette = GRAY_LEVELS row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT
with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette)
for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): idx = get_test_pattern_index(x, y, WIDTH, HEIGHT) byte_pos = x >> 2 bit_shift = 6 - ((x & 3) * 2) row[byte_pos] |= (idx << bit_shift) f.write(row)
print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)")
def generate_4bit(path): """4-bit BMP: 4-color grayscale test pattern (standard, should skip dithering).""" bpp = 4 palette = GRAY_LEVELS row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT
with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette)
for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): idx = get_test_pattern_index(x, y, WIDTH, HEIGHT) byte_pos = x >> 1 if x & 1: row[byte_pos] |= idx else: row[byte_pos] |= (idx << 4) f.write(row)
print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)")
def generate_8bit_4colors(path): """8-bit BMP with only 4 palette entries (colorsUsed=4, should skip dithering).""" bpp = 8 palette = GRAY_LEVELS row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT
with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette)
for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): row[x] = get_test_pattern_index(x, y, WIDTH, HEIGHT) f.write(row)
print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)")
def generate_8bit_256colors(path): """8-bit BMP with full 256 palette (should be dithered normally).""" bpp = 8 palette = list(range(256)) row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT
with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette)
for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): row[x] = get_test_pattern_lum(x, y, WIDTH, HEIGHT) f.write(row)
print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)")
def generate_24bit(path): """24-bit BMP: RGB grayscale test pattern (should be dithered normally).""" bpp = 24 row_bytes = (WIDTH * bpp + 31) // 32 * 4 pixel_offset = 14 + 40 file_size = pixel_offset + row_bytes * HEIGHT
with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, 0)
for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): gray = get_test_pattern_lum(x, y, WIDTH, HEIGHT) offset = x * 3 row[offset] = gray # B row[offset + 1] = gray # G row[offset + 2] = gray # R f.write(row)
print(f" Created: {path} ({bpp}-bit)")
def main(): output_dir = sys.argv[1] if len(sys.argv) > 1 else './test_bmps' os.makedirs(output_dir, exist_ok=True)
print(f"Generating test BMPs in {output_dir}/") print(f"Resolution: {WIDTH}x{HEIGHT}") print()
generate_1bit(os.path.join(output_dir, 'test_1bit_bw.bmp')) generate_2bit(os.path.join(output_dir, 'test_2bit_4gray.bmp')) generate_4bit(os.path.join(output_dir, 'test_4bit_4gray.bmp')) generate_8bit_4colors(os.path.join(output_dir, 'test_8bit_4colors.bmp')) generate_8bit_256colors(os.path.join(output_dir, 'test_8bit_256gray_gradient.bmp')) generate_24bit(os.path.join(output_dir, 'test_24bit_gradient.bmp'))
print() print("Test pattern layout (4 horizontal bands):") print(" Band 1 (top): 16x16 checkerboard cycling all 4 gray levels") print(" Band 2: Fine 1px horizontal lines alternating grays") print(" Band 3: 4 stripes with nested 4x4 checkerboard detail") print(" Band 4 (bottom): Diagonal bands cycling all 4 levels") print() print("What to look for:") print(" Direct mapping: Sharp, clean edges between gray blocks") print(" Dithering: Noisy/speckled boundaries, smeared fine lines") print() print("Expected results on device:") print(" 1-bit: Clean B&W checkerboard, no dithering") print(" 2-bit: Clean 4-gray pattern, no dithering (non-standard BMP, won't open on PC)") print(" 4-bit: Clean 4-gray pattern, no dithering (standard BMP, viewable on PC)") print(" 8-bit (4 colors): Clean 4-gray pattern, no dithering (standard BMP, viewable on PC)") print(" 8-bit (256 colors): Same layout but with intermediate grays, WITH dithering") print(" 24-bit: Same layout but with intermediate grays, WITH dithering") print() print("Note: 2-bit BMP is a non-standard CrossPoint extension. Standard image viewers") print("will not open it. Use the 4-bit BMP instead for images created with standard tools") print("(e.g. ImageMagick: convert input.png -colorspace Gray -colors 4 -depth 4 BMP3:output.bmp)") print() print("Copy files to /sleep/ folder on SD card to test as custom sleep screen images.")
if __name__ == '__main__': if any(arg in ("-h", "--help") for arg in sys.argv[1:]): print(__doc__.strip()) sys.exit(0) main()