まいだいありー

機械学習、技術系、日記など勉強したことのメモを書けたらなと思います。

二次元キャラで二次元キャラのモザイクアートをする

モザイクアートをする

二次元キャラで二次元キャラのモザイクアートをしてみたかったので, Pythonでサクッとやってみた.

手順

  • 元画像をタイル状に分割 ( 画像サイズ / 分割したい数 )
  • 元画像のタイル毎にカラーヒストグラムを求める. (1)
  • タイルを埋める画像のカラーヒストグラムを求めてリストに保管 (2)
  • 1 と 2 間の Bhattacharyya 距離 を全探索で計算し, 最小となる画像で埋める

Result

  • タイルを埋める画像の枚数は240枚程度
  • 入力画像( 1024x1024 )を 64分割したとき, モザイクアート(1024x1024)にかかる時間は 約 30 秒でした. (2.7 GHz dual core Intel Core i5)
  • もっと画像を使えば色彩豊かになると思うが, 使う画像を検索する手法はただの線形探索なので, 本当は二分探索とか使うべき. (とはいえこのスケールなら線形探索でも十分)
  • もしくは, 予めタイルを埋める画像の色特徴ベクトル化して, クラスタリング. そして, 元画像のタイルの色特徴ベクトルがどこのクラスタに属するかを計算して, 割り当てられたクラスタの画像をランダム使う, のようなことをすれば探索数はクラスタ数になるので全体の計算時間は減るかも.

f:id:kenzo1122:20210605171037p:plain:w300:h300

f:id:kenzo1122:20210605170950p:plain:w300:h300

f:id:kenzo1122:20210605170857p:plain:w300:h300

f:id:kenzo1122:20210605170544p:plain:w300:h300

f:id:kenzo1122:20210605170653p:plain:w300:h300

f:id:kenzo1122:20210605170754p:plain:w300:h300

コード

最後に使ったコードを残しておきます.

補足すると,
--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()