Skip to content

Commit 6eb783e

Browse files
Added updated 2D image generation example (interactive and novelty versions).
1 parent 3e1059a commit 6eb783e

File tree

8 files changed

+589
-0
lines changed

8 files changed

+589
-0
lines changed

examples/picture2d/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## 2D Image Evolution ##
2+
3+
These examples demonstrate one approach to evolve 2D patterns using NEAT. These sorts of networks are
4+
referred to as "Compositional Pattern-Producing Networks" in the NEAT literature.
5+
6+
## Interactive Example ##
7+
8+
`evolve.py` is an example that amounts to an offline picbreeder.org without any nice features. :)
9+
10+
Left-click on thumbnails to pick images to breed for next generation, right-click to
11+
render a high-resolution version of an image. Genomes and images chosen for breeding
12+
and rendering are saved to disk.
13+
14+
## Non-Interactive
15+
16+
`novelty.py` automatically selects images to breed based on how different they are from any images previously
17+
seen during the current run. The 'novelty' of an image is the minimum Euclidean distance from that image to
18+
each image in the set of archived images. The most novel image in each generation is always added to the archive,
19+
and other images are randomly added with low probability regardless of their novelty.
20+

examples/picture2d/clean.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
rm genome-*.bin image-*.png
3+
rm winning-novelty-*.png novelty-*.png

examples/picture2d/common.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import neat
2+
3+
4+
def eval_mono_image(genome, config, width, height):
5+
net = neat.nn.FeedForwardNetwork.create(genome, config)
6+
image = []
7+
for r in range(height):
8+
y = -2.0 + 4.0 * r / (height - 1)
9+
row = []
10+
for c in range(width):
11+
x = -2.0 + 4.0 * c / (width - 1)
12+
output = net.serial_activate([x, y])
13+
gray = 255 if output[0] > 0.0 else 0
14+
row.append(gray)
15+
image.append(row)
16+
17+
return image
18+
19+
20+
def eval_gray_image(genome, config, width, height):
21+
net = neat.nn.FeedForwardNetwork.create(genome, config)
22+
image = []
23+
for r in range(height):
24+
y = -1.0 + 2.0 * r / (height - 1)
25+
row = []
26+
for c in range(width):
27+
x = -1.0 + 2.0 * c / (width - 1)
28+
output = net.activate([x, y])
29+
gray = int(round((output[0] + 1.0) * 255 / 2.0))
30+
gray = max(0, min(255, gray))
31+
row.append(gray)
32+
image.append(row)
33+
34+
return image
35+
36+
37+
def eval_color_image(genome, config, width, height):
38+
net = neat.nn.FeedForwardNetwork.create(genome, config)
39+
image = []
40+
for r in range(height):
41+
y = -1.0 + 2.0 * r / (height - 1)
42+
row = []
43+
for c in range(width):
44+
x = -1.0 + 2.0 * c / (width - 1)
45+
output = net.serial_activate([x, y])
46+
red = int(round((output[0] + 1.0) * 255 / 2.0))
47+
green = int(round((output[1] + 1.0) * 255 / 2.0))
48+
blue = int(round((output[2] + 1.0) * 255 / 2.0))
49+
red = max(0, min(255, red))
50+
green = max(0, min(255, green))
51+
blue = max(0, min(255, blue))
52+
row.append((red, green, blue))
53+
image.append(row)
54+
55+
return image

examples/picture2d/interactive.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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()

examples/picture2d/interactive_config

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# The `NEAT` section specifies parameters particular to the NEAT algorithm
2+
# or the experiment itself. This is the only required section.
3+
[NEAT]
4+
pop_size = 0
5+
max_fitness_threshold = 1.01
6+
reset_on_extinction = False
7+
8+
[DefaultGenome]
9+
num_inputs = 2
10+
num_hidden = 4
11+
num_outputs = 3
12+
initial_connection = partial 0.5
13+
feed_forward = True
14+
compatibility_threshold = 3.0
15+
compatibility_disjoint_coefficient = 1.0
16+
compatibility_weight_coefficient = 0.6
17+
conn_add_prob = 0.2
18+
conn_delete_prob = 0.2
19+
node_add_prob = 0.2
20+
node_delete_prob = 0.2
21+
activation_default = sigmoid
22+
activation_options = abs clamped exp gauss hat identity inv log relu sigmoid sin tanh square cube
23+
activation_mutate_rate = 0.05
24+
aggregation_default = sum
25+
aggregation_options = sum
26+
aggregation_mutate_rate = 0.0
27+
bias_init_mean = 0.0
28+
bias_init_stdev = 1.0
29+
bias_replace_rate = 0.1
30+
bias_mutate_rate = 0.7
31+
bias_mutate_power = 0.5
32+
bias_max_value = 30.0
33+
bias_min_value = -30.0
34+
response_init_mean = 1.0
35+
response_init_stdev = 0.1
36+
response_replace_rate = 0.1
37+
response_mutate_rate = 0.1
38+
response_mutate_power = 0.1
39+
response_max_value = 30.0
40+
response_min_value = -30.0
41+
42+
weight_max_value = 30
43+
weight_min_value = -30
44+
weight_init_mean = 0.0
45+
weight_init_stdev = 1.0
46+
weight_mutate_rate = 0.8
47+
weight_replace_rate = 0.1
48+
weight_mutate_power = 0.5
49+
enabled_default = True
50+
enabled_mutate_rate = 0.01
51+
52+
[InteractiveStagnation]
53+
max_stagnation = 10
54+
55+
[DefaultReproduction]
56+
elitism = 0
57+
survival_threshold = 0.2

0 commit comments

Comments
 (0)