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