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://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.aviを使用)、出力: 手ぶれ補正された動画(OpenCV画面でリアルタイム表示、処理状況をprint()表示、終了時にresult.txtとresult.mp4を保存)
#   - 処理手順: 1.動画フレームの読み込み 2.特徴点の検出と追跡 3.フレーム間の変換行列推定 4.モーション平滑化 5.画像の幾何学的変換による補正
#   - 前処理、後処理: 前処理:グレースケール変換による特徴点検出、後処理:境界部分のクロップによる黒い縁の除去
#   - 追加処理: ローパスフィルタによるモーション平滑化 - 急激なカメラ動作を滑らかにし自然な映像を実現、RANSAC(Random Sample Consensus)による外れ値除去 - 動く物体による誤った特徴点対応を除外、カメラ入力時のフレームバッファリング - リアルタイム処理と後処理保存を両立
#   - 調整を必要とする設定値: SMOOTHING_RADIUS(平滑化半径)- 値が大きいほど手ぶれ補正が強くなるが、意図的なカメラ動作も除去される可能性がある(デフォルト: 30)
# 将来方策: SMOOTHING_RADIUSの自動調整機能 - 動画の揺れの程度を分析し、適切な平滑化半径を自動的に決定する機能の実装
# その他の重要事項: 手ぶれ補正は平滑化半径(30フレーム)の遅延で動作し、初期30フレームでは黒いフレームが表示される。動画保存時は追加の処理時間が必要
# 前準備: pip install vidstab opencv-python numpy pillow

import cv2
import tkinter as tk
from tkinter import filedialog
import os
import time
from datetime import datetime
from vidstab import VidStab
import numpy as np
import urllib.request
from PIL import Image, ImageDraw, ImageFont

# 手ぶれ補正パラメータ
SMOOTHING_RADIUS = 30  # 平滑化半径:値が大きいほど補正が強くなる(1-100推奨)

# 表示設定
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20

frame_count = 0
results_log = []

def video_frame_processing(frame, stabilizer):
    global frame_count
    current_time = time.time()
    frame_count += 1

    # 手ぶれ補正処理
    stabilized_frame = stabilizer.stabilize_frame(input_frame=frame, smoothing_window=SMOOTHING_RADIUS)

    if stabilized_frame is None:
        stabilized_frame = frame  # 補正できない場合は元のフレームを使用

    # 手ぶれレベルの計算(簡易版)
    shake_level = calculate_shake_level(frame)

    # 日本語テキストを画像に表示
    try:
        font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
        img_pil = Image.fromarray(cv2.cvtColor(stabilized_frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(img_pil)
        draw.text((10, 30), f'フレーム: {frame_count}', font=font, fill=(0, 255, 0))
        draw.text((10, 60), f'平滑化: {SMOOTHING_RADIUS}', font=font, fill=(0, 255, 0))
        draw.text((10, 90), f'手ぶれレベル: {shake_level:.1f}%', font=font, fill=(0, 255, 0))
        stabilized_frame = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
    except:
        cv2.putText(stabilized_frame, f'Frame: {frame_count}', (10, 30),
                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
        cv2.putText(stabilized_frame, f'Smoothing: {SMOOTHING_RADIUS}', (10, 60),
                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
        cv2.putText(stabilized_frame, f'Shake: {shake_level:.1f}%', (10, 90),
                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)

    result = f'フレーム {frame_count}, 手ぶれレベル: {shake_level:.1f}%'
    return stabilized_frame, result, current_time

def calculate_shake_level(frame):
    """簡易的な手ぶれレベル計算"""
    # グレースケール変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # ラプラシアンフィルタで画像のぼけ具合を評価
    laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
    # 0-100の範囲に正規化(値は経験的に調整)
    shake_level = min(100, max(0, 100 - laplacian_var / 10))
    return shake_level

print('動画手ぶれ補正プログラム')
print('手ぶれのある動画を安定化します')
print('重要: 手ぶれ補正は遅延処理のため、初期フレームでは効果が限定的です')
print('')
print('=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')
print('')
print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")

choice = input("選択: ")

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

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)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
else:
    # サンプル動画ダウンロード・処理
    SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
    SAMPLE_FILE = 'vtest.avi'
    urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
    cap = cv2.VideoCapture(SAMPLE_FILE)

if not cap.isOpened():
    print('動画ファイル・カメラを開けませんでした')
    exit()

# メイン処理
print('\n=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')

# 動画保存用の設定
save_frames = []
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
if fps == 0 or fps > 120:
    fps = 30.0

try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        MAIN_FUNC_DESC = "手ぶれ補正"
        processed_frame, result, current_time = video_frame_processing(frame, stabilizer)
        cv2.imshow(MAIN_FUNC_DESC, processed_frame)

        if choice == '1':  # カメラの場合
            print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], result)
            save_frames.append(frame.copy())
        else:  # 動画ファイルの場合
            print(frame_count, result)

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

finally:
    print('\n=== プログラム終了 ===')
    cap.release()
    cv2.destroyAllWindows()

    if results_log:
        with open('result.txt', 'w', encoding='utf-8') as f:
            f.write('=== 結果 ===\n')
            f.write(f'処理フレーム数: {frame_count}\n')
            f.write(f'平滑化半径: {SMOOTHING_RADIUS}\n')
            f.write('\n')
            f.write('\n'.join(results_log))
        print(f'\n処理結果をresult.txtに保存しました')

    # 動画保存
    if choice == '1' and len(save_frames) > 0:
        save_video = input('\n動画を保存しますか?(y/n): ')
        if save_video.lower() == 'y':
            print('手ぶれ補正済み動画を保存中...')
            fourcc = cv2.VideoWriter_fourcc(*'mp4v')
            out = cv2.VideoWriter('result.mp4', fourcc, fps, (width, height))

            # 保存用の新しいstabilizerを作成
            save_stabilizer = VidStab()
            for i, frame in enumerate(save_frames):
                stabilized = save_stabilizer.stabilize_frame(input_frame=frame, smoothing_window=SMOOTHING_RADIUS)
                if stabilized is not None:
                    out.write(stabilized)
                if i % 30 == 0:
                    print(f'保存中: {i}/{len(save_frames)} フレーム')

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