vidstabによる動画手ぶれ補正

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

管理者権限でコマンドプロンプトを起動し、以下のコマンドを実行する:


pip install vidstab opencv-python numpy

vidstabによる動画手ぶれ補正プログラム

AI能力の説明

このプログラムはカメラや動画ファイルから入力された画像に対して、ImageNetの1000クラスから最も適合するカテゴリを特定する。

主要技術

参考文献


# プログラム名: vidstabによる動画手ぶれ補正プログラム
# 特徴技術名: vidstab (Video Stabilization Library)
# 出典: Souza, A. (2020). vidstab: Video stabilization using Python. GitHub repository. https://github.com/AdamSpannbauer/python_video_stab
# 特徴機能: 適応的モーション推定と平滑化 - 特徴点追跡により複数フレーム間のカメラモーションを推定し、動く物体の影響を最小化しながら手ぶれを補正
# 学習済みモデル: 使用なし
# 方式設計:
#   - 関連利用技術: OpenCV - コンピュータビジョンライブラリ(特徴点検出、画像変換)、NumPy - 数値計算ライブラリ(行列演算)
#   - 入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://github.com/opencv/opencv/blob/master/samples/data/vtest.aviを使用)、出力: 手ぶれ補正された動画(OpenCV画面でリアルタイム表示、1秒ごとに処理状況をprint()表示、終了時にresult.txtとresult.mp4を保存)
#   - 処理手順: 1.動画フレームの読み込み 2.特徴点の検出と追跡 3.フレーム間の変換行列推定 4.モーション平滑化 5.画像の幾何学的変換による補正
#   - 前処理、後処理: 前処理:グレースケール変換による特徴点検出の高速化、後処理:境界部分のクロップによる黒い縁の除去
#   - 追加処理: ローパスフィルタによるモーション平滑化 - 急激なカメラ動作を滑らかにし自然な映像を実現、RANSAC(Random Sample Consensus)による外れ値除去 - 動く物体による誤った特徴点対応を除外、カメラ入力時のフレームバッファリング - リアルタイム処理と後処理保存を両立
#   - 調整を必要とする設定値: SMOOTHING_RADIUS(平滑化半径)- 値が大きいほど手ぶれ補正が強くなるが、意図的なカメラ動作も除去される可能性がある(デフォルト: 30)
# 将来方策: SMOOTHING_RADIUSの自動調整機能 - 動画の揺れの程度を分析し、適切な平滑化半径を自動的に決定する機能の実装
# その他の重要事項: ファイル動画は2パス処理(分析→補正)、カメラ入力はリアルタイム処理を実施。動画保存時は追加の処理時間が必要
# 前準備: pip install vidstab opencv-python numpy

import cv2
import tkinter as tk
from tkinter import filedialog
import os
import time
from vidstab import VidStab
import numpy as np

# 手ぶれ補正パラメータ
SMOOTHING_RADIUS = 30  # 平滑化半径:値が大きいほど補正が強くなる(1-100推奨)
DEFAULT_FPS = 30.0     # カメラ用デフォルトFPS
MAX_FPS = 120.0        # 異常値判定用の最大FPS

# 表示設定
FONT_SCALE = 1
FONT_COLOR = (0, 255, 0)
FONT_THICKNESS = 2
TEXT_POS_1 = (10, 30)
TEXT_POS_2 = (10, 60)

def save_stabilized_frames(frames, output_path, width, height, fps):
    """フレームリストから手ぶれ補正済み動画を保存"""
    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
    out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
    stab = VidStab()

    # 分析パス
    for frame in frames:
        stab.stabilize_frame(input_frame=frame, smoothing_window=SMOOTHING_RADIUS)

    # 書き込みパス
    for i, frame in enumerate(frames):
        stabilized = stab.stabilize_frame(input_frame=frame, border_type='black')
        if stabilized is not None:
            out.write(stabilized)
        if i % 30 == 0:
            print(f'保存中: {i}/{len(frames)} フレーム')

    out.release()

def save_stabilized_video(cap, output_path, width, height, fps):
    """VideoCaptureから手ぶれ補正済み動画を保存"""
    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
    out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
    stab = VidStab()

    # 分析パス
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        stab.stabilize_frame(input_frame=frame, smoothing_window=SMOOTHING_RADIUS)

    # 動画を最初から再読み込み
    cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

    # 書き込みパス
    frame_num = 0
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        stabilized = stab.stabilize_frame(input_frame=frame, border_type='black')
        if stabilized is not None:
            out.write(stabilized)

        if frame_num % 30 == 0:
            print(f'保存中: フレーム {frame_num}')
        frame_num += 1

    out.release()

def video_processing(frame, stabilizer):
    """フレームの手ぶれ補正処理"""
    return stabilizer.stabilize_frame(frame)

print('動画手ぶれ補正プログラム')
print('手ぶれのある動画を安定化します')
print('qキーで終了します')
print('')
print('0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')

choice = input('選択: ')
temp_file = None
frames = []  # カメラ入力時のフレームバッファ

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    if not path:
        exit()
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
elif choice == '2':
    # サンプル動画ダウンロード・処理
    import urllib.request
    url = 'https://github.com/opencv/opencv/raw/master/samples/data/vtest.avi'
    filename = 'vtest.avi'
    try:
        urllib.request.urlretrieve(url, filename)
        temp_file = filename
        cap = cv2.VideoCapture(filename)
    except Exception as e:
        print(f'動画のダウンロードに失敗しました: {url}')
        print(f'エラー: {e}')
        exit()
else:
    print('無効な選択です')
    exit()

# VidStabオブジェクトの初期化
stabilizer = VidStab()

# 結果を保存するリスト
results = []
last_print_time = time.time()

# メイン処理
try:
    print('手ぶれ補正処理を開始します...')

    if choice == '1':  # カメラの場合はリアルタイム処理
        frame_count = 0
        print('カメラからの入力を処理中...')

        while True:
            cap.grab()
            ret, frame = cap.retrieve()
            if not ret:
                break

            # フレームをバッファに保存
            frames.append(frame.copy())

            # 手ぶれ補正処理(リアルタイム)
            result = stabilizer.stabilize_frame(input_frame=frame, smoothing_window=SMOOTHING_RADIUS, border_type='black')

            # 処理結果の表示
            if result is not None:
                # フレーム番号を画像に表示
                cv2.putText(result, f'Frame: {frame_count}', TEXT_POS_1,
                           cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, FONT_COLOR, FONT_THICKNESS)
                cv2.putText(result, f'Smoothing: {SMOOTHING_RADIUS}', TEXT_POS_2,
                           cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, FONT_COLOR, FONT_THICKNESS)

                cv2.imshow('Video', result)

                # 1秒ごとに進捗を表示
                current_time = time.time()
                if current_time - last_print_time >= 1.0:
                    status = f'処理中: フレーム {frame_count}, 平滑化半径: {SMOOTHING_RADIUS}'
                    print(status)
                    results.append(status)
                    last_print_time = current_time

            frame_count += 1

            if cv2.waitKey(1) & 0xFF == ord('q'):
                break

    else:  # ファイル動画の場合は2パス処理
        # 最初のパスで動画を分析
        print('動画を分析中...')
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            stabilizer.stabilize_frame(input_frame=frame, smoothing_window=SMOOTHING_RADIUS)

        # 動画を最初から再読み込み
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

        print('手ぶれ補正を適用中...')
        frame_count = 0

        while True:
            cap.grab()
            ret, frame = cap.retrieve()
            if not ret:
                break

            # 手ぶれ補正処理
            result = stabilizer.stabilize_frame(input_frame=frame, border_type='black')

            # 処理結果の表示
            if result is not None:
                # フレーム番号を画像に表示
                cv2.putText(result, f'Frame: {frame_count}', TEXT_POS_1,
                           cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, FONT_COLOR, FONT_THICKNESS)
                cv2.putText(result, f'Smoothing: {SMOOTHING_RADIUS}', TEXT_POS_2,
                           cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, FONT_COLOR, FONT_THICKNESS)

                cv2.imshow('Video', result)

                # 1秒ごとに進捗を表示
                current_time = time.time()
                if current_time - last_print_time >= 1.0:
                    status = f'処理中: フレーム {frame_count}, 平滑化半径: {SMOOTHING_RADIUS}'
                    print(status)
                    results.append(status)
                    last_print_time = current_time

            frame_count += 1

            if cv2.waitKey(1) & 0xFF == ord('q'):
                break

finally:
    cap.release()
    cv2.destroyAllWindows()

    # 結果をファイルに保存
    if results:
        with open('result.txt', 'w', encoding='utf-8') as f:
            f.write('動画手ぶれ補正処理結果\n')
            f.write('=' * 50 + '\n')
            for result in results:
                f.write(result + '\n')
            f.write(f'\n総フレーム数: {frame_count}\n')
            f.write(f'使用した平滑化半径: {SMOOTHING_RADIUS}\n')
        print('result.txtに保存しました')

    # 動画保存の確認
    save_video = input('動画を保存しますか?(y/n): ')

    if save_video.lower() == 'y':
        print('手ぶれ補正済み動画を保存中...')

        try:
            # 動画書き込み設定
            if choice == '1' and len(frames) > 0:
                # カメラの場合はバッファから処理
                height, width = frames[0].shape[:2]
                fps = cap.get(cv2.CAP_PROP_FPS)
                if fps == 0 or fps > MAX_FPS:  # 異常値の場合はデフォルト値を使用
                    fps = DEFAULT_FPS

                save_stabilized_frames(frames, 'result.mp4', width, height, fps)

            else:
                # ファイル動画の場合は再読み込み
                if choice == '0':
                    cap = cv2.VideoCapture(path)
                else:  # choice == '2'
                    cap = cv2.VideoCapture(temp_file)

                # 動画情報取得
                fps = cap.get(cv2.CAP_PROP_FPS)
                if fps == 0 or fps > MAX_FPS:  # 異常値の場合はデフォルト値を使用
                    fps = DEFAULT_FPS
                width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
                height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

                save_stabilized_video(cap, 'result.mp4', width, height, fps)
                cap.release()

            print('result.mp4に保存しました')

        except Exception as e:
            print(f'動画の保存に失敗しました: {e}')

    if temp_file:
        os.remove(temp_file)