はじめに
「MVや音楽番組で、推しが映っている一瞬たりとも見逃したくない」
「場面切り替えや動きが早くて目が追いつかない」
K-POPオタクなら一度は抱えるこの悩み。
動体視力を鍛えるのも限界があるため、Pythonの力を使って 「動画の中から推しを自動検出し、マーキングするAI」 を自作してみました。
本記事では、技術的な実装手順だけでなく、 「K-POPアイドルならではの難易度の高さ」と、それを「圧倒的な推しへの愛」 で解決した過程を共有します。
開発環境・使用ライブラリ
- Python 3.10
- face_recognition: 顔認識ライブラリ(dlibベース)
- OpenCV: 動画処理
- MoviePy: 音声合成(OpenCVだけだと音が消えるため)
- Google Colab: 実行環境
直面した課題:K-POPアイドルの「ビジュアル変化」問題
開発当初、「推しの顔なんて10枚くらい学習させれば分かるだろう」と思っていました。
しかし、K-POPの世界はそんなに甘くありませんでした。
1. カムバごとのビジュアル激変
K-POPアイドルは、新曲(カムバック)のたびにコンセプトに合わせて髪色、髪型、メイク、衣装が劇的に変化します。
- 「清涼コンセプト」の時のナチュラルメイク
- 「ダークコンセプト」の時のスモーキーメイク
- 突然の金髪、赤髪、派手髪
これら全てを「同一人物」としてAIに認識させる必要がありました。
2. メンバーの識別難易度
同じコンセプトを共有しているため、衣装やメイクの系統が似通うことがあります。
初期のテスト(学習画像50枚)では、顔が似ている別のメンバーを推しと誤検知する事象が多発しました。
解決策:愛の重さ(データ量)で殴る
精度向上のため、以下の対策を行いました。
対策①:学習データを50枚→500枚へ増量
「AIが迷うなら、迷わないくらい教え込めばいい」
過去のMV、チッケム(推しカメラ)、セルカ(自撮り)などから、あらゆる角度・表情・メイクパターンの推し画像を500枚を学習させました。
推し活をしている人間からすれば、推しのベストショットを500枚選別してフォルダに分ける作業など、朝飯前です。
この作業が一番楽しかったかもしれません。
対策②:フレームレートは妥協しない(30fps)
最初は処理速度を上げるために「10フレームに1回判定」などで試しました。
しかし、K-POPのダンスはとても高速でキレがあるため、フレームを間引くと、「ターンした一瞬」や「キメ顔」 のタイミングで枠がズレてしまいました。
推し活において「一瞬の見逃し」は許されません。
処理時間はかかりますが、全フレームで解析する方針に切り替えました。
実装コード
以下は、Google Colabで動作するコードの抜粋です。
!pip install face_recognition moviepy
import face_recognition
import cv2
import os
import numpy as np
from moviepy.editor import VideoFileClip
from IPython.display import clear_output
# === 設定 ===
input_video_path = "input.mp4" # 解析したい動画
final_video_path = "final_output.mp4" # 完成動画
reference_folder = "references" # 推しの画像が入ったフォルダ
tolerance = 0.40 # 判定の厳しさ(0.6だと甘すぎるため厳しく設定)
frame_skip_rate = 1 # 1=全フレーム解析(推し活推奨設定)
# === 1. 推しの顔を学習 ===
print(f"【Step 1】推しの顔を学習中...(対象画像: {len(os.listdir(reference_folder))}枚)")
known_face_encodings = []
for filename in os.listdir(reference_folder):
if filename.endswith((".jpg", ".png", ".jpeg")):
img_path = os.path.join(reference_folder, filename)
try:
image = face_recognition.load_image_file(img_path)
# 顔が検出できた場合のみ学習
encodings = face_recognition.face_encodings(image)
if len(encodings) > 0:
known_face_encodings.append(encodings[0])
except Exception:
pass
print(f"学習完了! {len(known_face_encodings)}パターンの推しを脳に刻み込みました。")
# === 2. 動画解析(OpenCV) ===
input_movie = cv2.VideoCapture(input_video_path)
length = int(input_movie.get(cv2.CAP_PROP_FRAME_COUNT))
fps = input_movie.get(cv2.CAP_PROP_FPS)
# 一時保存用(音なし)
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
output_movie = cv2.VideoWriter("temp_silent.mp4", fourcc, fps, (int(input_movie.get(3)), int(input_movie.get(4))))
frame_number = 0
last_face_locations = []
last_face_names = []
print("【Step 2】動画解析スタート")
while True:
ret, frame = input_movie.read()
if not ret:
break
# 指定フレームごとに解析
if frame_number % frame_skip_rate == 0:
# 高速化のため縮小して検出
small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)
face_locations = face_recognition.face_locations(rgb_small_frame)
face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations)
face_names = []
for face_encoding in face_encodings:
# 登録データと照合
matches = face_recognition.compare_faces(known_face_encodings, face_encoding, tolerance=tolerance)
name = "Unknown"
if True in matches:
name = "OSHI" # 推し発見!
face_names.append(name)
last_face_locations = face_locations
last_face_names = face_names
else:
# スキップしたフレームは前の結果を引き継ぐ
face_locations = last_face_locations
face_names = last_face_names
# 描画処理
for (top, right, bottom, left), name in zip(face_locations, face_names):
top *= 4; right *= 4; bottom *= 4; left *= 4
if name == "OSHI":
# 推しだけを緑の太枠で囲む
cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 4)
cv2.putText(frame, "MY OSHI", (left, bottom - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
output_movie.write(frame)
frame_number += 1
if frame_number % 50 == 0:
clear_output(wait=True)
print(f"解析中... {frame_number}/{length} フレーム ({(frame_number/length)*100:.1f}%)")
input_movie.release()
output_movie.release()
# === 3. 音声合成(MoviePy) ===
# OpenCVで処理すると音が消えるため、元動画から音声を結合
print("【Step 3】音声合成中...")
video_clip = VideoFileClip("temp_silent.mp4")
audio_clip = VideoFileClip(input_video_path).audio
final_clip = video_clip.set_audio(audio_clip)
final_clip.write_videofile(final_video_path, codec='libx264', audio_codec='aac')
print("完了!")
結果
結論から言うと、精度は劇的に向上しました。
500枚の学習データのおかげで、激しいダンス中でも、しっかりと緑の枠が追従してくれるようになりました。
成果物の動画について
ここで「実際に完成した動画」を推しのご尊顔と一緒にお見せしたいところなのですが、K-POPアイドルの映像には著作権および肖像権が存在するため、解析結果の動画をここに掲載することはできません。。
動作イメージとしては、映像で推しの顔だけが常に緑色の四角でロックオンされている状態をご想像ください。
今後の展望
現状は枠をつけるだけですが、次はこれを進化させて 「推しが映っているシーンだけを自動で繋ぎ合わせた切り抜き動画を生成する機能」 を実装したいです。
まとめ
- AIの精度は、「推しへの愛(データ量)」で決まる
- K-POPアイドルのビジュアル変化に対応するには、500枚規模のデータセットが必要
- 推しを見逃さないためには、処理時間を犠牲にしてでも全フレーム解析すべき
Python×推し活、まだまだ無限の可能性がありそうです!