-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathevolver.go
203 lines (161 loc) · 5 KB
/
evolver.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package polygen
import (
"bytes"
"encoding/gob"
"fmt"
"image"
"io/ioutil"
"log"
"os"
"sort"
"time"
)
// Evolver uses a genetic algorithm to evolve a set of polygons to approximate an image.
type Evolver struct {
refImgRGBA *image.RGBA
dstImgFile string
checkPointFile string
candidates []*Candidate
mostFit *Candidate
generation int
generationsSinceChange int
}
// Checkpoint is used for serializing the current best candidate and corresponding generation
// count to a checkpoint file.
type Checkpoint struct {
Generation int
GenerationsSinceChange int
MostFit *Candidate
}
func NewEvolver(refImg image.Image, dstImageFile string, checkPointFile string) (*Evolver, error) {
result := &Evolver{
dstImgFile: dstImageFile,
checkPointFile: checkPointFile,
candidates: make([]*Candidate, PopulationCount),
}
result.refImgRGBA = ConvertToRGBA(refImg)
// if there's an existing checkpoint file, restore from last checkpoint
if _, err := os.Stat(checkPointFile); !os.IsNotExist(err) {
err := result.restoreFromCheckpoint()
if err != nil {
return nil, err
}
}
return result, nil
}
// Run runs the Evolver until maxGen generations have been evaluated.
// At each generation, the candidate images are rendered & evaluated, and the preview images are
// updated to reflect the current state.
func (e *Evolver) Run(maxGen, polyCount int, previews []*SafeImage) {
w := e.refImgRGBA.Bounds().Dx()
h := e.refImgRGBA.Bounds().Dy()
// no candidate from prev call to RestoreFromCheckpoint()
if e.mostFit == nil {
e.mostFit = randomCandidate(w, h, polyCount)
e.candidates[0] = e.mostFit
}
// TODO: probably move the polyCount arg to NewEvolver(). It makes more sense to check there,
// and complain about the checkpoint file by name, which we do not have here.
if len(e.mostFit.Polygons) != polyCount {
log.Fatalf("checkpoint file polygon count mismatch: %d != %d", len(e.mostFit.Polygons), polyCount)
}
e.renderAndEvaluate(e.mostFit)
stats := NewStats()
// to synchronize workers
c := make(chan struct{})
for ; e.generation < maxGen; e.generation++ {
processCandidate := func(cand *Candidate) {
for i := 0; i < MutationsPerIteration; i++ {
cand.mutateInPlace()
}
e.renderAndEvaluate(cand)
c <- struct{}{}
}
// mostFit is already in slot 0, so start at 1
for i := 1; i < PopulationCount; i++ {
e.candidates[i] = e.mostFit.copyOf()
go processCandidate(e.candidates[i])
}
// wait for all processCandidate() calls to return
for i := 1; i < PopulationCount; i++ {
<-c
}
stats.Increment(PopulationCount - 1)
// after sort, the best will be at [0], worst will be at [len() - 1]
sort.Sort(ByFitness(e.candidates))
for i := 0; i < len(previews); i++ {
previews[i].Update(e.candidates[i].img)
}
currBest := e.candidates[0]
worst := e.candidates[len(e.candidates)-1]
if currBest.Fitness < e.mostFit.Fitness {
e.generationsSinceChange = 0
e.mostFit = currBest
} else {
e.generationsSinceChange++
}
if e.generation%10 == 0 {
stats.Print(currBest, worst, e.generation, e.generationsSinceChange)
}
if e.generation%250 == 0 {
cpSave := time.Now()
err := e.mostFit.drawAndSave(e.dstImgFile)
if err != nil {
log.Fatalf("error saving output image: %s", err)
}
err = e.saveCheckpoint()
if err != nil {
log.Fatalf("error saving checkpoint file: %s", err)
}
dur := time.Since(cpSave)
log.Printf("checkpoint took %s", dur)
}
}
e.mostFit.drawAndSave(e.dstImgFile)
log.Printf("after %d generations, fitness is: %d, saved to %s", maxGen, e.mostFit.Fitness, e.dstImgFile)
}
func (e *Evolver) restoreFromCheckpoint() error {
b, err := ioutil.ReadFile(e.checkPointFile)
if err != nil {
return fmt.Errorf("error reading checkpoint file: %s: %s", e.checkPointFile, err)
}
decoder := gob.NewDecoder(bytes.NewBuffer(b))
var cp Checkpoint
err = decoder.Decode(&cp)
if err != nil {
return fmt.Errorf("error decoding checkpoint file: %s %s", e.checkPointFile, err)
}
e.generation = cp.Generation
e.generationsSinceChange = cp.GenerationsSinceChange
e.candidates[0] = cp.MostFit
e.mostFit = cp.MostFit
e.renderAndEvaluate(e.mostFit)
return nil
}
func (e *Evolver) saveCheckpoint() error {
log.Printf("checkpointing to %s", e.checkPointFile)
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
cp := &Checkpoint{
Generation: e.generation,
GenerationsSinceChange: e.generationsSinceChange,
MostFit: e.mostFit,
}
err := encoder.Encode(cp)
if err != nil {
return fmt.Errorf("error encoding checkpoint: %s", err)
}
err = ioutil.WriteFile(e.checkPointFile, buf.Bytes(), 0644)
if err != nil {
return fmt.Errorf("error writing checkpoint to file: %s", err)
}
return nil
}
func (e *Evolver) renderAndEvaluate(c *Candidate) {
c.renderImage()
diff, err := FastCompare(e.refImgRGBA, c.img)
if err != nil {
log.Fatalf("error comparing images: %s", err)
}
c.Fitness = diff
}