Menu
A landscape photo rendered as pointillism: a field of small non-overlapping coloured dots that resolve into a scene

Pointillize — a Photo-to-Dots Filter

A short Python 3 + OpenCV script that turns a photo into a pointillism-style image — a scatter of non-overlapping coloured dots, each sampled from the source.

Step 1Define

What It Had to Do

  • LookRound dots that never overlap, on a white canvas
  • ColourEach dot takes the colour of the source pixel at its centre
  • SizeOutput at the source image’s full resolution
  • ControlDot radius and number of attempts set from the command line

The source photo: a landscape scene

The same scene rebuilt from hundreds of small non-overlapping coloured dots

Source photo and the pointillized output (radius 6).

Step 3Build

How It Works

The output starts as a white canvas and gains one dot at a time:

  1. Pick a random pixel (row, col) in the source.
  2. Sample its colour.
  3. Try to stamp a filled circle of the chosen radius there.
  4. Keep it only if it does not touch any dot already placed.
  5. Repeat for a set number of attempts.

The collision test is the neat part: instead of comparing pixel lists, the candidate circle's mask is XOR‑ed against a running mask of every dot placed so far, using the candidate itself as the operation mask. If the result still equals the candidate mask, nothing overlapped and the dot is accepted.

Because circles can never overlap, the canvas saturates and later attempts are mostly rejected — the dot density plateaus on its own, which is what gives the hand‑stippled look rather than a solid mosaic.

Usage

python3 pointillize.py --image ./images/01.jpg --radius 10 --cycles 20 --debug True
FlagMeaning
--imagepath to the source image
--radiusdot radius, pixels
--cyclesnumber of placement attempts (not dots placed)
--debugTrue / False

Needs Python 3, opencv-python and numpy; the result is written to resultado.png.

Step 4Test

Another Example

A portrait photo

The portrait rendered as fine coloured stippling

A portrait at a small radius — the effect becomes a fine grain.

What the Run Log Shows

I logged every test run to the console (nohup.out, with --debug True), recording each attempt and whether its dot was kept. The portrait above is the output of the longest run in it: 1,000,000 attempts on the full 1836×3264 photo, which placed 473,500 dots. The share of attempts that still found free space fell steadily as the canvas filled:

Attempts so farDots placedKept, over the last 1,000 attempts
1,00099999.9%
10,0009,90197.9%
100,00090,21783.4%
500,000322,63841.6%
1,000,000473,50021.4%
Three pointillized versions of the same small portrait crop, left to right after 1,000, 10,000 and 1,000,000 attempts: scattered sparse dots, then a recognisable face, then dense fine stippling

The 271×295 portrait crop after 1,000, 10,000 and 1,000,000 attempts.

  • With a small radius you need tens of thousands of attempts to cover a photo, since --cycles counts tries, not hits.

Step 5Refine

A Faster Collision Check

The first version of the script (23 October 2017) checked each candidate dot pixel by pixel: it scanned the whole candidate image in Python to list the circle’s pixels, then compared every one of them against the stored pixel list of every dot already placed, so each attempt got slower as dots piled up. Later the same day I replaced that with the mask test described under How It Works: one running mask of all placed dots, checked with a single cv2.bitwise_xor per attempt.

  • An earlier per‑pixel collision function is still in the file but unused — the mask‑XOR approach replaced it.
Posted In:
Computer Vision