-
Notifications
You must be signed in to change notification settings - Fork 0
/
Main_ct.py
354 lines (282 loc) · 12 KB
/
Main_ct.py
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
import os
import numpy as np
from PIL import Image, ImageDraw
import random as rd
from numpy.random import choice, random
import json
POPULATION_SIZE = 200
GENERATIONS = 70000
NUMBER_CIRCLES = 600
UNIFORM_CROSSOVER_PROB = 0.5
### mutation amount 0.05
MUTATION_X_Y_AMOUNT = 0.01
MUTATION_RADIUS_AMOUNT = 0.01
MUTATION_COLOR_AMOUNT = 0.01
### mutation probability
MUTATION_PROBABILITY = 0.8
### Add and delete probability
CIRCLES_ADD_PROBABLITY = 1
CIRCLES_DELETE_PROBABLITY = 0.2
# Local Path
FILEPATH = os.path.abspath(__file__)
FILEDIR = FILEPATH.replace(os.path.basename(FILEPATH), "")
FILENAME = "fuji.png"
og_image_file = Image.open(FILEDIR + FILENAME, "r")
# save image as uint64 np array
og_image = np.array(og_image_file, dtype=np.uint64)
# Get width, height of the original image
width, height = og_image_file.size
print(width, height)
# set black canvas
blank = Image.new("RGB", (width, height), (255, 255, 255, 255))
# to get cordinates from decimal to (x0, y0) (x1, y1)
def get_ellipse_coordinates(x, y, radius_norm, width, height):
def map_coordinates_to_ellipse(x, y, radius_norm, width, height):
canvas_x, canvas_y = int(x * width), int(y * height)
canvas_radius = int(radius_norm * min(width, height))
return [
(canvas_x - canvas_radius, canvas_y - canvas_radius),
(canvas_x + canvas_radius, canvas_y + canvas_radius),
]
ellipse_coords = map_coordinates_to_ellipse(x, y, radius_norm, width, height)
return ellipse_coords
# Mapping the value from [0, 1] to [0.008, 0.045] used for radius
def map_rad(value):
mapped_value = 0.008 + (value * 0.045)
return mapped_value
# Mapping the value from [0, 1] to [0, 360] for hue
def map_h(value):
# Ensure hue is within the valid range [0, 360]
mapped_value = int(value * 360) % 360
return mapped_value
# Mapping the value from [0, 1] to [0, 100] for saturation and lightness
def map_sl(value):
# Ensure saturation and lightness are within the valid range [0, 100]
mapped_value = max(min(value * 100, 100), 0)
rounded_value = round(mapped_value, 2) # Round to two decimal places
return rounded_value
# draw a canvas with the circles and return the np array of the color values
def draw(chroma):
image = Image.new("RGBA", (width, height), color=(255, 255, 255))
draw = ImageDraw.Draw(image)
for gene in chroma:
x, y, r, h, s, l = gene
coords = get_ellipse_coordinates(x, y, map_rad(r), width, height)
circle_color = (
"hsl(" + str(map_h(h)) + "," + str(map_sl(s)) + "%," + str(map_sl(l)) + "%)"
)
draw.ellipse(coords, fill=circle_color, outline=None, width=1)
# print(x,y,r,h,s,l)
# image.show()
return np.array(image, dtype=np.uint64)
# draw a canvas with the circles and display the image
def draw_display(chroma):
image = Image.new("RGBA", (width, height), color=(255, 255, 255, 255))
draw = ImageDraw.Draw(image)
for gene in chroma:
x, y, r, h, s, l = gene
coords = get_ellipse_coordinates(x, y, map_rad(r), width, height)
circle_color = (
"hsl(" + str(map_h(h)) + "," + str(map_sl(s)) + "%," + str(map_sl(l)) + "%)"
)
draw.ellipse(coords, fill=circle_color, outline=None, width=1)
image.show()
# draw a canvas with the circles and return for saving
def draw_save(chroma):
image = Image.new("RGBA", (width, height), color=(255, 255, 255, 255))
draw = ImageDraw.Draw(image)
for gene in chroma:
x, y, r, h, s, l = gene
coords = get_ellipse_coordinates(x, y, map_rad(r), width, height)
circle_color = (
"hsl(" + str(map_h(h)) + "," + str(map_sl(s)) + "%," + str(map_sl(l)) + "%)"
)
draw.ellipse(coords, fill=circle_color, outline=None, width=1)
return image
# fitness function
def fitness(generated_image):
# Ensure both images have the same shape
if og_image.shape != generated_image.shape:
raise ValueError("Images must have the same dimensions")
pix1 = np.array(og_image, dtype=np.uint64)
pix2 = np.array(generated_image, dtype=np.uint64)
return round(np.sqrt(np.square(pix1 - pix2).sum(axis=-1)).sum(), 8)
# fitness function for 2 chromosones
def draw_fitness(chroma, chroma2):
pix = draw(chroma)
fit = fitness(pix)
pix2 = draw(chroma2)
fit2 = fitness(pix2)
return fit, fit2
# one point crossover
def crossover(parent1, parent2):
# Randomly select a crossover point (assuming the chromosome has the same length)
crossover_point = rd.randint(1, len(parent1) - 1)
# Create children by combining genetic information from parents
child1 = np.vstack((parent1[:crossover_point], parent2[crossover_point:]))
child2 = np.vstack((parent2[:crossover_point], parent1[crossover_point:]))
return child1, child2
# uniform crossover
def uniform_crossover(parent1, parent2):
max_length = max(len(parent1), len(parent2))
child1, child2 = [], []
for i in range(max_length):
if i < len(parent1) and i < len(parent2):
if random() < UNIFORM_CROSSOVER_PROB :
child1.append(parent2[i])
child2.append(parent1[i])
else:
child1.append(parent1[i])
child2.append(parent2[i])
elif i < len(parent1):
child1.append(parent1[i])
child2.append(parent1[i])
else:
child1.append(parent2[i])
child2.append(parent2[i])
return child1, child2
# convert from normal array to numpy_array
def convert_to_numpy_array(list_of_arrays):
# Find the maximum length among the arrays
max_length = max(len(arr) for arr in list_of_arrays)
# Fill shorter arrays with zeros to make them equal in length
equal_length_arrays = [np.pad(arr, (0, max_length - len(arr)), 'constant') for arr in list_of_arrays]
# Convert the list of arrays into a single NumPy array
numpy_array = np.array(equal_length_arrays)
return numpy_array
# convert from normal array to numpy_array for 2 arrays
def convert_to_numpy_array2(list1, list2):
valof1 = convert_to_numpy_array(list1)
valof2 = convert_to_numpy_array(list2)
return valof1,valof2
# picker for scalar
def value_picker():
values = [1.2, 1.15, 1.1, 1, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7]
return rd.choice(values)
def mutate_X(gene, scaler):
x_change = np.random.uniform(-MUTATION_X_Y_AMOUNT * scaler, MUTATION_X_Y_AMOUNT * scaler)
gene[0] = min(max(gene[0] + x_change, 0), 1)
return gene
# mutate functions
def mutate_Y(gene, scaler):
y_change = np.random.uniform(-MUTATION_X_Y_AMOUNT * scaler, MUTATION_X_Y_AMOUNT * scaler)
gene[1] = min(max(gene[1] + y_change, 0), 1)
return gene
def mutate_radius(gene, scaler):
gene[2] = min(max(gene[2] + np.random.uniform(-MUTATION_RADIUS_AMOUNT * scaler, MUTATION_RADIUS_AMOUNT * scaler), 0), 1)
return gene
def mutate_color_h(gene, scaler):
gene[3] = min(max(gene[3] + np.random.uniform(-MUTATION_COLOR_AMOUNT * scaler, MUTATION_COLOR_AMOUNT * scaler), 0), 1)
return gene
def mutate_color_s(gene, scaler):
gene[4] = min(max(gene[4] + np.random.uniform(-MUTATION_COLOR_AMOUNT * scaler, MUTATION_COLOR_AMOUNT * scaler), 0), 1)
return gene
def mutate_color_l(gene, scaler):
gene[5] = min(max(gene[5] + np.random.uniform(-MUTATION_COLOR_AMOUNT * scaler, MUTATION_COLOR_AMOUNT * scaler), 0), 1)
return gene
# make new Jean from 2 parents
def new_gene_make(parent1, parent2):
# Select a random crossover point
crossover_point = np.random.randint(1, len(parent1) - 1) # Exclude ends
if np.random.randint(0, 2) == 1:
child1 = np.concatenate((parent1[:crossover_point], parent2[crossover_point:]), axis=0)
else:
child1 = np.concatenate((parent2[:crossover_point], parent1[crossover_point:]), axis=0)
return child1
# given a number of types of mutations to make choose which ones to make
def apply_random_mutations(gene, scaler, num_mutations):
mutation_functions = [mutate_X, mutate_Y, mutate_radius, mutate_color_h, mutate_color_s, mutate_color_l]
selected_mutations = np.random.choice(mutation_functions, num_mutations, replace=False)
for mutation in selected_mutations:
gene = mutation(gene, scaler)
return gene
# Mutation function
def mutation(chromo):
changed_chromo = []
for i in chromo:
##### to change the scale of the mutation not always same
# if random() < 0.75:
# scaler = value_picker()
# else:
# scaler = 1
scaler = 1
if random() < MUTATION_PROBABILITY:
changed_chromo.append(apply_random_mutations(i, scaler, np.random.randint(1, 7)))
else:
changed_chromo.append(i)
if random() < CIRCLES_DELETE_PROBABLITY:
length = len(changed_chromo)
random_number = np.random.randint(0, length - 1)
changed_chromo = np.delete(changed_chromo, random_number, axis=0)
else:
new_gene = np.around(np.random.random((6)), decimals=10)
changed_chromo = np.vstack((changed_chromo, new_gene))
# Can choose from randomly generating a new jean or from 2 parents using one point crossover
# if random() < CIRCLES_ADD_TYPE_PROBABLITY:
# new_gene = np.around(np.random.random((6)), decimals=10)
# changed_chromo = np.vstack((changed_chromo, new_gene))
# else:
# rd1 = chromo[rd.randint(1, len(chromo)-1)]
# rd2 = chromo[rd.randint(1, len(chromo)-1)]
# new_gene = new_gene_make(rd1, rd2)
# changed_chromo = np.vstack((changed_chromo, new_gene))
return changed_chromo
# randomize position of the circles ex front to back (not used)
def randomizer(chromo):
np.random.shuffle(chromo)
return chromo
population = []
next_Gen_Population = []
# get population from Json file
file_path = os.path.join(FILEDIR, "population_data.json")
# Read and print the contents of the JSON file
with open(file_path, 'r') as file:
data = json.load(file)
for fit, chromo_list in data:
chromo_array = np.array(chromo_list)
population.append((fit, chromo_array))
population = [(fit, np.array(chromo)) for fit, chromo in population]
# offset for the number of generations already ran
start_offset = 21800
for generation in range(GENERATIONS):
new_population = []
for g in population:
new_population.append(g)
population.sort(key=lambda x: x[0])
generation = generation+1+start_offset
print(f"=== Generation {generation} ===")
print(f"Best Fitness: {population[0][0]}")
# save image and population every 50 generations
if generation % 50 == 0:
file_path = os.path.join(FILEDIR, "population_data.json") # File path using FILEDIR
# Convert NumPy arrays to lists in the population data
population_list = [(fit, chromo.tolist()) for fit, chromo in population]
# Save population data to a JSON file
with open(file_path, 'w') as file:
json.dump(population_list, file)
ig = draw_save(population[0][1])
ig.save(FILEDIR+f'{generation}.png')
print(len(population[0][1]))
# Kill half the population
next_generation = population[: POPULATION_SIZE // 2]
for i in range(POPULATION_SIZE // 2):
# choose 2 random parents and check if they are the same if so choose again
top = len(next_generation)-1
rd_1 = rd.randint(0, top)
rd_2 = rd.randint(0, top)
while rd_2 == 1 or rd_2 == rd_1:
rd_2 = rd.randint(0, top)
parent1 = next_generation[rd_1][1]
parent2 = next_generation[rd_2][1]
# crossover
child1, child2 = uniform_crossover(parent1, parent2)
ch1, ch2 = convert_to_numpy_array2(child1, child2)
# mutation
mut1 = mutation(ch1)
mut2 = mutation(ch2)
c1, c2 = convert_to_numpy_array2(mut1, mut2)
# calculate fitness
fit1, fit2 = draw_fitness(c1,c2)
next_generation.append((fit1 , c1))
next_generation.append((fit2 , c2))
population = next_generation