Reproducing images with geometric primitives.

Primitive Pictures

Reproducing images with geometric primitives.

Example

How it Works

A target image is provided as input. The algorithm tries to find the single most optimal shape that can be drawn to minimize the error between the target image and the drawn image. It repeats this process, adding one shape at a time. Around 50 to 200 shapes are needed to reach a result that is recognizable yet artistic and abstract.

Primitive for macOS

Now available as a native Mac application!

https://primitive.lol/

Twitter

Follow @PrimitivePic on Twitter to see a new primitive picture every 30 minutes!

The Twitter bot looks for interesting photos using the Flickr API, runs the algorithm using randomized parameters, and posts the picture using the Twitter API.

You can tweet a picture to the bot and it will process it for you.

Command-line Usage

Run it on your own images! First, install Go.

go get -u github.com/fogleman/primitive
primitive -i input.png -o output.png -n 100

Small input images should be used (like 256x256px). You don't need the detail anyway and the code will run faster.

Flag Default Description
i n/a input file
o n/a output file
n n/a number of shapes
m 1 mode: 0=combo, 1=triangle, 2=rect, 3=ellipse, 4=circle, 5=rotatedrect, 6=beziers, 7=rotatedellipse, 8=polygon
rep 0 add N extra shapes each iteration with reduced search (mostly good for beziers)
nth 1 save every Nth frame (only when %d is in output path)
r 256 resize large input images to this size before processing
s 1024 output image size
a 128 color alpha (use 0 to let the algorithm choose alpha for each shape)
bg avg starting background color (hex)
j 0 number of parallel workers (default uses all cores)
v off verbose output
vv off very verbose output

Output Formats

Depending on the output filename extension provided, you can produce different types of output.

  • PNG: raster output
  • JPG: raster output
  • SVG: vector output
  • GIF: animated output showing shapes being added - requires ImageMagick (specifically the convert command)

For PNG and SVG outputs, you can also include %d, %03d, etc. in the filename. In this case, each frame will be saved separately.

You can use the -o flag multiple times. This way you can save both a PNG and an SVG, for example.

Progression

This GIF demonstrates the iterative nature of the algorithm, attempting to minimize the mean squared error by adding one shape at a time. (Use a ".gif" output file to generate one yourself!)

Static Animation

Since the algorithm has a random component to it, you can run it against the same input image multiple times to bring life to a static image.

Pencils

Creative Constraints

If you're willing to dabble in the code, you can enforce constraints on the shapes to produce even more interesting results. Here, the rectangles are constrained to point toward the sun in this picture of a pyramid sunset.

Pyramids

Shape and Iteration Comparison Matrix

The matrix below shows triangles, ellipses and rectangles at 50, 100 and 200 iterations each.

Matrix

How it Works, Part II

Say we have a Target Image. This is what we're working towards recreating. We start with a blank canvas, but we fill it with a single solid color. Currently, this is the average color of the Target Image. We call this new blank canvas the Current Image. Now, we start evaluating shapes. To evaluate a shape, we draw it on top of the Current Image, producing a New Image. This New Image is compared to the Target Image to compute a score. We use the root-mean-square error for the score.

Current Image + Shape => New Image
RMSE(New Image, Target Image) => Score

The shapes are generated randomly. We can generate a random shape and score it. Then we can mutate the shape (by tweaking a triangle vertex, tweaking an ellipse radius or center, etc.) and score it again. If the mutation improved the score, we keep it. Otherwise we rollback to the previous state. Repeating this process is known as hill climbing. Hill climbing is prone to getting stuck in local minima, so we actually do this many different times with several different starting shapes. We can also generate N random shapes and pick the best one before we start hill climbing. Simulated annealing is another good option, but in my tests I found the hill climbing technique just as good and faster, at least for this particular problem.

Once we have found a good-scoring shape, we add it to the Current Image, where it will remain unchanged. Then we start the process again to find the next shape to draw. This process is repeated as many times as desired.

Primitives

The following primitives are supported:

  • Triangle
  • Rectangle (axis-aligned)
  • Ellipse (axis-aligned)
  • Circle
  • Rotated Rectangle
  • Combo (a mix of the above in a single image)

More shapes can be added by implementing the following interface:

type Shape interface {
	Rasterize() []Scanline
	Copy() Shape
	Mutate()
	Draw(dc *gg.Context)
	SVG(attrs string) string
}

Features

  • Hill Climbing or Simulated Annealing for optimization (hill climbing multiple random shapes is nearly as good as annealing and faster)
  • Scanline rasterization of shapes in pure Go (preferable for implementing the features below)
  • Optimal color computation based on affected pixels for each shape (color is directly computed, not optimized for)
  • Partial image difference for faster scoring (only pixels that change need be considered)
  • Anti-aliased output rendering

Inspiration

This project was originally inspired by the popular and excellent work of Roger Johansson - Genetic Programming: Evolution of Mona Lisa. Since seeing that article when it was quite new, I've tinkered with this problem here and there over the years. But only now am I satisfied with my results.

It should be noted that there are significant differences in my implementation compared to Roger's original work. Mine is not a genetic algorithm. Mine only operates on one shape at a time. Mine is much faster (AFAIK) and supports many types of shapes.

Examples

Here are more examples from interesting photos found on Flickr.

Example Example Example Example Example Example Example Example Example Example Example Example Example Example Example Example Example Example Example Example

Owner
Michael Fogleman
Software Engineer at Formlabs
Michael Fogleman
Comments
  • Going through all mode instead of random

    Going through all mode instead of random

    OK, so I pushed my original idea of going through all different mode at each iteration to pich the best form providing the lowest score. The result is that the final score doing it that way is significantly lower than just using mode 1. Mode 1 usually provide the best score of all methods.

    http://imgur.com/a/11jDL

    As you can see even with a lower score the triangle method provide the best looking image to my eye... but it is interesting to see that you can get lower score bay going through all possible shapes. Here is the code I used. Not optimal but you might want to try it:

    func (model *Model) BestHillClimbState(buffer *image.RGBA, t ShapeType, a, n, age, m int, rnd *rand.Rand) *State {
        var bestEnergy float64
        var bestState *State
        //v("%d\n",m)
        if t != 0 {
            for i := 0; i < m; i++ {
                state := model.BestRandomState(buffer, t, a, n, rnd)
                before := state.Energy()
                state = HillClimb(state, age).(*State)
                energy := state.Energy()
                vv("%dx random: %.6f -> %dx hill climb: %.6f\n", n, before, age, energy)
                if i == 0 || energy < bestEnergy {
                    bestEnergy = energy
                    bestState = state
                }
            }
        } else {
            for j := 1; j < 6; j++ {
                for i := 0; i < m; i++ {
                    state := model.BestRandomState(buffer, ShapeType(j), a, n, rnd)
                    before := state.Energy()
                    state = HillClimb(state, age).(*State)
                    energy := state.Energy()
    
                    if i == 0 && j == 1 || energy < bestEnergy {
                        vv("%dx random: %.6f -> %dx hill climb: %.6f\n", n, before, age, energy)
                        bestEnergy = energy
                        bestState = state
                    }
                }
            }
        }
    
        return bestState
    }
    

    This is the parameters I used:

    primitive -i src.jpg -o test0v1.svg -n 1000 -v -m 0

    vs

    primitive -i src.jpg -o test1v1.svg -n 1000 -v -m 1

    Obviously the downside is that it take 5 times longer to get the result...

  • Parallelize *Model.computeColor

    Parallelize *Model.computeColor

    This PR parallelizes *Model.computeColor. One goroutine per line.

    On my computer, in some very crude benchmark (owl.png, n=3) it shows a ~25% gain. It does not alter the results (checked using https://github.com/hectorj/primitive/blob/basic-test/main_test.go).

    (PS: I love your work, results look amazing)

  • Background color, edge-treatment, transparency and generated SVG integration

    Background color, edge-treatment, transparency and generated SVG integration

    Hi Michael

    I wanted to have a snooze on this before writing the issue as it was 4AM and my mind was far below even at its usually unimpressive level. Even now it's probably not the clearest, and could perhaps be better broken down in to a number of component issues by yourself.

    In short I am quite happy indeed with the generated SVGs and have been able to get them loading in to my Lua/LÖVE-based game environment after writing my quick and hacky svg2love tool.

    The issue I am facing now is that for anything but 'picture in a square' level of graphical integration, there is a need to obtain non-hard edges on the images, and/or some additional intelligence in primitive with respect to output and transparency.

    Transparency: Currently it seems that primitive assumes that an image should be drawn to 100% opacity in all places, drawing a background rectangle in output SVGs to kick off the process. What I would really like is the option to not include this background rectangle or the assumption that it is based on. For example, if I could run with a switch like primitive -b 000000 to have primitive assume that the background was already present and black. In this case, the background rectangle would not be drawn in the resulting SVG but more importantly the other colors would have been chosen in such a manner as to create the desired output assuming a black background. Then if I were to paste on to a blackish background, the majority of the image would have solid opacity, and the image would integrate more closely with arbitrary environments without demanding a square border.

    Edges: There are semi-frequently significant edge artifacts in the images. Thus far I have been dealing with these by cropping, but I wonder if there is an algorithmic enhancement or adjustment that you could make to avoid having, for example, long shapes occurring along the edge of an image bringing with them an additional 'gutter' to the output image.

  • I have downloaded the package ,how  can I run it ?

    I have downloaded the package ,how can I run it ?

    In the seconde step ,I input the line "primitive -i 1.jpg -o 1.jpg n-100",but show that "bash: primitive: command not found" ?How to run it ? I am a green hand ,thanks.

  • Not recognizing primitive as a command

    Not recognizing primitive as a command

    Hi!

    I'm having trouble trying to run primitive after following the instructions. It's similar to another open issue but not quite the same.

    When I use "go get -u github.com/fogleman/primitive", should I be expecting any output? I don't see anything, but there's a pause which makes it seems like it's fetching the dependencies. When it stops, there aren't any new directories or files in my current directory. But when I try "primitive -i input.png -o output.png -n 100" (filling in the input.png and output.png with my image name), I get an error message saying "'primitive' is not recognized as an internal or external command, operable program or batch file." I tried this both on my Windows machine and a virtual machine running Linux.

    Is this something that has occurred before? Are there any other steps I should take or check?

    Thank you!

  • failed MSpanList_Insert

    failed MSpanList_Insert

    Used it like so:

    primitive -i mushroom.jpg -o mushroom-primitive.jpg -n 100 -r=256
    

    With this pic:

    ![mushroom](https://cloud.githubusercontent.com/assets/1270998/18768354/8ddb334e-80f2-11e6-8bc2-2976f49cf947.jpg)

    Error log:

    failed MSpanList_Insert 0x35dd90 0x4b897ae7b8e 0x0
    fatal error: MSpanList_Insert
    
    runtime stack:
    runtime.MSpanList_Insert(0x30aa68, 0x35dd90)
        /usr/local/go/src/runtime/mheap.c:692 +0x8f
    runtime.MHeap_Alloc(0x307660, 0x1, 0x1000000002b, 0xe209)
        /usr/local/go/src/runtime/mheap.c:240 +0x66
    runtime.MCentral_CacheSpan(0x310ff8, 0x35dcc0)
        /usr/local/go/src/runtime/mcentral.c:85 +0x167
    runtime.MCache_Refill(0x349000, 0x2b, 0x35dcc0)
        /usr/local/go/src/runtime/mcache.c:90 +0xa0
    
    goroutine 1 [running]:
    runtime.switchtoM()
        /usr/local/go/src/runtime/asm_amd64.s:198 fp=0xc2088bf8b8 sp=0xc2088bf8b0
    runtime.mallocgc(0x1000, 0x0, 0x3, 0x1d3f9)
        /usr/local/go/src/runtime/malloc.go:178 +0x849 fp=0xc2088bf968 sp=0xc2088bf8b8
    runtime.rawmem(0x1000, 0x1000)
        /usr/local/go/src/runtime/malloc.go:371 +0x39 fp=0xc2088bf990 sp=0xc2088bf968
    runtime.growslice(0x174b20, 0xc2080e4800, 0x53, 0x53, 0x1a, 0x0, 0x0, 0x0)
        /usr/local/go/src/runtime/slice.go:83 +0x237 fp=0xc2088bf9f0 sp=0xc2088bf990
    github.com/fogleman/primitive/primitive.rasterizeTriangle(0xff, 0x21, 0xff, 0x73, 0x23, 0x8d, 0x0, 0x0, 0x0)
        /Users/jmazz/go/src/github.com/fogleman/primitive/primitive/triangle.go:126 +0x348 fp=0xc2088bfa98 sp=0xc2088bf9f0
    github.com/fogleman/primitive/primitive.(*Triangle).Rasterize(0xc2084c5e40, 0x0, 0x0, 0x0)
        /Users/jmazz/go/src/github.com/fogleman/primitive/primitive/triangle.go:101 +0x74 fp=0xc2088bfae8 sp=0xc2088bfa98
    github.com/fogleman/primitive/primitive.(*Model).Energy(0xc208058180, 0x35b770, 0xc2084c5e40, 0xc208032a40, 0xc2084c5e40)
        /Users/jmazz/go/src/github.com/fogleman/primitive/primitive/model.go:213 +0x44 fp=0xc2088bfb78 sp=0xc2088bfae8
    github.com/fogleman/primitive/primitive.(*State).Energy(0xc20803ccc0, 0x1db160)
        /Users/jmazz/go/src/github.com/fogleman/primitive/primitive/state.go:16 +0x4c fp=0xc2088bfba8 sp=0xc2088bfb78
    github.com/fogleman/primitive/primitive.HillClimb(0x35b7b8, 0xc20803ccc0, 0x64, 0x0, 0x0)
        /Users/jmazz/go/src/github.com/fogleman/primitive/primitive/anneal.go:63 +0xec fp=0xc2088bfc00 sp=0xc2088bfba8
    github.com/fogleman/primitive/primitive.(*Model).BestHillClimbState(0xc208058180, 0xc208032a40, 0x1, 0x64, 0x64, 0xa, 0x3a7e5)
        /Users/jmazz/go/src/github.com/fogleman/primitive/primitive/model.go:135 +0xf4 fp=0xc2088bfd00 sp=0xc2088bfc00
    github.com/fogleman/primitive/primitive.(*Model).Step(0xc208058180)
        /Users/jmazz/go/src/github.com/fogleman/primitive/primitive/model.go:122 +0x55 fp=0xc2088bfd58 sp=0xc2088bfd00
    main.main()
        /Users/jmazz/go/src/github.com/fogleman/primitive/main.go:109 +0x69a fp=0xc2088bff98 sp=0xc2088bfd58
    runtime.main()
        /usr/local/go/src/runtime/proc.go:63 +0xf3 fp=0xc2088bffe0 sp=0xc2088bff98
    runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:2232 +0x1 fp=0xc2088bffe8 sp=0xc2088bffe0
    
  • Documenting functions

    Documenting functions

    Hey I thought your project was a beautiful idea, and became interested in reading through the code.

    For what it's worth I have tried to document the functions in the code, from what I have gleaned from trying to comprehend the projects workings.

    You yourself may not see them as being so good or useful (though I hope you do), but I thought they might help someone new coming to this project's codebase, so I thought I'd pull-request what I've done. It might save them time breaking down the codebase.

  • Edge case bug?

    Edge case bug?

    Looks like 0 is passed to Intn?

    panic: invalid argument to Intn
    
    goroutine 8 [running]:
    panic(0x539800, 0xc42000edb0)
        /usr/lib/go/src/runtime/panic.go:500 +0x1a1
    math/rand.(*Rand).Intn(0xc4200104a0, 0x0, 0x0)
        /usr/lib/go/src/math/rand/rand.go:116 +0xd0
    github.com/fogleman/primitive/primitive.NewRandomEllipse(0x1, 0x1, 0xc4200104a0, 0x0)
        /usr/lib/go/src/github.com/fogleman/primitive/primitive/ellipse.go:22 +0x8d
    github.com/fogleman/primitive/primitive.(*Model).RandomState(0xc420072210, 0xc420018600, 0x3, 0x80, 0xc4200104a0, 0x20)
        /usr/lib/go/src/github.com/fogleman/primitive/primitive/model.go:198 +0x3bd
    github.com/fogleman/primitive/primitive.(*Model).BestRandomState(0xc420072210, 0xc420018600, 0x3, 0x80, 0x64, 0xc4200104a0, 0x0)
        /usr/lib/go/src/github.com/fogleman/primitive/primitive/model.go:179 +0x7e
    github.com/fogleman/primitive/primitive.(*Model).BestHillClimbState(0xc420072210, 0xc420018600, 0x3, 0x80, 0x64, 0x64, 0x2, 0xc4200104a0, 0x0)
        /usr/lib/go/src/github.com/fogleman/primitive/primitive/model.go:162 +0xcf
    github.com/fogleman/primitive/primitive.(*Model).runWorker(0xc420072210, 0x3, 0x80, 0x64, 0x64, 0x2, 0xc42005a180)
        /usr/lib/go/src/github.com/fogleman/primitive/primitive/model.go:154 +0x234
    created by github.com/fogleman/primitive/primitive.(*Model).runWorkers
        /usr/lib/go/src/github.com/fogleman/primitive/primitive/model.go:136 +0x13b
    
  • Export steps

    Export steps

    I'm having a great artistic time with Primitive. As a photograph myself there's a lot of reinterpretations of my previous works that are now accessible. So thank you for such a tool with massive palette of expressivity.

    Currently the tool mix a lot of steps together. Reading the file, analysing the content, finding the best solution to match the forms, drawing them and writing the file.

    Would it be possible to have an option to export the drawing steps in a basic file (Json ?) so we can draw the result in another tool (for people not confortable with Go) or have a handle in the Api so we can add other treatment to the form before it is drawn (for Gopher).

    Thank you Michael for any feedback on the idea.

  • Silently fails when specifying jpg as output

    Silently fails when specifying jpg as output

    Running latest Go on Windows 10, specifying

    primitive.exe -i file.jpg -o otherfile.jpg -n 100 -v
    

    fails to write the output file with no error, verbose output ends with writing otherfile.jpg. Switching to -o otherfile.png works.

  • Telegram bot?

    Telegram bot?

    @fogleman, are you ok if I make a telegram bot that modify user pictures and send them back? I'm already crafting it, but if you don't agree I'll stop.

  • Port lib to webassembly library

    Port lib to webassembly library

    This can allow for the website to have a sort of demo or web-based version that others can use.

    As far as I know, tinygo seems to have support for this. Would this be doable?

  • Install instructions no longer helpful

    Install instructions no longer helpful

    go get -u github.com/fogleman/primitive no longer works and I'm getting "-u provided but not defined" error when using the new go install. Please help :)

  • What's the meaning of

    What's the meaning of "worker"

    Your work is amazing and I am truly amazed! While reading the code, I have something that I don't understand. What does 'worker' mean? Is it to start multiple threads at the same time to perform the hill climbing algorithm to get the optimal result? If only one worker is used, will it affect the result?

  • question on quadratic bezier conditions

    question on quadratic bezier conditions

    What is the significance of the below conditions? I can't find a resource on it.

    https://github.com/fogleman/primitive/blob/master/primitive/quadratic.go#L78

Resize upladed images to s3 bucket with given sizes, and uploades new images back to bucket

Features Resize upladed images to s3 bucket with given sizes, and uploades new images back to bucket Environment Variables IMAGE_SIZES - formax 200x20

Feb 2, 2022
Read and write Netpbm images from Go programs

netpbm Introduction netpbm is a package for the Go programming language that implements image decoders and encoders for the Netpbm image formats. The

Dec 29, 2022
Pure Golang Library that allows simple LSB steganography on images
Pure Golang Library that allows simple LSB steganography on images

Steganography Lib Steganography is a library written in Pure go to allow simple LSB steganography on images. It is capable of both encoding and decodi

Dec 22, 2022
Resize images and animated GIFs in Go

Lilliput relies on mature, high-performance C libraries to do most of the work of decompressing, resizing and compressing images. It aims to do as little memory allocation as possible and especially not to create garbage in Go. As a result, it is suitable for very high throughput image resizing services.

Jan 3, 2023
Fast and secure standalone server for resizing and converting remote images

imgproxy imgproxy is a fast and secure standalone server for resizing and converting remote images. The main principles of imgproxy are simplicity, sp

Jan 1, 2023
An iterative algorithm to generate high quality triangulated images.
An iterative algorithm to generate high quality triangulated images.

Triangula uses a modified genetic algorithm to triangulate images. It works best with images smaller than 3000px and with fewer than 3000 points, typically producing an optimal result within a couple of minutes.

Jan 8, 2023
A go library for reading and creating ISO9660 images

iso9660 A package for reading and creating ISO9660, forked from https://github.com/kdomanski/iso9660. Requires Go 1.13 or newer. Joliet and Rock Ridge

Mar 4, 2021
Generate high-quality triangulated art from images.
Generate high-quality triangulated art from images.

An iterative algorithm to generate high quality triangulated images.

May 26, 2021
A cross-platform tool to convert images into ascii art and print them on the console
A cross-platform tool to convert images into ascii art and print them on the console

A cross-platform tool to convert images into ascii art and print them on the console

Dec 30, 2022
Convert images to computer generated art using delaunay triangulation.
Convert images to computer generated art using delaunay triangulation.

▲ Triangle is a tool for generating triangulated image using delaunay triangulation. It takes a source image and converts it to an abstract image comp

Dec 29, 2022
Emoji images for Ebiten
Emoji images for Ebiten

Ebiten Emoji Alpha version: The API is not stable yet Package emoji provides Emoji images for Ebiten. Usage func (*YourGame) Draw(screen *ebiten.Image

Dec 11, 2022
An experiment in rendering images with Slack custom emojis.
An experiment in rendering images with Slack custom emojis.

emojimage An experiment in rendering images with Slack custom emojis. Example Usage 1. Initializing your workspace First, you'll need to upload 1,332

Mar 12, 2022
a tool to output images as RGB ANSI graphics on the terminal
a tool to output images as RGB ANSI graphics on the terminal

imgcat Tool to output images in the terminal. Built with bubbletea install homebrew brew install trashhalo/homebrew-brews/imgcat prebuilt packages Pr

Dec 28, 2022
Image compression codec for 16 bit medical images

MIC - Medical Image Codec This library introduces a lossless medical image compression codec MIC for 16 bit images which provides compression ratio si

Dec 26, 2021
Small project to convert images to grayscale

This is a small http server that will read images from a provided path, convert the image to gray, and then serve back the converted image Usage You c

Nov 5, 2021
Cryptseaside generates seaside images using Unix nanoseconds as the seed value.
Cryptseaside generates seaside images using Unix nanoseconds as the seed value.

Cryptseaside Welcome to the Cryptseaside project. Cryptseaside generates seaside images using Unix nanoseconds as the seed value.

Nov 12, 2021
Repaint images by sorting pixels by colour
Repaint images by sorting pixels by colour

Every Frame A Painting Sorts pixels in an image by colour and redraws the image

Jan 5, 2022
Provides a method to create thumbnails from provided images.

Thumbnail Generation Package for Go This package provides method to create thumbnails from provided images. Installation Use to go command: $ go get g

Aug 31, 2022
This command line converts .html file into .html with images embed.

embed-html This command line converts .html file into .html with images embed. Install > go get github.com/gonejack/embed-html Usage > embed-html *.ht

Oct 6, 2022