Repository: fogleman/primitive
Branch: master
Commit: 0373c216458b
Files: 26
Total size: 63.6 KB
Directory structure:
gitextract_s2bcgc36/
├── .gitignore
├── LICENSE.md
├── README.md
├── bot/
│ ├── .gitignore
│ ├── main.py
│ └── requirements.txt
├── main.go
├── primitive/
│ ├── color.go
│ ├── core.go
│ ├── ellipse.go
│ ├── heatmap.go
│ ├── log.go
│ ├── model.go
│ ├── optimize.go
│ ├── polygon.go
│ ├── quadratic.go
│ ├── raster.go
│ ├── rectangle.go
│ ├── scanline.go
│ ├── shape.go
│ ├── state.go
│ ├── triangle.go
│ ├── util.go
│ └── worker.go
└── scripts/
├── html.py
└── process.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/*.png
/*.svg
/*.gif
================================================
FILE: LICENSE.md
================================================
Copyright (C) 2016 Michael Fogleman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Primitive Pictures
Reproducing images with geometric primitives.

### 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](https://twitter.com/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](https://golang.org/doc/install).
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.

### 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.

### Shape and Iteration Comparison Matrix
The matrix below shows triangles, ellipses and rectangles at 50, 100 and 200 iterations each.

### 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](https://en.wikipedia.org/wiki/Root-mean-square_deviation) 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](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/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:
```go
type Shape interface {
Rasterize() []Scanline
Copy() Shape
Mutate()
Draw(dc *gg.Context)
SVG(attrs string) string
}
```
### Features
- [Hill Climbing](https://en.wikipedia.org/wiki/Hill_climbing) or [Simulated Annealing](https://en.wikipedia.org/wiki/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](https://rogeralsing.com/2008/12/07/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.




















================================================
FILE: bot/.gitignore
================================================
config.py
env
venv
================================================
FILE: bot/main.py
================================================
import datetime
import os
import random
import requests
import subprocess
import time
import traceback
import twitter
RATE = 60 * 30
MENTION_RATE = 65
INPUT_FOLDER = ''
OUTPUT_FOLDER = ''
FLICKR_API_KEY = None
TWITTER_CONSUMER_KEY = None
TWITTER_CONSUMER_SECRET = None
TWITTER_ACCESS_TOKEN_KEY = None
TWITTER_ACCESS_TOKEN_SECRET = None
MODE_NAMES = [
'primitives', # 0
'triangles', # 1
'rectangles', # 2
'ellipses', # 3
'circles', # 4
'rectangles', # 5
'beziers', # 6
'ellipses', # 7
'polygons', # 8
]
SINCE_ID = None
START_DATETIME = datetime.datetime.utcnow()
USER_DATETIME = {}
try:
from config import *
except ImportError:
print 'no config found!'
class AttrDict(dict):
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self
class Config(AttrDict):
def randomize(self):
self.m = random.choice([1, 5, 6, 7])
self.n = random.randint(10, 50) * 10
self.rep = 0
self.a = 128
self.r = 300
self.s = 1200
def parse(self, text):
text = (text or '').lower()
tokens = text.split()
for i, name in enumerate(MODE_NAMES):
if name in text:
self.m = i
for token in tokens:
try:
self.n = int(token)
except Exception:
pass
def validate(self):
self.m = clamp(self.m, 0, 8)
if self.m == 6:
self.a = 0
self.rep = 19
self.n = 100
else:
self.n = clamp(self.n, 1, 500)
@property
def description(self):
total = self.n + self.n * self.rep
return '%d %s' % (total, MODE_NAMES[self.m])
def clamp(x, lo, hi):
if x < lo:
x = lo
if x > hi:
x = hi
return x
def random_date(max_days_ago=1000):
today = datetime.date.today()
days = random.randint(1, max_days_ago)
d = today - datetime.timedelta(days=days)
return d.strftime('%Y-%m-%d')
def interesting(date=None):
url = 'https://api.flickr.com/services/rest/'
params = dict(
api_key=FLICKR_API_KEY,
format='json',
nojsoncallback=1,
method='flickr.interestingness.getList',
)
if date:
params['date'] = date
r = requests.get(url, params=params)
return r.json()['photos']['photo']
def photo_url(p, size=None):
# See: https://www.flickr.com/services/api/misc.urls.html
if size:
url = 'https://farm%s.staticflickr.com/%s/%s_%s_%s.jpg'
return url % (p['farm'], p['server'], p['id'], p['secret'], size)
else:
url = 'https://farm%s.staticflickr.com/%s/%s_%s.jpg'
return url % (p['farm'], p['server'], p['id'], p['secret'])
def download_photo(url, path):
r = requests.get(url)
with open(path, 'wb') as fp:
fp.write(r.content)
def primitive(**kwargs):
args = []
for k, v in kwargs.items():
if v is None:
continue
args.append('-%s' % k)
args.append(str(v))
args = ' '.join(args)
cmd = 'primitive %s' % args
subprocess.call(cmd, shell=True)
def twitter_api():
return twitter.Api(
consumer_key=TWITTER_CONSUMER_KEY,
consumer_secret=TWITTER_CONSUMER_SECRET,
access_token_key=TWITTER_ACCESS_TOKEN_KEY,
access_token_secret=TWITTER_ACCESS_TOKEN_SECRET)
def tweet(status, media, in_reply_to_status_id=None):
api = twitter_api()
api.PostUpdate(status, media, in_reply_to_status_id=in_reply_to_status_id)
def handle_mentions():
global SINCE_ID
print 'checking for mentions'
api = twitter_api()
statuses = api.GetMentions(200, SINCE_ID)
for status in reversed(statuses):
SINCE_ID = status.id
print 'handling mention', status.id
handle_mention(status)
print 'done with mentions'
def handle_mention(status):
mentions = status.user_mentions or []
if len(mentions) != 1:
print 'mention does not have exactly one mention'
return
media = status.media or []
if len(media) != 1:
print 'mention does not have exactly one media'
return
url = media[0].media_url or None
if not url:
print 'mention does not have a media_url'
return
created_at = datetime.datetime.strptime(
status.created_at, '%a %b %d %H:%M:%S +0000 %Y')
if created_at < START_DATETIME:
print 'mention timestamp before bot started'
return
user_id = status.user.id
now = datetime.datetime.utcnow()
td = datetime.timedelta(minutes=5)
if user_id in USER_DATETIME:
if now - USER_DATETIME[user_id] < td:
print 'user mentioned me too recently'
return
USER_DATETIME[user_id] = now
in_path = os.path.join(INPUT_FOLDER, '%s.jpg' % status.id)
out_path = os.path.join(OUTPUT_FOLDER, '%s.png' % status.id)
print 'downloading', url
download_photo(url, in_path)
config = Config()
config.randomize()
config.parse(status.text)
config.validate()
status_text = '@%s %s.' % (status.user.screen_name, config.description)
print status_text
print 'running algorithm: %s' % config
primitive(i=in_path, o=out_path, **config)
if os.path.exists(out_path):
print 'uploading to twitter'
tweet(status_text, out_path, status.id)
print 'done'
else:
print 'failed!'
def flickr_url(photo_id):
alphabet = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
return 'https://flic.kr/p/%s' % base_encode(alphabet, int(photo_id))
def base_encode(alphabet, number, suffix=''):
base = len(alphabet)
if number >= base:
div, mod = divmod(number, base)
return base_encode(alphabet, div, alphabet[mod] + suffix)
else:
return alphabet[number] + suffix
def generate():
date = random_date()
print 'finding an interesting photo from', date
photos = interesting(date)
photo = random.choice(photos)
print 'picked photo', photo['id']
in_path = os.path.join(INPUT_FOLDER, '%s.jpg' % photo['id'])
out_path = os.path.join(OUTPUT_FOLDER, '%s.png' % photo['id'])
url = photo_url(photo, 'z')
print 'downloading', url
download_photo(url, in_path)
config = Config()
config.randomize()
config.validate()
status_text = '%s. %s' % (config.description, flickr_url(photo['id']))
print status_text
print 'running algorithm: %s' % config
primitive(i=in_path, o=out_path, **config)
if os.path.exists(out_path):
print 'uploading to twitter'
tweet(status_text, out_path)
print 'done'
else:
print 'failed!'
def main():
previous = 0
mention_previous = 0
while True:
now = time.time()
if now - previous > RATE:
previous = now
try:
generate()
except Exception:
traceback.print_exc()
if now - mention_previous > MENTION_RATE:
mention_previous = now
try:
handle_mentions()
except Exception:
traceback.print_exc()
time.sleep(5)
def download_photos(folder, date=None):
try:
os.makedirs(folder)
except Exception:
pass
date = date or random_date()
photos = interesting(date)
for photo in photos:
url = photo_url(photo, 'z')
path = '%s.jpg' % photo['id']
path = os.path.join(folder, path)
download_photo(url, path)
if __name__ == '__main__':
main()
================================================
FILE: bot/requirements.txt
================================================
python-twitter==3.1
requests==2.11.1
================================================
FILE: main.go
================================================
package main
import (
"flag"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/fogleman/primitive/primitive"
"github.com/nfnt/resize"
)
var (
Input string
Outputs flagArray
Background string
Configs shapeConfigArray
Alpha int
InputSize int
OutputSize int
Mode int
Workers int
Nth int
Repeat int
V, VV bool
)
type flagArray []string
func (i *flagArray) String() string {
return strings.Join(*i, ", ")
}
func (i *flagArray) Set(value string) error {
*i = append(*i, value)
return nil
}
type shapeConfig struct {
Count int
Mode int
Alpha int
Repeat int
}
type shapeConfigArray []shapeConfig
func (i *shapeConfigArray) String() string {
return ""
}
func (i *shapeConfigArray) Set(value string) error {
n, _ := strconv.ParseInt(value, 0, 0)
*i = append(*i, shapeConfig{int(n), Mode, Alpha, Repeat})
return nil
}
func init() {
flag.StringVar(&Input, "i", "", "input image path")
flag.Var(&Outputs, "o", "output image path")
flag.Var(&Configs, "n", "number of primitives")
flag.StringVar(&Background, "bg", "", "background color (hex)")
flag.IntVar(&Alpha, "a", 128, "alpha value")
flag.IntVar(&InputSize, "r", 256, "resize large input images to this size")
flag.IntVar(&OutputSize, "s", 1024, "output image size")
flag.IntVar(&Mode, "m", 1, "0=combo 1=triangle 2=rect 3=ellipse 4=circle 5=rotatedrect 6=beziers 7=rotatedellipse 8=polygon")
flag.IntVar(&Workers, "j", 0, "number of parallel workers (default uses all cores)")
flag.IntVar(&Nth, "nth", 1, "save every Nth frame (put \"%d\" in path)")
flag.IntVar(&Repeat, "rep", 0, "add N extra shapes per iteration with reduced search")
flag.BoolVar(&V, "v", false, "verbose")
flag.BoolVar(&VV, "vv", false, "very verbose")
}
func errorMessage(message string) bool {
fmt.Fprintln(os.Stderr, message)
return false
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
// parse and validate arguments
flag.Parse()
ok := true
if Input == "" {
ok = errorMessage("ERROR: input argument required")
}
if len(Outputs) == 0 {
ok = errorMessage("ERROR: output argument required")
}
if len(Configs) == 0 {
ok = errorMessage("ERROR: number argument required")
}
if len(Configs) == 1 {
Configs[0].Mode = Mode
Configs[0].Alpha = Alpha
Configs[0].Repeat = Repeat
}
for _, config := range Configs {
if config.Count < 1 {
ok = errorMessage("ERROR: number argument must be > 0")
}
}
if !ok {
fmt.Println("Usage: primitive [OPTIONS] -i input -o output -n count")
flag.PrintDefaults()
os.Exit(1)
}
// set log level
if V {
primitive.LogLevel = 1
}
if VV {
primitive.LogLevel = 2
}
// seed random number generator
rand.Seed(time.Now().UTC().UnixNano())
// determine worker count
if Workers < 1 {
Workers = runtime.NumCPU()
}
// read input image
primitive.Log(1, "reading %s\n", Input)
input, err := primitive.LoadImage(Input)
check(err)
// scale down input image if needed
size := uint(InputSize)
if size > 0 {
input = resize.Thumbnail(size, size, input, resize.Bilinear)
}
// determine background color
var bg primitive.Color
if Background == "" {
bg = primitive.MakeColor(primitive.AverageImageColor(input))
} else {
bg = primitive.MakeHexColor(Background)
}
// run algorithm
model := primitive.NewModel(input, bg, OutputSize, Workers)
primitive.Log(1, "%d: t=%.3f, score=%.6f\n", 0, 0.0, model.Score)
start := time.Now()
frame := 0
for j, config := range Configs {
primitive.Log(1, "count=%d, mode=%d, alpha=%d, repeat=%d\n",
config.Count, config.Mode, config.Alpha, config.Repeat)
for i := 0; i < config.Count; i++ {
frame++
// find optimal shape and add it to the model
t := time.Now()
n := model.Step(primitive.ShapeType(config.Mode), config.Alpha, config.Repeat)
nps := primitive.NumberString(float64(n) / time.Since(t).Seconds())
elapsed := time.Since(start).Seconds()
primitive.Log(1, "%d: t=%.3f, score=%.6f, n=%d, n/s=%s\n", frame, elapsed, model.Score, n, nps)
// write output image(s)
for _, output := range Outputs {
ext := strings.ToLower(filepath.Ext(output))
if output == "-" {
ext = ".svg"
}
percent := strings.Contains(output, "%")
saveFrames := percent && ext != ".gif"
saveFrames = saveFrames && frame%Nth == 0
last := j == len(Configs)-1 && i == config.Count-1
if saveFrames || last {
path := output
if percent {
path = fmt.Sprintf(output, frame)
}
primitive.Log(1, "writing %s\n", path)
switch ext {
default:
check(fmt.Errorf("unrecognized file extension: %s", ext))
case ".png":
check(primitive.SavePNG(path, model.Context.Image()))
case ".jpg", ".jpeg":
check(primitive.SaveJPG(path, model.Context.Image(), 95))
case ".svg":
check(primitive.SaveFile(path, model.SVG()))
case ".gif":
frames := model.Frames(0.001)
check(primitive.SaveGIFImageMagick(path, frames, 50, 250))
}
}
}
}
}
}
================================================
FILE: primitive/color.go
================================================
package primitive
import (
"fmt"
"image/color"
"strings"
)
type Color struct {
R, G, B, A int
}
func MakeColor(c color.Color) Color {
r, g, b, a := c.RGBA()
return Color{int(r / 257), int(g / 257), int(b / 257), int(a / 257)}
}
func MakeHexColor(x string) Color {
x = strings.Trim(x, "#")
var r, g, b, a int
a = 255
switch len(x) {
case 3:
fmt.Sscanf(x, "%1x%1x%1x", &r, &g, &b)
r = (r << 4) | r
g = (g << 4) | g
b = (b << 4) | b
case 4:
fmt.Sscanf(x, "%1x%1x%1x%1x", &r, &g, &b, &a)
r = (r << 4) | r
g = (g << 4) | g
b = (b << 4) | b
a = (a << 4) | a
case 6:
fmt.Sscanf(x, "%02x%02x%02x", &r, &g, &b)
case 8:
fmt.Sscanf(x, "%02x%02x%02x%02x", &r, &g, &b, &a)
}
return Color{r, g, b, a}
}
func (c *Color) NRGBA() color.NRGBA {
return color.NRGBA{uint8(c.R), uint8(c.G), uint8(c.B), uint8(c.A)}
}
================================================
FILE: primitive/core.go
================================================
package primitive
import (
"image"
"math"
)
func computeColor(target, current *image.RGBA, lines []Scanline, alpha int) Color {
var rsum, gsum, bsum, count int64
a := 0x101 * 255 / alpha
for _, line := range lines {
i := target.PixOffset(line.X1, line.Y)
for x := line.X1; x <= line.X2; x++ {
tr := int(target.Pix[i])
tg := int(target.Pix[i+1])
tb := int(target.Pix[i+2])
cr := int(current.Pix[i])
cg := int(current.Pix[i+1])
cb := int(current.Pix[i+2])
i += 4
rsum += int64((tr-cr)*a + cr*0x101)
gsum += int64((tg-cg)*a + cg*0x101)
bsum += int64((tb-cb)*a + cb*0x101)
count++
}
}
if count == 0 {
return Color{}
}
r := clampInt(int(rsum/count)>>8, 0, 255)
g := clampInt(int(gsum/count)>>8, 0, 255)
b := clampInt(int(bsum/count)>>8, 0, 255)
return Color{r, g, b, alpha}
}
func copyLines(dst, src *image.RGBA, lines []Scanline) {
for _, line := range lines {
a := dst.PixOffset(line.X1, line.Y)
b := a + (line.X2-line.X1+1)*4
copy(dst.Pix[a:b], src.Pix[a:b])
}
}
func drawLines(im *image.RGBA, c Color, lines []Scanline) {
const m = 0xffff
sr, sg, sb, sa := c.NRGBA().RGBA()
for _, line := range lines {
ma := line.Alpha
a := (m - sa*ma/m) * 0x101
i := im.PixOffset(line.X1, line.Y)
for x := line.X1; x <= line.X2; x++ {
dr := uint32(im.Pix[i+0])
dg := uint32(im.Pix[i+1])
db := uint32(im.Pix[i+2])
da := uint32(im.Pix[i+3])
im.Pix[i+0] = uint8((dr*a + sr*ma) / m >> 8)
im.Pix[i+1] = uint8((dg*a + sg*ma) / m >> 8)
im.Pix[i+2] = uint8((db*a + sb*ma) / m >> 8)
im.Pix[i+3] = uint8((da*a + sa*ma) / m >> 8)
i += 4
}
}
}
func differenceFull(a, b *image.RGBA) float64 {
size := a.Bounds().Size()
w, h := size.X, size.Y
var total uint64
for y := 0; y < h; y++ {
i := a.PixOffset(0, y)
for x := 0; x < w; x++ {
ar := int(a.Pix[i])
ag := int(a.Pix[i+1])
ab := int(a.Pix[i+2])
aa := int(a.Pix[i+3])
br := int(b.Pix[i])
bg := int(b.Pix[i+1])
bb := int(b.Pix[i+2])
ba := int(b.Pix[i+3])
i += 4
dr := ar - br
dg := ag - bg
db := ab - bb
da := aa - ba
total += uint64(dr*dr + dg*dg + db*db + da*da)
}
}
return math.Sqrt(float64(total)/float64(w*h*4)) / 255
}
func differencePartial(target, before, after *image.RGBA, score float64, lines []Scanline) float64 {
size := target.Bounds().Size()
w, h := size.X, size.Y
total := uint64(math.Pow(score*255, 2) * float64(w*h*4))
for _, line := range lines {
i := target.PixOffset(line.X1, line.Y)
for x := line.X1; x <= line.X2; x++ {
tr := int(target.Pix[i])
tg := int(target.Pix[i+1])
tb := int(target.Pix[i+2])
ta := int(target.Pix[i+3])
br := int(before.Pix[i])
bg := int(before.Pix[i+1])
bb := int(before.Pix[i+2])
ba := int(before.Pix[i+3])
ar := int(after.Pix[i])
ag := int(after.Pix[i+1])
ab := int(after.Pix[i+2])
aa := int(after.Pix[i+3])
i += 4
dr1 := tr - br
dg1 := tg - bg
db1 := tb - bb
da1 := ta - ba
dr2 := tr - ar
dg2 := tg - ag
db2 := tb - ab
da2 := ta - aa
total -= uint64(dr1*dr1 + dg1*dg1 + db1*db1 + da1*da1)
total += uint64(dr2*dr2 + dg2*dg2 + db2*db2 + da2*da2)
}
}
return math.Sqrt(float64(total)/float64(w*h*4)) / 255
}
================================================
FILE: primitive/ellipse.go
================================================
package primitive
import (
"fmt"
"math"
"github.com/fogleman/gg"
"github.com/golang/freetype/raster"
)
type Ellipse struct {
Worker *Worker
X, Y int
Rx, Ry int
Circle bool
}
func NewRandomEllipse(worker *Worker) *Ellipse {
rnd := worker.Rnd
x := rnd.Intn(worker.W)
y := rnd.Intn(worker.H)
rx := rnd.Intn(32) + 1
ry := rnd.Intn(32) + 1
return &Ellipse{worker, x, y, rx, ry, false}
}
func NewRandomCircle(worker *Worker) *Ellipse {
rnd := worker.Rnd
x := rnd.Intn(worker.W)
y := rnd.Intn(worker.H)
r := rnd.Intn(32) + 1
return &Ellipse{worker, x, y, r, r, true}
}
func (c *Ellipse) Draw(dc *gg.Context, scale float64) {
dc.DrawEllipse(float64(c.X), float64(c.Y), float64(c.Rx), float64(c.Ry))
dc.Fill()
}
func (c *Ellipse) SVG(attrs string) string {
return fmt.Sprintf(
" ",
attrs, c.X, c.Y, c.Rx, c.Ry)
}
func (c *Ellipse) Copy() Shape {
a := *c
return &a
}
func (c *Ellipse) Mutate() {
w := c.Worker.W
h := c.Worker.H
rnd := c.Worker.Rnd
switch rnd.Intn(3) {
case 0:
c.X = clampInt(c.X+int(rnd.NormFloat64()*16), 0, w-1)
c.Y = clampInt(c.Y+int(rnd.NormFloat64()*16), 0, h-1)
case 1:
c.Rx = clampInt(c.Rx+int(rnd.NormFloat64()*16), 1, w-1)
if c.Circle {
c.Ry = c.Rx
}
case 2:
c.Ry = clampInt(c.Ry+int(rnd.NormFloat64()*16), 1, h-1)
if c.Circle {
c.Rx = c.Ry
}
}
}
func (c *Ellipse) Rasterize() []Scanline {
w := c.Worker.W
h := c.Worker.H
lines := c.Worker.Lines[:0]
aspect := float64(c.Rx) / float64(c.Ry)
for dy := 0; dy < c.Ry; dy++ {
y1 := c.Y - dy
y2 := c.Y + dy
if (y1 < 0 || y1 >= h) && (y2 < 0 || y2 >= h) {
continue
}
s := int(math.Sqrt(float64(c.Ry*c.Ry-dy*dy)) * aspect)
x1 := c.X - s
x2 := c.X + s
if x1 < 0 {
x1 = 0
}
if x2 >= w {
x2 = w - 1
}
if y1 >= 0 && y1 < h {
lines = append(lines, Scanline{y1, x1, x2, 0xffff})
}
if y2 >= 0 && y2 < h && dy > 0 {
lines = append(lines, Scanline{y2, x1, x2, 0xffff})
}
}
return lines
}
type RotatedEllipse struct {
Worker *Worker
X, Y float64
Rx, Ry float64
Angle float64
}
func NewRandomRotatedEllipse(worker *Worker) *RotatedEllipse {
rnd := worker.Rnd
x := rnd.Float64() * float64(worker.W)
y := rnd.Float64() * float64(worker.H)
rx := rnd.Float64()*32 + 1
ry := rnd.Float64()*32 + 1
a := rnd.Float64() * 360
return &RotatedEllipse{worker, x, y, rx, ry, a}
}
func (c *RotatedEllipse) Draw(dc *gg.Context, scale float64) {
dc.Push()
dc.RotateAbout(radians(c.Angle), c.X, c.Y)
dc.DrawEllipse(c.X, c.Y, c.Rx, c.Ry)
dc.Fill()
dc.Pop()
}
func (c *RotatedEllipse) SVG(attrs string) string {
return fmt.Sprintf(
" ",
c.X, c.Y, c.Angle, c.Rx, c.Ry, attrs)
}
func (c *RotatedEllipse) Copy() Shape {
a := *c
return &a
}
func (c *RotatedEllipse) Mutate() {
w := c.Worker.W
h := c.Worker.H
rnd := c.Worker.Rnd
switch rnd.Intn(3) {
case 0:
c.X = clamp(c.X+rnd.NormFloat64()*16, 0, float64(w-1))
c.Y = clamp(c.Y+rnd.NormFloat64()*16, 0, float64(h-1))
case 1:
c.Rx = clamp(c.Rx+rnd.NormFloat64()*16, 1, float64(w-1))
c.Ry = clamp(c.Ry+rnd.NormFloat64()*16, 1, float64(w-1))
case 2:
c.Angle = c.Angle + rnd.NormFloat64()*32
}
}
func (c *RotatedEllipse) Rasterize() []Scanline {
var path raster.Path
const n = 16
for i := 0; i < n; i++ {
p1 := float64(i+0) / n
p2 := float64(i+1) / n
a1 := p1 * 2 * math.Pi
a2 := p2 * 2 * math.Pi
x0 := c.Rx * math.Cos(a1)
y0 := c.Ry * math.Sin(a1)
x1 := c.Rx * math.Cos(a1+(a2-a1)/2)
y1 := c.Ry * math.Sin(a1+(a2-a1)/2)
x2 := c.Rx * math.Cos(a2)
y2 := c.Ry * math.Sin(a2)
cx := 2*x1 - x0/2 - x2/2
cy := 2*y1 - y0/2 - y2/2
x0, y0 = rotate(x0, y0, radians(c.Angle))
cx, cy = rotate(cx, cy, radians(c.Angle))
x2, y2 = rotate(x2, y2, radians(c.Angle))
if i == 0 {
path.Start(fixp(x0+c.X, y0+c.Y))
}
path.Add2(fixp(cx+c.X, cy+c.Y), fixp(x2+c.X, y2+c.Y))
}
return fillPath(c.Worker, path)
}
================================================
FILE: primitive/heatmap.go
================================================
package primitive
import (
"image"
"image/color"
"math"
)
type Heatmap struct {
W, H int
Count []uint64
}
func NewHeatmap(w, h int) *Heatmap {
count := make([]uint64, w*h)
return &Heatmap{w, h, count}
}
func (h *Heatmap) Clear() {
for i := range h.Count {
h.Count[i] = 0
}
}
func (h *Heatmap) Add(lines []Scanline) {
for _, line := range lines {
i := line.Y*h.W + line.X1
for x := line.X1; x <= line.X2; x++ {
h.Count[i] += uint64(line.Alpha)
i++
}
}
}
func (h *Heatmap) AddHeatmap(a *Heatmap) {
for i, x := range a.Count {
h.Count[i] += x
}
}
func (h *Heatmap) Image(gamma float64) *image.Gray16 {
im := image.NewGray16(image.Rect(0, 0, h.W, h.H))
var hi uint64
for _, h := range h.Count {
if h > hi {
hi = h
}
}
i := 0
for y := 0; y < h.H; y++ {
for x := 0; x < h.W; x++ {
p := float64(h.Count[i]) / float64(hi)
p = math.Pow(p, gamma)
im.SetGray16(x, y, color.Gray16{uint16(p * 0xffff)})
i++
}
}
return im
}
================================================
FILE: primitive/log.go
================================================
package primitive
import "fmt"
var LogLevel int
func Log(level int, format string, a ...interface{}) {
if LogLevel >= level {
fmt.Printf(format, a...)
}
}
func v(format string, a ...interface{}) {
Log(1, format, a...)
}
func vv(format string, a ...interface{}) {
Log(2, " "+format, a...)
}
func vvv(format string, a ...interface{}) {
Log(3, " "+format, a...)
}
================================================
FILE: primitive/model.go
================================================
package primitive
import (
"fmt"
"image"
"strings"
"github.com/fogleman/gg"
)
type Model struct {
Sw, Sh int
Scale float64
Background Color
Target *image.RGBA
Current *image.RGBA
Context *gg.Context
Score float64
Shapes []Shape
Colors []Color
Scores []float64
Workers []*Worker
}
func NewModel(target image.Image, background Color, size, numWorkers int) *Model {
w := target.Bounds().Size().X
h := target.Bounds().Size().Y
aspect := float64(w) / float64(h)
var sw, sh int
var scale float64
if aspect >= 1 {
sw = size
sh = int(float64(size) / aspect)
scale = float64(size) / float64(w)
} else {
sw = int(float64(size) * aspect)
sh = size
scale = float64(size) / float64(h)
}
model := &Model{}
model.Sw = sw
model.Sh = sh
model.Scale = scale
model.Background = background
model.Target = imageToRGBA(target)
model.Current = uniformRGBA(target.Bounds(), background.NRGBA())
model.Score = differenceFull(model.Target, model.Current)
model.Context = model.newContext()
for i := 0; i < numWorkers; i++ {
worker := NewWorker(model.Target)
model.Workers = append(model.Workers, worker)
}
return model
}
func (model *Model) newContext() *gg.Context {
dc := gg.NewContext(model.Sw, model.Sh)
dc.Scale(model.Scale, model.Scale)
dc.Translate(0.5, 0.5)
dc.SetColor(model.Background.NRGBA())
dc.Clear()
return dc
}
func (model *Model) Frames(scoreDelta float64) []image.Image {
var result []image.Image
dc := model.newContext()
result = append(result, imageToRGBA(dc.Image()))
previous := 10.0
for i, shape := range model.Shapes {
c := model.Colors[i]
dc.SetRGBA255(c.R, c.G, c.B, c.A)
shape.Draw(dc, model.Scale)
dc.Fill()
score := model.Scores[i]
delta := previous - score
if delta >= scoreDelta {
previous = score
result = append(result, imageToRGBA(dc.Image()))
}
}
return result
}
func (model *Model) SVG() string {
bg := model.Background
var lines []string
lines = append(lines, fmt.Sprintf("", model.Sw, model.Sh))
lines = append(lines, fmt.Sprintf(" ", model.Sw, model.Sh, bg.R, bg.G, bg.B))
lines = append(lines, fmt.Sprintf("", model.Scale))
for i, shape := range model.Shapes {
c := model.Colors[i]
attrs := "fill=\"#%02x%02x%02x\" fill-opacity=\"%f\""
attrs = fmt.Sprintf(attrs, c.R, c.G, c.B, float64(c.A)/255)
lines = append(lines, shape.SVG(attrs))
}
lines = append(lines, " ")
lines = append(lines, " ")
return strings.Join(lines, "\n")
}
func (model *Model) Add(shape Shape, alpha int) {
before := copyRGBA(model.Current)
lines := shape.Rasterize()
color := computeColor(model.Target, model.Current, lines, alpha)
drawLines(model.Current, color, lines)
score := differencePartial(model.Target, before, model.Current, model.Score, lines)
model.Score = score
model.Shapes = append(model.Shapes, shape)
model.Colors = append(model.Colors, color)
model.Scores = append(model.Scores, score)
model.Context.SetRGBA255(color.R, color.G, color.B, color.A)
shape.Draw(model.Context, model.Scale)
}
func (model *Model) Step(shapeType ShapeType, alpha, repeat int) int {
state := model.runWorkers(shapeType, alpha, 1000, 100, 16)
// state = HillClimb(state, 1000).(*State)
model.Add(state.Shape, state.Alpha)
for i := 0; i < repeat; i++ {
state.Worker.Init(model.Current, model.Score)
a := state.Energy()
state = HillClimb(state, 100).(*State)
b := state.Energy()
if a == b {
break
}
model.Add(state.Shape, state.Alpha)
}
// for _, w := range model.Workers[1:] {
// model.Workers[0].Heatmap.AddHeatmap(w.Heatmap)
// }
// SavePNG("heatmap.png", model.Workers[0].Heatmap.Image(0.5))
counter := 0
for _, worker := range model.Workers {
counter += worker.Counter
}
return counter
}
func (model *Model) runWorkers(t ShapeType, a, n, age, m int) *State {
wn := len(model.Workers)
ch := make(chan *State, wn)
wm := m / wn
if m%wn != 0 {
wm++
}
for i := 0; i < wn; i++ {
worker := model.Workers[i]
worker.Init(model.Current, model.Score)
go model.runWorker(worker, t, a, n, age, wm, ch)
}
var bestEnergy float64
var bestState *State
for i := 0; i < wn; i++ {
state := <-ch
energy := state.Energy()
if i == 0 || energy < bestEnergy {
bestEnergy = energy
bestState = state
}
}
return bestState
}
func (model *Model) runWorker(worker *Worker, t ShapeType, a, n, age, m int, ch chan *State) {
ch <- worker.BestHillClimbState(t, a, n, age, m)
}
================================================
FILE: primitive/optimize.go
================================================
package primitive
import (
"math"
"math/rand"
)
type Annealable interface {
Energy() float64
DoMove() interface{}
UndoMove(interface{})
Copy() Annealable
}
func HillClimb(state Annealable, maxAge int) Annealable {
state = state.Copy()
bestState := state.Copy()
bestEnergy := state.Energy()
step := 0
for age := 0; age < maxAge; age++ {
undo := state.DoMove()
energy := state.Energy()
if energy >= bestEnergy {
state.UndoMove(undo)
} else {
// fmt.Printf("step: %d, energy: %.6f\n", step, energy)
bestEnergy = energy
bestState = state.Copy()
age = -1
}
step++
}
return bestState
}
func PreAnneal(state Annealable, iterations int) float64 {
state = state.Copy()
previous := state.Energy()
var total float64
for i := 0; i < iterations; i++ {
state.DoMove()
energy := state.Energy()
total += math.Abs(energy - previous)
previous = energy
}
return total / float64(iterations)
}
func Anneal(state Annealable, maxTemp, minTemp float64, steps int) Annealable {
factor := -math.Log(maxTemp / minTemp)
state = state.Copy()
bestState := state.Copy()
bestEnergy := state.Energy()
previousEnergy := bestEnergy
for step := 0; step < steps; step++ {
pct := float64(step) / float64(steps-1)
temp := maxTemp * math.Exp(factor*pct)
undo := state.DoMove()
energy := state.Energy()
change := energy - previousEnergy
if change > 0 && math.Exp(-change/temp) < rand.Float64() {
state.UndoMove(undo)
} else {
previousEnergy = energy
if energy < bestEnergy {
// pct := float64(step*100) / float64(steps)
// fmt.Printf("step: %d of %d (%.1f%%), temp: %.3f, energy: %.6f\n",
// step, steps, pct, temp, energy)
bestEnergy = energy
bestState = state.Copy()
}
}
}
return bestState
}
================================================
FILE: primitive/polygon.go
================================================
package primitive
import (
"fmt"
"strings"
"github.com/fogleman/gg"
"github.com/golang/freetype/raster"
)
type Polygon struct {
Worker *Worker
Order int
Convex bool
X, Y []float64
}
func NewRandomPolygon(worker *Worker, order int, convex bool) *Polygon {
rnd := worker.Rnd
x := make([]float64, order)
y := make([]float64, order)
x[0] = rnd.Float64() * float64(worker.W)
y[0] = rnd.Float64() * float64(worker.H)
for i := 1; i < order; i++ {
x[i] = x[0] + rnd.Float64()*40 - 20
y[i] = y[0] + rnd.Float64()*40 - 20
}
p := &Polygon{worker, order, convex, x, y}
p.Mutate()
return p
}
func (p *Polygon) Draw(dc *gg.Context, scale float64) {
dc.NewSubPath()
for i := 0; i < p.Order; i++ {
dc.LineTo(p.X[i], p.Y[i])
}
dc.ClosePath()
dc.Fill()
}
func (p *Polygon) SVG(attrs string) string {
ret := fmt.Sprintf(
" "
}
func (p *Polygon) Copy() Shape {
a := *p
a.X = make([]float64, p.Order)
a.Y = make([]float64, p.Order)
copy(a.X, p.X)
copy(a.Y, p.Y)
return &a
}
func (p *Polygon) Mutate() {
const m = 16
w := p.Worker.W
h := p.Worker.H
rnd := p.Worker.Rnd
for {
if rnd.Float64() < 0.25 {
i := rnd.Intn(p.Order)
j := rnd.Intn(p.Order)
p.X[i], p.Y[i], p.X[j], p.Y[j] = p.X[j], p.Y[j], p.X[i], p.Y[i]
} else {
i := rnd.Intn(p.Order)
p.X[i] = clamp(p.X[i]+rnd.NormFloat64()*16, -m, float64(w-1+m))
p.Y[i] = clamp(p.Y[i]+rnd.NormFloat64()*16, -m, float64(h-1+m))
}
if p.Valid() {
break
}
}
}
func (p *Polygon) Valid() bool {
if !p.Convex {
return true
}
var sign bool
for a := 0; a < p.Order; a++ {
i := (a + 0) % p.Order
j := (a + 1) % p.Order
k := (a + 2) % p.Order
c := cross3(p.X[i], p.Y[i], p.X[j], p.Y[j], p.X[k], p.Y[k])
if a == 0 {
sign = c > 0
} else if c > 0 != sign {
return false
}
}
return true
}
func cross3(x1, y1, x2, y2, x3, y3 float64) float64 {
dx1 := x2 - x1
dy1 := y2 - y1
dx2 := x3 - x2
dy2 := y3 - y2
return dx1*dy2 - dy1*dx2
}
func (p *Polygon) Rasterize() []Scanline {
var path raster.Path
for i := 0; i <= p.Order; i++ {
f := fixp(p.X[i%p.Order], p.Y[i%p.Order])
if i == 0 {
path.Start(f)
} else {
path.Add1(f)
}
}
return fillPath(p.Worker, path)
}
================================================
FILE: primitive/quadratic.go
================================================
package primitive
import (
"fmt"
"strings"
"github.com/fogleman/gg"
"github.com/golang/freetype/raster"
)
type Quadratic struct {
Worker *Worker
X1, Y1 float64
X2, Y2 float64
X3, Y3 float64
Width float64
}
func NewRandomQuadratic(worker *Worker) *Quadratic {
rnd := worker.Rnd
x1 := rnd.Float64() * float64(worker.W)
y1 := rnd.Float64() * float64(worker.H)
x2 := x1 + rnd.Float64()*40 - 20
y2 := y1 + rnd.Float64()*40 - 20
x3 := x2 + rnd.Float64()*40 - 20
y3 := y2 + rnd.Float64()*40 - 20
width := 1.0 / 2
q := &Quadratic{worker, x1, y1, x2, y2, x3, y3, width}
q.Mutate()
return q
}
func (q *Quadratic) Draw(dc *gg.Context, scale float64) {
dc.MoveTo(q.X1, q.Y1)
dc.QuadraticTo(q.X2, q.Y2, q.X3, q.Y3)
dc.SetLineWidth(q.Width * scale)
dc.Stroke()
}
func (q *Quadratic) SVG(attrs string) string {
// TODO: this is a little silly
attrs = strings.Replace(attrs, "fill", "stroke", -1)
return fmt.Sprintf(
" ",
attrs, q.X1, q.Y1, q.X2, q.Y2, q.X3, q.Y3, q.Width)
}
func (q *Quadratic) Copy() Shape {
a := *q
return &a
}
func (q *Quadratic) Mutate() {
const m = 16
w := q.Worker.W
h := q.Worker.H
rnd := q.Worker.Rnd
for {
switch rnd.Intn(3) {
case 0:
q.X1 = clamp(q.X1+rnd.NormFloat64()*16, -m, float64(w-1+m))
q.Y1 = clamp(q.Y1+rnd.NormFloat64()*16, -m, float64(h-1+m))
case 1:
q.X2 = clamp(q.X2+rnd.NormFloat64()*16, -m, float64(w-1+m))
q.Y2 = clamp(q.Y2+rnd.NormFloat64()*16, -m, float64(h-1+m))
case 2:
q.X3 = clamp(q.X3+rnd.NormFloat64()*16, -m, float64(w-1+m))
q.Y3 = clamp(q.Y3+rnd.NormFloat64()*16, -m, float64(h-1+m))
case 3:
q.Width = clamp(q.Width+rnd.NormFloat64(), 1, 16)
}
if q.Valid() {
break
}
}
}
func (q *Quadratic) Valid() bool {
dx12 := int(q.X1 - q.X2)
dy12 := int(q.Y1 - q.Y2)
dx23 := int(q.X2 - q.X3)
dy23 := int(q.Y2 - q.Y3)
dx13 := int(q.X1 - q.X3)
dy13 := int(q.Y1 - q.Y3)
d12 := dx12*dx12 + dy12*dy12
d23 := dx23*dx23 + dy23*dy23
d13 := dx13*dx13 + dy13*dy13
return d13 > d12 && d13 > d23
}
func (q *Quadratic) Rasterize() []Scanline {
var path raster.Path
p1 := fixp(q.X1, q.Y1)
p2 := fixp(q.X2, q.Y2)
p3 := fixp(q.X3, q.Y3)
path.Start(p1)
path.Add2(p2, p3)
width := fix(q.Width)
return strokePath(q.Worker, path, width, raster.RoundCapper, raster.RoundJoiner)
}
================================================
FILE: primitive/raster.go
================================================
package primitive
import (
"github.com/golang/freetype/raster"
"golang.org/x/image/math/fixed"
)
func fix(x float64) fixed.Int26_6 {
return fixed.Int26_6(x * 64)
}
func fixp(x, y float64) fixed.Point26_6 {
return fixed.Point26_6{fix(x), fix(y)}
}
type painter struct {
Lines []Scanline
}
func (p *painter) Paint(spans []raster.Span, done bool) {
for _, span := range spans {
p.Lines = append(p.Lines, Scanline{span.Y, span.X0, span.X1 - 1, span.Alpha})
}
}
func fillPath(worker *Worker, path raster.Path) []Scanline {
r := worker.Rasterizer
r.Clear()
r.UseNonZeroWinding = true
r.AddPath(path)
var p painter
p.Lines = worker.Lines[:0]
r.Rasterize(&p)
return p.Lines
}
func strokePath(worker *Worker, path raster.Path, width fixed.Int26_6, cr raster.Capper, jr raster.Joiner) []Scanline {
r := worker.Rasterizer
r.Clear()
r.UseNonZeroWinding = true
r.AddStroke(path, width, cr, jr)
var p painter
p.Lines = worker.Lines[:0]
r.Rasterize(&p)
return p.Lines
}
================================================
FILE: primitive/rectangle.go
================================================
package primitive
import (
"fmt"
"math"
"github.com/fogleman/gg"
)
type Rectangle struct {
Worker *Worker
X1, Y1 int
X2, Y2 int
}
func NewRandomRectangle(worker *Worker) *Rectangle {
rnd := worker.Rnd
x1 := rnd.Intn(worker.W)
y1 := rnd.Intn(worker.H)
x2 := clampInt(x1+rnd.Intn(32)+1, 0, worker.W-1)
y2 := clampInt(y1+rnd.Intn(32)+1, 0, worker.H-1)
return &Rectangle{worker, x1, y1, x2, y2}
}
func (r *Rectangle) bounds() (x1, y1, x2, y2 int) {
x1, y1 = r.X1, r.Y1
x2, y2 = r.X2, r.Y2
if x1 > x2 {
x1, x2 = x2, x1
}
if y1 > y2 {
y1, y2 = y2, y1
}
return
}
func (r *Rectangle) Draw(dc *gg.Context, scale float64) {
x1, y1, x2, y2 := r.bounds()
dc.DrawRectangle(float64(x1), float64(y1), float64(x2-x1+1), float64(y2-y1+1))
dc.Fill()
}
func (r *Rectangle) SVG(attrs string) string {
x1, y1, x2, y2 := r.bounds()
w := x2 - x1 + 1
h := y2 - y1 + 1
return fmt.Sprintf(
" ",
attrs, x1, y1, w, h)
}
func (r *Rectangle) Copy() Shape {
a := *r
return &a
}
func (r *Rectangle) Mutate() {
w := r.Worker.W
h := r.Worker.H
rnd := r.Worker.Rnd
switch rnd.Intn(2) {
case 0:
r.X1 = clampInt(r.X1+int(rnd.NormFloat64()*16), 0, w-1)
r.Y1 = clampInt(r.Y1+int(rnd.NormFloat64()*16), 0, h-1)
case 1:
r.X2 = clampInt(r.X2+int(rnd.NormFloat64()*16), 0, w-1)
r.Y2 = clampInt(r.Y2+int(rnd.NormFloat64()*16), 0, h-1)
}
}
func (r *Rectangle) Rasterize() []Scanline {
x1, y1, x2, y2 := r.bounds()
lines := r.Worker.Lines[:0]
for y := y1; y <= y2; y++ {
lines = append(lines, Scanline{y, x1, x2, 0xffff})
}
return lines
}
type RotatedRectangle struct {
Worker *Worker
X, Y int
Sx, Sy int
Angle int
}
func NewRandomRotatedRectangle(worker *Worker) *RotatedRectangle {
rnd := worker.Rnd
x := rnd.Intn(worker.W)
y := rnd.Intn(worker.H)
sx := rnd.Intn(32) + 1
sy := rnd.Intn(32) + 1
a := rnd.Intn(360)
r := &RotatedRectangle{worker, x, y, sx, sy, a}
r.Mutate()
return r
}
func (r *RotatedRectangle) Draw(dc *gg.Context, scale float64) {
sx, sy := float64(r.Sx), float64(r.Sy)
dc.Push()
dc.Translate(float64(r.X), float64(r.Y))
dc.Rotate(radians(float64(r.Angle)))
dc.DrawRectangle(-sx/2, -sy/2, sx, sy)
dc.Pop()
dc.Fill()
}
func (r *RotatedRectangle) SVG(attrs string) string {
return fmt.Sprintf(
" ",
r.X, r.Y, r.Angle, r.Sx, r.Sy, attrs)
}
func (r *RotatedRectangle) Copy() Shape {
a := *r
return &a
}
func (r *RotatedRectangle) Mutate() {
w := r.Worker.W
h := r.Worker.H
rnd := r.Worker.Rnd
switch rnd.Intn(3) {
case 0:
r.X = clampInt(r.X+int(rnd.NormFloat64()*16), 0, w-1)
r.Y = clampInt(r.Y+int(rnd.NormFloat64()*16), 0, h-1)
case 1:
r.Sx = clampInt(r.Sx+int(rnd.NormFloat64()*16), 1, w-1)
r.Sy = clampInt(r.Sy+int(rnd.NormFloat64()*16), 1, h-1)
case 2:
r.Angle = r.Angle + int(rnd.NormFloat64()*32)
}
// for !r.Valid() {
// r.Sx = clampInt(r.Sx+int(rnd.NormFloat64()*16), 0, w-1)
// r.Sy = clampInt(r.Sy+int(rnd.NormFloat64()*16), 0, h-1)
// }
}
func (r *RotatedRectangle) Valid() bool {
a, b := r.Sx, r.Sy
if a < b {
a, b = b, a
}
aspect := float64(a) / float64(b)
return aspect <= 5
}
func (r *RotatedRectangle) Rasterize() []Scanline {
w := r.Worker.W
h := r.Worker.H
sx, sy := float64(r.Sx), float64(r.Sy)
angle := radians(float64(r.Angle))
rx1, ry1 := rotate(-sx/2, -sy/2, angle)
rx2, ry2 := rotate(sx/2, -sy/2, angle)
rx3, ry3 := rotate(sx/2, sy/2, angle)
rx4, ry4 := rotate(-sx/2, sy/2, angle)
x1, y1 := int(rx1)+r.X, int(ry1)+r.Y
x2, y2 := int(rx2)+r.X, int(ry2)+r.Y
x3, y3 := int(rx3)+r.X, int(ry3)+r.Y
x4, y4 := int(rx4)+r.X, int(ry4)+r.Y
miny := minInt(y1, minInt(y2, minInt(y3, y4)))
maxy := maxInt(y1, maxInt(y2, maxInt(y3, y4)))
n := maxy - miny + 1
min := make([]int, n)
max := make([]int, n)
for i := range min {
min[i] = w
}
xs := []int{x1, x2, x3, x4, x1}
ys := []int{y1, y2, y3, y4, y1}
// TODO: this could be better probably
for i := 0; i < 4; i++ {
x, y := float64(xs[i]), float64(ys[i])
dx, dy := float64(xs[i+1]-xs[i]), float64(ys[i+1]-ys[i])
count := int(math.Sqrt(dx*dx+dy*dy)) * 2
for j := 0; j < count; j++ {
t := float64(j) / float64(count-1)
xi := int(x + dx*t)
yi := int(y+dy*t) - miny
min[yi] = minInt(min[yi], xi)
max[yi] = maxInt(max[yi], xi)
}
}
lines := r.Worker.Lines[:0]
for i := 0; i < n; i++ {
y := miny + i
if y < 0 || y >= h {
continue
}
a := maxInt(min[i], 0)
b := minInt(max[i], w-1)
if b >= a {
lines = append(lines, Scanline{y, a, b, 0xffff})
}
}
return lines
}
================================================
FILE: primitive/scanline.go
================================================
package primitive
type Scanline struct {
Y, X1, X2 int
Alpha uint32
}
func cropScanlines(lines []Scanline, w, h int) []Scanline {
i := 0
for _, line := range lines {
if line.Y < 0 || line.Y >= h {
continue
}
if line.X1 >= w {
continue
}
if line.X2 < 0 {
continue
}
line.X1 = clampInt(line.X1, 0, w-1)
line.X2 = clampInt(line.X2, 0, w-1)
if line.X1 > line.X2 {
continue
}
lines[i] = line
i++
}
return lines[:i]
}
================================================
FILE: primitive/shape.go
================================================
package primitive
import "github.com/fogleman/gg"
type Shape interface {
Rasterize() []Scanline
Copy() Shape
Mutate()
Draw(dc *gg.Context, scale float64)
SVG(attrs string) string
}
type ShapeType int
const (
ShapeTypeAny ShapeType = iota
ShapeTypeTriangle
ShapeTypeRectangle
ShapeTypeEllipse
ShapeTypeCircle
ShapeTypeRotatedRectangle
ShapeTypeQuadratic
ShapeTypeRotatedEllipse
ShapeTypePolygon
)
================================================
FILE: primitive/state.go
================================================
package primitive
type State struct {
Worker *Worker
Shape Shape
Alpha int
MutateAlpha bool
Score float64
}
func NewState(worker *Worker, shape Shape, alpha int) *State {
var mutateAlpha bool
if alpha == 0 {
alpha = 128
mutateAlpha = true
}
return &State{worker, shape, alpha, mutateAlpha, -1}
}
func (state *State) Energy() float64 {
if state.Score < 0 {
state.Score = state.Worker.Energy(state.Shape, state.Alpha)
}
return state.Score
}
func (state *State) DoMove() interface{} {
rnd := state.Worker.Rnd
oldState := state.Copy()
state.Shape.Mutate()
if state.MutateAlpha {
state.Alpha = clampInt(state.Alpha+rnd.Intn(21)-10, 1, 255)
}
state.Score = -1
return oldState
}
func (state *State) UndoMove(undo interface{}) {
oldState := undo.(*State)
state.Shape = oldState.Shape
state.Alpha = oldState.Alpha
state.Score = oldState.Score
}
func (state *State) Copy() Annealable {
return &State{
state.Worker, state.Shape.Copy(), state.Alpha, state.MutateAlpha, state.Score}
}
================================================
FILE: primitive/triangle.go
================================================
package primitive
import (
"fmt"
"math"
"github.com/fogleman/gg"
)
type Triangle struct {
Worker *Worker
X1, Y1 int
X2, Y2 int
X3, Y3 int
}
func NewRandomTriangle(worker *Worker) *Triangle {
rnd := worker.Rnd
x1 := rnd.Intn(worker.W)
y1 := rnd.Intn(worker.H)
x2 := x1 + rnd.Intn(31) - 15
y2 := y1 + rnd.Intn(31) - 15
x3 := x1 + rnd.Intn(31) - 15
y3 := y1 + rnd.Intn(31) - 15
t := &Triangle{worker, x1, y1, x2, y2, x3, y3}
t.Mutate()
return t
}
func (t *Triangle) Draw(dc *gg.Context, scale float64) {
dc.LineTo(float64(t.X1), float64(t.Y1))
dc.LineTo(float64(t.X2), float64(t.Y2))
dc.LineTo(float64(t.X3), float64(t.Y3))
dc.ClosePath()
dc.Fill()
}
func (t *Triangle) SVG(attrs string) string {
return fmt.Sprintf(
" ",
attrs, t.X1, t.Y1, t.X2, t.Y2, t.X3, t.Y3)
}
func (t *Triangle) Copy() Shape {
a := *t
return &a
}
func (t *Triangle) Mutate() {
w := t.Worker.W
h := t.Worker.H
rnd := t.Worker.Rnd
const m = 16
for {
switch rnd.Intn(3) {
case 0:
t.X1 = clampInt(t.X1+int(rnd.NormFloat64()*16), -m, w-1+m)
t.Y1 = clampInt(t.Y1+int(rnd.NormFloat64()*16), -m, h-1+m)
case 1:
t.X2 = clampInt(t.X2+int(rnd.NormFloat64()*16), -m, w-1+m)
t.Y2 = clampInt(t.Y2+int(rnd.NormFloat64()*16), -m, h-1+m)
case 2:
t.X3 = clampInt(t.X3+int(rnd.NormFloat64()*16), -m, w-1+m)
t.Y3 = clampInt(t.Y3+int(rnd.NormFloat64()*16), -m, h-1+m)
}
if t.Valid() {
break
}
}
}
func (t *Triangle) Valid() bool {
const minDegrees = 15
var a1, a2, a3 float64
{
x1 := float64(t.X2 - t.X1)
y1 := float64(t.Y2 - t.Y1)
x2 := float64(t.X3 - t.X1)
y2 := float64(t.Y3 - t.Y1)
d1 := math.Sqrt(x1*x1 + y1*y1)
d2 := math.Sqrt(x2*x2 + y2*y2)
x1 /= d1
y1 /= d1
x2 /= d2
y2 /= d2
a1 = degrees(math.Acos(x1*x2 + y1*y2))
}
{
x1 := float64(t.X1 - t.X2)
y1 := float64(t.Y1 - t.Y2)
x2 := float64(t.X3 - t.X2)
y2 := float64(t.Y3 - t.Y2)
d1 := math.Sqrt(x1*x1 + y1*y1)
d2 := math.Sqrt(x2*x2 + y2*y2)
x1 /= d1
y1 /= d1
x2 /= d2
y2 /= d2
a2 = degrees(math.Acos(x1*x2 + y1*y2))
}
a3 = 180 - a1 - a2
return a1 > minDegrees && a2 > minDegrees && a3 > minDegrees
}
func (t *Triangle) Rasterize() []Scanline {
buf := t.Worker.Lines[:0]
lines := rasterizeTriangle(t.X1, t.Y1, t.X2, t.Y2, t.X3, t.Y3, buf)
return cropScanlines(lines, t.Worker.W, t.Worker.H)
}
func rasterizeTriangle(x1, y1, x2, y2, x3, y3 int, buf []Scanline) []Scanline {
if y1 > y3 {
x1, x3 = x3, x1
y1, y3 = y3, y1
}
if y1 > y2 {
x1, x2 = x2, x1
y1, y2 = y2, y1
}
if y2 > y3 {
x2, x3 = x3, x2
y2, y3 = y3, y2
}
if y2 == y3 {
return rasterizeTriangleBottom(x1, y1, x2, y2, x3, y3, buf)
} else if y1 == y2 {
return rasterizeTriangleTop(x1, y1, x2, y2, x3, y3, buf)
} else {
x4 := x1 + int((float64(y2-y1)/float64(y3-y1))*float64(x3-x1))
y4 := y2
buf = rasterizeTriangleBottom(x1, y1, x2, y2, x4, y4, buf)
buf = rasterizeTriangleTop(x2, y2, x4, y4, x3, y3, buf)
return buf
}
}
func rasterizeTriangleBottom(x1, y1, x2, y2, x3, y3 int, buf []Scanline) []Scanline {
s1 := float64(x2-x1) / float64(y2-y1)
s2 := float64(x3-x1) / float64(y3-y1)
ax := float64(x1)
bx := float64(x1)
for y := y1; y <= y2; y++ {
a := int(ax)
b := int(bx)
ax += s1
bx += s2
if a > b {
a, b = b, a
}
buf = append(buf, Scanline{y, a, b, 0xffff})
}
return buf
}
func rasterizeTriangleTop(x1, y1, x2, y2, x3, y3 int, buf []Scanline) []Scanline {
s1 := float64(x3-x1) / float64(y3-y1)
s2 := float64(x3-x2) / float64(y3-y2)
ax := float64(x3)
bx := float64(x3)
for y := y3; y > y1; y-- {
ax -= s1
bx -= s2
a := int(ax)
b := int(bx)
if a > b {
a, b = b, a
}
buf = append(buf, Scanline{y, a, b, 0xffff})
}
return buf
}
================================================
FILE: primitive/util.go
================================================
package primitive
import (
"fmt"
"image"
"image/color"
"image/color/palette"
"image/draw"
"image/gif"
"image/jpeg"
"image/png"
"io/ioutil"
"math"
"os"
"os/exec"
"path/filepath"
)
func LoadImage(path string) (image.Image, error) {
if path == "-" {
im, _, err := image.Decode(os.Stdin)
return im, err
} else {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
im, _, err := image.Decode(file)
return im, err
}
}
func SaveFile(path, contents string) error {
if path == "-" {
_, err := fmt.Fprint(os.Stdout, contents)
return err
} else {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(contents)
return err
}
}
func SavePNG(path string, im image.Image) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return png.Encode(file, im)
}
func SaveJPG(path string, im image.Image, quality int) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return jpeg.Encode(file, im, &jpeg.Options{quality})
}
func SaveGIF(path string, frames []image.Image, delay, lastDelay int) error {
g := gif.GIF{}
for i, src := range frames {
dst := image.NewPaletted(src.Bounds(), palette.Plan9)
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
g.Image = append(g.Image, dst)
if i == len(frames)-1 {
g.Delay = append(g.Delay, lastDelay)
} else {
g.Delay = append(g.Delay, delay)
}
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return gif.EncodeAll(file, &g)
}
func SaveGIFImageMagick(path string, frames []image.Image, delay, lastDelay int) error {
dir, err := ioutil.TempDir("", "")
if err != nil {
return err
}
for i, im := range frames {
path := filepath.Join(dir, fmt.Sprintf("%06d.png", i))
SavePNG(path, im)
}
args := []string{
"-loop", "0",
"-delay", fmt.Sprint(delay),
filepath.Join(dir, "*.png"),
"-delay", fmt.Sprint(lastDelay - delay),
filepath.Join(dir, fmt.Sprintf("%06d.png", len(frames)-1)),
path,
}
cmd := exec.Command("convert", args...)
if err := cmd.Run(); err != nil {
return err
}
return os.RemoveAll(dir)
}
func NumberString(x float64) string {
suffixes := []string{"", "k", "M", "G"}
for _, suffix := range suffixes {
if x < 1000 {
return fmt.Sprintf("%.1f%s", x, suffix)
}
x /= 1000
}
return fmt.Sprintf("%.1f%s", x, "T")
}
func radians(degrees float64) float64 {
return degrees * math.Pi / 180
}
func degrees(radians float64) float64 {
return radians * 180 / math.Pi
}
func clamp(x, lo, hi float64) float64 {
if x < lo {
return lo
}
if x > hi {
return hi
}
return x
}
func clampInt(x, lo, hi int) int {
if x < lo {
return lo
}
if x > hi {
return hi
}
return x
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func rotate(x, y, theta float64) (rx, ry float64) {
rx = x*math.Cos(theta) - y*math.Sin(theta)
ry = x*math.Sin(theta) + y*math.Cos(theta)
return
}
func imageToRGBA(src image.Image) *image.RGBA {
dst := image.NewRGBA(src.Bounds())
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
return dst
}
func copyRGBA(src *image.RGBA) *image.RGBA {
dst := image.NewRGBA(src.Bounds())
copy(dst.Pix, src.Pix)
return dst
}
func uniformRGBA(r image.Rectangle, c color.Color) *image.RGBA {
im := image.NewRGBA(r)
draw.Draw(im, im.Bounds(), &image.Uniform{c}, image.ZP, draw.Src)
return im
}
func AverageImageColor(im image.Image) color.NRGBA {
rgba := imageToRGBA(im)
size := rgba.Bounds().Size()
w, h := size.X, size.Y
var r, g, b int
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := rgba.RGBAAt(x, y)
r += int(c.R)
g += int(c.G)
b += int(c.B)
}
}
r /= w * h
g /= w * h
b /= w * h
return color.NRGBA{uint8(r), uint8(g), uint8(b), 255}
}
================================================
FILE: primitive/worker.go
================================================
package primitive
import (
"image"
"math/rand"
"time"
"github.com/golang/freetype/raster"
)
type Worker struct {
W, H int
Target *image.RGBA
Current *image.RGBA
Buffer *image.RGBA
Rasterizer *raster.Rasterizer
Lines []Scanline
Heatmap *Heatmap
Rnd *rand.Rand
Score float64
Counter int
}
func NewWorker(target *image.RGBA) *Worker {
w := target.Bounds().Size().X
h := target.Bounds().Size().Y
worker := Worker{}
worker.W = w
worker.H = h
worker.Target = target
worker.Buffer = image.NewRGBA(target.Bounds())
worker.Rasterizer = raster.NewRasterizer(w, h)
worker.Lines = make([]Scanline, 0, 4096) // TODO: based on height
worker.Heatmap = NewHeatmap(w, h)
worker.Rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
return &worker
}
func (worker *Worker) Init(current *image.RGBA, score float64) {
worker.Current = current
worker.Score = score
worker.Counter = 0
worker.Heatmap.Clear()
}
func (worker *Worker) Energy(shape Shape, alpha int) float64 {
worker.Counter++
lines := shape.Rasterize()
// worker.Heatmap.Add(lines)
color := computeColor(worker.Target, worker.Current, lines, alpha)
copyLines(worker.Buffer, worker.Current, lines)
drawLines(worker.Buffer, color, lines)
return differencePartial(worker.Target, worker.Current, worker.Buffer, worker.Score, lines)
}
func (worker *Worker) BestHillClimbState(t ShapeType, a, n, age, m int) *State {
var bestEnergy float64
var bestState *State
for i := 0; i < m; i++ {
state := worker.BestRandomState(t, a, n)
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
}
}
return bestState
}
func (worker *Worker) BestRandomState(t ShapeType, a, n int) *State {
var bestEnergy float64
var bestState *State
for i := 0; i < n; i++ {
state := worker.RandomState(t, a)
energy := state.Energy()
if i == 0 || energy < bestEnergy {
bestEnergy = energy
bestState = state
}
}
return bestState
}
func (worker *Worker) RandomState(t ShapeType, a int) *State {
switch t {
default:
return worker.RandomState(ShapeType(worker.Rnd.Intn(8)+1), a)
case ShapeTypeTriangle:
return NewState(worker, NewRandomTriangle(worker), a)
case ShapeTypeRectangle:
return NewState(worker, NewRandomRectangle(worker), a)
case ShapeTypeEllipse:
return NewState(worker, NewRandomEllipse(worker), a)
case ShapeTypeCircle:
return NewState(worker, NewRandomCircle(worker), a)
case ShapeTypeRotatedRectangle:
return NewState(worker, NewRandomRotatedRectangle(worker), a)
case ShapeTypeQuadratic:
return NewState(worker, NewRandomQuadratic(worker), a)
case ShapeTypeRotatedEllipse:
return NewState(worker, NewRandomRotatedEllipse(worker), a)
case ShapeTypePolygon:
return NewState(worker, NewRandomPolygon(worker, 4, false), a)
}
}
================================================
FILE: scripts/html.py
================================================
import os
import sys
def run(in_folder, out_folder):
seen = set()
for name in os.listdir(out_folder):
if not name.endswith('.png'):
continue
seen.add(name.split('.')[0])
for name in os.listdir(in_folder):
if not name.endswith('.jpg'):
continue
name = name[:-4]
if name not in seen:
continue
for m in [1, 3, 5]:
print '
'
path = '%s.jpg' % name
print ' ' % os.path.join(in_folder, path)
for n in [50, 100, 200]:
path = '%s.%d.128.4.%d.png' % (name, n, m)
print ' ' % os.path.join(out_folder, path)
print ' '
def main():
args = sys.argv[1:]
print HEADER
run(args[0], args[1])
print FOOTER
HEADER = '''
PrimitivePic
original
50 shapes
100 shapes
200 shapes
'''
FOOTER = '''
'''
if __name__ == '__main__':
main()
================================================
FILE: scripts/process.py
================================================
from Queue import Queue
import itertools
import os
import subprocess
import sys
import threading
def makedirs(x):
try:
os.makedirs(x)
except Exception:
pass
def primitive(i, o, n, a, m):
makedirs(os.path.split(o)[0])
args = (i, o, n, a, m)
cmd = 'primitive -r 128 -s 512 -i %s -o %s -n %d -a %d -m %d' % args
subprocess.call(cmd, shell=True)
def create_jobs(in_folder, out_folder, n, a, m):
result = []
for name in os.listdir(in_folder):
base, ext = os.path.splitext(name)
if ext.lower() not in ['.jpg', '.jpeg', '.png']:
continue
out_name = '%d.%%d.png' % (m)
in_path = os.path.join(in_folder, name)
out_path = os.path.join(out_folder, base, out_name)
if os.path.exists(out_path):
continue
key = (base, n, m)
args = (in_path, out_path, n, a, m)
result.append((key, args))
return result
def worker(jobs, done):
while True:
job = jobs.get()
log(job)
primitive(*job)
done.put(True)
def process(in_folder, out_folder, nlist, alist, mlist, nworkers):
jobs = Queue()
done = Queue()
for i in xrange(nworkers):
t = threading.Thread(target=worker, args=(jobs, done))
t.setDaemon(True)
t.start()
count = 0
items = []
for n, a, m in itertools.product(nlist, alist, mlist):
for item in create_jobs(in_folder, out_folder, n, a, m):
items.append(item)
items.sort()
for _, job in items:
jobs.put(job)
count += 1
for i in xrange(count):
done.get()
log_lock = threading.Lock()
def log(x):
with log_lock:
print x
if __name__ == '__main__':
args = sys.argv[1:]
nlist = [500]
alist = [128]
mlist = [0, 1, 3, 5]
nworkers = 4
process(args[0], args[1], nlist, alist, mlist, nworkers)