代碼已重新整理上傳到了我的KLab,為了更好的閱讀體驗可點擊🔗【千圖成像】爬取微信好友頭像完成馬賽克拼圖🧩查看~
馬賽克拼圖
何謂馬賽克拼圖(千圖成像),簡單來說就是將若干小圖片平湊成為一張大圖,如下圖路飛一樣,如果放大看你會發現里面都是一些海賊王里面的圖片。
Our Tragets
- 爬取所有微信好友的頭像🕺🏻🕺🏻🕺🏻
- 將所有微信好友頭像拼湊成一張圖片🏙🏙🏙
- 然后就可以去朋友圈愉快的裝逼了🤪🤪🤪
Requirements
其實整個項目很小,項目總共代碼量不過100行左右。
- 爬取微信頭像依賴第三方庫
itchat
; - 馬賽克拼圖依賴依賴
numpy
和PIL
庫。
Content
爬取微信好友頭像
我這邊是用的所有微信好友頭像作為的數據源,你們如果有其他的數據源也可以的,可以直接跳過這步。
爬取微信好友頭像我使用的是itchat
,里面已經有封裝好了的API,直接調用就可以,將所有的好友頭像保存到一個文件夾供后期使用。
- 代碼部分
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : AwesomeTang
# @File : Wechat_Icon.py
# @Version : Python 3.7
# @Time : 2019-06-29 23:35
import os
import itchat
itchat.login()
friends = itchat.get_friends(update=True)
base_folder = 'wechat'
if os.path.isdir(base_folder):
pass
else:
os.mkdir(base_folder)
for item in friends:
img = itchat.get_head_img(item['UserName'])
# 使用用戶昵稱作為文件名
path = os.path.join(base_folder, '{}.jpg'.format(item['NickName'].replace('/', '')))
with open(path, 'wb') as f:
f.write(img)
print('{} 寫入完成...'.format(item['NickName']))
- 實現效果
馬賽克拼圖
思路梳理
- 選好你需要拼湊的圖片,將其切割為若干小塊,切割的越細生成的圖片效果會更好。
gay里gay氣的示意圖:
- 分別去我們之前保存的圖片中找與與之最相似的,最后將其拼接完成。
說起來好像很簡單,但實際操作起來只能......
最相似的圖片
其實困難的地方很明顯,那就是我們如何從一堆圖片中找出最相似的那張。
我們可以分為兩個步驟:
顏色相似
這個應該不難理解,我在代碼中實現了灰度圖像和RGB通道圖像的篩選方法:
- 灰度圖像:
直接計算所有像素灰度值的平均值,取最接近n個圖像供后期再次篩選; - RGB通道:
分別計算R,G,B的平均值,對於一個圖像我們得到的是一個類似與[20, 30,40]的數組,然后我們計算歐式距離,取最接近n個圖像供后期再次篩選。
結構相似
為什么還需要結構相似,舉個例子:
如果單純按照上述方法去判定相似,那上圖中的圖A和圖B肯定是最相似的,所以我們不能單純的因為兩張圖片中包含的顏色差不多就去判斷為最相似,還需要去判斷顏色的“位置”也要相似。
這部分實現方法參考了阮一峰的博客,具體邏輯如下:
- 只需要提取圖片結構,顏色意義不大,為計算簡便,我們直接將所有圖片轉為灰度通道;
- 將每張頭像resize為(8,8),然后計算所有像素值的平均值。
- 我們總共有64(即$8*8$)個像素點,分別去與平均值比較大小,高於平均值的記為1,小於平均值的記為0,這樣我們每張圖片都會得到一個長度為64類似[0,1,1,0,1,0....0,1,1]的‘編碼’。
- 對於切割的小圖片我們也進行上述操作,注意要保證是同一順序(譬如從左上角到右下角),然后分別去與每個頭像的‘編碼’進行比較,這邊在阮一峰的博客中是采用的計算漢明距離,我這邊使用的就直接通過
np.equal()
計算相同的點了,取相同位數最多的那張頭像即為最相似的圖片。
我在代碼中是先篩選顏色最接近的50張圖片,然后再在這50中圖片去尋找結構最相似的圖片,最后實現效果如下:
-
RGB通道
因為圖片源只有500來張,效果只能說湊合,勉強能看出來是啥。
-
灰度
-
放大之后是這個效果:
然后便能去朋友圈愉快的裝逼了😎😎😎
- Talk is cheap, show me the code.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : AwesomeTang
# @Version : Python 3.7
# @Time : 2019-06-23 13:52
from PIL import Image
import os
import numpy as np
from tqdm import tqdm
class Config:
corp_size = 40
filter_size = 20
num = 100
class PicMerge:
def __init__(self, pic_path, mode='RGB', pic_folder='wechat'):
if mode.upper() not in ('RGB', 'L'):
raise ValueError('Only accept "RGB" or "L" MODE, but we received "{}".'.format(self.mode))
else:
self.mode = mode.upper()
print('Coding for every picture in folder "{}".'.format(pic_folder))
self.mapping_table, self.pictures = self.mapping_table(pic_folder)
self.picture = self.resize_pic(pic_path).convert(self.mode)
@staticmethod
def resize_pic(pic_path):
picture = Image.open(pic_path)
width, height = picture.size
to_width = Config.corp_size * Config.num
to_height = ((to_width / width) * height // Config.corp_size) * Config.corp_size
picture = picture.resize((int(to_width), int(to_height)), Image.ANTIALIAS)
return picture
def merge(self):
width, height = self.picture.size
w_times, h_times = int(width / Config.corp_size), int(height / Config.corp_size)
picture = np.array(self.picture)
print('Corp & Merge...')
for i in tqdm(range(w_times), desc='CORP'):
for j in range(h_times):
if self.mode == 'L':
section = picture[j * Config.corp_size:(j + 1) * Config.corp_size,
i * Config.corp_size:(i + 1) * Config.corp_size]
section_mean = section.mean()
candidate = sorted([(key_, abs(np.array(value_).mean() - section_mean))
for key_, value_ in self.pictures.items()],
key=lambda item: item[1])[:Config.filter_size]
most_similar = self.structure_similarity(section, candidate)
picture[j * Config.corp_size:(j + 1) * Config.corp_size,
i * Config.corp_size:(i + 1) * Config.corp_size] = most_similar
elif self.mode == 'RGB':
section = picture[j * Config.corp_size:(j + 1) * Config.corp_size,
i * Config.corp_size:(i + 1) * Config.corp_size, :]
candidate = self.color_similarity(section)
most_similar = self.structure_similarity(section, candidate)
picture[j * Config.corp_size:(j + 1) * Config.corp_size,
i * Config.corp_size:(i + 1) * Config.corp_size, :] = most_similar
picture = Image.fromarray(picture)
picture.show()
picture.save('result.jpg')
print('Work Done...')
def structure_similarity(self, section, candidate):
section = Image.fromarray(section).convert('L')
one_hot = self.pic_code(np.array(section.resize((8, 8), Image.ANTIALIAS)))
candidate = [(key_, np.equal(one_hot, self.mapping_table[key_]).mean()) for key_, _ in candidate]
most_similar = max(candidate, key=lambda item: item[1])
return self.pictures[most_similar[0]]
def color_similarity(self, pic_slice, top_n=Config.filter_size):
slice_mean = self.rgb_mean(pic_slice)
diff_list = [(key_, np.linalg.norm(slice_mean - self.rgb_mean(value_)))
for key_, value_ in self.pictures.items()]
filter_ = sorted(diff_list, key=lambda item: item[1])[:top_n]
return filter_
@staticmethod
def rgb_mean(rgb_pic):
"""
if picture is RGB channel, calculate average [R, G, B].
"""
r_mean = np.mean(rgb_pic[:, :, 0])
g_mean = np.mean(rgb_pic[:, :, 1])
b_mean = np.mean(rgb_pic[:, :, 2])
val = np.array([r_mean, g_mean, b_mean])
return val
def mapping_table(self, pic_folder):
"""
What this function do?
1. transverse every image in PIC_FOLDER;
2. resize every image in (8, 8) and covert into GREY;
3. CODE for every image, CODE like [1, 0, 1, 1, 0....1]
4. build a dict to gather all image and its CODE.
:param pic_folder: path of pictures folder.
:return: a dict
"""
suffix = ['jpg', 'jpeg', 'JPG', 'JPEG', 'gif', 'GIF', 'png', 'PNG']
if not os.path.isdir(pic_folder):
raise OSError('Folder [{}] is not exist, please check.'.format(pic_folder))
pic_list = os.listdir(pic_folder)
results = {}
pic_dic = {}
for idx, pic in tqdm(enumerate(pic_list), desc='CODE'):
if pic.split('.')[-1] in suffix:
path = os.path.join(pic_folder, pic)
try:
img = Image.open(path).resize((Config.corp_size, Config.corp_size), Image.ANTIALIAS)
results[idx] = self.pic_code(np.array(img.convert('L').resize((8, 8), Image.ANTIALIAS)))
if self.mode == 'RGB':
pic_dic[idx] = np.array(img.convert(self.mode))
else:
pic_dic[idx] = np.array(img.convert(self.mode))
except OSError:
pass
return results, pic_dic
@staticmethod
def pic_code(image: np.ndarray):
"""
To make a one-hot code for IMAGE.
AVG is mean of the array(IMAGE).
Traverse every pixel of IMAGE, if the pixel value is more then AVG, make it 1, else 0.
:param image: an array of picture
:return: A sparse list with length [picture's width * picture's height].
"""
width, height = image.shape
avg = image.mean()
one_hot = np.array([1 if image[i, j] > avg else 0 for i in range(width) for j in range(height)])
return one_hot
if __name__ == "__main__":
P = PicMerge(pic_path='海賊王.jpeg', mode='RGB')
P.merge()
參考資料
- 相似圖片搜索的原理 :點我跳轉
skr~~ skr~~~