用Python实现批量把照片转化成马赛克效果!

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

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

环境

Python

Xface 终端

实验原理

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

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

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

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

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

1. 分割目标图像

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

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

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

2 .均颜色值

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

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

3. 匹配图像

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

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

这里计算了点 (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

实验步骤

1 .读入小块图像

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

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

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

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

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

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

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

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

首先,使用 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() 计算出颜色平均值。

3. 将目标图像分割成网格

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

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

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

4 .寻找小块的最佳匹配

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

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

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

5. 创建图像网格

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

在创建图像网格之前,需要先用 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 给出。

6 .创建照片马赛克

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

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) 创建最终的照片马赛克。

7.添加命令行选项

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

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

8 .控制照片马赛克的大小

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

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

9. 运行照片马赛克程序

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

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

$ firefox test-data/a.jpg # 查看原图

$ firefox mosaic.png # 查看结果图片

原图

结果

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181028A1EF4A00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券