Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Hirst Spot Painting Generator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__
*.png

# Virtual environment
venv/

# VS Code workspace settings
.vscode/
93 changes: 93 additions & 0 deletions Hirst Spot Painting Generator/README.md
Original file line number Diff line number Diff line change
@@ -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!
Binary file added Hirst Spot Painting Generator/hirst-painting.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions Hirst Spot Painting Generator/main.py
Original file line number Diff line number Diff line change
@@ -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()
80 changes: 80 additions & 0 deletions Hirst Spot Painting Generator/palette.py
Original file line number Diff line number Diff line change
@@ -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]
76 changes: 76 additions & 0 deletions Hirst Spot Painting Generator/renderer.py
Original file line number Diff line number Diff line change
@@ -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')
2 changes: 2 additions & 0 deletions Hirst Spot Painting Generator/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Pillow>=9.0.0
colorgram.py