遗传算法与深度学习实战(9)——使用遗传算法重建图像
- 0. 前言
- 1. 使用遗传算法重建图像
- 1.1 用多边形绘制图像
- 2. EvoLisa 项目
- 3. 实现遗传算法复现 EvoLisa 项目
- 3.1 基因构建
- 3.2 构建解决方案
- 小结
- 系列链接
0. 前言
遗传算法应用于图像处理的最流行方式之一是用一组半透明多边形重建图像。在此过程中,可以获得图像处理方面的有用经验,并获得对进化过程的直观见解。在本节中,我们通过复现 EvoLisa
项目重建《蒙娜丽莎》图像。
1. 使用遗传算法重建图像
在图像处理中使用遗传算法的最流行示例之一是使用一组半透明的重叠形状重建给定的图像。除了获得图像处理经验的机会外,这些实验还为进化过程提供了直观的了解,并有可能使人们对视觉艺术以及图像分析和图像压缩的发展有更好的了解。在图像重建实验中以熟悉的图像(通常是著名的绘画或其一部分)作为参考。通过组合颜色和透明度不同的重叠形状(通常是多边形)来构造相似的图像。
1.1 用多边形绘制图像
要从头开始绘制图像,可以使用 Pillow
的 Image
和 ImageDraw
类:
image = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(image, 'RGBA')
RGB
和 RGBA
是mode参数的可选值。RGB
值表示每个像素三个 8
位二进制值——红色 (R
),绿色 (G
) 和蓝色 (B
)。RGBA
值添加了第 4
个 8
位值 A
,代表图形的 alpha
(不透明度)级别。
可以使用 ImageDraw
类的多边形函数将多边形添加到图像中,以下语句将在图像上绘制一个三角形:
draw.polygon([(x1, y1), (x2, y2), (x3, y3)], (red, green, blue, alpha))
其中:
(x1, y1)
,(x2, y2)
和(x3, y3)
元组表示三角形的三个顶点。每个元组都包含图像内相应顶点的x
,y
坐标- 红色,绿色和蓝色是
[0, 255]
范围内的整数值,每个值代表多边形相应颜色的强度 alpha
是[0, 255]
范围内的整数值,表示多边形的不透明度值(值越低意味着透明度越高)
要绘制具有更多顶点的多边形,需要向列表中添加更多 (xi, yi)
元组。我们可以通过这种方式添加多个多边形,所有多边形都绘制在同一图像上,并且可能彼此重叠:
2. EvoLisa 项目
2008
年,Roger Johansson
展示了使用遗传算法用一组多边形绘制《蒙娜丽莎》的作品,下图展示了在演化后期取得的出色结果,优异的结果需要近一百万代才能演化出来。
EvoLisa
是一种生成建模的例子,算法的目标是模拟或复制某个过程的输出,近年来,生成建模迅速发展。利用 DEAP
,能够轻松复现 EvoLisa
项目,但是我们需要以一种更复杂和结构化的方式思考如何编码基因序列。在 N 皇后和旅行商问题中,一个基因是列表中的一个单独元素,现在需要将一个基因视为列表中的一组子元素。每个子元素(基因)定义了一个多边形,一个个体有多个用于在画布上绘制多边形的基因。
3. 实现遗传算法复现 EvoLisa 项目
接下来,使用 DEAP
实现遗传算法复现 EvoLisa
项目。我们已经知道,获得良好的结果可能需要很长时间。虽然我们并不会完全复制以上结果,但我们需要了解如何创建复杂基因,为复杂项目的构建奠定基础。
3.1 基因构建
我们首先需要了解如何将一系列数字转换为表示绘制多边形的基因。下图概述了如何从序列中提取一组属性,并将其转换为多边形笔刷,其中前六个元素表示简单多边形(三角形)的三个点。之后的三个元素表示颜色,最后一个元素表示 alpha
(透明度)。通过引入透明度,可以将画笔叠加在一起,产生更复杂的特征。
在 N 皇后和旅行商问题中,基因序列中的每个属性表示一个单独的基因。但在 EvoLisa
中,一组属性的子集构成了一个表示绘图笔刷的单个基因。
3.2 构建解决方案
(1) 导入所需库,并加载图像:
import random
import numpy as npfrom deap import algorithms
from deap import base
from deap import creator
from deap import toolsimport os
import cv2
import urllib.request
import matplotlib.pyplot as plt
from IPython.display import clear_outputdef load_target_image(image_url, color=True, size=None):image_path = "target_image" urllib.request.urlretrieve(image_url,image_path)if color:target = cv2.imread(image_path, cv2.IMREAD_COLOR)# Switch from bgr to rgbtarget = cv2.cvtColor(target, cv2.COLOR_BGR2RGB)else:target = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)if size:# Only resizes image if it is needed!target = cv2.resize(src=target, dsize=size, interpolation=cv2.INTER_AREA)return targetdef show_image(img_arr): plt.figure(figsize=(10,10))plt.axis("off")plt.imshow(img_arr/255)
plt.show()polygons = 128 #@param {type:"slider", min:10, max:1000, step:1}
size = 40 #@param {type:"slider", min:25, max:1000, step:5}
target_image = "Mona Lisa" #@param ["Mona Lisa", "Stop Sign", "Landscape", "Celebrity", "Art", "Abstract"]POLYGONS = polygons
SIZE = (size, size)target_urls = { "Mona Lisa" : 'https://upload.wikimedia.org/wikipedia/commons/b/b7/Mona_Lisa_face_800x800px.jpg',"Stop Sign" : 'https://images.uline.com/is/image//content/dam/images/H/H2500/H-2381.jpg',"Landscape" : 'https://www.adorama.com/alc/wp-content/uploads/2018/11/landscape-photography-tips-yosemite-valley-feature.jpg',"Celebrity" : 'https://s.abcnews.com/images/Entertainment/WireAP_91d6741d1954459f9993bd7a2f62b6bb_16x9_992.jpg',"Art" : "http://www.indianruminations.com/wp-content/uploads/what-is-modern-art-definition-2.jpg","Abstract" : "https://scx2.b-cdn.net/gfx/news/2020/abstractart.jpg"}target_image_url = target_urls[target_image]
target = load_target_image(target_image_url, size=SIZE)
show_image(target)
print(target.shape)
(2) 编写函数 extract_genes()
,接受属性(基因)序列,并按照基因的长度进行拆分。本节中我们基于多边形作为绘图笔刷,但是也可以使用其他形状的笔刷(如圆形或矩形):
GENE_LENGTH = 10
NUM_GENES = POLYGONS * GENE_LENGTH#create a sample invidiual
individual = np.random.uniform(0,1,NUM_GENES)
print(individual)
# [0.15593761 0.42941275 0.69346004 ... 0.77075339 0.99245891 0.80209134]def extract_genes(genes, length): for i in range(0, len(genes), length): yield genes[i:i + length]
(3) 编写函数,用于绘制每个基因。首先构建绘图画布并提取基因:
def render_individual(individual):if isinstance(individual,list):individual = np.array(individual)canvas = np.zeros(SIZE+(3,))radius_avg = (SIZE[0] + SIZE[1]) / 2 / 6genes = extract_genes(individual, GENE_LENGTH)for gene in genes:
提取相关基因属性来定义每个画笔,其中前六个值表示绘制多边形的三个点或坐标,使用 cv2.fillPoly()
函数进行绘制。然后,提取的 alpha
值用于使用 cv2.addWeighted()
函数将画笔(叠加)覆盖在画布上。最后,绘制完所有基因定义的画笔后,返回最终的画布进行评估:
x1 = int(gene[0] * SIZE[0])x2 = int(gene[2] * SIZE[0])x3 = int(gene[4] * SIZE[0])y1 = int(gene[1] * SIZE[1])y2 = int(gene[3] * SIZE[1])y3 = int(gene[5] * SIZE[1])color = (gene[6:-1] * 255).astype(int).tolist() pts = np.array([[x1,y1],[x2,y2],[x3,y3]], np.int32) pts = pts.reshape((-1, 1, 2))pts = np.array([[x1,y1],[x2,y2],[x3,y3]])cv2.fillPoly(overlay, [pts], color)alpha = gene[-1]canvas = cv2.addWeighted(overlay, alpha, canvas, 1 - alpha, 0) except:passreturn canvas
(4) 使用 render_individual()
函数渲染随机个体的结果如下所示:
render = render_individual(individual)
show_image(render)
在本节中,使用经典的蒙娜丽莎 (Mona Lisa
) 图像,但我们也可以加载其他图像,观察代码运行结果。
(5) 我们可以逐像素点比较,使用均方误差 (mean-squared error
, MSE
) 评估个体的适应度,计算计算使用多边形画笔绘制的图像和原始图像之间的 MSE
,然后将此误差作为个体的适应度分数返回,我们的目标是最小化此误差值:
from skimage.metrics import structural_similarity as ss
#@title Fitness Function
def fitness_mse(render):"""Calculates Mean Square Error Fitness for a render"""error = (np.square(render - target)).mean(axis=None)return errordef fitness_ss(render):"""Calculated Structural Similiarity Fitness"""index = ss(render, target, multichannel=True)return 1-indexprint(fitness_mse(render))
# 10661.935130594466
(6) 最后,定义遗传算子。定义函数 uniform()
,用于从由下限和上限定义的均匀分布中生成浮点值,该函数注册了一个 attr_float
运算符,并用于 creator.Individual
运算符的注册中;最后,将 evaluate
函数注册为 evaluate
运算符:
creator.create("FitnessMax", base.Fitness, weights=(-1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)def uniform(low, up, size=None):try:return [random.uniform(a, b) for a, b in zip(low, up)]except TypeError:return [random.uniform(a, b) for a, b in zip([low] * size, [up] * size)]toolbox = base.Toolbox()
toolbox.register("attr_float", uniform, 0, 1, NUM_GENES)
toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.attr_float)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)def evaluate(individual):render = render_individual(individual)print('.', end='')
return fitness_mse(render), #using MSE for fitnessdef register_selection(choice):choices = ["Tournament", "Roulette", "Random", "Best", "Worst", "NSGA2", "SPEA2"]if choice == choices[0]:toolbox.register("select", tools.selTournament, tournsize=3)elif choice == choices[1]:toolbox.register("select", tools.selRoulette)elif choice == choices[2]:toolbox.register("select", tools.selRandom)elif choice == choices[3]:toolbox.register("select", tools.selBest)elif choice == choices[4]:toolbox.register("select", tools.selWorst)elif choice == choices[5]:toolbox.register("select", tools.selNSGA2)elif choice == choices[6]:toolbox.register("select", tools.selSPEA2)def register_mating(choice):choices = ["Partially Matched", "One Point", "Two Point", "Ordered", "ES Two Point", "Uniform", "Uniform Partially Matched"]if choice == choices[0]:toolbox.register("mate", tools.cxPartialyMatched)elif choice == choices[1]:toolbox.register("mate", tools.cxOnePoint)elif choice == choices[2]:toolbox.register("mate", tools.cxTwoPoint)elif choice == choices[3]:toolbox.register("mate", tools.cxOrdered)elif choice == choices[4]:toolbox.register("mate", tools.cxESTwoPoint)elif choice == choices[5]:toolbox.register("mate", tools.cxUniform, indpb=.5)elif choice == choices[6]:toolbox.register("mate", tools.cxUniformPartialyMatched, indpb=.5)selection = "Tournament" #@param ["Tournament", "Random", "Best", "Worst", "NSGA2", "SPEA2"]
mating = "One Point" #@param ["Partially Matched", "One Point", "Two Point", "Ordered", "Uniform", "Uniform Partially Matched"]register_mating(mating)
register_selection(selection)toolbox.register("mutate", tools.mutGaussian, mu=0.0, sigma=1, indpb=.05)
toolbox.register("evaluate", evaluate)NGEN = 100000
CXPB = .6
MUTPB = .3
MU = 2500
pop = toolbox.population(n=MU)
hof = tools.HallOfFame(1)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max) best = Nonefor g in range(NGEN):pop, logbook = algorithms.eaSimple(pop, toolbox, cxpb=CXPB, mutpb=MUTPB, ngen=1, stats=stats, halloffame=hof, verbose=False)best = hof[0] clear_output()render = render_individual(best) show_image(render) print(f"Gen ({g}) : fitness = {fitness_mse(render)}")
下图展示了使用三角形笔刷运行约 5000
代的结果,我们也可以使用圆形或矩形笔刷进行绘制,并观察运行结果。
修改超参数,并重新运行代码,尝试改进 EvoLisa
绘制图像的速度,或尝试调整 EvoLisa
使用的多边形(基因)数量。
EvoLisa
是十多年前展示遗传算法生成建模的示例。通过生成对抗网络 (Generative Adversarial Network, GAN) 或扩散模型等先进的生成模型能够得到远远优于 EvoLisa
的结果。然而,EvoLisa
展示了如何对基因或指令集进行编码和演化。虽然 DEAP
应用相似,但 EvoLisa
的底层机制展示了一种不同的优化形式。
小结
EvoLisa
是一个经典的遗传算法应用案例,展示了如何利用计算机算法来模仿艺术家的创作风格,尤其是在复杂的艺术形式中,如绘画和图像生成。本节中,我们使用 DEAP
通过复现 EvoLisa
项目重建《蒙娜丽莎》图像。
系列链接
遗传算法与深度学习实战(1)——进化深度学习
遗传算法与深度学习实战(2)——生命模拟及其应用
遗传算法与深度学习实战(3)——生命模拟与进化论
遗传算法与深度学习实战(4)——遗传算法详解与实现
遗传算法与深度学习实战(5)——遗传算法框架DEAP
遗传算法与深度学习实战(6)——DEAP框架初体验
遗传算法与深度学习实战(7)——使用遗传算法解决N皇后问题
遗传算法与深度学习实战(8)——使用遗传算法解决旅行商问题