这是原图:
拼图的原理其实很简单,就是把原图划分成很多个小块,然后根据灰度或者rgb搜索图库中最相似的图片进行替换。接下来的问题就是如何实现图片搜索。这里可以参考阮一峰的博客
上代码:
第一步:获取目标图片的尺寸,计算每个子图的大小。例如:目标图片的尺寸为1600x1280,计算出这个尺寸的最大公约数为320,即拼出的图片由每行每列都有320张小图组成,这样计算出的小图尺寸则为5x4。但这个尺寸太小,所以设置一个min_unit,用来确定最终的小图片的尺寸,若min_unit=5,则每张小图片的尺寸为25x20,相应的每行每列最终的图片数也会变化。(注:代码中的flag用来处理目标图片尺寸无法计算出合理的数值的情况,这时候需要自定义一个图片尺寸)
def divide_sub_im(self, width, height):
flag = True
g = self.gcd(width, height)
if g < 20:
flag = False
width = self.__default_w
height = self.__default_h
g = 320
self.__sub_width = self.__min_unit * (width // g)
self.__sub_height = self.__min_unit * (height // g)
return flag
# 辗转相除法求最大公约数
@staticmethod
def gcd(a, b):
while a % b:
a, b = b, a % b
return b
第二步:读取图库,参数分别是图库的路径和上一步确定的小图片的尺寸(长,宽)。根据这个尺寸对图库的所有图片进行resize,方便之后的图片填充
def read_all_img(self, db_path, fin_w, fin_h):
files_name = os.listdir(db_path)
n = 1
for file_name in files_name:
full_path = db_path + "\\" + file_name
if os.path.isfile(full_path):
print("开始读取第%d张图片" % n)
# threading.Thread(target=self.read_img, args=(full_path, fin_w, fin_h)).start()
cur = Image.open(full_path)
# 计算key值(灰度值,平均RGB,hash值,三选一)
key = self.cal_key(cur)
# 将素材缩放到目标大小
cur = cur.resize((fin_w, fin_h), Image.ANTIALIAS)
self.__all_img.update({key: cur})
n += 1
第三步:计算图库中每张图片的key值,这里实现了三种模式。1:基于图片灰度计算出来的key值。2:基于图片平均RGB计算出来的key值。(效果图使用这种方式)3:基于感知哈希算法(Perceptual hash algorithm)计算出来的key值。最后将每张图片计算出来的key和Image对象保存在dict中,这个key值用来找出最适合的子图。
def cal_key(self, im):
if self.__mode == "RGB":
return self.cal_avg_rgb(im)
elif self.__mode == "gray":
return self.cal_gray(im)
elif self.__mode == "hash":
return self.cal_hash(im)
else:
return ""
# 计算灰度值
@staticmethod
def cal_gray(im):
if im.mode != "L":
im = im.convert("L")
return reduce(lambda x, y: x + y, im.getdata()) // (im.size[0] * im.size[1])
# 计算平均rgb值
@staticmethod
def cal_avg_rgb(im):
if im.mode != "RGB":
im = im.convert("RGB")
pix = im.load()
avg_r, avg_g, avg_b = 0, 0, 0
n = 1
for i in range(im.size[0]):
for j in range(im.size[1]):
r, g, b = pix[i, j]
avg_r += r
avg_g += g
avg_b += b
n += 1
avg_r /= n
avg_g /= n
avg_b /= n
return str(avg_r) + "-" + str(avg_g) + "-" + str(avg_b)
# 计算pHash
def cal_hash(self, im):
im = im.resize((8, 8), Image.ANTIALIAS)
im = im.convert("L")
avg_gray = self.cal_gray(im)
k = ""
_0 = "0"
_1 = "1"
for i in im.getdata():
if i < avg_gray:
k += _0
else:
k += _1
return k
第四步:开始拼图。遍历整个大图,利用之前计算的子图尺寸将大图分为若干个小图,计算每个小图的key值,然后在图库中搜索最相似的图片,然后将图库中搜索的结果填充到新图片中。
def core(self, aim_im, width, height):
new_im = Image.new("RGB", (width, height))
# 每行每列的图片数
w = width // self.__sub_width
print("源文件尺寸为:(w:%d h:%d)" % (width, height))
print("子图的尺寸为:(w:%d h:%d)" % (self.__sub_width, self.__sub_height))
print("w:%d" % w)
print("开始拼图,请稍等...")
start = time.time()
n = 1
for i in range(w):
for j in range(w):
print("正在拼第%d张素材" % n)
left = i * self.__sub_width
up = j * self.__sub_height
right = (i + 1) * self.__sub_width
down = (j + 1) * self.__sub_height
box = (left, up, right, down)
cur_sub_im = aim_im.crop(box)
# 计算key值(灰度值,平均RGB,hash值,三选一)
cur_sub_key = self.cal_key(cur_sub_im)
# 搜索最匹配图片(灰度值,平均RGB,hash值,三选一)
fit_sub = self.find_key(cur_sub_key)
new_im.paste(fit_sub, box)
n += 1
print("拼图完成,共耗时%f秒" % (time.time() - start))
new_im.save(self.__out_path)
完整代码:
参数用途:
db_path:图库目录
aim_path:目标图片路径
out_path:生成的图片的输出路径
sub_width=64:子图的尺寸(默认64,可自己更改)
sub_height=64:
min_unit=2:可理解成粒度,值越小拼出的图片越精细,每个子图也越小
mode="RGB":拼图方式,默认RGB
default_w=1600:默认生成的图片尺寸,只在无法计算有效合理的最大公约数时有效
default_h=1280
import os
import time
from functools import reduce
from threading import Thread
from PIL import Image
class MosaicMaker(object):
# 内部类,执行多线程拼图的任务类
class __SubTask:
def __init__(self, n, cur_sub_im, new_im, m, box):
self.n = n
self.cur_sub_im = cur_sub_im
self.new_im = new_im
self.m = m
self.box = box
def work(self):
# print("正在拼第%d张素材" % self.n)
# 计算key值(灰度值,平均RGB,hash值,三选一)
cur_sub_key = self.m.cal_key(self.cur_sub_im)
# 搜索最匹配图片(灰度值,平均RGB,hash值,三选一)
fit_sub = self.m.find_key(cur_sub_key)
self.new_im.paste(fit_sub, self.box)
# 内部类,执行多线程读取图库的任务类
class __ReadTask:
def __init__(self, n, full_path, fin_w, fin_h, m):
self.n = n
self.full_path = full_path
self.fin_w = fin_w
self.fin_h = fin_h
self.m = m
def read(self):
print("开始读取第%d张图片" % self.n)
cur = Image.open(self.full_path)
# 计算key值(灰度值,平均RGB,hash值,三选一)
key = self.m.cal_key(cur)
# 将素材缩放到目标大小
cur = cur.resize((self.fin_w, self.fin_h), Image.ANTIALIAS)
self.m.get_all_img().update({key: cur})
# 图库目录 目标文件 输出路径 子图尺寸 最小像素单位 拼图模式 默认尺寸
def __init__(self, db_path, aim_path, out_path, sub_width=64, sub_height=64, min_unit=5, mode="RGB", default_w=1600,
default_h=1280):
self.__db_path = db_path
self.__aim_path = aim_path
self.__out_path = out_path
self.__sub_width = sub_width
self.__sub_height = sub_height
self.__min_unit = min_unit
self.__mode = mode
self.__default_w = default_w
self.__default_h = default_h
self.__all_img = dict()
# 对外提供的接口
def make(self):
aim_im = Image.open(self.__aim_path)
aim_width = aim_im.size[0]
aim_height = aim_im.size[1]
print("计算子图尺寸")
if not self.__divide_sub_im(aim_width, aim_height):
print("使用默认尺寸")
aim_im = aim_im.resize((self.__default_w, self.__default_h), Image.ANTIALIAS)
aim_width = aim_im.size[0]
aim_height = aim_im.size[1]
print("读取图库")
start = time.time()
self.__read_all_img(self.__db_path, self.__sub_width, self.__sub_height)
print("耗时:%f秒" % (time.time() - start))
self.__core(aim_im, aim_width, aim_height)
def __core(self, aim_im, width, height):
new_im = Image.new("RGB", (width, height))
# 每行每列的图片数
w = width // self.__sub_width
print("源文件尺寸为:(w:%d h:%d)" % (width, height))
print("子图的尺寸为:(w:%d h:%d)" % (self.__sub_width, self.__sub_height))
print("w:%d" % w)
print("开始拼图,请稍等...")
start = time.time()
n = 1
thread_list = list()
for i in range(w):
task_list = list()
for j in range(w):
# 多线程版
left = i * self.__sub_width
up = j * self.__sub_height
right = (i + 1) * self.__sub_width
down = (j + 1) * self.__sub_height
box = (left, up, right, down)
cur_sub_im = aim_im.crop(box)
t = self.__SubTask(n, cur_sub_im, new_im, self, box)
task_list.append(t)
n += 1
thread = Thread(target=self.__sub_mission, args=(task_list,))
thread_list.append(thread)
for t in thread_list:
t.start()
for t in thread_list:
t.join()
print("拼图完成,共耗时%f秒" % (time.time() - start))
# 将原图与拼图合并,提升观感
new_im = Image.blend(new_im, aim_im, 0.35)
new_im.show()
new_im.save(self.__out_path)
# 拼图库线程执行的具体函数
@staticmethod
def __sub_mission(missions):
for task in missions:
task.work()
# 计算子图大小
def __divide_sub_im(self, width, height):
flag = True
g = self.__gcd(width, height)
if g < 20:
flag = False
width = self.__default_w
height = self.__default_h
g = 320
if g == width:
g = 320
self.__sub_width = self.__min_unit * (width // g)
self.__sub_height = self.__min_unit * (height // g)
return flag
# 读取全部图片,按(灰度值,平均RGB,hash值)保存 fin_w,fin_h素材最终尺寸
def __read_all_img(self, db_path, fin_w, fin_h):
files_name = os.listdir(db_path)
n = 1
# 开启5个线程加载图片
ts = list()
for i in range(5):
ts.append(list())
for file_name in files_name:
full_path = db_path + "\\" + file_name
if os.path.isfile(full_path):
read_task = self.__ReadTask(n, full_path, fin_w, fin_h, self)
ts[n % 5].append(read_task)
n += 1
tmp = list()
for i in ts:
t = Thread(target=self.__read_img, args=(i,))
t.start()
tmp.append(t)
for t in tmp:
t.join()
# 读取图库线程执行的具体函数
@staticmethod
def __read_img(tasks):
for task in tasks:
task.read()
# 计算key值
def cal_key(self, im):
if self.__mode == "RGB":
return self.__cal_avg_rgb(im)
elif self.__mode == "gray":
return self.__cal_gray(im)
elif self.__mode == "hash":
return self.__cal_hash(im)
else:
return ""
# 获取key值
def find_key(self, im):
if self.__mode == "RGB":
return self.__find_by_rgb(im)
elif self.__mode == "gray":
return self.__find_by_gray(im)
elif self.__mode == "hash":
return self.__find_by_hash(im)
else:
return ""
# 计算灰度值
@staticmethod
def __cal_gray(im):
if im.mode != "L":
im = im.convert("L")
return reduce(lambda x, y: x + y, im.getdata()) // (im.size[0] * im.size[1])
# 计算平均rgb值
@staticmethod
def __cal_avg_rgb(im):
if im.mode != "RGB":
im = im.convert("RGB")
pix = im.load()
avg_r, avg_g, avg_b = 0, 0, 0
n = 1
for i in range(im.size[0]):
for j in range(im.size[1]):
r, g, b = pix[i, j]
avg_r += r
avg_g += g
avg_b += b
n += 1
avg_r /= n
avg_g /= n
avg_b /= n
return str(avg_r) + "-" + str(avg_g) + "-" + str(avg_b)
# 计算hash
def __cal_hash(self, im):
im = im.resize((8, 8), Image.ANTIALIAS)
im = im.convert("L")
avg_gray = self.__cal_gray(im)
k = ""
_0 = "0"
_1 = "1"
for i in im.getdata():
if i < avg_gray:
k += _0
else:
k += _1
return k
# 辗转相除法求最大公约数
@staticmethod
def __gcd(a, b):
while a % b:
a, b = b, a % b
return b
# 获取最佳素材(按灰度)
def __find_by_gray(self, gray):
m = 255
k = 0
for key in self.__all_img.keys():
cur_dif = abs(key - gray)
if cur_dif < m:
k = key
m = cur_dif
return self.__all_img[k]
# 获取最佳素材(按pHash)
def __find_by_hash(self, sub_hash):
m = 65
k = 0
for key in self.__all_img.keys():
cur_dif = self.__dif_num(sub_hash, key)
if cur_dif < m:
k = key
m = cur_dif
return self.__all_img[k]
@staticmethod
def __dif_num(hash1, hash2):
n = 0
for i in range(64):
if hash1[i] != hash2[i]:
n += 1
return n
# # 获取最佳素材(按平均rgb)
def __find_by_rgb(self, sub_rgb):
sub_r, sub_g, sub_b = sub_rgb.split("-")
m = 255
k = ""
for key in self.__all_img.keys():
src_r, src_g, src_b = key.split("-")
cur_dif = abs(float(sub_r) - float(src_r)) + abs(float(sub_g) - float(src_g)) + abs(
float(sub_b) - float(src_b))
if cur_dif < m:
m = cur_dif
k = key
return self.__all_img[k]
def get_all_img(self):
return self.__all_img
if __name__ == '__main__':
m = MosaicMaker("G:\\image", "YUI.jpg",
"YUI-out-5.jpg")
m.make()
pass
最后讲一下三种key值的计算。
(一)灰度:使用PIL库的Image.mode可以查看当前图片的mode。常见的有rgb和L。当mode为rgb时Image.load()函数会返回一个三元组,例如(123,245,213)分别表示rgb的值。rgb模式下的灰度值计算公式为:(r*28+g*151+b*77) >> 8。但我在网上没有查到的一致的公式。所以可以用Image.convert()方法将图片转成L模式之后再计算平均灰度值。Image.gatdata()函数可以返回一个图片所有像素的一维数组,方便计算平均灰度。
(二)平均RGB:平均rgb值的计算原理和方法与计算灰度值大同小异,代码描述的应该已经够清楚了,不再赘述
(三)pHash:感知哈希算法(Perceptual hash algorithm),它的作用是对每张图片生成一个"指纹"(fingerprint)字符串,然后比较不同图片的指纹。结果越接近,就说明图片越相似。这个方法的最佳用途是根据缩略图,找出原图。所以不太适合用于实现马赛克拼图。pHash的计算略微复杂一些。
首先将图片缩小到8x8,即64个像素。这一步的作用是去除图片的细节,只保留结构、明暗等基本信息,摒弃不同尺寸、比例带来的图片差异。然后计算这64个像素的平均灰度值,计算方法如上所述。之后将每个像素的灰度,与平均值进行比较。大于或等于平均值,记为1;小于平均值,记为0。得到指纹以后,就可以对比不同的图片,看看64位中有多少位是不一样的。如果不相同的数据位不超过5,就说明两张图片很相似;如果大于10,就说明这是两张不同的图片。(详见阮一峰的博客)