はじめに

こんにちは。エンジニアの荒井です。
今回はPythonを使って、画像を色合いでグルーピングするスクリプトを作ってみました。

概要・背景

皆さんのPCやスマートフォンの中には、たくさんの写真データが眠っているのではないでしょうか?
旅行の思い出、イベントの記録、趣味で撮りためた風景写真など、気づけばストレージ容量を圧迫していることも少なくありません。
そこで、今回はPythonの力を借りて、これらの画像を「色」という特徴で自動的に分類し、整理するスクリプトを作成してみました。

今回作成したスクリプトは、指定したフォルダ内のJPEG画像を解析し、画像全体の色合いが似ているものを自動的にグループ化して出力するというものです。

実際に作ってみる

開発環境

macOS 15.4
Docker 20.10.17
Python 3.9.16

Githubリポジトリ

作成したコードは以下のリポジトリに格納しております。
https://github.com/milldea/blog-image-grouper

要件

  • 複数のJPEG画像を1つのディレクトリに格納した状態をinputとする
  • グループ毎にディレクトリを分けた状態をoutputとする
  • 画像全体の色合いでグループ分けする
  • 分割するグループ数は事前に指定する

設計

以下のような流れで処理しようと思います。

1. 入力画像を取得

指定した入力ディレクトリから、.jpegまたは.jpgの拡張子を持つ画像ファイルのパスを取得し、リストアップします。

2. 色の特徴量を抽出

画像の類似性を判断するために、画像全体の色分布を表す カラーヒストグラム ※1 を 特徴量 ※2 として利用します。

※1 カラーヒストグラム: 画像内の各色の出現頻度を数値化したもので、画像全体の雰囲気や主要な色調を捉えるのに適しています。
※2 特徴量: RGB各チャンネル(赤・緑・青)ごとにヒストグラムを計算し、それらを結合することで、画像の色情報を表現するデータです。

3. クラスタリングアルゴリズムでグループ分け

抽出したカラーヒストグラムに基づいて、画像の類似度を数値的に評価します。
直接的な類似度計算ではなく、類似した特徴量を持つ画像を自動的にグループ化します。
画像のグループ化には、 教師なし学習アルゴリズム ※3 の一種である クラスタリングアルゴリズム ※4 の中で、代表的な K-means 法を採用します。
K-means 法は、事前に指定したクラスタ数に基づいて、データ点を最も近いクラスタ中心に割り当てることでグループ分けを行うアルゴリズムです。
実装が比較的容易でありながら、画像の色のような連続的な特徴量を持つデータのクラスタリングに適しています。

※3 教師なし学習アルゴリズム: 正解ラベル(教師データ)を与えずに、アルゴリズム自身がデータ内のパターンや構造を学習する手法です。詳細については専門外のため省略します...気になる方はぜひ専門的な解説を別途ご参照ください。
※4 クラスタリングアルゴリズム: データセット内のデータを、その類似性に基づいてグループ分けする手法の総称です。

4. 出力

グルーピングの結果は、指定した出力ディレクトリ内に、グループごとに個別のフォルダを作成して出力します。
各フォルダ名にはグループ番号が付与され、そのフォルダ内に対応する画像ファイルのコピーを保存します。

実装

1. 入力画像を取得

指定した入力ディレクトリから、 .jpeg または .jpg の拡張子を持つ画像ファイルのパスを取得・リストアップする処理は、以下の get_jpeg_files 関数で実装しました。

def get_jpeg_files(directory):
    jpeg_files = []
    for f in os.listdir(directory):
        if f.lower().endswith(('.jpeg', '.jpg')):
            jpeg_files.append(os.path.join(directory, f))
    return jpeg_files
  • os.listdir() を用いて指定されたディレクトリ内のファイルとフォルダの一覧を取得する
  • 取得した一覧に対してfor ループを用いて、拡張子が .jpeg または .jpg であるファイルのみを抽出し、そのフルパスをリスト jpeg_files に追加して返却

2. 色の特徴量を抽出

画像の色の特徴量を抽出する処理は、以下の extract_color_histogram 関数で実装しました。

from PIL import Image
import numpy as np

def _calculate_channel_histogram(image_array_flattened, channel_index, bins):
    return np.histogram(image_array_flattened[channel_index::3], bins=bins)[0]

def extract_color_histogram(image_path, bins=8):
    try:
        img = Image.open(image_path).convert('RGB')
        img_array_flattened = np.array(img).flatten()
        histograms = []
        for i in range(3):
            histogram_channel = _calculate_channel_histogram(img_array_flattened, i, bins)
            histograms.append(histogram_channel)
        histogram = np.concatenate(histograms)
        return histogram / (img.width * img.height)
    except Exception as e:
        print(f"Error processing {image_path}: {e}")
        return None
  • PIL ライブラリで画像を RGB 形式で読み込む
  • NumPy 配列に変換して平坦化する
  • for ループを用いて 3チャンネル(R/G/B) に対して _calculate_channel_histogram 関数を呼び出す
  • ヒストグラムを計算し histograms リストに追加する
  • histograms 内のヒストグラムを結合して正規化されたカラーヒストグラムを返す

3. クラスタリングアルゴリズムでグループ分け

抽出したカラーヒストグラムを特徴量として、クラスタリングアルゴリズムに渡します。
K-means 法は、特徴量空間上での距離に基づいてクラスタリングを行うため、実質的に類似度の高い画像が同じグループに分類されることになります。
画像のグループ分けには、sklearn.cluster.KMeans を用います。

from sklearn.cluster import KMeans
from collections import defaultdict

def group_similar_images_by_color_and_output(input_directory, output_directory, n_clusters):
    # ... 省略 ...
    kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
    kmeans.fit(features)
    labels = kmeans.labels_

    grouped_images = defaultdict(list)
    for i, label in enumerate(labels):
        grouped_images[label].append(valid_files[i])
    # ... 省略 ...
  • 指定したクラスタ数 (n_clusters) で KMeans オブジェクトを初期化する
  • 抽出した特徴量 (features) を用いてクラスタリングを実行する
    • kmeans.labels_ に各画像がどのクラスタに属するかのラベルが格納される

4. 出力

グルーピングされた画像を、指定した出力ディレクトリにグループごとにフォルダを作成して保存します。

import os
import shutil

def group_similar_images_by_color_and_output(input_directory, output_directory, n_clusters):
    # ... 省略 ...
    # 出力ディレクトリが存在しない場合は作成
    os.makedirs(output_directory, exist_ok=True)

    # グループごとにディレクトリを作成し、画像をコピー
    for label, files in grouped_images.items():
        group_directory = os.path.join(output_directory, f"group_{label + 1}")
        os.makedirs(group_directory, exist_ok=True)
        print(f"Creating directory: {group_directory}")
        for file in files:
            try:
                shutil.copy2(file, os.path.join(group_directory, os.path.basename(file)))
                print(f"- Copied {os.path.basename(file)} to {group_directory}")
            except Exception as e:
                print(f"Error copying {file}: {e}")
        print("-" * 20)
  • 出力ディレクトリとグループごとのサブディレクトリを作成する
  • 元の画像ファイルを出力先のグループフォルダにコピーする

実行してみる

手順

  1. 上記のPythonスクリプトを .py ファイル(例: main.py)として保存します。
  2. 整理したいJPEG画像が格納されたディレクトリを作成し、スクリプト内の input_directory 変数にそのパスを設定します。
  3. 整理後の画像を保存するための空のディレクトリを作成し、スクリプト内の output_directory 変数にそのパスを設定します。
  4. スクリプト内の num_clusters 変数を、希望するグループ数に変更します。
  5. ターミナルまたはコマンドプロンプトでスクリプトを実行します。
    python3 main.py   
    

実行が完了すると、output_directory に指定したフォルダ内に group_1, group_2, ... といったグループ分けされたフォルダが作成され、それぞれのフォルダに似た色合いの画像がコピーされています。

結果

今回は2パターンで実行してみました。
サンプルとしてBEIZ imagesのフリー素材を使用しています。

パターン1

空、花火、木をメインとした写真を3枚ずつinputディレクトリに格納しました。

----------2025-05-16-13.35.46

意図的に、木の画像の一部(tree01, tree02)には青空が半分程度含まれており、アルゴリズムが空との類似性をどのように判断するかのテストケースとしています。
一方、tree03 は空がほとんど含まれておらず、全体的に暗めのトーンです。

# python3 main.py
Creating directory: output/group_2
- Copied fireworks01.jpg to output/group_2
- Copied fireworks02.jpg to output/group_2
- Copied fireworks03.jpg to output/group_2
- Copied tree03.jpg to output/group_2
--------------------
Creating directory: output/group_3
- Copied sky01.jpg to output/group_3
- Copied sky02.jpg to output/group_3
- Copied sky03.jpg to output/group_3
--------------------
Creating directory: output/group_1
- Copied tree01.jpg to output/group_1
- Copied tree02.jpg to output/group_1
--------------------
Image grouping and output complete.

結果として、意図的に入れた空を含まない木の画像(tree03)が花火のグループに分類されてしまいました。
tree03 の全体的な暗い色調が、花火の画像の色調と部分的に類似していたためと考えられます。

パターン2

木の画像のバリエーションを増やすため、さらに3枚の画像(tree04, tree05, tree06)を追加しました。

----------2025-05-16-13.36.28

tree04tree05tree03 と同様に空がほとんど含まれていませんが、全体的なトーンは明るめです。
tree06tree01, tree02 と同様に青空が半分以上を占めています。

# python3 main.py
Creating directory: output/group_1
- Copied fireworks01.jpg to output/group_1
- Copied fireworks02.jpg to output/group_1
- Copied fireworks03.jpg to output/group_1
--------------------
Creating directory: output/group_3
- Copied sky01.jpg to output/group_3
- Copied sky02.jpg to output/group_3
- Copied sky03.jpg to output/group_3
--------------------
Creating directory: output/group_2
- Copied tree01.jpg to output/group_2
- Copied tree02.jpg to output/group_2
- Copied tree03.jpg to output/group_2
- Copied tree04.jpg to output/group_2
- Copied tree05.jpg to output/group_2
- Copied tree06.jpg to output/group_2
--------------------
Image grouping and output complete.

この結果では、花火、空、木の画像が意図した通りにほぼ分離されました。
これは、教師なし学習であるK-means法が、入力データ全体の分布に基づいてクラスタリングを行うため、学習対象となるデータが増えたことで、より適切なグループ分けが可能になったと考えられます。

続きます

いかがでしたでしょうか。
今回は、Pythonを用いた画像の色によるグルーピングスクリプトの作成とその実行結果について、基本的な部分をご紹介しました。

大量の画像ファイルを色の特徴に基づいて整理できるようになったわけですが、「実際に何に使えるの?」と感じた方もいるかもしれません。
例えば、ご自身で撮影した数百枚のスナップ写真を少ないクラスタ数で実行してみると、どのような色合いの写真をよく撮っているかという傾向が見えてくるかもしれません。
また、主な背景が空の写真であれば、昼・夕・夜といった時間帯ごとのグループ分けも試せる可能性があります。
ぜひ、ご自身の画像データでこのスクリプトを試してみていただければ幸いです。

次回の記事では、さらにいくつかの実行例や、クラスタ数やヒストグラムのbin数といった設定値によるカスタマイズについてご紹介できればと思います。