-
Notifications
You must be signed in to change notification settings - Fork 0
/
ga-image.lisp
261 lines (217 loc) · 9.53 KB
/
ga-image.lisp
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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
;;;; ga-image.lisp
(in-package #:ga-image)
(require :cl-cairo2-xlib)
;;;
;;; Global parameters
;;;
(defparameter +side-bits+ 3
"The number of bits used to represent how many sides in a polygon. This can
range from three sides to 10 sides.")
(defparameter +min-sides+ 3
"The minimum number of vertices for a polygon.")
(defparameter +max-sides+ (+ +min-sides+ (1- (expt 2 +side-bits+)))
"The maximum number of vertices for a polygon.")
(defparameter +color-bits+ 32
"The number of bits to represent RGBA colors.")
(defparameter +position-bits+ 9
"The number of bits in a coordinate component.")
(defparameter +sides-start+ 0
"The bit position for the start of the bits that specify the number
of sides.")
(defparameter +color-start+ +side-bits+
"The bit position for the start of the bits that specify the polygon color.")
(defparameter +vertices-start+ (+ +color-start+ +color-bits+)
"The bit position for the start of the vertex bits.")
(defparameter +vertices-end+ (+ +vertices-start+
(* 2 +max-sides+ +position-bits+))
"The bit position for the end of the vertices and the end of a
single polygon.")
(defparameter +transparent+ (make-instance 'cl-colors:rgba :alpha 0)
"The color for a completely invisible pixel.")
;;;
;;; Problem definition for the GA engine
;;;
(defclass image-problem ()
((target-image :initarg :target-image :reader target-image :initform nil)
(polygons :reader polygons :initarg :polygons :initform nil)
(similarity :reader similarity :initarg :similarity :initform 0.90)
(width :accessor width :initarg :width :initform nil)
(height :accessor height :initarg :height :initform nil))
(:documentation "The configuration for the evolving image problem."))
(defmethod initialize-instance :after ((problem image-problem) &rest rest)
"Set the instance's WIDTH and HEIGHT slots from the TARGET-IMAGE."
(declare (ignore rest))
(setf (width problem) (cl-cairo2:image-surface-get-width
(target-image problem)))
(setf (height problem) (cl-cairo2:image-surface-get-height
(target-image problem))))
(defun make-image-problem (image-file polygons)
"Create an instance of IMAGE-PROBLEM with the target image read from
IMAGE-FILE using the number of polygons specified by POLYGONS for the
reconstruction."
(let ((surface (cl-cairo2:image-surface-create-from-png image-file)))
(make-instance 'image-problem :target-image surface :polygons polygons)))
(defmethod genome-length ((problem image-problem))
"Determine the number of bits require dfor a genome to solve the
specified image reconstruction problem."
(* (polygons problem)
(+ +side-bits+ (* 2 +position-bits+ +max-sides+) +color-bits+)))
(defmethod fitness ((problem image-problem) genome)
"The fitness function for the image reconstruction problem is the
sum of the absolute differences between all channels of all pixels in
the phenotype from GENOME and the TARGET-IMAGE."
(let* ((target-ptr (image-surface-get-data (target-image problem)
:pointer-only t))
(genome-image (render-genome-to-surface problem genome))
(genome-ptr (image-surface-get-data genome-image :pointer-only t))
(bytes (* (width problem) (height problem) 3))
(fitness 0))
(do ((index 0 (1+ index)))
((= index bytes) nil)
(incf fitness (abs (- (cffi:mem-aref target-ptr :uchar index)
(cffi:mem-aref genome-ptr :uchar index)))))
(destroy genome-image)
fitness))
(defmethod fitness-comparator ((problem image-problem))
"Return a fitness comparator function that takes two genomes and
returns T if the first is more fit according to the characteristics of
the PROBLEM."
(lesser-comparator problem))
(defun run-evolution (filename population-size polygons mutation-rate
iterations)
"Run the GA engine on the image evolution problem. Use a population
size POPULATION SIZE with each genome encoding POLYGONS number of
polygons. Use a mutation rate of MUTATION-RATE. Only run ITERATIONS
times."
(let* ((target-image (load-png filename))
(problem (make-instance 'image-problem :polygons polygons
:target-image target-image)))
(let* ((gene-pool
(solve problem population-size mutation-rate
(generation-terminator iterations)))
(best-genome (most-fit-genome gene-pool
(fitness-comparator problem))))
(format t "~%Best = ~F~%Average = ~F~%"
(fitness problem best-genome)
(average-fitness problem gene-pool))
best-genome)))
(defun solve-image (problem population-size mutation-rate)
"Run the GA engine against the PROBLEM."
(let* ((gene-pool
(solve problem population-size mutation-rate
(fitness-terminator problem (genome-length problem))))
(best-genome (most-fit-genome gene-pool (fitness-comparator problem))))
(format t "~%Best = ~F~%Average = ~F~%" (fitness problem best-genome)
(average-fitness problem gene-pool))
best-genome))
(defun evolve-image (filename population-size polygons mutation-rate)
"Evolve towards the PNG image in FILENAME. Use POPULATION-SIZE as
the number of individuals in a gene pool. POLYGONS should be the
number of polygons for a single genome. Use MUTATION-RATE for
generating mutations."
(let* ((target-image (load-png filename))
(problem (make-instance 'image-problem :polygons polygons
:target-image target-image))
(best-genome (solve-image problem population-size mutation-rate)))
(render-genome-to-file problem best-genome)
best-genome))
;;; Helper functions for the problem
(defstruct point
"Represents a point in 2-D Cartesian plane."
(x 0)
(y 0))
(defstruct polygon
"Represents a polygon with the specified number of sides, color and
list of vertices."
(sides 3)
(color +transparent+)
(vertices '()))
(defun decode-sides (bits)
"From BITS, decode the number of sides in this polygon."
(+ +min-sides+ (bit-vector->integer bits)))
(defun bits->color-comp (bits)
"Convert BITS from an integer RGBA component in the range 0 to 255
to a float RGBA component in the range zero to one."
(coerce (/ (bit-vector->integer bits) 255) 'float))
(defun decode-color (rgba-bits)
"From BITS, decode a 32-bit RGBA color."
(make-instance 'rgba
:red (bits->color-comp (subseq rgba-bits 0 8))
:green (bits->color-comp (subseq rgba-bits 8 16))
:blue (bits->color-comp (subseq rgba-bits 16 24))
:alpha (bits->color-comp (subseq rgba-bits 24))))
(defun bits->float (bits max-val)
"Convert BITS into a double between 0 and max-val."
(* (coerce max-val 'float)
(/ (bit-vector->integer bits) (1- (expt 2 (length bits))))))
(defun decode-point (bits)
"From BITS, decode a point."
(make-point :x (bit-vector->integer (subseq bits 0 +position-bits+))
:y (bit-vector->integer (subseq bits +position-bits+))))
; TODO decode-vertices feels decidedly non-Lispy.
(defun decode-vertices (sides bits)
"From BITS, decode the polygon with SIDES number of sides."
(do ((vertices '())
(offset1 0 (+ offset1 (* 2 +position-bits+)))
(offset2 +position-bits+ (+ offset2 (* 2 +position-bits+)))
(n 0 (1+ n)))
((= n sides) vertices)
(let ((x (bit-vector->integer (subseq bits offset1 offset2)))
(y (bit-vector->integer (subseq bits offset2
(+ offset2 +position-bits+)))))
(push (make-point :x x :y y) vertices))))
(defun decode-genome (genome &optional (phenotype '()))
"Decode GENOME into a list of colored polygons with between three
and ten sides."
(if (zerop (length genome))
phenotype
(let* ((sides (decode-sides (subseq genome 0 +color-start+)))
(color (decode-color
(subseq genome +color-start+ +vertices-start+)))
(vertices (decode-vertices sides
(subseq genome +vertices-start+ +vertices-end+))))
(push (make-polygon :sides sides :color color :vertices vertices)
phenotype)
(decode-genome (subseq genome +vertices-end+) phenotype))))
;;;
;;; Rendering functions
;;;
(defun load-png (filename)
"Load FILENAME into an image surface. The file to be loaded must be
a PNG file."
(cl-cairo2:image-surface-create-from-png filename))
(defun draw-polygon (poly &optional (context cl-cairo2:*context*))
(let ((v (rest (polygon-vertices poly)))
(first-pt (first (polygon-vertices poly))))
(cl-cairo2:move-to (point-x first-pt) (point-y first-pt) context)
(loop for pt in v
do (cl-cairo2:line-to (point-x pt) (point-y pt) context)))
(cl-cairo2:close-path context)
(cl-cairo2:set-source-color (polygon-color poly) context)
(cl-cairo2:fill-path context))
(defun render-genome-to-surface (problem genome)
"Renders the polygons represented by GENOME onto a Cairo image
surface. Returns the new surface."
(let* ((surface (cl-cairo2:create-image-surface :rgb24 (width problem)
(height problem)))
(context (cl-cairo2:create-context surface)))
(dolist (poly (decode-genome genome))
(draw-polygon poly context))
(cl-cairo2:destroy context)
surface))
(defun render-genome-to-file (problem genome
&optional (filename "ga-image.png"))
"Renders the polygons represented by GENOME into a PNG file."
(cl-cairo2:surface-write-to-png
(render-genome-to-surface problem genome) filename))
(defun render-genome-to-window (problem genome)
"Renders the polygons represented by GENOME into a window using
PROBLEM to provide image size information. Returns the context so it
can be destroyed later."
(let ((surface (render-genome-to-surface problem genome))
(context (cl-cairo2:create-xlib-image-context (width problem)
(height problem))))
(cl-cairo2:set-source-surface surface 0 0 context)
(cl-cairo2:paint context)
(cl-cairo2:destroy surface)
context))