Files
2025-04-27 09:15:08 -04:00

153 lines
6.0 KiB
Python

import json
import logging
import torch
from ....src.mediapipe_vision.interactive_segmentation.detector import (
InteractiveSegmenterProcessor,
)
from ....src.mediapipe_vision.types import PointOfInterest
# Import Base Loader and Detector
from ....src.mediapipe_vision.common.model_loader import MediaPipeModelLoaderBaseNode
logger = logging.getLogger(__name__)
_category = "Realtime Nodes/MediaPipe Vision/InteractiveSegmentation"
# --- Model Loader ---
class MediaPipeInteractiveSegmenterModelLoaderNode(MediaPipeModelLoaderBaseNode):
"""ComfyUI node for loading MediaPipe Interactive Segmenter models."""
# Usually uses a specific model like selfie_segmenter or a custom one
TASK_TYPE = "interactive_segmenter" # Need to define this task and models in model_loader.py
RETURN_TYPES = ("INTERACTIVE_SEGMENTER_MODEL_INFO",)
RETURN_NAMES = ("model_info",)
CATEGORY = _category
# --- Segmenter Node ---
class MediaPipeInteractiveSegmenterNode:
"""ComfyUI node for MediaPipe Interactive Image Segmentation."""
def __init__(self):
self._processor: InteractiveSegmenterProcessor = None
self._model_path: str = None
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"model_info": ("INTERACTIVE_SEGMENTER_MODEL_INFO",),
# Input points as a JSON string list of lists/tuples: e.g., "[[0.5, 0.5], [0.2, 0.3]]"
"points_json": (
"STRING",
{
"default": "[[0.5, 0.5]]",
"multiline": True,
"tooltip": "Points of interest (normalized coords) as JSON: [[x1,y1], [x2,y2], ...]",
},
),
"output_category_mask": (
"BOOLEAN",
{
"default": True,
"tooltip": "Output a category mask (0=background, 1=foreground). Priority over confidence.",
},
),
"output_confidence_mask": (
"BOOLEAN",
{
"default": False,
"tooltip": "Output a confidence mask (0.0-1.0). Used if category mask is False.",
},
),
}
}
RETURN_TYPES = ("MASK",) # Output is a standard ComfyUI mask
RETURN_NAMES = ("mask",)
FUNCTION = "segment_interactive"
CATEGORY = _category
def segment_interactive(
self,
image: torch.Tensor,
model_info: dict,
points_json: str,
output_category_mask: bool,
output_confidence_mask: bool,
):
"""Performs interactive segmentation."""
task_type = model_info.get("task_type")
expected_task_type = MediaPipeInteractiveSegmenterModelLoaderNode.TASK_TYPE
if not isinstance(model_info, dict) or task_type != expected_task_type:
raise ValueError(f"Invalid model_info. Expected task_type '{expected_task_type}' but got '{task_type}'.")
model_path = model_info.get("model_path")
if not model_path:
raise ValueError("Model path not found or invalid in model_info.")
# Parse points from JSON
try:
points_list = json.loads(points_json)
if not isinstance(points_list, list) or not all(isinstance(p, (list, tuple)) and len(p) == 2 for p in points_list):
raise ValueError("Invalid format")
points_of_interest = [PointOfInterest(x=p[0], y=p[1]) for p in points_list]
except Exception as e:
raise ValueError(f"Invalid points_json format: {e}. Expected list of [x, y] pairs, e.g., [[0.5, 0.5]].")
# Manage detector instance
if self._processor is None or self._model_path != model_path:
if self._processor and hasattr(self._processor, "close"):
try:
self._processor.close()
except Exception as e:
logger.warning(f"Error closing InteractiveSegmenterProcessor: {e}")
logger.info(f"Creating new InteractiveSegmenterProcessor instance for {model_path}")
self._processor = InteractiveSegmenterProcessor(model_path)
self._model_path = model_path
# Call the detector's segment method
mask_batch = self._processor.segment(
image,
points_of_interest=points_of_interest,
output_category_mask=output_category_mask,
output_confidence_mask=output_confidence_mask,
)
# Convert HW mask list to B1HW tensor for ComfyUI MASK type
# Assuming detector returns one mask per batch item
comfy_masks = []
for mask_hw in mask_batch:
if mask_hw is not None:
comfy_masks.append(mask_hw.unsqueeze(0)) # Add channel dim
else:
# Handle None case - maybe return zero mask? Needs image shape
h, w = image.shape[1], image.shape[2]
comfy_masks.append(torch.zeros((1, h, w), dtype=torch.float32, device=image.device))
output_tensor = torch.stack(comfy_masks, dim=0) # Stack along batch dim
return (output_tensor,)
def __del__(self):
if hasattr(self, "_processor") and self._processor and hasattr(self._processor, "close"):
try:
self._processor.close()
except Exception as e:
logger.warning(f"Error closing InteractiveSegmenterProcessor in __del__: {e}")
self._processor = None
# --- Mappings ---
NODE_CLASS_MAPPINGS = {
"MediaPipeInteractiveSegmenterModelLoader": MediaPipeInteractiveSegmenterModelLoaderNode,
"MediaPipeInteractiveSegmenter": MediaPipeInteractiveSegmenterNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MediaPipeInteractiveSegmenterModelLoader": "Load Interactive Segmenter Model (MediaPipe)",
"MediaPipeInteractiveSegmenter": "Interactive Segmenter (MediaPipe)",
}