| import numpy as np |
| import torch |
| import random |
| import math |
| import os |
| import json |
|
|
| from PIL import Image |
|
|
| from comfy.utils import common_upscale |
| from comfy_api.latest import io |
| import folder_paths |
| from nodes import MAX_RESOLUTION |
|
|
| from ..utility.utility import string_to_color, normalize_bboxes, bbox_to_bounding_box |
|
|
| def _upscale_mask(mask, width, height, method, crop): |
| if method == "lanczos": |
| return common_upscale(mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height, method, crop).movedim(1, -1)[:, :, :, 0] |
| return common_upscale(mask.unsqueeze(1), width, height, method, crop).squeeze(1) |
|
|
|
|
| def _resize_single_channel(tensor, width, height): |
| """Resize a 3D (B,H,W) tensor using bilinear interpolation.""" |
| return common_upscale(tensor.unsqueeze(1), width, height, "bilinear", "disabled").squeeze(1) |
|
|
|
|
| def _pad_inputs(): |
| """Shared pad_top/bottom/left/right input definitions for extra_padding options.""" |
| return [ |
| io.Int.Input("pad_top", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on top."), |
| io.Int.Input("pad_bottom", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on bottom."), |
| io.Int.Input("pad_left", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on left."), |
| io.Int.Input("pad_right", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on right."), |
| ] |
|
|
|
|
| def _apply_padding(tensor, pad_top, pad_bottom, pad_left, pad_right, mode, edge_mode="clamp", fill_rgb=None): |
| """Apply padding to a BHWC tensor. Returns the padded tensor. |
| mode: 'color' or 'edge' |
| edge_mode: 'clamp', 'repeat', 'mirror' (only used when mode='edge') |
| fill_rgb: list of [r, g, b] float values 0-1 (only used when mode='color') |
| """ |
| h, w = tensor.shape[1], tensor.shape[2] |
| new_h = h + pad_top + pad_bottom |
| new_w = w + pad_left + pad_right |
|
|
| if mode == "color": |
| fill = fill_rgb or [0.0, 0.0, 0.0] |
| padded = torch.zeros(tensor.shape[0], new_h, new_w, tensor.shape[3], device=tensor.device, dtype=tensor.dtype) |
| for c in range(min(3, tensor.shape[3])): |
| padded[:, :, :, c] = fill[c] |
| padded[:, pad_top:pad_top+h, pad_left:pad_left+w, :] = tensor |
| return padded |
|
|
| |
| if edge_mode == "clamp": |
| padded = torch.zeros(tensor.shape[0], new_h, new_w, tensor.shape[3], device=tensor.device, dtype=tensor.dtype) |
| padded[:, pad_top:pad_top+h, pad_left:pad_left+w, :] = tensor |
| if pad_top > 0: |
| padded[:, :pad_top, pad_left:pad_left+w, :] = tensor[:, 0:1, :, :].expand(-1, pad_top, -1, -1) |
| if pad_bottom > 0: |
| padded[:, pad_top+h:, pad_left:pad_left+w, :] = tensor[:, -1:, :, :].expand(-1, pad_bottom, -1, -1) |
| if pad_left > 0: |
| padded[:, :, :pad_left, :] = padded[:, :, pad_left:pad_left+1, :].expand(-1, -1, pad_left, -1) |
| if pad_right > 0: |
| padded[:, :, pad_left+w:, :] = padded[:, :, pad_left+w-1:pad_left+w, :].expand(-1, -1, pad_right, -1) |
| return padded |
| elif edge_mode == "repeat": |
| tiles_x = (new_w + w - 1) // w + 1 |
| tiles_y = (new_h + h - 1) // h + 1 |
| tiled = tensor.repeat(1, tiles_y, tiles_x, 1) |
| |
| off_x = (w - pad_left % w) % w |
| off_y = (h - pad_top % h) % h |
| return tiled[:, off_y:off_y+new_h, off_x:off_x+new_w, :] |
| elif edge_mode == "mirror": |
| flipped_h = tensor.flip(2) |
| flipped_v = tensor.flip(1) |
| flipped_hv = tensor.flip(1).flip(2) |
| mirror_block = torch.cat([ |
| torch.cat([tensor, flipped_h], dim=2), |
| torch.cat([flipped_v, flipped_hv], dim=2), |
| ], dim=1) |
| mb_h, mb_w = mirror_block.shape[1], mirror_block.shape[2] |
| tiles_x = (new_w + mb_w - 1) // mb_w + 1 |
| tiles_y = (new_h + mb_h - 1) // mb_h + 1 |
| tiled = mirror_block.repeat(1, tiles_y, tiles_x, 1) |
| |
| off_x = (mb_w - pad_left % mb_w) % mb_w |
| off_y = (mb_h - pad_top % mb_h) % mb_h |
| return tiled[:, off_y:off_y+new_h, off_x:off_x+new_w, :] |
| return tensor |
|
|
|
|
| class ImageTransformKJ(io.ComfyNode): |
| @classmethod |
| def define_schema(cls): |
| return io.Schema( |
| node_id="ImageTransformKJ", |
| display_name="Image Transform KJ", |
| category="KJNodes/image", |
| search_aliases=["resize", "crop", "pad", "upscale", "keep proportion", "bbox", "bounding box", "transform", "rotate", "mirror"], |
| is_experimental=True, |
| description=""" |
| Interactive image transform node: crop, resize, pad, and rotate. |
| Connect an image input — the preview appears automatically. |
| |
| Cropping: |
| Click + drag to draw a crop region. |
| Drag inside to move, drag edges/corners to resize. |
| Right-click to delete a region. |
| Ctrl to snap to grid. |
| Shift + resize to constrain aspect ratio. |
| Alt + resize to resize symmetrically. |
| |
| Padding: |
| Shift + drag to adjust padding position. |
| |
| Rotate button enables rotation cross (drag to rotate, right-click to reset). |
| Set target_width/height to resize output (0 = keep original). |
| Use keep_proportion to control how the image fits the target. |
| Use extra_padding to add padding with color or edge fill (clamp/repeat/mirror).""", |
| inputs=[ |
| io.MatchType.Input("image", io.MatchType.Template("img_or_mask", [io.Image, io.Mask]), tooltip="The image or mask to transform."), |
| io.Mask.Input("mask", optional=True, tooltip="Optional mask to transform alongside the image."), |
| io.Int.Input("target_width", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Target output width. 0 = keep original dimensions."), |
| io.Int.Input("target_height", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Target output height. 0 = keep original dimensions."), |
| io.Combo.Input("upscale_method", options=["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], default="lanczos", tooltip="Interpolation method for resizing."), |
| io.DynamicCombo.Input("keep_proportion", options=[ |
| io.DynamicCombo.Option(key="keep_long_edge", inputs=[]), |
| io.DynamicCombo.Option(key="keep_short_edge", inputs=[]), |
| io.DynamicCombo.Option(key="total_pixels", inputs=[]), |
| io.DynamicCombo.Option(key="stretch", inputs=[]), |
| io.DynamicCombo.Option(key="crop", inputs=[]), |
| io.DynamicCombo.Option(key="pad_color", inputs=[ |
| io.Float.Input("pad_x", default=0.5, min=0.0, max=1.0, step=0.01, |
| tooltip="Horizontal position of content within padding (0=left, 0.5=center, 1=right). Shift+drag content in preview to adjust."), |
| io.Float.Input("pad_y", default=0.5, min=0.0, max=1.0, step=0.01, |
| tooltip="Vertical position of content within padding (0=top, 0.5=center, 1=bottom). Shift+drag content in preview to adjust."), |
| ]), |
| io.DynamicCombo.Option(key="pad_edge", inputs=[ |
| io.Combo.Input("edge_mode", options=["clamp", "repeat", "mirror"], default="clamp", |
| tooltip="clamp: extend edge pixels. repeat: tile the image. mirror: tile with mirroring."), |
| io.Float.Input("pad_x", default=0.5, min=0.0, max=1.0, step=0.01, |
| tooltip="Horizontal position of content within padding (0=left, 0.5=center, 1=right). Shift+drag content in preview to adjust."), |
| io.Float.Input("pad_y", default=0.5, min=0.0, max=1.0, step=0.01, |
| tooltip="Vertical position of content within padding (0=top, 0.5=center, 1=bottom). Shift+drag content in preview to adjust."), |
| ]), |
| io.DynamicCombo.Option(key="multiplier", inputs=[ |
| io.Float.Input("width_mult", default=1.0, min=0.01, max=16.0, step=0.05, |
| tooltip="Multiply the crop width by this factor."), |
| io.Float.Input("height_mult", default=1.0, min=0.01, max=16.0, step=0.05, |
| tooltip="Multiply the crop height by this factor."), |
| ]), |
| ]), |
| io.Int.Input("divisible_by", default=2, min=0, max=512, step=1), |
| io.DynamicCombo.Input("extra_padding", options=[ |
| io.DynamicCombo.Option(key="disabled", inputs=[]), |
| io.DynamicCombo.Option(key="pad_color", inputs=_pad_inputs()), |
| io.DynamicCombo.Option(key="pad_edge", inputs=_pad_inputs() + [ |
| io.Combo.Input("edge_mode", options=["clamp", "repeat", "mirror"], default="clamp", |
| tooltip="clamp: extend edge pixels. repeat: tile the image. mirror: tile with mirroring."), |
| ]), |
| io.DynamicCombo.Option(key="pad_crop_color", inputs=_pad_inputs()), |
| io.DynamicCombo.Option(key="pad_crop_edge", inputs=_pad_inputs() + [ |
| io.Combo.Input("edge_mode", options=["clamp", "repeat", "mirror"], default="clamp", |
| tooltip="clamp: extend edge pixels. repeat: tile the image. mirror: tile with mirroring."), |
| ]), |
| ]), |
| io.DynamicCombo.Input("invert_crop", options=[ |
| io.DynamicCombo.Option(key="disabled", inputs=[]), |
| io.DynamicCombo.Option(key="enabled", inputs=[]), |
| ]), |
| io.String.Input("bboxes", default="", socketless=True, advanced=True), |
| ], |
| outputs=[ |
| io.MatchType.Output(io.MatchType.Template("img_or_mask", [io.Image, io.Mask]), id="cropped", display_name="output", is_output_list=True), |
| io.Mask.Output("cropped_mask", display_name="output_mask", is_output_list=True), |
| io.BBOX.Output("bbox", display_name="bbox", is_output_list=True), |
| io.Mask.Output("bbox_mask", display_name="bbox_mask", is_output_list=True), |
| io.Int.Output("width", display_name="width", tooltip="Width of the output image."), |
| io.Int.Output("height", display_name="height", tooltip="Height of the output image."), |
| ], |
| ) |
|
|
|
|
| @classmethod |
| def execute(cls, image, target_width, target_height, upscale_method, keep_proportion, divisible_by, |
| extra_padding, invert_crop, bboxes, mask=None): |
| |
| edge_mode = keep_proportion.get("edge_mode", "clamp") |
| pad_x = keep_proportion.get("pad_x", 0.5) |
| pad_y = keep_proportion.get("pad_y", 0.5) |
| width_mult = keep_proportion.get("width_mult", 1.0) |
| height_mult = keep_proportion.get("height_mult", 1.0) |
| keep_proportion = keep_proportion["keep_proportion"] |
| extra_top = extra_padding.get("pad_top", 0) |
| extra_bottom = extra_padding.get("pad_bottom", 0) |
| extra_left = extra_padding.get("pad_left", 0) |
| extra_right = extra_padding.get("pad_right", 0) |
| extra_edge_mode = extra_padding.get("edge_mode", "clamp") |
| extra_pad_mode = extra_padding.get("extra_padding", "disabled") |
| invert_crop = invert_crop["invert_crop"] |
|
|
|
|
| |
| fill_color_rgb = [0, 0, 0] |
| if bboxes: |
| try: |
| _parsed_tmp = json.loads(bboxes) |
| if isinstance(_parsed_tmp, dict) and "fillColor" in _parsed_tmp: |
| fill_color_rgb = string_to_color(_parsed_tmp["fillColor"]) |
| except (json.JSONDecodeError, Exception): |
| pass |
| fill_rgb = [c / 255.0 for c in fill_color_rgb[:3]] |
|
|
| |
| input_is_mask = image.ndim == 3 |
| if input_is_mask: |
| image = image.unsqueeze(-1).repeat(1, 1, 1, 3) |
|
|
| |
| temp_dir = folder_paths.get_temp_directory() |
| pil_img = Image.fromarray((image[0].cpu().numpy() * 255).astype(np.uint8)) |
| preview_filename = f"crop_preview_{random.randint(0, 0xFFFFFF):06x}.webp" |
| pil_img.save(os.path.join(temp_dir, preview_filename), format="WEBP", quality=80) |
| preview_ui = {"preview_filename": [preview_filename]} |
|
|
| img_height = image.shape[1] |
| img_width = image.shape[2] |
|
|
| |
| bbox_list = [] |
| rotation = 0.0 |
| if bboxes: |
| try: |
| parsed = json.loads(bboxes) |
| |
| if isinstance(parsed, dict): |
| bbox_list = [b for b in parsed.get("bboxes", []) if b and all(k in b for k in ("startX", "startY", "endX", "endY"))] |
| rotation = parsed.get("rotation", 0.0) |
| |
| elif isinstance(parsed, list): |
| bbox_list = [b for b in parsed if b and all(k in b for k in ("startX", "startY", "endX", "endY"))] |
| except json.JSONDecodeError: |
| pass |
|
|
| |
| content_mask = torch.ones(1, img_height, img_width, device=image.device) |
|
|
| |
| if rotation != 0: |
| from torchvision.transforms.functional import rotate as tv_rotate |
| import torch.nn.functional as F |
| |
| rot_fill = fill_rgb |
|
|
| is_edge_mode = extra_pad_mode in ("pad_edge", "pad_crop_edge") or keep_proportion == "pad_edge" |
| if is_edge_mode: |
| h, w = image.shape[1], image.shape[2] |
| pad_amt = max(h, w) |
| img_chw = image.movedim(-1, 1) |
| img_padded = F.pad(img_chw, [pad_amt, pad_amt, pad_amt, pad_amt], mode='replicate') |
| img_rotated = tv_rotate(img_padded, -rotation, expand=False, fill=rot_fill) |
| ch, cw = img_rotated.shape[2], img_rotated.shape[3] |
| cy, cx = ch // 2, cw // 2 |
| image = img_rotated[:, :, cy - h // 2:cy - h // 2 + h, cx - w // 2:cx - w // 2 + w].movedim(1, -1) |
| if mask is not None: |
| mask_padded = F.pad(mask.unsqueeze(1), [pad_amt, pad_amt, pad_amt, pad_amt], mode='replicate') |
| mask_rotated = tv_rotate(mask_padded, -rotation, expand=False, fill=[0.0]) |
| mask = mask_rotated[:, :, cy - h // 2:cy - h // 2 + h, cx - w // 2:cx - w // 2 + w].squeeze(1) |
| |
| cm_padded = F.pad(content_mask.unsqueeze(1), [pad_amt, pad_amt, pad_amt, pad_amt], mode='constant', value=0) |
| cm_rotated = tv_rotate(cm_padded, -rotation, expand=False, fill=[0.0]) |
| content_mask = cm_rotated[:, :, cy - h // 2:cy - h // 2 + h, cx - w // 2:cx - w // 2 + w].squeeze(1) |
| else: |
| image = tv_rotate(image.movedim(-1, 1), -rotation, expand=True, fill=rot_fill).movedim(1, -1) |
| if mask is not None: |
| mask = tv_rotate(mask.unsqueeze(1), -rotation, expand=True, fill=[0.0]).squeeze(1) |
| |
| content_mask = tv_rotate(content_mask.unsqueeze(1), -rotation, expand=True, fill=[0.0]).squeeze(1) |
| img_height = image.shape[1] |
| img_width = image.shape[2] |
|
|
| |
| if mask is not None: |
| if mask.shape[-2] != img_height or mask.shape[-1] != img_width: |
| if mask.shape[-2] == img_width and mask.shape[-1] == img_height: |
| mask = mask.transpose(-2, -1) |
| else: |
| mask = _resize_single_channel(mask, img_width, img_height) |
|
|
| |
| |
| is_pad_first = extra_pad_mode in ("pad_color", "pad_edge") |
| kp_is_pad_mode = keep_proportion in ("pad_color", "pad_edge") |
| if is_pad_first and not kp_is_pad_mode and (extra_top > 0 or extra_bottom > 0 or extra_left > 0 or extra_right > 0): |
| pad_mode = "color" if extra_pad_mode == "pad_color" else "edge" |
| padded_img = _apply_padding(image, extra_top, extra_bottom, extra_left, extra_right, pad_mode, extra_edge_mode, fill_rgb) |
|
|
| image = padded_img |
| img_height = image.shape[1] |
| img_width = image.shape[2] |
| |
| cm_new = torch.zeros(1, img_height, img_width, device=content_mask.device) |
| cm_new[:, extra_top:extra_top+content_mask.shape[1], extra_left:extra_left+content_mask.shape[2]] = content_mask |
| content_mask = cm_new |
| if mask is not None: |
| m_new = torch.zeros(mask.shape[0], img_height, img_width, device=mask.device, dtype=mask.dtype) |
| m_new[:, extra_top:extra_top+mask.shape[1], extra_left:extra_left+mask.shape[2]] = mask |
| mask = m_new |
|
|
| |
| if not bbox_list: |
| bbox_list = [None] |
|
|
| all_cropped = [] |
| all_cropped_masks = [] |
| all_bbox_tuples = [] |
| all_bbox_masks = [] |
|
|
| for bbox_data in bbox_list: |
| has_bbox = bbox_data is not None |
|
|
| if has_bbox: |
| preview_width = bbox_data.get("previewWidth", 0) |
| preview_height = bbox_data.get("previewHeight", 0) |
| sx = img_width / preview_width if preview_width > 0 else 1.0 |
| sy = img_height / preview_height if preview_height > 0 else 1.0 |
|
|
| x_min = int(min(bbox_data["startX"], bbox_data["endX"]) * sx) |
| y_min = int(min(bbox_data["startY"], bbox_data["endY"]) * sy) |
| x_max = int(max(bbox_data["startX"], bbox_data["endX"]) * sx) |
| y_max = int(max(bbox_data["startY"], bbox_data["endY"]) * sy) |
|
|
| x_min = max(0, min(x_min, img_width - 1)) |
| y_min = max(0, min(y_min, img_height - 1)) |
| x_max = max(x_min + 1, min(x_max, img_width)) |
| y_max = max(y_min + 1, min(y_max, img_height)) |
|
|
| cropped = image[:, y_min:y_max, x_min:x_max, :] |
| cropped_content_mask = content_mask[:, y_min:y_max, x_min:x_max] |
| all_bbox_tuples.append((x_min, y_min, x_max - x_min, y_max - y_min)) |
| bm = torch.zeros(1, img_height, img_width) |
| bm[0, y_min:y_max, x_min:x_max] = 1.0 |
| all_bbox_masks.append(bm) |
| cropped_mask = mask[:, y_min:y_max, x_min:x_max] if mask is not None else None |
| else: |
| cropped = image |
| cropped_content_mask = content_mask |
| all_bbox_tuples.append((0, 0, img_width, img_height)) |
| all_bbox_masks.append(torch.ones(1, img_height, img_width)) |
| cropped_mask = mask |
| x_min, y_min, x_max, y_max = 0, 0, img_width, img_height |
|
|
| |
| if keep_proportion == "multiplier": |
| crop_h, crop_w = cropped.shape[1], cropped.shape[2] |
| tw = round(crop_w * width_mult) |
| th = round(crop_h * height_mult) |
| target_width = tw |
| target_height = th |
|
|
| |
| if target_width > 0 or target_height > 0: |
| crop_h, crop_w = cropped.shape[1], cropped.shape[2] |
| tw = target_width if target_width > 0 else crop_w |
| th = target_height if target_height > 0 else crop_h |
|
|
| |
| |
| |
| has_extra = extra_top > 0 or extra_bottom > 0 or extra_left > 0 or extra_right > 0 |
| kp_is_pad = keep_proportion in ("pad_color", "pad_edge") |
| if has_extra and (kp_is_pad or not is_pad_first): |
| if target_width > 0: |
| tw = max(1, tw - extra_left - extra_right) |
| if target_height > 0: |
| th = max(1, th - extra_top - extra_bottom) |
|
|
| if keep_proportion == "keep_long_edge": |
| ratio = min(tw / crop_w, th / crop_h) |
| tw = round(crop_w * ratio) |
| th = round(crop_h * ratio) |
| elif keep_proportion == "keep_short_edge": |
| ratio = max(tw / crop_w, th / crop_h) |
| tw = round(crop_w * ratio) |
| th = round(crop_h * ratio) |
| elif keep_proportion == "total_pixels": |
| total_pixels = tw * th |
| aspect_ratio = crop_w / crop_h |
| th = int(math.sqrt(total_pixels / aspect_ratio)) |
| tw = int(math.sqrt(total_pixels * aspect_ratio)) |
| elif keep_proportion == "crop": |
| ratio = max(tw / crop_w, th / crop_h) |
| scale_w = round(crop_w * ratio) |
| scale_h = round(crop_h * ratio) |
| samples = common_upscale(cropped.movedim(-1, 1), scale_w, scale_h, upscale_method, "center") |
| cropped = samples.movedim(1, -1) |
| if cropped_mask is not None: |
| cropped_mask = _upscale_mask(cropped_mask, scale_w, scale_h, upscale_method, "center") |
| cropped_content_mask = _resize_single_channel(cropped_content_mask, scale_w, scale_h) |
| cx = (scale_w - tw) // 2 |
| cy = (scale_h - th) // 2 |
| cropped = cropped[:, cy:cy+th, cx:cx+tw, :] |
| if cropped_mask is not None: |
| cropped_mask = cropped_mask[:, cy:cy+th, cx:cx+tw] |
| cropped_content_mask = cropped_content_mask[:, cy:cy+th, cx:cx+tw] |
| elif keep_proportion in ("pad_color", "pad_edge"): |
| ratio = min(tw / crop_w, th / crop_h) |
| scale_w = round(crop_w * ratio) |
| scale_h = round(crop_h * ratio) |
| samples = common_upscale(cropped.movedim(-1, 1), scale_w, scale_h, upscale_method, "disabled") |
| resized = samples.movedim(1, -1) |
| |
| full_tw = target_width if target_width > 0 else crop_w |
| full_th = target_height if target_height > 0 else crop_h |
| pad_left = round((full_tw - scale_w) * pad_x) |
| pad_top = round((full_th - scale_h) * pad_y) |
| pad_right = full_tw - pad_left - scale_w |
| pad_bottom = full_th - pad_top - scale_h |
| tw = full_tw |
| th = full_th |
|
|
| pad_mode = "edge" if keep_proportion == "pad_edge" else "color" |
| cropped = _apply_padding(resized, pad_top, pad_bottom, pad_left, pad_right, pad_mode, edge_mode, fill_rgb) |
| if cropped_mask is not None: |
| mask_resized = _upscale_mask(cropped_mask, scale_w, scale_h, upscale_method, "disabled") |
| mask_padded = torch.zeros(mask_resized.shape[0], th, tw, device=mask_resized.device, dtype=mask_resized.dtype) |
| mask_padded[:, pad_top:pad_top+scale_h, pad_left:pad_left+scale_w] = mask_resized |
| cropped_mask = mask_padded |
| |
| cm_resized = _resize_single_channel(cropped_content_mask, scale_w, scale_h) |
| cm_padded = torch.zeros(1, th, tw, device=cropped_content_mask.device) |
| cm_padded[:, pad_top:pad_top+scale_h, pad_left:pad_left+scale_w] = cm_resized |
| cropped_content_mask = cm_padded |
|
|
| if divisible_by > 1: |
| tw = tw - (tw % divisible_by) |
| th = th - (th % divisible_by) |
|
|
| if tw > 0 and th > 0: |
| if keep_proportion in ("stretch", "keep_long_edge", "keep_short_edge", "total_pixels", "multiplier"): |
| cropped = common_upscale(cropped.movedim(-1, 1), tw, th, upscale_method, "disabled").movedim(1, -1) |
| if cropped_mask is not None: |
| cropped_mask = _upscale_mask(cropped_mask, tw, th, upscale_method, "disabled") |
| cropped_content_mask = _resize_single_channel(cropped_content_mask, tw, th) |
| else: |
| cropped = cropped[:, :th, :tw, :] |
| if cropped_mask is not None: |
| cropped_mask = cropped_mask[:, :th, :tw] |
| cropped_content_mask = cropped_content_mask[:, :th, :tw] |
|
|
| |
| elif divisible_by > 1: |
| final_w = cropped.shape[2] - (cropped.shape[2] % divisible_by) |
| final_h = cropped.shape[1] - (cropped.shape[1] % divisible_by) |
| if final_w != cropped.shape[2] or final_h != cropped.shape[1]: |
| cropped = cropped[:, :final_h, :final_w, :] |
| if cropped_mask is not None: |
| cropped_mask = cropped_mask[:, :final_h, :final_w] |
| cropped_content_mask = cropped_content_mask[:, :final_h, :final_w] |
|
|
| |
| kp_handles_ep = keep_proportion in ("pad_color", "pad_edge") |
| if not is_pad_first and not kp_handles_ep and (extra_top > 0 or extra_bottom > 0 or extra_left > 0 or extra_right > 0): |
| h_cur, w_cur = cropped.shape[1], cropped.shape[2] |
| pad_mode = "edge" if extra_pad_mode == "pad_crop_edge" else "color" |
| cropped = _apply_padding(cropped, extra_top, extra_bottom, extra_left, extra_right, pad_mode, extra_edge_mode, fill_rgb) |
| new_h, new_w = cropped.shape[1], cropped.shape[2] |
| if cropped_mask is not None: |
| padded_mask = torch.zeros(cropped_mask.shape[0], new_h, new_w, device=cropped_mask.device, dtype=cropped_mask.dtype) |
| padded_mask[:, extra_top:extra_top+h_cur, extra_left:extra_left+w_cur] = cropped_mask |
| cropped_mask = padded_mask |
| cm_h, cm_w = cropped_content_mask.shape[-2], cropped_content_mask.shape[-1] |
| if cm_h != h_cur or cm_w != w_cur: |
| cropped_content_mask = _resize_single_channel(cropped_content_mask, w_cur, h_cur) |
| cm_ep = torch.zeros(1, new_h, new_w, device=cropped_content_mask.device) |
| cm_ep[:, extra_top:extra_top+h_cur, extra_left:extra_left+w_cur] = cropped_content_mask |
| cropped_content_mask = cm_ep |
|
|
| |
| if cropped_mask is None: |
| cropped_mask = torch.zeros(1, cropped.shape[1], cropped.shape[2]) |
|
|
| |
| |
| if cropped_content_mask is not None: |
| out_h, out_w = cropped_mask.shape[1], cropped_mask.shape[2] |
| cm_h, cm_w = cropped_content_mask.shape[1], cropped_content_mask.shape[2] |
| if cm_h != out_h or cm_w != out_w: |
| cropped_content_mask = _resize_single_channel(cropped_content_mask, out_w, out_h) |
| |
| fill_mask = 1.0 - cropped_content_mask.clamp(0, 1) |
| |
| cropped_mask = torch.max(cropped_mask, fill_mask) |
|
|
| |
| if invert_crop == "enabled" and has_bbox: |
| inverted = image.clone() |
| for c in range(min(3, inverted.shape[3])): |
| inverted[:, y_min:y_max, x_min:x_max, c] = fill_rgb[c] |
| cropped = inverted |
|
|
| |
| if input_is_mask: |
| cropped = cropped[:, :, :, 0] |
|
|
| all_cropped.append(cropped) |
| all_cropped_masks.append(cropped_mask) |
|
|
| width, height = all_cropped[0].shape[2], all_cropped[0].shape[1] |
|
|
| return io.NodeOutput(all_cropped, all_cropped_masks, all_bbox_tuples, all_bbox_masks, width, height, ui=preview_ui) |
|
|
|
|
| class BBOXToBoundingBoxKJ(io.ComfyNode): |
| @classmethod |
| def define_schema(cls): |
| return io.Schema( |
| node_id="BBOXToBoundingBoxKJ", |
| display_name="BBOX to Bounding Box KJ", |
| category="KJNodes/image", |
| search_aliases=["bbox", "bounding box", "convert", "crop region"], |
| description="Convert a KJNodes BBOX to the native BOUNDING_BOX dict used by core nodes such as Crop Image.", |
| inputs=[ |
| io.BBOX.Input("bbox", tooltip="BBOX from any KJNodes node. Single boxes and lists of boxes are both accepted."), |
| io.Combo.Input("bbox_format", options=["xywh", "xyxy"], default="xywh", |
| tooltip="Layout of the incoming values."), |
| io.Int.Input("index", default=0, min=0, max=4096, |
| tooltip="When the input holds multiple boxes, pick which one to output. Clamped to the last box."), |
| ], |
| outputs=[ |
| io.BoundingBox.Output("bounding_box"), |
| ], |
| ) |
|
|
| @classmethod |
| def execute(cls, bbox, bbox_format, index) -> io.NodeOutput: |
| boxes = normalize_bboxes(bbox, bbox_format) |
| return io.NodeOutput(bbox_to_bounding_box(boxes[min(index, len(boxes) - 1)], bbox_format)) |
|
|