Files
judian17-ComfyUI_ZIM/zim_mask_preprocessing_node.py

150 lines
5.6 KiB
Python

import torch
import numpy as np
import cv2 # ComfyUI typically includes opencv-python-headless
import json
class MaskToPoints:
"""
A node that takes a mask and outputs the center points of its white areas.
If there are multiple disconnected white areas, it outputs the center point for each.
The output is a JSON string representing a list of points, e.g., [{"x": 100, "y": 150}, {"x": 200, "y": 250}].
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("points",) # Added for UI display name
FUNCTION = "get_points_from_mask"
CATEGORY = "ComfyUI_ZIM/mask" # Changed category
def get_points_from_mask(self, mask: torch.Tensor):
if mask is None:
return (json.dumps([]),)
if mask.ndim == 3:
mask_np = mask[0].cpu().numpy()
elif mask.ndim == 2:
mask_np = mask.cpu().numpy()
else:
print(f"MaskToPoints: Warning - Invalid mask dimensions: {mask.shape}. Expected 2 or 3 dimensions.")
return (json.dumps([]),)
if mask_np.ndim != 2:
print(f"MaskToPoints: Warning - Mask numpy conversion resulted in unexpected dimensions: {mask_np.shape}")
return (json.dumps([]),)
if mask_np.max() <= 1.0 and mask_np.min() >= 0.0:
processed_mask = (mask_np * 255).astype(np.uint8)
else:
processed_mask = np.clip(mask_np, 0, 255).astype(np.uint8)
_, binary_mask = cv2.threshold(processed_mask, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
points_list = []
if contours:
for contour in contours:
if cv2.contourArea(contour) > 0:
x, y, w, h = cv2.boundingRect(contour)
center_x = x + w / 2.0
center_y = y + h / 2.0
points_list.append({"x": int(round(center_x)), "y": int(round(center_y))})
return (json.dumps(points_list),)
class MaskToBbox:
"""
A node that takes a mask and outputs the bounding box of its white areas.
If there are multiple disconnected white areas, it outputs a single bounding box
that encompasses all white areas.
The output is a BBOX tuple: (x_min, y_min, width, height).
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
}
}
RETURN_TYPES = ("BBOX",) # ComfyUI might interpret this as a generic tuple/list.
# RETURN_NAMES = ("bbox",) # Optional
FUNCTION = "get_bbox_from_mask"
CATEGORY = "ComfyUI_ZIM/mask" # Changed category
def get_bbox_from_mask(self, mask: torch.Tensor):
if mask is None:
return ((0, 0, 0, 0),) # Return a zero bbox if no mask
# Convert mask tensor to NumPy array
if mask.ndim == 3: # Batch, Height, Width
mask_np = mask[0].cpu().numpy() # Process the first mask in the batch
elif mask.ndim == 2: # Height, Width
mask_np = mask.cpu().numpy()
else:
print(f"MaskToBbox: Warning - Invalid mask dimensions: {mask.shape}. Expected 2 or 3 dimensions.")
return ((0, 0, 0, 0),)
if mask_np.ndim != 2:
print(f"MaskToBbox: Warning - Mask numpy conversion resulted in unexpected dimensions: {mask_np.shape}")
return ((0, 0, 0, 0),)
# Normalize and binarize the mask
if mask_np.max() <= 1.0 and mask_np.min() >= 0.0: # Assuming mask is in 0.0-1.0 range
processed_mask = (mask_np * 255).astype(np.uint8)
else: # Assuming mask might be in 0-255 range already or other
processed_mask = np.clip(mask_np, 0, 255).astype(np.uint8)
_, binary_mask = cv2.threshold(processed_mask, 127, 255, cv2.THRESH_BINARY)
# Find contours
contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return ((0, 0, 0, 0),) # No contours found
# Calculate the overall bounding box for all contours
overall_x_min = mask_np.shape[1] # Initialize with image width
overall_y_min = mask_np.shape[0] # Initialize with image height
overall_x_max = 0
overall_y_max = 0
for contour in contours:
if cv2.contourArea(contour) > 0: # Consider only non-empty contours
x, y, w, h = cv2.boundingRect(contour)
overall_x_min = min(overall_x_min, x)
overall_y_min = min(overall_y_min, y)
overall_x_max = max(overall_x_max, x + w)
overall_y_max = max(overall_y_max, y + h)
if overall_x_max <= overall_x_min or overall_y_max <= overall_y_min:
# This case can happen if contours were found but had zero area after filtering,
# or if initial overall_x_min/max were not updated.
return ((0,0,0,0),)
bbox_x = overall_x_min
bbox_y = overall_y_min
bbox_width = overall_x_max - overall_x_min
bbox_height = overall_y_max - overall_y_min
return ((int(bbox_x), int(bbox_y), int(bbox_width), int(bbox_height)),)
# NODE CLASS MAPPINGS
NODE_CLASS_MAPPINGS = {
"MaskToPoints_ZIM": MaskToPoints, # Renamed to avoid potential conflicts if old file is still around
"MaskToBbox_ZIM": MaskToBbox
}
# NODE DISPLAY NAME MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS = {
"MaskToPoints_ZIM": "Mask To Points (ZIM)",
"MaskToBbox_ZIM": "Mask To Bbox (ZIM)"
}