641 lines
26 KiB
Python
641 lines
26 KiB
Python
import torch
|
|
import numpy as np
|
|
from PIL import Image
|
|
import cv2
|
|
|
|
from imgutils.detect import (
|
|
detect_person, detect_faces, detect_heads, detect_halfbody,
|
|
detect_hands, detect_eyes, detect_with_booru_yolo, detect_with_nudenet
|
|
)
|
|
from imgutils.detect.censor import detect_censors
|
|
|
|
from imgutils.detect.visual import detection_visualize
|
|
|
|
|
|
BBOX = "BBOX"
|
|
class BBOX_imagutils:
|
|
"""
|
|
A class to represent bounding boxes in imgutils detection.
|
|
This is a placeholder for future use if needed.
|
|
"""
|
|
def __init__(self, x0, y0, x1, y1,img_w,img_h, label="", confidence=0.0):
|
|
self.x0 = x0
|
|
self.y0 = y0
|
|
self.x1 = x1
|
|
self.y1 = y1
|
|
self.img_w = img_w
|
|
self.img_h = img_h
|
|
self.label = label
|
|
self.confidence = confidence
|
|
|
|
def __repr__(self):
|
|
return f"BBOX({self.x0}, {self.y0}, {self.x1}, {self.y1}, {self.img_w}, {self.img_h}, '{self.label}', {self.confidence})"
|
|
|
|
def get_bbox(self):
|
|
return (self.x0, self.y0, self.x1, self.y1)
|
|
|
|
def to_json(self):
|
|
"""Convert BBOX_imagutils to JSON serializable format"""
|
|
return {
|
|
"type": "BBOX_imagutils",
|
|
"x0": self.x0,
|
|
"y0": self.y0,
|
|
"x1": self.x1,
|
|
"y1": self.y1,
|
|
"img_w": self.img_w,
|
|
"img_h": self.img_h,
|
|
"label": self.label,
|
|
"confidence": self.confidence
|
|
}
|
|
|
|
@classmethod
|
|
def from_json(cls, data):
|
|
"""Create BBOX_imagutils from JSON data"""
|
|
if isinstance(data, dict) and data.get("type") == "BBOX_imagutils":
|
|
return cls(
|
|
x0=data["x0"],
|
|
y0=data["y0"],
|
|
x1=data["x1"],
|
|
y1=data["y1"],
|
|
img_w=data.get("img_w", 0),
|
|
img_h=data.get("img_h", 0),
|
|
label=data.get("label", ""),
|
|
confidence=data.get("confidence", 0.0)
|
|
)
|
|
return data
|
|
|
|
def __dict__(self):
|
|
"""Support for dict() conversion"""
|
|
return self.to_json()
|
|
|
|
|
|
DETECTION_FUNCTIONS = {
|
|
"Person Detection": detect_person,
|
|
"Face Detection": detect_faces,
|
|
"Head Detection": detect_heads,
|
|
"Halfbody Detection": detect_halfbody,
|
|
"Hand Detection": detect_hands,
|
|
"Eye Detection": detect_eyes,
|
|
"Censor Detection": detect_censors,
|
|
# "Booru YOLO Detection": detect_with_booru_yolo,
|
|
# "Nude Detection": detect_with_nudenet,
|
|
}
|
|
|
|
MODEL_VERSIONS = {
|
|
"Person Detection": ["v0", "v1", "v1.1"],
|
|
"Face Detection": ["v0", "v1", "v1.3", "v1.4"],
|
|
"Head Detection": [],
|
|
"Halfbody Detection": ["v1"],
|
|
"Hand Detection": ["v1"],
|
|
"Eye Detection": ["v1"],
|
|
"Censor Detection": ["v1"],
|
|
}
|
|
|
|
class ImgutilsGenericDetector:
|
|
"""
|
|
A generic imgutils detector node that unifies various detection functions.
|
|
"""
|
|
CATEGORY = "imgutils/detection"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"detection_type": (list(DETECTION_FUNCTIONS.keys()),),
|
|
"conf_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
|
|
"iou_threshold": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
|
|
"draw_boxes": ("BOOLEAN", {"default": True}),
|
|
# Model specific parameters, always present for simplicity in this generic node
|
|
"level": ("STRING", {"default": "s", "options": ["n", "s"]}),
|
|
"version": ("STRING", {"default": "", "options": ["v0", "v1", "v1.1"]}),
|
|
# "model_name": ("STRING", {"default": ""}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK",BBOX, )
|
|
RETURN_NAMES = ("image_with_boxes", "detection_mask","detection_bbox",)
|
|
FUNCTION = "detect"
|
|
|
|
def detect(self, image, detection_type, conf_threshold,
|
|
iou_threshold, draw_boxes, level, version, model_name=None):
|
|
# Convert ComfyUI image tensor to PIL Image
|
|
# image is in format [B, H, W, C] with values 0-1
|
|
img_tensor = image[0] # Get first image from batch
|
|
img_np = (img_tensor.cpu().numpy() * 255).astype(np.uint8) # Convert to 0-255 range
|
|
img_pil = Image.fromarray(img_np)
|
|
|
|
# For visualization and mask generation, we still need numpy array
|
|
img_np_rgb = np.array(img_pil.convert("RGB"))
|
|
|
|
selected_detect_func = DETECTION_FUNCTIONS.get(detection_type)
|
|
detection_results_formatted = []
|
|
output_image_tensor = image # Default to original image tensor
|
|
output_mask_np = np.zeros((img_np_rgb.shape[0], img_np_rgb.shape[1]), dtype=np.float32)
|
|
output_string = ""
|
|
|
|
if not selected_detect_func:
|
|
output_string = f"Error: Detection type '{detection_type}' not supported."
|
|
output_mask_tensor = torch.from_numpy(output_mask_np).unsqueeze(0) # Add batch dimension
|
|
return (output_image_tensor, output_string, output_mask_tensor)
|
|
|
|
func_params = {
|
|
"image": img_pil, # Use PIL Image directly for imgutils functions
|
|
"conf_threshold": conf_threshold,
|
|
"iou_threshold": iou_threshold,
|
|
}
|
|
|
|
func_params["level"] = level
|
|
|
|
if version in MODEL_VERSIONS[detection_type]:
|
|
func_params["version"] = version
|
|
if model_name is None or model_name == "":
|
|
pass
|
|
else:
|
|
func_params["model_name"] = model_name
|
|
|
|
# if 1 + 1 == 2:
|
|
try:
|
|
# print(f"Running {detection_type} with parameters:")
|
|
# print(f" Original image type: {type(img_pil)}")
|
|
# print(f" Original image mode: {img_pil.mode}")
|
|
# print(f" Original image size: {img_pil.size}")
|
|
|
|
# Ensure image is in RGB mode
|
|
if img_pil.mode != 'RGB':
|
|
img_pil = img_pil.convert('RGB')
|
|
|
|
print(f"Using detection function: {selected_detect_func.__name__}")
|
|
|
|
# # Try different calling patterns based on the function
|
|
# if detection_type in ["Face Detection", "Person Detection"]:
|
|
# # These functions might have different parameter requirements
|
|
# if detection_type == "Face Detection":
|
|
# # Try with minimal parameters first
|
|
# raw_detections = selected_detect_func(
|
|
# image=img_pil,
|
|
# conf_threshold=conf_threshold
|
|
# )
|
|
# else: # Person Detection
|
|
# raw_detections = selected_detect_func(
|
|
# image=img_pil,
|
|
# level=level,
|
|
# version=version if version in MODEL_VERSIONS[detection_type] else "v1.1",
|
|
# conf_threshold=conf_threshold
|
|
# )
|
|
# else:
|
|
# # For other detection types, use the standard parameters
|
|
# raw_detections = selected_detect_func(
|
|
# image=img_pil,
|
|
# level=level,
|
|
# conf_threshold=conf_threshold
|
|
# )
|
|
|
|
if "version" in func_params:
|
|
raw_detections = selected_detect_func(
|
|
image=img_pil,
|
|
level=level,
|
|
version=func_params["version"],
|
|
conf_threshold=conf_threshold,
|
|
iou_threshold=iou_threshold
|
|
)
|
|
else:
|
|
raw_detections = selected_detect_func(
|
|
image=img_pil,
|
|
level=level,
|
|
conf_threshold=conf_threshold,
|
|
iou_threshold=iou_threshold
|
|
)
|
|
img_w, img_h = img_pil.size
|
|
|
|
for item in raw_detections:
|
|
if isinstance(item, tuple) and len(item) == 3:
|
|
box, label, confidence = item
|
|
if isinstance(box, tuple) and len(box) == 4:
|
|
detection_results_formatted.append({
|
|
'box': [int(b) for b in box],
|
|
'label': str(label),
|
|
'confidence': float(confidence)
|
|
})
|
|
bbox_list = []
|
|
|
|
if detection_results_formatted:
|
|
# output_string = f"{detection_type} Results:\n"
|
|
# output_string += "\n".join([
|
|
# f"- {res['label']}: Confidence={res['confidence']:.2f}, Box={res['box']}"
|
|
# for res in detection_results_formatted if res['confidence'] >= conf_threshold # Apply conf_threshold for string output too
|
|
# ])
|
|
# print(f"Detection results: {output_string}")
|
|
for res in detection_results_formatted:
|
|
if res['confidence'] >= conf_threshold:
|
|
bbox = BBOX_imagutils(
|
|
x0=res['box'][0],
|
|
y0=res['box'][1],
|
|
x1=res['box'][2],
|
|
y1=res['box'][3],
|
|
img_w=img_w,
|
|
img_h=img_h,
|
|
label=res['label'],
|
|
confidence=res['confidence']
|
|
)
|
|
bbox_list.append(bbox)
|
|
else:
|
|
print(f"No {detection_type} detected with confidence >= {conf_threshold}.")
|
|
# output_string = f"No {detection_type} detected with confidence >= {conf_threshold}."
|
|
|
|
|
|
if draw_boxes and detection_results_formatted:
|
|
vis_input_for_detection_visualize = []
|
|
for res in detection_results_formatted:
|
|
if res['confidence'] >= conf_threshold:
|
|
vis_input_for_detection_visualize.append(
|
|
(tuple(res['box']), res['label'], res['confidence'])
|
|
)
|
|
|
|
if vis_input_for_detection_visualize: # Only visualize if there are results after filtering
|
|
output_image_pil = detection_visualize(
|
|
image=img_pil,
|
|
detection=vis_input_for_detection_visualize,
|
|
fontsize=12,
|
|
no_label=False
|
|
)
|
|
# Convert PIL back to ComfyUI tensor format
|
|
output_image_np = np.array(output_image_pil.convert("RGB"))
|
|
output_image_tensor = torch.from_numpy(output_image_np.astype(np.float32) / 255.0).unsqueeze(0)
|
|
else:
|
|
output_image_tensor = image # Use original image tensor
|
|
|
|
else: # If not drawing or no results, use original image tensor
|
|
output_image_tensor = image # Generate mask
|
|
for res in detection_results_formatted:
|
|
if res['confidence'] >= conf_threshold:
|
|
box = res['box']
|
|
cv2.rectangle(output_mask_np, (box[0], box[1]), (box[2], box[3]), 1.0, -1)
|
|
|
|
# Convert mask to ComfyUI tensor format [B, H, W]
|
|
output_mask_tensor = torch.from_numpy(output_mask_np).unsqueeze(0)
|
|
|
|
return (
|
|
output_image_tensor,
|
|
output_mask_tensor,
|
|
bbox_list if bbox_list else [],
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"Error during {detection_type} detection: {e}")
|
|
# Error handling - return original image, error message, and empty mask
|
|
# output_string = f"Error during '{detection_type}' detection: {e}"
|
|
empty_mask = torch.zeros((1, img_np_rgb.shape[0], img_np_rgb.shape[1]), dtype=torch.float32)
|
|
empty_bbox_list = []
|
|
return (image, empty_mask, empty_bbox_list)
|
|
|
|
|
|
class MaskToBBoxNode:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"mask": ("MASK",),
|
|
"threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Threshold to binarize the mask before finding bbox"}),
|
|
"combine": ("BOOLEAN", {"default": False, "tooltip": "If True, combine all mask regions into one bbox. If False, output separate bboxes for each connected component."}),
|
|
"nms_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "IoU threshold for Non-Maximum Suppression. Higher values merge more overlapping boxes."})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("BBOX",)
|
|
FUNCTION = "mask_to_bbox"
|
|
CATEGORY = "imgutils/bbox"
|
|
DESCRIPTION = "Calculates the bounding box(es) of a mask. Can combine all regions or separate them with NMS."
|
|
|
|
def calculate_iou(self, box1, box2):
|
|
"""
|
|
Calculate Intersection over Union (IoU) of two bounding boxes.
|
|
|
|
Args:
|
|
box1, box2: [x_min, y_min, x_max, y_max]
|
|
|
|
Returns:
|
|
IoU value between 0 and 1
|
|
"""
|
|
x1_min, y1_min, x1_max, y1_max = box1
|
|
x2_min, y2_min, x2_max, y2_max = box2
|
|
|
|
# Calculate intersection area
|
|
inter_x_min = max(x1_min, x2_min)
|
|
inter_y_min = max(y1_min, y2_min)
|
|
inter_x_max = min(x1_max, x2_max)
|
|
inter_y_max = min(y1_max, y2_max)
|
|
|
|
if inter_x_max <= inter_x_min or inter_y_max <= inter_y_min:
|
|
return 0.0
|
|
|
|
inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
|
|
|
|
# Calculate union area
|
|
box1_area = (x1_max - x1_min) * (y1_max - y1_min)
|
|
box2_area = (x2_max - x2_min) * (y2_max - y2_min)
|
|
union_area = box1_area + box2_area - inter_area
|
|
|
|
if union_area == 0:
|
|
return 0.0
|
|
|
|
return inter_area / union_area
|
|
|
|
def merge_boxes(self, box1, box2):
|
|
"""
|
|
Merge two bounding boxes by taking the outer bounds.
|
|
|
|
Args:
|
|
box1, box2: [x_min, y_min, x_max, y_max]
|
|
|
|
Returns:
|
|
Merged bounding box [x_min, y_min, x_max, y_max]
|
|
"""
|
|
x1_min, y1_min, x1_max, y1_max = box1
|
|
x2_min, y2_min, x2_max, y2_max = box2
|
|
|
|
return [
|
|
min(x1_min, x2_min),
|
|
min(y1_min, y2_min),
|
|
max(x1_max, x2_max),
|
|
max(y1_max, y2_max)
|
|
]
|
|
|
|
def apply_nms(self, bbox_list, nms_threshold):
|
|
"""
|
|
Apply Non-Maximum Suppression to merge overlapping bounding boxes.
|
|
|
|
Args:
|
|
bbox_list: List of bounding boxes [[x_min, y_min, x_max, y_max], ...]
|
|
nms_threshold: IoU threshold for merging
|
|
|
|
Returns:
|
|
List of merged bounding boxes
|
|
"""
|
|
if len(bbox_list) <= 1:
|
|
return bbox_list
|
|
|
|
merged_boxes = []
|
|
used = [False] * len(bbox_list)
|
|
img_w = bbox_list[0].img_w if isinstance(bbox_list[0], BBOX_imagutils) else 1
|
|
img_h = bbox_list[0].img_h if isinstance(bbox_list[0], BBOX_imagutils) else 1
|
|
|
|
for i in range(len(bbox_list)):
|
|
if used[i]:
|
|
continue
|
|
|
|
current_box = bbox_list[i]
|
|
boxes_to_merge = [current_box]
|
|
used[i] = True
|
|
|
|
# Find all boxes that overlap with current box above threshold
|
|
# for j in range(i + 1, len(bbox_list)):
|
|
# if used[j]:
|
|
# continue
|
|
|
|
# iou = self.calculate_iou(current_box, bbox_list[j])
|
|
# if iou >= nms_threshold:
|
|
# boxes_to_merge.append(bbox_list[j])
|
|
# used[j] = True
|
|
|
|
# # Merge all overlapping boxes
|
|
# if len(boxes_to_merge) == 1:
|
|
# merged_boxes.append(current_box)
|
|
# else:
|
|
# merged_box = boxes_to_merge[0]
|
|
# for box in boxes_to_merge[1:]:
|
|
# merged_box = self.merge_boxes(merged_box, box)
|
|
# merged_boxes.append(merged_box)
|
|
|
|
for j in range(i + 1, len(bbox_list)):
|
|
if used[j]:
|
|
continue
|
|
|
|
# 取出坐标用于IoU计算
|
|
iou = self.calculate_iou(current_box.get_bbox(), bbox_list[j].get_bbox())
|
|
if iou >= nms_threshold:
|
|
boxes_to_merge.append(bbox_list[j])
|
|
used[j] = True
|
|
|
|
# Merge all overlapping boxes
|
|
if len(boxes_to_merge) == 1:
|
|
merged_boxes.append(current_box)
|
|
else:
|
|
# 合并所有box,取最大外接框,label和confidence可自定义合并策略
|
|
merged_box_coords = boxes_to_merge[0].get_bbox()
|
|
label = boxes_to_merge[0].label
|
|
confidence = max(box.confidence for box in boxes_to_merge)
|
|
for box in boxes_to_merge[1:]:
|
|
merged_box_coords = self.merge_boxes(merged_box_coords, box.get_bbox())
|
|
merged_box = BBOX_imagutils(
|
|
x0=merged_box_coords[0],
|
|
y0=merged_box_coords[1],
|
|
x1=merged_box_coords[2],
|
|
y1=merged_box_coords[3],
|
|
img_w=img_w,
|
|
img_h=img_h,
|
|
label=label,
|
|
confidence=confidence
|
|
)
|
|
merged_boxes.append(merged_box)
|
|
|
|
return merged_boxes
|
|
|
|
def mask_to_bbox(self, mask, threshold=0.5, combine=True, nms_threshold=0.5):
|
|
if mask.shape[0] > 1:
|
|
print(f"Warning: MaskToBBoxNode received a batch of masks ({mask.shape[0]}). Calculating bbox for the first mask only.")
|
|
|
|
# Convert to boolean numpy array (H, W)
|
|
mask_np = mask[0].squeeze(0).cpu().numpy() > threshold
|
|
mask_w, mask_h = mask_np.shape
|
|
|
|
if combine:
|
|
# Original behavior - combine all mask regions into one bbox
|
|
rows = np.any(mask_np, axis=1)
|
|
cols = np.any(mask_np, axis=0)
|
|
|
|
if not np.any(rows) or not np.any(cols):
|
|
print("Warning: Mask is empty or fully transparent, cannot calculate bbox.")
|
|
bbox = [0, 0, 0, 0]
|
|
else:
|
|
y_min, y_max = np.where(rows)[0][[0, -1]]
|
|
x_min, x_max = np.where(cols)[0][[0, -1]]
|
|
bbox = [int(x_min), int(y_min), int(x_max), int(y_max)]
|
|
|
|
print(f"Calculated combined BBox: {bbox}")
|
|
return (bbox,)
|
|
|
|
else:
|
|
from scipy import ndimage
|
|
|
|
# Label connected components
|
|
labeled_mask, num_components = ndimage.label(mask_np)
|
|
|
|
if num_components == 0:
|
|
print("Warning: Mask is empty or fully transparent, cannot calculate bbox.")
|
|
bbox_list = []
|
|
else:
|
|
bbox_list = []
|
|
|
|
for component_id in range(1, num_components + 1):
|
|
# Get mask for this component
|
|
component_mask = (labeled_mask == component_id)
|
|
|
|
# Find bounding box for this component
|
|
rows = np.any(component_mask, axis=1)
|
|
cols = np.any(component_mask, axis=0)
|
|
|
|
if np.any(rows) and np.any(cols):
|
|
y_min, y_max = np.where(rows)[0][[0, -1]]
|
|
x_min, x_max = np.where(cols)[0][[0, -1]]
|
|
# bbox = [int(x_min), int(y_min), int(x_max), int(y_max)]
|
|
# bbox = [int(x_min), int(y_min), int(x_max), int(y_max),""]
|
|
bbox = BBOX_imagutils(
|
|
x0=int(x_min),
|
|
y0=int(y_min),
|
|
x1=int(x_max),
|
|
y1=int(y_max),
|
|
img_w=mask_w,
|
|
img_h=mask_h,
|
|
label="", # No label for generic bbox
|
|
confidence=1.0 # Confidence is 1.0 for mask-based bboxes
|
|
)
|
|
bbox_list.append(bbox)
|
|
|
|
# Apply NMS to merge overlapping boxes
|
|
if bbox_list and nms_threshold < 1.0:
|
|
bbox_list = self.apply_nms(bbox_list, nms_threshold)
|
|
print(f"After NMS (threshold={nms_threshold}): {len(bbox_list)} BBoxes remain")
|
|
|
|
print(f"Final BBoxes: {bbox_list}")
|
|
|
|
return (bbox_list,)
|
|
|
|
class BBoxToMaskNode:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
# "image_shape": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 1, "tooltip": "Height and width of the output mask"}),
|
|
"merge_options": ("BOOLEAN", {"default": False, "tooltip": "If True, merge all bounding boxes into one mask. If False, create separate masks for each bbox."}),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("MASK",)
|
|
FUNCTION = "bbox_to_mask"
|
|
|
|
CATEGORY = "imgutils/bbox"
|
|
DESCRIPTION = "Converts bounding boxes to a binary mask."
|
|
|
|
def bbox_to_mask(self, bbox, merge_options=False, image_shape=512):
|
|
|
|
mask_list = []
|
|
bbox_input_list = []
|
|
if bbox is not None:
|
|
if isinstance(bbox, list) and len(bbox) > 0:
|
|
if isinstance(bbox[0], BBOX_imagutils):
|
|
bbox_input_list = [b.get_bbox() for b in bbox]
|
|
elif isinstance(bbox, BBOX_imagutils):
|
|
bbox_input_list = [bbox.get_bbox()]
|
|
|
|
if len(bbox_input_list) == 0:
|
|
print("Warning: No bounding boxes provided, returning empty mask.")
|
|
return (torch.zeros((1, image_shape, image_shape), dtype=torch.float32),)
|
|
|
|
mask_w, mask_h = image_shape, image_shape
|
|
|
|
if merge_options:
|
|
# make sure all boxes are in same image shape
|
|
img_w, img_h = bbox_input_list[0].img_w, bbox_input_list[0].img_h
|
|
for box in bbox_input_list:
|
|
if box.img_w != img_w or box.img_h != img_h:
|
|
|
|
print(f"Warning: Bounding boxes have different image shapes ({box.img_w}x{box.img_h}), using first box shape ({img_w}x{img_h}) for mask.")
|
|
break
|
|
mask_w, mask_h = img_w, img_h
|
|
|
|
for box in bbox_input_list:
|
|
x0, y0, x1, y1 = box.get_bbox()
|
|
# Ensure coordinates are within bounds
|
|
x0 = max(0, min(x0, mask_w - 1))
|
|
y0 = max(0, min(y0, mask_h - 1))
|
|
x1 = max(0, min(x1, mask_w - 1))
|
|
y1 = max(0, min(y1, mask_h - 1))
|
|
|
|
mask = torch.zeros((1, mask_h, mask_w), dtype=torch.float32)
|
|
mask[0, y0:y1, x0:x1] = 1.
|
|
mask_list.append(mask)
|
|
|
|
if merge_options:
|
|
# Merge all masks into one
|
|
merged_mask = torch.zeros((1, mask_h, mask_w), dtype=torch.float32)
|
|
for mask in mask_list:
|
|
merged_mask += mask
|
|
merged_mask = (merged_mask > 0).float()
|
|
|
|
return (merged_mask,)
|
|
else:
|
|
return (mask_list,)
|
|
|
|
class BBoxFilter:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"bboxes": (BBOX,),
|
|
"labels": ("STRING", {"default": "", "tooltip": "Comma-separated list of labels to filter by. Leave empty to keep all labels."}),
|
|
"include_labels": ("BOOLEAN", {"default": True, "tooltip": "If True, only keep bounding boxes with specified labels. If False, exclude these labels."}),
|
|
"min_area": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000000.0, "step": 1.0, "tooltip": "Minimum area of bounding boxes to keep"}),
|
|
"min_confidence": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Minimum confidence of bounding boxes to keep"}),
|
|
}
|
|
}
|
|
RETURN_TYPES = (BBOX,)
|
|
RETURN_NAMES = ("filtered_bboxes",)
|
|
FUNCTION = "filter_bboxes"
|
|
|
|
CATEGORY = "imgutils/bbox"
|
|
DESCRIPTION = "Filters bounding boxes based on area, confidence, and labels."
|
|
def filter_bboxes(self, bboxes,include_labels, min_area=0.0, min_confidence=0.0, labels=""):
|
|
|
|
filtered_bboxes = []
|
|
# Labels 应该是一个逗号分隔的字符串,如果 labels 为空或者仅有空格,则表示不过滤标签
|
|
if labels:
|
|
labels = [label.strip() for label in labels.split(",") if label.strip()]
|
|
else:
|
|
labels = None
|
|
|
|
for bbox in bboxes:
|
|
if isinstance(bbox, BBOX_imagutils):
|
|
area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0)
|
|
if area < min_area or bbox.confidence < min_confidence:
|
|
continue
|
|
if include_labels:
|
|
if labels and bbox.label not in labels:
|
|
continue
|
|
else: # Exclude labels
|
|
if labels and bbox.label in labels:
|
|
continue
|
|
|
|
filtered_bboxes.append(bbox)
|
|
else:
|
|
print(f"Warning: Skipping non-BBOX_imagutils object: {bbox}")
|
|
|
|
return (filtered_bboxes,)
|
|
|
|
|
|
# ComfyUI Node Registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"ImgutilsGenericDetector": ImgutilsGenericDetector,
|
|
"MaskToBBoxNode": MaskToBBoxNode,
|
|
"BBoxToMaskNode": BBoxToMaskNode,
|
|
"BBoxFilter": BBoxFilter,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"ImgutilsGenericDetector": "Imgutils Generic Detector",
|
|
"MaskToBBoxNode": "Mask to BBox",
|
|
"BBoxToMaskNode": "BBox to Mask",
|
|
"BBoxFilter": "BBox Filter",
|
|
}
|
|
|
|
|