add ソースコードを追加
This commit is contained in:
276
src/batch_equirect2persp_ffmpeg.py
Normal file
276
src/batch_equirect2persp_ffmpeg.py
Normal 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
49
src/config.py
Normal 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
175
src/face_blur.py
Normal 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
232
src/main.py
Normal 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())
|
||||
209
src/video_frame_extractor.py
Normal file
209
src/video_frame_extractor.py
Normal 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())
|
||||
Reference in New Issue
Block a user