Compare commits
10
Commits
levind_abhi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ee4d01da6 | ||
|
|
21ee6a4f71 | ||
|
|
48024f3901 | ||
|
|
2a4e6731d2 | ||
|
|
42cc6d54bd | ||
|
|
75ba9bfb25 | ||
|
|
c0817a1882 | ||
|
|
18b8c18c5e | ||
|
|
c9ba9c9c59 | ||
|
|
67aee87652 |
+22
-3
@@ -20,6 +20,8 @@ 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 (
|
||||
@@ -1930,7 +1932,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'))
|
||||
json.dump(pose_dict, open(save_file_path, 'w'), indent=4)
|
||||
np_result = cv2.resize(np_result, (W, H),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
out_image_list.append(
|
||||
@@ -3694,8 +3696,11 @@ 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
|
||||
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 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
|
||||
@@ -3759,14 +3764,22 @@ 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 = "4.9.0"
|
||||
VERSION = "5.1.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,
|
||||
@@ -3830,8 +3843,14 @@ 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,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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,)
|
||||
@@ -0,0 +1,68 @@
|
||||
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"
|
||||
# }
|
||||
@@ -0,0 +1,117 @@
|
||||
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
@@ -818,3 +818,110 @@ 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,)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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"
|
||||
# }
|
||||
Reference in New Issue
Block a user