Redraw a picture as a set of smooth closed curves, by tracing the outlines of everything in the image and rebuilding each outline from a truncated Fourier series. Turning the number of retained modes down gives a loose, gestural sketch; turning it up converges back on the original outline.
Available as the original MATLAB script and as a Python port.
As the accuracy drops the sharp corners round off, the specks fall below the size filter, and each outline converges on the handful of epicycles that best describe it.
- Binarize — grayscale, adaptive histogram equalization, then Otsu's threshold to a black-and-white mask.
- Trace — pull out every connected object's boundary, holes included.
Outlines with fewer than
SmallestObjpoints are discarded as noise. - Parameterize — each boundary comes back already walked in order, so it
only needs the repeated closing point dropped and its direction normalized
via the signed area (objects and holes are traced opposite ways round). The
ordered points are packed into a complex signal
z = x + iy. This is the key step: a 2-D closed curve becomes a 1-D complex periodic signal, so each Fourier coefficient is one rotating epicycle. - Truncate — take the FFT, shift so DC sits mid-array, and zero everything outside a symmetric window around the DC bin.
- Reconstruct — invert the transform and plot
realagainst-imag.
| Setting | Meaning |
|---|---|
FileIn / image |
Path to the input image |
SmallestObj / --min-points |
Minimum outline length; smaller ones are dropped |
AcuFrac / --accuracy |
Fraction of maximum accuracy — 1 keeps every mode, lower values give a looser drawing |
Needs the Image Processing Toolbox (adapthisteq, graythresh, im2bw,
bwboundaries). Run DrawingPlay.m; it draws sample.png out of the box.
Point FileIn at any other image to draw that instead.
pip install -r python/requirements.txt
python python/drawing_with_fft.py sample.png --accuracy 0.3 -o out.pngfrom drawing_with_fft import trace_image, plot_curves
plot_curves(trace_image("sample.png", accuracy=0.3))scikit-image's find_contours returns contours already walked in order, so
the ordering step is the same two lines as the MATLAB version.
Watch the indexing when reading the two side by side. MATLAB is 1-based and
puts DC at floor(N/2)+1; numpy is 0-based and puts it at N//2. That is what
dc_index exists for, and the tests guard it. Getting it wrong is subtle: the
error is invisible at full accuracy and only blows up as the accuracy knob
comes down, at which point objects lose their centroid and collapse to the
origin.
Both implementations on sample.png, default settings:
| Result | |
|---|---|
| Binary masks | 99.993% pixel agreement (13 of 193,600 differ) |
| Outlines over threshold | 14 in both |
| Centroid error, accuracy 1 → 0.005 | 0 in both |
Python's contours carry more points for the same outline, since marching
squares emits a subpixel vertex per edge crossing where MATLAB's
bwboundaries walks pixel centres. The drawn result is the same.
The binarization is the step most sensitive to implementation differences —
skimage.exposure.equalize_adapthist and MATLAB's adapthisteq use the same
tiling and clip limit but differ internally. Tune with --clip-limit if a
particular image diverges.
sample.png is generated, not photographed — run python python/make_sample.py
to rebuild it. Generating it keeps the repository free of any third-party image
rights, and makes the demo reproducible.
It is deliberately line art. Adaptive histogram equalization tiles the image and stretches each tile's tonal range, so a tile sitting entirely inside a solid fill contains almost no range, and equalizing it amplifies quantization noise until the threshold lands on nonsense. Thin strokes keep both ink and paper in every tile. Line art is also the honest use case, since the pipeline traces outlines.
python python/test_drawing_with_fft.pynumpy only — no imaging stack needed. The DC-index and centroid tests were both checked against a deliberately reintroduced version of the original bug, to confirm they actually fail when it comes back.
MIT — see LICENSE.
