二次元キャラで二次元キャラのモザイクアートをする
モザイクアートをする
二次元キャラで二次元キャラのモザイクアートをしてみたかったので, Pythonでサクッとやってみた.
手順
- 元画像をタイル状に分割 ( 画像サイズ / 分割したい数 )
- 元画像のタイル毎にカラーヒストグラムを求める. (1)
- タイルを埋める画像のカラーヒストグラムを求めてリストに保管 (2)
- 1 と 2 間の Bhattacharyya 距離 を全探索で計算し, 最小となる画像で埋める
Result
- タイルを埋める画像の枚数は240枚程度
- 入力画像( 1024x1024 )を 64分割したとき, モザイクアート(1024x1024)にかかる時間は 約 30 秒でした. (2.7 GHz dual core Intel Core i5)
- もっと画像を使えば色彩豊かになると思うが, 使う画像を検索する手法はただの線形探索なので, 本当は二分探索とか使うべき. (とはいえこのスケールなら線形探索でも十分)
- もしくは, 予めタイルを埋める画像の色特徴ベクトル化して, クラスタリング. そして, 元画像のタイルの色特徴ベクトルがどこのクラスタに属するかを計算して, 割り当てられたクラスタの画像をランダム使う, のようなことをすれば探索数はクラスタ数になるので全体の計算時間は減るかも.
コード
最後に使ったコードを残しておきます.
補足すると,
--input_path
: モザイクアート化したい画像のパス
--ref_path
: タイルを埋める画像のディレクトリのパス
--n_tile
: 何分割するかを指定
example
$ python mosaic_art.py --input_path "input.png" --ref_path "./images" --n_tile 64
import cv2 import argparse import numpy as np import glob from tqdm import tqdm parser = argparse.ArgumentParser() parser.add_argument("--input_path", default="./input.png",type=str) parser.add_argument("--ref_path", default="./images",type=str) parser.add_argument("--output_path", default="./output.png",type=str) parser.add_argument("--n_tile", default = 64,type=int) parser.add_argument("--output_size", default = 1024,type=int) opt = parser.parse_args() tsize_x,tsize_y = None,None def init_load(): img = cv2.imread(opt.input_path) #img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB) ref_path_l = glob.glob(f"{opt.ref_path}/*") if opt.input_path in ref_path_l: del ref_path_l[ref_path_l.index(opt.input_path)] return img , ref_path_l def ref_img_load(p): ref_img = cv2.resize(cv2.imread(p),(tsize_x,tsize_y)) #ref_img = cv2.cvtColor(ref_img,cv2.COLOR_BGR2RGB) return ref_img def get_tile_size(img): global tsize_x,tsize_y tsize_x = img.shape[0]//opt.n_tile tsize_y = img.shape[1]//opt.n_tile return tsize_x, tsize_y def img2tile_stats(img): img_tile_stats = [] tsize_x,tsize_y = get_tile_size(img) for x in range(opt.n_tile): for y in range(opt.n_tile): img_t = img[x*tsize_x : (x + 1)*tsize_x , y*tsize_y : (y + 1)*tsize_y ] s = get_colorhist(img_t) img_tile_stats.append(s) return np.array(img_tile_stats) def cos_sim(v1, v2): return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) def get_colorhist(img): img_h = [cv2.calcHist([img], [i], None, [256], [0, 256]) for i in range(3)] return np.array(img_h) def normlize(img_h): s = img_h.sum(1) return img_h/s[:,np.newaxis] def calc_hist_distance(img_h,ref_h): img_h = normlize(img_h) ref_h = normlize(ref_h) dist = cv2.compareHist(img_h,ref_h,3) return dist def ref_img_alloc(img,ref_path): img_tile_stats = img2tile_stats(img) ref_img_stats=[get_colorhist(ref_img_load(p)) for p in ref_path] alloc_path = [] print("==> calculating distance between img_tile and ref_img") for img_st in tqdm(img_tile_stats): min_dist = 10e10 min_dist_ref = "" for i,refim_st in enumerate(ref_img_stats): dist = calc_hist_distance(img_st,refim_st) if min_dist > dist: min_dist = dist min_dist_ref = ref_path[i] alloc_path.append(min_dist_ref) del ref_img_stats del img_tile_stats return alloc_path def create_mosaic_art(ref_path): ref_img=[ref_img_load(p) for p in ref_path] output_img = np.zeros((opt.output_size,opt.output_size,3),dtype=np.uint8) tsize_x,tsize_y = opt.output_size//opt.n_tile,opt.output_size//opt.n_tile print("==> allocating ref_img") for x in tqdm(range(opt.n_tile)): for y in range(opt.n_tile): output_img[x*tsize_x : (x + 1)*tsize_x , y*tsize_y : (y + 1)*tsize_y ] = ref_img[y+opt.n_tile*x] cv2.imwrite(opt.output_path, output_img) def main(): img,ref_path_list = init_load() tsize_x,tsize_y = get_tile_size(img) alloc_path = ref_img_alloc(img,ref_path_list) create_mosaic_art(alloc_path) if __name__ == '__main__': main()