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. ![Example](https://www.michaelfogleman.com/static/primitive/examples/16550611738.200.128.4.5.png) ### 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. ![Pencils](https://www.michaelfogleman.com/static/primitive/examples/pencils.gif) ### Creative Constraints If you're willing to dabble in the code, you can enforce constraints on the shapes to produce even more interesting results. Here, the rectangles are constrained to point toward the sun in this picture of a pyramid sunset. ![Pyramids](https://www.michaelfogleman.com/static/primitive/examples/pyramids.png) ### Shape and Iteration Comparison Matrix The matrix below shows triangles, ellipses and rectangles at 50, 100 and 200 iterations each. ![Matrix](http://i.imgur.com/H5NYpL4.png) ### 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. ![Example](https://www.michaelfogleman.com/static/primitive/examples/29167683201.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/26574286221.200.128.4.1.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/15011768709.200.128.4.1.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/27540729075.200.128.4.1.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/28896874003.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/20414282102.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/15199237095.200.128.4.1.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/11707819764.200.128.4.1.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/18270231645.200.128.4.3.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/15705764893.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/25213252889.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/15015411870.200.128.4.3.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/25766500104.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/27471731151.50.128.4.1.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/11720700033.200.128.4.3.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/18782606664.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/21374478713.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/15196426112.200.128.4.5.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/24696847962.png) ![Example](https://www.michaelfogleman.com/static/primitive/examples/18276676312.100.128.4.1.png) ================================================ 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 ''' FOOTER = '''
original 50 shapes 100 shapes 200 shapes
''' 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)