# THE PAINT MACHINE — Guide for Claude
### How to turn any photo into a brush-stroke painting (by Peter + Claude, 2026)

Hi Claude! Peter built this with another you. Follow this guide exactly and you can do it too.
The method is called **painterly rendering** (invented by Aaron Hertzmann in 1998): repaint a
photo using curved brush strokes of shrinking sizes, where each stroke rides ALONG the photo's
edges like a real painter's brush follows the shape it's painting.

---

## STEP 0 — What Peter uploads to you

1. A photo (any photo)
2. The file `paint_anything.py` (the complete working code — don't rewrite it, just use it)
3. Possibly this guide

If he only uploads this guide, the full code is not here — ask him for `paint_anything.py`,
or rebuild it from the ALGORITHM section below.

## STEP 1 — Set the config

Open `paint_anything.py`. At the top there's a CONFIG block. Change only these:

```python
PHOTO = "/mnt/user-data/uploads/<his-photo-filename>"
OUT   = "/mnt/user-data/outputs/painted.png"
MODE  = "ultrafine"     # see mode table below
WORLD = "none"          # "none" | "cloud" | "meadow"
SEED  = 1107            # any number; different number = different strokes, same photo
```

## STEP 2 — Run it

```bash
pip install scipy --break-system-packages   # only needed if WORLD != "none"
python3 paint_anything.py
```

It prints strokes-per-brush as it paints. Takes well under a minute.

## STEP 3 — Check your own work, then show him

**Always `view` the output image yourself before presenting it.** Look for bugs: bare canvas
showing through, the cutout grabbing the wrong thing, a world drawn wrong. Peter notices
everything. Fix and re-run if needed (it happened to the first me too — a black gap at a
meadow horizon). Then present the file and tell him the stroke count — he likes the numbers.

---

## THE MODES

`REALISTIC_BRUSHES = True` (the default, and Peter's preference) upgrades all four
painting modes with real brush behavior: strokes TAPER (land thin, press fat, lift
thin), have BRISTLE streaks inside them, get IMPASTO relief (a light shine on the
top-left edge and a paint-ridge shadow on the bottom-right, faking 3D paint
thickness), run DRY at the end of long strokes (broken gaps), and the whole thing
sits on a woven LINEN canvas texture that shows through thin paint. Set it False
only if he asks for "the old flat brushes".

| Mode      | Brushes | Smallest brush | Canvas | Look |
|-----------|---------|----------------|--------|------|
| chunky    | 3       | 9 px           | 1100   | abstract, thick oil paint, funny portraits |
| normal    | 5       | 2 px           | 1200   | classic painting |
| fine      | 6       | 1 px           | 1400   | detailed painting |
| ultrafine | 6       | 1 px           | 1600   | right on the line between painting and photo |
| marker    | 3 + ink | 3 px + pen     | 1300   | comic/cartoon: flat colors, long marker sweeps, ink outlines, panel frame |

**Marker dials** (in the CONFIG block): `MARKER_VIBRANCE` boosts the SHY (muted) colors
most while barely touching already-loud ones — "more vibrant" / "make it pop" means raise
it (1.0 off → 1.5 punchy default → 2.0 maximum pop). `MARKER_SAT` boosts ALL colors
equally — "more saturated". If Peter says "saturation and colors down" or similar, lower
`MARKER_SAT` (1.45 punchy comic → 1.0 natural → 0.6 faded/vintage) and
`MARKER_COLORS` (12 normal comic → 8 simpler → 5 poster → 3 extreme). If he says
"pure color" / "flat" / "no lines in the fills" set `MARKER_STREAKS = False`
(cel-shaded flat fills, his preferred default); "streaky marker" sets it True.
He'll ask in plain words like "marker mode but muted" or "marker, only 5 colors" —
map that to these numbers yourself, don't ask him for numbers. Two color tricks that
aren't dials: (1) if the subject sits in shadow (like ducklings inside a box), lift
shadows BEFORE marker mode with `img.point(lambda v: int(255 * (v / 255) ** 0.62))`;
(2) if a subject's HUE is wrong (shade turned yellow ducklings tan, and no saturation
boost can fix a wrong hue — it only makes tan louder), use SELECTIVE COLOR: measure the
subject's HSV (convert("HSV"), sample patches), build a mask by hue/sat/value ranges
that separates subject from background, Gaussian-feather it, then steer masked pixels'
H toward the true color and boost their S and V. Ducklings example: mask
H 26–85 & S>30 & V>110, steer H to 42 (yellow), S*1.9+40, V*1.18. Comics draw things
the color you KNOW they are, not the color the light made them.

**Marker mode is different from the others**: it first flattens the photo to ~12 comic
colors (quantize), then fills each color region with LONG marker sweeps at a consistent
hatch angle (markers never cross a color boundary), then draws bold ink lines along the
edges — the ink follows its own extra-blurred gradient field (blur 2.4), because on the
sharp quantized image the edges are only 1px wide and the ink pen "falls off" them
(this bug happened; the wider blur is the fix). A comic panel frame goes on last.

## THE WORLDS

`WORLD = "none"` keeps the photo's own background and just paints it.

`"cloud"` and `"meadow"` cut the subject out and place it in an invented scene
(night sky + moon + fluffy cloud, or sunny flower meadow). **The cutout only works when
the subject is clearly DARKER than everything else** (Peter's black cat on a light bed
is perfect; an orange cat on an orange couch will fail). Knob: `CUTOUT_DARKNESS = 78` —
raise it if part of the subject is missing, lower it if background sneaks in.

**New worlds:** Peter will ask for new ones (space, giant hamburger, the moon, whatever).
Write a new `world_xxx(mask)` function copying the pattern of `world_cloud` /
`world_meadow`: draw sky/scenery with PIL, put the "surface" the subject lies on at
`by1 - (by1-by0)*0.18` (the subject's bottom), and in `composite()` tuck a bit of the
surface (fluff, grass, sand...) OVER the subject's bottom edge so it sinks in instead
of floating. Then paint. That tuck-over trick is what sells the whole illusion.

---

## THE ALGORITHM (in case you must rebuild the code)

1. **Load** the photo, resize to canvas width (see mode table).
2. **Precompute** a slightly blurred color reference, a luminance map, and its
   gradients (np.gradient of Gaussian-blurred luminance).
3. **Paint in layers**, big brush radius to small, e.g. ultrafine =
   `(26,-1),(14,50),(8,50),(4,55),(2,60),(1,72)` as (radius, error_threshold):
   - Lay a jittered grid over the canvas with spacing = radius, shuffled.
   - For every grid point: skip it if the current canvas is already within
     `error_threshold` (Manhattan RGB distance) of the target there. The first layer
     paints everywhere (threshold -1). This is the "squinting painter" rule — later
     brushes only fix what still looks wrong.
   - Otherwise make a **stroke**: start at the point, take the stroke's color from the
     target there, then walk 4–12 steps of length = radius. Each step goes
     **perpendicular to the luminance gradient** (i.e. along the edge, not across it),
     with 65/35 smoothing against the previous direction so strokes curve gently,
     never reversing. Stop at the canvas border or when the target color drifts too
     far from the stroke's color (stroke stays inside its own color region).
   - **Render** the stroke as a polyline: width = 2×radius ×(0.75–1.25 random),
     round joints, a filled circle at each end for round brush caps, color jittered
     ±14 (±5 for tiny brushes) for a hand-mixed paint feel. Brushes with radius ≥ 8
     get one thinner, lighter offset line on top — a bristle highlight.
4. **Canvas** starts as warm linen `(239,230,212)`, never pure white.
5. For worlds: mask = darkest pixels → binary opening → keep largest blob → fill
   holes → dilate 2 → Gaussian-feather for soft fur edges. Composite subject over the
   generated background with the feathered mask, tuck surface bits over its bottom edge.

## KNOBS PETER MIGHT ASK YOU TO TURN

- **"more detail"** → bigger canvas W, add/lower the last layers' thresholds
- **"chunkier / more abstract"** → drop small layers, raise radii
- **"longer strokes"** → raise the 4–12 step count and the color-stay limit
- **"different painting, same photo"** → just change SEED
- **"make it swirlier / van Gogh"** → increase step count a lot and lower the
  direction smoothing from 0.65 toward 0.5

## THINGS THAT WENT WRONG BEFORE (learn from the first me)

- Bare black canvas peeking through gaps between drawn shapes in a world → always lay a
  full base color under scenery before detailing.
- Don't invent animals/faces from geometric shapes and expect them to look real — Peter
  will call it "still shapes" and he will be right. Photos in, paint out.
- Present the actual image file. He wants the result, not a simulation of the result.

Have fun. The stroke counts are real, count them honestly. — Claude (the earlier one)
