1 Commits
Author SHA1 Message Date
Ubuntu a57e48c814 levind_abhi 2025-05-23 10:17:25 +00:00
9 changed files with 86 additions and 681 deletions
+30 -37
View File
@@ -20,8 +20,6 @@ from utility_nodes import TRI3D_extract_facer_mask
from .AEMatter import (load_AEMatter_Model, run_AEMatter_inference)
from .light_layer import main_light_layer
from .remove_small_mask_islands import TRI3D_RemoveSmallMaskIslands
from .mask_area_percentage import TRI3D_MaskAreaPercentage
from .image_stack import (
@@ -240,7 +238,7 @@ class TRI3DLEVINDABHICLOTHSEGBATCH:
},
}
RETURN_TYPES = ("IMAGE", )
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE")
FUNCTION = "main"
CATEGORY = "TRI3D"
@@ -294,21 +292,33 @@ class TRI3DLEVINDABHICLOTHSEGBATCH:
# Collect and return the results
batch_results = []
mask0_batch = []
mask1_batch = []
mask2_batch = []
for i in range(images.shape[0]):
cv2_segm = cv2.imread(LSEG_OUTPUT_PATH + f'image{i}.png', cv2.IMREAD_UNCHANGED) # Read PNG with alpha channel
cv2_segm = cv2.cvtColor(cv2_segm, cv2.COLOR_BGRA2RGBA) # Convert from BGRA to RGBA
b_tensor_img = cv2_img_to_tensor(cv2_segm)
batch_results.append(b_tensor_img.squeeze(0))
batch_results = torch.stack(batch_results)
return (batch_results, )
mask0_path = os.path.join(LSEG_OUTPUT_PATH, f"{i}__mask0.png")
mask1_path = os.path.join(LSEG_OUTPUT_PATH, f"{i}__mask1.png")
mask2_path = os.path.join(LSEG_OUTPUT_PATH, f"{i}__mask2.png")
mask0_img = cv2.imread(mask0_path, cv2.IMREAD_UNCHANGED)
mask1_img = cv2.imread(mask1_path, cv2.IMREAD_UNCHANGED)
mask2_img = cv2.imread(mask2_path, cv2.IMREAD_UNCHANGED)
# Ensure single channel, convert to 3 channel if needed for consistency
if mask0_img is not None and len(mask0_img.shape) == 2:
mask0_img = cv2.cvtColor(mask0_img, cv2.COLOR_GRAY2RGB)
if mask1_img is not None and len(mask1_img.shape) == 2:
mask1_img = cv2.cvtColor(mask1_img, cv2.COLOR_GRAY2RGB)
if mask2_img is not None and len(mask2_img.shape) == 2:
mask2_img = cv2.cvtColor(mask2_img, cv2.COLOR_GRAY2RGB)
mask0_tensor = cv2_img_to_tensor(mask0_img).squeeze(0)
mask1_tensor = cv2_img_to_tensor(mask1_img).squeeze(0)
mask2_tensor = cv2_img_to_tensor(mask2_img).squeeze(0)
mask0_batch.append(mask0_tensor)
mask1_batch.append(mask1_tensor)
mask2_batch.append(mask2_tensor)
mask0_batch = torch.stack(mask0_batch)
mask1_batch = torch.stack(mask1_batch)
mask2_batch = torch.stack(mask2_batch)
return (mask0_batch, mask1_batch, mask2_batch)
@@ -1932,7 +1942,7 @@ class TRI3DDWPose_Preprocessor:
cur_file_dir = os.path.dirname(os.path.realpath(__file__))
save_file_path = os.path.join(cur_file_dir,
filename_path)
json.dump(pose_dict, open(save_file_path, 'w'), indent=4)
json.dump(pose_dict, open(save_file_path, 'w'))
np_result = cv2.resize(np_result, (W, H),
interpolation=cv2.INTER_AREA)
out_image_list.append(
@@ -3696,11 +3706,8 @@ class TRI3D_BGREMOVE_MEGA():
from photoroom import TRI3D_photoroom_bgremove_api
from smart_box import TRI3D_SmartBox, TRI3D_Skip_HeadMask, TRI3D_Skip_HeadMask_AddNeck, TRI3D_Image_extend, TRI3D_Smart_Depth, TRI3D_NarrowfyImage, TRI3D_Skip_LipMask
from smart_box import TRI3D_SmartBox, TRI3D_Skip_HeadMask, TRI3D_Skip_HeadMask_AddNeck, TRI3D_Image_extend, TRI3D_Smart_Depth, TRI3D_NarrowfyImage
from nsfw import TRI3DNSFWFilter
from cut_by_mask_aspect_ratio import TRI3D_CutByMaskAspectRatio
from string_check import TRI3D_StringContains
from .dwpose_conversion import SaveFlattenedPoseKpsAsJsonFile
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
@@ -3764,22 +3771,14 @@ NODE_CLASS_MAPPINGS = {
"tri3d_SmartBox": TRI3D_SmartBox,
"tri3d_Skip_HeadMask": TRI3D_Skip_HeadMask,
"tri3d_Skip_HeadMask_AddNeck": TRI3D_Skip_HeadMask_AddNeck,
"tri3d_Skip_LipMask": TRI3D_Skip_LipMask,
"tri3d_Image_extend": TRI3D_Image_extend,
"tri3d_Smart_Depth": TRI3D_Smart_Depth,
"tri3d_NSFWFilter": TRI3DNSFWFilter,
"tri3d_NarrowfyImage": TRI3D_NarrowfyImage,
"tri3d_Skip_LipMask": TRI3D_Skip_LipMask,
"tri3d_Remove_Small_Mask_Islands": TRI3D_RemoveSmallMaskIslands,
"tri3d_CutByMaskAspectRatio": TRI3D_CutByMaskAspectRatio,
"tri3d_StringContains": TRI3D_StringContains,
"tri3d_MaskAreaPercentage": TRI3D_MaskAreaPercentage,
"tri3d_SaveFlattenedPoseKpsAsJsonFile": SaveFlattenedPoseKpsAsJsonFile,
}
VERSION = "5.1.0"
VERSION = "4.9.0"
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"tri3d-photoroom-bgremove-api": "Photoroom BG Remove" + " v" + VERSION,
@@ -3843,14 +3842,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"tri3d_SmartBox": "Smart Box" + " v" + VERSION,
"tri3d_Skip_HeadMask": "Skip Head Mask" + " v" + VERSION,
"tri3d_Skip_HeadMask_AddNeck": "Skip Head Mask and add neck" + " v" + VERSION,
"tri3d_Skip_LipMask": "Skip Lip Mask" + " v" + VERSION,
"tri3d_NSFWFilter": "TRI3D NSFW Filter" + " v" + VERSION,
"tri3d_Image_extend": "Image extend" + " v" + VERSION,
"tri3d_Smart_Depth": "Smart Depth" + " v" + VERSION,
"tri3d_NarrowfyImage": "Narrowfy Image" + " v" + VERSION,
"tri3d_Remove_Small_Mask_Islands": "Remove Small Mask Islands" + " v" + VERSION,
"tri3d_CutByMaskAspectRatio": "Cut by mask aspect ratio" + " v" + VERSION,
"tri3d_StringContains": "String contains" + " v" + VERSION,
"tri3d_MaskAreaPercentage": "Mask Area Percentage" + " v" + VERSION,
"tri3d_SaveFlattenedPoseKpsAsJsonFile": "Save Flattened Pose Keypoints as JSON File" + " v" + VERSION,
}
+24 -6
View File
@@ -14,16 +14,34 @@ def initialize_and_load_models():
net = initialize_and_load_models()
def run(img):
def run(img, image_id, output_dir):
palette = get_palette(4)
cloth_seg = generate_mask(img, net=net,device=device)
return cloth_seg
mask0, mask1, mask2, cloth_seg = generate_mask(img, net=net, device=device, image_id=image_id, output_dir=output_dir)
return mask0, mask1, mask2, cloth_seg
INPUT_PATH = "./input/"
OUTPUT_PATH = "./output/"
import os
for cur_image in os.listdir(INPUT_PATH):
mask0_paths = []
mask1_paths = []
mask2_paths = []
cloth_paths = []
for idx, cur_image in enumerate(os.listdir(INPUT_PATH)):
img = PIL.Image.open(INPUT_PATH + cur_image)
cloth_seg = run(img)
cloth_seg.save(OUTPUT_PATH + cur_image, format="PNG")
mask0, mask1, mask2, cloth_seg = run(img, image_id=idx, output_dir=OUTPUT_PATH)
# Save masks and cloth_seg with unique names (already saved in generate_mask)
mask0_path = os.path.join(OUTPUT_PATH, f"{idx}__mask0.png")
mask1_path = os.path.join(OUTPUT_PATH, f"{idx}__mask1.png")
mask2_path = os.path.join(OUTPUT_PATH, f"{idx}__mask2.png")
cloth_path = os.path.join(OUTPUT_PATH, f"{idx}__extracted_garment.png")
mask0_paths.append(mask0_path)
mask1_paths.append(mask1_path)
mask2_paths.append(mask2_path)
cloth_paths.append(cloth_path)
print("Mask0 batch:", mask0_paths)
print("Mask1 batch:", mask1_paths)
print("Mask2 batch:", mask2_paths)
print("Garment batch:", cloth_paths)
+32 -10
View File
@@ -101,14 +101,16 @@ def apply_transform(img):
from PIL import Image
def generate_mask(input_image, net, device='cpu'):
def generate_mask(input_image, net, device='cpu', image_id=None, output_dir=None):
img = input_image
img_size = img.size
img = img.resize((768, 768), Image.BICUBIC)
image_tensor = apply_transform(img)
image_tensor = torch.unsqueeze(image_tensor, 0)
output_dir = os.path.join(opt.output, 'extracted_garment')
# Allow output_dir override for batch processing
if output_dir is None:
output_dir = os.path.join(opt.output, 'extracted_garment')
os.makedirs(output_dir, exist_ok=True)
with torch.no_grad():
@@ -118,30 +120,50 @@ def generate_mask(input_image, net, device='cpu'):
output_tensor = torch.squeeze(output_tensor, dim=0)
output_arr = output_tensor.cpu().numpy()
# Create and save individual masks for classes 1, 2, 3
classes_of_interest = [1, 2, 3]
mask_imgs = []
for idx, cls in enumerate(classes_of_interest):
mask = np.zeros_like(output_arr, dtype=np.uint8)
mask[output_arr == cls] = 255
if mask.ndim > 2:
mask = mask.squeeze()
if mask.ndim != 2:
raise ValueError(f"mask{idx} must be a 2-dimensional array")
mask_img = Image.fromarray(mask, mode='L').resize(img_size, Image.BICUBIC)
# Save with unique name if image_id is provided
if image_id is not None:
mask_path = os.path.join(output_dir, f'{image_id}__mask{idx}.png')
else:
mask_path = os.path.join(output_dir, f'mask{idx}.png')
mask_img.save(mask_path, format="PNG")
print(f"Saved mask{idx} at: {mask_path}")
mask_imgs.append(mask_img)
# Create a binary mask where selected classes are 1, others are 0
binary_mask = np.zeros_like(output_arr, dtype=np.uint8)
classes_of_interest = [1, 2, 3] # Modify this list according to your classes of interest
for cls in classes_of_interest:
binary_mask[output_arr == cls] = 255
# Ensure binary_mask is 2D
if binary_mask.ndim > 2:
binary_mask = binary_mask.squeeze() # Removes single-dimensional entries from the shape
binary_mask = binary_mask.squeeze()
if binary_mask.ndim != 2:
raise ValueError("binary_mask must be a 2-dimensional array")
binary_mask_img = Image.fromarray(binary_mask, mode='L').resize(img_size, Image.BICUBIC)
# Create an RGBA image for the output
extracted_garment = Image.new("RGBA", img_size)
original_img = img.resize(img_size) # Resize the processed image back to original size
original_img = img.resize(img_size)
extracted_garment.paste(original_img, mask=binary_mask_img)
# Save the garment image with transparency
garment_path = os.path.join(output_dir, 'extracted_garment.png')
if image_id is not None:
garment_path = os.path.join(output_dir, f'{image_id}__extracted_garment.png')
else:
garment_path = os.path.join(output_dir, 'extracted_garment.png')
extracted_garment.save(garment_path, format="PNG")
print(f"Saved extracted garment at: {garment_path}")
return extracted_garment
return (*mask_imgs, extracted_garment)
# def generate_mask(input_image, net, device='cpu'):
# img = input_image
-183
View File
@@ -1,183 +0,0 @@
import os
import cv2
import numpy as np
import torch
class TRI3D_CutByMaskAspectRatio:
"""
ComfyUI node that crops an image based on a mask's bounding box,
adjusts the aspect ratio, and resizes to specified dimensions.
"""
def from_torch_image(self, image):
"""Convert a torch tensor image to numpy array for OpenCV processing"""
image = image.cpu().numpy() * 255.0
image = np.clip(image, 0, 255).astype(np.uint8)
return image
def to_torch_image(self, image):
"""Convert numpy array back to torch tensor format"""
image = image.astype(dtype=np.float32)
image /= 255.0
image = torch.from_numpy(image)
return image
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"mask": ("IMAGE",),
"margin": ("INT", {"default": 10, "min": 0, "max": 100, "step": 1}),
"target_width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
"target_height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
"padding_color": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
},
}
FUNCTION = "run"
RETURN_TYPES = ("IMAGE",)
CATEGORY = "TRI3D"
def run(self, image, mask, margin, target_width, target_height, padding_color=255):
# Convert Torch images to OpenCV format
cv_image = self.from_torch_image(image)
cv_mask = self.from_torch_image(mask)
# Remove batch dimension if present
if len(cv_image.shape) == 4:
cv_image = cv_image[0]
if len(cv_mask.shape) == 4:
cv_mask = cv_mask[0]
# Convert mask to grayscale if it's not already
if len(cv_mask.shape) == 3 and cv_mask.shape[2] > 1:
mask_gray = cv2.cvtColor(cv_mask, cv2.COLOR_RGB2GRAY)
else:
mask_gray = cv_mask[:, :, 0]
# Create binary mask
_, binary_mask = cv2.threshold(mask_gray, 127, 255, cv2.THRESH_BINARY)
# Find contours in the binary mask
contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
# If no contours found, return the original image
print("No contours found in mask. Returning original image.")
return (image,)
# Find bounding box around all contours
x_min, y_min = float('inf'), float('inf')
x_max, y_max = 0, 0
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
x_min = min(x_min, x)
y_min = min(y_min, y)
x_max = max(x_max, x + w)
y_max = max(y_max, y + h)
# Add margin to bounding box
x_min = max(0, x_min - margin)
y_min = max(0, y_min - margin)
x_max = min(cv_image.shape[1], x_max + margin)
y_max = min(cv_image.shape[0], y_max + margin)
# Current dimensions of the bounding box
height = y_max - y_min
width = x_max - x_min
# Calculate the target aspect ratio (width/height)
target_aspect_ratio = target_width / target_height
# Calculate current aspect ratio
current_aspect_ratio = width / height
# Adjust width to match the target aspect ratio while keeping height constant
if current_aspect_ratio < target_aspect_ratio:
# Current width is too narrow - need to extend it
# Calculate the required width for the target aspect ratio
required_width = int(height * target_aspect_ratio)
width_difference = required_width - width
# Calculate how much to extend on each side
left_extend = width_difference // 2
right_extend = width_difference - left_extend
# Calculate new potential boundaries
new_x_min = x_min - left_extend
new_x_max = x_max + right_extend
# Check if the new boundaries are within the original image
left_padding_needed = abs(min(0, new_x_min))
right_padding_needed = max(0, new_x_max - cv_image.shape[1])
# Adjust boundaries to be within the original image
new_x_min = max(0, new_x_min)
new_x_max = min(cv_image.shape[1], new_x_max)
# Get the portion of the original image within valid boundaries
extended_image = cv_image[y_min:y_max, new_x_min:new_x_max]
# If we need padding (i.e., extension goes beyond image boundaries)
if left_padding_needed > 0 or right_padding_needed > 0:
# Create canvas with padding color
num_channels = extended_image.shape[2] if len(extended_image.shape) == 3 else 1
if num_channels == 1:
canvas = np.full((height, required_width), padding_color, dtype=np.uint8)
else:
canvas = np.full((height, required_width, num_channels), padding_color, dtype=np.uint8)
# Calculate the position to place the extended image
place_x = left_padding_needed
# Place the extended image on the canvas
if num_channels == 1:
canvas[:, place_x:place_x+extended_image.shape[1]] = extended_image
else:
canvas[:, place_x:place_x+extended_image.shape[1], :] = extended_image
# Use the canvas as our cropped image
cropped_image = canvas
else:
# No padding needed, use the extended image
cropped_image = extended_image
elif current_aspect_ratio > target_aspect_ratio:
# Current width is too wide, crop it
new_width = int(height * target_aspect_ratio)
width_difference = width - new_width
# Crop equally from both sides if possible
left_crop = width_difference // 2
right_crop = width_difference - left_crop
# Apply the crop
cropped_image = cv_image[y_min:y_max, x_min+left_crop:x_max-right_crop]
else:
# Aspect ratio is already correct
cropped_image = cv_image[y_min:y_max, x_min:x_max]
# Resize the cropped/padded image to the target dimensions using Lanczos interpolation
resized_image = cv2.resize(cropped_image, (target_width, target_height), interpolation=cv2.INTER_LANCZOS4)
# Convert back to torch format
torch_image = self.to_torch_image(resized_image)
# Add batch dimension back
torch_image = torch_image.unsqueeze(0)
return (torch_image,)
# Node registration for ComfyUI
NODE_CLASS_MAPPINGS = {
"TRI3D_CutByMaskAspectRatio": TRI3D_CutByMaskAspectRatio
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TRI3D_CutByMaskAspectRatio": "TRI3D Cut By Mask Aspect Ratio"
}
-111
View File
@@ -1,111 +0,0 @@
import os
import json
import torch
import numpy as np
import folder_paths
print("Loading TRI3D_SavePoseKeypointsJSON module")
class SaveFlattenedPoseKpsAsJsonFile:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pose_kps": ("POSE_KEYPOINT",),
"file_path": ("STRING", {"default": "dwpose/keypoints/input.json"})
}
}
RETURN_TYPES = (
"STRING",
)
FUNCTION = "save_flattened_pose_kps"
OUTPUT_NODE = True
CATEGORY = "ControlNet Preprocessors/Pose Keypoint Postprocess"
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = ""
def _flatten_openpose_dict(self, pose_dict: dict) -> dict:
"""
Converts a single OpenPose dictionary into flattened format.
"""
# Get canvas dimensions from the input dictionary
H = pose_dict.get('canvas_height', 512)
W = pose_dict.get('canvas_width', 512)
flat_keypoints = []
# Check if any person was detected
if not pose_dict.get('people'):
# If no people, return a list of 130 invalid keypoints
flat_keypoints.extend([[-1, -1]] * 130)
return {"height": H, "width": W, "keypoints": flat_keypoints}
person = pose_dict['people'][0] # Process the first person found
# Helper function to process each body part
def process_part(keypoints_data, expected_length):
processed_kps = []
if keypoints_data:
# Iterate in steps of 3 (x, y, confidence)
for i in range(0, len(keypoints_data), 3):
x, y, conf = keypoints_data[i], keypoints_data[i+1], keypoints_data[i+2]
# Use confidence score to check for validity. If 0, it's a missing point.
if conf > 0:
processed_kps.append([x, y])
else:
processed_kps.append([-1, -1])
# Ensure the list has the exact expected length
while len(processed_kps) < expected_length:
processed_kps.append([-1, -1])
return processed_kps
# Process parts in order: body -> face -> left hand -> right hand
body_kps = process_part(person.get('pose_keypoints_2d'), 18)
face_kps = process_part(person.get('face_keypoints_2d'), 70)
left_hand_kps = process_part(person.get('hand_left_keypoints_2d'), 21)
right_hand_kps = process_part(person.get('hand_right_keypoints_2d'), 21)
# Combine all parts into the final flat list
flat_keypoints.extend(body_kps)
flat_keypoints.extend(face_kps)
flat_keypoints.extend(left_hand_kps)
flat_keypoints.extend(right_hand_kps)
return {"height": H, "width": W, "keypoints": flat_keypoints}
def save_flattened_pose_kps(self, pose_kps, file_path):
# filename_prefix += self.prefix_append
# # Get the save path using the first pose keypoint's dimensions
# full_output_folder, filename, counter, subfolder, filename_prefix = \
# folder_paths.get_save_image_path(filename_prefix, self.output_dir,
# pose_kps[0]["canvas_width"],
# pose_kps[0]["canvas_height"])
# Process each pose keypoint in the batch
flattened_poses = []
for pose_dict in pose_kps:
flattened_data = self._flatten_openpose_dict(pose_dict)
flattened_poses.append(flattened_data)
# # Save the flattened data
# file = f"{filename}_{counter:05}.json"
# save_path = os.path.join(full_output_folder, file)
cur_file_dir = os.path.dirname(os.path.realpath(__file__))
save_path = os.path.join(cur_file_dir,
file_path)
with open(save_path, 'w') as f:
if len(flattened_poses) == 1:
json.dump(flattened_poses[0], f, indent=4) # Save single pose directly
else:
json.dump(flattened_poses, f, indent=4) # Save batch as array
print(f"Saved flattened pose keypoints to: {save_path}")
return (save_path,)
-68
View File
@@ -1,68 +0,0 @@
import os
import cv2
import numpy as np
import torch
class TRI3D_MaskAreaPercentage:
"""
ComfyUI node that calculates the percentage of white pixels in an image
relative to the total image area.
"""
def from_torch_image(self, image):
"""Convert a torch tensor image to numpy array for OpenCV processing"""
image = image.cpu().numpy() * 255.0
image = np.clip(image, 0, 255).astype(np.uint8)
return image
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"threshold": ("INT", {"default": 127, "min": 0, "max": 255, "step": 1}),
},
}
FUNCTION = "run"
RETURN_TYPES = ("FLOAT", "INT", "INT",)
RETURN_NAMES = ("percentage", "white_pixels", "total_pixels",)
CATEGORY = "TRI3D"
def run(self, image, threshold=127):
# Convert Torch image to OpenCV format
cv_image = self.from_torch_image(image)
# Remove batch dimension if present
if len(cv_image.shape) == 4:
cv_image = cv_image[0]
# Convert to grayscale if it's a color image
if len(cv_image.shape) == 3 and cv_image.shape[2] > 1:
gray_image = cv2.cvtColor(cv_image, cv2.COLOR_RGB2GRAY)
else:
gray_image = cv_image[:, :, 0]
# Calculate total number of pixels
total_pixels = gray_image.shape[0] * gray_image.shape[1]
# Count white pixels (pixels with values above threshold)
_, binary_image = cv2.threshold(gray_image, threshold, 255, cv2.THRESH_BINARY)
white_pixels = cv2.countNonZero(binary_image)
# Calculate percentage of white pixels
percentage = (white_pixels / total_pixels) * 100.0
return (percentage, white_pixels, total_pixels,)
# # Node registration for ComfyUI
# NODE_CLASS_MAPPINGS = {
# "TRI3D_MaskAreaPercentage": TRI3D_MaskAreaPercentage
# }
# NODE_DISPLAY_NAME_MAPPINGS = {
# "TRI3D_MaskAreaPercentage": "TRI3D Mask Area Percentage"
# }
-117
View File
@@ -1,117 +0,0 @@
import os
import cv2
import numpy as np
import torch
class TRI3D_RemoveSmallMaskIslands:
"""
ComfyUI node that removes small islands of white pixels from a mask image
based on a specified area threshold.
"""
def from_torch_image(self, image):
"""Convert a torch tensor image to numpy array for OpenCV processing"""
image = image.cpu().numpy() * 255.0
image = np.clip(image, 0, 255).astype(np.uint8)
return image
def to_torch_image(self, image):
"""Convert numpy array back to torch tensor format"""
image = image.astype(dtype=np.float32)
image /= 255.0
image = torch.from_numpy(image)
return image
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE", ),
"min_island_area": ("INT", {"default": 100, "min": 1, "max": 10000, "step": 10}),
"invert": ("BOOLEAN", {"default": False}),
},
}
FUNCTION = "run"
RETURN_TYPES = ("IMAGE", )
CATEGORY = "TRI3D"
def run(self, image, min_island_area, invert):
# Convert Torch image to OpenCV format
cv_image = self.from_torch_image(image)
# Remove batch dimension if present
if len(cv_image.shape) == 4:
cv_image = cv_image[0]
# Make a copy to work with
result_image = cv_image.copy()
# Process each channel (if grayscale, it will just be one iteration)
height, width = cv_image.shape[:2]
# If the image has 3 channels (RGB), convert to grayscale for contour detection
if len(cv_image.shape) == 3 and cv_image.shape[2] == 3:
# Convert to grayscale for processing
gray = cv2.cvtColor(cv_image, cv2.COLOR_RGB2GRAY)
else:
# Use the first channel if it's already grayscale or has alpha
gray = cv_image[:, :, 0]
# Invert if needed (to work with black islands instead of white)
if invert:
gray = 255 - gray
# Create binary image
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# Find contours in the binary image
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Create a blank mask for the cleaned image
clean_mask = np.zeros((height, width), dtype=np.uint8)
# Draw only contours with area greater than the threshold
for contour in contours:
area = cv2.contourArea(contour)
if area >= min_island_area:
cv2.drawContours(clean_mask, [contour], 0, 255, -1)
# Invert back if needed
if invert:
clean_mask = 255 - clean_mask
# Apply the clean mask to each channel of the original image
if len(cv_image.shape) == 3 and cv_image.shape[2] == 3:
# RGB image
for i in range(3):
result_image[:, :, i] = cv2.bitwise_and(cv_image[:, :, i], clean_mask)
elif len(cv_image.shape) == 3 and cv_image.shape[2] == 4:
# RGBA image
for i in range(4):
result_image[:, :, i] = cv2.bitwise_and(cv_image[:, :, i], clean_mask)
else:
# Single channel image
result_image = cv2.bitwise_and(cv_image, clean_mask)
# Reshape to match expected dimensions
result_image = result_image.reshape(height, width, 1)
# Convert back to torch format
torch_image = self.to_torch_image(result_image)
# Add batch dimension back
torch_image = torch_image.unsqueeze(0)
return (torch_image,)
# # Node registration for ComfyUI
# NODE_CLASS_MAPPINGS = {
# "TRI3D_RemoveSmallMaskIslands": TRI3D_RemoveSmallMaskIslands
# }
# NODE_DISPLAY_NAME_MAPPINGS = {
# "TRI3D_RemoveSmallMaskIslands": "TRI3D Remove Small Mask Islands"
# }
-107
View File
@@ -818,110 +818,3 @@ class TRI3D_CropAndExtend:
torch_human_mask = self.to_torch_image(cropped_human_mask).unsqueeze(0)
return (torch_garment, torch_garment_mask, torch_human, torch_human_mask, cropped_width, cropped_height)
class TRI3D_Skip_LipMask:
def adjust_keypoints(self, keypoints, image_shape, original_height, original_width):
image_height, image_width = image_shape[:2]
scale_x = image_width / original_width
scale_y = image_height / original_height
adjusted_keypoints = [
(int(x * scale_x), int(y * scale_y)) for x, y in keypoints
]
return adjusted_keypoints
def from_torch_image(self, image):
image = image.cpu().numpy() * 255.0
image = np.clip(image, 0, 255).astype(np.uint8)
return image
def to_torch_image(self, image):
image = image.astype(dtype=np.float32)
image /= 255.0
image = torch.from_numpy(image)
return image
def extract_lip_keypoints(self, keypoints):
# In DWPose, lips are typically keypoints in face area
# Assuming standard face keypoint format where lips are around indices 61-68
# This may need adjustment based on your specific keypoint format
lip_indices = range(61, 69) # Adjust these indices based on your keypoint format
# Filter out invalid keypoints (those with negative confidence or coordinates)
lip_keypoints = []
for idx in lip_indices:
if idx < len(keypoints):
x, y = keypoints[idx]
if x >= 0 and y >= 0: # Check for valid coordinates
lip_keypoints.append((x, y))
return lip_keypoints
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE", ),
"keypoints_json": ("STRING", {"multiline": True}),
},
}
FUNCTION = "run"
RETURN_TYPES = ("IMAGE", )
CATEGORY = "TRI3D"
def run(self, image, keypoints_json):
# Convert Torch image to OpenCV format
cv_image = self.from_torch_image(image)
# Remove the batch dimension if present
if len(cv_image.shape) == 4:
cv_image = cv_image[0]
# Make a copy of the original image
result_image = cv_image.copy()
# Parse keypoints JSON
try:
kp_data = json.loads(open(keypoints_json, 'r').read())
original_height, original_width = kp_data['height'], kp_data['width']
keypoints = kp_data['keypoints']
# Extract lip keypoints
lip_keypoints = self.extract_lip_keypoints(keypoints)
# If no valid lip keypoints found, use a fallback approach
if not lip_keypoints:
# Fallback: use the nose point (index 0) as reference
nose_point = keypoints[0]
if nose_point[1] > 0: # If y-coordinate is valid
# Estimate lip position slightly below nose
lip_y = int(nose_point[1] + 0.15 * cv_image.shape[0])
lowest_y = lip_y
else:
# If no valid reference point, use 1/3 of the image height
lowest_y = cv_image.shape[0] // 3
else:
# Find the lowest y-coordinate among lip keypoints
adjusted_lip_keypoints = self.adjust_keypoints(lip_keypoints, cv_image.shape, original_height, original_width)
lowest_y = max([kp[1] for kp in adjusted_lip_keypoints])
# Black out everything above the lowest lip point
result_image[:lowest_y, :] = 0
except Exception as e:
print(f"Error processing keypoints JSON: {e}")
# In case of error, return the original image
result_image = cv_image
# Convert back to Torch format
torch_image = self.to_torch_image(result_image)
# Add the batch dimension back
torch_image = torch_image.unsqueeze(0)
return (torch_image,)
-42
View File
@@ -1,42 +0,0 @@
import os
class TRI3D_StringContains:
"""
ComfyUI node that checks if a specified string exists within another string.
Performs case-insensitive comparison by converting all text to lowercase.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_string": ("STRING", {"multiline": True}),
"search_string": ("STRING", {"default": "", "multiline": False}),
},
}
FUNCTION = "run"
RETURN_TYPES = ("BOOLEAN",)
CATEGORY = "TRI3D"
def run(self, input_string, search_string):
# Convert both strings to lowercase for case-insensitive comparison
input_lower = input_string.lower()
search_lower = search_string.lower()
# Check if search string exists in input string
contains = search_lower in input_lower
return (contains,)
# # Node registration for ComfyUI
# NODE_CLASS_MAPPINGS = {
# "TRI3D_StringContains": TRI3D_StringContains
# }
# NODE_DISPLAY_NAME_MAPPINGS = {
# "TRI3D_StringContains": "TRI3D String Contains"
# }