update node

This commit is contained in:
rodrigoskohl
2024-08-26 17:23:22 -03:00
parent 6f5ba6e2c1
commit f7e080f7ee
6 changed files with 433 additions and 12 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__/
+8
View File
@@ -0,0 +1,8 @@
<div align="center">
# Panoramic Image Stitcher for ComfyUI
</div>
## Simple Node to make panoramic images using [OpenCV](github.com/opencv) stitch function
+1 -1
View File
@@ -1,4 +1,4 @@
from .nodes import ImageStitchingNode
from .nodes.nodes import ImageStitchingNode
NODE_CLASS_MAPPINGS = {
+130 -11
View File
@@ -1,28 +1,130 @@
import cv2
import torch
import torch.nn.functional as F
import numpy as np
from PIL import Image
def pil2tensor(image, device):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0).to(device)
def pil2tensor(image, device, rgb=True):
if rgb:
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
cv2pill = Image.fromarray(image)
return torch.from_numpy(np.array(cv2pill).astype(np.float32) / 255.0).unsqueeze(0).to(device)
def image_mask(image, device):
# Converte a imagem para escala de cinza
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Define um limiar baixo para detectar a borda preta
_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
# Remove pequenos pontos internos que não fazem parte da borda externa
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
# Opcional: Use uma máscara de área mínima para garantir que apenas a borda externa seja capturada
# Encontra contornos e filtra os pequenos contornos
cnts, _ = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mask = np.zeros_like(mask)
for c in cnts:
if cv2.contourArea(c) > 100: # Ajuste o valor conforme o tamanho da borda externa
cv2.drawContours(mask, [c], -1, 255, thickness=cv2.FILLED)
# Converte a máscara para tensor
mask_tensor = pil2tensor(mask, device, rgb=False)
return mask_tensor
def apply_mask(image_tensor, mask_tensor):
image_tensor = image_tensor.float()
# Redimensiona a máscara para combinar com a imagem, se necessário
if image_tensor.shape[2:] != mask_tensor.shape[2:]:
mask_tensor = F.interpolate(mask_tensor, size=image_tensor.shape[2:], mode='bilinear', align_corners=False)
# Expande a máscara para ter o mesmo número de canais da imagem
if mask_tensor.shape[1] != image_tensor.shape[1]:
mask_tensor = mask_tensor.repeat(1, image_tensor.shape[1], 1, 1)
# Aplica a máscara
masked_image = image_tensor * mask_tensor
print("Image tensor shape:", image_tensor.shape)
print("Mask tensor shape before resize:", mask_tensor.shape)
print("Mask tensor shape after resize:", mask_tensor.shape)
return masked_image
def remove_black_border(image):
image = cv2.copyMakeBorder(image, 10, 10, 10, 10, cv2.BORDER_CONSTANT, (0, 0, 0)) # Adiciona uma borda preta
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)[1]
kernel = np.ones((5, 5), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)
cnts,_ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
thresh = np.zeros_like(thresh)
for c in cnts:
if cv2.contourArea(c) > 100: # Ajuste o valor conforme o tamanho da borda externa
cv2.drawContours(thresh, [c], -1, 255, thickness=cv2.FILLED)
#cnts_h = imutils.grab_contours(cnts)
c = max(cnts, key=cv2.contourArea)
mask = np.zeros(thresh.shape, dtype="uint8")
(x, y, w, h) = cv2.boundingRect(c)
cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)
minRect = mask.copy()
sub = mask.copy()
while cv2.countNonZero(sub) > 0:
minRect = cv2.erode(minRect, None)
sub = cv2.subtract(minRect, thresh)
cnts,_ = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
#cnts = imutils.grab_contours(cnts)
# Se não houver contornos, retorna a imagem original
if not cnts:
return image
c = max(cnts, key=cv2.contourArea)
(x, y, w, h) = cv2.boundingRect(c)
image = image[y:y + h, x:x + w]
return image
class ImageStitchingNode:
CATEGORY = "🧩 Custom Nodes"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE", {"list": True}), # Especifica que espera uma lista de imagens
"device": ("STRING", {"default": "cuda:0"}), # Permite escolher a GPU
"device": (["cuda", "cpu"],), # Permite escolher a GPU
"crop": (["enable", "disable"],), # Permite escolher se deseja cortar a imagem
"mode": (["panoramic", "scans"],), # Permite escolher o modo de stitching
"threshold": ("FLOAT",{
"min": 0.0,
"max": 1.0,
"default": 1.0,
"step": 0.01,
"round": 0.001,
"display": "number",
}), # Permite escolher o limiar para a má
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("Panoramic Image",)
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("IMAGE", "MASK")
FUNCTION = "stitch_images"
CATEGORY = "🧩 Custom Nodes"
def stitch_images(self, images, device):
def stitch_images(self, images, device, crop, mode, threshold):
# Verifica se recebeu pelo menos duas imagens
if len(images) < 2:
raise ValueError("At least two images are required for stitching.")
@@ -37,15 +139,32 @@ class ImageStitchingNode:
np_images = [cv2.cvtColor(img, cv2.COLOR_RGB2BGR) for img in np_images]
# Cria o objeto Stitcher e realiza o stitching
stitcher = cv2.Stitcher_create()
if mode == 'panoramic':
stitcher = cv2.Stitcher_create(cv2.Stitcher_PANORAMA)
elif mode == 'scans':
stitcher = cv2.Stitcher_create(cv2.Stitcher_SCANS)
else:
raise ValueError("Invalid mode. Use 'PANORAMA' or 'SCANS'.")
stitcher.setPanoConfidenceThresh(threshold)
(status, pano) = stitcher.stitch(np_images)
# Verifica se o stitching foi bem-sucedido
if status != cv2.Stitcher_OK:
raise RuntimeError(f"Error when stitching: {status}")
# Corta a imagem para remover as bordas pretas usando a técnica de bounding box
if crop == "enable":
pano = remove_black_border(pano)
else:
pano_mask = np.ones(pano.shape[:2], dtype=np.uint8) * 255 # Máscara branca se não for cortar
pano_mask = image_mask(pano, device)
# Converte a imagem resultante para um tensor que o ComfyUI pode usar
pano_pil = Image.fromarray(cv2.cvtColor(pano, cv2.COLOR_BGR2RGB))
pano_tensor = pil2tensor(pano_pil, device)
pano_tensor = pil2tensor(pano, device)
return (pano_tensor, pano_mask)
return (pano_tensor,)
+293
View File
@@ -0,0 +1,293 @@
{
"last_node_id": 53,
"last_link_id": 79,
"nodes": [
{
"id": 43,
"type": "MaskToImage",
"pos": [
224,
192
],
"size": {
"0": 210,
"1": 26
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 79,
"slot_index": 0
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
53
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "MaskToImage"
}
},
{
"id": 39,
"type": "PreviewImage",
"pos": [
512,
288
],
"size": {
"0": 224,
"1": 256
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 53
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 30,
"type": "PreviewImage",
"pos": [
-64,
160
],
"size": {
"0": 224,
"1": 256
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 28
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 34,
"type": "PreviewImage",
"pos": [
480,
-224
],
"size": {
"0": 576,
"1": 448
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 78
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 13,
"type": "VHS_LoadImagesPath",
"pos": [
-512,
96
],
"size": [
352,
192
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "meta_batch",
"type": "VHS_BatchManager",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
28,
77
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
},
{
"name": "frame_count",
"type": "INT",
"links": [],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "VHS_LoadImagesPath"
},
"widgets_values": {
"directory": "C:\\ComfyUI_windows_portable\\ComfyUI\\input\\test",
"image_load_cap": 0,
"skip_first_images": 0,
"select_every_nth": 1,
"choose folder to upload": "image",
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"frame_load_cap": 0,
"skip_first_images": 0,
"filename": "C:\\ComfyUI_windows_portable\\ComfyUI\\input\\test",
"type": "path",
"format": "folder",
"select_every_nth": 1
}
}
}
},
{
"id": 49,
"type": "Image Stitching Node",
"pos": [
-128,
-32
],
"size": {
"0": 315,
"1": 150
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 77,
"slot_index": 0
}
],
"outputs": [
{
"name": "Panoramic Image",
"type": "IMAGE",
"links": [
78
],
"shape": 3,
"slot_index": 0
},
{
"name": "Mask",
"type": "MASK",
"links": [
79
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "Image Stitching Node"
},
"widgets_values": [
"cuda",
"enable",
"panoramic",
1
]
}
],
"links": [
[
28,
13,
0,
30,
0,
"IMAGE"
],
[
53,
43,
0,
39,
0,
"IMAGE"
],
[
77,
13,
0,
49,
0,
"IMAGE"
],
[
78,
49,
0,
34,
0,
"IMAGE"
],
[
79,
49,
1,
43,
0,
"MASK"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.7627768444385521,
"offset": [
532.7374172065774,
249.47383484481796
]
}
},
"version": 0.4
}