動画ファイルに対するシーン検出と人物検出



## はじめに
動画ファイル（MP4）に対してシーン検出と人物検出を行い、人物の有無に基づいてクリップを分類するpythonスクリプトを作成します。


## 使用ライブラリ一覧

| ライブラリ名 | 種類 | 主な用途 |
|------------|------|---------|
| OpenCV (cv2) | 外部ライブラリ | 画像・動画処理、Haar Cascade検出 |
| NumPy | 外部ライブラリ | 数値計算、配列操作 |
| PySceneDetect | 外部ライブラリ | 動画内のシーンカット検出 |
| MediaPipe | 外部ライブラリ | 高精度な人物姿勢検出 |
| os | 標準ライブラリ | ファイル・ディレクトリ操作 |
| shutil | 標準ライブラリ | ファイルのコピー・移動 |
| time | 標準ライブラリ | 処理時間計測 |
| argparse | 標準ライブラリ | コマンドライン引数処理 |

# 処理

## 主要機能
- シーンカット検出による動画分割
- MediaPipeまたはHaar Cascadeを使用した人物検出
- 人物の有無に基づくクリップ分類
- バッチ処理による複数動画の自動処理

## 機能設計

### 1.シーン検出機能

#### 関数: `detect_scenes(video_path, threshold=30.0)`
**目的**: 動画内のシーンカットを検出

**パラメータ**:
- `video_path` (str): 処理対象の動画ファイルパス
- `threshold` (float): シーン検出の閾値（デフォルト: 30.0）

**戻り値**:
- `scene_cuts` (list): シーンカットのフレーム番号リスト

**処理フロー**:
1. VideoManagerとSceneManagerを初期化
2. ContentDetectorを指定閾値で追加
3. 処理速度向上のためダウンスケール設定
4. シーン検出を実行
5. シーンカットのフレーム番号を抽出
6. リソースを解放


### 2.人物検出機能

#### 関数: `has_human_mediapipe(video_path, sample_rate=24, confidence_threshold=0.5)`
**目的**: MediaPipeを使用した人物検出

**パラメータ**:
- `video_path` (str): 動画ファイルパス
- `sample_rate` (int): フレームサンプリング間隔
- `confidence_threshold` (float): 検出信頼度閾値

**戻り値**:
- `bool`: 人物検出結果

**処理フロー**:
1. MediaPipeポーズ検出を初期化
2. 動画を開いてフレームを順次処理
3. サンプリング間隔に基づいてフレームを選択
4. BGRからRGBに変換
5. ポーズランドマークを検出
6. 検出結果に基づいて判定

#### 関数: `detect_humans_haarcascade(video_path, sample_rate=24)`
**目的**: Haar Cascadeを使用した人物検出（フォールバック機能）

**パラメータ**:
- `video_path` (str): 動画ファイルパス
- `sample_rate` (int): フレームサンプリング間隔

**戻り値**:
- `bool`: 人物検出結果

**使用するCascade**:
- `haarcascade_fullbody.xml`: 全身検出
- `haarcascade_frontalface_default.xml`: 顔検出
- `haarcascade_upperbody.xml`: 上半身検出

#### 関数: `has_human(video_path, sample_rate=24)`
**目的**: 統合人物検出

**処理優先度**:
1. MediaPipe
2. Haar Cascade

### 3.動画分割機能

#### 関数: `split_video(video_path, output_dir, scene_cuts, fps)`
**目的**: シーンカットに基づく動画分割

**パラメータ**:
- `video_path` (str): 入力動画パス
- `output_dir` (str): 出力ディレクトリ
- `scene_cuts` (list): シーンカットフレーム番号
- `fps` (float): フレームレート

**戻り値**:
- `clip_paths` (list): 生成されたクリップファイルパスリスト

**処理フロー**:
1. 出力ディレクトリ作成
2. 動画プロパティ取得
3. シーン境界の設定（先頭0、末尾総フレーム数含む）
4. 各シーンごとにクリップ作成
5. フレーム書き込み
6. リソース解放

**技術仕様**
- **コーデック**: mp4v
- **ファイル命名**: `{動画名}_scene_{番号:03d}.mp4`
- **空シーンの処理**: 1フレーム以下のシーンはスキップ

### 4.バッチ処理機能

#### 関数: `process_videos(input_dir, output_base_dir)`
**目的**: ディレクトリ内の全MP4ファイルを一括処理

**パラメータ**:
- `input_dir` (str): 入力ディレクトリパス
- `output_base_dir` (str): 出力ベースディレクトリパス

**出力ディレクトリ構造**:
```
output_base_dir/
├── with_human/     # 人物検出されたクリップ
├── no_human/       # 人物検出されなかったクリップ
└── temp/          # 一時ファイル（処理後削除）
```

**処理フロー**:
1. 出力ディレクトリ構造作成
2. MP4ファイル検索
3. 各動画に対する処理：
   - シーン検出
   - 動画分割
   - 各クリップの人物検出
   - 分類ディレクトリへの移動
4. 処理結果レポート出力

---

# 実装！

```python
import os
import cv2
import numpy as np
import shutil
import time
from scenedetect import VideoManager
from scenedetect import SceneManager
from scenedetect.detectors import ContentDetector

# MediaPipeが利用可能か確認
try:
    import mediapipe as mp
    MEDIAPIPE_AVAILABLE = True
except ImportError:
    MEDIAPIPE_AVAILABLE = False
    print("MediaPipeが見つかりません。代替方法で人物検出を行います")

def detect_scenes(video_path, threshold=30.0):
    """動画内のシーンカットを検出する"""
    # VideoManagerとSceneManagerを作成
    video_manager = VideoManager([video_path])
    scene_manager = SceneManager()
    
    # しきい値でContentDetectorを追加
    scene_manager.add_detector(ContentDetector(threshold=threshold))
    
    # 処理速度向上のためダウンスケール係数を設定
    video_manager.set_downscale_factor()
    
    # VideoManagerを開始
    video_manager.start()
    
    # シーン検出を実行
    scene_manager.detect_scenes(frame_source=video_manager)
    
    # シーンリストを取得
    scene_list = scene_manager.get_scene_list()
    
    # VideoManagerを解放
    video_manager.release()
    
    # シーンカットのフレーム番号を抽出
    scene_cuts = []
    for scene in scene_list:
        scene_cuts.append(scene[0].frame_num)
    
    return scene_cuts

def has_human_mediapipe(video_path, sample_rate=24, confidence_threshold=0.5):
    """MediaPipeを使用して動画内に人物が含まれているか確認する"""
    if not MEDIAPIPE_AVAILABLE:
        return False
    
    # MediaPipeポーズ検出を初期化
    mp_pose = mp.solutions.pose
    pose = mp_pose.Pose(min_detection_confidence=confidence_threshold)
    
    # 動画を開く
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print(f"動画ファイルを開けませんでした: {video_path}")
        return False
    
    frame_count = 0
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        # sample_rateフレームごとに処理
        if frame_count % sample_rate == 0:
            # BGRをRGBに変換
            rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            
            # 画像を処理してポーズを検出
            results = pose.process(rgb_frame)
            
            # ポーズランドマークが検出されたか確認
            if results.pose_landmarks:
                cap.release()
                pose.close()
                return True
        
        frame_count += 1
    
    cap.release()
    pose.close()
    return False

def detect_humans_haarcascade(video_path, sample_rate=24):
    """Haar Cascadeを使用して人物を検出"""
    # 人体検出用の事前学習済みHaar Cascadeを読み込み
    body_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_fullbody.xml')
    face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
    upper_body_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_upperbody.xml')
    
    # 動画を開く
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print(f"動画ファイルを開けませんでした: {video_path}")
        return False
    
    frame_count = 0
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        # sample_rateフレームごとに処理
        if frame_count % sample_rate == 0:
            # グレースケールに変換
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            
            # 人体と顔を検出
            bodies = body_cascade.detectMultiScale(gray, 1.1, 4)
            faces = face_cascade.detectMultiScale(gray, 1.1, 4)
            upper_bodies = upper_body_cascade.detectMultiScale(gray, 1.1, 4)
            
            # 人体または顔が検出された場合
            if len(bodies) > 0 or len(faces) > 0 or len(upper_bodies) > 0:
                cap.release()
                return True
        
        frame_count += 1
    
    cap.release()
    return False

def has_human(video_path, sample_rate=24):
    """利用可能な方法を使用して動画内に人物が含まれているか確認する"""
    # 可能であればMediaPipeを最初に試す
    if MEDIAPIPE_AVAILABLE:
        if has_human_mediapipe(video_path, sample_rate):
            return True
    
    # Haar Cascade検出にフォールバック
    return detect_humans_haarcascade(video_path, sample_rate)

def split_video(video_path, output_dir, scene_cuts, fps):
    """シーンカットで動画を分割してクリップを保存する"""
    # 出力ディレクトリが存在しない場合は作成
    os.makedirs(output_dir, exist_ok=True)
    
    # 拡張子なしの動画ファイル名を取得
    video_name = os.path.splitext(os.path.basename(video_path))[0]
    
    # 動画ファイルを開く
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print(f"動画ファイルを開けませんでした: {video_path}")
        return []
    
    # 動画プロパティを取得
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # mp4コーデック
    
    # 先頭にフレーム0、末尾に合計フレーム数を追加
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    scene_boundaries = [0] + scene_cuts + [total_frames]
    
    # 各シーンの動画クリップを作成
    clip_paths = []
    for i in range(len(scene_boundaries) - 1):
        start_frame = scene_boundaries[i]
        end_frame = scene_boundaries[i+1]
        
        # 空のシーンをスキップ
        if end_frame - start_frame <= 1:
            continue
        
        # このクリップの出力パスを定義
        clip_path = os.path.join(output_dir, f"{video_name}_scene_{i:03d}.mp4")
        clip_paths.append(clip_path)
        
        # このクリップのビデオライターを作成
        out = cv2.VideoWriter(clip_path, fourcc, fps, (width, height))
        
        # フレーム位置を開始フレームに設定
        cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
        
        # このクリップにフレームを追加
        for _ in range(start_frame, end_frame):
            ret, frame = cap.read()
            if not ret:
                break
            out.write(frame)
        
        # ビデオライターを解放
        out.release()
    
    # ビデオキャプチャを解放
    cap.release()
    
    return clip_paths

def process_videos(input_dir, output_base_dir):
    """入力ディレクトリ内のすべてのMP4ファイルを処理する"""
    # 出力ディレクトリを作成
    os.makedirs(output_base_dir, exist_ok=True)
    human_dir = os.path.join(output_base_dir, "with_human")
    no_human_dir = os.path.join(output_base_dir, "no_human")
    os.makedirs(human_dir, exist_ok=True)
    os.makedirs(no_human_dir, exist_ok=True)
    
    # 入力ディレクトリ内のすべてのMP4ファイルを検索
    mp4_files = [f for f in os.listdir(input_dir) if f.lower().endswith('.mp4')]
    
    if not mp4_files:
        print(f"{input_dir}にMP4ファイルが見つかりません")
        return
    
    for file_name in mp4_files:
        video_path = os.path.join(input_dir, file_name)
        print(f"\n動画処理中: {video_path}")
        
        # クリップ用の一時ディレクトリを作成
        temp_dir = os.path.join(output_base_dir, "temp")
        os.makedirs(temp_dir, exist_ok=True)
        
        try:
            # 動画を開いてプロパティを取得
            cap = cv2.VideoCapture(video_path)
            if not cap.isOpened():
                print(f"動画ファイルを開けませんでした: {video_path}")
                continue
            
            fps = cap.get(cv2.CAP_PROP_FPS)
            total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
            duration = total_frames / fps
            
            print(f"動画情報: {total_frames}フレーム, {fps:.2f}fps, {duration:.2f}秒")
            cap.release()
            
            # シーン検出
            print("シーンを検出中...")
            start_time = time.time()
            scene_cuts = detect_scenes(video_path)
            scene_detect_time = time.time() - start_time
            print(f"{len(scene_cuts)}個のシーンカットを検出しました（所要時間: {scene_detect_time:.2f}秒）")
            
            # 動画をクリップに分割
            print("動画を分割中...")
            start_time = time.time()
            clip_paths = split_video(video_path, temp_dir, scene_cuts, fps)
            split_time = time.time() - start_time
            print(f"{len(clip_paths)}個のクリップを作成しました（所要時間: {split_time:.2f}秒）")
            
            # 各クリップを処理
            human_clips = 0
            non_human_clips = 0
            
            for clip_path in clip_paths:
                clip_name = os.path.basename(clip_path)
                print(f"クリップ内の人物を確認中: {clip_name}")
                
                # クリップに人物が含まれているか確認
                start_time = time.time()
                has_humans = has_human(clip_path)
                detect_time = time.time() - start_time
                
                # クリップを適切なディレクトリに移動
                if has_humans:
                    dest_dir = human_dir
                    human_clips += 1
                    print(f"  人物が検出されました！（所要時間: {detect_time:.2f}秒）")
                else:
                    dest_dir = no_human_dir
                    non_human_clips += 1
                    print(f"  人物は検出されませんでした（所要時間: {detect_time:.2f}秒）")
                
                dest_path = os.path.join(dest_dir, clip_name)
                shutil.move(clip_path, dest_path)
            
            print(f"\n{file_name}の処理結果:")
            print(f"  合計クリップ数: {len(clip_paths)}")
            print(f"  人物ありクリップ: {human_clips}")
            print(f"  人物なしクリップ: {non_human_clips}")
            
        except Exception as e:
            print(f"{file_name}の処理中にエラーが発生しました: {str(e)}")
        
        # 一時ディレクトリを削除
        try:
            os.rmdir(temp_dir)
        except:
            pass

if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description='動画ファイルのシーン検出と人物検出を行います。')
    parser.add_argument('input_dir', type=str, help='MP4ファイルが含まれるディレクトリ')
    parser.add_argument('output_dir', type=str, help='出力ベースディレクトリ')
    parser.add_argument('--threshold', type=float, default=30.0, help='シーン検出のしきい値（デフォルト: 30.0）')
    parser.add_argument('--sample_rate', type=int, default=24, help='人物検出でスキップするフレーム数（デフォルト: 24）')
    
    args = parser.parse_args()
    
    start_time = time.time()
    process_videos(args.input_dir, args.output_dir)
    total_time = time.time() - start_time
    
    print(f"\n処理完了！合計時間: {total_time:.2f}秒")
```

## おわり

動画ファイルに対するシーン検出と人物検出スクリプトを作成しました。