用Python生成马赛克拼图

用Python生成马赛克拼图

I. 马赛克拼图

所谓的马赛克拼图照片墙,效果如下所示

Fig. I-1 马赛克图片示例

很炫酷有没有!再想象一下,如果用许多男 (女) 朋友的照片拼出这么一张图片,是不是炫酷又浪漫!

其实这种马赛克拼图的原理挺简单的,几句话就能说清楚:首先把模板图片切分成许多小块,对于每一小块,在许多图片里寻找最接近的一张进行替代即可。用Python实现马赛克拼图,几十行代码就可以了,代码不难看懂,直接贴在下面了

II. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import argparse
import cv2
import glob
import numpy as np
import os
from tqdm import tqdm
from itertools import product


def parse_args():
parser = argparse.ArgumentParser("Mosaic Photo")
parser.add_argument("--source-path", type=str, default='source',
help="the path for all source images")
parser.add_argument("--target-path", type=str, default='target',
help="the path for the template photo")
parser.add_argument("--output-path", type=str, default='output',
help="the path for the mosaic photo")
parser.add_argument("--block-size", type=int, default=20,
help="the block size of each image in the mosaic image")
parser.add_argument("--expand-factor", type=int, default=1,
help="expand the size of the template image; 1 means "
"keeping the same size as the template image")
args = parser.parse_args()
return args


def rename_by_date(path):
""" Rename all source images by the modification time. """
all_imgs = glob.glob("{}/*.jpg".format(path))
all_cts = []
for img in all_imgs:
ct = os.path.getmtime(img)
all_cts.append(ct)
sort_ind = np.asarray(all_cts).argsort().tolist()
for i, ind in enumerate(sort_ind):
os.rename(all_imgs[ind], "source/{}.jpg".format(str(i + 1).zfill(3)))


def read_source_images(source_path, block_size, expand_factor):
""" Read source images and calculate the average RGB values. """
source_images = glob.glob('{}/*.jpg'.format(source_path))
resized_source_images = []
avg_colors = []
resize_size = block_size * expand_factor
print("Begin reading source images")
for path in tqdm(source_images):
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image.shape[-1] != 3: # alpha channel
continue
image = cv2.resize(image, (resize_size, resize_size))
# calculate the averaged RGB values
avg_rgb = np.sum(np.sum(image, axis=0), axis=0) / (
resize_size * resize_size)
resized_source_images.append(image)
avg_colors.append(avg_rgb)
print("End reading source images")
return resized_source_images, np.array(avg_colors)


def main():
opts = parse_args()

source_images, avg_colors = read_source_images(
opts.source_path, opts.block_size, opts.expand_factor)
assert len(source_images) > 0, "Fail to load source images"
print("Successfully load {} source images".format(len(source_images)))

assert os.path.exists(opts.target_path), \
"{} does not exist".format(opts.target_path)
target_imgs = glob.glob('{}/*.jpg'.format(opts.target_path))
assert len(target_imgs) > 0, "Fail to load template images"
os.makedirs(opts.output_path, exist_ok=True)

output_block_size = opts.block_size * opts.expand_factor
print("Begin concatenating")
for target_img_path in tqdm(target_imgs):
target_img = cv2.imread(target_img_path)
w, h, c = target_img.shape
output_img = np.zeros(
[w * opts.expand_factor, h * opts.expand_factor, c], np.uint8)
iterator = product(
range(int(target_img.shape[0] / opts.block_size)),
range(int(target_img.shape[1] / opts.block_size)))
for i, j in iterator:
# select the target block
block = target_img[
i * opts.block_size: (i + 1) * opts.block_size,
j * opts.block_size: (j + 1) * opts.block_size, :]
# average rgb value of the target block
avg_rgb = np.sum(np.sum(block, axis=0), axis=0) / (
opts.block_size * opts.block_size)
# calculate the distance to the target block based on RGB values
distances = np.linalg.norm(avg_colors - avg_rgb, axis=1)
# find the closest photo to the target block
idx = np.argmin(distances)
output_img[i * output_block_size: (i + 1) * output_block_size,
j * output_block_size: (j + 1) * output_block_size, :] \
= source_images[idx]
cv2.imwrite(target_img_path.replace(opts.target_path, opts.output_path),
output_img)
print("End concatenating")


if __name__ == '__main__':
# rename_by_date("source") # this step is not necessary
main()

III. 如何使用

看懂了代码可以自行 DIY,只是使用的话也很简单:

  1. 把所有用于生成拼图的图片默认放在source目录下,图片越多,最终生成的效果越好

  2. 把所有用于生成拼图的模板图片默认放在target目录下

  3. 运行Python main.py --block-size 20 --expand-factor 1

    两个参数都会对生成拼图的精细程度造成影响

    • --block-size决定每张小图片占据模板图片的像素块大小,数值越小,拼图长/宽上的小图片越多,整个拼图越精细,但是每个小图片越小 (看起来模糊)
    • --expand-factor默认情况下生成的拼图与模板图片尺寸相同,此参数可以扩展生成拼图的大小,数值越大,生成的拼图越大,无论拼图还是小图片保存的细节越多,但是拼图的尺寸也越大 (约等于平方倍增加),谨慎修改。
  4. 生成的图片默认放在output目录下

By the way,难的从来不是代码,而是一个你为他 (她) 拍了好多图片的对象

(((┏(; ̄▽ ̄)┛~~逃~~

参考


用Python生成马赛克拼图
https://zray111.github.io/2022/11/15/用Python生成马赛克拼图/
作者
ZRay
发布于
2022年11月15日
许可协议