今回はPythonでモザイクアートを作ってみました.
モザイクアートを綺麗に作るために以下のような工夫をしました.
モザイクアートを作るためには素材となる画像がたくさん必要になります.
すでに1つのフォルダにまとまったたくさんの画像データを持っている方はそれらを利用することができます.
素材画像が手元にない もしくはわざわざPCに移動するのが面倒な場合は以下のサイトで素材画像をダウンロードすることができます.
【関連(外部サイト)】Flickr 8k Dataset - Kaggle
8091枚のパブリックドメイン画像データセットがダウンロード可能です.後述のメトロポリタン美術館のパブリック・ドメイン画像データセットより色のバリエーションが多いです.
また,メトロポリタン美術館のパブリック・ドメイン絵画画像をPythonで収集するで紹介されている方法でパブリックドメインの画像データセットをダウンロードすることができます.
こちらは20万枚程度のパブリックドメイン画像データが利用できます.
以下ではFlickr 8k Datasetを素材画像として秋の草木と見る本栖湖 - 写真ACを作ります.
素材画像を必要な大きさにトリミングします. 本当は人間の目で重要な部分をトリミングすべきですが,今回は一括で画像の中心から正方形を切り出して,リサイズするようにしています. ここではOriginalImagesフォルダの中の画像を32×32に素材をリサイズしmaterialsに保存します.
from PIL import Image
import os
# 元画像を中央から正方形にトリミングし、指定サイズにリサイズする。
def crop_and_resize(image, target_size):
width, height = image.size
# 最小辺を基準に正方形を作る
min_dim = min(width, height)
left = (width - min_dim) // 2
top = (height - min_dim) // 2
right = (width + min_dim) // 2
bottom = (height + min_dim) // 2
cropped_image = image.crop((left, top, right, bottom))
return cropped_image.resize(target_size, Image.ANTIALIAS)
# 入力フォルダと出力フォルダのパス
input_folder = "OriginalImages" # 元画像フォルダ
output_folder = "materials" # 出力先フォルダ
target_size = (32, 32) # 最終的なリサイズサイズ(例: 32x32ピクセル)
# 出力フォルダを作成
os.makedirs(output_folder, exist_ok=True)
# 画像を処理
for filename in os.listdir(input_folder):
file_path = os.path.join(input_folder, filename)
try:
with Image.open(file_path) as img:
# トリミング&リサイズ
resized_img = crop_and_resize(img, target_size)
# 出力フォルダに保存
resized_img.save(os.path.join(output_folder, filename))
print(f"Processed: {filename}")
except Exception as e:
print(f"Error processing {filename}: {e}")
materialsフォルダの中の素材画像を使用してtest.jpgを作ります. 以下では単純にRGB座標間のユークリッド距離が小さくなるようにタイルを選んでいますが, list(np.argsort(np.linalg.norm(avg_colors - region_color, axis=1))) を書き換えることでCIEDE2000などにもできます.
import random
from PIL import Image
import os
import numpy as np
from tqdm import tqdm # 進捗バーの表示
from scipy.spatial import KDTree # kd木による素材画像選択の高速化
from collections import defaultdict
# 画像の平均色を計算する(画像を1点につぶして計算する)
def calculate_average_color(image):
image = image.resize((1, 1))
return np.array(image.getpixel((0, 0)), dtype=np.float32)
# 素材画像を読み込み画像と対応する平均色のリストを作成する
def load_material_images(folder_path):
material_images = [] # 素材画像のリスト
avg_colors = [] # 平均色のリスト
for filename in os.listdir(folder_path):
file_path = os.path.join(folder_path, filename)
try:
img = Image.open(file_path).convert("RGB")
avg_color = calculate_average_color(img)
material_images.append(img)
avg_colors.append(avg_color)
except:
print(f"Failed to process image: {file_path}")
return material_images, np.array(avg_colors)
# 色を許容値内で目標色に近づける
def apply_color_correction(original_color, target_color, tolerance):
adjustment = np.clip(target_color - original_color, -tolerance, tolerance)
corrected_color = original_color + adjustment
corrected_color = np.clip(corrected_color, 0, 255) # RGBの範囲に制限
return corrected_color
def create_mosaic(base_image_path, material_images_folder, tile_size, output_path, tolerance=10, max_usage=None, random_percentage=0.1):
# ベース画像の読み込み
base_image = Image.open(base_image_path).convert("RGB")
base_image = base_image.resize((base_image.width // tile_size, base_image.height // tile_size))
base_colors = np.array([[base_image.getpixel((x, y)) for x in range(base_image.width)] for y in range(base_image.height)])
# 素材画像の読み込み
material_images, avg_colors = load_material_images(material_images_folder)
kdtree = KDTree(avg_colors) # KDTreeの構築
# 素材画像の使用回数を記録
usage_counts = defaultdict(int)
# 空のモザイク画像を作成
mosaic = Image.new("RGB", (base_image.width * tile_size, base_image.height * tile_size))
# タイル座標リストを作成してランダムにシャッフル
tile_coords = [(x, y) for y in range(base_image.height) for x in range(base_image.width)]
random.shuffle(tile_coords) # 座標をランダムにシャッフル
# ランダムで配置するタイルの割合に基づき、処理の一部をランダムに変更
random_count = int(len(tile_coords) * random_percentage) # ランダムに配置するタイルの数
random_tiles = random.sample(tile_coords, random_count) # ランダムに選んだタイル
# タイルごとの進捗バー
total_tiles = base_image.width * base_image.height
with tqdm(total=total_tiles, desc="Processing tiles", unit="tile") as pbar:
for x, y in tile_coords: # 順番に処理
region_color = base_colors[y, x]
# ランダムに配置するタイルなら、ランダムに素材を選択
if (x, y) in random_tiles:
idx = random.choice(range(len(material_images))) # ランダムに素材画像を選択
else:
# KDTreeを使用して最も近い素材画像を検索
if max_usage == None:
_, idx = kdtree.query(region_color)
# max_usageの制限を考慮して素材画像を選択
if max_usage is not None:
candidates = list(np.argsort(np.linalg.norm(avg_colors - region_color, axis=1)))
for candidate_idx in candidates:
if usage_counts[candidate_idx] %lt; max_usage:
idx = candidate_idx
break
# 素材画像を取得し、使用回数を増加
closest_img = material_images[idx]
usage_counts[idx] += 1
# 元画像領域と素材画像の色補正
corrected_color = apply_color_correction(avg_colors[idx], region_color, tolerance)
corrected_tile = closest_img.resize((tile_size, tile_size))
# タイル画像の補正を適用
tile_array = np.array(corrected_tile, dtype=np.float32)
correction_factor = corrected_color / avg_colors[idx]
corrected_tile_array = tile_array * correction_factor
corrected_tile_array = np.clip(corrected_tile_array, 0, 255).astype(np.uint8)
corrected_tile_image = Image.fromarray(corrected_tile_array)
# 補正後のタイルを配置
mosaic.paste(corrected_tile_image, (x * tile_size, y * tile_size))
pbar.update(1)
mosaic.save(output_path)
print(f"モザイクアートを保存しました: {output_path}")
# 実行例
base_image_path = "test.jpg"
material_images_folder = "materials"
tile_size = 32 # タイルサイズ
tolerance = 30 # 色補正の許容値(0~255 大きすぎると元の色味が失われます)
max_usage = 5 # 1枚の素材を最大で何回使うか(大きすぎると同じ画像ばかり使われる)
random_percentage = 0.05 # 5%のタイルをランダムに配置
output_path = f"mosaic_art_size{tile_size}_randomOrder_{int(random_percentage*100)}percent_tolerance{tolerance}_maxusage{max_usage}.jpg"
create_mosaic(base_image_path, material_images_folder, tile_size, output_path, tolerance=tolerance, max_usage=max_usage, random_percentage=random_percentage)
秋の草木と見る本栖湖 - 写真ACを6400×4820にリサイズしてベース画像としました.