使用python创造照片马赛克

  • 一.实验介绍
    • 1.1 实验内容
    • 1.2 实验来源
    • 1.3 实验知识点
    • 1.4 实验环境
  • 二、实验原理
    • 2.1 分割目标图像
    • 2.2 平均颜色值
    • 2.3匹配图像
  • 三、开发准备
    • 3.1 CIFAR10图片数据库下载和图片提取
      • 3.1.1 CIFAR简介
      • 3.1.2 下载解压图片数据集
      • 3.1.3 提取图片
    • 3.2 安装运行代码必须库
  • 四、项目文件结构
  • 五、实验步骤
    • 5.1 读入小块图像
    • 5.2 计算输入图像的平均颜色值
    • 5.3 将目标图像分割成网格
    • 5.4 寻找小块的最佳匹配
    • 5.5 创建图像网格
    • 5.6 创建照片马赛克
    • 5.7 添加命令行选项
    • 5.8 控制照片马赛克的大小
    • 5.9 完整代码
    • 5.10 运行照片马赛克程序

一.实验介绍


上图是一张照片马赛克,它被分割成长方形的网格,每个长方形中是另一幅图像(最终希望出现在照片马赛克中的图像)替代。换言之,如果从远处看照片马赛克,会看到目标图像;但如果走近,会看到该图像实际上包含许多较小的图像

1.1 实验内容

本课程中,我们将学习如何使用 Python 创建照片马赛克。我们将目标图像划分成较小图像的网格,并用适当的图像替换网格中的每一小块,创建原始图像的照片马赛克。你可以指定网格的尺寸,并选择输入图像是否可以在马赛克中重复使用。

1.2 实验来源

本实验源自实验楼,在此基础上略有改变,目的是使之完全适用于pycharm环境。

1.3 实验知识点

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

  • 计算图像的平均 RGB 值;

  • 剪切图像;

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

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

1.4 实验环境

  • Python 3.8
  • pycharm
  • Pillow
  • numpy

二、实验原理

要创建照片马赛克,就从目标图像的块状低分辨率开始(因为在高分辨率的图像中,小块图像的数量会太大)。该图像的分辨率将决定马赛克的维度 :

M∗N(M 是行数,N 是列数)
接着,根据这种方法替换原始图像中的每一小块:

  1. 读入一些小块图像,他们将取代原始图像中的小块;
  2. 读入目标图像,将他们分割成 M∗N的小块网格;
  3. 对于目标图像中的每个小块,从输入的小块图像中找到最佳匹配;
  4. 将选择的输入图像安排在 M∗N的网格中,创建最终的照片马赛克。

2.1 分割目标图像

按照下图中的方案,开始将目标图像划分成 M∗N的网格。

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

注意,上图中 i 和 j 的取值范围应该分别是“0到n列”和“0到m行”。

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

2.2 平均颜色值

图像中的每个像素都有颜色,由它的红、绿、蓝值来表示。在这个例子中,使用 8 位的图像,因此每个部分都有 8 位值,范围在 [0,255]。如果一副图像共有 N 个像素,平均 RGB 计算如下:
图像中的每个像素都有颜色,由它的红、绿、蓝值来表示。在这个例子中,使用 8 位的图像,因此每个部分都有 8 位值,范围在 [0,255]。如果一副图像共有 N 个像素,平均 RGB 计算如下:
(r,g,b)‾=(r1+r2+...+rNN,g1+g2+...+gNN,b1+b2+...+bNN)\overline{(r,g,b)}=(\frac{r_1+r_2+...+r_N}N,\frac{g_1+g_2+...+g_N}N,\frac{b_1+b_2+...+b_N}N) (r,g,b)​=(Nr1​+r2​+...+rN​​,Ng1​+g2​+...+gN​​,Nb1​+b2​+...+bN​​)
请注意,平均 RGB 也是一个三元组,不是标量或一个数字,因为平均值是针对每个颜色成分分别计算的。计算平均 RGB 是为了匹配图像小块和目标图像。

2.3匹配图像

对于目标图像中的每个小块,需要在用户指定的输入文件夹下的图像中找到一幅匹配的图像。要确定两个图像是否匹配,可以通过比较平均 RGB 值,最匹配的图像就是平均 RGB 值最接近的图像。
要做到这一点,最简单的方法是计算一个像素中 RGB 值之间的距离,以便从输入图像中找到最佳匹配。对于几何中的三维点,可以用以下的距离计算方法
D1,2=(r1−r2)2+(g1−g2)2+(b1−b2)2D_{1,2}=\sqrt{(r_1-r_2)^2+(g_1-g_2)^2+(b_1-b_2)^2} D1,2​=(r1​−r2​)2+(g1​−g2​)2+(b1​−b2​)2​
这里计算了点 (r1,g1,b1)(r_1,g_1,b_1)(r1​,g1​,b1​)

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

三、开发准备

3.1 CIFAR10图片数据库下载和图片提取

3.1.1 CIFAR简介

CIFAR是由加拿大先进技术研究院的AlexKrizhevsky, Vinod Nair和Geoffrey Hinton收集而成的80百万小图片数据集。包含CIFAR-10和CIFAR-100两个数据集。 Cifar-10由60000张32*32的RGB彩色图片构成,共10个分类。50000张训练,10000张测试(交叉验证)。这个数据集最大的特点在于将识别迁移到了普适物体,而且应用于多分类。CIFAR-100由60000张图像构成,包含100个类别,每个类别600张图像,其中500张用于训练,100张用于测试。其中这100个类别又组成了20个大的类别,每个图像包含小类别和大类别两个标签。官网提供了Matlab,C,python三个版本的数据格式。图像如下图所示:

3.1.2 下载解压图片数据集

下载链接为:http://www.cs.toronto.edu/~kriz/cifar.html
点击下载python版本,下载完成后会获得一个压缩文件,把文件解压后获得包含下图文件的文件夹

3.1.3 提取图片

我们需要使用python代码将图片提取出来:

# -*- coding:utf-8 -*-import pickleimport numpy as npimport matplotlib.pyplot as pltfrom PIL import Imageimport osclassification = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']images_dir = "./images/"test_dir = "./test/"images_batch = "./cifar-10-batches-py/data_batch_"test_batch = "./cifar-10-batches-py/test_batch"def load_images(save_path, path_batch, num=1):data = []labels = []print("path_batch", path_batch)with open(path_batch, mode='rb') as file:data_dict = pickle.load(file, encoding='bytes')data += list(data_dict[b'data'])labels += list(data_dict[b'labels'])img = np.reshape(data, [-1, 3, 32, 32])if not os.path.exists(save_path):os.mkdir(save_path)for i in range(img.shape[0]):r = img[i][0]g = img[i][1]b = img[i][2]ir = Image.fromarray(r)ig = Image.fromarray(g)ib = Image.fromarray(b)rgb = Image.merge("RGB", (ir, ig, ib))if i < 10:  # 只显示前10张图片plt.imshow(rgb)plt.axis('off')  # 不显示坐标轴plt.show()label = classification[labels[i]]save_dir = save_path + label + "/"if not os.path.exists(save_dir):os.mkdir(save_dir)name = label + "_img-" + str(i) + ".png"rgb.save(save_dir + name, "PNG")  # 文件夹下是RGB融合后的图像if __name__ == "__main__":# imagesprint("正在保存images图片:")for i in range(5):path = images_batch + str(i + 1)load_images(images_dir, path)print("保存images完毕.")# testprint("正在保存test图片:")load_images(test_dir, test_batch)print("保存test完毕.")

提取完成后,会出现images和test文件夹,里面包含大量可直接查看的图片。这里以images/airplane/举例:

3.2 安装运行代码必须库

使用pip install在pycharm的Terminal中安装本项目所需的Pillow(其中包含PIL)和numpy这两个库:

# 使用 pip 安装 Pillow 和 numpy
$ pip install Pillow, numpy

四、项目文件结构

本实验包括一个 .py 文件和若干图片组成,所有的图片都放置在 D: \pycharm\pycharm scripts\ mosaic\文件夹下,把目标图像放置在images/文件夹下,本代码使用的小块图像是images/dog/,当然,读者也可自行选择别的小块图像,只需要在代码中修改下输入参数即可。

五、实验步骤

本节将通过实践操作,带领大家实现使用 python 创建照片马赛克。

5.1 读入小块图像

在 D:\pycharm\pycharm scripts\mosaic 文件夹下新建 main.py文件,并写入如下代码:
首先,从给定的文件夹中读取小块图像:

def getImages(imageDir):"""从给定目录里加载所有替换图像@param {str} imageDir 目录路径@return {List[Image]} 替换图像列表"""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:# 加载某个图像识别,直接跳过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):"""计算图像的平均 RGB 值将图像包含的每个像素点的 R、G、B 值分别累加,然后除以像素点数,就得到图像的平均 R、G、B值@param {Image} image PIL Image 对象@return {Tuple[int, int, int]} 平均 RGB 值"""# 计算像素点数npixels = image.size[0] * image.size[1]# 获得图像包含的每种颜色及其计数,结果类似# [(c1, (r1, g1, b1)), (c2, (r2, g2, b2)), ...]cols = image.getcolors(npixels)# 获得每种颜色的 R、G、B 累加值,结果类似# [(c1 * r1, c1 * g1, c1 * b1), (c2 * r2, c2 * g2, c2 * b2), ...]sumRGB = [(x[0] * x[1][0], x[0] * x[1][1], x[0] * x[1][2]) for x in cols]# 先用 zip 方法对 sumRGB 列表里的元组对象按列进行合并,结果类似# [(c1 * r1, c2 * r2, ...), (c1 * g1, c2 * g2, ...),# (c1 * b1, c2 * b2, ...)]# 然后计算所有颜色的 R、G、B 平均值,算法为# (sum(ci * ri) / np, sum(ci * gi) / np, sum(ci * bi) / np)avg = tuple([int(sum(x) / npixels) for x in zip(*sumRGB)])return avg

首先得到图像中包含的每种颜色及其计数,然后累加得到每种颜色的累加值,最后将所有不同颜色的累加值汇总后求平均即可。

5.3 将目标图像分割成网格

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

def splitImage(image, size):"""将图像按网格划分成多个小图像@param {Image} image PIL Image 对象@param {Tuple[int, int]} size 网格的行数和列数@return {List[Image]} 小图像列表"""W, H = image.size[0], image.size[1]m, n = sizew, h = int(W / n), int(H / m)imgs = []# 先按行再按列裁剪出 m * n 个小图像for j in range(m):for i in range(n):# 坐标原点在图像左上角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):"""找出颜色值最接近的索引把颜色值看做三维空间里的一个点,依次计算目标点跟列表里每个点在三维空间里的距离,从而得到距离最近的那个点的索引。@param {Tuple[int, int, int]} input_avg 目标颜色值@param {List[Tuple[int, int, int]]} avgs 要搜索的颜色值列表@return {int} 命中元素的索引"""index = 0min_index = 0min_dist = float("inf")for val in avgs:# 三维空间两点距离计算公式 (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)# + (z1 - z2) * (z1 - z2),这里只需要比较大小,所以无需求平方根值dist = ((val[0] - input_avg[0]) * (val[0] - input_avg[0]) +(val[1] - input_avg[1]) * (val[1] - input_avg[1]) +(val[2] - input_avg[2]) * (val[2] - input_avg[2]))if dist < min_dist:min_dist = distmin_index = indexindex += 1return 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∗NM*NM∗N的图像网格,往这个网格中填入小块图像,就可以创建出照片马赛克。

def createImageGrid(images, dims):"""将图像列表里的小图像按先行后列的顺序拼接为一个大图像@param {List[Image]} images 小图像列表@param {Tuple[int, int]} dims 大图像的行数和列数@return Image 拼接得到的大图像"""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+colN*row+colN∗row+col 给出,其中 NNN是一行的小块数,(row,col)(row,col)(row,col)是在该网格中的坐标。行和列的分别由 row=int(index/n) 和 col=index-n*row 给出。

5.6 创建照片马赛克

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

def createPhotomosaic(target_image, input_images, grid_size,reuse_images=True):"""图片马赛克生成@param {Image} target_image 目标图像@param {List[Image]} input_images 替换图像列表@param {Tuple[int, int]} grid_size 网格行数和列数@param {bool} reuse_images 是否允许重复使用替换图像@return {Image} 马赛克图像"""# 将目标图像切成网格小图像print('splitting input image...')target_images = splitImage(target_image, grid_size)# 为每个网格小图像在替换图像列表里找到颜色最相似的替换图像print('finding image matches...')output_images = []# 分 10 组进行,每组完成后打印进度信息,避免用户长时间等待count = 0batch_size = int(len(target_images) / 10)# 计算替换图像列表里每个图像的颜色平均值avgs = []for img in input_images:avgs.append(getAverageRGB(img))# 对每个网格小图像,从替换图像列表找到颜色最相似的那个,添加到 output_images 里for img in target_images:# 计算颜色平均值avg = getAverageRGB(img)# 找到最匹配的那个小图像,添加到 output_images 里match_index = getBestMatchIndex(avg, avgs)output_images.append(input_images[match_index])# 如果完成了一组,打印进度信息if count > 0 and batch_size > 10 and count % batch_size == 0:print('processed %d of %d...' % (count, len(target_images)))count += 1# 如果不允许重用替换图像,则用过后就从列表里移除if not reuse_images:input_images.remove(match)# 将 output_images 里的图像按网格大小拼接成一个大图像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('resizing images...')dims = (int(target_image.size[0] / grid_size[1]),int(target_image.size[1] / grid_size[0]))for img in input_images:img.thumbnail(dims)

5.9 完整代码

"""
使用 Python 创建照片马赛克输入一张目标照片和多张替换照片,将目标照片按网格划分为许多小方块,然后将每个小方块替换为颜色值最
接近的那张替换照片,就形成了马赛克效果。
"""import argparse
import osimport numpy as np
from PIL import Imagedef splitImage(image, size):"""将图像按网格划分成多个小图像@param {Image} image PIL Image 对象@param {Tuple[int, int]} size 网格的行数和列数@return {List[Image]} 小图像列表"""W, H = image.size[0], image.size[1]m, n = sizew, h = int(W / n), int(H / m)imgs = []# 先按行再按列裁剪出 m * n 个小图像for j in range(m):for i in range(n):# 坐标原点在图像左上角imgs.append(image.crop((i * w, j * h, (i + 1) * w, (j + 1) * h)))return imgsdef getImages(imageDir):"""从给定目录里加载所有替换图像@param {str} imageDir 目录路径@return {List[Image]} 替换图像列表"""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:# 加载某个图像识别,直接跳过print("Invalid image: %s" % (filePath,))return imagesdef getAverageRGB(image):"""计算图像的平均 RGB 值将图像包含的每个像素点的 R、G、B 值分别累加,然后除以像素点数,就得到图像的平均 R、G、B值@param {Image} image PIL Image 对象@return {Tuple[int, int, int]} 平均 RGB 值"""# 计算像素点数npixels = image.size[0] * image.size[1]# 获得图像包含的每种颜色及其计数,结果类似 [(cnt1, (r1, g1, b1)), ...]cols = image.getcolors(npixels)# 获得每种颜色的 R、G、B 累加值,结果类似 [(c1 * r1, c1 * g1, c1 * g2), ...]sumRGB = [(x[0] * x[1][0], x[0] * x[1][1], x[0] * x[1][2]) for x in cols]# 分别计算所有颜色的 R、G、B 平均值,算法类似(sum(ci * ri)/np, sum(ci * gi)/np,# sum(ci * bi)/np)# zip 的结果类似[(c1 * r1, c2 * r2, ..), (c1 * g1, c1 * g2, ...), (c1 * b1,# c1 * b2, ...)]avg = tuple([int(sum(x) / npixels) for x in zip(*sumRGB)])return avgdef getAverageRGBNumpy(image):"""计算图像的平均 RGB 值,使用 numpy 来计算以提升性能@param {Image} image PIL Image 对象@return {Tuple[int, int, int]} 平均 RGB 值"""# 将 PIL Image 对象转换为 numpy 数据数组im = np.array(image)# 获得图像的宽、高和深度w, h, d = im.shape# 将数据数组变形并计算平均值return tuple(np.average(im.reshape(w * h, d), axis=0))def getBestMatchIndex(input_avg, avgs):"""找出颜色值最接近的索引把颜色值看做三维空间里的一个点,依次计算目标点跟列表里每个点在三维空间里的距离,从而得到距离最近的那个点的索引。@param {Tuple[int, int, int]} input_avg 目标颜色值@param {List[Tuple[int, int, int]]} avgs 要搜索的颜色值列表@return {int} 命中元素的索引"""index = 0min_index = 0min_dist = float("inf")for val in avgs:# 三维空间两点距离计算公式 (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)# + (z1 - z2) * (z1 - z2),这里只需要比较大小,所以无需求平方根值dist = ((val[0] - input_avg[0]) * (val[0] - input_avg[0]) +(val[1] - input_avg[1]) * (val[1] - input_avg[1]) +(val[2] - input_avg[2]) * (val[2] - input_avg[2]))if dist < min_dist:min_dist = distmin_index = indexindex += 1return min_indexdef createImageGrid(images, dims):"""将图像列表里的小图像按先行后列的顺序拼接为一个大图像@param {List[Image]} images 小图像列表@param {Tuple[int, int]} dims 大图像的行数和列数@return Image 拼接得到的大图像"""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_imgdef createPhotomosaic(target_image, input_images, grid_size,reuse_images=True):"""图片马赛克生成@param {Image} target_image 目标图像@param {List[Image]} input_images 替换图像列表@param {Tuple[int, int]} grid_size 网格行数和列数@param {bool} reuse_images 是否允许重复使用替换图像@return {Image} 马赛克图像"""# 将目标图像切成网格小图像print('splitting input image...')target_images = splitImage(target_image, grid_size)# 为每个网格小图像在替换图像列表里找到颜色最相似的替换图像print('finding image matches...')output_images = []# 分 10 组进行,每组完成后打印进度信息,避免用户长时间等待count = 0batch_size = int(len(target_images) / 10)# 计算替换图像列表里每个图像的颜色平均值avgs = []for img in input_images:avgs.append(getAverageRGB(img))# 对每个网格小图像,从替换图像列表找到颜色最相似的那个,添加到 output_images 里for img in target_images:# 计算颜色平均值avg = getAverageRGB(img)# 找到最匹配的那个小图像,添加到 output_images 里match_index = getBestMatchIndex(avg, avgs)output_images.append(input_images[match_index])# 如果完成了一组,打印进度信息if count > 0 and batch_size > 10 and count % batch_size == 0:print('processed %d of %d...' % (count, len(target_images)))count += 1# 如果不允许重用替换图像,则用过后就从列表里移除if not reuse_images:input_images.remove(match)# 将 output_images 里的图像按网格大小拼接成一个大图像print('creating mosaic...')mosaic_image = createImageGrid(output_images, grid_size)return mosaic_imagedef 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()# 网格大小grid_size = (int(args.grid_size[0]), int(args.grid_size[1]))# 马赛克图像保存路径,默认为 mosaic.pngoutput_filename = 'mosaic.png'if args.outfile:output_filename = args.outfile# 打开目标图像print('reading targe image...')target_image = Image.open(args.target_image)# 从指定文件夹下加载所有替换图像print('reading input images...')input_images = getImages(args.input_folder)# 如果替换图像列表为空则退出程序if input_images == []:print('No input images found in %s. Exiting.' % (args.input_folder, ))exit()# 将所有替换图像缩放到指定的网格大小print('resizing images...')dims = (int(target_image.size[0] / grid_size[1]),int(target_image.size[1] / grid_size[0]))for img in input_images:img.thumbnail(dims)# 生成马赛克图像print('starting photomosaic creation...')mosaic_image = createPhotomosaic(target_image, input_images, grid_size)# 保存马赛克图像mosaic_image.save(output_filename, 'PNG')print("saved output to %s" % (output_filename,))print('done.')if __name__ == '__main__':main()

5.10 运行照片马赛克程序

在 D:\pycharm\pycharm scripts\mosaic 文件夹下执行命令:

(运行参数需要根据自己实际图片稍作修改,否则可能会出现找不到图片的情况)

shiyanlou:Code/ $ python3 main.py --target-image images/xiaoni.jpg --input-folder images/dog/ --grid-size 128 96
reading targe image...
reading input images...
resizing images...
starting photomosaic creation...
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.

原图:

结果:

使用python创造照片马赛克相关推荐

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

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

  2. Python小项目—照片马赛克

    项目内容 该项目用Python创建照片马赛克,将目标图像划分成较小图像的网络,并用适当的图像替换网络中的每一小块,创建原始图像的照片的马赛克 项目知识点 用Python图像库(PIL)创建图像 计算图 ...

  3. python图片马赛克_利用Python来打马赛克!少儿不宜的东西永不再有!就是这么牛逼!...

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

  4. qpython获取手机gps_基于Python获取照片的GPS位置信息

    这篇文章主要介绍了基于Python获取照片的GPS位置信息,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 昨天听人说,用手机拍照会带着GPS信息,原 ...

  5. opengl实现三维动画简单代码_使用Python简单实现马赛克拼图!内附完整代码

    今天小编带大家使用python简单实现马赛克拼图,内容比以往会稍长一些,各位看官老爷可以慢慢细读,若有不足之处还望请斧正,闲话不多说,请看文章. 先看原图: 效果图: 思路: 拼图的原理其实很简单,就 ...

  6. python 获取照片拍摄时间_Python实现获取照片拍摄日期并重命名的方法

    本文实例讲述了Python实现获取照片拍摄日期并重命名的方法.分享给大家供大家参考,具体如下: python获取照片的拍摄日期并重命名.不支持重复处理的中断. 重命名为:拍摄日期__原文件名 impo ...

  7. python换照片底色_详解Python给照片换底色(蓝底换红底)

    现在网上出现了很多在线换底色的网页版工具是这么做的呢?其实用Python就可以实现. 环境要求 Python3 numpy函数库 opencv库 安装 下载适应版本的numpy函数库,我电脑是WIN1 ...

  8. python去除图片马赛克_python 检测图片是否有马赛克

    首先是canny边缘检测,将图片的边缘检测出来,参考博客 原理讲的很清晰,给原博主一个赞 边缘检测之后按照正方形检索来判定是否是马赛克内容 原理知晓了之后就很好做了 话说matlab转化为python ...

  9. 用Python获取照片GPS信息

    用Python获取照片GPS信息 注意事项: 1.调用了百度地图的接口. 2.能够获取信息的照片是本身就带有这些信息的,只不过我们把它查出来了而已. 3.如果是微信接收到的非原图的照片.拍摄时没开定位 ...

最新文章

  1. 【Nginx】面试官:给我讲讲Nginx如何实现四层负载均衡?
  2. swift UI专项训练39 用Swift实现摇一摇功能
  3. Serverless 风起云涌,为什么阿里,微软,AWS 却开始折腾 OAM?
  4. Python 根据地址获取经纬度及求距离
  5. Nest Secure智能保全系统内建麦克风 引发用户反弹
  6. PCB 电子线路板制作流程
  7. SAP ui5 ABAP repository handler class的 get_webcontent方法
  8. 直接插入排序,折半插入排序,希尔排序,简单选择排序,冒泡排序,快速排序模板以及比较次数与移动次数的分析,折半搜索算法模板
  9. Mybatis各种模糊查询及#和$区别
  10. ajax data参数
  11. 在 VM的CentOS 中 安装 sspanel 宝塔面板 总结
  12. 傅里叶变换 相位谱 幅度谱
  13. pandas 中 rank 的用法
  14. putty连接服务器显示连接超时,putty连接云服务器超时连接
  15. 利用函数求出两个数的最大值
  16. 苹果审核返回崩溃日志 iOS .crash文件处理 symbolicatecrash
  17. 台灣人如何在大陸工作
  18. 计算机应用基础试题操作题,计算机应用基础期末考试操作题.doc
  19. Ryzen 5 5600G windows 10 企业版 - 高温 BUG
  20. librosa 语音库(三) librosa.feature. 中的 spectrogram 与 melspectrogram

热门文章

  1. 将url网址转换成对象
  2. ListBox美化重绘,不积硅步无以至千里
  3. CVPR2018——以属性为指导的无监督行人重识别
  4. 什么是哈希?哈希的模拟实现
  5. 开源OA协同办公平台搭建教程:开源O2OA中log4j2使用配置
  6. 局域网内计算机时间同步
  7. 浅谈晶振作用功能、晶体和晶振、外部时钟和内部时钟、分频倍频预分频后分频、定时器和计数器
  8. 面试官:说说微信小程序的登录流程?
  9. 夸计算机老师的成语,四字夸奖老师的成语
  10. nslookup 查邮件服务器地址