Blurサイズ問題の修正

This commit is contained in:
Keisuke Hirata 2026-02-19 09:45:05 +09:00
parent 9ce6ec99d3
commit da9de60697
8 changed files with 61 additions and 51 deletions

View File

@ -40,15 +40,6 @@ def register():
step=0.01, step=0.01,
) )
bpy.types.Scene.facemask_mask_scale = FloatProperty(
name="Mask Scale",
description="Scale factor for mask region (1.0 = exact face size)",
default=1.5,
min=1.0,
max=3.0,
step=0.1,
)
bpy.types.Scene.facemask_cache_dir = StringProperty( bpy.types.Scene.facemask_cache_dir = StringProperty(
name="Cache Directory", name="Cache Directory",
description="Optional cache root directory (empty = default .mask_cache)", description="Optional cache root directory (empty = default .mask_cache)",
@ -64,6 +55,15 @@ def register():
max=501, max=501,
) )
bpy.types.Scene.facemask_bake_display_scale = FloatProperty(
name="Mask Scale",
description="Scale factor for the blur mask ellipse at bake time (1.0 = raw detection size)",
default=1.3,
min=0.5,
max=3.0,
step=0.1,
)
bpy.types.Scene.facemask_bake_format = EnumProperty( bpy.types.Scene.facemask_bake_format = EnumProperty(
name="Bake Format", name="Bake Format",
description="Output format for baked blur video", description="Output format for baked blur video",
@ -91,9 +91,9 @@ def unregister():
# Unregister scene properties # Unregister scene properties
del bpy.types.Scene.facemask_conf_threshold del bpy.types.Scene.facemask_conf_threshold
del bpy.types.Scene.facemask_iou_threshold del bpy.types.Scene.facemask_iou_threshold
del bpy.types.Scene.facemask_mask_scale
del bpy.types.Scene.facemask_cache_dir del bpy.types.Scene.facemask_cache_dir
del bpy.types.Scene.facemask_bake_blur_size del bpy.types.Scene.facemask_bake_blur_size
del bpy.types.Scene.facemask_bake_display_scale
del bpy.types.Scene.facemask_bake_format del bpy.types.Scene.facemask_bake_format

View File

@ -32,6 +32,7 @@ class AsyncBakeGenerator:
detections_path: str, detections_path: str,
output_path: str, output_path: str,
blur_size: int, blur_size: int,
display_scale: float,
fmt: str, fmt: str,
on_complete: Optional[Callable] = None, on_complete: Optional[Callable] = None,
on_progress: Optional[Callable] = None, on_progress: Optional[Callable] = None,
@ -53,7 +54,7 @@ class AsyncBakeGenerator:
self.worker_thread = threading.Thread( self.worker_thread = threading.Thread(
target=self._worker, target=self._worker,
args=(video_path, detections_path, output_path, blur_size, fmt), args=(video_path, detections_path, output_path, blur_size, display_scale, fmt),
daemon=True, daemon=True,
) )
self.worker_thread.start() self.worker_thread.start()
@ -75,6 +76,7 @@ class AsyncBakeGenerator:
detections_path: str, detections_path: str,
output_path: str, output_path: str,
blur_size: int, blur_size: int,
display_scale: float,
fmt: str, fmt: str,
): ):
import time import time
@ -88,6 +90,7 @@ class AsyncBakeGenerator:
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,
fmt=fmt, fmt=fmt,
) )

View File

@ -44,7 +44,6 @@ class AsyncMaskGenerator:
fps: float, fps: float,
conf_threshold: float = 0.5, conf_threshold: float = 0.5,
iou_threshold: float = 0.45, iou_threshold: float = 0.45,
mask_scale: float = 1.5,
on_complete: Optional[Callable] = None, on_complete: Optional[Callable] = None,
on_progress: Optional[Callable] = None, on_progress: Optional[Callable] = None,
): ):
@ -94,7 +93,6 @@ class AsyncMaskGenerator:
fps, fps,
conf_threshold, conf_threshold,
iou_threshold, iou_threshold,
mask_scale,
), ),
daemon=True, daemon=True,
) )
@ -121,7 +119,6 @@ class AsyncMaskGenerator:
fps: float, fps: float,
conf_threshold: float, conf_threshold: float,
iou_threshold: float, iou_threshold: float,
mask_scale: float,
): ):
""" """
Worker thread function. Delegates to inference server and polls status. Worker thread function. Delegates to inference server and polls status.
@ -141,7 +138,6 @@ class AsyncMaskGenerator:
end_frame=end_frame, end_frame=end_frame,
conf_threshold=conf_threshold, conf_threshold=conf_threshold,
iou_threshold=iou_threshold, iou_threshold=iou_threshold,
mask_scale=mask_scale,
) )
print(f"[FaceMask] Task started: {task_id}") print(f"[FaceMask] Task started: {task_id}")

View File

@ -204,7 +204,6 @@ class InferenceClient:
end_frame: int, end_frame: int,
conf_threshold: float, conf_threshold: float,
iou_threshold: float, iou_threshold: float,
mask_scale: float,
) -> str: ) -> str:
""" """
Request mask generation. Request mask generation.
@ -222,7 +221,6 @@ class InferenceClient:
"end_frame": end_frame, "end_frame": end_frame,
"conf_threshold": conf_threshold, "conf_threshold": conf_threshold,
"iou_threshold": iou_threshold, "iou_threshold": iou_threshold,
"mask_scale": mask_scale,
} }
req = urllib.request.Request( req = urllib.request.Request(
@ -255,6 +253,7 @@ class InferenceClient:
detections_path: str, detections_path: str,
output_path: str, output_path: str,
blur_size: int, blur_size: int,
display_scale: float,
fmt: str, fmt: str,
) -> str: ) -> str:
""" """
@ -271,6 +270,7 @@ class InferenceClient:
"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,
"format": fmt, "format": fmt,
} }

View File

@ -20,6 +20,7 @@ KEY_BAKED = "facemask_baked_filepath"
KEY_MODE = "facemask_source_mode" KEY_MODE = "facemask_source_mode"
KEY_FORMAT = "facemask_bake_format" KEY_FORMAT = "facemask_bake_format"
KEY_BLUR_SIZE = "facemask_bake_blur_size" KEY_BLUR_SIZE = "facemask_bake_blur_size"
KEY_DISPLAY_SCALE = "facemask_bake_display_scale"
FORMAT_EXT = { FORMAT_EXT = {
@ -86,20 +87,27 @@ class SEQUENCER_OT_bake_and_swap_blur_source(Operator):
bake_format = scene.facemask_bake_format bake_format = scene.facemask_bake_format
output_path = _output_path(video_strip, detections_path, bake_format) output_path = _output_path(video_strip, detections_path, bake_format)
blur_size = int(scene.facemask_bake_blur_size) 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. # 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)
cached_display_scale = video_strip.get(KEY_DISPLAY_SCALE)
try: try:
cached_blur_size_int = int(cached_blur_size) cached_blur_size_int = int(cached_blur_size)
except (TypeError, ValueError): except (TypeError, ValueError):
cached_blur_size_int = None cached_blur_size_int = None
try:
cached_display_scale_f = float(cached_display_scale)
except (TypeError, ValueError):
cached_display_scale_f = None
if ( if (
cached_baked_path cached_baked_path
and os.path.exists(cached_baked_path) and os.path.exists(cached_baked_path)
and cached_format == bake_format and cached_format == bake_format
and cached_blur_size_int == blur_size and cached_blur_size_int == blur_size
and cached_display_scale_f == display_scale
): ):
if video_strip.get(KEY_MODE) != "baked": if video_strip.get(KEY_MODE) != "baked":
video_strip[KEY_MODE] = "baked" video_strip[KEY_MODE] = "baked"
@ -126,6 +134,7 @@ class SEQUENCER_OT_bake_and_swap_blur_source(Operator):
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
_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":
@ -153,6 +162,7 @@ class SEQUENCER_OT_bake_and_swap_blur_source(Operator):
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,
fmt=bake_format.lower(), fmt=bake_format.lower(),
on_complete=on_complete, on_complete=on_complete,
on_progress=on_progress, on_progress=on_progress,

View File

@ -110,7 +110,6 @@ class SEQUENCER_OT_generate_face_mask(Operator):
# Get parameters from scene properties # Get parameters from scene properties
conf_threshold = scene.facemask_conf_threshold conf_threshold = scene.facemask_conf_threshold
iou_threshold = scene.facemask_iou_threshold iou_threshold = scene.facemask_iou_threshold
mask_scale = scene.facemask_mask_scale
# Start generation # Start generation
generator.start( generator.start(
@ -121,7 +120,6 @@ class SEQUENCER_OT_generate_face_mask(Operator):
fps=fps, fps=fps,
conf_threshold=conf_threshold, conf_threshold=conf_threshold,
iou_threshold=iou_threshold, iou_threshold=iou_threshold,
mask_scale=mask_scale,
on_complete=on_complete, on_complete=on_complete,
on_progress=on_progress, on_progress=on_progress,
) )

View File

@ -74,7 +74,6 @@ class SEQUENCER_PT_face_mask(Panel):
col = box.column(align=True) col = box.column(align=True)
col.prop(scene, "facemask_conf_threshold") col.prop(scene, "facemask_conf_threshold")
col.prop(scene, "facemask_iou_threshold") col.prop(scene, "facemask_iou_threshold")
col.prop(scene, "facemask_mask_scale")
def _draw_server_status(self, layout): def _draw_server_status(self, layout):
"""Draw server status and GPU info.""" """Draw server status and GPU info."""
@ -225,6 +224,7 @@ class SEQUENCER_PT_face_mask(Panel):
# Bake parameters # Bake parameters
col = box.column(align=True) col = box.column(align=True)
col.prop(context.scene, "facemask_bake_blur_size") col.prop(context.scene, "facemask_bake_blur_size")
col.prop(context.scene, "facemask_bake_display_scale")
col.prop(context.scene, "facemask_bake_format") col.prop(context.scene, "facemask_bake_format")
# Source status # Source status

View File

@ -83,7 +83,6 @@ class GenerateRequest(BaseModel):
end_frame: int end_frame: int
conf_threshold: float = 0.5 conf_threshold: float = 0.5
iou_threshold: float = 0.45 iou_threshold: float = 0.45
mask_scale: float = 1.5
class BakeRequest(BaseModel): class BakeRequest(BaseModel):
@ -91,6 +90,7 @@ class BakeRequest(BaseModel):
detections_path: str detections_path: str
output_path: str output_path: str
blur_size: int = 50 blur_size: int = 50
display_scale: float = 1.0
format: str = "mp4" format: str = "mp4"
@ -305,20 +305,15 @@ def process_video_task(task_id: str, req: GenerateRequest):
for detections in batch_detections: for detections in batch_detections:
packed_detections: List[List[float]] = [] packed_detections: List[List[float]] = []
for x, y, w, h, conf in detections: for x, y, w, h, conf in detections:
scaled = _scale_bbox( # bboxをそのまま保存表示スケールはBake時に適用
int(x), bx, by, bw, bh = int(x), int(y), int(w), int(h)
int(y), bx = max(0, bx)
int(w), by = max(0, by)
int(h), bw = min(width - bx, bw)
float(req.mask_scale), bh = min(height - by, bh)
width, if bw <= 0 or bh <= 0:
height,
)
if scaled is None:
continue continue
packed_detections.append( packed_detections.append([bx, by, bw, bh, float(conf)])
[scaled[0], scaled[1], scaled[2], scaled[3], float(conf)]
)
frame_detections.append(packed_detections) frame_detections.append(packed_detections)
current_count += 1 current_count += 1
tasks[task_id].progress = current_count tasks[task_id].progress = current_count
@ -356,7 +351,7 @@ def process_video_task(task_id: str, req: GenerateRequest):
"width": width, "width": width,
"height": height, "height": height,
"fps": fps, "fps": fps,
"mask_scale": float(req.mask_scale), "mask_scale": 1.0,
"frames": frame_detections, "frames": frame_detections,
} }
with open(output_msgpack_path, "wb") as f: with open(output_msgpack_path, "wb") as f:
@ -435,9 +430,9 @@ def process_bake_task(task_id: str, req: BakeRequest):
blur_size = max(1, int(req.blur_size)) blur_size = max(1, int(req.blur_size))
if blur_size % 2 == 0: if blur_size % 2 == 0:
blur_size += 1 blur_size += 1
feather_radius = max(3, min(25, blur_size // 3)) display_scale = max(0.1, float(req.display_scale))
feather_kernel = feather_radius * 2 + 1 # blur_margin は境界問題回避のための計算用余白のみ(表示には使わない)
blur_margin = max(1, (blur_size // 2) + feather_radius) blur_margin = blur_size // 2
# Queues # Queues
queue_size = 8 queue_size = 8
@ -507,29 +502,37 @@ def process_bake_task(task_id: str, req: BakeRequest):
continue continue
for x, y, w, h in valid_boxes: for x, y, w, h in valid_boxes:
roi_x1 = max(0, x - blur_margin) # display_scale で表示サイズを決定
roi_y1 = max(0, y - blur_margin) cx = x + w / 2
roi_x2 = min(src_width, x + w + blur_margin) cy = y + h / 2
roi_y2 = min(src_height, y + h + blur_margin) dw = max(1, int(w * display_scale))
dh = max(1, int(h * display_scale))
dx = int(cx - dw / 2)
dy = int(cy - dh / 2)
# ROIは表示サイズ + blur_margin計算用余白、境界問題回避のみ
roi_x1 = max(0, dx - blur_margin)
roi_y1 = max(0, dy - blur_margin)
roi_x2 = min(src_width, dx + dw + blur_margin)
roi_y2 = min(src_height, dy + dh + blur_margin)
roi_width = roi_x2 - roi_x1 roi_width = roi_x2 - roi_x1
roi_height = roi_y2 - roi_y1 roi_height = roi_y2 - roi_y1
if roi_width <= 0 or roi_height <= 0: if roi_width <= 0 or roi_height <= 0:
continue continue
roi_mask = np.zeros((roi_height, roi_width), dtype=np.uint8) # ブラーはROI全体で計算余白があるので端の精度が保証される
center = (x + w // 2 - roi_x1, y + h // 2 - roi_y1)
axes = (max(1, w // 2), max(1, h // 2))
cv2.ellipse(roi_mask, center, axes, 0, 0, 360, 255, -1)
roi_mask = cv2.GaussianBlur(roi_mask, (feather_kernel, feather_kernel), 0)
roi_src = frame[roi_y1:roi_y2, roi_x1:roi_x2] roi_src = frame[roi_y1:roi_y2, roi_x1:roi_x2]
roi_blurred = cv2.GaussianBlur(roi_src, (blur_size, blur_size), 0) roi_blurred = cv2.GaussianBlur(roi_src, (blur_size, blur_size), 0)
# 合成マスクはdisplay_scaleサイズの楕円のみfeatheringなし
roi_mask = np.zeros((roi_height, roi_width), dtype=np.uint8)
center = (int(cx) - roi_x1, int(cy) - roi_y1)
axes = (max(1, dw // 2), max(1, dh // 2))
cv2.ellipse(roi_mask, center, axes, 0, 0, 360, 255, -1)
roi_alpha = (roi_mask.astype(np.float32) / 255.0)[..., np.newaxis] roi_alpha = (roi_mask.astype(np.float32) / 255.0)[..., np.newaxis]
roi_composed = (roi_src.astype(np.float32) * (1.0 - roi_alpha)) + ( roi_composed = roi_src.astype(np.float32) * (1.0 - roi_alpha) + roi_blurred.astype(np.float32) * roi_alpha
roi_blurred.astype(np.float32) * roi_alpha
)
frame[roi_y1:roi_y2, roi_x1:roi_x2] = np.clip(roi_composed, 0, 255).astype(np.uint8) frame[roi_y1:roi_y2, roi_x1:roi_x2] = np.clip(roi_composed, 0, 255).astype(np.uint8)
process_queue.put((idx, frame)) process_queue.put((idx, frame))