Skip to main content

images/chunky_pixels.py

1#!/usr/bin/env python3
2"""
3Redraws the given image with 'chunky pixels' – every NxN chunk of pixels
4is replaced by a single colour.
5"""
7import colorsys
8import random
9import subprocess
10import sys
11import tempfile
13from PIL import Image, ImageDraw
14import tqdm
17def clamp(x: float, /, *, between: tuple[int, int]) -> float:
18 """
19 Restrict the value to fit between the lower/upper bounds.
20 """
21 lower, upper = between
22 return min([max([x, lower]), upper])
25if __name__ == "__main__":
26 try:
27 path = sys.argv[1]
28 pixel_size = int(sys.argv[2])
29 except (IndexError, ValueError):
30 sys.exit(f"Usage: {__file__} <PATH> <PIXEL_SIZE>")
32 im = Image.open(path)
34 assert im.width % pixel_size == 0
35 assert im.height % pixel_size == 0
37 all_colours = {}
39 with tempfile.TemporaryDirectory() as tmpdir:
40 for x_start in tqdm.tqdm(range(0, im.width, pixel_size)):
41 for y_start in range(0, im.height, pixel_size):
42 im_crop = im.crop(
43 (x_start, y_start, x_start + pixel_size, y_start + pixel_size)
44 )
45 tmp_path = f"{tmpdir}/cropped_{x_start}_{y_start}.png"
46 im_crop.save(tmp_path)
47 colour = subprocess.check_output(
48 ["dominant_colours", tmp_path, "--max-colours=1", "--no-palette"]
49 ).strip()
50 all_colours[(x_start, y_start)] = (
51 int(colour[1:3], 16),
52 int(colour[3:5], 16),
53 int(colour[5:7], 16),
54 )
56 im = Image.new("RGB", im.size)
57 draw = ImageDraw.Draw(im)
59 for (x_start, y_start), (r, g, b) in all_colours.items():
60 hue, lightness, saturation = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
61 lightness = clamp(lightness * random.uniform(0.9, 1.1), between=(0, 1))
62 saturation = clamp(saturation * random.uniform(0.9, 1.1), between=(0, 1))
64 r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
65 draw.rectangle(
66 [(x_start, y_start), (x_start + pixel_size), (y_start + pixel_size)],
67 fill=(int(r * 255), int(g * 255), int(b * 255)),
68 )
70 base, extension = path.rsplit(".", 1)
72 out_path = f"{base}_{pixel_size}.{extension}"
73 im.save(out_path)