Project

General

Profile

gen_bmp.py

Alexandru Lungu, 03/04/2026 06:30 AM

Download (4.41 KB)

 
1
#!/usr/bin/env python3
2
"""
3
Generate 100 small BMP images: img001.bmp .. img100.bmp
4
- 16x16 pixels
5
- 8-bit indexed (palette) BMP (BI_RGB, uncompressed)
6
- Each image has a distinct pattern + embedded index color cue.
7

8
No external dependencies.
9
"""
10

    
11
import os
12
import struct
13
from pathlib import Path
14

    
15
OUT_DIR = Path(r"images/")  # change to your folder
16
WIDTH, HEIGHT = 16, 16
17
BPP = 8  # indexed 8-bit
18
NUM_IMAGES = 100
19

    
20
def bmp_bytes_indexed_8bit(width: int, height: int, pixels_topdown: bytes, palette_bgr0: bytes) -> bytes:
21
    """
22
    Build a minimal BMP (BITMAPFILEHEADER + BITMAPINFOHEADER + palette + pixel array).
23
    pixels_topdown: width*height bytes, row-major top-to-bottom.
24
    palette_bgr0: 256*4 bytes, each entry B,G,R,0.
25
    """
26
    assert len(pixels_topdown) == width * height
27
    assert len(palette_bgr0) == 256 * 4
28

    
29
    # BMP rows are stored bottom-up by default, and each row padded to 4-byte boundary.
30
    row_bytes = width  # 1 byte per pixel at 8bpp
31
    row_padded = (row_bytes + 3) & ~3
32
    padding = b"\x00" * (row_padded - row_bytes)
33

    
34
    # Convert top-down buffer to bottom-up with padding per row
35
    pixel_rows = []
36
    for y in range(height - 1, -1, -1):
37
        start = y * width
38
        pixel_rows.append(pixels_topdown[start:start + width] + padding)
39
    pixel_array = b"".join(pixel_rows)
40

    
41
    # Headers
42
    bfType = b"BM"
43
    bfOffBits = 14 + 40 + len(palette_bgr0)  # file header + info header + palette
44
    bfSize = bfOffBits + len(pixel_array)
45

    
46
    file_header = struct.pack("<2sIHHI", bfType, bfSize, 0, 0, bfOffBits)
47

    
48
    biSize = 40
49
    biWidth = width
50
    biHeight = height  # positive => bottom-up
51
    biPlanes = 1
52
    biBitCount = BPP
53
    biCompression = 0  # BI_RGB
54
    biSizeImage = len(pixel_array)
55
    biXPelsPerMeter = 2835  # ~72 DPI
56
    biYPelsPerMeter = 2835
57
    biClrUsed = 256
58
    biClrImportant = 0
59

    
60
    info_header = struct.pack(
61
        "<IIIHHIIIIII",
62
        biSize, biWidth, biHeight, biPlanes, biBitCount,
63
        biCompression, biSizeImage,
64
        biXPelsPerMeter, biYPelsPerMeter,
65
        biClrUsed, biClrImportant
66
    )
67

    
68
    return file_header + info_header + palette_bgr0 + pixel_array
69

    
70
def make_palette() -> bytes:
71
    """
72
    Create a simple 256-color palette (B,G,R,0).
73
    We'll do a grayscale ramp, but reserve a few indices for accent colors.
74
    """
75
    pal = bytearray()
76
    for i in range(256):
77
        # default grayscale
78
        r = g = b = i
79
        pal += bytes([b, g, r, 0])
80

    
81
    # Add a few vivid accents (indices near the end)
82
    accents = {
83
        250: (255, 0, 0),    # red
84
        251: (0, 255, 0),    # green
85
        252: (0, 0, 255),    # blue
86
        253: (255, 255, 0),  # yellow
87
        254: (255, 0, 255),  # magenta
88
        255: (0, 255, 255),  # cyan
89
    }
90
    for idx, (r, g, b) in accents.items():
91
        off = idx * 4
92
        pal[off:off+4] = bytes([b, g, r, 0])
93

    
94
    return bytes(pal)
95

    
96
def generate_pixels(i: int, width: int, height: int) -> bytes:
97
    """
98
    Create a distinct 16x16 pattern per index using simple math:
99
    - background is grayscale gradient
100
    - a diagonal + border use accent colors depending on i
101
    """
102
    accent_idx = 250 + (i % 6)  # 250..255
103

    
104
    buf = bytearray(width * height)
105
    for y in range(height):
106
        for x in range(width):
107
            # base grayscale pattern varies by i
108
            base = (x * 16 + y * 8 + i * 3) & 0xFF
109

    
110
            # border
111
            if x == 0 or y == 0 or x == width - 1 or y == height - 1:
112
                val = accent_idx
113
            # diagonal
114
            elif x == y or x == (width - 1 - y):
115
                val = accent_idx
116
            # small "marker" block encoding i
117
            elif 2 <= x <= 5 and 2 <= y <= 5:
118
                val = (i * 7 + x * 13 + y * 17) & 0xFF
119
            else:
120
                val = base
121

    
122
            buf[y * width + x] = val
123

    
124
    return bytes(buf)
125

    
126
def main():
127
    OUT_DIR.mkdir(parents=True, exist_ok=True)
128
    palette = make_palette()
129

    
130
    sizes = []
131
    for i in range(1, NUM_IMAGES + 1):
132
        pixels = generate_pixels(i, WIDTH, HEIGHT)
133
        bmp = bmp_bytes_indexed_8bit(WIDTH, HEIGHT, pixels, palette)
134
        fname = OUT_DIR / f"img{i:03d}.bmp"
135
        fname.write_bytes(bmp)
136
        sizes.append((fname.name, len(bmp)))
137

    
138
    # Print a small report
139
    min_s = min(s for _, s in sizes)
140
    max_s = max(s for _, s in sizes)
141
    print(f"Generated {NUM_IMAGES} BMPs in: {OUT_DIR}")
142
    print(f"Size range: {min_s} .. {max_s} bytes")
143
    print("First 5 sizes:", sizes[:5])
144

    
145
if __name__ == "__main__":
146
    main()