如何用Python实现照片马赛克效果?

关注「实验楼」,每天分享一个项目教程

如何使用 Python 创建照片马赛克呢?我们将目标图像划分若干个网格,再用相近的颜色或图像去替换即可。跟着下面的操作,你一定能学会用Python自动实现这个效果。

今天,带大家学习如何使用 Python 创建照片马赛克。我们将目标图像划分成较小图像的网格,并用适当的图像替换网格中的每一小块,即可创建原始图像的照片马赛克。比如这样:

如何用Python实现照片马赛克效果?_第1张图片

你可以指定网格的尺寸,并选择输入图像是否可以在马赛克中重复使用。

实验来源

本实验源自 异步社区  《Python极客项目编程》 第七章,感谢 异步社区 授权实验楼发布。如需系统的学习本书,请购买 《Python极客项目编程》

实验知识点

  • 用 Python 图像库(PIL)创建图像;

  • 计算图像的平均 RGB 值;

  • 剪切图像;

  • 通过粘贴另一张图像来替代原图像的一部分;

  • 利用平均距离测量来比较 RGB 值。

实验环境

  • Python

  • Xface 终端

二、实验原理

要创建照片马赛克,就从目标图像的块状低分辨率开始(因为在高分辨率的图像中,小块图像的数量会太大)。该图像的分辨率将决定马赛克的维度 M*N(M 是行数,N 是列数)。接着,根据这种方法替换原始图像中的每一小块:

  1. 读入一些小块图像,他们将取代原始图像中的小块;

  2. 读入目标图像,将他们分割成 M*N 的小块网格;

  3. 对于目标图像中的每个小块,从输入的小块图像中找到最佳匹配;

  4. 将选择的输入图像安排在 M*N 的网格中,创建最终的照片马赛克。

2.1 分割目标图像

按照图 7-2 中的方案,开始将目标图像划分成 M*N 的网格。

如何用Python实现照片马赛克效果?_第2张图片


图 7-2 中的图像展示了如何将原始图像分割成小块的网格。x 轴表示网格的列,y 轴表示网格的行。

现在,看看如何计算网格中一个小块的坐标。下标为 (i,j) 的小块,左上角坐标为 (i*w,i*j) ,右下角坐标为 ((i+1)∗w,(j+1)∗h),其中w  h 分别是小块的宽度和高度,PIL 可以利用这些数据,从原图像创建小块。

2.2 平均颜色值

图像中的每个像素都有颜色,由它的红、绿、蓝值来表示。在这个例子中,使用 8 位的图像,因此每个部分都有 8 位值,范围在 [0,255]。如果一副图像共有 N 个像素,平均 RGB 计算如下:

请注意,平均 RGB 也是一个三元组,不是标量或一个数字,因为平均值是针对每个颜色成分分别计算的。计算平均 RGB 是为了匹配图像小块和目标图像。

2.3 匹配图像

对于目标图像中的每个小块,需要在用户指定的输入文件夹下的图像中找到一幅匹配的图像。要确定两个图像是否匹配,可以通过比较平均 RGB 值,最匹配的图像就是平均 RGB 值最接近的图像。

要做到这一点,最简单的方法是计算一个像素中 RGB 值之间的距离,以便从输入图像中找到最佳匹配。对于几何中的三维点,可以用以下的距离计算方法:

640?wx_fmt=png

这里计算了点 (r_1,g_1,b_1)  (r_2,g_2,b_2) 之间的距离。给定一个目标图像的平均 RGB 值,以及来自输入图像的平均 RGB 值列表,你可以使用线性搜索和三维点距离的计算,来找到最匹配的图像。

三、开发准备

试验开始之前还需要安装 Pillow(其中包含PIL) 和 numpy 这两个库:

$ sudo apt-get update
$ sudo pip install --upgrade pip # 更新 pip
$ sudo pip install Pillow numpy # 使用 pip 安装 Pillow 和 numpy

然后下载用来实验所需的素材:

$ wget http://labfile.oss.aliyuncs.com/courses/1041/test-data.zip
$ unzip test-data.zip

四、项目文件结构

本实验包括一个 .py 文件和若干图片组成,所有的图片都放置在 test-data 文件夹下,test-data/a.jpg 是目标图像,test-data/set1/ 文件夹下存放的是小块图像。

五、实验步骤

5.1 读入小块图像

首先,从给定的文件夹中读取小块图像:

def getImages(imageDir):
"""
 给定一个目录,加载该目录下的图像,并以列表的形式返回
 """
files = os.listdir(imageDir)
images = []
for file in files:
filePath = os.path.abspath(os.path.join(imageDir, file))
try:
# 显式加载以避免资源危机
fp = open(filePath, "rb")
im = Image.open(fp)
images.append(im)
# 强制从文件中加载图像数据
im.load()
# 关闭文件
fp.close()
except:
# skip
print("Invalid image: %s" % (filePath,))
return images

首先调用 os.listdir()  imageDir 目录中的文件放入一个列表。接下来,迭代遍历列表中的每个文件,将它载入为一个 PIL Image 对象。

然后 os.path.abspath()  os.path.join() 来获取图像的完整文件名。这个习惯用法在 Python 中经常使用,以确保代码既能在相对路径下工作(如foo\bar),也能在绝对路径下工作,并且能跨操作系统,不同的操作系统有不同的目录命名惯例(Windows 用 \ 而 Linux 用 /)。

要将文件加载为 PIL 的 Image 对象,可以将每个文件名传入 Image.open() 方法,但如果照片马赛克文件夹中有几百张甚至几千张图片,这样做非常消耗系统资源。作为替代,可以用 Python 分别打开每个小块图像,利用 Image.open() 将文件句柄 fp 传入 PIL。图像加载完成后,立即关闭文件句柄释放系统资源。

所以先用 open() 打开图像文件,随后将文件句柄传入 Image.open(),将得到的图像对象 im 存入到一个列表,因为 open() 是一个惰性操作,所以接下来需要强制调用 Image.load(),强制 im 加载文件中的图像数据。Image.open() 确定了图像,但它实际上没有读取全部图像数据,直到使用该图像时才会那么做。

最后就是使用 fp.close() 关闭文件句柄,释放系统资源。

5.2 计算输入图像的平均颜色值

读入输入图像后,需要计算它们的平均颜色值,以及目标图像中的每个小块的值。创建一个方法 getAcerageRGB() 来计算这两个值。

def getAverageRGB(image):
"""
 计算并返回给定 Image 对象 (r,g,b) 形式的颜色平均值
 """
# 将图像转换成 numpy 中的数组 (三维数组)
im = np.array(image)
# 获取宽度, 高度, 深度
w,h,d = im.shape
# 计算平均值
return tuple(np.average(im.reshape(w*h, d), axis=0))

首先,使用 numpy 将 Image 对象转换为数据数组。返回的 numpy 数组形为(w,h,d),其中 w 是图像的宽度,h 是图像的高度,d 是深度,在这个例子中,是 RGB 图像的三个单位(分别对应 R,G 和 B),所以我们使用 w,h,d=im.shape 来将 shape 元组保存在 w,h,d当中。然后计算平均值,通过 im.reshape(w*h,d) 将原来的 (w,h,d) 三维数组变成了大小为 (w*h,d) 的二维数组,这样就能使用numpy.average() 计算出颜色平均值。

5.3 将目标图像分割成网格

现在,需要将目标图像分割成 M*N 网格,包含更小的图像。让我们创建一个方法来实现。

def splitImage(image, size):
"""
 根据给定图像的维度来分割图像,返回一个大小为  m*n 的图像列表
 """
W, H = image.size[0], image.size[1]
m, n = size
w, h = int(W/n), int(H/m)
# 图像列表
imgs = []
# 生成列表
for j in range(m):
for i in range(n):
# 向 imgs 中追加裁减后的小块图像
imgs.append(image.crop((i*w, j*h, (i+1)*w, (j+1)*h)))
return imgs

首先,W,H=image.size[0],image.size[1] 得到目标图像的维度,然后m,n=size 得到尺寸。接下来,w,h=int(W/n),int(H/m) 计算目标图像中每一小块的尺寸。

计算出小块的尺寸后,就可以根据网格的维度进行迭代遍历,分割并将每一小块保存为单独的图像。最后 image.crop() 利用左上角图像坐标和裁剪图像的维度作为参数,裁剪出图像的一部分(见 2.1 小节)。

5.4 寻找小块的最佳匹配

现在,让我们从输入图像的文件夹中,找到小块的最佳匹配。创建一个工具方法getBestMatchIndex(),如下所示:

def getBestMatchIndex(input_avg, avgs):
"""
 返回按照 RGB 值的距离挑选出的最佳匹配。
 """
# 图像的均值
avg = input_avg

# 根据 x/y/z 的距离从 avgs 中挑选出距 input_avg 最近的值
index = 0
min_index = 0
min_dist = float("inf")
for val in avgs:
dist = ((val[0] - avg[0])*(val[0] - avg[0]) +
(val[1] - avg[1])*(val[1] - avg[1]) +
(val[2] - avg[2])*(val[2] - avg[2]))
if dist < min_dist:
min_dist = dist
min_index = index
index += 1

return min_index

需要从列表 avgs 中,找到最匹配平均 RGB 值 input_avg 的。avgs 是小块图像平均 RGB 值的列表。

为了找到最佳匹配,比较这些输入图像的平均 RGB 值,min_index = 0,min_dist = float("inf"),将最接近的匹配下标初始化为 0,最小距离初始化为无穷大。该测试在第一次总是会通过,因为任何距离都小于无穷大。接下来,遍历平均值列表中的值,依次计算与 input_avg 的距离(比较距离的平方,以减少计算时间)。如果新的距离比原有的距离要小,就使用新的纪录替代原有的数据。迭代结束后,就得到了平均 RGB 值列表 avgs 中,最接近 input_avg 的下标。现在可以利用这个下标,从小块图像的列表中选择匹配的小块图像了。

5.5 创建图像网格

在创建照片马赛克之前,还需要一个工具方法:createImageGrid()。这个方法将创建大小为 $$M*N$$ 的图像网格,往这个网格中填入小块图像,就可以创建出照片马赛克。

def createImageGrid(images, dims):
"""
 给定网格大小和小块图像列表, 创建由图像组成的网格
 """
m, n = dims

# 检查参数
assert m*n == len(images)

# 获取图像宽度, 高度的最大值
# 注意: 并不是每个小块图像的大小都是相同的
width = max([img.size[0] for img in images])
height = max([img.size[1] for img in images])

# 创建输出图像
grid_img = Image.new('RGB', (n*width, m*height))

# 粘贴图像
for index in range(len(images)):
row = int(index/n)
col = index - n*row
grid_img.paste(images[index], (col*width, row*height))

return grid_img

在创建图像网格之前,需要先用 assert 检查提供给 createImageGrid() 的图像数量是否符合网格的大小( assert 方法检查代码中的假定,特别是在开发和测试过程中的假定)。现在你有一个小块图像列表,基于最接近的 RGB 值,你将用它来创建一幅图像,表现照片马赛克。由于大小差异,某些选定的图像可能不会正好填充一个小块,但这不会是一个问题,因为你首先用黑色背景填充小块。

width = max([img.size[0] for img in images]) height = max([img.size[1] for img in images]) 的作用是计算小块图像的最大宽度和高度(你没有对选择的输入图像的大小做出任何假定,无论它们相同或不同,代码都能工作),如果输入图像不能完全填充小块,小块之间的空间将显示为背景色,默认是黑色。

grid_img = Image.new('RGB', (n*width, m*height)) 创建一个空的 Image,大小符合网格中的所有图像。小块图像会粘贴到这个图像,填充图像网格。随后,循环遍历选定的图像,调用 Image.paste() 方法,将它们粘贴到相应的网格中。Image.paste() 的第一个参数是要粘贴的 Image 对象,第二个参数是左上角的坐标。现在,你要搞清楚小块图像要粘贴到图像网格的行和列。为了做到这一点,将图像下标表示为行和列。小块在图像网格中的下标由 N*row+col 给出,其中 N 是一行的小块数,(row,col) 是在该网格中的坐标。行和列的分别由 row=int(index/n)  col=index-n*row 给出。

5.6 创建照片马赛克

现在,有了所有必需的工具方法,让我们编写一个 main 函数,创建照片马赛克。

def createPhotomosaic(target_image, input_images, grid_size,
reuse_images=True):
"""
 从给定的目标图像和小块图像创建照片马赛克
 """

print('splitting input image...')
# 将目标图像分割分割成子图像
target_images = splitImage(target_image, grid_size)

print('finding image matches...')
# 对于每一个目标图像,从输入选择一个
output_images = []
# 用于向用户反馈
count = 0
batch_size = int(len(target_images)/10)

# 计算输入的小块图像的平均值
avgs = []
for img in input_images:
avgs.append(getAverageRGB(img))

for img in target_images:
# 子图像的均值
avg = getAverageRGB(img)
# 寻找最匹配的索引
match_index = getBestMatchIndex(avg, avgs)
output_images.append(input_images[match_index])
# 向用户反馈进度
if count > 0 and batch_size > 10 and count % batch_size is 0:
print('processed %d of %d...' %(count, len(target_images)))
count += 1
# 如果不允许重用图像, 就移除被选中的图像
if not reuse_images:
input_images.remove(match_index)

print('creating mosaic...')
# 将照片马赛克保存在图像中
mosaic_image = createImageGrid(output_images, grid_size)

# 返回照片马赛克
return mosaic_image

createPhotomosaic() 方法的输入是目标图像,输入图像列表,生成照片马赛克的大小,以及一个表明图像是否可以复用的标志。首先调用 splitImage 将图像分割成一个网格。图像被分割后,针对每个小块,从输入文件夹中寻找匹配的图像(因为这个过程可能很长,所以提供反馈给用以,让他们知道程序仍在工作)。

随后将 batch_size 设置为小块图像总数的十分之一。后面的程序将会依据 batch_size 的大小来向用户更新信息(选择十分之一是任意的,只是一种方式让程序说:“我还活着。”每次处理了图像的十分之一,就打印一条消息,表用程序仍在运行)。

在设置好 batch_size 后,为输入文件夹中的每个图像计算平均 RGB 值,并保存在列表 avgs 中。然后,开始迭代遍历目标图像网格中的每个小块。对于每个小块,avg=getAverage(img) 计算平均 RGB 值。然后,从输入图像的评价值列表中,match_index=getBestMatchIndex(avg,avgs) 寻找该值的最佳匹配。返回结果是一个下标,output_images.append(input_images[match_index]) 取得该下标对应的图像,并保存在列表中。

随后就是创建照片马赛克,由于这个过程相对耗时,所以每处理 batch_size 个图像,就为用户打印一条消息。如果 reuse_images 标志设置为 False,就从列表中删除选定的输入图像,这样就不会再另一个小块中重用(如果有广泛的输入图像可选,这种方式效果最好)。最后mosaic_image=createImageGrid(output_images,grid_size) 创建最终的照片马赛克。

5.7 添加命令行选项

该程序的 main() 方法支持这些命令行选项:

  # 解析命令行参数
parser = argparse.ArgumentParser(description='Creates a photomosaic from input images')
# 添加参数
parser.add_argument('--target-image', dest='target_image', required=True)
parser.add_argument('--input-folder', dest='input_folder', required=True)
parser.add_argument('--grid-size', nargs=2, dest='grid_size', required=True)
parser.add_argument('--output-file', dest='outfile', required=False)

args = parser.parse_args()

包括三个必需的命令行参数:目标图像的名称,输入图像文件夹的名称,以及网格尺寸。第四个参数是可选的文件名,如果省略该文件名,照片将写入文件 mosaic.png 中。

5.8 控制照片马赛克的大小

要解决的最后一个问题是照片马赛克的大小,如果基于目标图像中匹配的小块,盲目地将输入图像粘贴在一起,就会得到一个巨大的照片马赛克,比目标图像大得多。为了避免这种情况,调整输入图像的大小,以匹配网格中每个小块的大小(这样做还有一个好处,可以加快平均 RGB 的计算,因为用了较小的图像)。

main() 方法也进行这样的处理:

# 载入小块图像
print('reading input folder...')
input_images = getImages(args.input_folder)

# 判断小块图像列表是否为空
if input_images ==[]:
print('No input images found in %s. Exiting.' % (args.input_folder, ))
exit()

# 是否使用随机列表 - 来增加输出的多样性?
random.shuffle(input_images)

5.9 完整代码

完整代码您可以点击阅读原文,或登录实验楼找到:https://www.shiyanlou.com/courses/1041

5.10 运行照片马赛克程序

在 test-data 的父文件夹下执行命令:

$ python photomosaic.py --target-image test-data/a.jpg --input-folder test-data/set1/ --grid-size 128 128
reading input folder...
starting photomosaic creation...
resizing images...
max tile dims: (14, 10)
splitting input image...
finding image matches...
processed 1638 of 16384...
processed 3276 of 16384...
processed 4914 of 16384...
processed 6552 of 16384...
processed 8190 of 16384...
processed 9828 of 16384...
processed 11466 of 16384...
processed 13104 of 16384...
processed 14742 of 16384...
processed 16380 of 16384...
creating mosaic...
saved output to mosaic.png
done.

使用火狐浏览器查看图片:

$ firefox test-data/a.jpg # 查看原图
$ firefox mosaic.png # 查看结果图片

原图

如何用Python实现照片马赛克效果?_第3张图片

结果

如何用Python实现照片马赛克效果?_第4张图片

七、课后练习

  1. 编写一个程序,创建图像的块状版本,类似实验中出现的第一张图片。

  2. 利用本章的代码,通过粘贴匹配的图像创建照片马赛克,小块图像之间没有间隙。更艺术的表现形式,是在每个小块图像之间流出几个像素的均匀间隙。如何创建这样的间隙(提示:在计算最终的图像尺寸以及在 createImageGrid() 中粘贴时,考虑间隙的因素)?

  3. 程序的大部分时间,用于从输入文件夹中寻找小块图像的最佳匹配。为了加快程序, getBestMatchIndex() 需要运行得更快。这儿方法是对平均值(看成三维的点)列表进行简单的线性搜索。这个任务的一般问题就是最近邻居搜索。找到最近点有一种特别有效的方法,即 K-D 树搜索。 SciPy 库有一个方便的类 scipy.spatial.KDTree,可以创建 K-D 并向它查询最近点的匹配。请尝试用 SciPy 的 K-D 树替代线性搜索(参见http://docs.scipy.org/doc/scipy/reference/generated/ scipy.spatial.KDTree.html)


更多项目细节和完整代码,请点击阅读原文进入实验楼课程观看 :https://www.shiyanlou.com/courses/1041。学编程,敲一边胜过看10遍,欢迎大家来实验楼敲出这个项目~


 学习更多:

备受好评的 楼+「 Python实战 」、「 Linux运维与Devops实战 」正在优惠报名中——

实验楼CEO、CTO、高级工程师亲自上阵,通过直播、录播、全程助教、作业挑战等方式,带你12周内打通Python、Linux的任督二脉,成为拥有真正工作能力的IT工程师!

点击下面的链接了解详情:

楼+ Python实战·第6期 开战在即,超多福利等着你!

三个月打造全能的Linux运维工程师——「Linux运维与DevOps实战」


你可能感兴趣的:(如何用Python实现照片马赛克效果?)