1
+ """
2
+ This is an example that amounts to an offline picbreeder.org without any nice features. :)
3
+
4
+ Left-click on thumbnails to pick images to breed for next generation, right-click to
5
+ render a high-resolution version of an image. Genomes and images chosen for breeding
6
+ and rendering are saved to disk.
7
+
8
+ This example also demonstrates how to customize species stagnation.
9
+ """
10
+ import math
11
+ import os
12
+ import pickle
13
+ import pygame
14
+
15
+ from multiprocessing import Pool
16
+ import neat
17
+
18
+ from common import eval_mono_image , eval_gray_image , eval_color_image
19
+
20
+
21
+ class InteractiveStagnation (object ):
22
+ """
23
+ This class is used as a drop-in replacement for the default species stagnation scheme.
24
+
25
+ A species is only marked as stagnant if the user has not selected one of its output images
26
+ within the last `max_stagnation` generations.
27
+ """
28
+ def __init__ (self , config , reporters ):
29
+ self .max_stagnation = int (config .get ('max_stagnation' ))
30
+
31
+ self .reporters = reporters
32
+ self .stagnant_counts = {}
33
+
34
+ @classmethod
35
+ def parse_config (cls , param_dict ):
36
+ config = {'max_stagnation' : 15 }
37
+ config .update (param_dict )
38
+
39
+ return config
40
+
41
+ @classmethod
42
+ def write_config (cls , f , config ):
43
+ max_stagnation = config .get ('max_stagnation' , 15 )
44
+ f .write ('max_stagnation = {}\n ' .format (max_stagnation ))
45
+
46
+ def remove (self , species ):
47
+ if species .key in self .stagnant_counts :
48
+ del self .stagnant_counts [species .key ]
49
+
50
+ def update (self , species ):
51
+ result = []
52
+ for s in species .values ():
53
+ # If any member of the species is selected (i.e., has a fitness above zero), then we reset
54
+ # the stagnation count. Otherwise we increment the count.
55
+ scount = self .stagnant_counts .get (s .key , 0 ) + 1
56
+ for m in s .members .values ():
57
+ if m .fitness > 0 :
58
+ scount = 0
59
+ break
60
+
61
+ self .stagnant_counts [s .key ] = scount
62
+
63
+ is_stagnant = scount >= self .max_stagnation
64
+ result .append ((s .key , s , is_stagnant ))
65
+
66
+ if is_stagnant :
67
+ self .remove (s )
68
+
69
+ self .reporters .info ('Species no improv: {0!r}' .format (self .stagnant_counts ))
70
+
71
+ return result
72
+
73
+
74
+ class PictureBreeder (object ):
75
+ def __init__ (self , thumb_width , thumb_height , full_width , full_height ,
76
+ window_width , window_height , scheme , num_workers ):
77
+ """
78
+ :param thumb_width: Width of preview image
79
+ :param thumb_height: Height of preview image
80
+ :param full_width: Width of full rendered image
81
+ :param full_height: Height of full rendered image
82
+ :param window_width: Width of the view window
83
+ :param window_height: Height of the view window
84
+ :param scheme: Image type to generate: mono, gray, or color
85
+ """
86
+ self .thumb_width = thumb_width
87
+ self .thumb_height = thumb_height
88
+ self .full_width = full_width
89
+ self .full_height = full_height
90
+
91
+ self .window_width = window_width
92
+ self .window_height = window_height
93
+
94
+ assert scheme in ('mono' , 'gray' , 'color' )
95
+ self .scheme = scheme
96
+
97
+ # Compute the number of thumbnails we can show in the viewer window, while
98
+ # leaving one row to handle minor variations in the population size.
99
+ self .num_cols = int (math .floor ((window_width - 16 ) / (thumb_width + 4 )))
100
+ self .num_rows = int (math .floor ((window_height - 16 ) / (thumb_height + 4 )))
101
+
102
+ self .pool = Pool (num_workers )
103
+
104
+ def make_image_from_data (self , image_data ):
105
+ # For mono and grayscale, we need a palette because the evaluation function
106
+ # only returns a single integer instead of an (R, G, B) tuple.
107
+ if self .scheme == 'color' :
108
+ image = pygame .Surface ((self .thumb_width , self .thumb_height ))
109
+ else :
110
+ image = pygame .Surface ((self .thumb_width , self .thumb_height ), depth = 8 )
111
+ palette = tuple ([(i , i , i ) for i in range (256 )])
112
+ image .set_palette (palette )
113
+
114
+ for r , row in enumerate (image_data ):
115
+ for c , color in enumerate (row ):
116
+ image .set_at ((r , c ), color )
117
+
118
+ return image
119
+
120
+ def make_thumbnails (self , genomes , config ):
121
+ img_func = eval_mono_image
122
+ if self .scheme == 'gray' :
123
+ img_func = eval_gray_image
124
+ elif self .scheme == 'color' :
125
+ img_func = eval_color_image
126
+
127
+ jobs = []
128
+ for genome_id , genome in genomes :
129
+ jobs .append (self .pool .apply_async (img_func , (genome , config , self .thumb_width , self .thumb_height )))
130
+
131
+ thumbnails = []
132
+ for j in jobs :
133
+ # TODO: This code currently generates the image data using the multiprocessing
134
+ # pool, and then does the actual image construction here because pygame complained
135
+ # about not being initialized if the pool workers tried to construct an image.
136
+ # Presumably there is some way to fix this, but for now this seems fast enough
137
+ # for the purposes of a demo.
138
+ image_data = j .get ()
139
+
140
+ thumbnails .append (self .make_image_from_data (image_data ))
141
+
142
+ return thumbnails
143
+
144
+ def make_high_resolution (self , genome , config ):
145
+ genome_id , genome = genome
146
+
147
+ # Make sure the output directory exists.
148
+ if not os .path .isdir ('rendered' ):
149
+ os .mkdir ('rendered' )
150
+
151
+ if self .scheme == 'gray' :
152
+ image_data = eval_gray_image (genome , config , self .full_width , self .full_height )
153
+ elif self .scheme == 'color' :
154
+ image_data = eval_color_image (genome , config , self .full_width , self .full_height )
155
+ else :
156
+ image_data = eval_mono_image (genome , config , self .full_width , self .full_height )
157
+
158
+ image = self .make_image_from_data (image_data )
159
+ pygame .image .save (image , "rendered/rendered-{}-{}.png" .format (os .getpid (), genome_id ))
160
+
161
+ with open ("rendered/genome-{}-{}.bin" .format (os .getpid (), genome_id ), "wb" ) as f :
162
+ pickle .dump (genome , f , 2 )
163
+
164
+ def eval_fitness (self , genomes , config ):
165
+ selected = []
166
+ rects = []
167
+ for n , (genome_id , genome ) in enumerate (genomes ):
168
+ selected .append (False )
169
+ row , col = divmod (n , self .num_cols )
170
+ rects .append (pygame .Rect (4 + (self .thumb_width + 4 ) * col ,
171
+ 4 + (self .thumb_height + 4 ) * row ,
172
+ self .thumb_width , self .thumb_height ))
173
+
174
+ pygame .init ()
175
+ screen = pygame .display .set_mode ((self .window_width , self .window_height ))
176
+
177
+ buttons = self .make_thumbnails (genomes , config )
178
+
179
+ running = True
180
+ while running :
181
+ for event in pygame .event .get ():
182
+ if event .type == pygame .QUIT :
183
+ pygame .quit ()
184
+ running = False
185
+ break
186
+
187
+ if event .type == pygame .MOUSEBUTTONDOWN :
188
+ clicked_button = - 1
189
+ for n , button in enumerate (buttons ):
190
+ if rects [n ].collidepoint (pygame .mouse .get_pos ()):
191
+ clicked_button = n
192
+ break
193
+
194
+ if event .button == 1 :
195
+ selected [clicked_button ] = not selected [clicked_button ]
196
+ else :
197
+ self .make_high_resolution (genomes [clicked_button ], config )
198
+
199
+ if running :
200
+ screen .fill ((128 ,128 ,192 ))
201
+ for n , button in enumerate (buttons ):
202
+ screen .blit (button , rects [n ])
203
+ if selected [n ]:
204
+ pygame .draw .rect (screen , (255 , 0 , 0 ), rects [n ], 3 )
205
+ pygame .display .flip ()
206
+
207
+ for n , (genome_id , genome ) in enumerate (genomes ):
208
+ if selected [n ]:
209
+ genome .fitness = 1.0
210
+ pygame .image .save (buttons [n ], "image-{}.{}.png" .format (os .getpid (), genome_id ))
211
+ with open ("genome-{}-{}.bin" .format (os .getpid (), genome_id ), "wb" ) as f :
212
+ pickle .dump (genome , f , 2 )
213
+ else :
214
+ genome .fitness = 0.0
215
+
216
+ def run ():
217
+ # 128x128 thumbnails, 1500x1500 rendered images, 1100x810 viewer, grayscale images, 4 worker processes.
218
+ pb = PictureBreeder (128 , 128 , 1500 , 1500 , 1100 , 810 , 'gray' , 4 )
219
+
220
+ # Determine path to configuration file.
221
+ local_dir = os .path .dirname (__file__ )
222
+ config_path = os .path .join (local_dir , 'interactive_config' )
223
+ # Note that we provide the custom stagnation class to the Config constructor.
224
+ config = neat .Config (neat .DefaultGenome , neat .DefaultReproduction , InteractiveStagnation , config_path )
225
+
226
+ # Make sure the network has the expected number of outputs.
227
+ if pb .scheme == 'color' :
228
+ config .output_nodes = 3
229
+ else :
230
+ config .output_nodes = 1
231
+
232
+ config .pop_size = pb .num_cols * pb .num_rows
233
+ pop = neat .Population (config )
234
+
235
+ # Add a stdout reporter to show progress in the terminal.
236
+ pop .add_reporter (neat .StdOutReporter ())
237
+ stats = neat .StatisticsReporter ()
238
+ pop .add_reporter (stats )
239
+
240
+ while 1 :
241
+ pop .run (pb .eval_fitness , 1 )
242
+
243
+ if __name__ == '__main__' :
244
+ run ()
0 commit comments