Thumbnail.

Dopamine

Description

#!/usr/bin/env python3
"""Create a 4096x4096 allRGB image resembling fluorescent dopaminergic neurons.

Every RGB triplet occurs exactly once. Requires numpy, pillow and scipy.
Example: python dopamine_allrgb.py --seed 17 --output dopamine_allrgb.png

The scene is illustrative, not a real micrograph. Sorting the 24-bit palette
by luminance and matching it to the scene's luminance preserves structure while
ensuring the entire RGB gamut is used exactly once.
"""

import argparse
import gc
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
from scipy.ndimage import gaussian_filter

SIZE = 4096
SMALL = 1024
N = SIZE * SIZE


def make_scene(seed):
    rng = np.random.default_rng(seed)
    # Draw at quarter resolution and supersample the finished luminous scene.
    glow = Image.new('L', (SMALL, SMALL), 0)
    sharp = Image.new('L', (SMALL, SMALL), 0)
    gd, sd = ImageDraw.Draw(glow), ImageDraw.Draw(sharp)

    # Curved axon/dendrite paths, with bead-like varicosities and soma shapes.
    for _ in range(33):
        x, y = rng.uniform(60, 970, 2)
        theta = rng.uniform(0, 2 * np.pi)
        length = int(rng.integers(80, 260))
        points = [(x, y)]
        for step in range(length):
            theta += rng.normal(0, 0.085)
            x += np.cos(theta) * rng.uniform(1.3, 2.8)
            y += np.sin(theta) * rng.uniform(1.3, 2.8)
            points.append((x, y))
            if step % 12 == 0 and 0 <= x < SMALL and 0 <= y < SMALL:
                r = rng.uniform(1.8, 5.0)
                gd.ellipse((x-r*2, y-r*2, x+r*2, y+r*2), fill=205)
                sd.ellipse((x-r, y-r, x+r, y+r), fill=240)
        gd.line(points, fill=120, width=9, joint='curve')
        sd.line(points, fill=195, width=2, joint='curve')

    for _ in range(9):
        x, y = rng.uniform(80, 940, 2)
        r = rng.uniform(12, 31)
        gd.ellipse((x-r*1.8, y-r*1.8, x+r*1.8, y+r*1.8), fill=220)
        sd.ellipse((x-r, y-r, x+r, y+r), fill=225)
        sd.ellipse((x-r*.42, y-r*.42, x+r*.42, y+r*.42), fill=45)

    for _ in range(3600):
        x, y = rng.uniform(0, SMALL, 2)
        r = rng.uniform(.4, 2.5)
        value = int(rng.integers(75, 230))
        sd.ellipse((x-r, y-r, x+r, y+r), fill=value)

    # Composite broad halo and crisp signal.
    g = np.asarray(glow.filter(ImageFilter.GaussianBlur(13)), dtype=np.float32)
    s = np.asarray(sharp.filter(ImageFilter.GaussianBlur(1.2)), dtype=np.float32)
    field = np.clip(0.9*g + 1.05*s, 0, 255)
    scene = Image.fromarray(field.astype(np.uint8), 'L').resize(
        (SIZE, SIZE), Image.Resampling.BICUBIC)
    del g, s, field
    return np.asarray(scene, dtype=np.uint8).copy()


def render(seed, output):
    rng = np.random.default_rng(seed + 1)
    scene = make_scene(seed)
    # A low-level uneven background plus film-like digital grain.
    arr = scene.astype(np.float32)
    del scene
    # Dimming some bright processes preserves highlights in the allRGB remap.
    arr *= 0.82
    arr += rng.normal(0, 13, size=arr.shape).astype(np.float32)
    arr += 16
    np.clip(arr, 0, 255, out=arr)
    target = arr.astype(np.uint8).ravel()
    del arr

    print('Sorting pixels by fluorescence intensity...', flush=True)
    # Stable sort is unnecessary: randomized grain breaks up equal-valued regions.
    pixel_order = np.argsort(target, kind='stable')
    del target
    gc.collect()

    print('Sorting the complete RGB palette by perceived brightness...', flush=True)
    palette_ids = np.arange(N, dtype=np.uint32)
    r = (palette_ids >> 16).astype(np.uint16)
    g = ((palette_ids >> 8) & 255).astype(np.uint16)
    b = (palette_ids & 255).astype(np.uint16)
    # Integer BT.709 brightness approximation. All RGB colors are retained.
    luminosity = (54*r + 183*g + 19*b).astype(np.uint16)
    del r, g, b
    palette_order = np.argsort(luminosity, kind='stable')
    del luminosity
    gc.collect()

    print('Distributing each RGB value exactly once...', flush=True)
    # Keep peak RAM relatively low: uint32 packed pixels, then a uint8 RGB view.
    packed = np.empty(N, dtype=np.uint32)
    # Chunk to avoid an additional enormous temporary array.
    chunk = 500_000
    for start in range(0, N, chunk):
        end = min(start + chunk, N)
        packed[pixel_order[start:end]] = palette_ids[palette_order[start:end]]
    del palette_ids, palette_order, pixel_order
    gc.collect()

    # Convert 0xRRGGBB packed numbers to an RGB byte array.
    rgb = np.empty((N, 3), dtype=np.uint8)
    rgb[:, 0] = packed >> 16
    rgb[:, 1] = packed >> 8
    rgb[:, 2] = packed
    del packed
    Image.fromarray(rgb.reshape(SIZE, SIZE, 3), 'RGB').save(output, optimize=False)
    print(f'Saved {output}: {SIZE} x {SIZE}, all {N:,} RGB colors exactly once.')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--output', default='dopamine_allrgb.png')
    parser.add_argument('--seed', type=int, default=17)
    args = parser.parse_args()
    render(args.seed, args.output)


if __name__ == '__main__':
    main()
python dopamine_allrgb.py --output dopamine_allrgb.png --seed 17

Author

ACJ
79 entries

Stats

Date
Colors16,777,216
Pixels16,777,216
Dimensions4,096 × 4,096
Bytes50,247,447