diff --git a/README.md b/README.md index cc5b108..21c1111 100644 --- a/README.md +++ b/README.md @@ -106,16 +106,10 @@ This node merges all tiled conditions into one and prepares them for building th ![Condition Merge Node](https://github.com/user-attachments/assets/3039c8a3-8284-4b71-a9de-4120723258c7) --- -### **7. Smart Tile Nodes** -These nodes allow object-aware tiling using a YOLO detection or segmentation model. Use **TTP Smart Tile Batch** to crop detected regions and **TTP Smart Image Assy** to paste the edited tiles back. +### **7. Manual Tile Nodes** +Use **TTP Smart Tile Batch** to crop arbitrary regions defined by a list of bounding boxes. After processing the tiles with any other ComfyUI nodes, **TTP Smart Image Assy** pastes them back in place. -`TTP Smart Tile Batch` now accepts an optional *padding* value to expand bounding boxes before cropping. - -The nodes depend on the `ultralytics` package: - -```bash -pip install ultralytics -``` +Bounding boxes should be provided as `[x1, y1, x2, y2]` coordinates. See `examples/smart_tile_example.json` for a minimal workflow using these nodes. @@ -131,7 +125,7 @@ See `examples/smart_tile_example.json` for a minimal workflow using these nodes. ![Latent Example Workflow](https://github.com/TTPlanetPig/Comfyui_TTP_Toolset/blob/main/examples/Flux_8Mega_Pixel_image_upscale_process.png) ### **Smart Tile Example** -`examples/smart_tile_example.json` demonstrates object-aware tiling and recombination. +`examples/smart_tile_example.json` demonstrates manual tiling and recombination. --- diff --git a/TTP_smart_tile.py b/TTP_smart_tile.py index 8d07b67..8b04ac6 100644 --- a/TTP_smart_tile.py +++ b/TTP_smart_tile.py @@ -1,81 +1,47 @@ import torch -import numpy as np from PIL import Image from typing import List, Tuple from .TTP_toolsets import pil2tensor, tensor2pil -try: - from ultralytics import YOLO -except Exception: # pragma: no cover - ultralytics optional - YOLO = None - class TTP_Smart_Tile_Batch: - """Split image into smart tiles using detection or segmentation.""" + """Manually crop image regions based on provided bounding boxes.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE",), - "model_path": ("STRING", {"default": "yolov8n-seg.pt"}), - }, - "optional": { - "task": (["detect", "segment"], {"default": "detect"}), - "conf": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 1.0}), - "iou": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0}), - "padding": ("INT", {"default": 0, "min": 0, "max": 256}), - }, + "boxes": ("LIST",), + } } - RETURN_TYPES = ("IMAGE", "LIST", "LIST") - RETURN_NAMES = ("tiles", "infos", "ids") + RETURN_TYPES = ("IMAGE", "LIST") + RETURN_NAMES = ("tiles", "positions") FUNCTION = "tile_image" CATEGORY = "TTP/Image" - def tile_image(self, image, model_path="yolov8n-seg.pt", task="detect", conf=0.25, iou=0.45, padding=0): - if YOLO is None: - raise RuntimeError("ultralytics package is required for smart tiling") - + def tile_image(self, image, boxes: List[Tuple[int, int, int, int]]): pil_img = tensor2pil(image.squeeze(0)) - model = YOLO(model_path) - results = model.predict(pil_img, conf=conf, iou=iou, verbose=False) - result = results[0] - tiles: List[torch.Tensor] = [] - infos: List = [] - ids: List[int] = [] + tiles = [] + positions = [] max_w = 0 max_h = 0 - - boxes = result.boxes.xyxy.cpu().numpy().astype(int) - classes = result.boxes.cls.cpu().numpy().astype(int) - masks = None - if task == "segment" and result.masks is not None: - masks = result.masks.data.cpu().numpy() - - width, height = pil_img.size - for idx, box in enumerate(boxes): - x1, y1, x2, y2 = box.tolist() - x1 = max(0, x1 - padding) - y1 = max(0, y1 - padding) - x2 = min(width, x2 + padding) - y2 = min(height, y2 + padding) - tile = pil_img.crop((x1, y1, x2, y2)) - tile_tensor = pil2tensor(tile) + for box in boxes: + if len(box) != 4: + raise ValueError(f"Each box must contain 4 values, got {box}") + x1, y1, x2, y2 = map(int, box) + crop = pil_img.crop((x1, y1, x2, y2)) + tile_tensor = pil2tensor(crop) _, h, w, _ = tile_tensor.shape max_h = max(max_h, h) max_w = max(max_w, w) tiles.append(tile_tensor) - if masks is not None: - mask = masks[idx][y1:y2, x1:x2] - infos.append((x1, y1, x2, y2, torch.from_numpy(mask).unsqueeze(0))) - else: - infos.append((x1, y1, x2, y2)) - ids.append(int(classes[idx])) + positions.append((x1, y1, x2, y2)) - padded_tiles = [] + padded = [] for tile in tiles: _, h, w, _ = tile.shape pad_w = max_w - w @@ -84,13 +50,14 @@ class TTP_Smart_Tile_Batch: tile_chw = tile.permute(0, 3, 1, 2) tile_chw = torch.nn.functional.pad(tile_chw, (0, pad_w, 0, pad_h)) tile = tile_chw.permute(0, 2, 3, 1) - padded_tiles.append(tile) - tiles_tensor = torch.cat(padded_tiles, dim=0) if padded_tiles else torch.empty((0, max_h, max_w, 3)) - return tiles_tensor, infos, ids + padded.append(tile) + + tiles_tensor = torch.cat(padded, dim=0) if padded else torch.empty((0, max_h, max_w, 3)) + return tiles_tensor, positions class TTP_Smart_Image_Assy: - """Reassemble edited smart tiles back to the original image.""" + """Reassemble manually cropped tiles back onto the base image.""" @classmethod def INPUT_TYPES(cls): @@ -98,8 +65,7 @@ class TTP_Smart_Image_Assy: "required": { "base_image": ("IMAGE",), "tiles": ("IMAGE", {"forceInput": True}), - "infos": ("LIST",), - "task": (["detect", "segment"], {"default": "detect"}), + "positions": ("LIST",), } } @@ -108,20 +74,12 @@ class TTP_Smart_Image_Assy: FUNCTION = "assemble_image" CATEGORY = "TTP/Image" - def assemble_image(self, base_image, tiles, infos, task="detect"): + def assemble_image(self, base_image, tiles, positions): canvas = tensor2pil(base_image.squeeze(0)).copy() for idx, tile in enumerate(tiles): - tile_img = tensor2pil(tile.unsqueeze(0)) - info = infos[idx] - if task == "segment": - x1, y1, x2, y2, mask_tensor = info - width, height = x2 - x1, y2 - y1 - tile_crop = tile_img.crop((0, 0, width, height)) - mask = tensor2pil(mask_tensor) - canvas.paste(tile_crop, (x1, y1, x2, y2), mask) - else: - x1, y1, x2, y2 = info - width, height = x2 - x1, y2 - y1 - tile_crop = tile_img.crop((0, 0, width, height)) - canvas.paste(tile_crop, (x1, y1, x2, y2)) + x1, y1, x2, y2 = positions[idx] + width = x2 - x1 + height = y2 - y1 + tile_img = tensor2pil(tile.unsqueeze(0)).crop((0, 0, width, height)) + canvas.paste(tile_img, (x1, y1, x2, y2)) return pil2tensor(canvas) diff --git a/examples/smart_tile_example.json b/examples/smart_tile_example.json index 2a03839..c47bd64 100644 --- a/examples/smart_tile_example.json +++ b/examples/smart_tile_example.json @@ -14,14 +14,14 @@ "type": "TTP_Smart_Tile_Batch", "pos": [250, 0], "size": [250, 120], - "widgets_values": ["yolov8n-seg.pt", "detect", 0.25, 0.45, 10] + "widgets_values": [[ [10, 10, 110, 110], [130, 130, 220, 220] ]] }, { "id": 3, "type": "TTP_Smart_Image_Assy", "pos": [500, 0], "size": [250, 120], - "widgets_values": ["detect"] + "widgets_values": [] } ], "links": [ diff --git a/pyproject.toml b/pyproject.toml index 5f48d16..18410a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "comfyui_ttp_toolset" description = "This is a workflow for my simple logic amazing upscale node for DIT model. it can be common use for Flux,Hunyuan,SD3 It can simple tile the initial image into pieces and then use image-interrogator to get each tile prompts for more accurate upscale process. The condition will be properly handled and the hallucination will be significantly eliminated." -version = "1.0.6" +version = "1.0.7" license = {file = "LICENSE"} -dependencies = ["ultralytics>=8.1"] +dependencies = [] [project.urls] Repository = "https://github.com/TTPlanetPig/Comfyui_TTP_Toolset"