Merge pull request #14 from ryanontheinside/refactor/phase-1

Refactor/phase 1
This commit is contained in:
RyanOnTheInside
2025-04-27 02:54:54 +02:00
committed by GitHub
33 changed files with 185 additions and 90 deletions
@@ -3,7 +3,7 @@ from typing import List, Dict, Tuple, Any, Optional, Type
import cv2
import numpy as np
from ....src.mediapipe_vision.utils.image_utils import convert_to_cv2, convert_to_tensor
from ....src.utils.image import convert_to_cv2, convert_to_tensor
logger = logging.getLogger(__name__)
@@ -3,11 +3,11 @@ import logging
from typing import Optional, List, Any, Dict, Tuple
from .....src.mediapipe_vision.types import BLENDSHAPES_LIST # Updated type alias
from .....src.mediapipe_vision.utils.misc_utils import get_blendshape_categories # Utility to get all category names
from .....src.mediapipe_vision.utils.blendshape_utils import get_blendshape_categories # Utility to get all category names
# Import base trigger/control logic if we adapt base classes later, or implement directly for now
from ...location_landmark.universal_landmark_nodes import FLOAT_EQUALITY_TOLERANCE # For trigger node comparisons
from .....src.mediapipe_vision.utils.delta_utils import scale_value
from .....src.utils.math import scale_value
logger = logging.getLogger(__name__)
@@ -6,7 +6,7 @@ import logging
from typing import List, Dict, Tuple
from .....src.mediapipe_vision.types import LandmarkPoint
from .....src.mediapipe_vision.utils.mask_utils import create_mask_from_points
from .....src.utils.image import create_mask_from_points
logger = logging.getLogger(__name__)
_category = "Realtime Nodes/MediaPipe Vision/Face/FaceLandmark/Masking"
@@ -5,7 +5,7 @@ from scipy.spatial.transform import Rotation as R
from typing import Optional, List, Any, Tuple, Dict
from .....src.mediapipe_vision.types import TRANSFORM_MATRIX_LIST
from .....src.mediapipe_vision.utils.delta_utils import scale_value
from .....src.utils.math import scale_value
from ...location_landmark.universal_landmark_nodes import FLOAT_EQUALITY_TOLERANCE # For trigger node comparisons
logger = logging.getLogger(__name__)
@@ -5,8 +5,8 @@ import logging
from typing import List
from .....src.mediapipe_vision.types import HandLandmarksResult
# Reusing the helper function from face masking
from .....src.mediapipe_vision.utils.mask_utils import create_mask_from_points
# Updated import path
from .....src.utils.image import create_mask_from_points
logger = logging.getLogger(__name__)
_category = "Realtime Nodes/MediaPipe Vision/Hand/HandLandmark/Masking"
@@ -15,9 +15,9 @@ from ....src.mediapipe_vision.types import (
LandmarkPoint, FaceLandmarksResult, HandLandmarksResult, PoseLandmarksResult
)
# Re-import needed utils if not available globally
from ....src.mediapipe_vision.utils.delta_utils import scale_value
from ....src.mediapipe_vision.utils.comfy_utils import AlwaysEqualProxy
# Import from the new consolidated utilities
from ....src.utils.math import scale_value
from ....src.utils.general import AlwaysEqualProxy
logger = logging.getLogger(__name__)
@@ -3,7 +3,7 @@ import torch
import numpy as np
import cv2
from ....src.mediapipe_vision.utils.image_utils import convert_to_cv2, convert_to_tensor
from ....src.utils.image import convert_to_cv2, convert_to_tensor
from ..common.base_visualization_nodes import BaseDetectionVisualizationNode
import logging
@@ -6,7 +6,7 @@ import logging
from typing import List, Dict, Tuple
from ....src.mediapipe_vision.types import PoseLandmarksResult
from ....src.mediapipe_vision.utils.mask_utils import create_mask_from_points
from ....src.utils.image import create_mask_from_points
# Removed GetImageDimensionsNode import
logger = logging.getLogger(__name__)
@@ -1,6 +1,6 @@
from ....src.realtimenodes.control_base import ControlNodeBase
import copy
from ....src.utils.general_utils import AlwaysEqualProxy
from ....src.utils.general import AlwaysEqualProxy
class StateResetNode(ControlNodeBase):
"""Node that resets all control node states when triggered"""
+1 -1
View File
@@ -1,5 +1,5 @@
from ....src.utils.general_utils import AlwaysEqualProxy
from ....src.realtimenodes.control_base import ControlNodeBase
from ....src.utils.general import AlwaysEqualProxy
class LazyCondition(ControlNodeBase):
DESCRIPTION = "Uses lazy evaluation to truly skip execution of unused paths. Maintains state of the last value to circumvent feedback loops."
@@ -5,7 +5,7 @@ import numpy as np
import cv2
import torch
import random
from ....src.utils.general_utils import AlwaysEqualProxy
from ....src.utils.general import AlwaysEqualProxy
# # Add package root to Python path # <-- REMOVED
# sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ....src.realtimenodes.control_base import ControlNodeBase
+1 -1
View File
@@ -9,7 +9,7 @@ from PIL import Image, ImageFont, ImageDraw, ImageOps
import nodes
import random
from torchvision import transforms
from ...src.utils.general_utils import AlwaysEqualProxy
from ...src.utils.general import AlwaysEqualProxy
MAX_RESOLUTION = nodes.MAX_RESOLUTION # Get the same max resolution as core nodes
class DTypeConverter:
@@ -12,7 +12,7 @@ from mediapipe.tasks.python.core.base_options import BaseOptions
from typing import List, Tuple
# Import new types
from ..types import FaceDetectionResult, BoundingBox, FaceKeypoint
from ..utils.timestamp_provider import TimestampProvider # Import from utils
from ...utils.timing import TimestampProvider # Import from utils
class FaceDetector:
"""Detects faces in an image using MediaPipe FaceDetector."""
@@ -7,7 +7,7 @@ from mediapipe.tasks.python.core.base_options import BaseOptions
from typing import List, Tuple, Optional, Dict
# Import new types
from ..types import LandmarkPoint, FaceLandmarksResult
from ..utils.timestamp_provider import TimestampProvider # Import TimestampProvider
from ...utils.timing import TimestampProvider # Import TimestampProvider
class FaceLandmarkDetector:
"""Detects face landmarks and blendshapes using MediaPipe FaceLandmarker."""
@@ -4,7 +4,7 @@ import torch
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from typing import List, Optional
from ..utils.timestamp_provider import TimestampProvider
from ...utils.timing import TimestampProvider
class FaceStylizer:
"""Applies stylization to faces using MediaPipe FaceStylizer."""
@@ -7,7 +7,7 @@ from mediapipe.tasks.python.core.base_options import BaseOptions
from typing import List
# Import new types
from ..types import GestureRecognitionResult, GestureCategory
from ..utils.timestamp_provider import TimestampProvider # Import TimestampProvider
from ...utils.timing import TimestampProvider # Import TimestampProvider
class GestureRecognizer:
"""Recognizes hand gestures using MediaPipe GestureRecognizer."""
@@ -7,7 +7,7 @@ from mediapipe.tasks.python.core.base_options import BaseOptions
from typing import List, Tuple, Optional
# Import new types
from ..types import LandmarkPoint, HandLandmarksResult
from ..utils.timestamp_provider import TimestampProvider # Import TimestampProvider
from ...utils.timing import TimestampProvider # Import TimestampProvider
class HandLandmarkDetector:
"""Detects hand landmarks using MediaPipe HandLandmarker."""
@@ -12,7 +12,7 @@ from typing import List, Tuple, Optional, Dict, Any
# Import needed types
from ..types import LandmarkPoint, HolisticLandmarksResult
from ..utils.timestamp_provider import TimestampProvider
from ...utils.timing import TimestampProvider
class HolisticLandmarkDetector:
"""Detects holistic landmarks combining face, pose and hand tracking.
@@ -7,7 +7,8 @@ from mediapipe.tasks.python.core.base_options import BaseOptions
from typing import List, Optional
# Import new types
from ..types import ImageEmbedderResult
from ..utils.timestamp_provider import TimestampProvider # Import TimestampProvider
# Import TimestampProvider from the new location
from ...utils.timing import TimestampProvider
class ImageEmbedder:
"""Generates image embeddings using MediaPipe ImageEmbedder."""
@@ -11,7 +11,7 @@ from mediapipe.tasks.python import vision
from mediapipe.tasks.python.vision import ImageSegmenter
import platform # Needed for OS check
import logging
from ..utils.timestamp_provider import TimestampProvider # Import TimestampProvider
from ...utils.timing import TimestampProvider
logger = logging.getLogger(__name__)
@@ -1,3 +1,4 @@
import logging
import mediapipe as mp
import numpy as np
import torch
@@ -11,7 +12,7 @@ from mediapipe.tasks.python.components.containers import keypoint
from typing import List, Optional
from ..types import PointOfInterest
from ..utils.timestamp_provider import TimestampProvider
from ...utils.timing import TimestampProvider
import logging
logger = logging.getLogger(__name__)
@@ -7,7 +7,7 @@ from mediapipe.tasks.python.core.base_options import BaseOptions
from typing import List
# Import new types
from ..types import ObjectDetectionResult, ObjectDetectionCategory, BoundingBox
from ..utils.timestamp_provider import TimestampProvider # Import TimestampProvider
from ...utils.timing import TimestampProvider
class ObjectDetector:
"""Detects objects using MediaPipe ObjectDetector."""
@@ -7,7 +7,7 @@ from mediapipe.tasks.python.core.base_options import BaseOptions
from typing import List, Tuple, Optional
# Import new types
from ..types import LandmarkPoint, PoseLandmarksResult
from ..utils.timestamp_provider import TimestampProvider # Import TimestampProvider
from ...utils.timing import TimestampProvider
class PoseLandmarkDetector:
"""Detects pose landmarks in an image using MediaPipe PoseLandmarker."""
+10 -3
View File
@@ -1,12 +1,19 @@
"""Utility functions for MediaPipe Vision."""
# DEPRECATED: These imports are maintained only for backward compatibility.
# For new code, please import directly from src.utils
from ...utils.math import scale_value
from ...utils.image import convert_to_cv2, convert_to_tensor
# Keep only the functions specific to mediapipe_vision
from .download_utils import download_model
from .image_utils import convert_to_cv2, convert_to_tensor
from .delta_utils import calculate_euclidean_delta, scale_value
from .delta_utils import calculate_euclidean_delta
__all__ = [
"convert_to_cv2",
"convert_to_tensor",
"calculate_euclidean_delta",
"scale_value"
"scale_value",
"download_model"
]
+10 -19
View File
@@ -1,8 +1,16 @@
# DEPRECATED: This file is maintained only for backward compatibility.
# For new code, please import directly from:
# src.utils.math
# scale_value has been moved to src/utils/math.py
# Please update your imports.
import math
from typing import Optional
from ...utils.math import scale_value
from ..types import LandmarkPoint # Assuming LandmarkPoint is in src/types.py
#TODO: refactor to common after unified coordinate system
def calculate_euclidean_delta(p1: Optional[LandmarkPoint], p2: Optional[LandmarkPoint]) -> Optional[float]:
"""Calculates the Euclidean distance between two LandmarkPoint objects."""
if p1 is None or p2 is None:
@@ -15,21 +23,4 @@ def calculate_euclidean_delta(p1: Optional[LandmarkPoint], p2: Optional[Landmark
distance = math.sqrt(delta_x**2 + delta_y**2 + delta_z**2)
return distance
def scale_value(value: Optional[float],
delta_min: float, delta_max: float,
output_min: float, output_max: float,
clamp: bool = True) -> Optional[float]:
"""Linearly scales a value from one range to another."""
if value is None:
return None
if delta_max == delta_min: # Avoid division by zero
return output_min if value <= delta_min else output_max
# Scale
scaled = output_min + (value - delta_min) * (output_max - output_min) / (delta_max - delta_min)
# Clamp
if clamp:
scaled = max(output_min, min(output_max, scaled))
return scaled
# scale_value is now imported from utils.math
-26
View File
@@ -1,26 +0,0 @@
# --- Helper Function ---
import torch
import numpy as np
import cv2
from typing import List, Tuple
import logging
logger = logging.getLogger(__name__)
def create_mask_from_points(height: int, width: int, points: List[Tuple[int, int]], device='cpu') -> torch.Tensor:
"""Creates a binary mask tensor from a list of points using convex hull."""
mask = np.zeros((height, width), dtype=np.uint8)
if len(points) < 3:
logger.warning(f"Cannot create mask from {len(points)} points (need >= 3 for convex hull). Returning empty mask.")
return torch.from_numpy(mask).float().unsqueeze(0).to(device)
try:
np_points = np.array(points, dtype=np.int32)
hull = cv2.convexHull(np_points)
cv2.fillPoly(mask, [hull], 1)
except Exception as e:
logger.error(f"Error creating mask polygon: {e}")
return torch.from_numpy(mask).float().unsqueeze(0).to(device)
mask_tensor = torch.from_numpy(mask).float().unsqueeze(0).to(device)
return mask_tensor
+20 -1
View File
@@ -1 +1,20 @@
# This file intentionally left blank
"""
Unified utilities package for ComfyUI RealTimeNodes.
"""
# General utilities
from .general import AlwaysEqualProxy
# Image utilities
from .image import convert_to_cv2, convert_to_tensor, create_mask_from_points
# Math utilities
from .math import scale_value, calculate_euclidean_distance, calculate_euclidean_distance_2d
# Timing utilities
from .timing import TimestampProvider
# For backward compatibility, expose the original names
# This allows existing code to continue working while we transition
image_to_cv2 = convert_to_cv2
cv2_to_image = convert_to_tensor
@@ -1,8 +1,10 @@
"""Utilities specific to ComfyUI integration."""
"""
General utilities for ComfyUI RealTimeNodes.
"""
class AlwaysEqualProxy(str):
"""Helper class to allow connecting multiple specific input types to a single input node.
Borrowed from https://github.com/theUpsider/ComfyUI-Logic
Originally borrowed from https://github.com/theUpsider/ComfyUI-Logic
"""
def __eq__(self, _):
return True
-7
View File
@@ -1,7 +0,0 @@
class AlwaysEqualProxy(str):
#borrowed from https://github.com/theUpsider/ComfyUI-Logic
def __eq__(self, _):
return True
def __ne__(self, _):
return False
@@ -1,6 +1,14 @@
"""
Image processing utilities for ComfyUI RealTimeNodes.
Contains functions for converting between different image formats,
particularly between ComfyUI tensor format (BHWC) and OpenCV format.
"""
import torch
import numpy as np
import cv2
from typing import List, Union, Tuple
def convert_to_cv2(tensor: torch.Tensor) -> list:
"""Converts a ComfyUI IMAGE tensor (BHWC, float32, 0-1) to a list of cv2 images (BGR, uint8)."""
@@ -41,4 +49,20 @@ def convert_to_tensor(cv2_images) -> torch.Tensor:
# Convert to tensor (already has batch dimension)
tensor = torch.from_numpy(batch_np)
return tensor
return tensor
def create_mask_from_points(height: int, width: int, points: List[Tuple[int, int]], device='cpu') -> torch.Tensor:
"""Creates a binary mask tensor from a list of points using convex hull."""
mask = np.zeros((height, width), dtype=np.uint8)
if len(points) < 3:
return torch.from_numpy(mask).float().unsqueeze(0).to(device)
try:
np_points = np.array(points, dtype=np.int32)
hull = cv2.convexHull(np_points)
cv2.fillPoly(mask, [hull], 1)
except Exception as e:
return torch.from_numpy(mask).float().unsqueeze(0).to(device)
mask_tensor = torch.from_numpy(mask).float().unsqueeze(0).to(device)
return mask_tensor
+80
View File
@@ -0,0 +1,80 @@
"""
Math utility functions for RealTimeNodes.
Contains functions for mathematical operations including:
- Value scaling and normalization
- Distance calculations
- Other general math operations
"""
import math
from typing import Optional, Tuple, Any
def scale_value(value: Optional[float],
input_min: float, input_max: float,
output_min: float, output_max: float,
clamp: bool = True) -> Optional[float]:
"""
Linearly scales a value from one range to another.
Args:
value: The value to scale
input_min: Minimum value of input range
input_max: Maximum value of input range
output_min: Minimum value of output range
output_max: Maximum value of output range
clamp: Whether to clamp the output to the output range
Returns:
Scaled value, or None if input was None
"""
if value is None:
return None
if input_max == input_min: # Avoid division by zero
return output_min if value <= input_min else output_max
# Scale
scaled = output_min + (value - input_min) * (output_max - output_min) / (input_max - input_min)
# Clamp
if clamp:
scaled = max(output_min, min(output_max, scaled))
return scaled
def calculate_euclidean_distance(point1: Tuple[float, float, float],
point2: Tuple[float, float, float]) -> float:
"""
Calculates the Euclidean distance between two 3D points.
Args:
point1: First point as (x, y, z)
point2: Second point as (x, y, z)
Returns:
Euclidean distance between the points
"""
delta_x = point1[0] - point2[0]
delta_y = point1[1] - point2[1]
delta_z = point1[2] - point2[2]
distance = math.sqrt(delta_x**2 + delta_y**2 + delta_z**2)
return distance
def calculate_euclidean_distance_2d(point1: Tuple[float, float],
point2: Tuple[float, float]) -> float:
"""
Calculates the Euclidean distance between two 2D points.
Args:
point1: First point as (x, y)
point2: Second point as (x, y)
Returns:
Euclidean distance between the points
"""
delta_x = point1[0] - point2[0]
delta_y = point1[1] - point2[1]
distance = math.sqrt(delta_x**2 + delta_y**2)
return distance
@@ -1,19 +1,22 @@
"""
Timing and timestamp utilities for RealTimeNodes.
"""
import time
class TimestampProvider:
"""
Provides real-time, monotonic timestamps in milliseconds.
MediaPipe's RunningMode.VIDEO requires strictly increasing timestamp_ms values.
Using time.time() alone can result in duplicates during tight loops, which causes
MediaPipe to skip or freeze frames. This class ensures timestamps are always
increasing while staying aligned with real elapsed time.
This ensures timestamps are always increasing while staying aligned with real elapsed time.
Useful for any process that requires monotonically increasing time values.
"""
def __init__(self):
self._start_time = time.time()
self._last_timestamp = 0
def next(self) -> int:
"""Get the next timestamp in milliseconds, guaranteed to be greater than the previous one."""
now = int((time.time() - self._start_time) * 1000)
if now <= self._last_timestamp:
now = self._last_timestamp + 1