Hand-drawn weighted Voronoi stippling pipeline (Python 3.9.10 / Rhino CPython compatible)
Project description
Rhino Grasshopper-Compatible Stippling Processor
for parameters used here, see Example Parameters
This is a replication of the following article:
Weighted Voronoi Stippling, Adrian Secord. In: Proceedings of the 2nd International Symposium on Non-photorealistic Animation and Rendering. NPAR ’02. ACM, 2002, pp. 37– 43.
where the author introduced a techniques for generating stipple drawings from grayscale images using weighted centroidal Voronoi diagrams as in the traditional artistic technique of stippling that places small dots of ink onto paper such that their density give the impression of tone.
Authors and credits
- Original replication — Nicolas P. Rougier (BSD license, 2017), replicating Adrian Secord, Weighted Voronoi Stippling, NPAR 2002.
- Python 3.9.10 port, revised hand-drawn stippling pipeline, Lu et al. scientific-illustration extensions, and Grasshopper / Rhino integration — Max Benjamin Eschenbach.
Pre-requisites
The original replication was written and tested on OSX 10.12 (Sierra) using Python 3.6, Numpy 1.12, Scipy 0.18, Matplotlib 2.0 and tqdm 4.10.
This revision ports and extends that code for Python 3.9.10, matching the
Rhino 8 / Grasshopper CPython runtime. The port replaces the removed
scipy.misc.imread with a small Pillow-based loader; the compute core remains
plain numpy / scipy.spatial. It has been verified with:
- Python 3.9.10
- Numpy 2.0
- Scipy 1.13
- Pillow 11.3
- Matplotlib 3.9
- tqdm 4.x
Create the environment with conda:
conda create -n stippler -c conda-forge python=3.9.10 numpy scipy matplotlib pillow tqdm
Original data is in the data directory and you can also obtain it from Adrian Secord homepage.
Installation
Install from PyPI:
pip install stippler
For local development from a clone:
pip install -e .
This installs the dependencies (numpy, scipy, Pillow, tqdm, matplotlib), the
importable package stippler, and a stippler console command.
Usage (classic stippler)
The original replication CLI is preserved as the stippler.classic module
(run it with python -m stippler.classic ...):
usage: python -m stippler.classic
[--n_iter n] [--n_point n] [--save] [--force]
[--pointsize min,max] [--figsize w,h]
[--display] [--interactive] file
Weighted Vororonoi Stippler
positional arguments:
file Density image filename
optional arguments:
-h, --help show this help message and exit
--n_iter n Maximum number of iterations
--n_point n Number of points
--pointsize (min,max) (min,max)
Point mix/max size for final display
--figsize w,h Figure size
--force Force recomputation
--save Save computed points
--display Display final result
--interactive Display intermediate results (slower)
Hand-drawn pipeline (stippler command)
The stippler console command (module stippler.pipeline) wraps the relaxation
in a grayscale-image → stipple-output pipeline and adds three controls that make
the result read as hand drawn rather than machine generated. The compute core
depends only on numpy, scipy and Pillow (Rhino-friendly); matplotlib is used
only for preview rendering.
- Early-stopped relaxation — fewer Lloyd iterations means the points never
settle into the regular hexagonal lattice, so spacing stays organically
uneven. This is the single biggest dial.
--n_iter n— hard cap on iterations (lower = looser).--epsilon d— optional: stop when mean point movement drops belowd.
To control contrast (denser blacks / cleaner whites), use --gamma g. It
applies a power curve d ** g to the density that drives point placement,
relaxation and dot size, so g > 1 (e.g. 2.0–2.5) concentrates the same
n_point dots into the dark areas and thins the light ones; g < 1 flattens
the tonal range; g = 1 is the original linear mapping. (--threshold is a
blunter, hard cutoff that forces lighter greys to pure white.)
2. Varying dot size — a min/max radius spread plus per-dot random jitter
breaks the constant-radius "machine stipple" giveaway.
--pointsize min max— radius range (density drives the base size).--size_jitter f— per-dot multiplicative radius noise, e.g.0.15.
- Imperfect placement & edges — Gaussian positional jitter plus wobbly,
non-circular dot outlines.
--position_jitter s— Gaussian noise std on final positions.--edge_noise f— dot-edge wobble as a fraction of radius, e.g.0.08.--edge_segments n— vertices per dot outline.
Output format follows the --out extension (.png, .pdf, .svg).
stippler data/boots.jpg --n_point 12000 --n_iter 8 \
--pointsize 0.8 3.0 --size_jitter 0.2 --position_jitter 0.5 \
--edge_noise 0.09 --seed 3 --out data/boots-stipple.png
The pipeline can also be imported as a library: stippler.stipple(...) returns
a StippleResult carrying points, radii and per-dot polygons (handy for
feeding geometry into Rhino later), which render_matplotlib, render_svg and
save_points consume.
Scientific-illustration pipeline (Lu et al.)
The hand-drawn pipeline is further extended with optional controls adapted from
Lu et al., Non-Photorealistic Volume Rendering Using Stippling Techniques
(resources/vis_stipple.pdf). Each feature is
independently toggled via IllustrationParams (library) or the CLI
illustration argument group (stippler --help):
- Boundary — boost stipple density on high-gradient edges.
- Silhouette density — concentrate dots on view-facing silhouette regions (uses an optional normal map).
- Interior — sparse stipples in flat, low-gradient areas.
- Lighting — modulate density from inferred or supplied normals.
- Depth attenuation — thin stipples in far regions (requires a depth map).
- Gradient size — scale dot radius by local gradient magnitude.
- Silhouette curves — extract and draw feature-line strokes over the dots.
Normal and depth maps should be aligned with the beauty-pass image. In Rhino, the Grasshopper component can capture these automatically when the options that need them are enabled.
Grasshopper / Rhino integration
Max Benjamin Eschenbach integrated the revised pipeline into Grasshopper for Rhino 8 CPython 3.9.10:
grasshopper_userobjects/STIPPLER_StippledViewCapture.ghuser— drop-in user object that captures the active viewport and writes a_stippledoutput (PNG, SVG, or PDF), including hand-drawn controls and the Lu et al. options above.grasshopper_userobjects_src/— Python 3 script source and input/output reference. Re-export the user object from Grasshopper after editing the script (right-click → Save User Object…).
Install the stippler package into the Rhino CPython environment the component
uses (see Installation above), then paste or sync the script into a GH Python 3
component.
Example Parameters
Parameters used for the Donut example
| Parameter | Value |
|---|---|
| Resolution | 1920 x 1080 px |
| NumDots | 100000 |
| Iterations | 12 |
| Gamma | 1.0 |
| RMin | 1.0 |
| RMax | 6.0 |
| SizeJitter | 0.15 |
| PositionJitter | 0.1 |
| EdgeNoise | 0.1 |
| Seed | 42 |
| DPI | 300 |
| Boundary | false |
| SilhouetteDensity | false |
| Interior | false |
| Lighting | false |
| DepthAttenuation | false |
| GradientSize | false |
| SilhouetteCurves | true |
| CurveWidth | 0.5 |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stippler-0.2.1.1.tar.gz.
File metadata
- Download URL: stippler-0.2.1.1.tar.gz
- Upload date:
- Size: 24.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41129c84a5c98b8a8f7db9bc955c3ef3d103e1beb36a06ca96b1be2231611c11
|
|
| MD5 |
4a0bbac2a302aa504b6f62a1a06f9e98
|
|
| BLAKE2b-256 |
332d2c0ddac46156dec4f377682b9b69170382d875d68a635f075952a9eb9597
|
Provenance
The following attestation bundles were made for stippler-0.2.1.1.tar.gz:
Publisher:
publish-pypi.yml on fstwn/stippler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stippler-0.2.1.1.tar.gz -
Subject digest:
41129c84a5c98b8a8f7db9bc955c3ef3d103e1beb36a06ca96b1be2231611c11 - Sigstore transparency entry: 2034883045
- Sigstore integration time:
-
Permalink:
fstwn/stippler@8c0cf46cae87f57b940988a073c9f8389ba96b34 -
Branch / Tag:
refs/heads/python39-rhino-port - Owner: https://github.com/fstwn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@8c0cf46cae87f57b940988a073c9f8389ba96b34 -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file stippler-0.2.1.1-py3-none-any.whl.
File metadata
- Download URL: stippler-0.2.1.1-py3-none-any.whl
- Upload date:
- Size: 23.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d73a6223a51ca59e620eb74de32ded5de98cd8c20c0237c3d4970f1f366ac31c
|
|
| MD5 |
17a2277fe51407cd4716af615dd47cf7
|
|
| BLAKE2b-256 |
93e075e9e2f261eec43645917da4e21d4e44ce7a330715a766e15e623a315ccc
|
Provenance
The following attestation bundles were made for stippler-0.2.1.1-py3-none-any.whl:
Publisher:
publish-pypi.yml on fstwn/stippler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stippler-0.2.1.1-py3-none-any.whl -
Subject digest:
d73a6223a51ca59e620eb74de32ded5de98cd8c20c0237c3d4970f1f366ac31c - Sigstore transparency entry: 2034883408
- Sigstore integration time:
-
Permalink:
fstwn/stippler@8c0cf46cae87f57b940988a073c9f8389ba96b34 -
Branch / Tag:
refs/heads/python39-rhino-port - Owner: https://github.com/fstwn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@8c0cf46cae87f57b940988a073c9f8389ba96b34 -
Trigger Event:
workflow_run
-
Statement type: