diff --git a/Hirst Spot Painting Generator/.gitignore b/Hirst Spot Painting Generator/.gitignore new file mode 100644 index 0000000..cff0408 --- /dev/null +++ b/Hirst Spot Painting Generator/.gitignore @@ -0,0 +1,8 @@ +__pycache__ +*.png + +# Virtual environment +venv/ + +# VS Code workspace settings +.vscode/ diff --git a/Hirst Spot Painting Generator/README.md b/Hirst Spot Painting Generator/README.md new file mode 100644 index 0000000..74f95e7 --- /dev/null +++ b/Hirst Spot Painting Generator/README.md @@ -0,0 +1,93 @@ +# Hirst Spot Painting Generator + +A small Python project that generates Hirst-style spot paintings. The code supports both an interactive Turtle-based renderer and a headless Pillow renderer that can export PNG images. + +Quick overview +- Interactive renderer: opens a Turtle window and draws a grid of colored dots. +- Headless renderer: creates PNG images using Pillow (no GUI required). +- Palette extraction: uses `colorgram.py` when available, falls back to Pillow's adaptive quantization, and finally a built-in palette. + +Features +- Generate spot paintings with configurable rows/columns, dot size, spacing and background color. +- Export to PNG (`--export`) for headless workflows. +- Deterministic output with `--seed`. +- Safe dry-run (`--no-window`) to print configuration without opening a GUI. + +Files of interest +- `main.py` — CLI entrypoint (flags: `--rows`, `--cols`, `--dot-size`, `--spacing`, `--bg-color`, `--image`, `--export`, `--no-window`, `--seed`). +- `palette.py` — palette extraction utilities (tries colorgram -> Pillow -> built-in fallback). +- `renderer.py` — rendering backends (Turtle interactive and Pillow PNG export). + +Requirements +- Python 3.8+ (virtualenv recommended) +- Pillow (for PNG export and palette fallback) +- colorgram.py (optional; install only if you prefer it) + +Install +1. Create and activate a virtual environment (PowerShell example): + +```powershell +python -m venv venv +& "${PWD}\venv\Scripts\Activate.ps1" +``` + +2. Install dependencies: + +```powershell +pip install -r requirements.txt +``` + +Basic usage examples (PowerShell) + +- Dry-run (prints configuration and palette, no GUI): + +```powershell +python .\main.py --no-window +``` + +- Interactive Turtle window: + +```powershell +python .\main.py +``` + +- Export a PNG (headless). Requires Pillow in your venv: + +```powershell +python .\main.py --export my_painting.png --no-window +``` + +- Reproducible output with a seed: + +```powershell +python .\main.py --export seed_painting.png --seed 42 --no-window +``` + +Changing layout +- Example: 12 rows, 8 columns, larger dots and more spacing: + +```powershell +python .\main.py --rows 12 --cols 8 --dot-size 24 --spacing 60 --export big.png --no-window +``` + +Notes and troubleshooting +- If PNG export fails with an error about Pillow, run: + +```powershell +pip install pillow +``` + +- If you want to use `colorgram.py` for palette extraction, install it in the venv: + +```powershell +pip install colorgram.py +``` + +Development notes +- The project is modular: `palette.py` encapsulates color extraction strategies and `renderer.py` contains both interactive and headless renderers. This makes it easy to extend or replace backends. +- Recommended next steps: add a small test suite that validates PNG export and palette extraction, or add a simple GUI to tweak parameters. + +License +- This repo doesn't include an explicit license. If you plan to publish, consider adding an appropriate LICENSE file. + +Enjoy creating art! \ No newline at end of file diff --git a/Hirst Spot Painting Generator/hirst-painting.jpg b/Hirst Spot Painting Generator/hirst-painting.jpg new file mode 100644 index 0000000..6cb98d5 Binary files /dev/null and b/Hirst Spot Painting Generator/hirst-painting.jpg differ diff --git a/Hirst Spot Painting Generator/main.py b/Hirst Spot Painting Generator/main.py new file mode 100644 index 0000000..dca73f0 --- /dev/null +++ b/Hirst Spot Painting Generator/main.py @@ -0,0 +1,67 @@ +import argparse +import random +from typing import Optional + +from palette import extract_palette +from renderer import draw_turtle_grid, render_png + + +def dry_run(info): + palette = extract_palette(info.get('image', 'hirst-painting.jpg'), info.get('palette_count', 10)) + print('Dry run:') + print(f" rows={info['rows']}, cols={info['cols']}, dot_size={info['dot_size']}, spacing={info['spacing']}") + print(f" background={info['bg_color']}") + print(f" palette (first {min(10, len(palette))}): {palette[:10]}") + + +def main(argv: Optional[list] = None): + parser = argparse.ArgumentParser(description='Hirst spot painting generator') + parser.add_argument('--rows', type=int, default=10) + parser.add_argument('--cols', type=int, default=10) + parser.add_argument('--dot-size', type=int, default=20) + parser.add_argument('--spacing', type=int, default=50) + parser.add_argument('--bg-color', default='black') + parser.add_argument('--image', default='hirst-painting.jpg') + parser.add_argument('--no-window', action='store_true', help='Run a dry-run without opening the turtle window') + parser.add_argument('--export', type=str, default=None, help='Export output to PNG file (headless)') + parser.add_argument('--seed', type=int, default=None, help='Random seed for reproducible output') + + args = parser.parse_args(argv) + + if args.seed is not None: + random.seed(args.seed) + + info = { + 'rows': args.rows, + 'cols': args.cols, + 'dot_size': args.dot_size, + 'spacing': args.spacing, + 'bg_color': args.bg_color, + 'image': args.image, + 'palette_count': 10, + } + + if args.no_window and not args.export: + dry_run(info) + return + + palette = extract_palette(args.image, 10) + + # If export requested, try headless PNG render + if args.export: + try: + render_png(args.export, args.rows, args.cols, args.dot_size, args.spacing, args.bg_color, palette) + print(f'Exported PNG to: {args.export}') + except Exception as e: + print('Failed to export PNG:', e) + print('You can install Pillow via: pip install pillow') + # if no-window was requested, return after exporting + if args.no_window: + return + + # Otherwise open a turtle window and draw interactively + draw_turtle_grid(args.rows, args.cols, args.dot_size, args.spacing, args.bg_color, palette) + + +if __name__ == '__main__': + main() diff --git a/Hirst Spot Painting Generator/palette.py b/Hirst Spot Painting Generator/palette.py new file mode 100644 index 0000000..c2dc182 --- /dev/null +++ b/Hirst Spot Painting Generator/palette.py @@ -0,0 +1,80 @@ +"""Palette extraction utilities. + +Try to use colorgram to extract colors; if unavailable or failing, fall back to Pillow-based extraction +using Image.quantize/adaptive palette. If neither is available, return a built-in fallback palette. +""" +from typing import List, Tuple + +try: + import colorgram # type: ignore +except Exception: + colorgram = None + +try: + from PIL import Image +except Exception: + Image = None # type: ignore + + +FALLBACK_PALETTE: List[Tuple[int, int, int]] = [ + (253, 251, 247), (253, 248, 252), (235, 252, 243), (198, 13, 32), + (248, 236, 25), (40, 76, 188), (244, 247, 253), (39, 216, 69), + (238, 227, 5), (227, 159, 49) +] + + +def extract_with_colorgram(image_path: str, count: int = 10) -> List[Tuple[int, int, int]]: + palette = [] + if colorgram is None: + return palette + try: + colors = colorgram.extract(image_path, count) + for c in colors: + r = c.rgb.r + g = c.rgb.g + b = c.rgb.b + palette.append((r, g, b)) + except Exception: + return [] + return palette + + +def extract_with_pillow(image_path: str, count: int = 10) -> List[Tuple[int, int, int]]: + if Image is None: + return [] + try: + img = Image.open(image_path).convert('RGBA') + # Resize to speed up palette extraction + img_thumb = img.copy() + img_thumb.thumbnail((200, 200)) + # Convert to palette using adaptive quantization + paletted = img_thumb.convert('P', palette=Image.ADAPTIVE, colors=count) + palette = paletted.getpalette() or [] + result = [] + for i in range(0, min(len(palette), count * 3), 3): + r = palette[i] + g = palette[i + 1] + b = palette[i + 2] + result.append((r, g, b)) + return result + except Exception: + return [] + + +def extract_palette(image_path: str = 'hirst-painting.jpg', count: int = 10) -> List[Tuple[int, int, int]]: + """Try multiple strategies and return a palette list of (r,g,b) tuples. + + Order: colorgram -> Pillow -> fallback built-in palette. + """ + # 1) try colorgram + pal = extract_with_colorgram(image_path, count) + if pal: + return pal[:count] + + # 2) try Pillow + pal = extract_with_pillow(image_path, count) + if pal: + return pal[:count] + + # 3) fallback + return FALLBACK_PALETTE[:count] diff --git a/Hirst Spot Painting Generator/renderer.py b/Hirst Spot Painting Generator/renderer.py new file mode 100644 index 0000000..54df23f --- /dev/null +++ b/Hirst Spot Painting Generator/renderer.py @@ -0,0 +1,76 @@ +"""Rendering utilities. + +Provides two rendering backends: +- Turtle-based interactive renderer (opens a window) +- Pillow-based headless renderer that can export PNG files + +The main script can choose which backend to use. Pillow is optional. +""" +from typing import List, Tuple, Optional + +try: + from PIL import Image, ImageDraw +except Exception: + Image = None # type: ignore + ImageDraw = None # type: ignore + +import turtle as t +import random + + +def draw_turtle_grid(rows: int, cols: int, dot_size: int, spacing: int, bg_color: str, palette: List[Tuple[int, int, int]]): + """Open a Turtle window and draw the grid. This call blocks until the window is closed.""" + t.colormode(255) + screen = t.Screen() + screen.bgcolor(bg_color) + + rex = t.Turtle() + rex.hideturtle() + rex.penup() + + start_x = -((cols - 1) * spacing) / 2 + start_y = -((rows - 1) * spacing) / 2 + + for row in range(rows): + for col in range(cols): + x = start_x + col * spacing + y = start_y + row * spacing + rex.goto(x, y) + rex.dot(dot_size, random.choice(palette)) + + screen.mainloop() + + +def render_png(path: str, rows: int, cols: int, dot_size: int, spacing: int, bg_color: str, palette: List[Tuple[int, int, int]], margin: Optional[int] = None): + """Render the grid into a PNG file using Pillow. + + If Pillow is not installed, raise ImportError. + """ + if Image is None or ImageDraw is None: + raise ImportError('Pillow is required for PNG export. Install with: pip install pillow') + + # compute canvas size. margin makes circles fully visible on edges + if margin is None: + margin = int(dot_size * 1.5) + + width = spacing * (cols - 1) + margin * 2 + dot_size + height = spacing * (rows - 1) + margin * 2 + dot_size + + img = Image.new('RGB', (width, height), color=bg_color) + draw = ImageDraw.Draw(img) + + start_x = margin + dot_size // 2 + start_y = margin + dot_size // 2 + + for r in range(rows): + for c in range(cols): + cx = start_x + c * spacing + cy = start_y + r * spacing + color = random.choice(palette) + left = cx - dot_size // 2 + top = cy - dot_size // 2 + right = cx + dot_size // 2 + bottom = cy + dot_size // 2 + draw.ellipse([left, top, right, bottom], fill=tuple(color)) + + img.save(path, format='PNG') diff --git a/Hirst Spot Painting Generator/requirements.txt b/Hirst Spot Painting Generator/requirements.txt new file mode 100644 index 0000000..2786f1a --- /dev/null +++ b/Hirst Spot Painting Generator/requirements.txt @@ -0,0 +1,2 @@ +Pillow>=9.0.0 +colorgram.py \ No newline at end of file