UI周りの改善
This commit is contained in:
parent
a3de61d5ce
commit
d67265aa39
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user