UI周りの改善

This commit is contained in:
Keisuke Hirata 2026-02-19 12:55:30 +09:00
parent a3de61d5ce
commit d67265aa39
3 changed files with 197 additions and 115 deletions

View File

@ -50,46 +50,28 @@ def _set_strip_source(strip, filepath: str):
_reload_movie_strip(strip) _reload_movie_strip(strip)
class SEQUENCER_OT_bake_and_swap_blur_source(Operator): def _start_bake_impl(operator, context, force: bool = False):
"""Bake masked blur and replace active strip source with baked video.""" """Bakeの共通実装。force=True でキャッシュを無視して再Bakeする。"""
seq_editor = context.scene.sequence_editor
scene = context.scene
video_strip = seq_editor.active_strip
bl_idname = "sequencer.bake_and_swap_blur_source" video_path = bpy.path.abspath(video_strip.filepath)
bl_label = "Bake & Swap Source" detections_path = get_detections_path_for_strip(video_strip.name)
bl_description = "Bake masked blur to video and swap active strip source" if not os.path.exists(video_path):
bl_options = {"REGISTER", "UNDO"} 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"}
@classmethod bake_format = scene.facemask_bake_format
def poll(cls, context): output_path = _output_path(video_strip, detections_path, bake_format)
if not context.scene.sequence_editor: blur_size = int(scene.facemask_bake_blur_size)
return False display_scale = float(scene.facemask_bake_display_scale)
# Prevent overlapping heavy tasks
if get_mask_generator().is_running:
return False
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
return bool(strip and strip.type == "MOVIE")
def execute(self, context): if not force:
seq_editor = context.scene.sequence_editor # パラメータが一致するキャッシュがあればswapのみ
scene = context.scene
video_strip = seq_editor.active_strip
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):
self.report({"ERROR"}, f"Source video not found: {video_path}")
return {"CANCELLED"}
if not os.path.exists(detections_path):
self.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)
# Reuse baked cache when parameters match and file still exists.
cached_baked_path = video_strip.get(KEY_BAKED) cached_baked_path = video_strip.get(KEY_BAKED)
cached_format = video_strip.get(KEY_FORMAT) cached_format = video_strip.get(KEY_FORMAT)
cached_blur_size = video_strip.get(KEY_BLUR_SIZE) cached_blur_size = video_strip.get(KEY_BLUR_SIZE)
@ -112,66 +94,141 @@ class SEQUENCER_OT_bake_and_swap_blur_source(Operator):
if video_strip.get(KEY_MODE) != "baked": if video_strip.get(KEY_MODE) != "baked":
video_strip[KEY_MODE] = "baked" video_strip[KEY_MODE] = "baked"
_set_strip_source(video_strip, cached_baked_path) _set_strip_source(video_strip, cached_baked_path)
self.report({"INFO"}, "Using cached baked blur") operator.report({"INFO"}, "Using cached baked blur")
return {"FINISHED"} return {"FINISHED"}
bake_generator = get_bake_generator() bake_generator = get_bake_generator()
wm = context.window_manager wm = context.window_manager
def on_complete(status, data): def on_complete(status, data):
strip = context.scene.sequence_editor.strips.get(video_strip.name) strip = context.scene.sequence_editor.strips.get(video_strip.name)
if not strip: if not strip:
print(f"[FaceMask] Bake complete but strip no longer exists: {video_strip.name}") print(f"[FaceMask] Bake complete but strip no longer exists: {video_strip.name}")
return return
if status == "done": if status == "done":
result_path = data or output_path result_path = data or output_path
original_path = strip.get(KEY_ORIGINAL) original_path = strip.get(KEY_ORIGINAL)
current_mode = strip.get(KEY_MODE, "original") current_mode = strip.get(KEY_MODE, "original")
if not original_path or current_mode != "baked": if not original_path or current_mode != "baked":
strip[KEY_ORIGINAL] = video_path strip[KEY_ORIGINAL] = video_path
strip[KEY_BAKED] = result_path strip[KEY_BAKED] = result_path
strip[KEY_MODE] = "baked" strip[KEY_MODE] = "baked"
strip[KEY_FORMAT] = bake_format strip[KEY_FORMAT] = bake_format
strip[KEY_BLUR_SIZE] = blur_size strip[KEY_BLUR_SIZE] = blur_size
strip[KEY_DISPLAY_SCALE] = display_scale strip[KEY_DISPLAY_SCALE] = display_scale
_set_strip_source(strip, result_path) _set_strip_source(strip, result_path)
print(f"[FaceMask] Bake completed and source swapped: {result_path}") print(f"[FaceMask] Bake completed and source swapped: {result_path}")
elif status == "error": elif status == "error":
print(f"[FaceMask] Bake failed: {data}") print(f"[FaceMask] Bake failed: {data}")
elif status == "cancelled": elif status == "cancelled":
print("[FaceMask] Bake cancelled") print("[FaceMask] Bake cancelled")
for area in context.screen.areas: for area in context.screen.areas:
if area.type == "SEQUENCE_EDITOR": if area.type == "SEQUENCE_EDITOR":
area.tag_redraw() area.tag_redraw()
def on_progress(current, total): def on_progress(current, total):
wm.bake_progress = current wm.bake_progress = current
wm.bake_total = max(total, 1) wm.bake_total = max(total, 1)
for area in context.screen.areas: for area in context.screen.areas:
if area.type == "SEQUENCE_EDITOR": if area.type == "SEQUENCE_EDITOR":
area.tag_redraw() area.tag_redraw()
wm.bake_progress = 0 wm.bake_progress = 0
wm.bake_total = 1 wm.bake_total = 1
try: try:
bake_generator.start( bake_generator.start(
video_path=video_path, video_path=video_path,
detections_path=detections_path, detections_path=detections_path,
output_path=output_path, output_path=output_path,
blur_size=blur_size, blur_size=blur_size,
display_scale=display_scale, display_scale=display_scale,
fmt=bake_format.lower(), fmt=bake_format.lower(),
on_complete=on_complete, on_complete=on_complete,
on_progress=on_progress, on_progress=on_progress,
) )
except Exception as e: except Exception as e:
self.report({"ERROR"}, f"Failed to start bake: {e}") operator.report({"ERROR"}, f"Failed to start bake: {e}")
return {"CANCELLED"} return {"CANCELLED"}
self.report({"INFO"}, "Started blur bake in background") operator.report({"INFO"}, "Started blur bake in background")
return {"FINISHED"}
class SEQUENCER_OT_bake_and_swap_blur_source(Operator):
"""Bake masked blur (reuse cache if parameters match)."""
bl_idname = "sequencer.bake_and_swap_blur_source"
bl_label = "Bake"
bl_description = "Bake masked blur to video and swap active strip source"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not context.scene.sequence_editor:
return False
if get_mask_generator().is_running:
return False
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
return bool(strip and strip.type == "MOVIE")
def execute(self, context):
return _start_bake_impl(self, context, force=False)
class SEQUENCER_OT_force_rebake_blur(Operator):
"""Force re-bake, ignoring any existing cached result."""
bl_idname = "sequencer.force_rebake_blur"
bl_label = "Re-bake"
bl_description = "Discard cached bake and re-bake from scratch"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not context.scene.sequence_editor:
return False
if get_mask_generator().is_running:
return False
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
return bool(strip and strip.type == "MOVIE")
def execute(self, context):
return _start_bake_impl(self, context, force=True)
class SEQUENCER_OT_swap_to_baked_blur(Operator):
"""Swap active strip source to already-baked video (no re-bake)."""
bl_idname = "sequencer.swap_to_baked_blur"
bl_label = "Swap to Baked"
bl_description = "Switch active strip source to the baked video without re-baking"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not context.scene.sequence_editor:
return False
if get_bake_generator().is_running:
return False
strip = context.scene.sequence_editor.active_strip
if not strip or strip.type != "MOVIE":
return False
baked_path = strip.get(KEY_BAKED)
return bool(baked_path and os.path.exists(baked_path))
def execute(self, context):
strip = context.scene.sequence_editor.active_strip
baked_path = strip.get(KEY_BAKED)
_set_strip_source(strip, baked_path)
strip[KEY_MODE] = "baked"
self.report({"INFO"}, "Swapped to baked source")
return {"FINISHED"} return {"FINISHED"}
@ -179,7 +236,7 @@ class SEQUENCER_OT_restore_original_source(Operator):
"""Restore active strip source filepath to original video.""" """Restore active strip source filepath to original video."""
bl_idname = "sequencer.restore_original_source" bl_idname = "sequencer.restore_original_source"
bl_label = "Restore Original Source" bl_label = "Restore Original"
bl_description = "Restore active strip to original source filepath" bl_description = "Restore active strip to original source filepath"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@ -192,6 +249,8 @@ class SEQUENCER_OT_restore_original_source(Operator):
strip = context.scene.sequence_editor.active_strip strip = context.scene.sequence_editor.active_strip
if not strip or strip.type != "MOVIE": if not strip or strip.type != "MOVIE":
return False return False
if strip.get(KEY_MODE, "original") == "original":
return False
return bool(strip.get(KEY_ORIGINAL)) return bool(strip.get(KEY_ORIGINAL))
def execute(self, context): def execute(self, context):
@ -215,7 +274,7 @@ class SEQUENCER_OT_apply_mask_blur(Operator):
bl_idname = "sequencer.apply_mask_blur" bl_idname = "sequencer.apply_mask_blur"
bl_label = "Apply Mask Blur" bl_label = "Apply Mask Blur"
bl_description = "Compatibility alias for Bake & Swap Source" bl_description = "Compatibility alias for Bake"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
@ -246,6 +305,8 @@ class SEQUENCER_OT_cancel_bake_blur(Operator):
classes = [ classes = [
SEQUENCER_OT_bake_and_swap_blur_source, SEQUENCER_OT_bake_and_swap_blur_source,
SEQUENCER_OT_force_rebake_blur,
SEQUENCER_OT_swap_to_baked_blur,
SEQUENCER_OT_restore_original_source, SEQUENCER_OT_restore_original_source,
SEQUENCER_OT_cancel_bake_blur, SEQUENCER_OT_cancel_bake_blur,
SEQUENCER_OT_apply_mask_blur, SEQUENCER_OT_apply_mask_blur,

View File

@ -58,10 +58,16 @@ class SEQUENCER_OT_generate_face_mask(Operator):
self.report({'INFO'}, f"Using cached detections from {output_dir}") self.report({'INFO'}, f"Using cached detections from {output_dir}")
return {'FINISHED'} return {'FINISHED'}
# Get frame range # 動画の実際のフレーム数を取得Blenderプロジェクトのfpsと動画のfpsが
start_frame = strip.frame_final_start # 異なる場合にタイムライン上のフレーム数では不足するため)
end_frame = strip.frame_final_end import cv2 as _cv2
fps = scene.render.fps / scene.render.fps_base _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 # Start async generation
generator = get_generator() generator = get_generator()
@ -105,7 +111,7 @@ class SEQUENCER_OT_generate_face_mask(Operator):
# Initialize progress # Initialize progress
wm = context.window_manager wm = context.window_manager
wm.mask_progress = 0 wm.mask_progress = 0
wm.mask_total = end_frame - start_frame + 1 wm.mask_total = total_video_frames
# Get parameters from scene properties # Get parameters from scene properties
conf_threshold = scene.facemask_conf_threshold conf_threshold = scene.facemask_conf_threshold
@ -115,8 +121,8 @@ class SEQUENCER_OT_generate_face_mask(Operator):
generator.start( generator.start(
video_path=video_path, video_path=video_path,
output_dir=output_dir, output_dir=output_dir,
start_frame=0, # Frame indices in video start_frame=0,
end_frame=end_frame - start_frame, end_frame=total_video_frames - 1,
fps=fps, fps=fps,
conf_threshold=conf_threshold, conf_threshold=conf_threshold,
iou_threshold=iou_threshold, iou_threshold=iou_threshold,

View File

@ -216,7 +216,7 @@ class SEQUENCER_PT_face_mask(Panel):
has_mask = bpy.path.abspath(detections_path) and os.path.exists( has_mask = bpy.path.abspath(detections_path) and os.path.exists(
bpy.path.abspath(detections_path) bpy.path.abspath(detections_path)
) )
if not has_mask: if not has_mask:
box.label(text="Generate detection cache first", icon='INFO') box.label(text="Generate detection cache first", icon='INFO')
return return
@ -227,24 +227,39 @@ class SEQUENCER_PT_face_mask(Panel):
col.prop(context.scene, "facemask_bake_display_scale") col.prop(context.scene, "facemask_bake_display_scale")
col.prop(context.scene, "facemask_bake_format") col.prop(context.scene, "facemask_bake_format")
# Source status box.separator()
source_mode = strip.get("facemask_source_mode", "original")
if source_mode == "baked":
box.label(text="Source: Baked", icon='CHECKMARK')
else:
box.label(text="Source: Original", icon='FILE_MOVIE')
# Bake and restore buttons baked_path = strip.get("facemask_baked_filepath", "")
box.operator( has_baked = bool(baked_path and os.path.exists(bpy.path.abspath(baked_path)))
"sequencer.bake_and_swap_blur_source", source_mode = strip.get("facemask_source_mode", "original")
text="Bake & Swap Source",
icon='RENDER_STILL', if not has_baked:
) # 初回: Bakeのみ
box.operator( box.operator(
"sequencer.restore_original_source", "sequencer.bake_and_swap_blur_source",
text="Restore Original Source", text="Bake",
icon='LOOP_BACK', icon='RENDER_STILL',
) )
else:
# Bake済み: ソース切り替え + Re-bake
row = box.row(align=True)
if source_mode == "baked":
row.operator(
"sequencer.restore_original_source",
text="Restore Original",
icon='LOOP_BACK',
)
else:
row.operator(
"sequencer.swap_to_baked_blur",
text="Swap to Baked",
icon='PLAY',
)
row.operator(
"sequencer.force_rebake_blur",
text="Re-bake",
icon='FILE_REFRESH',
)
# Registration # Registration