写在前面
大家都玩过拼图,玩法是将一张张小的图像块拼成一张大图。因为小图是大图的一部分,大家可以根据大图的内容判断小图的位置,从而完成拼图。问题来了,如果每个图块是一张独立的图像呢,这样的拼图该怎么拼呢?今天在这里带大家制作这种由独立图像块组成的拼图,这样的图像也有另外一个名称——马赛克图像。使用的素材是二次元头像,目的是拼成一副DOTA游戏里痛苦女王和骷髅王的形象。下面是图片素材,大家可以自行感受下两者的画风差异。
二次元头像
DOTA英雄
拼图原理
马赛克拼图的原理很简单,大家都知道,一张图像是由若干个像素组成,例如一张64x64的图像就是由4096个像素组成。如果把这张64x64的图像分成16份4x4的图像,然后把每一个4x4的区域换成一个特征相近的4x4的独立图像,这样这张64x64的图像就变成了一张马赛克拼图,而且整体的结构信息不会有太大的改变。保证结构信息不变的关键有两个:
- 特征选择要合理,能表示颜色和纹理特征
- 用于拼图的图块要充足,至少要能覆盖大图的颜色
这里有一份示例代码,特征直接选择像素值,用欧几里德距离计算相似度,大家如果要改进可以直接换特征,比如换成LBP算子、HOG特征描述子等等,当然最完美的是采用卷积神经网络来提取特征。这里可以先看下主要的代码结构,总体分三步:
- 将大图按小图的个数分块
- 找到大图每个图像块区域颜色最接近的小图
- 将小图粘贴到大图对应位置
import ctypes
import re
import numpy as np
from numpy import ctypeslib
import os
import cv2
import multiprocessing as mp
from mul import RawArray
from import euclidean
from tqdm import tqdm
IMG_DIR = "images" # 二次元图像路径
RATIO = 10
def resize(im, tile_row, tile_col):
shape_row = im.shape[0]
shape_col = im.shape[1]
shrink_ratio = min(shape_row/tile_row, shape_col/tile_col)
resized = cv2.resize(im, (int(shape_col/shrink_ratio)+1, int(shape_row/shrink_ratio)+1), interpolation=cv2.INTER_CUBIC)
result = resized[:tile_row, :tile_col,:]
return result
def img_distance(im1, im2):
if im1.shape != im2.shape:
msg = "shapes are different {} {}".forma, im2.shape)
raise Exception(msg)
array1 = im1.flatten()
array2 = im2.flatten()
dist = euclidean(array1, array2)
return dist
def load_all_images(tile_row, tile_col):
img_dir = IMG_DIR
filenames = os.listdir(img_dir)
result = []
print(len(filenames))
for filename in tqdm(filenames):
if not re.search(".jpg", filename, re.I):
continue
try:
filepath = os.(img_dir, filename)
im = cv2.imread(filepath)
row = im.shape[0]
col = im.shape[1]
im = resize(im, tile_row, tile_col)
re(im))
except Exception as e:
msg = "error with {} - {}".format(filepath, str(e))
print(msg)
return np.array(result, dtype=np.uint8)
def find_closest_image(q, shared_tile_images, tile_images_shape, shared_result, img_shape, tile_row, tile_col):
tile_images_array = np.frombuffer(shared_tile_images, dtype=np.uint8)
tile_images = tile_images_array.reshape(tile_images_shape)
while True:
[row, col, im_roi] = q.get()
print(row)
min_dist = float("inf")
min_img = None
for im in tile_images:
dist = img_distance(im_roi, im)
if dist < min_dist:
min_dist = dist
min_img = im
im_res = np.frombuffer(shared_result, dtype=np.uint8).reshape(img_shape)
im_res[row:row+tile_row,col:col+tile_col,:] = min_img
q.task_done()
def get_tile_row_col(shape):
if shape[0] >= shape[1]:
return [120, 90]
else:
return [90, 120]
def generate_mosaic(infile, outfile):
img = cv2.imread(infile)
tile_row, tile_col = get_tile_row_col)
img_shape = list)
img_shape[0] = int(img_shape[0]/tile_row) * tile_row * RATIO
img_shape[1] = int(img_shape[1]/tile_col) * tile_col * RATIO
img = cv2.resize(img, (img_shape[1], img_shape[0]), interpolation=cv2.INTER_CUBIC)
print(img_shape)
im_res = np.zeros(img_shape, np.uint8)
tile_images = load_all_images(tile_row, tile_col)
shared_tile_images = mp., len(tile_images.flatten()))
tile_images_shape = tile_images.shape
np.copyto(shared_tile_images, dtype=np.uint8).reshape(tile_images_shape), tile_images)
shared_result = mp., len()))
q = mp.JoinableQueue()
for i in range(5):
p = mp.Process(target=find_closest_image,
args=(q, shared_tile_images, tile_images_shape, shared_result, img_shape, tile_row, tile_col),
daemon=True)
p.start()
print("started process")
for row in range(0, img_shape[0], tile_row):
for col in range(0, img_shape[1], tile_col):
roi = img[row:row+tile_row,col:col+tile_col,:]
q.put([row, col, roi])
q.join()
cv2.imwrite(outfile, np.frombuffer(shared_result, dtype=np.uint8).reshape(img_shape))
if __name__ == "__main__":
generate_mosaic(";, "out.jpg") # 是DOTA图像
效果展示
这里要说明下,上面的代码只是方便大家理解制作马赛克拼图的原理,但是要达到比较好的效果这份代码是远远不够的。但没关系,已经有人给大家做好了轮子!有个软件叫 Foto-Mosaik-Edda,下载地址 ,使用起来也很简单。这里我用2000张二次元头像拼成了两个DOTA英雄,效果还不错吧,放大看还能看到二次元头像的细节。大家快去尝试吧。
马赛克拼图效果