add ソースコードを追加

This commit is contained in:
2026-03-19 10:56:25 +00:00
parent bcd0664b5a
commit d94272fbac
5 changed files with 941 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
"""
ffmpeg を使って、Equirect 360 画像を複数の直線的パースペクティブ画像に変換するスクリプト
設定は config.py から読み込む
"""
import os
import glob
import shutil
import subprocess
import argparse
import re
# config から設定を読み込む
import config
def ensure_ffmpeg():
if shutil.which("ffmpeg") is None:
raise RuntimeError("ffmpeg が見つかりません。PATH を通すか、インストールしてください。")
def parse_args():
p = argparse.ArgumentParser(
description="Equirect 360 images -> multiple rectilinear perspective images using ffmpeg v360."
)
p.add_argument(
"subdir",
help="output/<subdir> を生成先にする。例: test_145frames_0min4sec_20260122_211204"
)
return p.parse_args()
def validate_subdir(subdir: str) -> str:
# ifの確認: パス注入対策(../ や \ などを拒否)
if not subdir or subdir.strip() == "":
raise RuntimeError("subdir が空です。処理を中断します。")
if any(sep in subdir for sep in ("/", "\\", ":", "..")):
raise RuntimeError("subdir に禁止文字が含まれています(/, \\, :, ..)。処理を中断します。")
# 文字を絞りたい場合(安全寄り)
if not re.fullmatch(r"[A-Za-z0-9._-]+", subdir):
raise RuntimeError("subdir は英数字と ._- のみ許可です。処理を中断します。")
return subdir
def build_transforms_14():
transforms = [
( 0, 90, 0),
( 0, -90, 0),
(-90, 0, 0),
( 0, 0, 0),
( 90, 0, 0),
(180, 0, 0),
(-135, 45, 0),
( -45, 45, 0),
( 45, 45, 0),
( 135, 45, 0),
(-135,-45, 0),
( -45,-45, 0),
( 45,-45, 0),
( 135,-45, 0),
]
return transforms
def build_transforms_dense(ring_step=None):
"""密集度高い変換パターンを構築"""
if ring_step is None:
ring_step = config.RING_STEP_DEG
def ring(yaw_step, pitch):
yaws = list(range(0, 360, yaw_step))
return [(yaw if yaw <= 180 else yaw-360, pitch, 0) for yaw in yaws]
transforms = []
transforms += [(0, 90, 0), (0, -90, 0)]
transforms += ring(ring_step, 0)
transforms += ring(ring_step, 45)
transforms += ring(ring_step, -45)
return transforms
def build_v360_options(yaw, pitch, roll):
"""v360フィルタオプションを構築"""
opts = [
"input=e",
"output=rectilinear",
f"h_fov={config.HORIZONTAL_FOV}",
f"v_fov={config.VERTICAL_FOV}",
f"w={config.PERSPECTIVE_WIDTH}",
f"h={config.PERSPECTIVE_HEIGHT}",
f"yaw={yaw}",
f"pitch={pitch}",
f"roll={roll}",
]
return "v360=" + ":".join(opts)
def build_output_jobs(base, output_dir, transforms, preset_name):
"""出力対象のジョブ一覧を構築する"""
digits = len(str(len(transforms)))
jobs = []
for idx, (yaw, pitch, roll) in enumerate(transforms):
out_name = f"{base}_{preset_name}_{idx:0{digits}d}_yaw{yaw:+d}_pit{pitch:+d}_rol{roll:+d}.jpg"
out_path = str(output_dir / out_name)
if (not config.OVERWRITE) and os.path.exists(out_path):
print(f"skip: {out_name}")
continue
jobs.append({
"idx": idx,
"yaw": yaw,
"pitch": pitch,
"roll": roll,
"out_name": out_name,
"out_path": out_path,
})
return jobs
def build_ffmpeg_multi_output_cmd(img_path, jobs):
"""1入力画像から複数出力画像を生成する ffmpeg コマンドを構築する"""
cmd = [
"ffmpeg",
"-y" if config.OVERWRITE else "-n",
"-loglevel", config.FFMPEG_LOGLEVEL,
"-i", img_path,
]
if len(jobs) == 1:
job = jobs[0]
cmd += [
"-vf", build_v360_options(job["yaw"], job["pitch"], job["roll"]),
"-frames:v", "1",
job["out_path"],
]
return cmd
split_labels = [f"s{job['idx']}" for job in jobs]
filter_parts = [
f"[0:v]split={len(jobs)}" + "".join(f"[{label}]" for label in split_labels)
]
for label, job in zip(split_labels, jobs):
filter_parts.append(
f"[{label}]{build_v360_options(job['yaw'], job['pitch'], job['roll'])}[o{job['idx']}]"
)
cmd += ["-filter_complex", ";".join(filter_parts)]
for job in jobs:
cmd += [
"-map", f"[o{job['idx']}]",
"-frames:v", "1",
job["out_path"],
]
return cmd
def run_ffmpeg_for_image(img_path, jobs):
"""1画像分の変換をまとめて実行する"""
if not jobs:
return
cmd = build_ffmpeg_multi_output_cmd(img_path, jobs)
subprocess.run(cmd, check=True)
def run_ffmpeg_with_fallback(img_path, jobs, base):
"""複数出力が失敗した場合は個別実行にフォールバックする"""
try:
run_ffmpeg_for_image(img_path, jobs)
for job in jobs:
print(f" -> {job['out_name']}")
except subprocess.CalledProcessError as e:
print(f" !! 一括変換に失敗: {base} ({e})")
print(" !! 個別変換にフォールバックします")
for job in jobs:
try:
run_ffmpeg_for_image(img_path, [job])
print(f" -> {job['out_name']}")
except subprocess.CalledProcessError as single_error:
print(f" !! 失敗: {job['out_name']} ({single_error})")
def _convert_images(output_dir, input_dir, transforms, preset_name):
"""
実際の変換処理を実行する共通関数
"""
images = []
images += glob.glob(str(input_dir / "*.jpg"))
images += glob.glob(str(input_dir / "*.jpeg"))
images += glob.glob(str(input_dir / "*.png"))
images.sort()
if not images:
raise RuntimeError(f"入力画像が見つかりません: {input_dir}")
print(f"[情報] 方向数: {len(transforms)} プリセット: {preset_name}")
for img_path in images:
base = os.path.splitext(os.path.basename(img_path))[0]
print(f"\n=== {base} ===")
jobs = build_output_jobs(base, output_dir, transforms, preset_name)
if not jobs:
print(" -> すべて既存ファイルのためスキップ")
continue
run_ffmpeg_with_fallback(img_path, jobs, base)
print("\n✅ 変換が完了しました。")
def main():
args = parse_args()
subdir = validate_subdir(args.subdir)
# 基準を「このスクリプトの場所」に固定(実行場所に依存しない)
output_dir = config.OUTPUT_DIR / subdir
input_dir = output_dir / "temp" / "frames"
# フォルダ確認
print("input_dir :", input_dir)
print("output_dir:", output_dir)
if not input_dir.is_dir():
raise RuntimeError("input_dir が存在しません。処理を中断します。")
ensure_ffmpeg()
output_dir.mkdir(parents=True, exist_ok=True)
if config.USE_DENSE_RING:
transforms = build_transforms_dense()
preset_name = f"dense{config.RING_STEP_DEG}"
else:
transforms = build_transforms_14()
preset_name = "rc14"
_convert_images(output_dir, input_dir, transforms, preset_name)
def process_frames(subdir: str, use_dense_ring: bool = False):
"""
main.py から呼ばれる用の関数
Args:
subdir: output/ 配下のサブディレクトリ名
use_dense_ring: True なら密集度高い変換、False なら標準14方向
"""
# 一時的に config の設定を上書き
original_dense = config.USE_DENSE_RING
config.USE_DENSE_RING = use_dense_ring
try:
subdir = validate_subdir(subdir)
output_dir = config.OUTPUT_DIR / subdir
input_dir = output_dir / "temp" / "frames"
if not input_dir.is_dir():
raise RuntimeError(f"入力フォルダが見つかりません: {input_dir}")
ensure_ffmpeg()
output_dir.mkdir(parents=True, exist_ok=True)
if config.USE_DENSE_RING:
transforms = build_transforms_dense()
preset_name = f"dense{config.RING_STEP_DEG}"
else:
transforms = build_transforms_14()
preset_name = "rc14"
_convert_images(output_dir, input_dir, transforms, preset_name)
finally:
# 設定を戻す
config.USE_DENSE_RING = original_dense
if __name__ == "__main__":
main()

49
src/config.py Normal file
View File

@@ -0,0 +1,49 @@
"""
プロジェクト全体の設定ファイル
"""
from pathlib import Path
# ==================== パス設定 ====================
PROJECT_ROOT = Path(__file__).parent.parent
# フォルダパス
DATA_DIR = PROJECT_ROOT / "data"
DATA_INPUT_DIR = DATA_DIR / "input"
OUTPUT_DIR = DATA_DIR / "output"
# ==================== フレーム抽出設定 ====================
# video_frame_extractor.py で使用
FRAME_PREFIX = "frame"
FRAME_EXTENSION = "jpg"
# ==================== 顔ぼかし設定 ====================
# face_blur.py で使用
BLUR_STRENGTH = 51 # ぼかしの強さ(奇数)
# ==================== Equirect→パースペクティブ変換設定 ====================
# batch_equirect2persp_ffmpeg.py で使用
# 出力画像の解像度
PERSPECTIVE_WIDTH = 1024
PERSPECTIVE_HEIGHT = 1024
# FOV (視野角)
HORIZONTAL_FOV = 90
VERTICAL_FOV = 90
# ffmpeg オーバーライト設定
OVERWRITE = True
# ========== 変換方法選択 ==========
# False: 14方向上下左右+斜めなど標準的)
# True: 密集度高くring_step_deg ごとにリング状)
USE_DENSE_RING = False
RING_STEP_DEG = 30 # USE_DENSE_RING=True の場合に使用
# ==================== 共通設定 ====================
# ログレベルOpenCV や ffmpeg へのオプション)
FFMPEG_LOGLEVEL = "error"
# 一度に処理する画像数など(今後の拡張用)
BATCH_SIZE = None # None = 制限なし

175
src/face_blur.py Normal file
View File

@@ -0,0 +1,175 @@
"""
画像内の顔を検出してぼかすスクリプト
顔検出と Gaussian ぼかし処理を実装
"""
import cv2
from pathlib import Path
import config
_FACE_CASCADE = None
def get_face_cascade():
"""顔検出器を遅延初期化して再利用する"""
global _FACE_CASCADE
if _FACE_CASCADE is None:
_FACE_CASCADE = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
)
if _FACE_CASCADE.empty():
raise RuntimeError("OpenCV の顔検出器を読み込めませんでした")
return _FACE_CASCADE
def blur_faces(image_path, output_path, blur_strength=51):
"""
画像内の顔を検出してぼかす
Args:
image_path (str): 入力画像のパス
output_path (str): 出力画像のパス
blur_strength (int): ぼかしの強さ(奇数、大きいほど強い)
Returns:
int: 検出された顔の数
"""
# 画像を読み込む
image = cv2.imread(image_path)
if image is None:
print(f"エラー: 画像 '{image_path}' を読み込めませんでした")
return 0
# グレースケールに変換
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 顔検出器を読み込むOpenCVのHaar Cascade分類器
face_cascade = get_face_cascade()
# 顔を検出
faces = face_cascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30)
)
# ぼかし強度を奇数に調整
if blur_strength % 2 == 0:
blur_strength += 1
# 検出された各顔にぼかしを適用
for (x, y, w, h) in faces:
# 顔領域を取得
face_region = image[y:y+h, x:x+w]
# Gaussian ぼかしを適用
blurred_face = cv2.GaussianBlur(face_region, (blur_strength, blur_strength), 0)
# ぼかした顔を元の画像に戻す
image[y:y+h, x:x+w] = blurred_face
# 結果を保存
cv2.imwrite(output_path, image)
return len(faces)
def process_folder(input_folder, blur_strength=51):
"""
フォルダ内の全画像の顔をぼかす
frames フォルダ内の画像を上書きして保存
Args:
input_folder (str): 入力フォルダのパスframes フォルダ)
blur_strength (int): ぼかしの強さ
"""
input_path = Path(input_folder)
if not input_path.exists():
print(f"エラー: フォルダ '{input_folder}' が見つかりません")
return
# サポートする画像拡張子
image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
# フォルダ内の画像ファイルを取得
image_files = [
f for f in input_path.iterdir()
if f.suffix.lower() in image_extensions
]
if not image_files:
print(f"エラー: '{input_folder}' に画像ファイルが見つかりません")
return
# ファイルをソート(順序を保証)
image_files.sort()
get_face_cascade()
print(f"処理対象: {len(image_files)} 枚の画像")
print(f"出力先: {input_path} (上書き)")
print(f"ぼかし強度: {blur_strength}")
print("\n処理を開始します...\n")
total_faces = 0
processed_count = 0
for i, image_file in enumerate(image_files, 1):
input_image_path = str(image_file)
# 同じファイルに上書き保存
output_image_path = str(image_file)
# 顔検出とぼかし処理
face_count = blur_faces(input_image_path, output_image_path, blur_strength)
total_faces += face_count
processed_count += 1
print(f"[{i}/{len(image_files)}] {image_file.name}: {face_count}個の顔を検出")
print(f"\n完了!")
print(f" - 処理した画像: {processed_count}")
print(f" - 検出した顔の総数: {total_faces}")
print(f" - 保存先: {input_path}")
def main():
"""
スタンドアロン実行用のメイン関数
"""
import argparse
import sys
parser = argparse.ArgumentParser(description='画像内の顔を検出してぼかす')
parser.add_argument('folder', help='output/ 内のフォルダ名(例: test_145frames_0min4sec_20260122_211204')
parser.add_argument('-b', '--blur', type=int, default=51,
help='ぼかしの強さ(奇数、デフォルト: 51')
args = parser.parse_args()
# 入力パスから"output/"プレフィックスを除去
folder_name = args.folder.replace('output/', '').replace('output\\', '')
# config から OUTPUT_DIR を使用
frame_folder = config.OUTPUT_DIR / folder_name / 'temp' / 'frames'
if not frame_folder.exists():
print(f"エラー: '{frame_folder}' が見つかりません")
print(f"正しいフォルダ名を指定してください")
return 1
# 顔ぼかし処理を実行
try:
process_folder(str(frame_folder), args.blur)
return 0
except Exception as e:
print(f"エラー: {e}")
return 1
if __name__ == "__main__":
import sys
sys.exit(main())

232
src/main.py Normal file
View File

@@ -0,0 +1,232 @@
"""
main.py - フレーム抽出 → (顔ぼかし) → パースペクティブ変換のメインスクリプト
使用方法:
python main.py extract <video_file>
python main.py blur <output_folder>
python main.py convert <output_folder>
python main.py pipeline <video_file> [--blur] [--dense]
"""
import argparse
import sys
from pathlib import Path
# config と各機能をインポート
import config
from video_frame_extractor import extract_frames
from face_blur import process_folder as blur_faces_folder
from batch_equirect2persp_ffmpeg import process_frames as convert_equirect
def cmd_extract(args):
"""
フレーム抽出コマンド
data/input/ 配下の動画ファイルを output/ に展開
"""
print(f"\n{'='*60}")
print(f"🎬 フレーム抽出を開始します")
print(f"{'='*60}")
video_path = config.DATA_INPUT_DIR / args.video_file
if not video_path.exists():
print(f"❌ エラー: '{video_path}' が見つかりません")
return False
try:
extract_frames(
str(video_path),
str(config.OUTPUT_DIR),
prefix=config.FRAME_PREFIX,
extension=config.FRAME_EXTENSION
)
print(f"✅ フレーム抽出が完了しました\n")
return True
except Exception as e:
print(f"❌ エラー: {e}\n")
return False
def cmd_blur(args):
"""
顔ぼかしコマンド
output/<output_folder>/temp/frames/ 配下の画像に顔ぼかしを適用
結果を output/<output_folder>/face_blurred_frames/ に保存
"""
print(f"\n{'='*60}")
print(f"😊 顔ぼかし処理を開始します")
print(f"{'='*60}")
folder_name = args.output_folder
# temp/frames フォルダを確認
frame_folder = config.OUTPUT_DIR / folder_name / "temp" / "frames"
if not frame_folder.exists():
print(f"❌ エラー: '{frame_folder}' が見つかりません")
print(f" 先に extract コマンドでフレーム抽出を実行してください")
return False
try:
blur_faces_folder(str(frame_folder), config.BLUR_STRENGTH)
print(f"✅ 顔ぼかしが完了しました\n")
return True
except Exception as e:
print(f"❌ エラー: {e}\n")
return False
def cmd_convert(args):
"""
Equirect→パースペクティブ変換コマンド
output/<output_folder>/temp/frames/ の画像を複数方向に変換
"""
print(f"\n{'='*60}")
print(f"🔄 Equirect→パースペクティブ変換を開始します")
print(f"{'='*60}")
folder_name = args.output_folder
try:
convert_equirect(
folder_name,
use_dense_ring=args.dense
)
print(f"✅ 変換が完了しました\n")
return True
except Exception as e:
print(f"❌ エラー: {e}\n")
return False
def cmd_pipeline(args):
"""
フルパイプライン実行
extract → (blur) → convert を一気に実行
"""
print(f"\n{'='*60}")
print(f"🚀 フルパイプラインを開始します")
print(f"{'='*60}\n")
# Step 1: フレーム抽出
class ExtractArgs:
def __init__(self, video_file):
self.video_file = video_file
extract_ok = cmd_extract(ExtractArgs(args.video_file))
if not extract_ok:
return False
# フレーム抽出で生成されたフォルダ名を特定
import os
output_folders = sorted([
d for d in os.listdir(config.OUTPUT_DIR)
if (config.OUTPUT_DIR / d).is_dir()
], reverse=True)
if not output_folders:
print("❌ エラー: 出力フォルダが見つかりません")
return False
latest_folder = output_folders[0]
print(f"📁 生成されたフォルダ: {latest_folder}\n")
# Step 2: 顔ぼかし(オプション)
if args.blur:
class BlurArgs:
def __init__(self, folder):
self.output_folder = folder
blur_ok = cmd_blur(BlurArgs(latest_folder))
if not blur_ok:
print("⚠️ 顔ぼかしがスキップされました\n")
# Step 3: パースペクティブ変換
class ConvertArgs:
def __init__(self, folder, dense):
self.output_folder = folder
self.dense = dense
convert_ok = cmd_convert(ConvertArgs(latest_folder, args.dense))
if not convert_ok:
return False
print(f"{'='*60}")
print(f"🎉 パイプライン完了!")
print(f"{'='*60}\n")
return True
def main():
parser = argparse.ArgumentParser(
description="Equirect 360動画 → パースペクティブ画像 変換ツール",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
例:
# フレーム抽出のみ
python main.py extract video.mp4
# 顔ぼかしのみ
python main.py blur test_145frames_0min4sec_20260122_220022
# パースペクティブ変換のみ
python main.py convert test_145frames_0min4sec_20260122_220022
# 全処理を実行(顔ぼかしなし)
python main.py pipeline video.mp4
# 全処理を実行(顔ぼかしあり、密集度高い変換)
python main.py pipeline video.mp4 --blur --dense
"""
)
subparsers = parser.add_subparsers(dest='command', help='実行するコマンド')
# === extract サブコマンド ===
extract_parser = subparsers.add_parser('extract', help='動画からフレームを抽出')
extract_parser.add_argument('video_file', help='data/input/ 内の動画ファイル名 (例: video.mp4)')
# === blur サブコマンド ===
blur_parser = subparsers.add_parser('blur', help='抽出されたフレームの顔をぼかす')
blur_parser.add_argument('output_folder', help='output/ 内のフォルダ名')
# === convert サブコマンド ===
convert_parser = subparsers.add_parser('convert', help='Equirect画像をパースペクティブ変換')
convert_parser.add_argument('output_folder', help='output/ 内のフォルダ名')
convert_parser.add_argument('--dense', action='store_true', help='密集度高い変換を使用')
# === pipeline サブコマンド ===
pipeline_parser = subparsers.add_parser('pipeline', help='フレーム抽出→変換を一気に実行')
pipeline_parser.add_argument('video_file', help='data/input/ 内の動画ファイル名 (例: video.mp4)')
pipeline_parser.add_argument('--blur', action='store_true', help='顔ぼかしを有効にする')
pipeline_parser.add_argument('--dense', action='store_true', help='密集度高い変換を使用')
# パースしてコマンド実行
args = parser.parse_args()
if not args.command:
parser.print_help()
return 1
# 各コマンドに対応した処理
if args.command == 'extract':
success = cmd_extract(args)
elif args.command == 'blur':
success = cmd_blur(args)
elif args.command == 'convert':
success = cmd_convert(args)
elif args.command == 'pipeline':
success = cmd_pipeline(args)
else:
parser.print_help()
return 1
return 0 if success else 1
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,209 @@
import cv2
import os
from pathlib import Path
from datetime import datetime
import config
def extract_frames(video_path, output_base_folder, prefix="frame", extension="jpg"):
"""
動画をフレームごとに分割して指定フォルダに保存する
Args:
video_path (str): 入力動画のパス
output_base_folder (str): 出力先ベースフォルダのパス
prefix (str): 出力ファイル名のプレフィックス (デフォルト: "frame")
extension (str): 出力画像の拡張子 (デフォルト: "jpg")
"""
# 動画ファイルを開く
video = cv2.VideoCapture(video_path)
if not video.isOpened():
print(f"エラー: 動画ファイル '{video_path}' を開けませんでした")
return
# 動画情報を取得
fps = video.get(cv2.CAP_PROP_FPS)
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
duration_sec = total_frames / fps if fps > 0 else 0
# 動画ファイル名(拡張子なし)を取得
video_filename = Path(video_path).stem
# 時間を分秒形式に変換
minutes = int(duration_sec // 60)
seconds = int(duration_sec % 60)
# フォルダ名を生成
# 形式: "動画名_フレーム数frames_時間min秒sec_タイムスタンプ"
# 例: "video_3000frames_1min40sec_20260122_143025"
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
folder_name = f"{video_filename}_{total_frames}frames_{minutes}min{seconds}sec_{timestamp}"
# 出力フォルダのパスを作成temp/frames/ 内に保存)
output_folder = os.path.join(output_base_folder, folder_name, "temp", "frames")
Path(output_folder).mkdir(parents=True, exist_ok=True)
print(f"動画情報:")
print(f" - FPS: {fps}")
print(f" - 総フレーム数: {total_frames}")
print(f" - 解像度: {width}x{height}")
print(f" - 再生時間: {minutes}{seconds}")
print(f" - 出力フォルダ: {folder_name}")
print(f"\nフレーム抽出を開始します...")
frame_count = 0
saved_count = 0
while True:
# フレームを読み込む
ret, frame = video.read()
# フレームの読み込みに失敗したら終了
if not ret:
break
# ファイル名を生成(ゼロパディング付き)
filename = f"{prefix}_{frame_count:06d}.{extension}"
output_path = os.path.join(output_folder, filename)
# フレームを保存
cv2.imwrite(output_path, frame)
saved_count += 1
# 進捗表示
if (frame_count + 1) % 100 == 0:
print(f" 処理中: {frame_count + 1}/{total_frames} フレーム")
frame_count += 1
# リソースを解放
video.release()
print(f"\n完了!")
print(f" - 保存されたフレーム数: {saved_count}")
print(f" - 保存先: {output_folder}")
def extract_frames_interval(video_path, output_base_folder, interval=1, prefix="frame", extension="jpg"):
"""
動画から指定間隔でフレームを抽出して保存する
注: 現在 main.py からは呼び出されていません
スタンドアロン実行時のみ使用可能です
Args:
video_path (str): 入力動画のパス
output_base_folder (str): 出力先ベースフォルダのパス
interval (int): フレーム抽出間隔1なら全フレーム、2なら1フレームおき
prefix (str): 出力ファイル名のプレフィックス
extension (str): 出力画像の拡張子
"""
video = cv2.VideoCapture(video_path)
if not video.isOpened():
print(f"エラー: 動画ファイル '{video_path}' を開けませんでした")
return
fps = video.get(cv2.CAP_PROP_FPS)
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
duration_sec = total_frames / fps if fps > 0 else 0
# 動画ファイル名(拡張子なし)を取得
video_filename = Path(video_path).stem
# 時間を分秒形式に変換
minutes = int(duration_sec // 60)
seconds = int(duration_sec % 60)
# 抽出予定フレーム数を計算
expected_frames = (total_frames + interval - 1) // interval
# フォルダ名を生成
# 形式: "動画名_フレーム数frames_interval間隔_時間min秒sec_タイムスタンプ"
# 例: "video_300frames_interval10_1min40sec_20260122_143025"
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
folder_name = f"{video_filename}_{expected_frames}frames_interval{interval}_{minutes}min{seconds}sec_{timestamp}"
# 出力フォルダのパスを作成temp/frames/ 内に保存)
output_folder = os.path.join(output_base_folder, folder_name, "temp", "frames")
Path(output_folder).mkdir(parents=True, exist_ok=True)
print(f"動画情報:")
print(f" - FPS: {fps}")
print(f" - 総フレーム数: {total_frames}")
print(f" - 再生時間: {minutes}{seconds}")
print(f" - 抽出間隔: {interval}フレームごと")
print(f" - 出力フォルダ: {folder_name}")
print(f"\nフレーム抽出を開始します...")
frame_count = 0
saved_count = 0
while True:
ret, frame = video.read()
if not ret:
break
# 指定間隔でフレームを保存
if frame_count % interval == 0:
filename = f"{prefix}_{saved_count:06d}.{extension}"
output_path = os.path.join(output_folder, filename)
cv2.imwrite(output_path, frame)
saved_count += 1
if saved_count % 100 == 0:
print(f" 処理中: {saved_count} フレーム保存済み")
frame_count += 1
video.release()
print(f"\n完了!")
print(f" - 保存されたフレーム数: {saved_count}")
print(f" - 保存先: {output_folder}")
def main():
"""
スタンドアロン実行用のメイン関数
"""
import argparse
import sys
parser = argparse.ArgumentParser(description='動画をフレームごとに分割して保存')
parser.add_argument('filename', help='data/input フォルダ内の動画ファイル名 (例: video.mp4)')
parser.add_argument('interval', type=int, nargs='?', default=1,
help='フレーム抽出間隔 (デフォルト: 1=全フレーム)')
args = parser.parse_args()
# config から設定を取得
video_path = config.DATA_INPUT_DIR / args.filename
output_base_folder = config.OUTPUT_DIR
prefix = config.FRAME_PREFIX
extension = config.FRAME_EXTENSION
# 動画ファイルの存在確認
if not video_path.exists():
print(f"エラー: '{video_path}' が見つかりません")
return 1
# フレーム抽出を実行
try:
if args.interval == 1:
extract_frames(str(video_path), str(output_base_folder), prefix, extension)
else:
extract_frames_interval(str(video_path), str(output_base_folder), args.interval,
prefix, extension)
return 0
except Exception as e:
print(f"エラー: {e}")
return 1
if __name__ == "__main__":
import sys
sys.exit(main())