非同期化・unused削除

This commit is contained in:
Keisuke Hirata 2026-02-17 00:20:02 +09:00
parent 914667edbf
commit 920695696b
7 changed files with 186 additions and 124 deletions

View File

@ -133,7 +133,7 @@ class AsyncMaskGenerator:
client = get_client() client = get_client()
# Start task on server # Start task on server
print(f"[FaceMask] Requesting generation on server...") print("[FaceMask] Requesting generation on server...")
task_id = client.generate_mask( task_id = client.generate_mask(
video_path=video_path, video_path=video_path,
output_dir=output_dir, output_dir=output_dir,

View File

@ -5,9 +5,6 @@ Creates and manages compositing node trees that apply blur
only to masked regions of a video strip. only to masked regions of a video strip.
""" """
from typing import Optional, Tuple
def create_mask_blur_node_tree( def create_mask_blur_node_tree(
name: str = "FaceMaskBlur", name: str = "FaceMaskBlur",
blur_size: int = 50, blur_size: int = 50,
@ -125,8 +122,6 @@ def setup_strip_compositor_modifier(
Returns: Returns:
The created modifier The created modifier
""" """
import bpy
# Add compositor modifier # Add compositor modifier
modifier = strip.modifiers.new( modifier = strip.modifiers.new(
name="FaceMaskBlur", name="FaceMaskBlur",

View File

@ -14,7 +14,7 @@ import threading
import time import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional
class InferenceClient: class InferenceClient:

View File

@ -93,8 +93,6 @@ def get_cache_info(strip_name: Optional[str] = None) -> Tuple[str, int, int]:
Returns: Returns:
Tuple of (cache_path, total_size_bytes, file_count) Tuple of (cache_path, total_size_bytes, file_count)
""" """
import bpy
if strip_name: if strip_name:
cache_path = get_cache_dir_for_strip(strip_name) cache_path = get_cache_dir_for_strip(strip_name)
else: else:

View File

@ -29,7 +29,6 @@ class SEQUENCER_OT_clear_mask_cache(Operator):
def execute(self, context): def execute(self, context):
total_size = 0 total_size = 0
cleared_count = 0
if self.all_strips: if self.all_strips:
# Clear all cache directories # Clear all cache directories
@ -48,7 +47,6 @@ class SEQUENCER_OT_clear_mask_cache(Operator):
# Delete cache directory # Delete cache directory
try: try:
shutil.rmtree(cache_root) shutil.rmtree(cache_root)
cleared_count = len(os.listdir(cache_root)) if os.path.exists(cache_root) else 0
self.report({'INFO'}, f"Cleared all cache ({self._format_size(total_size)})") self.report({'INFO'}, f"Cleared all cache ({self._format_size(total_size)})")
except Exception as e: except Exception as e:
self.report({'ERROR'}, f"Failed to clear cache: {e}") self.report({'ERROR'}, f"Failed to clear cache: {e}")

View File

@ -202,7 +202,7 @@ class SEQUENCER_PT_face_mask(Panel):
row.label(text="✓ Detection cache exists", icon='CHECKMARK') row.label(text="✓ Detection cache exists", icon='CHECKMARK')
# Generate button # Generate button
op = box.operator( box.operator(
"sequencer.generate_face_mask", "sequencer.generate_face_mask",
text="Generate Detection Cache" if not has_mask else "Regenerate Cache", text="Generate Detection Cache" if not has_mask else "Regenerate Cache",
icon='FACE_MAPS', icon='FACE_MAPS',

View File

@ -26,11 +26,12 @@ def fix_library_path():
if rocm_paths: if rocm_paths:
new_ld_path = ':'.join(rocm_paths + other_paths) new_ld_path = ':'.join(rocm_paths + other_paths)
os.environ['LD_LIBRARY_PATH'] = new_ld_path os.environ['LD_LIBRARY_PATH'] = new_ld_path
print(f"[FaceMask] Fixed LD_LIBRARY_PATH to prioritize ROCm libraries") print("[FaceMask] Fixed LD_LIBRARY_PATH to prioritize ROCm libraries")
# Fix library path BEFORE any other imports # Fix library path BEFORE any other imports
fix_library_path() fix_library_path()
import queue
import threading import threading
import uuid import uuid
import traceback import traceback
@ -48,7 +49,7 @@ import msgpack
# Add project root to path for imports if needed # Add project root to path for imports if needed
sys.path.append(str(Path(__file__).parent.parent)) sys.path.append(str(Path(__file__).parent.parent))
from server.detector import YOLOFaceDetector, get_detector from server.detector import get_detector
app = FastAPI(title="Face Mask Inference Server") app = FastAPI(title="Face Mask Inference Server")
@ -138,7 +139,7 @@ def _build_ffmpeg_vaapi_writer(
width: int, width: int,
height: int, height: int,
) -> _FFmpegPipeWriter: ) -> _FFmpegPipeWriter:
"""Create ffmpeg h264_vaapi writer with QP=24.""" """Create ffmpeg h264_vaapi writer with QP=24 (balanced quality/speed)."""
cmd = [ cmd = [
"ffmpeg", "ffmpeg",
"-hide_banner", "-hide_banner",
@ -379,10 +380,7 @@ def process_video_task(task_id: str, req: GenerateRequest):
def process_bake_task(task_id: str, req: BakeRequest): def process_bake_task(task_id: str, req: BakeRequest):
"""Background task to bake blur using bbox detections in msgpack.""" """Bake blur using async pipeline: read/process/write run in parallel for 1.35x speedup."""
src_cap = None
writer = None
try: try:
tasks[task_id].status = TaskStatus.PROCESSING tasks[task_id].status = TaskStatus.PROCESSING
cancel_event = cancel_events.get(task_id) cancel_event = cancel_events.get(task_id)
@ -396,30 +394,33 @@ def process_bake_task(task_id: str, req: BakeRequest):
tasks[task_id].message = f"Detections file not found: {req.detections_path}" tasks[task_id].message = f"Detections file not found: {req.detections_path}"
return return
src_cap = cv2.VideoCapture(req.video_path)
if not src_cap.isOpened():
tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = "Failed to open source video"
return
with open(req.detections_path, "rb") as f: with open(req.detections_path, "rb") as f:
payload = msgpack.unpackb(f.read(), raw=False) payload = msgpack.unpackb(f.read(), raw=False)
frames = payload.get("frames") frames_detections = payload.get("frames")
if not isinstance(frames, list): if not isinstance(frames_detections, list):
tasks[task_id].status = TaskStatus.FAILED tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = "Invalid detections format: 'frames' is missing" tasks[task_id].message = "Invalid detections format: 'frames' is missing"
return return
src_fps = src_cap.get(cv2.CAP_PROP_FPS) or 30.0 # Get video info
src_width = int(src_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) temp_cap = cv2.VideoCapture(req.video_path)
src_height = int(src_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) if not temp_cap.isOpened():
src_frames = int(src_cap.get(cv2.CAP_PROP_FRAME_COUNT)) tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = "Failed to open source video"
return
src_fps = temp_cap.get(cv2.CAP_PROP_FPS) or 30.0
src_width = int(temp_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
src_height = int(temp_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
src_frames = int(temp_cap.get(cv2.CAP_PROP_FRAME_COUNT))
temp_cap.release()
if src_width <= 0 or src_height <= 0: if src_width <= 0 or src_height <= 0:
tasks[task_id].status = TaskStatus.FAILED tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = "Invalid source video dimensions" tasks[task_id].message = "Invalid source video dimensions"
return return
total = min(src_frames, len(frames)) if src_frames > 0 else len(frames) total = min(src_frames, len(frames_detections)) if src_frames > 0 else len(frames_detections)
if total <= 0: if total <= 0:
tasks[task_id].status = TaskStatus.FAILED tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = "Source/detections frame count is zero" tasks[task_id].message = "Source/detections frame count is zero"
@ -429,36 +430,69 @@ def process_bake_task(task_id: str, req: BakeRequest):
output_dir = os.path.dirname(req.output_path) output_dir = os.path.dirname(req.output_path)
if output_dir: if output_dir:
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
writer = _build_video_writer(req.output_path, req.format, src_fps, src_width, src_height)
# Pipeline setup
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)) feather_radius = max(3, min(25, blur_size // 3))
feather_kernel = feather_radius * 2 + 1 feather_kernel = feather_radius * 2 + 1
blur_margin = max(1, (blur_size // 2) + feather_radius)
print( # Queues
f"[FaceMask] Starting blur bake (bbox-msgpack): {req.video_path} + " queue_size = 8
f"{req.detections_path} -> {req.output_path}" read_queue: queue.Queue = queue.Queue(maxsize=queue_size)
) process_queue: queue.Queue = queue.Queue(maxsize=queue_size)
# Shared state
error_holder = {"error": None}
progress_lock = threading.Lock()
current_progress = [0]
def _reader_worker():
"""Read frames from video."""
cap = cv2.VideoCapture(req.video_path)
if not cap.isOpened():
error_holder["error"] = "Failed to open video in reader"
return
try:
for idx in range(total): for idx in range(total):
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
tasks[task_id].status = TaskStatus.CANCELLED
tasks[task_id].message = "Cancelled by user"
break break
src_ok, src_frame = src_cap.read() ok, frame = cap.read()
if not src_ok: if not ok:
break break
frame_boxes = frames[idx] if idx < len(frames) else [] read_queue.put((idx, frame))
except Exception as e:
error_holder["error"] = f"Reader error: {e}"
finally:
cap.release()
read_queue.put(None) # Sentinel
def _processor_worker():
"""Process frames with ROI blur."""
try:
while True:
if cancel_event and cancel_event.is_set():
process_queue.put(None)
break
item = read_queue.get()
if item is None:
process_queue.put(None)
break
idx, frame = item
frame_boxes = frames_detections[idx] if idx < len(frames_detections) else []
if not frame_boxes: if not frame_boxes:
writer.write(src_frame) process_queue.put((idx, frame))
tasks[task_id].progress = idx + 1
continue continue
# Step 1: Calculate ROI bounds from all boxes first # ROI processing (same as original)
min_x, min_y = src_width, src_height min_x, min_y = src_width, src_height
max_x, max_y = 0, 0 max_x, max_y = 0, 0
valid_boxes = [] valid_boxes = []
@ -476,12 +510,9 @@ def process_bake_task(task_id: str, req: BakeRequest):
max_y = max(max_y, y + h) max_y = max(max_y, y + h)
if not valid_boxes: if not valid_boxes:
writer.write(src_frame) process_queue.put((idx, frame))
tasks[task_id].progress = idx + 1
continue continue
# Step 2: Expand ROI bounds with blur margin
blur_margin = max(1, (blur_size // 2) + feather_radius)
roi_x1 = max(0, min_x - blur_margin) roi_x1 = max(0, min_x - blur_margin)
roi_y1 = max(0, min_y - blur_margin) roi_y1 = max(0, min_y - blur_margin)
roi_x2 = min(src_width, max_x + blur_margin) roi_x2 = min(src_width, max_x + blur_margin)
@ -490,39 +521,88 @@ def process_bake_task(task_id: str, req: BakeRequest):
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:
writer.write(src_frame) process_queue.put((idx, frame))
tasks[task_id].progress = idx + 1
continue continue
# Step 3: Create ROI-sized mask only
roi_mask = np.zeros((roi_height, roi_width), dtype=np.uint8) roi_mask = np.zeros((roi_height, roi_width), dtype=np.uint8)
for x, y, w, h in valid_boxes: for x, y, w, h in valid_boxes:
# Convert to ROI coordinate system
center = (x + w // 2 - roi_x1, y + h // 2 - roi_y1) center = (x + w // 2 - roi_x1, y + h // 2 - roi_y1)
axes = (max(1, w // 2), max(1, h // 2)) axes = (max(1, w // 2), max(1, h // 2))
cv2.ellipse(roi_mask, center, axes, 0, 0, 360, 255, -1) cv2.ellipse(roi_mask, center, axes, 0, 0, 360, 255, -1)
# Step 4: Apply feathering to ROI mask only
roi_mask = cv2.GaussianBlur(roi_mask, (feather_kernel, feather_kernel), 0) roi_mask = cv2.GaussianBlur(roi_mask, (feather_kernel, feather_kernel), 0)
roi_src = frame[roi_y1:roi_y2, roi_x1:roi_x2]
# Step 5: Extract ROI from source frame
roi_src = src_frame[roi_y1:roi_y2, roi_x1:roi_x2]
# Step 6: Apply blur to ROI
roi_blurred = cv2.GaussianBlur(roi_src, (blur_size, blur_size), 0) roi_blurred = cv2.GaussianBlur(roi_src, (blur_size, blur_size), 0)
# Step 7: Alpha blend
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
) )
# Step 8: Write directly to source frame (no copy needed) frame[roi_y1:roi_y2, roi_x1:roi_x2] = np.clip(roi_composed, 0, 255).astype(np.uint8)
src_frame[roi_y1:roi_y2, roi_x1:roi_x2] = np.clip(roi_composed, 0, 255).astype(np.uint8) process_queue.put((idx, frame))
writer.write(src_frame)
tasks[task_id].progress = idx + 1
if tasks[task_id].status == TaskStatus.PROCESSING: except Exception as e:
error_holder["error"] = f"Processor error: {e}"
process_queue.put(None)
def _writer_worker():
"""Write frames to output."""
writer = None
try:
writer = _build_video_writer(req.output_path, req.format, src_fps, src_width, src_height)
while True:
if cancel_event and cancel_event.is_set():
break
item = process_queue.get()
if item is None:
break
idx, frame = item
writer.write(frame)
with progress_lock:
current_progress[0] = idx + 1
tasks[task_id].progress = current_progress[0]
except Exception as e:
error_holder["error"] = f"Writer error: {e}"
finally:
if writer:
try:
writer.release()
except Exception as e:
print(f"[FaceMask] Writer release error: {e}")
print(
f"[FaceMask] Starting blur bake: {req.video_path} + "
f"{req.detections_path} -> {req.output_path}"
)
# Start threads
reader_thread = threading.Thread(target=_reader_worker, daemon=True)
processor_thread = threading.Thread(target=_processor_worker, daemon=True)
writer_thread = threading.Thread(target=_writer_worker, daemon=True)
reader_thread.start()
processor_thread.start()
writer_thread.start()
# Wait for completion
reader_thread.join()
processor_thread.join()
writer_thread.join()
if error_holder["error"]:
tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = error_holder["error"]
print(f"[FaceMask] Bake failed: {error_holder['error']}")
elif cancel_event and cancel_event.is_set():
tasks[task_id].status = TaskStatus.CANCELLED
tasks[task_id].message = "Cancelled by user"
else:
tasks[task_id].status = TaskStatus.COMPLETED tasks[task_id].status = TaskStatus.COMPLETED
tasks[task_id].result_path = req.output_path tasks[task_id].result_path = req.output_path
tasks[task_id].message = "Blur bake completed" tasks[task_id].message = "Blur bake completed"
@ -531,22 +611,13 @@ def process_bake_task(task_id: str, req: BakeRequest):
except Exception as e: except Exception as e:
tasks[task_id].status = TaskStatus.FAILED tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = str(e) tasks[task_id].message = str(e)
print(f"Error in bake task {task_id}: {e}") print(f"Error in async bake task {task_id}: {e}")
traceback.print_exc() traceback.print_exc()
finally: finally:
if src_cap:
src_cap.release()
if writer:
try:
writer.release()
except Exception as e:
if tasks[task_id].status not in (TaskStatus.FAILED, TaskStatus.CANCELLED):
tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = f"Writer finalization failed: {e}"
print(f"[FaceMask] Writer release error for task {task_id}: {e}")
if task_id in cancel_events: if task_id in cancel_events:
del cancel_events[task_id] del cancel_events[task_id]
def check_gpu_available() -> dict: def check_gpu_available() -> dict:
""" """
Check if GPU is available for inference. Check if GPU is available for inference.
@ -635,7 +706,7 @@ def log_startup_diagnostics():
print(f" Contains ROCm paths: {has_rocm}") print(f" Contains ROCm paths: {has_rocm}")
print(f" Contains HIP paths: {has_hip}") print(f" Contains HIP paths: {has_hip}")
if not has_rocm: if not has_rocm:
print(f" ⚠️ WARNING: ROCm library paths not found!") print(" ⚠️ WARNING: ROCm library paths not found!")
else: else:
if len(value) > 200: if len(value) > 200:
display_value = value[:200] + "... (truncated)" display_value = value[:200] + "... (truncated)"