Batch処理
This commit is contained in:
@@ -3,15 +3,18 @@
|
||||
from . import generate_mask
|
||||
from . import apply_blur
|
||||
from . import clear_cache
|
||||
from . import batch_bake
|
||||
|
||||
|
||||
def register():
|
||||
generate_mask.register()
|
||||
apply_blur.register()
|
||||
clear_cache.register()
|
||||
batch_bake.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
batch_bake.unregister()
|
||||
clear_cache.unregister()
|
||||
apply_blur.unregister()
|
||||
generate_mask.unregister()
|
||||
|
||||
+11
-3
@@ -50,11 +50,16 @@ def _set_strip_source(strip, filepath: str):
|
||||
_reload_movie_strip(strip)
|
||||
|
||||
|
||||
def _start_bake_impl(operator, context, force: bool = False):
|
||||
"""Bakeの共通実装。force=True でキャッシュを無視して再Bakeする。"""
|
||||
def _start_bake_impl(operator, context, force: bool = False, strip=None, on_complete_extra=None):
|
||||
"""Bakeの共通実装。force=True でキャッシュを無視して再Bakeする。
|
||||
|
||||
strip: 処理対象のstrip。None の場合は active_strip を使用。
|
||||
on_complete_extra: 非同期Bake完了時に追加で呼ばれるコールバック (status, data)。
|
||||
キャッシュヒット即時完了の場合は呼ばれない。
|
||||
"""
|
||||
seq_editor = context.scene.sequence_editor
|
||||
scene = context.scene
|
||||
video_strip = seq_editor.active_strip
|
||||
video_strip = strip if strip is not None else seq_editor.active_strip
|
||||
|
||||
video_path = bpy.path.abspath(video_strip.filepath)
|
||||
detections_path = get_detections_path_for_strip(video_strip.name)
|
||||
@@ -128,6 +133,9 @@ def _start_bake_impl(operator, context, force: bool = False):
|
||||
if area.type == "SEQUENCE_EDITOR":
|
||||
area.tag_redraw()
|
||||
|
||||
if on_complete_extra:
|
||||
on_complete_extra(status, data)
|
||||
|
||||
def on_progress(current, total):
|
||||
wm.bake_progress = current
|
||||
wm.bake_total = max(total, 1)
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
Batch Bake operator: sequentially Generate Detection Cache → Bake
|
||||
for all selected MOVIE strips in the VSE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import bpy
|
||||
from bpy.props import IntProperty, StringProperty
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..core.batch_processor import get_batch_processor
|
||||
from ..core.async_generator import get_generator as get_mask_generator
|
||||
from ..core.async_bake_generator import get_bake_generator
|
||||
from .apply_blur import KEY_ORIGINAL, KEY_MODE, _set_strip_source
|
||||
|
||||
|
||||
class SEQUENCER_OT_batch_bake_selected(Operator):
|
||||
"""Generate detection cache and bake blur for all selected MOVIE strips."""
|
||||
|
||||
bl_idname = "sequencer.batch_bake_selected"
|
||||
bl_label = "Batch Bake Selected"
|
||||
bl_description = "Generate detection cache and bake blur for all selected MOVIE strips"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.scene.sequence_editor:
|
||||
return False
|
||||
if get_batch_processor().is_running:
|
||||
return False
|
||||
if get_mask_generator().is_running:
|
||||
return False
|
||||
if get_bake_generator().is_running:
|
||||
return False
|
||||
seq_editor = context.scene.sequence_editor
|
||||
return any(s.select and s.type == "MOVIE" for s in seq_editor.strips)
|
||||
|
||||
def execute(self, context):
|
||||
seq_editor = context.scene.sequence_editor
|
||||
strips = [s for s in seq_editor.strips if s.select and s.type == "MOVIE"]
|
||||
|
||||
if not strips:
|
||||
self.report({"WARNING"}, "No MOVIE strips selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
batch = get_batch_processor()
|
||||
|
||||
def on_item_complete(idx, total, strip_name, status):
|
||||
pass # wm properties already updated by BatchProcessor
|
||||
|
||||
def on_all_complete(results):
|
||||
done = sum(1 for r in results if r["status"] == "done")
|
||||
total = len(results)
|
||||
print(f"[FaceMask] Batch finished: {done}/{total} strips completed")
|
||||
|
||||
wm = context.window_manager
|
||||
wm.batch_current = 0
|
||||
wm.batch_total = len(strips)
|
||||
wm.batch_current_name = ""
|
||||
|
||||
batch.start(context, strips, on_item_complete=on_item_complete, on_all_complete=on_all_complete)
|
||||
self.report({"INFO"}, f"Batch bake started for {len(strips)} strips")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SEQUENCER_OT_batch_regenerate_cache(Operator):
|
||||
"""Regenerate detection cache for all selected MOVIE strips (ignore existing cache)."""
|
||||
|
||||
bl_idname = "sequencer.batch_regenerate_cache"
|
||||
bl_label = "Batch Regenerate Cache"
|
||||
bl_description = "Regenerate detection cache for all selected MOVIE strips"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.scene.sequence_editor:
|
||||
return False
|
||||
if get_batch_processor().is_running:
|
||||
return False
|
||||
if get_mask_generator().is_running:
|
||||
return False
|
||||
if get_bake_generator().is_running:
|
||||
return False
|
||||
seq_editor = context.scene.sequence_editor
|
||||
return any(s.select and s.type == "MOVIE" for s in seq_editor.strips)
|
||||
|
||||
def execute(self, context):
|
||||
seq_editor = context.scene.sequence_editor
|
||||
strips = [s for s in seq_editor.strips if s.select and s.type == "MOVIE"]
|
||||
|
||||
if not strips:
|
||||
self.report({"WARNING"}, "No MOVIE strips selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
batch = get_batch_processor()
|
||||
|
||||
def on_all_complete(results):
|
||||
done = sum(1 for r in results if r["status"] == "done")
|
||||
print(f"[FaceMask] Batch regenerate finished: {done}/{len(results)} strips")
|
||||
|
||||
batch.start(
|
||||
context,
|
||||
strips,
|
||||
on_all_complete=on_all_complete,
|
||||
mode="mask_only",
|
||||
)
|
||||
self.report({"INFO"}, f"Batch regenerate cache started for {len(strips)} strips")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SEQUENCER_OT_batch_restore_original(Operator):
|
||||
"""Restore original source for all selected MOVIE strips."""
|
||||
|
||||
bl_idname = "sequencer.batch_restore_original"
|
||||
bl_label = "Batch Restore Original"
|
||||
bl_description = "Restore original source filepath for all selected MOVIE strips"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.scene.sequence_editor:
|
||||
return False
|
||||
if get_batch_processor().is_running:
|
||||
return False
|
||||
seq_editor = context.scene.sequence_editor
|
||||
return any(s.select and s.type == "MOVIE" for s in seq_editor.strips)
|
||||
|
||||
def execute(self, context):
|
||||
seq_editor = context.scene.sequence_editor
|
||||
strips = [s for s in seq_editor.strips if s.select and s.type == "MOVIE"]
|
||||
|
||||
restored = 0
|
||||
skipped = 0
|
||||
for strip in strips:
|
||||
original_path = strip.get(KEY_ORIGINAL)
|
||||
if not original_path or not os.path.exists(original_path):
|
||||
skipped += 1
|
||||
continue
|
||||
if strip.get(KEY_MODE, "original") != "original":
|
||||
_set_strip_source(strip, original_path)
|
||||
strip[KEY_MODE] = "original"
|
||||
restored += 1
|
||||
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Restored {restored} strip(s)"
|
||||
+ (f", skipped {skipped} (no original stored)" if skipped else ""),
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SEQUENCER_OT_cancel_batch_bake(Operator):
|
||||
"""Cancel ongoing batch bake."""
|
||||
|
||||
bl_idname = "sequencer.cancel_batch_bake"
|
||||
bl_label = "Cancel Batch Bake"
|
||||
bl_description = "Cancel the current batch bake process"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
batch = get_batch_processor()
|
||||
if batch.is_running:
|
||||
batch.cancel()
|
||||
self.report({"INFO"}, "Batch bake cancelled")
|
||||
else:
|
||||
self.report({"WARNING"}, "No batch bake in progress")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = [
|
||||
SEQUENCER_OT_batch_bake_selected,
|
||||
SEQUENCER_OT_batch_regenerate_cache,
|
||||
SEQUENCER_OT_batch_restore_original,
|
||||
SEQUENCER_OT_cancel_batch_bake,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.WindowManager.batch_current = IntProperty(default=0)
|
||||
bpy.types.WindowManager.batch_total = IntProperty(default=0)
|
||||
bpy.types.WindowManager.batch_current_name = StringProperty(default="")
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.WindowManager.batch_current_name
|
||||
del bpy.types.WindowManager.batch_total
|
||||
del bpy.types.WindowManager.batch_current
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
+90
-115
@@ -7,11 +7,66 @@ from video strips in the Video Sequence Editor.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
from bpy.props import IntProperty
|
||||
from bpy.props import IntProperty, BoolProperty
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..core.async_generator import get_generator
|
||||
from ..core.utils import get_cache_dir_for_strip
|
||||
from ..core.inference_client import get_client
|
||||
from ..core.utils import get_cache_dir_for_strip, check_detection_cache
|
||||
|
||||
|
||||
def compute_strip_frame_range(strip, scene, client) -> tuple:
|
||||
"""(start_frame, end_frame, source_fps) を返す。失敗時は例外を送出。"""
|
||||
video_path = bpy.path.abspath(strip.filepath)
|
||||
video_info = client.get_video_info(video_path)
|
||||
total_video_frames = int(video_info.get("frame_count", 0))
|
||||
source_fps = float(video_info.get("fps", 0.0))
|
||||
if total_video_frames <= 0:
|
||||
raise ValueError(f"Could not read frame count from video: {video_path}")
|
||||
if source_fps <= 0:
|
||||
source_fps = scene.render.fps / scene.render.fps_base
|
||||
project_fps = scene.render.fps / scene.render.fps_base
|
||||
fps_ratio = source_fps / project_fps
|
||||
start_frame = int(round(strip.frame_offset_start * fps_ratio))
|
||||
end_frame = start_frame + int(round(strip.frame_final_duration * fps_ratio)) - 1
|
||||
start_frame = max(0, min(start_frame, total_video_frames - 1))
|
||||
end_frame = max(start_frame, min(end_frame, total_video_frames - 1))
|
||||
return start_frame, end_frame, source_fps
|
||||
|
||||
|
||||
def start_mask_gen_for_strip(context, strip, on_complete, on_progress):
|
||||
"""Strip のマスク生成を開始する共通処理。
|
||||
|
||||
generator.is_running 等のエラー時は例外を送出する。
|
||||
wm.mask_progress / mask_total を初期化してから generator.start() を呼ぶ。
|
||||
"""
|
||||
scene = context.scene
|
||||
wm = context.window_manager
|
||||
generator = get_generator()
|
||||
|
||||
if generator.is_running:
|
||||
raise RuntimeError("Mask generation already in progress")
|
||||
|
||||
client = get_client()
|
||||
start_frame, end_frame, source_fps = compute_strip_frame_range(strip, scene, client)
|
||||
|
||||
output_dir = get_cache_dir_for_strip(strip.name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
wm.mask_progress = 0
|
||||
wm.mask_total = end_frame - start_frame + 1
|
||||
|
||||
generator.start(
|
||||
video_path=bpy.path.abspath(strip.filepath),
|
||||
output_dir=output_dir,
|
||||
start_frame=start_frame,
|
||||
end_frame=end_frame,
|
||||
fps=source_fps,
|
||||
conf_threshold=scene.facemask_conf_threshold,
|
||||
iou_threshold=scene.facemask_iou_threshold,
|
||||
on_complete=on_complete,
|
||||
on_progress=on_progress,
|
||||
)
|
||||
|
||||
|
||||
class SEQUENCER_OT_generate_face_mask(Operator):
|
||||
@@ -21,63 +76,43 @@ class SEQUENCER_OT_generate_face_mask(Operator):
|
||||
bl_label = "Generate Face Mask"
|
||||
bl_description = "Detect faces and generate mask image sequence"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
|
||||
force: BoolProperty(
|
||||
name="Force Regenerate",
|
||||
description="既存のキャッシュを無視して再生成する",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""Check if operator can run."""
|
||||
if not context.scene.sequence_editor:
|
||||
return False
|
||||
|
||||
strip = context.scene.sequence_editor.active_strip
|
||||
if not strip:
|
||||
return False
|
||||
|
||||
return strip.type in {'MOVIE', 'IMAGE'}
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
strip = context.scene.sequence_editor.active_strip
|
||||
scene = context.scene
|
||||
|
||||
# Get video path
|
||||
|
||||
# ファイル存在確認
|
||||
if strip.type == 'MOVIE':
|
||||
video_path = bpy.path.abspath(strip.filepath)
|
||||
else:
|
||||
# Image sequence - get directory
|
||||
video_path = bpy.path.abspath(strip.directory)
|
||||
|
||||
|
||||
if not os.path.exists(video_path):
|
||||
self.report({'ERROR'}, f"Video file not found: {video_path}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Determine output directory
|
||||
output_dir = self._get_cache_dir(context, strip)
|
||||
|
||||
# Check cache - if masks already exist, use them
|
||||
expected_frame_count = strip.frame_final_end - strip.frame_final_start + 1
|
||||
if self._check_cache(output_dir, expected_frame_count):
|
||||
self.report({'INFO'}, f"Using cached detections from {output_dir}")
|
||||
|
||||
# キャッシュ確認(force=True の場合はスキップ)
|
||||
if not self.force and check_detection_cache(strip.name):
|
||||
self.report({'INFO'}, f"Using cached detections for {strip.name}")
|
||||
return {'FINISHED'}
|
||||
|
||||
# 動画の実際のフレーム数を取得(Blenderプロジェクトのfpsと動画のfpsが
|
||||
# 異なる場合にタイムライン上のフレーム数では不足するため)
|
||||
import cv2 as _cv2
|
||||
_cap = _cv2.VideoCapture(video_path)
|
||||
total_video_frames = int(_cap.get(_cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = _cap.get(_cv2.CAP_PROP_FPS) or (scene.render.fps / scene.render.fps_base)
|
||||
_cap.release()
|
||||
if total_video_frames <= 0:
|
||||
self.report({'ERROR'}, f"Could not read frame count from video: {video_path}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Start async generation
|
||||
|
||||
generator = get_generator()
|
||||
|
||||
if generator.is_running:
|
||||
self.report({'WARNING'}, "Mask generation already in progress")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
def on_complete(status, data):
|
||||
"""Called when mask generation completes."""
|
||||
wm = context.window_manager
|
||||
wm.mask_total = max(wm.mask_total, generator.total_frames)
|
||||
if status == "done":
|
||||
@@ -95,103 +130,45 @@ class SEQUENCER_OT_generate_face_mask(Operator):
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'SEQUENCE_EDITOR':
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def on_progress(current, total):
|
||||
"""Called on progress updates."""
|
||||
# Update window manager properties for UI
|
||||
wm = context.window_manager
|
||||
wm.mask_progress = current
|
||||
wm.mask_total = total
|
||||
|
||||
# Force UI redraw
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'SEQUENCE_EDITOR':
|
||||
area.tag_redraw()
|
||||
|
||||
# Initialize progress
|
||||
wm = context.window_manager
|
||||
wm.mask_progress = 0
|
||||
wm.mask_total = total_video_frames
|
||||
|
||||
# Get parameters from scene properties
|
||||
conf_threshold = scene.facemask_conf_threshold
|
||||
iou_threshold = scene.facemask_iou_threshold
|
||||
|
||||
# Start generation
|
||||
generator.start(
|
||||
video_path=video_path,
|
||||
output_dir=output_dir,
|
||||
start_frame=0,
|
||||
end_frame=total_video_frames - 1,
|
||||
fps=fps,
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold,
|
||||
on_complete=on_complete,
|
||||
on_progress=on_progress,
|
||||
)
|
||||
|
||||
try:
|
||||
start_mask_gen_for_strip(context, strip, on_complete, on_progress)
|
||||
except RuntimeError as e:
|
||||
self.report({'WARNING'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Failed to start mask generation: {e}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, f"Started mask generation for {strip.name}")
|
||||
return {'FINISHED'}
|
||||
|
||||
def _get_cache_dir(self, context, strip) -> str:
|
||||
"""Get or create cache directory for mask images."""
|
||||
cache_dir = get_cache_dir_for_strip(strip.name)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
return cache_dir
|
||||
|
||||
def _check_cache(self, cache_dir: str, expected_frames: int) -> bool:
|
||||
"""Check if cached masks exist and are complete.
|
||||
|
||||
Args:
|
||||
cache_dir: Path to cache directory
|
||||
expected_frames: Number of frames expected
|
||||
|
||||
Returns:
|
||||
True if cache exists and is valid
|
||||
"""
|
||||
if not os.path.exists(cache_dir):
|
||||
return False
|
||||
|
||||
detections_path = os.path.join(cache_dir, "detections.msgpack")
|
||||
if not os.path.exists(detections_path):
|
||||
return False
|
||||
|
||||
# Quick sanity check: non-empty file
|
||||
try:
|
||||
if os.path.getsize(detections_path) <= 0:
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# Optional frame count verification if msgpack is available
|
||||
try:
|
||||
import msgpack
|
||||
|
||||
with open(detections_path, "rb") as f:
|
||||
payload = msgpack.unpackb(f.read(), raw=False)
|
||||
frames = payload.get("frames", [])
|
||||
return len(frames) >= expected_frames * 0.9
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
class SEQUENCER_OT_cancel_mask_generation(Operator):
|
||||
"""Cancel ongoing mask generation."""
|
||||
|
||||
|
||||
bl_idname = "sequencer.cancel_mask_generation"
|
||||
bl_label = "Cancel Mask Generation"
|
||||
bl_description = "Cancel the current mask generation process"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
generator = get_generator()
|
||||
|
||||
|
||||
if generator.is_running:
|
||||
generator.cancel()
|
||||
self.report({'INFO'}, "Mask generation cancelled")
|
||||
else:
|
||||
self.report({'WARNING'}, "No mask generation in progress")
|
||||
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -205,16 +182,14 @@ classes = [
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# Add progress properties to window manager
|
||||
|
||||
bpy.types.WindowManager.mask_progress = IntProperty(default=0)
|
||||
bpy.types.WindowManager.mask_total = IntProperty(default=0)
|
||||
|
||||
|
||||
def unregister():
|
||||
# Remove properties
|
||||
del bpy.types.WindowManager.mask_progress
|
||||
del bpy.types.WindowManager.mask_total
|
||||
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
Reference in New Issue
Block a user