feat: 静画に対応

This commit is contained in:
2026-02-22 16:35:51 +09:00
parent 32e4fbceb2
commit be65abc6b0
9 changed files with 693 additions and 76 deletions
+74 -37
View File
@@ -37,6 +37,12 @@ def _output_path(video_strip, detections_path: str, fmt: str) -> str:
return os.path.join(out_dir, f"{safe_name}_blurred.{ext}")
def _output_dir_for_images(strip, detections_path: str) -> str:
out_dir = os.path.dirname(detections_path)
safe_name = strip.name.replace("/", "_").replace("\\", "_")
return os.path.join(out_dir, f"{safe_name}_blurred")
def _reload_movie_strip(strip):
if hasattr(strip, "reload"):
try:
@@ -45,9 +51,12 @@ def _reload_movie_strip(strip):
pass
def _set_strip_source(strip, filepath: str):
strip.filepath = filepath
_reload_movie_strip(strip)
def _set_strip_source(strip, path: str):
if strip.type == "IMAGE":
strip.directory = path
else:
strip.filepath = path
_reload_movie_strip(strip)
def _start_bake_impl(operator, context, force: bool = False, strip=None, on_complete_extra=None):
@@ -56,29 +65,42 @@ def _start_bake_impl(operator, context, force: bool = False, strip=None, on_comp
strip: 処理対象のstrip。None の場合は active_strip を使用。
on_complete_extra: 非同期Bake完了時に追加で呼ばれるコールバック (status, data)。
キャッシュヒット即時完了の場合は呼ばれない。
MOVIE / IMAGE 両対応。
"""
seq_editor = context.scene.sequence_editor
scene = context.scene
video_strip = strip if strip is not None else seq_editor.active_strip
is_image = video_strip.type == "IMAGE"
video_path = bpy.path.abspath(video_strip.filepath)
detections_path = get_detections_path_for_strip(video_strip.name)
if not os.path.exists(video_path):
operator.report({"ERROR"}, f"Source video not found: {video_path}")
return {"CANCELLED"}
if not os.path.exists(detections_path):
operator.report({"ERROR"}, f"Detection cache not found: {detections_path}")
return {"CANCELLED"}
bake_format = scene.facemask_bake_format
output_path = _output_path(video_strip, detections_path, bake_format)
blur_size = int(scene.facemask_bake_blur_size)
display_scale = float(scene.facemask_bake_display_scale)
if is_image:
image_dir = bpy.path.abspath(video_strip.directory)
filenames = [elem.filename for elem in video_strip.elements]
if not os.path.isdir(image_dir):
operator.report({"ERROR"}, f"Image directory not found: {image_dir}")
return {"CANCELLED"}
output_dir = _output_dir_for_images(video_strip, detections_path)
original_source = image_dir
bake_format = None # IMAGE strips don't use format
else:
video_path = bpy.path.abspath(video_strip.filepath)
if not os.path.exists(video_path):
operator.report({"ERROR"}, f"Source video not found: {video_path}")
return {"CANCELLED"}
bake_format = scene.facemask_bake_format
output_path = _output_path(video_strip, detections_path, bake_format)
original_source = video_path
if not force:
# パラメータが一致するキャッシュがあればswapのみ
cached_baked_path = video_strip.get(KEY_BAKED)
cached_format = video_strip.get(KEY_FORMAT)
cached_blur_size = video_strip.get(KEY_BLUR_SIZE)
cached_display_scale = video_strip.get(KEY_DISPLAY_SCALE)
try:
@@ -89,13 +111,16 @@ def _start_bake_impl(operator, context, force: bool = False, strip=None, on_comp
cached_display_scale_f = float(cached_display_scale)
except (TypeError, ValueError):
cached_display_scale_f = None
if (
cached_baked_path
and os.path.exists(cached_baked_path)
and cached_format == bake_format
cache_exists = (
cached_baked_path and os.path.exists(cached_baked_path)
and cached_blur_size_int == blur_size
and cached_display_scale_f == display_scale
):
)
if not is_image:
cache_exists = cache_exists and video_strip.get(KEY_FORMAT) == bake_format
if cache_exists:
if video_strip.get(KEY_MODE) != "baked":
video_strip[KEY_MODE] = "baked"
_set_strip_source(video_strip, cached_baked_path)
@@ -112,18 +137,18 @@ def _start_bake_impl(operator, context, force: bool = False, strip=None, on_comp
return
if status == "done":
result_path = data or output_path
original_path = strip.get(KEY_ORIGINAL)
result = data or (output_dir if is_image else output_path)
current_mode = strip.get(KEY_MODE, "original")
if not original_path or current_mode != "baked":
strip[KEY_ORIGINAL] = video_path
strip[KEY_BAKED] = result_path
if not strip.get(KEY_ORIGINAL) or current_mode != "baked":
strip[KEY_ORIGINAL] = original_source
strip[KEY_BAKED] = result
strip[KEY_MODE] = "baked"
strip[KEY_FORMAT] = bake_format
strip[KEY_BLUR_SIZE] = blur_size
strip[KEY_DISPLAY_SCALE] = display_scale
_set_strip_source(strip, result_path)
print(f"[FaceMask] Bake completed and source swapped: {result_path}")
if not is_image:
strip[KEY_FORMAT] = bake_format
_set_strip_source(strip, result)
print(f"[FaceMask] Bake completed and source swapped: {result}")
elif status == "error":
print(f"[FaceMask] Bake failed: {data}")
elif status == "cancelled":
@@ -147,16 +172,28 @@ def _start_bake_impl(operator, context, force: bool = False, strip=None, on_comp
wm.bake_total = 1
try:
bake_generator.start(
video_path=video_path,
detections_path=detections_path,
output_path=output_path,
blur_size=blur_size,
display_scale=display_scale,
fmt=bake_format.lower(),
on_complete=on_complete,
on_progress=on_progress,
)
if is_image:
bake_generator.start_images(
image_dir=image_dir,
filenames=filenames,
output_dir=output_dir,
detections_path=detections_path,
blur_size=blur_size,
display_scale=display_scale,
on_complete=on_complete,
on_progress=on_progress,
)
else:
bake_generator.start(
video_path=video_path,
detections_path=detections_path,
output_path=output_path,
blur_size=blur_size,
display_scale=display_scale,
fmt=bake_format.lower(),
on_complete=on_complete,
on_progress=on_progress,
)
except Exception as e:
operator.report({"ERROR"}, f"Failed to start bake: {e}")
return {"CANCELLED"}
@@ -182,7 +219,7 @@ class SEQUENCER_OT_bake_and_swap_blur_source(Operator):
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
return bool(strip and strip.type == "MOVIE")
return bool(strip and strip.type in {"MOVIE", "IMAGE"})
def execute(self, context):
return _start_bake_impl(self, context, force=False)
@@ -205,7 +242,7 @@ class SEQUENCER_OT_force_rebake_blur(Operator):
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
return bool(strip and strip.type == "MOVIE")
return bool(strip and strip.type in {"MOVIE", "IMAGE"})
def execute(self, context):
return _start_bake_impl(self, context, force=True)
@@ -226,7 +263,7 @@ class SEQUENCER_OT_swap_to_baked_blur(Operator):
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
if not strip or strip.type != "MOVIE":
if not strip or strip.type not in {"MOVIE", "IMAGE"}:
return False
baked_path = strip.get(KEY_BAKED)
return bool(baked_path and os.path.exists(baked_path))
@@ -255,7 +292,7 @@ class SEQUENCER_OT_restore_original_source(Operator):
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
if not strip or strip.type != "MOVIE":
if not strip or strip.type not in {"MOVIE", "IMAGE"}:
return False
if strip.get(KEY_MODE, "original") == "original":
return False
+14 -14
View File
@@ -15,11 +15,11 @@ 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."""
"""Generate detection cache and bake blur for all selected MOVIE/IMAGE 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_description = "Generate detection cache and bake blur for all selected MOVIE/IMAGE strips"
bl_options = {"REGISTER"}
@classmethod
@@ -33,14 +33,14 @@ class SEQUENCER_OT_batch_bake_selected(Operator):
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)
return any(s.select and s.type in {"MOVIE", "IMAGE"} 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"]
strips = [s for s in seq_editor.strips if s.select and s.type in {"MOVIE", "IMAGE"}]
if not strips:
self.report({"WARNING"}, "No MOVIE strips selected")
self.report({"WARNING"}, "No MOVIE or IMAGE strips selected")
return {"CANCELLED"}
batch = get_batch_processor()
@@ -64,11 +64,11 @@ class SEQUENCER_OT_batch_bake_selected(Operator):
class SEQUENCER_OT_batch_regenerate_cache(Operator):
"""Regenerate detection cache for all selected MOVIE strips (ignore existing cache)."""
"""Regenerate detection cache for all selected MOVIE/IMAGE 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_description = "Regenerate detection cache for all selected MOVIE/IMAGE strips"
bl_options = {"REGISTER"}
@classmethod
@@ -82,14 +82,14 @@ class SEQUENCER_OT_batch_regenerate_cache(Operator):
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)
return any(s.select and s.type in {"MOVIE", "IMAGE"} 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"]
strips = [s for s in seq_editor.strips if s.select and s.type in {"MOVIE", "IMAGE"}]
if not strips:
self.report({"WARNING"}, "No MOVIE strips selected")
self.report({"WARNING"}, "No MOVIE or IMAGE strips selected")
return {"CANCELLED"}
batch = get_batch_processor()
@@ -109,11 +109,11 @@ class SEQUENCER_OT_batch_regenerate_cache(Operator):
class SEQUENCER_OT_batch_restore_original(Operator):
"""Restore original source for all selected MOVIE strips."""
"""Restore original source for all selected MOVIE/IMAGE strips."""
bl_idname = "sequencer.batch_restore_original"
bl_label = "Batch Restore Original"
bl_description = "Restore original source filepath for all selected MOVIE strips"
bl_description = "Restore original source filepath for all selected MOVIE/IMAGE strips"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@@ -123,11 +123,11 @@ class SEQUENCER_OT_batch_restore_original(Operator):
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)
return any(s.select and s.type in {"MOVIE", "IMAGE"} 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"]
strips = [s for s in seq_editor.strips if s.select and s.type in {"MOVIE", "IMAGE"}]
restored = 0
skipped = 0
+51 -18
View File
@@ -34,11 +34,28 @@ def compute_strip_frame_range(strip, scene, client) -> tuple:
return start_frame, end_frame, source_fps
def get_image_strip_files(strip) -> tuple:
"""IMAGE strip の (abs_image_dir, filenames_list) を返す。"""
image_dir = bpy.path.abspath(strip.directory)
filenames = [elem.filename for elem in strip.elements]
return image_dir, filenames
def compute_image_strip_range(strip) -> tuple:
"""IMAGE strip のアクティブ範囲 (start_index, end_index) を返す。"""
total_elements = len(strip.elements)
start_idx = max(0, int(strip.frame_offset_start))
end_idx = start_idx + int(strip.frame_final_duration) - 1
start_idx = min(start_idx, total_elements - 1)
end_idx = max(start_idx, min(end_idx, total_elements - 1))
return start_idx, end_idx
def start_mask_gen_for_strip(context, strip, on_complete, on_progress):
"""Strip のマスク生成を開始する共通処理。
"""Strip のマスク生成を開始する共通処理MOVIE / IMAGE 両対応)
generator.is_running 等のエラー時は例外を送出する。
wm.mask_progress / mask_total を初期化してから generator.start() を呼ぶ。
wm.mask_progress / mask_total を初期化してから generator.start*() を呼ぶ。
"""
scene = context.scene
wm = context.window_manager
@@ -47,26 +64,42 @@ def start_mask_gen_for_strip(context, strip, on_complete, on_progress):
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,
)
if strip.type == "IMAGE":
image_dir, filenames = get_image_strip_files(strip)
if not filenames:
raise ValueError("Image strip has no elements")
start_idx, end_idx = compute_image_strip_range(strip)
wm.mask_total = end_idx - start_idx + 1
generator.start_images(
image_dir=image_dir,
filenames=filenames,
output_dir=output_dir,
start_index=start_idx,
end_index=end_idx,
conf_threshold=scene.facemask_conf_threshold,
iou_threshold=scene.facemask_iou_threshold,
on_complete=on_complete,
on_progress=on_progress,
)
else:
client = get_client()
start_frame, end_frame, source_fps = compute_strip_frame_range(strip, scene, client)
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):