Compare commits
No commits in common. "d8d27ddf23719f39d6cc595b197c9c68decab865" and "f2665a49dd6591d8545f7fefa37efbb3951dd46c" have entirely different histories.
d8d27ddf23
...
f2665a49dd
41
__init__.py
41
__init__.py
|
|
@ -15,58 +15,21 @@ bl_info = {
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
"""Register all extension components."""
|
"""Register all extension components."""
|
||||||
import bpy
|
|
||||||
from bpy.props import FloatProperty
|
|
||||||
|
|
||||||
from . import operators
|
from . import operators
|
||||||
from . import panels
|
from . import panels
|
||||||
|
|
||||||
# Register scene properties for face detection parameters
|
|
||||||
bpy.types.Scene.facemask_conf_threshold = FloatProperty(
|
|
||||||
name="Confidence",
|
|
||||||
description="YOLO confidence threshold (higher = fewer false positives)",
|
|
||||||
default=0.5,
|
|
||||||
min=0.1,
|
|
||||||
max=1.0,
|
|
||||||
step=0.01,
|
|
||||||
)
|
|
||||||
|
|
||||||
bpy.types.Scene.facemask_iou_threshold = FloatProperty(
|
|
||||||
name="IOU Threshold",
|
|
||||||
description="Non-maximum suppression IOU threshold",
|
|
||||||
default=0.45,
|
|
||||||
min=0.1,
|
|
||||||
max=1.0,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
operators.register()
|
operators.register()
|
||||||
panels.register()
|
panels.register()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
"""Unregister all extension components."""
|
"""Unregister all extension components."""
|
||||||
import bpy
|
|
||||||
from . import operators
|
from . import operators
|
||||||
from . import panels
|
from . import panels
|
||||||
|
|
||||||
panels.unregister()
|
panels.unregister()
|
||||||
operators.unregister()
|
operators.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
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
register()
|
register()
|
||||||
|
|
|
||||||
|
|
@ -5,36 +5,36 @@ Manages the server process and handles HTTP communication
|
||||||
using standard library (avoiding requests dependency).
|
using standard library (avoiding requests dependency).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
import json
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import threading
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
from typing import Optional, Dict, Any, Tuple
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
from typing import Any, Dict, Optional, Tuple
|
|
||||||
|
|
||||||
|
|
||||||
class InferenceClient:
|
class InferenceClient:
|
||||||
"""Client for the YOLO inference server."""
|
"""Client for the YOLO inference server."""
|
||||||
|
|
||||||
SERVER_URL = "http://127.0.0.1:8181"
|
SERVER_URL = "http://127.0.0.1:8181"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.server_process: Optional[subprocess.Popen] = None
|
self.server_process: Optional[subprocess.Popen] = None
|
||||||
self._server_lock = threading.Lock()
|
self._server_lock = threading.Lock()
|
||||||
self.log_file = None
|
self.log_file = None
|
||||||
self.log_file_path = None
|
self.log_file_path = None
|
||||||
|
|
||||||
def start_server(self):
|
def start_server(self):
|
||||||
"""Start the inference server process."""
|
"""Start the inference server process."""
|
||||||
with self._server_lock:
|
with self._server_lock:
|
||||||
if self.is_server_running():
|
if self.is_server_running():
|
||||||
return
|
return
|
||||||
|
|
||||||
print("[FaceMask] Starting inference server...")
|
print("[FaceMask] Starting inference server...")
|
||||||
|
|
||||||
# Find project root
|
# Find project root
|
||||||
# Assuming this file is in core/inference_client.py
|
# Assuming this file is in core/inference_client.py
|
||||||
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
@ -46,24 +46,24 @@ class InferenceClient:
|
||||||
# Load environment variables from .env file if it exists
|
# Load environment variables from .env file if it exists
|
||||||
env_file = os.path.join(root_dir, ".env")
|
env_file = os.path.join(root_dir, ".env")
|
||||||
if os.path.exists(env_file):
|
if os.path.exists(env_file):
|
||||||
with open(env_file, "r") as f:
|
with open(env_file, 'r') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line and not line.startswith("#") and "=" in line:
|
if line and not line.startswith('#') and '=' in line:
|
||||||
key, value = line.split("=", 1)
|
key, value = line.split('=', 1)
|
||||||
server_env[key] = value
|
server_env[key] = value
|
||||||
print(f"[FaceMask] Loaded environment from: {env_file}")
|
print(f"[FaceMask] Loaded environment from: {env_file}")
|
||||||
|
|
||||||
# Clean PYTHONPATH to avoid conflicts with Nix Python packages
|
# Clean PYTHONPATH to avoid conflicts with Nix Python packages
|
||||||
# Only include project root to allow local imports
|
# Only include project root to allow local imports
|
||||||
server_env["PYTHONPATH"] = root_dir
|
server_env['PYTHONPATH'] = root_dir
|
||||||
|
|
||||||
# Remove Python-related environment variables that might cause conflicts
|
# Remove Python-related environment variables that might cause conflicts
|
||||||
# These can cause venv to import packages from Nix instead of venv
|
# These can cause venv to import packages from Nix instead of venv
|
||||||
env_vars_to_remove = [
|
env_vars_to_remove = [
|
||||||
"PYTHONUNBUFFERED",
|
'PYTHONUNBUFFERED',
|
||||||
"__PYVENV_LAUNCHER__", # macOS venv variable
|
'__PYVENV_LAUNCHER__', # macOS venv variable
|
||||||
"VIRTUAL_ENV", # Will be set by venv's Python automatically
|
'VIRTUAL_ENV', # Will be set by venv's Python automatically
|
||||||
]
|
]
|
||||||
for var in env_vars_to_remove:
|
for var in env_vars_to_remove:
|
||||||
server_env.pop(var, None)
|
server_env.pop(var, None)
|
||||||
|
|
@ -73,27 +73,25 @@ class InferenceClient:
|
||||||
if os.path.isdir(venv_bin):
|
if os.path.isdir(venv_bin):
|
||||||
# Build a clean PATH with venv first, then essential system paths
|
# Build a clean PATH with venv first, then essential system paths
|
||||||
# Filter out any Nix Python-specific paths to avoid version conflicts
|
# Filter out any Nix Python-specific paths to avoid version conflicts
|
||||||
current_path = server_env.get("PATH", "")
|
current_path = server_env.get('PATH', '')
|
||||||
path_entries = current_path.split(":")
|
path_entries = current_path.split(':')
|
||||||
|
|
||||||
# Filter out Nix Python 3.11 paths
|
# Filter out Nix Python 3.11 paths
|
||||||
filtered_paths = [
|
filtered_paths = [
|
||||||
p
|
p for p in path_entries
|
||||||
for p in path_entries
|
if not ('/python3.11/' in p.lower() or '/python3-3.11' in p.lower())
|
||||||
if not ("/python3.11/" in p.lower() or "/python3-3.11" in p.lower())
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Reconstruct PATH with venv first
|
# Reconstruct PATH with venv first
|
||||||
clean_path = ":".join([venv_bin] + filtered_paths)
|
clean_path = ':'.join([venv_bin] + filtered_paths)
|
||||||
server_env["PATH"] = clean_path
|
server_env['PATH'] = clean_path
|
||||||
print(f"[FaceMask] Using venv from: {venv_bin}")
|
print(f"[FaceMask] Using venv from: {venv_bin}")
|
||||||
|
|
||||||
# Prepare log file for server output
|
# Prepare log file for server output
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
log_dir = tempfile.gettempdir()
|
log_dir = tempfile.gettempdir()
|
||||||
self.log_file_path = os.path.join(log_dir, "facemask_server.log")
|
self.log_file_path = os.path.join(log_dir, "facemask_server.log")
|
||||||
self.log_file = open(self.log_file_path, "w", buffering=1) # Line buffered
|
self.log_file = open(self.log_file_path, 'w', buffering=1) # Line buffered
|
||||||
print(f"[FaceMask] Server log: {self.log_file_path}")
|
print(f"[FaceMask] Server log: {self.log_file_path}")
|
||||||
|
|
||||||
# Start process with 'python' command (will use venv if PATH is set correctly)
|
# Start process with 'python' command (will use venv if PATH is set correctly)
|
||||||
|
|
@ -122,12 +120,12 @@ class InferenceClient:
|
||||||
try:
|
try:
|
||||||
if self.log_file:
|
if self.log_file:
|
||||||
self.log_file.close()
|
self.log_file.close()
|
||||||
with open(self.log_file_path, "r") as f:
|
with open(self.log_file_path, 'r') as f:
|
||||||
log_content = f.read()
|
log_content = f.read()
|
||||||
if log_content.strip():
|
if log_content.strip():
|
||||||
print("[FaceMask] Server log:")
|
print("[FaceMask] Server log:")
|
||||||
# Show last 50 lines
|
# Show last 50 lines
|
||||||
lines = log_content.strip().split("\n")
|
lines = log_content.strip().split('\n')
|
||||||
for line in lines[-50:]:
|
for line in lines[-50:]:
|
||||||
print(line)
|
print(line)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -145,18 +143,18 @@ class InferenceClient:
|
||||||
try:
|
try:
|
||||||
if self.log_file:
|
if self.log_file:
|
||||||
self.log_file.close()
|
self.log_file.close()
|
||||||
with open(self.log_file_path, "r") as f:
|
with open(self.log_file_path, 'r') as f:
|
||||||
log_content = f.read()
|
log_content = f.read()
|
||||||
if log_content.strip():
|
if log_content.strip():
|
||||||
print("[FaceMask] Server log (partial):")
|
print("[FaceMask] Server log (partial):")
|
||||||
lines = log_content.strip().split("\n")
|
lines = log_content.strip().split('\n')
|
||||||
for line in lines[-30:]:
|
for line in lines[-30:]:
|
||||||
print(line)
|
print(line)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise RuntimeError("Server startup timed out")
|
raise RuntimeError("Server startup timed out")
|
||||||
|
|
||||||
def stop_server(self):
|
def stop_server(self):
|
||||||
"""Stop the inference server."""
|
"""Stop the inference server."""
|
||||||
with self._server_lock:
|
with self._server_lock:
|
||||||
|
|
@ -177,17 +175,15 @@ class InferenceClient:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.log_file = None
|
self.log_file = None
|
||||||
|
|
||||||
def is_server_running(self) -> bool:
|
def is_server_running(self) -> bool:
|
||||||
"""Check if server is responding."""
|
"""Check if server is responding."""
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(
|
with urllib.request.urlopen(f"{self.SERVER_URL}/status", timeout=1) as response:
|
||||||
f"{self.SERVER_URL}/status", timeout=1
|
|
||||||
) as response:
|
|
||||||
return response.status == 200
|
return response.status == 200
|
||||||
except (urllib.error.URLError, ConnectionRefusedError, TimeoutError):
|
except (urllib.error.URLError, ConnectionRefusedError, TimeoutError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def generate_mask(
|
def generate_mask(
|
||||||
self,
|
self,
|
||||||
video_path: str,
|
video_path: str,
|
||||||
|
|
@ -200,13 +196,13 @@ class InferenceClient:
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Request mask generation.
|
Request mask generation.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
task_id (str)
|
task_id (str)
|
||||||
"""
|
"""
|
||||||
if not self.is_server_running():
|
if not self.is_server_running():
|
||||||
self.start_server()
|
self.start_server()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"video_path": video_path,
|
"video_path": video_path,
|
||||||
"output_dir": output_dir,
|
"output_dir": output_dir,
|
||||||
|
|
@ -216,36 +212,35 @@ class InferenceClient:
|
||||||
"iou_threshold": iou_threshold,
|
"iou_threshold": iou_threshold,
|
||||||
"mask_scale": mask_scale,
|
"mask_scale": mask_scale,
|
||||||
}
|
}
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{self.SERVER_URL}/generate",
|
f"{self.SERVER_URL}/generate",
|
||||||
data=json.dumps(data).encode("utf-8"),
|
data=json.dumps(data).encode('utf-8'),
|
||||||
headers={"Content-Type": "application/json"},
|
headers={'Content-Type': 'application/json'},
|
||||||
method="POST",
|
method='POST'
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req) as response:
|
with urllib.request.urlopen(req) as response:
|
||||||
result = json.loads(response.read().decode("utf-8"))
|
result = json.loads(response.read().decode('utf-8'))
|
||||||
return result["id"]
|
return result['id']
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
raise RuntimeError(f"Server error: {e.read().decode('utf-8')}")
|
raise RuntimeError(f"Server error: {e.read().decode('utf-8')}")
|
||||||
|
|
||||||
def get_task_status(self, task_id: str) -> Dict[str, Any]:
|
def get_task_status(self, task_id: str) -> Dict[str, Any]:
|
||||||
"""Get status of a task."""
|
"""Get status of a task."""
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(
|
with urllib.request.urlopen(f"{self.SERVER_URL}/tasks/{task_id}") as response:
|
||||||
f"{self.SERVER_URL}/tasks/{task_id}"
|
return json.loads(response.read().decode('utf-8'))
|
||||||
) as response:
|
|
||||||
return json.loads(response.read().decode("utf-8"))
|
|
||||||
except urllib.error.HTTPError:
|
except urllib.error.HTTPError:
|
||||||
return {"status": "unknown"}
|
return {"status": "unknown"}
|
||||||
|
|
||||||
def cancel_task(self, task_id: str):
|
def cancel_task(self, task_id: str):
|
||||||
"""Cancel a task."""
|
"""Cancel a task."""
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{self.SERVER_URL}/tasks/{task_id}/cancel", method="POST"
|
f"{self.SERVER_URL}/tasks/{task_id}/cancel",
|
||||||
|
method='POST'
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req):
|
with urllib.request.urlopen(req):
|
||||||
pass
|
pass
|
||||||
|
|
@ -256,7 +251,6 @@ class InferenceClient:
|
||||||
# Singleton
|
# Singleton
|
||||||
_client: Optional[InferenceClient] = None
|
_client: Optional[InferenceClient] = None
|
||||||
|
|
||||||
|
|
||||||
def get_client() -> InferenceClient:
|
def get_client() -> InferenceClient:
|
||||||
global _client
|
global _client
|
||||||
if _client is None:
|
if _client is None:
|
||||||
|
|
|
||||||
111
core/utils.py
111
core/utils.py
|
|
@ -1,111 +0,0 @@
|
||||||
"""
|
|
||||||
Utility functions for Face Mask extension.
|
|
||||||
|
|
||||||
Provides helper functions for server status, cache info, etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import json
|
|
||||||
import tempfile
|
|
||||||
from typing import Dict, Tuple, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def get_server_status() -> Dict:
|
|
||||||
"""
|
|
||||||
Get server status and GPU information.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: {
|
|
||||||
'running': bool,
|
|
||||||
'gpu_available': bool,
|
|
||||||
'gpu_device': str or None,
|
|
||||||
'gpu_count': int,
|
|
||||||
'rocm_version': str or None,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
result = {
|
|
||||||
'running': False,
|
|
||||||
'gpu_available': False,
|
|
||||||
'gpu_device': None,
|
|
||||||
'gpu_count': 0,
|
|
||||||
'rocm_version': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen("http://127.0.0.1:8181/status", timeout=1) as response:
|
|
||||||
data = json.loads(response.read().decode('utf-8'))
|
|
||||||
result['running'] = data.get('status') == 'running'
|
|
||||||
result['gpu_available'] = data.get('gpu_available', False)
|
|
||||||
result['gpu_device'] = data.get('gpu_device')
|
|
||||||
result['gpu_count'] = data.get('gpu_count', 0)
|
|
||||||
result['rocm_version'] = data.get('rocm_version')
|
|
||||||
except (urllib.error.URLError, ConnectionRefusedError, TimeoutError):
|
|
||||||
result['running'] = False
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_cache_info(strip_name: Optional[str] = None) -> Tuple[str, int, int]:
|
|
||||||
"""
|
|
||||||
Get cache directory information.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
strip_name: If provided, get info for specific strip. Otherwise, get info for all cache.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (cache_path, total_size_bytes, file_count)
|
|
||||||
"""
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
blend_file = bpy.data.filepath
|
|
||||||
|
|
||||||
if strip_name:
|
|
||||||
# Get cache for specific strip
|
|
||||||
if blend_file:
|
|
||||||
project_dir = os.path.dirname(blend_file)
|
|
||||||
cache_path = os.path.join(project_dir, ".mask_cache", strip_name)
|
|
||||||
else:
|
|
||||||
cache_path = os.path.join(tempfile.gettempdir(), "blender_mask_cache", strip_name)
|
|
||||||
else:
|
|
||||||
# Get cache root
|
|
||||||
if blend_file:
|
|
||||||
project_dir = os.path.dirname(blend_file)
|
|
||||||
cache_path = os.path.join(project_dir, ".mask_cache")
|
|
||||||
else:
|
|
||||||
cache_path = os.path.join(tempfile.gettempdir(), "blender_mask_cache")
|
|
||||||
|
|
||||||
# Calculate size and count
|
|
||||||
total_size = 0
|
|
||||||
file_count = 0
|
|
||||||
|
|
||||||
if os.path.exists(cache_path):
|
|
||||||
for root, dirs, files in os.walk(cache_path):
|
|
||||||
for file in files:
|
|
||||||
if file.endswith('.png'): # Only count mask images
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
try:
|
|
||||||
total_size += os.path.getsize(file_path)
|
|
||||||
file_count += 1
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return cache_path, total_size, file_count
|
|
||||||
|
|
||||||
|
|
||||||
def format_size(size_bytes: int) -> str:
|
|
||||||
"""
|
|
||||||
Format bytes to human-readable size.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
size_bytes: Size in bytes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Formatted string (e.g., "1.5 MB")
|
|
||||||
"""
|
|
||||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
||||||
if size_bytes < 1024.0:
|
|
||||||
return f"{size_bytes:.1f} {unit}"
|
|
||||||
size_bytes /= 1024.0
|
|
||||||
return f"{size_bytes:.1f} TB"
|
|
||||||
|
|
@ -48,8 +48,8 @@
|
||||||
export ROCM_PATH="${pkgs.rocmPackages.clr}"
|
export ROCM_PATH="${pkgs.rocmPackages.clr}"
|
||||||
export HSA_OVERRIDE_GFX_VERSION="11.0.0" # RX 7900 (RDNA 3 / gfx1100)
|
export HSA_OVERRIDE_GFX_VERSION="11.0.0" # RX 7900 (RDNA 3 / gfx1100)
|
||||||
|
|
||||||
# LD_LIBRARY_PATH: ROCm libraries FIRST (critical for GPU inference)
|
# LD_LIBRARY_PATH: ROCm、C++標準ライブラリ、その他必要なライブラリ
|
||||||
export LD_LIBRARY_PATH="${pkgs.rocmPackages.clr}/lib:${pkgs.rocmPackages.rocm-runtime}/lib:${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:${pkgs.zstd.out}/lib:$LD_LIBRARY_PATH"
|
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:${pkgs.zstd.out}/lib:${pkgs.rocmPackages.clr}/lib:${pkgs.rocmPackages.rocm-runtime}/lib:$LD_LIBRARY_PATH"
|
||||||
|
|
||||||
# venvのセットアップ
|
# venvのセットアップ
|
||||||
VENV_DIR="$PWD/.venv"
|
VENV_DIR="$PWD/.venv"
|
||||||
|
|
@ -89,11 +89,12 @@
|
||||||
export BLENDER_USER_ADDONS="$BLENDER_USER_SCRIPTS/addons"
|
export BLENDER_USER_ADDONS="$BLENDER_USER_SCRIPTS/addons"
|
||||||
|
|
||||||
# 環境変数をファイルに保存(サーバープロセス用)
|
# 環境変数をファイルに保存(サーバープロセス用)
|
||||||
# CRITICAL: ROCm library paths MUST come first for GPU inference
|
|
||||||
cat > "$PWD/.env" << EOF
|
cat > "$PWD/.env" << EOF
|
||||||
LD_LIBRARY_PATH=${pkgs.rocmPackages.clr}/lib:${pkgs.rocmPackages.rocm-runtime}/lib:${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:${pkgs.zstd.out}/lib
|
LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:${pkgs.zstd.out}/lib:${pkgs.rocmPackages.clr}/lib:${pkgs.rocmPackages.rocm-runtime}/lib
|
||||||
ROCM_PATH=${pkgs.rocmPackages.clr}
|
ROCM_PATH=${pkgs.rocmPackages.clr}
|
||||||
HSA_OVERRIDE_GFX_VERSION=11.0.0
|
HSA_OVERRIDE_GFX_VERSION=11.0.0
|
||||||
|
PYTORCH_ROCM_ARCH=gfx1100
|
||||||
|
ROCBLAS_TENSILE_LIBPATH=${pkgs.rocmPackages.clr}/lib/rocblas/library
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "[Setup] Environment ready with GPU support"
|
echo "[Setup] Environment ready with GPU support"
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,13 @@
|
||||||
|
|
||||||
from . import generate_mask
|
from . import generate_mask
|
||||||
from . import apply_blur
|
from . import apply_blur
|
||||||
from . import clear_cache
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
generate_mask.register()
|
generate_mask.register()
|
||||||
apply_blur.register()
|
apply_blur.register()
|
||||||
clear_cache.register()
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
clear_cache.unregister()
|
|
||||||
apply_blur.unregister()
|
apply_blur.unregister()
|
||||||
generate_mask.unregister()
|
generate_mask.unregister()
|
||||||
|
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
"""
|
|
||||||
Clear Cache Operator.
|
|
||||||
|
|
||||||
Provides operators to clear mask cache directories.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import bpy
|
|
||||||
from bpy.types import Operator
|
|
||||||
from bpy.props import BoolProperty
|
|
||||||
|
|
||||||
|
|
||||||
class SEQUENCER_OT_clear_mask_cache(Operator):
|
|
||||||
"""Clear mask cache directories."""
|
|
||||||
|
|
||||||
bl_idname = "sequencer.clear_mask_cache"
|
|
||||||
bl_label = "Clear Mask Cache"
|
|
||||||
bl_description = "Delete cached mask images"
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
all_strips: BoolProperty(
|
|
||||||
name="All Strips",
|
|
||||||
description="Clear cache for all strips (otherwise only current strip)",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
blend_file = bpy.data.filepath
|
|
||||||
total_size = 0
|
|
||||||
cleared_count = 0
|
|
||||||
|
|
||||||
if self.all_strips:
|
|
||||||
# Clear all cache directories
|
|
||||||
if blend_file:
|
|
||||||
# Project cache
|
|
||||||
project_dir = os.path.dirname(blend_file)
|
|
||||||
cache_root = os.path.join(project_dir, ".mask_cache")
|
|
||||||
else:
|
|
||||||
# Temp cache
|
|
||||||
cache_root = os.path.join(tempfile.gettempdir(), "blender_mask_cache")
|
|
||||||
|
|
||||||
if os.path.exists(cache_root):
|
|
||||||
# Calculate size before deletion
|
|
||||||
for root, dirs, files in os.walk(cache_root):
|
|
||||||
for file in files:
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
try:
|
|
||||||
total_size += os.path.getsize(file_path)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Delete cache directory
|
|
||||||
try:
|
|
||||||
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)})")
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, f"Failed to clear cache: {e}")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
else:
|
|
||||||
self.report({'INFO'}, "No cache to clear")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Clear cache for active strip only
|
|
||||||
seq_editor = context.scene.sequence_editor
|
|
||||||
if not seq_editor or not seq_editor.active_strip:
|
|
||||||
self.report({'WARNING'}, "No strip selected")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
strip = seq_editor.active_strip
|
|
||||||
if blend_file:
|
|
||||||
project_dir = os.path.dirname(blend_file)
|
|
||||||
cache_dir = os.path.join(project_dir, ".mask_cache", strip.name)
|
|
||||||
else:
|
|
||||||
cache_dir = os.path.join(tempfile.gettempdir(), "blender_mask_cache", strip.name)
|
|
||||||
|
|
||||||
if os.path.exists(cache_dir):
|
|
||||||
# Calculate size
|
|
||||||
for root, dirs, files in os.walk(cache_dir):
|
|
||||||
for file in files:
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
try:
|
|
||||||
total_size += os.path.getsize(file_path)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Delete
|
|
||||||
try:
|
|
||||||
shutil.rmtree(cache_dir)
|
|
||||||
self.report({'INFO'}, f"Cleared cache for {strip.name} ({self._format_size(total_size)})")
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, f"Failed to clear cache: {e}")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
else:
|
|
||||||
self.report({'INFO'}, f"No cache for {strip.name}")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def _format_size(self, size_bytes):
|
|
||||||
"""Format bytes to human-readable size."""
|
|
||||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
||||||
if size_bytes < 1024.0:
|
|
||||||
return f"{size_bytes:.1f} {unit}"
|
|
||||||
size_bytes /= 1024.0
|
|
||||||
return f"{size_bytes:.1f} TB"
|
|
||||||
|
|
||||||
|
|
||||||
# Registration
|
|
||||||
classes = [
|
|
||||||
SEQUENCER_OT_clear_mask_cache,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
for cls in classes:
|
|
||||||
bpy.utils.register_class(cls)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
for cls in reversed(classes):
|
|
||||||
bpy.utils.unregister_class(cls)
|
|
||||||
|
|
@ -7,7 +7,7 @@ from video strips in the Video Sequence Editor.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import IntProperty
|
from bpy.props import FloatProperty, IntProperty
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
|
|
||||||
from ..core.async_generator import get_generator
|
from ..core.async_generator import get_generator
|
||||||
|
|
@ -15,12 +15,37 @@ from ..core.async_generator import get_generator
|
||||||
|
|
||||||
class SEQUENCER_OT_generate_face_mask(Operator):
|
class SEQUENCER_OT_generate_face_mask(Operator):
|
||||||
"""Generate face mask image sequence from video strip."""
|
"""Generate face mask image sequence from video strip."""
|
||||||
|
|
||||||
bl_idname = "sequencer.generate_face_mask"
|
bl_idname = "sequencer.generate_face_mask"
|
||||||
bl_label = "Generate Face Mask"
|
bl_label = "Generate Face Mask"
|
||||||
bl_description = "Detect faces and generate mask image sequence"
|
bl_description = "Detect faces and generate mask image sequence"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
# YOLO Detection parameters
|
||||||
|
conf_threshold: FloatProperty(
|
||||||
|
name="Confidence",
|
||||||
|
description="YOLO confidence threshold (higher = fewer false positives)",
|
||||||
|
default=0.25,
|
||||||
|
min=0.1,
|
||||||
|
max=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
iou_threshold: FloatProperty(
|
||||||
|
name="IOU Threshold",
|
||||||
|
description="Non-maximum suppression IOU threshold",
|
||||||
|
default=0.45,
|
||||||
|
min=0.1,
|
||||||
|
max=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
"""Check if operator can run."""
|
"""Check if operator can run."""
|
||||||
|
|
@ -101,11 +126,6 @@ class SEQUENCER_OT_generate_face_mask(Operator):
|
||||||
wm.mask_progress = 0
|
wm.mask_progress = 0
|
||||||
wm.mask_total = end_frame - start_frame + 1
|
wm.mask_total = end_frame - start_frame + 1
|
||||||
|
|
||||||
# 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
|
# Start generation
|
||||||
generator.start(
|
generator.start(
|
||||||
video_path=video_path,
|
video_path=video_path,
|
||||||
|
|
@ -113,9 +133,9 @@ class SEQUENCER_OT_generate_face_mask(Operator):
|
||||||
start_frame=0, # Frame indices in video
|
start_frame=0, # Frame indices in video
|
||||||
end_frame=end_frame - start_frame,
|
end_frame=end_frame - start_frame,
|
||||||
fps=fps,
|
fps=fps,
|
||||||
conf_threshold=conf_threshold,
|
conf_threshold=self.conf_threshold,
|
||||||
iou_threshold=iou_threshold,
|
iou_threshold=self.iou_threshold,
|
||||||
mask_scale=mask_scale,
|
mask_scale=self.mask_scale,
|
||||||
on_complete=on_complete,
|
on_complete=on_complete,
|
||||||
on_progress=on_progress,
|
on_progress=on_progress,
|
||||||
)
|
)
|
||||||
|
|
@ -143,109 +163,54 @@ class SEQUENCER_OT_generate_face_mask(Operator):
|
||||||
|
|
||||||
def _check_cache(self, cache_dir: str, expected_frames: int) -> bool:
|
def _check_cache(self, cache_dir: str, expected_frames: int) -> bool:
|
||||||
"""Check if cached masks exist and are complete.
|
"""Check if cached masks exist and are complete.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cache_dir: Path to cache directory
|
cache_dir: Path to cache directory
|
||||||
expected_frames: Number of frames expected
|
expected_frames: Number of frames expected
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if cache exists and is valid
|
True if cache exists and has at least 90% of expected frames
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(cache_dir):
|
if not os.path.exists(cache_dir):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check for MP4 video (new format)
|
mask_files = [f for f in os.listdir(cache_dir)
|
||||||
mask_video = os.path.join(cache_dir, "mask.mp4")
|
|
||||||
if os.path.exists(mask_video):
|
|
||||||
# Verify video has expected number of frames
|
|
||||||
import cv2
|
|
||||||
cap = cv2.VideoCapture(mask_video)
|
|
||||||
if cap.isOpened():
|
|
||||||
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
||||||
cap.release()
|
|
||||||
# Accept cache if at least 90% of frames exist
|
|
||||||
return frame_count >= expected_frames * 0.9
|
|
||||||
cap.release()
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Fallback: check for PNG sequence (backward compatibility)
|
|
||||||
mask_files = [f for f in os.listdir(cache_dir)
|
|
||||||
if f.startswith("mask_") and f.endswith(".png")]
|
if f.startswith("mask_") and f.endswith(".png")]
|
||||||
|
|
||||||
# Accept cache if at least 90% of frames exist
|
# Accept cache if at least 90% of frames exist
|
||||||
|
# (some frames may have been skipped due to read errors)
|
||||||
return len(mask_files) >= expected_frames * 0.9
|
return len(mask_files) >= expected_frames * 0.9
|
||||||
|
|
||||||
def _add_mask_strip(self, context, source_strip_name: str, mask_path: str):
|
def _add_mask_strip(self, context, source_strip_name: str, mask_dir: str):
|
||||||
"""Add mask video as a new strip.
|
"""Add mask image sequence as a new strip."""
|
||||||
|
|
||||||
Args:
|
|
||||||
context: Blender context
|
|
||||||
source_strip_name: Name of the source video strip
|
|
||||||
mask_path: Path to mask video file or directory (for backward compatibility)
|
|
||||||
"""
|
|
||||||
scene = context.scene
|
scene = context.scene
|
||||||
seq_editor = scene.sequence_editor
|
seq_editor = scene.sequence_editor
|
||||||
|
|
||||||
if not seq_editor:
|
if not seq_editor:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Find source strip (Blender 5.0 uses 'strips' instead of 'sequences')
|
# Find source strip (Blender 5.0 uses 'strips' instead of 'sequences')
|
||||||
source_strip = seq_editor.strips.get(source_strip_name)
|
source_strip = seq_editor.strips.get(source_strip_name)
|
||||||
if not source_strip:
|
if not source_strip:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if mask_path is a video file or directory (backward compatibility)
|
# Get first mask image
|
||||||
if os.path.isfile(mask_path):
|
mask_files = sorted([
|
||||||
# New format: single MP4 file
|
f for f in os.listdir(mask_dir)
|
||||||
mask_video = mask_path
|
if f.startswith("mask_") and f.endswith(".png")
|
||||||
else:
|
])
|
||||||
# Old format: directory with PNG sequence (backward compatibility)
|
|
||||||
mask_video = os.path.join(mask_path, "mask.mp4")
|
if not mask_files:
|
||||||
if not os.path.exists(mask_video):
|
|
||||||
# Fallback to PNG sequence
|
|
||||||
mask_files = sorted([
|
|
||||||
f for f in os.listdir(mask_path)
|
|
||||||
if f.startswith("mask_") and f.endswith(".png")
|
|
||||||
])
|
|
||||||
if not mask_files:
|
|
||||||
return
|
|
||||||
first_mask = os.path.join(mask_path, mask_files[0])
|
|
||||||
self._add_mask_strip_png_sequence(context, source_strip_name, mask_path, mask_files, first_mask)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find an empty channel
|
|
||||||
used_channels = {s.channel for s in seq_editor.strips}
|
|
||||||
new_channel = source_strip.channel + 1
|
|
||||||
while new_channel in used_channels:
|
|
||||||
new_channel += 1
|
|
||||||
|
|
||||||
# Add movie strip (Blender 5.0 API)
|
|
||||||
mask_strip = seq_editor.strips.new_movie(
|
|
||||||
name=f"{source_strip_name}_mask",
|
|
||||||
filepath=mask_video,
|
|
||||||
channel=new_channel,
|
|
||||||
frame_start=source_strip.frame_final_start,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set blend mode for mask
|
|
||||||
mask_strip.blend_type = 'ALPHA_OVER'
|
|
||||||
mask_strip.blend_alpha = 0.5
|
|
||||||
|
|
||||||
def _add_mask_strip_png_sequence(self, context, source_strip_name, mask_dir, mask_files, first_mask):
|
|
||||||
"""Backward compatibility: Add PNG sequence as mask strip."""
|
|
||||||
scene = context.scene
|
|
||||||
seq_editor = scene.sequence_editor
|
|
||||||
source_strip = seq_editor.strips.get(source_strip_name)
|
|
||||||
|
|
||||||
if not source_strip:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
first_mask = os.path.join(mask_dir, mask_files[0])
|
||||||
|
|
||||||
# Find an empty channel
|
# Find an empty channel
|
||||||
used_channels = {s.channel for s in seq_editor.strips}
|
used_channels = {s.channel for s in seq_editor.strips}
|
||||||
new_channel = source_strip.channel + 1
|
new_channel = source_strip.channel + 1
|
||||||
while new_channel in used_channels:
|
while new_channel in used_channels:
|
||||||
new_channel += 1
|
new_channel += 1
|
||||||
|
|
||||||
# Add image sequence (Blender 5.0 API)
|
# Add image sequence (Blender 5.0 API)
|
||||||
mask_strip = seq_editor.strips.new_image(
|
mask_strip = seq_editor.strips.new_image(
|
||||||
name=f"{source_strip_name}_mask",
|
name=f"{source_strip_name}_mask",
|
||||||
|
|
@ -253,11 +218,11 @@ class SEQUENCER_OT_generate_face_mask(Operator):
|
||||||
channel=new_channel,
|
channel=new_channel,
|
||||||
frame_start=source_strip.frame_final_start,
|
frame_start=source_strip.frame_final_start,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add remaining frames
|
# Add remaining frames
|
||||||
for mask_file in mask_files[1:]:
|
for mask_file in mask_files[1:]:
|
||||||
mask_strip.elements.append(mask_file)
|
mask_strip.elements.append(mask_file)
|
||||||
|
|
||||||
# Set blend mode for mask
|
# Set blend mode for mask
|
||||||
mask_strip.blend_type = 'ALPHA_OVER'
|
mask_strip.blend_type = 'ALPHA_OVER'
|
||||||
mask_strip.blend_alpha = 0.5
|
mask_strip.blend_alpha = 0.5
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import bpy
|
||||||
from bpy.types import Panel
|
from bpy.types import Panel
|
||||||
|
|
||||||
from ..core.async_generator import get_generator
|
from ..core.async_generator import get_generator
|
||||||
from ..core.utils import get_server_status, get_cache_info, format_size
|
|
||||||
|
|
||||||
|
|
||||||
class SEQUENCER_PT_face_mask(Panel):
|
class SEQUENCER_PT_face_mask(Panel):
|
||||||
|
|
@ -23,29 +22,21 @@ class SEQUENCER_PT_face_mask(Panel):
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
scene = context.scene
|
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
seq_editor = context.scene.sequence_editor
|
seq_editor = context.scene.sequence_editor
|
||||||
# Note: Blender 5.0 uses 'strips' instead of 'sequences'
|
# Note: Blender 5.0 uses 'strips' instead of 'sequences'
|
||||||
|
|
||||||
generator = get_generator()
|
generator = get_generator()
|
||||||
|
|
||||||
# Always show parameters and status
|
|
||||||
self._draw_parameters(layout, scene)
|
|
||||||
self._draw_server_status(layout)
|
|
||||||
self._draw_cache_info(layout, seq_editor)
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
# Show progress if generating
|
# Show progress if generating
|
||||||
if generator.is_running:
|
if generator.is_running:
|
||||||
self._draw_progress(layout, wm, generator)
|
self._draw_progress(layout, wm, generator)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show controls if strip selected
|
# Show controls if strip selected
|
||||||
if seq_editor and seq_editor.active_strip:
|
if seq_editor and seq_editor.active_strip:
|
||||||
strip = seq_editor.active_strip
|
strip = seq_editor.active_strip
|
||||||
|
|
||||||
if strip.type in {'MOVIE', 'IMAGE'}:
|
if strip.type in {'MOVIE', 'IMAGE'}:
|
||||||
self._draw_generation_controls(layout, context, strip)
|
self._draw_generation_controls(layout, context, strip)
|
||||||
self._draw_blur_controls(layout, context, strip)
|
self._draw_blur_controls(layout, context, strip)
|
||||||
|
|
@ -54,96 +45,18 @@ class SEQUENCER_PT_face_mask(Panel):
|
||||||
else:
|
else:
|
||||||
layout.label(text="No strip selected")
|
layout.label(text="No strip selected")
|
||||||
|
|
||||||
def _draw_parameters(self, layout, scene):
|
|
||||||
"""Draw detection parameters."""
|
|
||||||
box = layout.box()
|
|
||||||
box.label(text="Parameters", icon='PREFERENCES')
|
|
||||||
|
|
||||||
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."""
|
|
||||||
box = layout.box()
|
|
||||||
box.label(text="Server Status", icon='SYSTEM')
|
|
||||||
|
|
||||||
status = get_server_status()
|
|
||||||
|
|
||||||
# Server status
|
|
||||||
row = box.row()
|
|
||||||
if status['running']:
|
|
||||||
row.label(text="Server:", icon='CHECKMARK')
|
|
||||||
row.label(text="Running")
|
|
||||||
else:
|
|
||||||
row.label(text="Server:", icon='ERROR')
|
|
||||||
row.label(text="Stopped")
|
|
||||||
|
|
||||||
# GPU status
|
|
||||||
if status['running']:
|
|
||||||
row = box.row()
|
|
||||||
if status['gpu_available']:
|
|
||||||
row.label(text="GPU:", icon='CHECKMARK')
|
|
||||||
gpu_name = status['gpu_device'] or "Available"
|
|
||||||
# Truncate long GPU names
|
|
||||||
if len(gpu_name) > 25:
|
|
||||||
gpu_name = gpu_name[:22] + "..."
|
|
||||||
row.label(text=gpu_name)
|
|
||||||
else:
|
|
||||||
row.label(text="GPU:", icon='ERROR')
|
|
||||||
row.label(text="Not Available")
|
|
||||||
|
|
||||||
def _draw_cache_info(self, layout, seq_editor):
|
|
||||||
"""Draw cache information and clear button."""
|
|
||||||
box = layout.box()
|
|
||||||
box.label(text="Cache", icon='FILE_CACHE')
|
|
||||||
|
|
||||||
# Get cache info
|
|
||||||
if seq_editor and seq_editor.active_strip:
|
|
||||||
strip_name = seq_editor.active_strip.name
|
|
||||||
cache_path, total_size, file_count = get_cache_info(strip_name)
|
|
||||||
else:
|
|
||||||
cache_path, total_size, file_count = get_cache_info()
|
|
||||||
|
|
||||||
# Cache info
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Size:")
|
|
||||||
row.label(text=format_size(total_size))
|
|
||||||
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Files:")
|
|
||||||
row.label(text=str(file_count))
|
|
||||||
|
|
||||||
# Clear cache buttons
|
|
||||||
row = box.row(align=True)
|
|
||||||
if seq_editor and seq_editor.active_strip:
|
|
||||||
op = row.operator(
|
|
||||||
"sequencer.clear_mask_cache",
|
|
||||||
text="Clear Strip Cache",
|
|
||||||
icon='TRASH',
|
|
||||||
)
|
|
||||||
op.all_strips = False
|
|
||||||
|
|
||||||
op = row.operator(
|
|
||||||
"sequencer.clear_mask_cache",
|
|
||||||
text="Clear All",
|
|
||||||
icon='TRASH',
|
|
||||||
)
|
|
||||||
op.all_strips = True
|
|
||||||
|
|
||||||
def _draw_progress(self, layout, wm, generator):
|
def _draw_progress(self, layout, wm, generator):
|
||||||
"""Draw progress bar during generation."""
|
"""Draw progress bar during generation."""
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text="Generating Masks...", icon='RENDER_ANIMATION')
|
box.label(text="Generating Masks...", icon='RENDER_ANIMATION')
|
||||||
|
|
||||||
# Progress bar
|
# Progress bar
|
||||||
progress = wm.mask_progress / max(wm.mask_total, 1)
|
progress = wm.mask_progress / max(wm.mask_total, 1)
|
||||||
box.progress(
|
box.progress(
|
||||||
factor=progress,
|
factor=progress,
|
||||||
text=f"Frame {wm.mask_progress} / {wm.mask_total}",
|
text=f"Frame {wm.mask_progress} / {wm.mask_total}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cancel button
|
# Cancel button
|
||||||
box.operator(
|
box.operator(
|
||||||
"sequencer.cancel_mask_generation",
|
"sequencer.cancel_mask_generation",
|
||||||
|
|
|
||||||
|
|
@ -147,69 +147,6 @@ class YOLOFaceDetector:
|
||||||
|
|
||||||
return detections
|
return detections
|
||||||
|
|
||||||
def detect_batch(self, frames: List[np.ndarray]) -> List[List[Tuple[int, int, int, int, float]]]:
|
|
||||||
"""
|
|
||||||
Detect faces in multiple frames at once (batch processing).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frames: List of BGR images as numpy arrays (H, W, C)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of detection lists, one per frame.
|
|
||||||
Each detection: (x, y, width, height, confidence)
|
|
||||||
"""
|
|
||||||
if not frames:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Run batch inference
|
|
||||||
try:
|
|
||||||
results = self.model.predict(
|
|
||||||
frames,
|
|
||||||
conf=self.conf_threshold,
|
|
||||||
iou=self.iou_threshold,
|
|
||||||
imgsz=self.input_size[0],
|
|
||||||
verbose=False,
|
|
||||||
device=self._device,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[FaceMask] ERROR during batch inference: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
# Fallback to CPU
|
|
||||||
print("[FaceMask] Falling back to CPU inference...")
|
|
||||||
self._device = 'cpu'
|
|
||||||
results = self.model.predict(
|
|
||||||
frames,
|
|
||||||
conf=self.conf_threshold,
|
|
||||||
iou=self.iou_threshold,
|
|
||||||
imgsz=self.input_size[0],
|
|
||||||
verbose=False,
|
|
||||||
device='cpu',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract detections for each frame
|
|
||||||
all_detections = []
|
|
||||||
for result in results:
|
|
||||||
detections = []
|
|
||||||
if result.boxes is not None:
|
|
||||||
boxes = result.boxes
|
|
||||||
for box in boxes:
|
|
||||||
# Get coordinates in xyxy format
|
|
||||||
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
|
||||||
conf = float(box.conf[0].cpu().numpy())
|
|
||||||
|
|
||||||
# Convert to x, y, width, height
|
|
||||||
x = int(x1)
|
|
||||||
y = int(y1)
|
|
||||||
w = int(x2 - x1)
|
|
||||||
h = int(y2 - y1)
|
|
||||||
|
|
||||||
detections.append((x, y, w, h, conf))
|
|
||||||
|
|
||||||
all_detections.append(detections)
|
|
||||||
|
|
||||||
return all_detections
|
|
||||||
|
|
||||||
def generate_mask(
|
def generate_mask(
|
||||||
self,
|
self,
|
||||||
frame_shape: Tuple[int, int, int],
|
frame_shape: Tuple[int, int, int],
|
||||||
|
|
|
||||||
177
server/main.py
177
server/main.py
|
|
@ -8,29 +8,6 @@ GPU-accelerated face detection using ONNX Runtime.
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
# CRITICAL: Fix LD_LIBRARY_PATH before importing cv2 or torch
|
|
||||||
# cv2 adds its own lib path to the front, which can override ROCm libraries
|
|
||||||
def fix_library_path():
|
|
||||||
"""Ensure ROCm libraries are loaded before cv2's bundled libraries."""
|
|
||||||
ld_path = os.environ.get('LD_LIBRARY_PATH', '')
|
|
||||||
|
|
||||||
# Split and filter paths
|
|
||||||
paths = [p for p in ld_path.split(':') if p]
|
|
||||||
|
|
||||||
# Separate ROCm/GPU paths from other paths
|
|
||||||
rocm_paths = [p for p in paths if 'rocm' in p.lower() or 'clr-' in p or 'hip' in p.lower()]
|
|
||||||
other_paths = [p for p in paths if p not in rocm_paths]
|
|
||||||
|
|
||||||
# Rebuild with ROCm paths first
|
|
||||||
if rocm_paths:
|
|
||||||
new_ld_path = ':'.join(rocm_paths + other_paths)
|
|
||||||
os.environ['LD_LIBRARY_PATH'] = new_ld_path
|
|
||||||
print(f"[FaceMask] Fixed LD_LIBRARY_PATH to prioritize ROCm libraries")
|
|
||||||
|
|
||||||
# Fix library path BEFORE any other imports
|
|
||||||
fix_library_path()
|
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
import queue
|
import queue
|
||||||
|
|
@ -84,15 +61,11 @@ class GenerateRequest(BaseModel):
|
||||||
mask_scale: float = 1.5
|
mask_scale: float = 1.5
|
||||||
|
|
||||||
def process_video_task(task_id: str, req: GenerateRequest):
|
def process_video_task(task_id: str, req: GenerateRequest):
|
||||||
"""Background task to process video with async MP4 output."""
|
"""Background task to process video."""
|
||||||
writer = None
|
|
||||||
write_queue = None
|
|
||||||
writer_thread = 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)
|
||||||
|
|
||||||
# Verify video exists
|
# Verify video exists
|
||||||
if not os.path.exists(req.video_path):
|
if not os.path.exists(req.video_path):
|
||||||
tasks[task_id].status = TaskStatus.FAILED
|
tasks[task_id].status = TaskStatus.FAILED
|
||||||
|
|
@ -105,132 +78,67 @@ def process_video_task(task_id: str, req: GenerateRequest):
|
||||||
conf_threshold=req.conf_threshold,
|
conf_threshold=req.conf_threshold,
|
||||||
iou_threshold=req.iou_threshold
|
iou_threshold=req.iou_threshold
|
||||||
)
|
)
|
||||||
|
# Ensure model is loaded
|
||||||
_ = detector.model
|
_ = detector.model
|
||||||
|
|
||||||
# Open video
|
# Open video
|
||||||
cap = cv2.VideoCapture(req.video_path)
|
cap = cv2.VideoCapture(req.video_path)
|
||||||
if not cap.isOpened():
|
if not cap.isOpened():
|
||||||
tasks[task_id].status = TaskStatus.FAILED
|
tasks[task_id].status = TaskStatus.FAILED
|
||||||
tasks[task_id].message = "Failed to open video"
|
tasks[task_id].message = "Failed to open video"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get video properties
|
# Determine frame range
|
||||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
total_video_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
total_video_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
end_frame = min(req.end_frame, total_video_frames - 1)
|
end_frame = min(req.end_frame, total_video_frames - 1)
|
||||||
frames_to_process = end_frame - req.start_frame + 1
|
frames_to_process = end_frame - req.start_frame + 1
|
||||||
|
|
||||||
tasks[task_id].total = frames_to_process
|
tasks[task_id].total = frames_to_process
|
||||||
|
|
||||||
# Ensure output directory exists
|
# Ensure output directory exists
|
||||||
os.makedirs(req.output_dir, exist_ok=True)
|
os.makedirs(req.output_dir, exist_ok=True)
|
||||||
|
|
||||||
# Setup MP4 writer (grayscale)
|
print(f"Starting processing: {req.video_path} ({frames_to_process} frames)")
|
||||||
output_video_path = os.path.join(req.output_dir, "mask.mp4")
|
|
||||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
# Process loop
|
||||||
writer = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height), isColor=False)
|
|
||||||
|
|
||||||
if not writer.isOpened():
|
|
||||||
tasks[task_id].status = TaskStatus.FAILED
|
|
||||||
tasks[task_id].message = "Failed to create video writer"
|
|
||||||
cap.release()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Async writer setup
|
|
||||||
write_queue = queue.Queue(maxsize=30) # Buffer up to 30 frames
|
|
||||||
writer_running = threading.Event()
|
|
||||||
writer_running.set()
|
|
||||||
|
|
||||||
def async_writer():
|
|
||||||
"""Background thread for writing frames to video."""
|
|
||||||
while writer_running.is_set() or not write_queue.empty():
|
|
||||||
try:
|
|
||||||
mask = write_queue.get(timeout=0.1)
|
|
||||||
if mask is not None:
|
|
||||||
writer.write(mask)
|
|
||||||
write_queue.task_done()
|
|
||||||
except queue.Empty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
writer_thread = threading.Thread(target=async_writer, daemon=True)
|
|
||||||
writer_thread.start()
|
|
||||||
|
|
||||||
print(f"Starting processing: {req.video_path} ({frames_to_process} frames) -> {output_video_path}")
|
|
||||||
|
|
||||||
# Batch processing configuration
|
|
||||||
BATCH_SIZE = 5 # Optimal batch size for 4K video (72.9% improvement)
|
|
||||||
frame_buffer = []
|
|
||||||
|
|
||||||
def process_batch():
|
|
||||||
"""Process accumulated batch of frames."""
|
|
||||||
if not frame_buffer:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Batch inference at full resolution
|
|
||||||
batch_detections = detector.detect_batch(frame_buffer)
|
|
||||||
|
|
||||||
# Generate masks for each frame
|
|
||||||
for i, detections in enumerate(batch_detections):
|
|
||||||
frame = frame_buffer[i]
|
|
||||||
|
|
||||||
# Generate mask at original resolution
|
|
||||||
mask = detector.generate_mask(
|
|
||||||
frame.shape,
|
|
||||||
detections,
|
|
||||||
mask_scale=req.mask_scale
|
|
||||||
)
|
|
||||||
|
|
||||||
# Async write to queue
|
|
||||||
write_queue.put(mask)
|
|
||||||
|
|
||||||
# Clear buffer
|
|
||||||
frame_buffer.clear()
|
|
||||||
|
|
||||||
# Process loop with batching
|
|
||||||
current_count = 0
|
current_count = 0
|
||||||
for frame_idx in range(req.start_frame, end_frame + 1):
|
for frame_idx in range(req.start_frame, end_frame + 1):
|
||||||
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].status = TaskStatus.CANCELLED
|
||||||
tasks[task_id].message = "Cancelled by user"
|
tasks[task_id].message = "Cancelled by user"
|
||||||
break
|
break
|
||||||
|
|
||||||
# Read frame
|
# Read frame
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
||||||
ret, frame = cap.read()
|
ret, frame = cap.read()
|
||||||
|
|
||||||
if ret:
|
if ret:
|
||||||
# Store frame for batch processing
|
# Detect
|
||||||
frame_buffer.append(frame)
|
detections = detector.detect(frame)
|
||||||
|
|
||||||
# Process batch when full
|
# Generate mask
|
||||||
if len(frame_buffer) >= BATCH_SIZE:
|
mask = detector.generate_mask(
|
||||||
process_batch()
|
frame.shape,
|
||||||
|
detections,
|
||||||
|
mask_scale=req.mask_scale
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save
|
||||||
|
mask_filename = f"mask_{current_count:06d}.png" # Note: using relative index for filename
|
||||||
|
mask_path = os.path.join(req.output_dir, mask_filename)
|
||||||
|
cv2.imwrite(mask_path, mask)
|
||||||
|
|
||||||
# Update progress
|
# Update progress
|
||||||
current_count += 1
|
current_count += 1
|
||||||
tasks[task_id].progress = current_count
|
tasks[task_id].progress = current_count
|
||||||
|
|
||||||
# Process remaining frames in buffer
|
|
||||||
if frame_buffer:
|
|
||||||
process_batch()
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
writer_running.clear()
|
|
||||||
write_queue.join() # Wait for all frames to be written
|
|
||||||
if writer_thread:
|
|
||||||
writer_thread.join(timeout=5)
|
|
||||||
|
|
||||||
cap.release()
|
cap.release()
|
||||||
if writer:
|
|
||||||
writer.release()
|
|
||||||
|
|
||||||
if tasks[task_id].status == TaskStatus.PROCESSING:
|
if tasks[task_id].status == TaskStatus.PROCESSING:
|
||||||
tasks[task_id].status = TaskStatus.COMPLETED
|
tasks[task_id].status = TaskStatus.COMPLETED
|
||||||
tasks[task_id].result_path = output_video_path # Return video path
|
tasks[task_id].result_path = req.output_dir
|
||||||
tasks[task_id].message = "Processing completed successfully"
|
tasks[task_id].message = "Processing completed successfully"
|
||||||
print(f"Task {task_id} completed: {output_video_path}")
|
print(f"Task {task_id} completed.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tasks[task_id].status = TaskStatus.FAILED
|
tasks[task_id].status = TaskStatus.FAILED
|
||||||
|
|
@ -322,21 +230,12 @@ def log_startup_diagnostics():
|
||||||
for var in rocm_vars:
|
for var in rocm_vars:
|
||||||
value = os.environ.get(var)
|
value = os.environ.get(var)
|
||||||
if value:
|
if value:
|
||||||
# For LD_LIBRARY_PATH, show if ROCm paths are included
|
# Truncate very long values
|
||||||
if var == 'LD_LIBRARY_PATH':
|
if len(value) > 200:
|
||||||
has_rocm = 'rocm' in value.lower() or 'clr-' in value
|
display_value = value[:200] + "... (truncated)"
|
||||||
has_hip = 'hip' in value.lower()
|
|
||||||
print(f" {var}: {value[:100]}...")
|
|
||||||
print(f" Contains ROCm paths: {has_rocm}")
|
|
||||||
print(f" Contains HIP paths: {has_hip}")
|
|
||||||
if not has_rocm:
|
|
||||||
print(f" ⚠️ WARNING: ROCm library paths not found!")
|
|
||||||
else:
|
else:
|
||||||
if len(value) > 200:
|
display_value = value
|
||||||
display_value = value[:200] + "... (truncated)"
|
print(f" {var}: {display_value}")
|
||||||
else:
|
|
||||||
display_value = value
|
|
||||||
print(f" {var}: {display_value}")
|
|
||||||
else:
|
else:
|
||||||
print(f" {var}: (not set)")
|
print(f" {var}: (not set)")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user