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!)
<img src="https://www.michaelfogleman.com/static/primitive/examples/monalisa.3.2000.gif" width="440"/> <img src="https://www.michaelfogleman.com/static/primitive/examples/monalisa-original.png" width="440"/>
### 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(
"<ellipse %s cx=\"%d\" cy=\"%d\" rx=\"%d\" ry=\"%d\" />",
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(
"<g transform=\"translate(%f %f) rotate(%f) scale(%f %f)\"><ellipse %s cx=\"0\" cy=\"0\" rx=\"1\" ry=\"1\" /></g>",
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("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"%d\" height=\"%d\">", model.Sw, model.Sh))
lines = append(lines, fmt.Sprintf("<rect x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" fill=\"#%02x%02x%02x\" />", model.Sw, model.Sh, bg.R, bg.G, bg.B))
lines = append(lines, fmt.Sprintf("<g transform=\"scale(%f) translate(0.5 0.5)\">", 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, "</g>")
lines = append(lines, "</svg>")
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(
"<polygon %s points=\"",
attrs)
points := make([]string, len(p.X))
for i := 0; i < len(p.X); i++ {
points[i] = fmt.Sprintf("%f,%f", p.X[i], p.Y[i])
}
return ret + strings.Join(points, ",") + "\" />"
}
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(
"<path %s fill=\"none\" d=\"M %f %f Q %f %f, %f %f\" stroke-width=\"%f\" />",
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(
"<rect %s x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" />",
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(
"<g transform=\"translate(%d %d) rotate(%d) scale(%d %d)\"><rect %s x=\"-0.5\" y=\"-0.5\" width=\"1\" height=\"1\" /></g>",
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(
"<polygon %s points=\"%d,%d %d,%d %d,%d\" />",
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 '<tr>'
path = '%s.jpg' % name
print '<td><img src="%s"></td>' % os.path.join(in_folder, path)
for n in [50, 100, 200]:
path = '%s.%d.128.4.%d.png' % (name, n, m)
print '<td><img src="%s"></td>' % os.path.join(out_folder, path)
print '</tr>'
def main():
args = sys.argv[1:]
print HEADER
run(args[0], args[1])
print FOOTER
HEADER = '''
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PrimitivePic</title>
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
table {
border-collapse: collapse;
margin: 4px;
}
img {
width: 400px;
display: block;
margin: 4px;
}
td {
padding: 0;
}
</style>
</head>
<body>
<table>
<tr>
<th>original</th>
<th>50 shapes</th>
<th>100 shapes</th>
<th>200 shapes</th>
</tr>
'''
FOOTER = '''
</table>
</body>
</html>
'''
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)
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
SYMBOL INDEX (182 symbols across 21 files)
FILE: bot/main.py
class AttrDict (line 43) | class AttrDict(dict):
method __init__ (line 44) | def __init__(self, *args, **kwargs):
class Config (line 48) | class Config(AttrDict):
method randomize (line 49) | def randomize(self):
method parse (line 56) | def parse(self, text):
method validate (line 67) | def validate(self):
method description (line 76) | def description(self):
function clamp (line 80) | def clamp(x, lo, hi):
function random_date (line 87) | def random_date(max_days_ago=1000):
function interesting (line 93) | def interesting(date=None):
function photo_url (line 106) | def photo_url(p, size=None):
function download_photo (line 115) | def download_photo(url, path):
function primitive (line 120) | def primitive(**kwargs):
function twitter_api (line 131) | def twitter_api():
function tweet (line 138) | def tweet(status, media, in_reply_to_status_id=None):
function handle_mentions (line 142) | def handle_mentions():
function handle_mention (line 153) | def handle_mention(status):
function flickr_url (line 198) | def flickr_url(photo_id):
function base_encode (line 202) | def base_encode(alphabet, number, suffix=''):
function generate (line 210) | def generate():
function main (line 235) | def main():
function download_photos (line 254) | def download_photos(folder, date=None):
FILE: main.go
type flagArray (line 34) | type flagArray
method String (line 36) | func (i *flagArray) String() string {
method Set (line 40) | func (i *flagArray) Set(value string) error {
type shapeConfig (line 45) | type shapeConfig struct
type shapeConfigArray (line 52) | type shapeConfigArray
method String (line 54) | func (i *shapeConfigArray) String() string {
method Set (line 58) | func (i *shapeConfigArray) Set(value string) error {
function init (line 64) | func init() {
function errorMessage (line 80) | func errorMessage(message string) bool {
function check (line 85) | func check(err error) {
function main (line 91) | func main() {
FILE: primitive/color.go
type Color (line 9) | type Color struct
method NRGBA (line 42) | func (c *Color) NRGBA() color.NRGBA {
function MakeColor (line 13) | func MakeColor(c color.Color) Color {
function MakeHexColor (line 18) | func MakeHexColor(x string) Color {
FILE: primitive/core.go
function computeColor (line 8) | func computeColor(target, current *image.RGBA, lines []Scanline, alpha i...
function copyLines (line 36) | func copyLines(dst, src *image.RGBA, lines []Scanline) {
function drawLines (line 44) | func drawLines(im *image.RGBA, c Color, lines []Scanline) {
function differenceFull (line 65) | func differenceFull(a, b *image.RGBA) float64 {
function differencePartial (line 91) | func differencePartial(target, before, after *image.RGBA, score float64,...
FILE: primitive/ellipse.go
type Ellipse (line 11) | type Ellipse struct
method Draw (line 35) | func (c *Ellipse) Draw(dc *gg.Context, scale float64) {
method SVG (line 40) | func (c *Ellipse) SVG(attrs string) string {
method Copy (line 46) | func (c *Ellipse) Copy() Shape {
method Mutate (line 51) | func (c *Ellipse) Mutate() {
method Rasterize (line 72) | func (c *Ellipse) Rasterize() []Scanline {
function NewRandomEllipse (line 18) | func NewRandomEllipse(worker *Worker) *Ellipse {
function NewRandomCircle (line 27) | func NewRandomCircle(worker *Worker) *Ellipse {
type RotatedEllipse (line 102) | type RotatedEllipse struct
method Draw (line 119) | func (c *RotatedEllipse) Draw(dc *gg.Context, scale float64) {
method SVG (line 127) | func (c *RotatedEllipse) SVG(attrs string) string {
method Copy (line 133) | func (c *RotatedEllipse) Copy() Shape {
method Mutate (line 138) | func (c *RotatedEllipse) Mutate() {
method Rasterize (line 154) | func (c *RotatedEllipse) Rasterize() []Scanline {
function NewRandomRotatedEllipse (line 109) | func NewRandomRotatedEllipse(worker *Worker) *RotatedEllipse {
FILE: primitive/heatmap.go
type Heatmap (line 9) | type Heatmap struct
method Clear (line 19) | func (h *Heatmap) Clear() {
method Add (line 25) | func (h *Heatmap) Add(lines []Scanline) {
method AddHeatmap (line 35) | func (h *Heatmap) AddHeatmap(a *Heatmap) {
method Image (line 41) | func (h *Heatmap) Image(gamma float64) *image.Gray16 {
function NewHeatmap (line 14) | func NewHeatmap(w, h int) *Heatmap {
FILE: primitive/log.go
function Log (line 7) | func Log(level int, format string, a ...interface{}) {
function v (line 13) | func v(format string, a ...interface{}) {
function vv (line 17) | func vv(format string, a ...interface{}) {
function vvv (line 21) | func vvv(format string, a ...interface{}) {
FILE: primitive/model.go
type Model (line 11) | type Model struct
method newContext (line 57) | func (model *Model) newContext() *gg.Context {
method Frames (line 66) | func (model *Model) Frames(scoreDelta float64) []image.Image {
method SVG (line 86) | func (model *Model) SVG() string {
method Add (line 103) | func (model *Model) Add(shape Shape, alpha int) {
method Step (line 119) | func (model *Model) Step(shapeType ShapeType, alpha, repeat int) int {
method runWorkers (line 147) | func (model *Model) runWorkers(t ShapeType, a, n, age, m int) *State {
method runWorker (line 172) | func (model *Model) runWorker(worker *Worker, t ShapeType, a, n, age, ...
function NewModel (line 25) | func NewModel(target image.Image, background Color, size, numWorkers int...
FILE: primitive/optimize.go
type Annealable (line 8) | type Annealable interface
function HillClimb (line 15) | func HillClimb(state Annealable, maxAge int) Annealable {
function PreAnneal (line 36) | func PreAnneal(state Annealable, iterations int) float64 {
function Anneal (line 49) | func Anneal(state Annealable, maxTemp, minTemp float64, steps int) Annea...
FILE: primitive/polygon.go
type Polygon (line 11) | type Polygon struct
method Draw (line 33) | func (p *Polygon) Draw(dc *gg.Context, scale float64) {
method SVG (line 42) | func (p *Polygon) SVG(attrs string) string {
method Copy (line 54) | func (p *Polygon) Copy() Shape {
method Mutate (line 63) | func (p *Polygon) Mutate() {
method Valid (line 84) | func (p *Polygon) Valid() bool {
method Rasterize (line 111) | func (p *Polygon) Rasterize() []Scanline {
function NewRandomPolygon (line 18) | func NewRandomPolygon(worker *Worker, order int, convex bool) *Polygon {
function cross3 (line 103) | func cross3(x1, y1, x2, y2, x3, y3 float64) float64 {
FILE: primitive/quadratic.go
type Quadratic (line 11) | type Quadratic struct
method Draw (line 33) | func (q *Quadratic) Draw(dc *gg.Context, scale float64) {
method SVG (line 40) | func (q *Quadratic) SVG(attrs string) string {
method Copy (line 48) | func (q *Quadratic) Copy() Shape {
method Mutate (line 53) | func (q *Quadratic) Mutate() {
method Valid (line 78) | func (q *Quadratic) Valid() bool {
method Rasterize (line 91) | func (q *Quadratic) Rasterize() []Scanline {
function NewRandomQuadratic (line 19) | func NewRandomQuadratic(worker *Worker) *Quadratic {
FILE: primitive/raster.go
function fix (line 8) | func fix(x float64) fixed.Int26_6 {
function fixp (line 12) | func fixp(x, y float64) fixed.Point26_6 {
type painter (line 16) | type painter struct
method Paint (line 20) | func (p *painter) Paint(spans []raster.Span, done bool) {
function fillPath (line 26) | func fillPath(worker *Worker, path raster.Path) []Scanline {
function strokePath (line 37) | func strokePath(worker *Worker, path raster.Path, width fixed.Int26_6, c...
FILE: primitive/rectangle.go
type Rectangle (line 10) | type Rectangle struct
method bounds (line 25) | func (r *Rectangle) bounds() (x1, y1, x2, y2 int) {
method Draw (line 37) | func (r *Rectangle) Draw(dc *gg.Context, scale float64) {
method SVG (line 43) | func (r *Rectangle) SVG(attrs string) string {
method Copy (line 52) | func (r *Rectangle) Copy() Shape {
method Mutate (line 57) | func (r *Rectangle) Mutate() {
method Rasterize (line 71) | func (r *Rectangle) Rasterize() []Scanline {
function NewRandomRectangle (line 16) | func NewRandomRectangle(worker *Worker) *Rectangle {
type RotatedRectangle (line 80) | type RotatedRectangle struct
method Draw (line 99) | func (r *RotatedRectangle) Draw(dc *gg.Context, scale float64) {
method SVG (line 109) | func (r *RotatedRectangle) SVG(attrs string) string {
method Copy (line 115) | func (r *RotatedRectangle) Copy() Shape {
method Mutate (line 120) | func (r *RotatedRectangle) Mutate() {
method Valid (line 140) | func (r *RotatedRectangle) Valid() bool {
method Rasterize (line 149) | func (r *RotatedRectangle) Rasterize() []Scanline {
function NewRandomRotatedRectangle (line 87) | func NewRandomRotatedRectangle(worker *Worker) *RotatedRectangle {
FILE: primitive/scanline.go
type Scanline (line 3) | type Scanline struct
function cropScanlines (line 8) | func cropScanlines(lines []Scanline, w, h int) []Scanline {
FILE: primitive/shape.go
type Shape (line 5) | type Shape interface
type ShapeType (line 13) | type ShapeType
constant ShapeTypeAny (line 16) | ShapeTypeAny ShapeType = iota
constant ShapeTypeTriangle (line 17) | ShapeTypeTriangle
constant ShapeTypeRectangle (line 18) | ShapeTypeRectangle
constant ShapeTypeEllipse (line 19) | ShapeTypeEllipse
constant ShapeTypeCircle (line 20) | ShapeTypeCircle
constant ShapeTypeRotatedRectangle (line 21) | ShapeTypeRotatedRectangle
constant ShapeTypeQuadratic (line 22) | ShapeTypeQuadratic
constant ShapeTypeRotatedEllipse (line 23) | ShapeTypeRotatedEllipse
constant ShapeTypePolygon (line 24) | ShapeTypePolygon
FILE: primitive/state.go
type State (line 3) | type State struct
method Energy (line 20) | func (state *State) Energy() float64 {
method DoMove (line 27) | func (state *State) DoMove() interface{} {
method UndoMove (line 38) | func (state *State) UndoMove(undo interface{}) {
method Copy (line 45) | func (state *State) Copy() Annealable {
function NewState (line 11) | func NewState(worker *Worker, shape Shape, alpha int) *State {
FILE: primitive/triangle.go
type Triangle (line 10) | type Triangle struct
method Draw (line 30) | func (t *Triangle) Draw(dc *gg.Context, scale float64) {
method SVG (line 38) | func (t *Triangle) SVG(attrs string) string {
method Copy (line 44) | func (t *Triangle) Copy() Shape {
method Mutate (line 49) | func (t *Triangle) Mutate() {
method Valid (line 72) | func (t *Triangle) Valid() bool {
method Rasterize (line 105) | func (t *Triangle) Rasterize() []Scanline {
function NewRandomTriangle (line 17) | func NewRandomTriangle(worker *Worker) *Triangle {
function rasterizeTriangle (line 111) | func rasterizeTriangle(x1, y1, x2, y2, x3, y3 int, buf []Scanline) []Sca...
function rasterizeTriangleBottom (line 137) | func rasterizeTriangleBottom(x1, y1, x2, y2, x3, y3 int, buf []Scanline)...
function rasterizeTriangleTop (line 155) | func rasterizeTriangleTop(x1, y1, x2, y2, x3, y3 int, buf []Scanline) []...
FILE: primitive/util.go
function LoadImage (line 19) | func LoadImage(path string) (image.Image, error) {
function SaveFile (line 34) | func SaveFile(path, contents string) error {
function SavePNG (line 49) | func SavePNG(path string, im image.Image) error {
function SaveJPG (line 58) | func SaveJPG(path string, im image.Image, quality int) error {
function SaveGIF (line 67) | func SaveGIF(path string, frames []image.Image, delay, lastDelay int) er...
function SaveGIFImageMagick (line 87) | func SaveGIFImageMagick(path string, frames []image.Image, delay, lastDe...
function NumberString (line 111) | func NumberString(x float64) string {
function radians (line 122) | func radians(degrees float64) float64 {
function degrees (line 126) | func degrees(radians float64) float64 {
function clamp (line 130) | func clamp(x, lo, hi float64) float64 {
function clampInt (line 140) | func clampInt(x, lo, hi int) int {
function minInt (line 150) | func minInt(a, b int) int {
function maxInt (line 157) | func maxInt(a, b int) int {
function rotate (line 164) | func rotate(x, y, theta float64) (rx, ry float64) {
function imageToRGBA (line 170) | func imageToRGBA(src image.Image) *image.RGBA {
function copyRGBA (line 176) | func copyRGBA(src *image.RGBA) *image.RGBA {
function uniformRGBA (line 182) | func uniformRGBA(r image.Rectangle, c color.Color) *image.RGBA {
function AverageImageColor (line 188) | func AverageImageColor(im image.Image) color.NRGBA {
FILE: primitive/worker.go
type Worker (line 11) | type Worker struct
method Init (line 39) | func (worker *Worker) Init(current *image.RGBA, score float64) {
method Energy (line 46) | func (worker *Worker) Energy(shape Shape, alpha int) float64 {
method BestHillClimbState (line 56) | func (worker *Worker) BestHillClimbState(t ShapeType, a, n, age, m int...
method BestRandomState (line 73) | func (worker *Worker) BestRandomState(t ShapeType, a, n int) *State {
method RandomState (line 87) | func (worker *Worker) RandomState(t ShapeType, a int) *State {
function NewWorker (line 24) | func NewWorker(target *image.RGBA) *Worker {
FILE: scripts/html.py
function run (line 4) | def run(in_folder, out_folder):
function main (line 25) | def main():
FILE: scripts/process.py
function makedirs (line 8) | def makedirs(x):
function primitive (line 14) | def primitive(i, o, n, a, m):
function create_jobs (line 20) | def create_jobs(in_folder, out_folder, n, a, m):
function worker (line 36) | def worker(jobs, done):
function process (line 43) | def process(in_folder, out_folder, nlist, alist, mlist, nworkers):
function log (line 64) | def log(x):
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (72K chars).
[
{
"path": ".gitignore",
"chars": 22,
"preview": "/*.png\n/*.svg\n/*.gif\n\n"
},
{
"path": "LICENSE.md",
"chars": 1060,
"preview": "Copyright (C) 2016 Michael Fogleman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof thi"
},
{
"path": "README.md",
"chars": 9113,
"preview": "# Primitive Pictures\n\nReproducing images with geometric primitives.\n\n\n\ntype Color struct {\n\tR, G, B, A int\n}\n\nfunc MakeColor(c "
},
{
"path": "primitive/core.go",
"chars": 3217,
"preview": "package primitive\n\nimport (\n\t\"image\"\n\t\"math\"\n)\n\nfunc computeColor(target, current *image.RGBA, lines []Scanline, alpha i"
},
{
"path": "primitive/ellipse.go",
"chars": 4037,
"preview": "package primitive\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\n\t\"github.com/fogleman/gg\"\n\t\"github.com/golang/freetype/raster\"\n)\n\ntype Ellip"
},
{
"path": "primitive/heatmap.go",
"chars": 978,
"preview": "package primitive\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"math\"\n)\n\ntype Heatmap struct {\n\tW, H int\n\tCount []uint64\n}\n\nfunc "
},
{
"path": "primitive/log.go",
"chars": 378,
"preview": "package primitive\n\nimport \"fmt\"\n\nvar LogLevel int\n\nfunc Log(level int, format string, a ...interface{}) {\n\tif LogLevel >"
},
{
"path": "primitive/model.go",
"chars": 4673,
"preview": "package primitive\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"strings\"\n\n\t\"github.com/fogleman/gg\"\n)\n\ntype Model struct {\n\tSw, Sh int"
},
{
"path": "primitive/optimize.go",
"chars": 1769,
"preview": "package primitive\n\nimport (\n\t\"math\"\n\t\"math/rand\"\n)\n\ntype Annealable interface {\n\tEnergy() float64\n\tDoMove() interface{}\n"
},
{
"path": "primitive/polygon.go",
"chars": 2405,
"preview": "package primitive\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/fogleman/gg\"\n\t\"github.com/golang/freetype/raster\"\n)\n\ntype Po"
},
{
"path": "primitive/quadratic.go",
"chars": 2367,
"preview": "package primitive\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/fogleman/gg\"\n\t\"github.com/golang/freetype/raster\"\n)\n\ntype Qu"
},
{
"path": "primitive/raster.go",
"chars": 987,
"preview": "package primitive\n\nimport (\n\t\"github.com/golang/freetype/raster\"\n\t\"golang.org/x/image/math/fixed\"\n)\n\nfunc fix(x float64)"
},
{
"path": "primitive/rectangle.go",
"chars": 4667,
"preview": "package primitive\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\n\t\"github.com/fogleman/gg\"\n)\n\ntype Rectangle struct {\n\tWorker *Worker\n\tX1, Y1"
},
{
"path": "primitive/scanline.go",
"chars": 460,
"preview": "package primitive\n\ntype Scanline struct {\n\tY, X1, X2 int\n\tAlpha uint32\n}\n\nfunc cropScanlines(lines []Scanline, w, h "
},
{
"path": "primitive/shape.go",
"chars": 414,
"preview": "package primitive\n\nimport \"github.com/fogleman/gg\"\n\ntype Shape interface {\n\tRasterize() []Scanline\n\tCopy() Shape\n\tMutate"
},
{
"path": "primitive/state.go",
"chars": 1035,
"preview": "package primitive\n\ntype State struct {\n\tWorker *Worker\n\tShape Shape\n\tAlpha int\n\tMutateAlpha bool\n\tScore"
},
{
"path": "primitive/triangle.go",
"chars": 3752,
"preview": "package primitive\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\n\t\"github.com/fogleman/gg\"\n)\n\ntype Triangle struct {\n\tWorker *Worker\n\tX1, Y1 "
},
{
"path": "primitive/util.go",
"chars": 3925,
"preview": "package primitive\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/color/palette\"\n\t\"image/draw\"\n\t\"image/gif\"\n\t\"image/jpe"
},
{
"path": "primitive/worker.go",
"chars": 2980,
"preview": "package primitive\n\nimport (\n\t\"image\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/golang/freetype/raster\"\n)\n\ntype Worker struct {\n"
},
{
"path": "scripts/html.py",
"chars": 1377,
"preview": "import os\nimport sys\n\ndef run(in_folder, out_folder):\n seen = set()\n for name in os.listdir(out_folder):\n i"
},
{
"path": "scripts/process.py",
"chars": 1894,
"preview": "from Queue import Queue\nimport itertools\nimport os\nimport subprocess\nimport sys\nimport threading\n\ndef makedirs(x):\n t"
}
]
About this extraction
This page contains the full source code of the fogleman/primitive GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (63.6 KB), approximately 21.6k tokens, and a symbol index with 182 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.