特に理由はないのですがモザイクアートを作リたくなったのでPythonでモザイクアートを作成してみようと思います!今回は環境構築を簡単にするためGoogle Colab環境(とGoogleドライブ)を使ってお手軽に作ってみます。
用意するもの&方法
まずはモザイクアートを作るのに必要なものを確認しておきます。まずは当然ながらモザイクアートにする元となる画像(以降 対象画像)が必要になります。この対象画像を元に画像を割り当てていくことでモザイクアートを作成していきます。そして実際に割当先にはめていく画像(以降 素材画像)が必要になります。きれいなモザイクアートを作るためにはカラフルで大量の画像が必要になりますので頑張って集めてください。今回の記事と同じようにモザイクアート生成に挑戦している方の記事を読むとメトロポリタン美術館のAPIを使っている方が多そうです。(※素材画像収集は迷惑にならない方法でお願いします!)
今回は権利周りに問題が無いよう#000~#FFFまでの単色画像4096枚を用意しました。そのためモザイクアートというよりもドット絵の雰囲気が強くなりますが、The モザイクなアートはそれぞれの環境で実際に作ってみてください!集めた画像たちは、今回ColabとGoogleドライブを接続して処理を行うため、ドライブにアップロードしておいてください!後ほどコード内でディレクトリを指定します。
方法
次に今回作成したモザイクアート生成のアルゴリズムを紹介します。
① 対象画像をタイル毎に分割
まずは対象画像をモザイクの大きさに合わせてタイル状に分割していきます。この分割数を多くするとより詳細な画像が生成され、少なくするとタイルに使用した素材画像が目立つモザイクアートが生成されます。
②分割したタイル毎に特徴量を算出する
タイルごとに下記の4つを計算します。
1. タイル内の全ピクセルの平均値
2-4. RGB毎の平均値
この4つの値を使用してタイルに入れる素材画像を選択することになります。
③ 素材画像も同じように計算する
素材画像に対しても同じく4つの値を計算します。
④ タイルと素材画像で4つの値が近い画像をそのタイルに対応する画像に採用する
⑤ 完成!
ソースコード
下記が今回のソースコードです。こちらをColabに貼り付けて実行するとモザイクアートが生成されます。
import sys import glob import numpy as np from PIL import Image from scipy import spatial def get_ref_images(tile_photos_path): """ 指定されたディレクトリからタイルの画像ファイルのパスを取得する関数。 Args: tile_photos_path (str): タイル画像が保存されているディレクトリへのパス。 Returns: list: タイル画像ファイルへのパスを格納したリスト。 """ tile_paths = [] # タイルのパスを格納するための空のリストを初期化 for file in glob.glob(tile_photos_path + '/*'): # 指定されたパスにある全てのファイルをループ処理 try: tile_paths.append(file) # ファイルのパスをリストに追加 except Exception as e: tb = sys.exc_info()[2] # 例外情報を取得 print("message:{0}".format(e.with_traceback(tb))) # 例外メッセージとトレースバックを表示 return tile_paths def get_resize_images(tile_paths, tile_size): """画像のリサイズと変換を行う関数。 指定されたパスから画像を開いて、指定されたサイズにリサイズし、RGB形式に変換します。 エラーが発生した場合は、そのエラーメッセージを出力します。 Args: tile_paths (List[str]): 画像のパスのリスト。 tile_size (tuple): リサイズする画像のサイズ (幅, 高さ)。 Returns: List[Image]: リサイズと変換が完了した画像オブジェクトのリスト。 """ tiles = [] for path in tile_paths: try: tile = Image.open(path) tile = tile.resize(tile_size).convert('RGB') tiles.append(tile) except Exception as e: tb = sys.exc_info()[2] print("message(get_resize_images):{0}".format(e.with_traceback(tb))) pass return tiles def get_mean_colors(tiles): """ 与えられたタイルリストから、各タイルの平均色を計算します。 Args: tiles (array-like): 画像タイルのリスト。各タイルはNumPy配列で表されます。 Returns: np.ndarray: 各タイルの平均色と平均輝度を含む、4要素のNumPy配列。平均輝度は最初の要素として格納されます。 Raises: Exception: タイルの処理中にエラーが発生した場合、エラーメッセージを出力します。 """ colors = np.empty((0, 4), dtype=np.uint32) for tile in tiles: try: img = np.array(tile) mean_br = np.array([img.mean(dtype=np.uint32)]) mean_color = img.mean(axis=0, dtype=np.uint32).mean(axis=0, dtype=np.uint32) mean_br = np.append(mean_br, mean_color) colors = np.append(colors, [mean_br], axis=0) except Exception as e: tb = sys.exc_info()[2] print("message(get_mean_colors):{0}".format(e.with_traceback(tb))) pass return colors def get_closest_tile(colors, width, height, resized_photo): """ 画像内の各ピクセルに対応する最も近いタイルを検索する関数。 Args: colors (array-like): 検索対象となる色情報の配列。 width (int): 画像の幅。 height (int): 画像の高さ。 resized_photo (Image): リサイズ済みの画像オブジェクト。 Returns: np.array: 最も近いタイルのインデックスを保持する2次元配列。 """ tree = spatial.cKDTree(colors, leafsize=16) closest_tiles = np.zeros((width, height), dtype=np.uint32) img_array = np.array(resized_photo) for y in range(height): for x in range(width): try: pixdata = img_array[y][x] comp = np.empty((0, 4), dtype=np.uint32) mean_brightness = np.array(pixdata).mean(dtype=np.uint32) tmp_array = np.append([mean_brightness], pixdata) comp = np.append(comp, np.array([tmp_array], dtype=np.uint32), axis=0) closest = tree.query(comp) closest_tiles[x, y] = closest[1] except Exception as e: tb = sys.exc_info()[2] print("message(get_closest_tile):{0}".format(e.with_traceback(tb))) pass return closest_tiles def save_mosaic_img(width, height, closest_tiles, tiles, tile_size, output_size, output_path): """ モザイク画像を保存します。 例外を補足し、問題がある場合はエラーメッセージを表示します。 Args: width (int): モザイク画像の幅。 height (int): モザイク画像の高さ。 closest_tiles (array): 最も近いタイルのインデックスを格納する配列。 tiles (list): タイルのリスト。 tile_size (tuple): 個々のタイルのサイズ (幅, 高さ)。 output_size (tuple): 出力画像のサイズ (幅, 高さ)。 output_path (str): 出力画像のファイルパス。 """ try: output = Image.new('RGB', (output_size[0], output_size[1])) for j in range(height): for i in range(width): x, y = i * tile_size[0], j * tile_size[1] index = closest_tiles[i, j] output.paste(tiles[index], (x, y)) output.save(output_path) except Exception as e: tb = sys.exc_info()[2] print("message(save_mosaic_img):{0}".format(e.with_traceback(tb))) def gen_mosaic_img(main_photo_path, tile_photos_path, tile_size, output_size, output_path): """ 与えられたメイン画像とタイル画像からモザイク画像を生成します。 Args: main_photo_path (str): メイン画像のパス。 tile_photos_path (str): タイル画像が保存されているディレクトリのパス。 tile_size (tuple): タイル画像のサイズ (width, height)。 output_size (tuple): 出力画像のサイズ (width, height)。 output_path (str): 出力画像の保存先のパス。 Returns: なし。 """ tile_paths = get_ref_images(tile_photos_path) # タイル画像の参照先を取得 tiles = get_resize_images(tile_paths, tile_size) # タイル画像をリサイズ colors = get_mean_colors(tiles) # タイル画像の平均色を取得 width = int(output_size[0] / tile_size[0]) # 出力画像の幅を計算 height = int(output_size[1] / tile_size[1]) # 出力画像の高さを計算 # メイン画像を開いてリサイズし、RGBモードに変換 resized_photo = Image.open(main_photo_path).resize((width, height)) resized_photo = resized_photo.convert('RGB') # 最も近いタイルを取得 closest_tiles = get_closest_tile(colors, width, height, resized_photo) # モザイク画像を保存 save_mosaic_img(width, height, closest_tiles, tiles, tile_size, output_size, output_path)
関数群は用意できたので下記でモザイクアートを作成します。下記コード内の`target_file_name`で対象画像を、`workspace_dir*`の変数でディレクトリを指定しているので各々の設置したディレクトリに合わせて調整してください。
## モザイクアートのサイズ # 1つのタイルの大きさ(px) tile_width_size = 5 tile_height_size = 5 # 出力する画像の大きさ(px) output_width_size = 1000 output_height_size = 1000 # 作業ディレクトリ workspace_dir = '/content/gdrive/MyDrive/hogehoge' # モザイクを作成する対象画像のファイル名 target_file_name = 't.png' import os from google.colab import drive if not os.path.exists('/content/gdrive'): drive.mount('/content/gdrive') workspace_dir_download = f'{workspace_dir}/download' workspace_dir_target = f'{workspace_dir}/target' workspace_dir_output = f'{workspace_dir}/output' import os if not os.path.exists(workspace_dir): os.mkdir(workspace_dir) if not os.path.exists(workspace_dir_download): os.mkdir(workspace_dir_download) if not os.path.exists(workspace_dir_target): os.mkdir(workspace_dir_target) if not os.path.exists(workspace_dir_output): os.mkdir(workspace_dir_output) import datetime d = datetime.datetime.now() target_image_path = f'{workspace_dir_target}/{target_file_name}' outpu_image_path = f'{workspace_dir_output}/{d.strftime("%Y%m%d%H%M%S%f")}_{tile_width_size}x{tile_height_size}_{output_width_size}x{output_height_size}.png' tile_size = (tile_width_size, tile_height_size) output_size = (output_width_size, output_height_size) gen_mosaic_img(target_image_path, workspace_dir_download, tile_size, output_size, outpu_image_path)
結果
いらすとやさんのイラストを元にモザイクアートを作成してみました。
-
元画像
-
生成結果
-
元画像
-
生成結果
正常に動作していることは確認できます。さすがに単色画像のみだと画質が落ちただけに見えますね…。 よりきれいなモザイクアートを作成するには、もっと複雑で沢山の素材画像が必要になると思います。
改善点
今回のモザイクアートは簡単のためかなりシンプルな処理で作成しています。そのためモザイクアートの精度にはまだまだ改善の余地が残されています。
・同じ素材画像が何回でも使われる
今回は4つの値が近ければ同じ素材画像でも何回も使用するようになっています。素材が豊富にあれば一度使われた画像は使わなくしたり、選ばれにくいように重みをかけてあげるのもいいかと思いますが、そもそも素材画像自体がそこまでない場合も多いかと思いますので、今回は気づかなかったことにします。
・素材画像をタイルに入れるときに縦横比が変わってしまう
今回の実装では、素材の画像をタイルに当てはめる際に元の縦横比を無視しています。そのため、完成したモザイクアートを見ると素材画像が伸びた状態となっています。こちらの対策としては下処理として素材画像から切り出しておくかタイルの切り方を正方形から適切な長方形に変更するといった方法が考えられます。
・精度が低い
今回は簡単のため全体の平均とRGBの平均値という4次元のベクトルとして近似値を求めましたが、1つのタイルに対して内部をさらに分割してあげることで更に精度を上げることができると予想しています。その分計算量は増えるので制度と時間のトレードオフにはなると思います。
おわりに
今回は比較的シンプルに実装したため簡単でしたが、凝れる部分を突き詰めていくとどんどん沼にハマっていきそうです…。ぜひとも挑戦してみてください!