Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,25 @@ 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. 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.

Bounding boxes should be provided as `[x1, y1, x2, y2]` coordinates.

See `examples/smart_tile_example.json` for a minimal workflow using these nodes.

You can also draw bounding boxes interactively using `manual_crop_tool.py`:

```bash
python manual_crop_tool.py input.jpg output_dir --tile-size 512
```

Draw rectangles on the displayed image. Press `q` when finished. The script saves
all tiles to `output_dir` and writes `boxes.json` containing the coordinates.
Large selections are automatically split into smaller tiles using the specified
tile size.

---

## **Examples**
Expand All @@ -116,6 +135,9 @@ This node merges all tiled conditions into one and prepares them for building th
### **Latent Example**

![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 manual tiling and recombination.


---

Expand Down
85 changes: 85 additions & 0 deletions TTP_smart_tile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import torch
from PIL import Image
from typing import List, Tuple

from .TTP_toolsets import pil2tensor, tensor2pil


class TTP_Smart_Tile_Batch:
"""Manually crop image regions based on provided bounding boxes."""

@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"boxes": ("LIST",),
}
}

RETURN_TYPES = ("IMAGE", "LIST")
RETURN_NAMES = ("tiles", "positions")
FUNCTION = "tile_image"
CATEGORY = "TTP/Image"

def tile_image(self, image, boxes: List[Tuple[int, int, int, int]]):
pil_img = tensor2pil(image.squeeze(0))

tiles = []
positions = []
max_w = 0
max_h = 0
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)
positions.append((x1, y1, x2, y2))

padded = []
for tile in tiles:
_, h, w, _ = tile.shape
pad_w = max_w - w
pad_h = max_h - h
if pad_w or pad_h:
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.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 manually cropped tiles back onto the base image."""

@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base_image": ("IMAGE",),
"tiles": ("IMAGE", {"forceInput": True}),
"positions": ("LIST",),
}
}

RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "assemble_image"
CATEGORY = "TTP/Image"

def assemble_image(self, base_image, tiles, positions):
canvas = tensor2pil(base_image.squeeze(0)).copy()
for idx, tile in enumerate(tiles):
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)
19 changes: 16 additions & 3 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
from .TTP_toolsets import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS

__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
from .TTP_toolsets import NODE_CLASS_MAPPINGS as TOOLSET_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as TOOLSET_DISPLAY
from .TTP_smart_tile import TTP_Smart_Tile_Batch, TTP_Smart_Image_Assy

NODE_CLASS_MAPPINGS = {
**TOOLSET_MAPPINGS,
"TTP_Smart_Tile_Batch": TTP_Smart_Tile_Batch,
"TTP_Smart_Image_Assy": TTP_Smart_Image_Assy,
}

NODE_DISPLAY_NAME_MAPPINGS = {
**TOOLSET_DISPLAY,
"TTP_Smart_Tile_Batch": "TTP Smart Tile Batch",
"TTP_Smart_Image_Assy": "TTP Smart Image Assy",
}

__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
32 changes: 32 additions & 0 deletions examples/smart_tile_example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"last_node_id": 4,
"last_link_id": 4,
"nodes": [
{
"id": 1,
"type": "ImageInput",
"pos": [0, 0],
"size": [210, 70],
"widgets_values": ["input.png"]
},
{
"id": 2,
"type": "TTP_Smart_Tile_Batch",
"pos": [250, 0],
"size": [250, 120],
"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": []
}
],
"links": [
[1, 0, 2, 0],
[2, 0, 3, 1],
[2, 1, 3, 2]
]
}
84 changes: 84 additions & 0 deletions manual_crop_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import argparse
import json
import os
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.widgets import RectangleSelector


def split_box(box, tile_w, tile_h):
x1, y1, x2, y2 = box
boxes = []
for top in range(y1, y2, tile_h):
for left in range(x1, x2, tile_w):
boxes.append([
left,
top,
min(left + tile_w, x2),
min(top + tile_h, y2),
])
return boxes


def crop_tiles(img, boxes, out_dir):
tiles = []
for idx, b in enumerate(boxes):
tile = img.crop(b)
tiles.append(tile)
tile.save(os.path.join(out_dir, f"tile_{idx}.png"))
return tiles


def main():
parser = argparse.ArgumentParser(description="Draw rectangles on an image and export tiles")
parser.add_argument("image", help="Path to image")
parser.add_argument("output", help="Directory to save tiles")
parser.add_argument("--tile-size", type=int, default=512, help="Max tile size when splitting selections")
args = parser.parse_args()

os.makedirs(args.output, exist_ok=True)
img = Image.open(args.image).convert("RGB")

fig, ax = plt.subplots()
ax.imshow(img)
ax.set_title("Drag rectangles, press 'q' when done")

boxes = []

def onselect(eclick, erelease):
x1, y1 = int(eclick.xdata), int(eclick.ydata)
x2, y2 = int(erelease.xdata), int(erelease.ydata)
if x2 < x1:
x1, x2 = x2, x1
if y2 < y1:
y1, y2 = y2, y1
ax.add_patch(plt.Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, edgecolor='red'))
boxes.append([x1, y1, x2, y2])
fig.canvas.draw()

toggle_selector = RectangleSelector(ax, onselect, drawtype="box", useblit=True)

def on_key(event):
if event.key == 'q':
plt.close(event.canvas.figure)
fig.canvas.mpl_connect('key_press_event', on_key)
plt.show()

final_boxes = []
for b in boxes:
width = b[2] - b[0]
height = b[3] - b[1]
if width > args.tile_size or height > args.tile_size:
final_boxes.extend(split_box(b, args.tile_size, args.tile_size))
else:
final_boxes.append(b)

with open(os.path.join(args.output, "boxes.json"), "w", encoding="utf-8") as f:
json.dump(final_boxes, f)

crop_tiles(img, final_boxes, args.output)
print(f"Saved {len(final_boxes)} tiles to {args.output}")


if __name__ == "__main__":
main()
12 changes: 11 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +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.5"
version = "1.0.7"
license = {file = "LICENSE"}
dependencies = []

[project.urls]
Repository = "https://github.com/TTPlanetPig/Comfyui_TTP_Toolset"
Expand All @@ -12,3 +13,12 @@ Repository = "https://github.com/TTPlanetPig/Comfyui_TTP_Toolset"
PublisherId = "ttplanet"
DisplayName = "Comfyui_TTP_Toolset"
Icon = "🪐"

[build-system]
requires = ["setuptools>=66", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = ["Comfyui_TTP_Toolset"]
[tool.setuptools.package-dir]
Comfyui_TTP_Toolset = "."