Posse
Camera's zetten elke bezoeker om in een gegenereerd figuur in dezelfde houding, teruggebracht tot 18 bij 32 pixels op een wand van schermen.
Ga voor een van de schermen staan. Even later verschijnt er een figuur dat jouw houding aanneemt, gekleed in de stijl van de tentoonstelling Supreme – Resampling the World. Van dichtbij is het een vlak van kleur; van een afstand, of door de camera van je telefoon, herken je jezelf. Beweeg, en het figuur beweegt mee. In twee maanden in de lobby van Design Museum Den Bosch registreerde Posse ruim 150.000 interacties. Daarna reisde de installatie naar TORCH in Amsterdam.
In Den Bosch bestond Posse uit tien Raspberry Pi 5's met Pi-camera's, elk met een YOLO-posemodel, en een centrale Django-server op het Mesmera-platform die de Pi's over websockets aanstuurde. Ik bouwde de pose-detectie op de Pi's, de server, de stap van pose naar gegenereerd beeld en de omzetting naar pixel-SVG. Voor de vervolgshows heb ik Posse opnieuw gebouwd als één GPU-machine met USB-camera's en vier portretschermen, die in een dag op een nieuwe locatie staat. Eén Django-applicatie met twee apps: sense detecteert poses met YOLO11 op dertig frames per seconde, synth genereert beelden met Stable Diffusion 1.5, ControlNet OpenPose en een IP-Adapter, in twaalf stappen binnen 16 GB videogeheugen. Elk beeld bouwt voort op het vorige. Het resultaat wordt zonder interpolatie opgeschaald en als SVG van 18 bij 32 blokken naar het scherm gestuurd; zes systemd-services houden het geheel draaiend. Van de bezoeker wordt alleen het skelet bewaard, geen camerabeeld.
Colofon
- Getoond: 2025 Design Museum Den Bosch, lobby, bij Supreme – Resampling the World; 2026 TORCH, Amsterdam
- Studio: Scape Agency, op het Mesmera-platform
- Mijn aandeel: pose-detectie, server, beeldpipeline, pixel-SVG; herbouw als reizende versie, installatie en services
Code
Het einde van de keten, ingekort: van gegenereerd beeld naar de SVG op het scherm. Eén rechthoek per blok, 18 breed en 32 hoog.
def image_to_svg(image: Image.Image, pixel_size: int = 120) -> bytes:
width, height = image.size
svg = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}">']
for y in range(0, height, pixel_size):
h = min(pixel_size, height - y)
for x in range(0, width, pixel_size):
w = min(pixel_size, width - x)
r, g, b = image.getpixel((x, y))
svg.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="rgb({r},{g},{b})" />')
svg.append("</svg>")
return "\n".join(svg).encode("utf-8")
# In the synth loop: upscale without interpolation, then 1440 / 80 = 18 blocks wide, 2560 / 80 = 32 high
upscaled = generated.resize((1440, 2560), Image.Resampling.NEAREST)
svg_bytes = image_to_svg(upscaled, pixel_size=80)
pose.pixelated_image.save(f"{pose.uuid}_pixelated.svg", ContentFile(svg_bytes), save=False)