Files

171 lines
5.1 KiB
Python

import hashlib
import json
import os
import numpy as np
import torch
from PIL import Image, ImageOps, ImageSequence
import comfy.model_management
import folder_paths
import node_helpers
ASPECT_RATIOS = [
"free",
"1:1",
"4:3",
"3:4",
"3:2",
"2:3",
"16:9",
"9:16",
"21:9",
]
def _ratio_value(aspect_ratio):
if aspect_ratio == "free" or ":" not in aspect_ratio:
return None
left, right = aspect_ratio.split(":", 1)
try:
return float(left) / float(right)
except (TypeError, ValueError, ZeroDivisionError):
return None
def _load_image_from_input(image):
image_path = folder_paths.get_annotated_filepath(image)
dtype = comfy.model_management.intermediate_dtype()
device = comfy.model_management.intermediate_device()
img = node_helpers.pillow(Image.open, image_path)
output_images = []
width = None
height = None
for frame in ImageSequence.Iterator(img):
frame = node_helpers.pillow(ImageOps.exif_transpose, frame).convert("RGB")
if width is None:
width, height = frame.size
if frame.size != (width, height):
continue
array = np.array(frame).astype(np.float32) / 255.0
output_images.append(torch.from_numpy(array)[None,].to(dtype=dtype))
if not output_images:
raise ValueError(f"No image frames could be loaded from {image}")
return torch.cat(output_images, dim=0).to(device=device, dtype=dtype)
def _normalize_crop(x, y, width, height, image_width, image_height, aspect_ratio, aspect_lock):
image_width = max(1, int(image_width))
image_height = max(1, int(image_height))
x = int(round(x))
y = int(round(y))
width = int(round(width))
height = int(round(height))
width = max(1, width)
height = max(1, height)
ratio = _ratio_value(aspect_ratio) if aspect_lock else None
if ratio:
width = max(1, width)
height = max(1, int(round(width / ratio)))
if height > image_height:
height = image_height
width = max(1, int(round(height * ratio)))
if width > image_width:
width = image_width
height = max(1, int(round(width / ratio)))
width = max(1, min(width, image_width))
height = max(1, min(height, image_height))
x = max(0, min(x, image_width - width))
y = max(0, min(y, image_height - height))
return x, y, width, height
class VisualImageCrop:
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
files = folder_paths.filter_files_content_types(files, ["image"])
return {
"required": {
"image": (sorted(files), {"image_upload": True}),
"x": ("INT", {"default": 0, "min": 0, "max": 65535, "step": 1}),
"y": ("INT", {"default": 0, "min": 0, "max": 65535, "step": 1}),
"width": ("INT", {"default": 1, "min": 1, "max": 65535, "step": 1}),
"height": ("INT", {"default": 1, "min": 1, "max": 65535, "step": 1}),
"aspect_lock": ("BOOLEAN", {"default": False}),
"aspect_ratio": (ASPECT_RATIOS, {"default": "free"}),
},
}
CATEGORY = "image/crop"
RETURN_TYPES = ("IMAGE", "IMAGE", "CROP_METADATA", "STRING", "INT", "INT", "INT", "INT")
RETURN_NAMES = ("uncropped_out", "cropped_out", "crop_metadata", "crop_metadata_json", "x", "y", "width", "height")
FUNCTION = "crop"
def crop(self, image, x, y, width, height, aspect_lock, aspect_ratio):
source = _load_image_from_input(image)
image_height = source.shape[1]
image_width = source.shape[2]
x, y, width, height = _normalize_crop(
x,
y,
width,
height,
image_width,
image_height,
aspect_ratio,
aspect_lock,
)
cropped = source[:, y : y + height, x : x + width, :]
metadata = {
"x": x,
"y": y,
"width": width,
"height": height,
"source_width": int(image_width),
"source_height": int(image_height),
"aspect_lock": bool(aspect_lock),
"aspect_ratio": aspect_ratio,
"source": "image_picker",
}
return (source, cropped, metadata, json.dumps(metadata), x, y, width, height)
@classmethod
def IS_CHANGED(cls, image, x, y, width, height, aspect_lock, aspect_ratio):
image_path = folder_paths.get_annotated_filepath(image)
digest = hashlib.sha256()
with open(image_path, "rb") as handle:
digest.update(handle.read())
return (digest.hexdigest(), x, y, width, height, aspect_lock, aspect_ratio)
@classmethod
def VALIDATE_INPUTS(cls, image, **kwargs):
if image and folder_paths.exists_annotated_filepath(image):
return True
return f"Invalid image file: {image}"
NODE_CLASS_MAPPINGS = {
"VisualImageCrop": VisualImageCrop,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"VisualImageCrop": "Visual Image Crop",
}