文章目录(Table of Contents)
简介
有的时候我们会在网上看到类似下面的图片, 下面的图片来自淘宝店, 颜之希旗舰店. 通过若干张图片, 制作一个马赛克图片的效果.
这一篇我们来介绍一下如何使用python来实现这样的效果, 使用Python来实现马赛克图片. 这一篇主要参考自基于 hsv 的马赛克拼图效果. 这一部分的文章会分为两个部分进行介绍:
- 我会首先介绍RGB和HSV颜色空间, 我们会基于HSV颜色空间来比较图像的相似度.
- 接着给出详细的如何使用python制作马赛克图片的教程.
参考资料
- 基本是基于这一篇做的修改, 基于 hsv 的马赛克拼图效果
- 一些Pillow图像处理的基础, Python中Pillow使用介绍
- 关于RGB与HSV颜色空间的介绍, RGB、HSV和HSL颜色空间
- 维基百科, HSL和HSV色彩空间
- 所有的代码, 可以参考github链接, Python制作马赛克图片
RGB与HSV颜色空间
RGB颜色空间
RGB是我们接触最多的颜色空间, 分别为红色(R), 绿色(G)和蓝色(B). 这三种颜色的不同组合可以形成几乎所有的其他颜色.
但是在自然环境下, 图像容易受自然光照, 遮挡和阴影等情况的影响, 即对亮度比较敏感. 但是RGB颜色空间的三个向量都与亮度相关, 即如果我们要改变亮度, 这三个量都需要进行改变.
同时, 人眼对于RGB这三种颜色的敏感度是不一样的. 在单色中, 人眼对红色最不敏感, 蓝色最敏感. 所以 RGB 颜色空间是一种均匀性较差的颜色空间.
如果我们在RGB的颜色空间上, 直接使用欧式距离来度量颜色的相似度, 最终的结果会与人眼的结果有较大的差距.
HSV颜色空间
正是由于上面讲到的RGB空间的一些问题, 于是在图像处理的时候我们会更多的使用HSV颜色空间.
HSV以人类更熟悉的方式封装了关于颜色的信息:“这是什么颜色?深浅如何?明暗如何?”
HSV 表达彩色图像的方式由三个部分组成:
- Hue (色调, 色相)
- Saturation (饱和度, 色彩纯净度)
- Value (明度)
通常我们会使用下面这个圆柱体来表示 HSV 颜色空间. 圆柱体的横截面可以看做是一个极坐标系, H (Hue) 用极坐标的极角表示, S (Saturation) 用极坐标的极轴长度表示, V (Value) 用圆柱中轴的高度表示.
颜色圆环上所有的颜色都是光谱上的颜色, 从红色开始按逆时针方向旋转, Hue=0 表示红色, Hue=120表示绿色, Hue=240 表示蓝色等等.
在RGB中, 颜色由三个值共同决定, 比如黄色为即 (255,255,0); 在HSV中, 黄色只由一个值决定, Hue=60即可.
在确定了Hue之后, 我们可以更改其Saturation和Value. 如下所示:
- Saturation (饱和度): 其中水平方向表示饱和度, 饱和度表示颜色接近光谱色的程度. 饱和度越高, 说明颜色越深, 越接近光谱色; 饱和度越低, 说明颜色越浅, 越接近白色. 饱和度为0表示纯白色. 取值范围为0~100, 值越大, 颜色越饱和. 可以理解为, 饱和度减少, 就是将颜色与白色混合, 饱和度为0, 就全部是白色.
- Value (明度): 竖直方向表示明度, 决定颜色空间中颜色的明暗程度. 明度越高, 表示颜色越明亮, 范围是 0-100. 明度为0表示纯黑色 (此时颜色最暗). 可以理解为, 明度减少, 就是将颜色与黑色进行混合, 明度是0, 就是全黑的.
我们可以在网站, RGB 轉換 HSV 及 HSL, 进行颜色的在线转换测试. 查看每一个分量的含义. 下面如果把value设置为0, 就是全黑的了.
RGB与HSV空间的转换
下面看一下如何在RGB和HSV两个颜色空间之间相互转换. 首先是从RGB到HSV空间的转换. 其中max和min分别是r, g, b三色中最大值和最小值. 这里r, g, b是在0-1之间的数字.
下面是一个比较完整的从RGB颜色空间计算到HSV颜色空间的例子.
接下来看一下如何从HSV转换到RGB空间.这里h的范围是0-360, s和v的范围是0-1.
我们举一个例子来具体说明一下. 对于下图中的第二个颜色, 我们来进行测试.
我们使用下面的代码来进行RGB和HSV两个颜色空间的相互转换.
- from colorsys import rgb_to_hsv, hsv_to_rgb
首先是从rgb空间转换为hsv的颜色空间. 这里0.333就是120/360.
接着是从hsv空间转换为rgb空间. 同样, 再输入h范围的时候是要除360, 使其范围在0-1之间.
马赛克图片制作
整个马赛克图片制作的流程如下所示:
- 首先生成素材数据库, 我们将素材图片转换为统一大小, 并计算他的平均HSV值.
- 接着对需要转换的图片切分为一小块一小块, 对每一块计算他的评价HSV值, 并与素材数据库进行对比, 找出最接近的素材进行替换.
- 对原图的每一个小块进行替换, 生成一个大图.
- 将生成的大图与原始图片重合, 融合, 生成最后的图片.
进行准备-导入需要的库
首先我们导入整个实验中需要用到的库.
- import os
- from PIL import Image, ImageOps
- import time
- from multiprocessing import Pool
- import random
- import math
- import sys
- from colorsys import rgb_to_hsv, hsv_to_rgb
计算图像颜色-平均hsv值
首先, 我们定义一个基类mosaic, 其中包含两个函数:
- 一个是对图像进行resize的操作;
- 另一个是计算一个图像的平均hsv值, 之后每一个图像相当于使用一个三个特征的向量进行表示, 我们会通过这个特征来计算图像之间的相似度.
- class mosaic(object):
- """定义计算图片的平均hsv值
- """
- def __init__(self, IN_DIR, OUT_DIR, SLICE_SIZE, REPATE, OUT_SIZE):
- self.IN_DIR = IN_DIR # 原始的图像素材所在文件夹
- self.OUT_DIR = OUT_DIR # 输出素材的文件夹, 这些都是计算过hsv和经过resize之后的图像
- self.SLICE_SIZE = SLICE_SIZE # 图像放缩后的大小
- self.REPATE = REPATE # 同一张图片可以重复使用的次数
- self.OUT_SIZE = OUT_SIZE # 最终图片输出的大小
- def resize_pic(self, in_name, size):
- """转换图像大小
- """
- img = Image.open(in_name)
- img = ImageOps.fit(img, (size, size), Image.ANTIALIAS)
- return img
- def get_avg_color(self, img):
- """计算图像的平均hsv
- """
- width, height = img.size
- pixels = img.load()
- if type(pixels) is not int:
- data = [] # 存储图像像素的值
- for x in range(width):
- for y in range(height):
- cpixel = pixels[x, y] # 获得每一个像素的值
- data.append(cpixel)
- h = 0
- s = 0
- v = 0
- count = 0
- for x in range(len(data)):
- r = data[x][0]
- g = data[x][1]
- b = data[x][2] # 得到一个点的GRB三色
- count += 1
- hsv = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
- h += hsv[0]
- s += hsv[1]
- v += hsv[2]
- hAvg = round(h / count,3)
- sAvg = round(s / count,3)
- vAvg = round(v / count,3)
- if count > 0: # 像素点的个数大于0
- return (hAvg, sAvg, vAvg)
- else:
- raise IOError("读取图片数据失败")
- else:
- raise IOError("PIL 读取图片数据失败")
简单说一下如何计算一个图像的平均hsv值:
- 首先遍历图像的每一个像素点, 获得每一个点的rgb色.
- 接着使用函数rgb_to_hsv将rgb色转换为hsv的颜色.
- 最后计算hsv的平均值, 此时一个图像就使用了一个三维的向量表示了.
生成素材数据库
为了生成马赛克图片, 我们需要准备一些图像. 我们将图片放在images
文件夹内. 接着我们对图片进行大小的转换和计算图像的平均hsv颜色. 整体的代码如下所示:
- class create_image_db(mosaic):
- """创建所需要的数据
- """
- def __init__(self, IN_DIR, OUT_DIR, SLICE_SIZE, REPATE, OUT_SIZE):
- super(create_image_db, self).__init__(IN_DIR, OUT_DIR, SLICE_SIZE, REPATE, OUT_SIZE)
- def get_image_paths(self):
- """获取文件夹内图像的地址
- """
- paths = []
- suffixs = ['png','jpg']
- for file_ in os.listdir(self.IN_DIR):
- suffix = file_.split('.', 1)[1] # 获得文件后缀
- if suffix in suffixs: # 通过后缀判断是否是图片
- paths.append(self.IN_DIR + file_) # 添加图像路径
- else:
- print("非图片:%s" % file_)
- if len(paths) > 0:
- print("一共找到了%s" % len(paths) + "张图片")
- else:
- raise IOError("未找到任何图片")
- return paths
- def convert_image(self, path):
- """转换图像大小, 同时计算一个图像的平均hsv值.
- """
- img = self.resize_pic(path, self.SLICE_SIZE)
- color = self.get_avg_color(img)
- img.save(str(self.OUT_DIR) + str(color) + ".png")
- def convert_all_images(self):
- """将所有图像进行转换
- """
- paths = self.get_image_paths()
- print("正在生成马赛克块...")
- pool = Pool() # 多进程处理
- pool.map(self.convert_image, paths) # 对已有的图像进行处理, 转换为对应的色块
- pool.close()
- pool.join()
上面的整体流程如下:
- 首先遍历素材的文件夹, 获得所有image的地址;
- 对每一个image, 我们首先将其转换为正方形的指定大小的图像, 接着计算这个图像的平均hsv颜色, 并用这个颜色值对图像进行命名.
最后会在一个文件夹内生成如下所示的图像, 后面我们就会使用这些图像来进行马赛克图片的拼接.
生成马赛克图片
在有了上面的素材准备之后, 我们可以开始生成马赛克图片了. 有下面的几个步骤:
- 首先读取素材文件夹内的所有图片, 并将他们的颜色保存在
img_db
里面. - 接着将原始图像分成一个一个的小块, 计算小块的平均hsv颜色.
- 将这个颜色值与
img_db
里的颜色做对比, 使用欧式距离, 找出最近的那个颜色, 和对应的图像. - 将找到的图像依次拼接, 最后组成马赛克图片. 这里将拼接后的图像保存一次.
- 最后, 我们将拼接的图像与原始图像进行重叠, 这样可以使得效果好一些.
下面是关于生成马赛克图片的完整的代码.
- class create_mosaic(mosaic):
- """创建马赛克图片
- """
- def __init__(self, IN_DIR, OUT_DIR, SLICE_SIZE, REPATE, OUT_SIZE):
- super(create_mosaic, self).__init__(IN_DIR, OUT_DIR, SLICE_SIZE, REPATE,
- OUT_SIZE)
- def read_img_db(self):
- """读取所有的图片
- """
- img_db = [] # 存储color_list
- for file_ in os.listdir(self.OUT_DIR):
- if file_ == 'None.png':
- pass
- else:
- file_ = file_.split('.png')[0] # 获得文件名
- file_ = file_[1:-1].split(',') # 获得hsv三个值
- file_ = [float(i) for i in file_]
- file_.append(0) # 最后一位计算图像使用次数
- img_db.append(file_)
- return img_db
- def find_closiest(self, color, list_colors):
- """寻找与像素块颜色最接近的图像
- """
- FAR = 10000000
- for cur_color in list_colors: # list_color是图像库中所以图像的平均hsv颜色
- n_diff = np.sum((color - np.absolute(cur_color[:3]))**2)
- if cur_color[3] <= self.REPATE: # 同一个图片使用次数不能太多
- if n_diff < FAR: # 修改最接近的颜色
- FAR = n_diff
- cur_closer = cur_color
- cur_closer[3] += 1
- return "({}, {}, {})".format(cur_closer[0], cur_closer[1],
- cur_closer[2]) # 返回hsv颜色
- def make_puzzle(self, img):
- """制作拼图
- """
- img = self.resize_pic(img, self.OUT_SIZE) # 读取图片并修改大小
- color_list = self.read_img_db() # 获取所有的颜色的list
- width, height = img.size # 获得图片的宽度和高度
- print("Width = {}, Height = {}".format(width, height))
- background = Image.new('RGB', img.size,
- (255, 255, 255)) # 创建一个空白的背景, 之后向里面填充图片
- total_images = math.floor(
- (width * height) / (self.SLICE_SIZE * self.SLICE_SIZE)) # 需要多少小图片
- now_images = 0 # 用来计算完成度
- for y1 in range(0, height, self.SLICE_SIZE):
- for x1 in range(0, width, self.SLICE_SIZE):
- try:
- # 计算当前位置
- y2 = y1 + self.SLICE_SIZE
- x2 = x1 + self.SLICE_SIZE
- # 截取图像的一小块, 并计算平均hsv
- new_img = img.crop((x1, y1, x2, y2))
- color = self.get_avg_color(new_img)
- # 找到最相似颜色的照片
- close_img_name = self.find_closiest(color, color_list)
- close_img_name = self.OUT_DIR + str(
- close_img_name) + '.png' # 图片的地址
- paste_img = Image.open(close_img_name)
- # 计算完成度
- now_images += 1
- now_done = math.floor((now_images / total_images) * 100)
- r = '\r[{}{}]{}%'.format("#" * now_done,
- " " * (100 - now_done), now_done)
- sys.stdout.write(r)
- sys.stdout.flush()
- background.paste(paste_img, (x1, y1))
- except IOError:
- print('创建马赛克块失败')
- # 保持最后的结果
- background.save('out_without_background.jpg')
- img = Image.blend(background, img, 0.5)
- img.save('out_with_background.jpg')
- return True
整体代码结构
上面三个类的关系如下所示, 下面mosaic.create_mosaic少打了一个e, 我在上面修改了, 下面这张图暂时不修改了:
马赛克图片的效果
我们使用我在塞尔达里截的300张图片, 来作为素材图像, 来简单做一个示范. 整个文件夹的框架如下所示:
- ├
- ├─images (存储原始的图像数据)
- ├─outputImages (存储修改后的图像数据)
- ├─mosaic.py (主文件)
其中images中存储的是我们原始的截图, 之后会使用上面的create_image_db
来对原始的素材图像进行大小的变化, 和计算图像的平均hsv颜色.
- # 创建马赛克块, 创建素材库
- createdb = create_image_db(IN_DIR=os.path.join(filePath, 'images/'),
- OUT_DIR=os.path.join(filePath, 'outputImages/'),
- SLICE_SIZE=100,
- REPATE=20,
- OUT_SIZE=5000)
- createdb.convert_all_images()
之后我们根据上面创建的素材图像, 来创建马赛克拼图.
- # 创建拼图
- createM = create_mosaic(IN_DIR=os.path.join(filePath, 'images/'),
- OUT_DIR=os.path.join(filePath, 'outputImages/'),
- SLICE_SIZE=100,
- REPATE=20,
- OUT_SIZE=5000)
- out = createM.make_puzzle(img=os.path.join(filePath, 'Zelda.jpg'))
最后会生成两张图片, 第一张是没有与原始的图像进行重叠的, 纯使用小的照片拼接起来的. 这里因为图像素材比较少, 所以最后的马赛克图片的效果一般:
但是, 我们将其与原始的图像进行重合之后, 效果就会好上很多.
最后就可以得到上面的图像, 放大看就可以看到每一个小的图像, 是由每一个小的图像拼接而来.
- 微信公众号
- 关注微信公众号
- QQ群
- 我们的QQ群号
评论