使用Python简单实现马赛克拼图!内附完整代码

使用Python简单实现马赛克拼图!内附完整代码

首页休闲益智3D像素拼图更新时间:2024-04-30

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

先看原图:

效果图:

思路:

拼图的原理其实很简单,就是把原图划分成很多个小块,然后根据灰度或者rgb搜索图库中最相似的图片进行替换。接下来的问题就是如何实现图片搜索。这里可以参考阮一峰的博客

阮一峰博客:http://www.ruanyifeng.com/blog/2011/07/principle_of_similar_image_search.html

代码:

第一步:获取目标图片的尺寸,计算每个子图的大小。例如:目标图片的尺寸为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,就说明这是两张不同的图片。


最后多说一句,小编是一名python开发工程师,这里有我自己整理了一套最新的python系统学习教程,包括从基础的python脚本到web开发、爬虫、数据分析、数据可视化、机器学习等。想要这些资料的可以关注小编,并在后台私信小编:“01”即可领取。

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved