Transform any image into automated brush strokes.
Pyaint recreates a picture by driving the mouse in a painting app — MS Paint, skribbl.io, and anything else with an on-screen palette you can teach it.
Pyaint.Demo.mp4
skribbl.io — sampled palette and demo of edge-tracing and layered mode.
The bundled sample Pyaint opens with — a koala by Ahmed Hossam via Unsplash (Unsplash License).
- Recreates any image by painting it with the mouse, stroke by stroke.
- Works with palette-only apps. Pyaint samples colours from the target's on-screen palette, so it does not need an API or a plugin.
- Plans before it paints. Source, planned result and a side-by-side Compare view update as you tune the settings, and repeat runs can skip processing.
It is for drawing your own images in apps you control: art, accessibility, offline demos, and testing. See Intended use.
The original Tk-based app — the 0.0.0 baseline this rewrite started from — is
preserved on the
legacy branch. It is kept
as-is for anyone who prefers the old interface; active development happens on
the current app.
Requirements: Windows 10/11 and a drawing app with an on-screen palette.
The .exe needs nothing else; a source checkout needs Python 3.8+.
Option A — download the .exe: grab pyaint.exe from
Releases. No Python needed.
Option B — pipx:
pipx install git+https://github.com/Trev241/pyaint.git
pyaintOption C — from source:
git clone https://github.com/Trev241/pyaint.git
cd pyaint
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python main.py- Open the target app on a blank canvas. Maximize it on the primary monitor, then choose it in Drawing in.
- Click Find the canvas, review the detected regions on screen, and confirm. If detection fails, click Pick the regions and click the canvas and palette corners yourself.
- Pick an image. On first run the bundled koala is already loaded; otherwise drop a file, paste a URL, or type words to search online. Source chooses Openverse (default) or Wikimedia Commons for searches.
- Check Source / Preview / Compare. Choose the mode and Look in the column, then expand Drawing for detail and pacing. The preview updates as you go.
- Click Start drawing. Press
ESCto stop,Pto pause/resume.
That's it. For skribbl.io the first draw takes about a minute.
Display scaling / multi-monitor: Pyaint captures the primary monitor at physical pixels and is per-monitor DPI aware, so its own window reflows when Windows scaling changes or the window moves between displays. Saved canvas/palette boxes are absolute pixels though: after changing scaling or moving to another monitor, re-run Find the canvas or Pick the regions. Mixed-DPI multi-monitor setups (a target on a different monitor from the primary) are not supported.
Honest note: auto-detection is tuned against specific window layouts and resolutions. When it cannot find a region it falls back to manual teaching, so always review the on-screen overlay before applying.
image → fit + scale to the canvas
→ map each pixel to the nearest palette colour
→ encode strokes (scan-line runs or traced contours)
→ replay them with pyautogui, selecting colours as it goes
Three drawing modes, grouped in the Mode rail:
| Family | Mode | What it draws |
|---|---|---|
| Runs | Layered | Colour runs, with lower layers merged away: fewer strokes, faster, smoother joins |
| Runs | Slotted | Every run exactly as planned: no overdraw, more strokes |
| Contours | Edge tracing (default) | Black line art from the image's edges (no colour fills) |
Outline is retired from the UI in favour of Edge tracing. Its planner and settings are retained for compatibility, but old configs that selected it open in Edge tracing.
Look presets: Balanced, Smooth and Fast draft for the fill modes set pacing only; Cartoon and Realistic for Edge tracing set the edge cleanup (merge by colour, smoothing, weak-edge pruning). Editing a knob switches the panel to Custom.
Live preview. Preview plans on a worker thread as settings change, debounced and cancelled by generation, so a moving slider never applies a stale frame. Compare shows source and plan side by side.
Cache. A finished plan is stored in cache/{image_hash}_{settings_hash}.json
and reused while the image, canvas, settings and age (24 h) still match. It
skips processing, not drawing.
| Layer | Module | Responsibility |
|---|---|---|
| UI | pyaint/ui/ |
PySide6 window, worker threads, preview, setup |
| Planner | pyaint/planner.py |
Pure: image → colour grid or contours → stroke map |
| Executor | pyaint/bot.py |
Bot facade and Bot.draw: pause/resume, progress, replay |
| Driver | pyaint/painter.py |
ScreenPainter: screen capture + synthetic input |
Supporting modules: profile.py (the taught environment), targets.py
(recipes), locators.py (auto-detection), palette.py (swatch sampling and
nearest colour), render.py (preview frames), cache.py (stroke cache),
config.py/paths.py (persistence).
The key rule is that the planner never branches on app identity. An app
difference is either a declarative recipe (data) or a ScreenPainter seam
(code). MainWindow owns one Profile and hands the same object to Bot, so
the UI and the engine always agree on where the canvas, palette and buttons
are. config.json is split on load into the environment (Profile) and
preferences (MainWindow), then merged again on save.
Pyaint finds the canvas and palette on screen, then replays the image. Support depends on how much of the target can be detected.
| App | Support | Setup |
|---|---|---|
| skribbl.io | Canvas + fixed 2×13 palette auto-detected | Find the canvas — no manual picking |
| MS Paint (Windows 11) | Canvas + palette auto-detected when maximized at 1920×1080 | Find the canvas, or pick manually |
| Anything else with a palette grid | Generic path | Teach the palette and canvas |
Pyaint selects colours from an on-screen palette, so apps whose only colour
picker is a dialog (GIMP, Clip Studio Paint, Photoshop) are not supported.
Support for another palette app is a data change, not a code change: see the
schema in targets/README.md.
Why does Pyaint open with a koala?
It is the bundled sample, shown only when there is no remembered image. Load
your own image and it replaces the sample; Reset all settings brings the
sample back. The sample is never copied into last_image.png.
Does it work on macOS or Linux?
No. Pyaint relies on Windows screen capture and pyautogui input, and the
detection is tuned for Windows apps.
Drawing doesn't start.
Check that the canvas and palette are configured (Find the canvas or
Pick the regions), that an image is loaded, and that a run is not already in
progress. Click Start drawing to see the missing step.
Colours are wrong. Re-run Find the canvas (or pick the palette) so the swatch centres are sampled correctly — the detection overlay draws a dot at each cell centre. Lower Detail for finer detail.
Colours are skipped entirely. Pyaint only draws colours that exist in the sampled palette. Others map to the nearest swatch, and a colour with no close match may be skipped.
Find the canvas found nothing. The canvas must be blank — existing artwork defeats detection. Also make sure the window is maximized on the primary monitor. Clear the canvas and retry, or use Pick the regions.
Drawing is too slow / too fast. Adjust Detail, Time per stroke and Stroke speed. Lower Detail means fewer, bigger strokes. Save the plan for next time skips processing on repeat runs but does not change drawing speed.
Does it work at 125% / 150% scaling? Yes, on the primary monitor. Pyaint tells Windows it is per-monitor DPI aware before any automation library loads, so changing Windows scaling or moving the window between displays reflows it immediately instead of leaving it stretched at the old scale. Manual picks are stored as physical pixels, so they do not inherit the old scale either. Saved canvas and palette boxes are not rescaled when you change Windows scaling, though: re-run Find the canvas or Pick the regions afterwards. A target on a different monitor from the primary is not supported.
How do I stop or pause?
ESC stops the current run and P pauses/resumes. The UI stays responsive
because the engine runs on a worker thread.
Where does it save data?
config.json, cache/ and targets/ sit next to the executable (the repo
root for a source checkout). Nothing is uploaded; the network is used only to
fetch an image you give it by URL, or to search Openverse / Wikimedia Commons
when you type search words.
How do I reset everything?
Click Reset all settings at the bottom of the settings column. That
deletes config.json,
the taught positions, the remembered image and the caches.
Can I use this on skribbl.io? Locating the app's UI to draw your own image is allowed here, but using it to deceive other players is not the intended use. See Intended use.
pip install -e ".[dev,build]"
python -m pytest -q # fast, headless suite
python -m pytest -m ui # Qt smoke tests (offscreen)
python main.py # GUI (Windows)
python scripts/build_exe.py # build dist/pyaint.exeThe tests never touch the screen: Palette is built from an explicit colour
map and screen actions are replaced with fakes. The build is Windows-only and
produces a single dist/pyaint.exe that bundles Python and all dependencies.
Setup, test, build and release details live in
CONTRIBUTING.md.
GNU General Public License v3.0 or later — see LICENSE.md.