feat: 静画に対応
This commit is contained in:
+74
-37
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user