A much simplified implementation of paper Paint by relaxation, a bit more like Painterly rendering with curved brush strokes of multiple sizes in practice, coursework for Computer Vision course at Cardiff University in 2022.
java Relaxation your_image.ppm example-brushes/brushSquare.pgm example-brushes/brushEllipse.pgm 0.01javac Relaxation.java
javadoc -d doc ./*.javaUsage:
> java Relaxation -h | --help
> java Relaxation <input_image> <compact_brush> <elongated_brush> <density> [-f] [-t <threads>] [-r <seed>] [-s <scale>] [-n <std>]
Options:
-f force the program to proceed with an input image at any size
-t specifies the number of threads to use for painting, 0 (default): use cpu count
-r specify a random seed for a consistent output
-s specify a scaling factor for brush images
-n specify a standard deviation for optional gaussian noise added to the smaller strokes in the painting
(if a negative value is set for threads, an unsafe multi-threading strategy is used)
The -n option can sometimes add a tiny bit of desired randomness.
| Input Image | Painted Output | With Imperfection (-n 100) |
|---|---|---|
![]() |
![]() |
![]() |
Stylized Animation (created via Std-Raytracer):
Animation.mp4
Optimistic thread locking is used for safe multi-threading painting, unsafe version does not check the pixel version numbers
- Load inputs: target image, two brush masks (compact + elongated), and a stroke density parameter.
- Build an image pyramid of the target (repeated 2× downsampling).
- Compute Sobel edge magnitude across pyramid levels (per RGB channel), then combine into one full-resolution edge-strength map.
- Compute Sobel edge direction (orientation map) from a smoothed grayscale version of the target.
- Precompute brush variants: a small set of discrete sizes and discrete orientations (scaled + rotated masks).
- Compute a multi-scale DoG (Difference of Gaussians) map and a thresholded map for region classification.
- Paint coarse-to-fine: for each scale, sample many candidate stroke positions; choose stroke size from edge strength (stronger edges → smaller), orientation from Sobel direction, colour as mean RGB under the mask; apply a greedy accept/reject test (only keep strokes that improve similarity).
- Second pass: repeat painting using elongated strokes in regions indicated by the DoG threshold, typically with higher density.
- Deal with unpainted areas: region growing to form patches of unpainted areas, fill them with average colour.
From left-to-right: Five level of stroke sizes, right-most image is the final output
find . -name "*.java" -exec clang-format -i {} +

