From da9de60697a3f6c38ca938fd5b72086867a985df Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 19 Feb 2026 09:45:05 +0900 Subject: [PATCH] =?UTF-8?q?Blur=E3=82=B5=E3=82=A4=E3=82=BA=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 20 +++++------ core/async_bake_generator.py | 5 ++- core/async_generator.py | 4 --- core/inference_client.py | 4 +-- operators/apply_blur.py | 10 ++++++ operators/generate_mask.py | 2 -- panels/vse_panel.py | 2 +- server/main.py | 65 +++++++++++++++++++----------------- 8 files changed, 61 insertions(+), 51 deletions(-) diff --git a/__init__.py b/__init__.py index 76c7c7d..4086447 100644 --- a/__init__.py +++ b/__init__.py @@ -40,15 +40,6 @@ def register(): 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( name="Cache Directory", description="Optional cache root directory (empty = default .mask_cache)", @@ -64,6 +55,15 @@ def register(): 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( name="Bake Format", description="Output format for baked blur video", @@ -91,9 +91,9 @@ def unregister(): # Unregister scene properties del bpy.types.Scene.facemask_conf_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_bake_blur_size + del bpy.types.Scene.facemask_bake_display_scale del bpy.types.Scene.facemask_bake_format diff --git a/core/async_bake_generator.py b/core/async_bake_generator.py index d8facea..073e444 100644 --- a/core/async_bake_generator.py +++ b/core/async_bake_generator.py @@ -32,6 +32,7 @@ class AsyncBakeGenerator: detections_path: str, output_path: str, blur_size: int, + display_scale: float, fmt: str, on_complete: Optional[Callable] = None, on_progress: Optional[Callable] = None, @@ -53,7 +54,7 @@ class AsyncBakeGenerator: self.worker_thread = threading.Thread( 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, ) self.worker_thread.start() @@ -75,6 +76,7 @@ class AsyncBakeGenerator: detections_path: str, output_path: str, blur_size: int, + display_scale: float, fmt: str, ): import time @@ -88,6 +90,7 @@ class AsyncBakeGenerator: detections_path=detections_path, output_path=output_path, blur_size=blur_size, + display_scale=display_scale, fmt=fmt, ) diff --git a/core/async_generator.py b/core/async_generator.py index d642a9e..8a031c4 100644 --- a/core/async_generator.py +++ b/core/async_generator.py @@ -44,7 +44,6 @@ class AsyncMaskGenerator: fps: float, conf_threshold: float = 0.5, iou_threshold: float = 0.45, - mask_scale: float = 1.5, on_complete: Optional[Callable] = None, on_progress: Optional[Callable] = None, ): @@ -94,7 +93,6 @@ class AsyncMaskGenerator: fps, conf_threshold, iou_threshold, - mask_scale, ), daemon=True, ) @@ -121,7 +119,6 @@ class AsyncMaskGenerator: fps: float, conf_threshold: float, iou_threshold: float, - mask_scale: float, ): """ Worker thread function. Delegates to inference server and polls status. @@ -141,7 +138,6 @@ class AsyncMaskGenerator: end_frame=end_frame, conf_threshold=conf_threshold, iou_threshold=iou_threshold, - mask_scale=mask_scale, ) print(f"[FaceMask] Task started: {task_id}") diff --git a/core/inference_client.py b/core/inference_client.py index 20566ca..4a061ed 100644 --- a/core/inference_client.py +++ b/core/inference_client.py @@ -204,7 +204,6 @@ class InferenceClient: end_frame: int, conf_threshold: float, iou_threshold: float, - mask_scale: float, ) -> str: """ Request mask generation. @@ -222,7 +221,6 @@ class InferenceClient: "end_frame": end_frame, "conf_threshold": conf_threshold, "iou_threshold": iou_threshold, - "mask_scale": mask_scale, } req = urllib.request.Request( @@ -255,6 +253,7 @@ class InferenceClient: detections_path: str, output_path: str, blur_size: int, + display_scale: float, fmt: str, ) -> str: """ @@ -271,6 +270,7 @@ class InferenceClient: "detections_path": detections_path, "output_path": output_path, "blur_size": blur_size, + "display_scale": display_scale, "format": fmt, } diff --git a/operators/apply_blur.py b/operators/apply_blur.py index e7ec761..69e9ede 100644 --- a/operators/apply_blur.py +++ b/operators/apply_blur.py @@ -20,6 +20,7 @@ KEY_BAKED = "facemask_baked_filepath" KEY_MODE = "facemask_source_mode" KEY_FORMAT = "facemask_bake_format" KEY_BLUR_SIZE = "facemask_bake_blur_size" +KEY_DISPLAY_SCALE = "facemask_bake_display_scale" FORMAT_EXT = { @@ -86,20 +87,27 @@ class SEQUENCER_OT_bake_and_swap_blur_source(Operator): 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_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: cached_blur_size_int = int(cached_blur_size) except (TypeError, ValueError): cached_blur_size_int = None + try: + 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 and cached_blur_size_int == blur_size + and cached_display_scale_f == display_scale ): if video_strip.get(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_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}") elif status == "error": @@ -153,6 +162,7 @@ class SEQUENCER_OT_bake_and_swap_blur_source(Operator): 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, diff --git a/operators/generate_mask.py b/operators/generate_mask.py index 3d2226c..ba0455c 100644 --- a/operators/generate_mask.py +++ b/operators/generate_mask.py @@ -110,7 +110,6 @@ class SEQUENCER_OT_generate_face_mask(Operator): # Get parameters from scene properties conf_threshold = scene.facemask_conf_threshold iou_threshold = scene.facemask_iou_threshold - mask_scale = scene.facemask_mask_scale # Start generation generator.start( @@ -121,7 +120,6 @@ class SEQUENCER_OT_generate_face_mask(Operator): fps=fps, conf_threshold=conf_threshold, iou_threshold=iou_threshold, - mask_scale=mask_scale, on_complete=on_complete, on_progress=on_progress, ) diff --git a/panels/vse_panel.py b/panels/vse_panel.py index a35a6b9..5b3d496 100644 --- a/panels/vse_panel.py +++ b/panels/vse_panel.py @@ -74,7 +74,6 @@ class SEQUENCER_PT_face_mask(Panel): col = box.column(align=True) col.prop(scene, "facemask_conf_threshold") col.prop(scene, "facemask_iou_threshold") - col.prop(scene, "facemask_mask_scale") def _draw_server_status(self, layout): """Draw server status and GPU info.""" @@ -225,6 +224,7 @@ class SEQUENCER_PT_face_mask(Panel): # Bake parameters col = box.column(align=True) col.prop(context.scene, "facemask_bake_blur_size") + col.prop(context.scene, "facemask_bake_display_scale") col.prop(context.scene, "facemask_bake_format") # Source status diff --git a/server/main.py b/server/main.py index decc232..150143e 100644 --- a/server/main.py +++ b/server/main.py @@ -83,7 +83,6 @@ class GenerateRequest(BaseModel): end_frame: int conf_threshold: float = 0.5 iou_threshold: float = 0.45 - mask_scale: float = 1.5 class BakeRequest(BaseModel): @@ -91,6 +90,7 @@ class BakeRequest(BaseModel): detections_path: str output_path: str blur_size: int = 50 + display_scale: float = 1.0 format: str = "mp4" @@ -305,20 +305,15 @@ def process_video_task(task_id: str, req: GenerateRequest): for detections in batch_detections: packed_detections: List[List[float]] = [] for x, y, w, h, conf in detections: - scaled = _scale_bbox( - int(x), - int(y), - int(w), - int(h), - float(req.mask_scale), - width, - height, - ) - if scaled is None: + # bboxをそのまま保存(表示スケールはBake時に適用) + bx, by, bw, bh = int(x), int(y), int(w), int(h) + bx = max(0, bx) + by = max(0, by) + bw = min(width - bx, bw) + bh = min(height - by, bh) + if bw <= 0 or bh <= 0: continue - packed_detections.append( - [scaled[0], scaled[1], scaled[2], scaled[3], float(conf)] - ) + packed_detections.append([bx, by, bw, bh, float(conf)]) frame_detections.append(packed_detections) current_count += 1 tasks[task_id].progress = current_count @@ -356,7 +351,7 @@ def process_video_task(task_id: str, req: GenerateRequest): "width": width, "height": height, "fps": fps, - "mask_scale": float(req.mask_scale), + "mask_scale": 1.0, "frames": frame_detections, } 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)) if blur_size % 2 == 0: blur_size += 1 - feather_radius = max(3, min(25, blur_size // 3)) - feather_kernel = feather_radius * 2 + 1 - blur_margin = max(1, (blur_size // 2) + feather_radius) + display_scale = max(0.1, float(req.display_scale)) + # blur_margin は境界問題回避のための計算用余白のみ(表示には使わない) + blur_margin = blur_size // 2 # Queues queue_size = 8 @@ -507,29 +502,37 @@ def process_bake_task(task_id: str, req: BakeRequest): continue for x, y, w, h in valid_boxes: - roi_x1 = max(0, x - blur_margin) - roi_y1 = max(0, y - blur_margin) - roi_x2 = min(src_width, x + w + blur_margin) - roi_y2 = min(src_height, y + h + blur_margin) + # display_scale で表示サイズを決定 + cx = x + w / 2 + cy = y + h / 2 + 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_height = roi_y2 - roi_y1 if roi_width <= 0 or roi_height <= 0: continue - roi_mask = np.zeros((roi_height, roi_width), dtype=np.uint8) - 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全体で計算(余白があるので端の精度が保証される) roi_src = frame[roi_y1:roi_y2, roi_x1:roi_x2] 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_composed = (roi_src.astype(np.float32) * (1.0 - roi_alpha)) + ( - roi_blurred.astype(np.float32) * roi_alpha - ) + roi_composed = roi_src.astype(np.float32) * (1.0 - 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) process_queue.put((idx, frame))