This script implements a simplified version of the String Art greedy algorithm. Given an image, it attempts to create a similar representation using a single thread wound around a circle of nails, effectively constructing an image using only lines. This technique is probably most well-know for the artistic works of Petros Vrllis.
Most implementations often require high-contrast images, and still the results can vary significantly from one image to another. In this version, I've tweaked the algorithm parameters to enhance the contrast and detail in the final output. While this adjustment impacts performance, it remains usable.
Additionally, the script features a command-line interface (CLI) with various tweakable parameters, option flags, RGB color mode and you can also save the GIF animation with easy. Feel free to explore these options to customize the output according to your preferences, if you want to reuse the code or call it somewhere else, you should look at StringArt.run
.
Useful Resources:
- Setup:
- Load the source image and create an empty output image.
- Calculate pin positions and all possible lines between 2 pins.
- Iteration Step:
- Choose a pin (P).
- Create all possible lines connecting P to the other pins in the circle.
- Calculate the error between each lines and the source image.
- Find the line (L) that gives the minimum error.
- Update the output image adding L, and the source image subtracting L.
- Set the pin to be opposite point in the line L.
Line Generating Function: One-pixel-width lines (or any square/stair-like lines) do not yield good results. Experimentation with different line functions is essential here. I ended up choosing to apply the Gaussian Blur Kernel to the line. It's simple, and it works (also, it eliminates the need to fine-tune other parameters).
Line Pixel Strength: Opt for low line pixel values to create nuanced shades of gray in the output image.
Choose Pin: Randomizing the pin position periodically (every N steps) tends to give better results.
Error Function: Arguably the most critical part of the algorithm. You should minimize the error here and not any other metric (I lost a lot of time doing that...). The best function that I found was the squared difference between the source image and the line (but the performance takes a considerable hit here).
Excluding Already Visited Lines:
While excluding used lines each iteration improves performance, it results in a more diffuse and noisy image. In this implementation, visited lines are retained. If you prefer the noisy style, just change the variable EXCLUDE_REPEATED_PINS
to true.
The Libraries:
- ArgParse
- Images
- Logging
- LRUCache
# basic
$ julia -O3 -t 8 main.jl -i [input image] -o [output image]
# suggested
$ julia -O3 -t 8 main.jl -s 720 --steps 2000 -i [input image] -o [output image]
# alter the image resolution
$ julia -O3 -t 8 main.jl -s 800 -i [input image] -o [output image]
# RGB color mode
$ julia -O3 -t 8 main.jl --rgb -i [input image] -o [output image]
# RGB color mode with custom colors
$ julia -O3 -t 8 main.jl --custom-colors "#FFFF33,#33FFFF" -i [input image] -o [output image]
# RGB color using color palette (n = 4) extracted from the input image
$ julia -O3 -t 8 main.jl --palette 4 -i [input image] -o [output image]
# Saves GIF output
$ julia -O3 -t 8 main.jl --gif -i [input image] -o [output image]
# Saves as SVG
$ julia -O3 -t 8 main.jl --svg -i [input image] -o [output image]
# plot pins used in the image
$ julia utils.jl -f plot_pins -i [input image] -o [output image]
# plot color channel for a given color and input image
$ julia utils.jl -f plot_colors --colors "#FF0000" -i [input image] -o [output image]
usage: main.jl -i INPUT [-o OUTPUT] [--gif] [--svg] [-s SIZE]
[-n PINS] [--steps STEPS]
[--line-strength LINE-STRENGTH] [--blur BLUR] [--rgb]
[--custom-colors CUSTOM-COLORS] [--palette PALETTE]
[--verbose] [-h]
StringArt - Convert images to string art
optional arguments:
-i, --input INPUT input image path
-o, --output OUTPUT output image path without extension (default:
"output")
--gif Save output as a GIF
--svg Save output as a SVG
-s, --size SIZE output image size in pixels (type: Int64,
default: 512)
-n, --pins PINS number of pins to use in canvas (type: Int64,
default: 180)
--steps STEPS number of algorithm iterations (type: Int64,
default: 1000)
--line-strength LINE-STRENGTH
line intensity ranging from 1-100 (type:
Int64, default: 25)
--blur BLUR gaussian blur kernel size (type: Int64,
default: 1)
--rgb use basic RGB color mode (default: red, green,
blue)
--custom-colors CUSTOM-COLORS
comma-separated HEX color codes for custom
color strands (e.g. #FF0000,#00FF00)
--palette PALETTE number of colors to extract as a palette from
the input image (k-means clustering) (type:
Int64)
--verbose verbose mode
-h, --help show this help message and exit
Example: julia main.jl -i input.jpg -o output --svg
keep the number of pins below 250 and the image size below 1000.
the number of iteration steps is dependent on the image size. For size between 500 and 800, 2000 iteration is more than enough.
- Write output image as GIF
- Write output image as SVG
- Optimize memory usage
- Support GIF in
--rgb
mode - Use user provided colors (
--custom-colors
flag) to create the output image - Use color palette (
--palette
flag) from input image to create the output image - Enhance Image Contrast (needed?)
- Port Code to the GPU