今天小編帶大家使用python簡(jiǎn)單實(shí)現(xiàn)馬賽克拼圖,內(nèi)容比以往會(huì)稍長(zhǎng)一些,各位看官老爺可以慢慢細(xì)讀,若有不足之處還望請(qǐng)斧正,閑話不多說(shuō),請(qǐng)看文章。
先看原圖:
效果圖:
思路:
拼圖的原理其實(shí)很簡(jiǎn)單,就是把原圖劃分成很多個(gè)小塊,然后根據(jù)灰度或者rgb搜索圖庫(kù)中最相似的圖片進(jìn)行替換。接下來(lái)的問(wèn)題就是如何實(shí)現(xiàn)圖片搜索。這里可以參考阮一峰的博客
阮一峰博客:http://www.ruanyifeng.com/blog/2011/07/principle_of_similar_image_search.html
代碼:
第一步:獲取目標(biāo)圖片的尺寸,計(jì)算每個(gè)子圖的大小。例如:目標(biāo)圖片的尺寸為1600x1280,計(jì)算出這個(gè)尺寸的最大公約數(shù)為320,即拼出的圖片由每行每列都有320張小圖組成,這樣計(jì)算出的小圖尺寸則為5x4。但這個(gè)尺寸太小,所以設(shè)置一個(gè)min_unit,用來(lái)確定最終的小圖片的尺寸,若min_unit=5,則每張小圖片的尺寸為25x20,相應(yīng)的每行每列最終的圖片數(shù)也會(huì)變化。(注:代碼中的flag用來(lái)處理目標(biāo)圖片尺寸無(wú)法計(jì)算出合理的數(shù)值的情況,這時(shí)候需要自定義一個(gè)圖片尺寸)
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 # 輾轉(zhuǎn)相除法求最大公約數(shù) @staticmethod def gcd(a, b): while a % b: a, b = b, a % b return b
第二步:讀取圖庫(kù),參數(shù)分別是圖庫(kù)的路徑和上一步確定的小圖片的尺寸(長(zhǎng),寬)。根據(jù)這個(gè)尺寸對(duì)圖庫(kù)的所有圖片進(jìn)行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) # 計(jì)算key值(灰度值,平均RGB,hash值,三選一) key = self.cal_key(cur) # 將素材縮放到目標(biāo)大小 cur = cur.resize((fin_w, fin_h), Image.ANTIALIAS) self.__all_img.update({key: cur}) n += 1
第三步:計(jì)算圖庫(kù)中每張圖片的key值,這里實(shí)現(xiàn)了三種模式。1:基于圖片灰度計(jì)算出來(lái)的key值。2:基于圖片平均RGB計(jì)算出來(lái)的key值。(效果圖使用這種方式)3:基于感知哈希算法(Perceptual hash algorithm)計(jì)算出來(lái)的key值。最后將每張圖片計(jì)算出來(lái)的key和Image對(duì)象保存在dict中,這個(gè)key值用來(lái)找出最適合的子圖。
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 "" # 計(jì)算灰度值 @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]) # 計(jì)算平均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) # 計(jì)算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
第四步:開始拼圖。遍歷整個(gè)大圖,利用之前計(jì)算的子圖尺寸將大圖分為若干個(gè)小圖,計(jì)算每個(gè)小圖的key值,然后在圖庫(kù)中搜索最相似的圖片,然后將圖庫(kù)中搜索的結(jié)果填充到新圖片中。
def core(self, aim_im, width, height): new_im = Image.new("RGB", (width, height)) # 每行每列的圖片數(shù) 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("開始拼圖,請(qǐng)稍等...") 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) # 計(jì)算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("拼圖完成,共耗時(shí)%f秒" % (time.time() - start)) new_im.save(self.__out_path)
完整代碼:
參數(shù)用途:
db_path:圖庫(kù)目錄
aim_path:目標(biāo)圖片路徑
out_path:生成的圖片的輸出路徑
sub_width=64:子圖的尺寸(默認(rèn)64,可自己更改)
sub_height=64:
min_unit=2:可理解成粒度,值越小拼出的圖片越精細(xì),每個(gè)子圖也越小
mode="RGB":拼圖方式,默認(rèn)RGB
default_w=1600:默認(rèn)生成的圖片尺寸,只在無(wú)法計(jì)算有效合理的最大公約數(shù)時(shí)有效 default_h=1280
import osimport timefrom functools import reducefrom threading import Threadfrom PIL import Image class MosaicMaker(object): # 內(nèi)部類,執(zhí)行多線程拼圖的任務(wù)類 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) # 計(jì)算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) # 內(nèi)部類,執(zhí)行多線程讀取圖庫(kù)的任務(wù)類 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) # 計(jì)算key值(灰度值,平均RGB,hash值,三選一) key = self.m.cal_key(cur) # 將素材縮放到目標(biāo)大小 cur = cur.resize((self.fin_w, self.fin_h), Image.ANTIALIAS) self.m.get_all_img().update({key: cur}) # 圖庫(kù)目錄 目標(biāo)文件 輸出路徑 子圖尺寸 最小像素單位 拼圖模式 默認(rèn)尺寸 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() # 對(duì)外提供的接口 def make(self): aim_im = Image.open(self.__aim_path) aim_width = aim_im.size[0] aim_height = aim_im.size[1] print("計(jì)算子圖尺寸") if not self.__divide_sub_im(aim_width, aim_height): print("使用默認(rèn)尺寸") 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("讀取圖庫(kù)") start = time.time() self.__read_all_img(self.__db_path, self.__sub_width, self.__sub_height) print("耗時(shí):%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)) # 每行每列的圖片數(shù) 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("開始拼圖,請(qǐng)稍等...") 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("拼圖完成,共耗時(shí)%f秒" % (time.time() - start)) # 將原圖與拼圖合并,提升觀感 new_im = Image.blend(new_im, aim_im, 0.35) new_im.show() new_im.save(self.__out_path) # 拼圖庫(kù)線程執(zhí)行的具體函數(shù) @staticmethod def __sub_mission(missions): for task in missions: task.work() # 計(jì)算子圖大小 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個(gè)線程加載圖片 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() # 讀取圖庫(kù)線程執(zhí)行的具體函數(shù) @staticmethod def __read_img(tasks): for task in tasks: task.read() # 計(jì)算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 "" # 計(jì)算灰度值 @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]) # 計(jì)算平均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) # 計(jì)算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 # 輾轉(zhuǎn)相除法求最大公約數(shù) @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值的計(jì)算。
(一)灰度:使用PIL庫(kù)的Image.mode可以查看當(dāng)前圖片的mode。常見的有rgb和L。當(dāng)mode為rgb時(shí)Image.load()函數(shù)會(huì)返回一個(gè)三元組,例如(123,245,213)分別表示rgb的值。rgb模式下的灰度值計(jì)算公式為:(r*28+g*151+b*77) >> 8。但我在網(wǎng)上沒有查到的一致的公式。所以可以用Image.convert()方法將圖片轉(zhuǎn)成L模式之后再計(jì)算平均灰度值。Image.gatdata()函數(shù)可以返回一個(gè)圖片所有像素的一維數(shù)組,方便計(jì)算平均灰度。
(二)平均RGB:平均rgb值的計(jì)算原理和方法與計(jì)算灰度值大同小異,代碼描述的應(yīng)該已經(jīng)夠清楚了,不再贅述
(三)pHash:感知哈希算法(Perceptual hash algorithm),它的作用是對(duì)每張圖片生成一個(gè)"指紋"(fingerprint)字符串,然后比較不同圖片的指紋。結(jié)果越接近,就說(shuō)明圖片越相似。這個(gè)方法的最佳用途是根據(jù)縮略圖,找出原圖。所以不太適合用于實(shí)現(xiàn)馬賽克拼圖。pHash的計(jì)算略微復(fù)雜一些。
首先將圖片縮小到8x8,即64個(gè)像素。這一步的作用是去除圖片的細(xì)節(jié),只保留結(jié)構(gòu)、明暗等基本信息,摒棄不同尺寸、比例帶來(lái)的圖片差異。然后計(jì)算這64個(gè)像素的平均灰度值,計(jì)算方法如上所述。之后將每個(gè)像素的灰度,與平均值進(jìn)行比較。大于或等于平均值,記為1;小于平均值,記為0。得到指紋以后,就可以對(duì)比不同的圖片,看看64位中有多少位是不一樣的。如果不相同的數(shù)據(jù)位不超過(guò)5,就說(shuō)明兩張圖片很相似;如果大于10,就說(shuō)明這是兩張不同的圖片。
最后多說(shuō)一句,小編是一名python開發(fā)工程師,這里有我自己整理了一套最新的python系統(tǒng)學(xué)習(xí)教程,包括從基礎(chǔ)的python腳本到web開發(fā)、爬蟲、數(shù)據(jù)分析、數(shù)據(jù)可視化、機(jī)器學(xué)習(xí)等。想要這些資料的可以關(guān)注小編,并在后臺(tái)私信小編:“01”即可領(lǐng)取。
總結(jié)
以上是生活随笔 為你收集整理的opengl实现三维动画简单代码_使用Python简单实现马赛克拼图!内附完整代码 的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔 推薦給好友。