A scene is an entry in the open registry LabScenes, shown on a plate: a flat lit disc, the package's own small sky with the sun at one hour, and a frame clock. Headless each camera (lab_shot, lab_shot_at) settles and is written as build/lab/<scene>/<shot>.png (a stored PNG, written here); in a window N and P step through them. tools/lab/run.sh and sheet.py make the contact sheet; tools/lab/plate_build.py makes plate/. ludic.render3d gains r3d_plate_mode (no terrain, grass or water is made) and r3d_sky_path. quit() in a program with no frame loop links. The example takes ~120 MB resident, 417 MiB peak footprint. Reseed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
29 lines
1.2 KiB
Python
Executable file
29 lines
1.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""sheet.py DIR [DIR ...] - a contact sheet of a lab scene's shots: DIR/sheet.png, the PNGs in DIR
|
|
(sheet.png itself excepted) in name order, two to a row at 640 wide, each captioned with its name."""
|
|
import glob, os, sys
|
|
from PIL import Image, ImageDraw
|
|
|
|
def sheet(out):
|
|
paths = [p for p in sorted(glob.glob(os.path.join(out, '*.png'))) if os.path.basename(p) != 'sheet.png']
|
|
if not paths:
|
|
print(f'lab: {out}: no shots')
|
|
return 1
|
|
ims = [Image.open(p).convert('RGB') for p in paths]
|
|
w = 640
|
|
h = w * ims[0].height // ims[0].width
|
|
cols = 2 if len(ims) > 1 else 1
|
|
rows = (len(ims) + cols - 1) // cols
|
|
s = Image.new('RGB', (w * cols, h * rows), (0, 0, 0))
|
|
d = ImageDraw.Draw(s)
|
|
for i, (p, im) in enumerate(zip(paths, ims)):
|
|
x, y = (i % cols) * w, (i // cols) * h
|
|
s.paste(im.resize((w, h)), (x, y))
|
|
d.rectangle((x, y, x + w, y + 18), fill=(0, 0, 0))
|
|
d.text((x + 6, y + 3), os.path.splitext(os.path.basename(p))[0], fill=(255, 255, 255))
|
|
s.save(os.path.join(out, 'sheet.png'))
|
|
print(f'lab: {out}: {len(ims)} shots, sheet.png')
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(max([sheet(d) for d in sys.argv[1:]] or [2]))
|