イラスト調の画像から、(リッチなAIなどは使わずに)自動で切り抜き範囲を推定してサムネイルっぽいものを生成する方法の解説です。
これは以前にブログでも紹介したイラスト管理アプリに最近実装した機能です。以前は新規の投稿を登録したときに先頭の画像をそのままサムネイルとして一覧ページで表示していましたが、解像度の高い画像が多いのに表示するサイズは小さいため画像の読み込みで毎回無駄なリソースを使っている気がしたので、200x200サイズの画像を事前に生成してそれをサムネイルとして使うように仕様変更しました。

プレビュー用の小さい画像を生成しておくというのは、画像をたくさん表示するギャラリーのようなWebサイトでよくとられる手法ですね。pixivとか。
表示するサイズは小さいのに毎回原寸大のデータを取りに行ってたら通信量もすごいことになるので、Webサイト運営をやるならほぼ対応必須の項目です。ただ、こういうケースではわざわざ自分で実装するよりCloudflare Imagesなどを利用した方が長期的にサービスを運営していく上で楽できるように思います。
自分の場合はデスクトップアプリだったので、自作で簡易的なサムネイル生成システムを実装してみました。この記事で紹介するサンプルコードは以下のリポジトリに置いてあります。
1. 顔認識でサムネ生成
どうやって機械的にサムネイル画像となる領域を決定するかですが、まず最初に思いつくのが顔認識です。キャラクターの顔が含まれる領域を選択すれば、それっぽいものができそうな感じがします。
顔認識をするとなると結構難しそうに聞こえるかもしれませんが、意外と簡単にできます。今回使うのは、OpenCVの物体検出の機能です。
これはCascade Classifierという機械学習ベースの手法ではあるのですが、非常に軽量で高速に動作します。分類器は以下の方が公開してくれているlbpcascade_animeface.xmlというやつを使います。GitHubのリポジトリからダウンロードしておいてください。
自分の実装したシステムでは、thumbnail_generator.pyのget_center_point内で使用されています。イラスト内に顔が複数含まれる場合、最も領域の大きいものの中心をサムネイルの中心座標として採用します。
def get_center_point(image_path: str, use_smartcrop=True): # 1. 顔検出 np_array = np.fromfile(image_path, np.uint8) image_cv = cv2.imdecode(np_array, cv2.IMREAD_COLOR) gray = cv2.cvtColor(image_cv, cv2.COLOR_BGR2GRAY) cascade = cv2.CascadeClassifier(CASCADE_MODEL_PATH) faces = cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(24, 24) ) print(f"{len(faces)} face(s) detected") if len(faces) > 0: # 最も大きい顔を選択 # 顔の座標: (x, y, w, h) x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) return (x + w // 2, y + h // 2)
この手法で例としていくつかサムネイルを自動生成してみました。左が元画像、右が生成されたサムネイルです。サムネが黒い画像となっているものは、顔が検出できなかった例です。
結果を見てもらえば分かる通り、以外とこの手法うまくいきません。元々正面顔でないとあまりうまくいかないらしいのですが、顔が正面を向いていてもちょっと周りに障害物があると検出できなかったり、そもそも横を向いていたりするとだめです。
モデルが結構古いため、最近のイラストだとうまくいってない気がします。明確に顔が描かれているイラストには有効ですが、もっと別のアプローチを考えたほうが良さそうです。
追記:顔検出をもっと頑張りたい場合
以下のyolov8_animefaceを使うとより高精度に顔認識ができそうです。10000件のイラストでトレーニングされたYOLOv8モデルが公開されています。
2. 画像の特徴を使う
顔を使ったサムネ生成をやってみましたが、顔を目印にするとキャラクターが後ろを向いているイラストやそもそもキャラクターが描かれていない風景画などに対応できません。
「もっと画像の色の情報などを活用できないだろうか?」と思っていたところ、以下のsmartcrop.pyというライブラリを見つけました。
もともとsmartcrop.jsというライブラリがあり、それをPython環境に移植したものです。
どのようなアルゴリズムで領域を決定しているのだろうと気になりましたが、READMEによると以下のような手続きになっているようです。古典的な手法のみで画像の切り抜きが実現されています。
- ラプラシアンフィルタによるエッジ検出
- 人の肌のような色を持つ領域を検出
- 色の彩度の高い領域を検出
- オプションで指定された領域の重みを強くする(顔検出など)
- 指定されたアスペクト比で、切り抜く領域の候補を大量に生成
- 評価関数で候補をランク付けする(中央付近にあるかどうか、画像の端付近を避ける)
- 最もスコアの良かった候補を出力
この手法の良いところは、どのような画像を入力しても必ず候補となる領域が得られるところです。顔が検出できなかった場合のような例外処理をしなくてよくなります。
今回自分が実装したものではOpenCVが顔を検出できなかった場合のフォールバックとしてsmartcropを使っていますが、このライブラリだけ使う実装にしても実用に耐えるかと思います。
ただ、古典的な手法だけ使っているとはいえ、入力画像が高解像度になると先程の手法よりもちょっとだけ重いです。
1.の手法のテストで使用した画像と同じものでテストしてみました。
先ほどはうまくサムネイル画像が生成できなかった例でも、それらしい領域が切り抜けています。
extra. 人の手で調整する
smartcropでかなり高い品質でサムネイル画像が自動生成できましたが、しばしば「ちょっと切り抜く範囲を調整したい」と思うときもあります。
こういう場合でも対応できるように、GUIで切り抜き範囲を確認・修正できるようにしました。
thumbnail_generator.pyを実行して顔認識→smartcropによる領域の推定が終わると、以下のようなGUIが立ち上がります。

最初に表示されているのはOpenCVかsmartcropが提案した切り抜き位置であり、位置を変えたければ黄色の枠をドラッグして移動することで切り抜かれる範囲を手動で修正できます。
修正する必要が無いと思った場合は「キャンセル」を押すことで表示されている範囲でそのまま切り抜きが行われます。
GUIはPySide6で作りました。作りましたというか、Geminiに書いてもらいました。
本当に数分くらいで作ったものなので、よしなに調理していただければと思います。






























