746 lines
26 KiB
Python
746 lines
26 KiB
Python
"""Shared image helpers for 1hewNodes.
|
|
|
|
Tensor/PIL/mask conversion, colour parsing, fit-resize and alpha helpers
|
|
used by several nodes. This is a shared module, not a node: files under
|
|
nodes/<category>/ must define exactly one ComfyNode, so reusable code
|
|
lives here (utils/_*.py) instead of inside a node file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections import deque
|
|
|
|
import numpy as np
|
|
import torch
|
|
import torch.nn.functional as F
|
|
from PIL import Image, ImageColor
|
|
|
|
|
|
def generate_default_mask(
|
|
target_width, target_height, fit_mode, orig_width, orig_height
|
|
):
|
|
"""
|
|
生成默认 mask
|
|
- pad 模式:原图区域为白色,填充区域为黑色
|
|
- 其他模式:按模式生成
|
|
"""
|
|
if fit_mode == "pad":
|
|
if orig_width <= 0 or orig_height <= 0:
|
|
return Image.new("L", (target_width, target_height), 255)
|
|
# 计算原图在目标尺寸中的位置和大小
|
|
orig_ratio = orig_width / orig_height
|
|
target_ratio = target_width / target_height
|
|
|
|
# 创建黑色背景
|
|
mask = Image.new("L", (target_width, target_height), 0)
|
|
|
|
if orig_ratio > target_ratio:
|
|
# 原图更宽,按宽度缩放
|
|
new_width = target_width
|
|
new_height = int(new_width / orig_ratio)
|
|
pad_top = (target_height - new_height) // 2
|
|
|
|
# 在原图区域填充白色
|
|
white_region = Image.new("L", (new_width, new_height), 255)
|
|
mask.paste(white_region, (0, pad_top))
|
|
else:
|
|
# 原图更高,按高度缩放
|
|
new_height = target_height
|
|
new_width = int(new_height * orig_ratio)
|
|
pad_left = (target_width - new_width) // 2
|
|
|
|
# 在原图区域填充白色
|
|
white_region = Image.new("L", (new_width, new_height), 255)
|
|
mask.paste(white_region, (pad_left, 0))
|
|
|
|
return mask
|
|
elif fit_mode == "crop":
|
|
if orig_width <= 0 or orig_height <= 0:
|
|
return Image.new("L", (target_width, target_height), 255)
|
|
orig_ratio = orig_width / orig_height
|
|
target_ratio = target_width / target_height
|
|
if orig_ratio > target_ratio:
|
|
scale = target_height / orig_height
|
|
new_width = int(orig_width * scale)
|
|
left_resized = max(0, (new_width - target_width) // 2)
|
|
left_original = int(left_resized / scale)
|
|
crop_w_original = int(target_width / scale)
|
|
crop_w_original = max(0, min(crop_w_original, orig_width))
|
|
if left_original + crop_w_original > orig_width:
|
|
crop_w_original = orig_width - left_original
|
|
mask = Image.new("L", (orig_width, orig_height), 0)
|
|
white_region = Image.new("L", (crop_w_original, orig_height), 255)
|
|
mask.paste(white_region, (left_original, 0))
|
|
return mask
|
|
elif orig_ratio < target_ratio:
|
|
scale = target_width / orig_width
|
|
new_height = int(orig_height * scale)
|
|
top_resized = max(0, (new_height - target_height) // 2)
|
|
top_original = int(top_resized / scale)
|
|
crop_h_original = int(target_height / scale)
|
|
crop_h_original = max(0, min(crop_h_original, orig_height))
|
|
if top_original + crop_h_original > orig_height:
|
|
crop_h_original = orig_height - top_original
|
|
mask = Image.new("L", (orig_width, orig_height), 0)
|
|
white_region = Image.new("L", (orig_width, crop_h_original), 255)
|
|
mask.paste(white_region, (0, top_original))
|
|
return mask
|
|
else:
|
|
return Image.new("L", (orig_width, orig_height), 255)
|
|
else:
|
|
return Image.new("L", (target_width, target_height), 255)
|
|
|
|
|
|
def fit_resize_mask(
|
|
mask, target_width, target_height, fit_mode, resize_sampler, orig_width, orig_height
|
|
):
|
|
"""
|
|
根据不同适应模式调整 mask 大小
|
|
"""
|
|
if fit_mode == "pad":
|
|
# pad 模式下的特殊处理
|
|
orig_ratio = orig_width / orig_height
|
|
target_ratio = target_width / target_height
|
|
|
|
# 创建黑色背景
|
|
result_mask = Image.new("L", (target_width, target_height), 0)
|
|
|
|
if orig_ratio > target_ratio:
|
|
# 原图更宽,按宽度缩放
|
|
new_width = target_width
|
|
new_height = int(new_width / orig_ratio)
|
|
resized_mask = mask.resize((new_width, new_height), resize_sampler)
|
|
pad_top = (target_height - new_height) // 2
|
|
result_mask.paste(resized_mask, (0, pad_top))
|
|
else:
|
|
# 原图更高,按高度缩放
|
|
new_height = target_height
|
|
new_width = int(new_height * orig_ratio)
|
|
resized_mask = mask.resize((new_width, new_height), resize_sampler)
|
|
pad_left = (target_width - new_width) // 2
|
|
result_mask.paste(resized_mask, (pad_left, 0))
|
|
|
|
return result_mask
|
|
else:
|
|
# stretch 和 crop 模式:直接使用原有逻辑
|
|
return fit_resize_image(
|
|
mask, target_width, target_height, fit_mode, resize_sampler
|
|
).convert("L")
|
|
|
|
|
|
def parse_color(color_str):
|
|
"""解析不同格式的颜色输入"""
|
|
if not color_str:
|
|
return (255, 255, 255)
|
|
|
|
if color_str.lower() in ["edge", "e"]:
|
|
return "edge"
|
|
if color_str.lower() in ["average", "a"]:
|
|
return "average"
|
|
if color_str.lower() in ["extend", "ex"]:
|
|
return "extend"
|
|
if color_str.lower() in ["mirror", "mr"]:
|
|
return "mirror"
|
|
|
|
# 移除括号(如果存在)
|
|
color_str = color_str.strip()
|
|
if color_str.startswith("(") and color_str.endswith(")"):
|
|
color_str = color_str[1:-1].strip()
|
|
|
|
# 单字母颜色缩写映射
|
|
single_letter_colors = {
|
|
"r": "red",
|
|
"g": "green",
|
|
"b": "blue",
|
|
"c": "cyan",
|
|
"m": "magenta",
|
|
"y": "yellow",
|
|
"k": "black",
|
|
"w": "white",
|
|
"o": "orange",
|
|
"p": "purple",
|
|
"n": "brown",
|
|
"s": "silver",
|
|
"l": "lime",
|
|
"i": "indigo",
|
|
"v": "violet",
|
|
"t": "turquoise",
|
|
"q": "aqua",
|
|
"f": "fuchsia",
|
|
"h": "hotpink",
|
|
"d": "darkblue",
|
|
}
|
|
|
|
# 检查是否为单字母颜色缩写
|
|
if len(color_str) == 1 and color_str.lower() in single_letter_colors:
|
|
color_str = single_letter_colors[color_str.lower()]
|
|
|
|
# 尝试解析为灰度值
|
|
try:
|
|
gray = float(color_str)
|
|
return (int(gray * 255), int(gray * 255), int(gray * 255))
|
|
except ValueError:
|
|
pass
|
|
|
|
# 尝试解析为 RGB 格式 (如 "0.5,0.7,0.9" 或 "128,192,255")
|
|
if "," in color_str:
|
|
try:
|
|
# 分割并清理每个部分
|
|
parts = [part.strip() for part in color_str.split(",")]
|
|
if len(parts) >= 3:
|
|
r, g, b = [float(parts[i]) for i in range(3)]
|
|
# 判断是否为 0-1 范围
|
|
if max(r, g, b) <= 1.0:
|
|
return (int(r * 255), int(g * 255), int(b * 255))
|
|
else:
|
|
return (int(r), int(g), int(b))
|
|
except (ValueError, IndexError):
|
|
pass
|
|
|
|
# 尝试解析为十六进制或颜色名称
|
|
try:
|
|
return ImageColor.getrgb(color_str)
|
|
except ValueError:
|
|
return (255, 255, 255)
|
|
|
|
|
|
def get_average_color(img):
|
|
arr = np.array(img.convert("RGB"))
|
|
v = arr.reshape(-1, 3).mean(axis=0)
|
|
return tuple(int(x) for x in v)
|
|
|
|
|
|
def get_edge_color(img, side):
|
|
"""获取图像边缘的平均颜色"""
|
|
width, height = img.size
|
|
img = img.convert("RGB")
|
|
|
|
if side == "left":
|
|
edge = img.crop((0, 0, 1, height))
|
|
elif side == "right":
|
|
edge = img.crop((width - 1, 0, width, height))
|
|
elif side == "top":
|
|
edge = img.crop((0, 0, width, 1))
|
|
elif side == "bottom":
|
|
edge = img.crop((0, height - 1, width, height))
|
|
else:
|
|
# 所有边缘
|
|
top = np.array(img.crop((0, 0, width, 1)))
|
|
bottom = np.array(img.crop((0, height - 1, width, height)))
|
|
left = np.array(img.crop((0, 0, 1, height)))
|
|
right = np.array(img.crop((width - 1, 0, width, height)))
|
|
|
|
# 合并所有边缘像素并计算平均值
|
|
all_edges = np.vstack(
|
|
[
|
|
top.reshape(-1, 3),
|
|
bottom.reshape(-1, 3),
|
|
left.reshape(-1, 3),
|
|
right.reshape(-1, 3),
|
|
]
|
|
)
|
|
return tuple(np.mean(all_edges, axis=0).astype(int))
|
|
|
|
# 计算平均颜色
|
|
edge_array = np.array(edge)
|
|
return tuple(np.mean(edge_array.reshape(-1, 3), axis=0).astype(int))
|
|
|
|
|
|
def fit_resize_image(
|
|
img, target_width, target_height, fit_mode, resize_sampler, background_color=None
|
|
):
|
|
"""根据不同适应模式调整图像大小"""
|
|
# 解析背景颜色
|
|
bg_color = parse_color(background_color) if background_color else (0, 0, 0)
|
|
|
|
# 获取原始尺寸
|
|
orig_width, orig_height = img.size
|
|
|
|
# 计算宽高比
|
|
orig_ratio = orig_width / orig_height
|
|
target_ratio = target_width / target_height
|
|
|
|
# 根据适应模式处理图像
|
|
if fit_mode == "stretch": # 直接拉伸
|
|
# 直接拉伸到目标尺寸
|
|
return img.resize((target_width, target_height), resize_sampler)
|
|
|
|
elif fit_mode == "crop": # 裁剪
|
|
# 调整大小保持比例,然后裁剪
|
|
if orig_ratio > target_ratio:
|
|
# 原图更宽,按高度缩放后裁剪宽度
|
|
new_height = target_height
|
|
new_width = int(new_height * orig_ratio)
|
|
img = img.resize((new_width, new_height), resize_sampler)
|
|
# 居中裁剪
|
|
left = (new_width - target_width) // 2
|
|
img = img.crop((left, 0, left + target_width, new_height))
|
|
else:
|
|
# 原图更高,按宽度缩放后裁剪高度
|
|
new_width = target_width
|
|
new_height = int(new_width / orig_ratio)
|
|
img = img.resize((new_width, new_height), resize_sampler)
|
|
# 居中裁剪
|
|
top = (new_height - target_height) // 2
|
|
img = img.crop((0, top, new_width, top + target_height))
|
|
return img
|
|
|
|
elif fit_mode == "pad": # 填充
|
|
# 调整大小保持比例,然后填充
|
|
if orig_ratio > target_ratio:
|
|
# 原图更宽,按宽度缩放后填充高度
|
|
new_width = target_width
|
|
new_height = int(new_width / orig_ratio)
|
|
img = img.resize((new_width, new_height), resize_sampler)
|
|
|
|
# 计算需要填充的像素数
|
|
pad_top = (target_height - new_height) // 2
|
|
pad_bottom = target_height - new_height - pad_top
|
|
|
|
if bg_color == "extend":
|
|
arr = np.array(img).astype(np.float32) / 255.0
|
|
t = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0)
|
|
padded = F.pad(t, (0, 0, pad_top, pad_bottom), mode="replicate")
|
|
out = (padded.squeeze(0).permute(1, 2, 0).numpy() * 255.0).astype(
|
|
np.uint8
|
|
)
|
|
return Image.fromarray(out)
|
|
elif bg_color == "mirror":
|
|
arr = np.array(img).astype(np.float32) / 255.0
|
|
t = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0)
|
|
pt = pad_top
|
|
pb = pad_bottom
|
|
while pt > 0 or pb > 0:
|
|
hh = int(t.shape[2])
|
|
st = min(pt, max(hh - 1, 0))
|
|
sb = min(pb, max(hh - 1, 0))
|
|
t = F.pad(t, (0, 0, st, sb), mode="reflect")
|
|
pt -= st
|
|
pb -= sb
|
|
out = (t.squeeze(0).permute(1, 2, 0).numpy() * 255.0).astype(np.uint8)
|
|
return Image.fromarray(out)
|
|
if bg_color == "edge":
|
|
top_color = get_edge_color(img, "top")
|
|
bottom_color = get_edge_color(img, "bottom")
|
|
|
|
# 创建填充图像
|
|
padded = Image.new("RGB", (target_width, target_height), (0, 0, 0))
|
|
# 填充顶部
|
|
if pad_top > 0:
|
|
top_pad = Image.new("RGB", (target_width, pad_top), top_color)
|
|
padded.paste(top_pad, (0, 0))
|
|
# 粘贴原图
|
|
padded.paste(img, (0, pad_top))
|
|
# 填充底部
|
|
if pad_bottom > 0:
|
|
bottom_pad = Image.new(
|
|
"RGB", (target_width, pad_bottom), bottom_color
|
|
)
|
|
padded.paste(bottom_pad, (0, pad_top + new_height))
|
|
elif bg_color == "average":
|
|
avg_color = get_average_color(img)
|
|
padded = Image.new("RGB", (target_width, target_height), avg_color)
|
|
padded.paste(img, (0, pad_top))
|
|
else:
|
|
padded = Image.new("RGB", (target_width, target_height), bg_color)
|
|
padded.paste(img, (0, pad_top))
|
|
else:
|
|
# 原图更高,按高度缩放后填充宽度
|
|
new_height = target_height
|
|
new_width = int(new_height * orig_ratio)
|
|
img = img.resize((new_width, new_height), resize_sampler)
|
|
|
|
# 计算需要填充的像素数
|
|
pad_left = (target_width - new_width) // 2
|
|
pad_right = target_width - new_width - pad_left
|
|
|
|
if bg_color == "extend":
|
|
arr = np.array(img).astype(np.float32) / 255.0
|
|
t = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0)
|
|
padded = F.pad(t, (pad_left, pad_right, 0, 0), mode="replicate")
|
|
out = (padded.squeeze(0).permute(1, 2, 0).numpy() * 255.0).astype(
|
|
np.uint8
|
|
)
|
|
return Image.fromarray(out)
|
|
elif bg_color == "mirror":
|
|
arr = np.array(img).astype(np.float32) / 255.0
|
|
t = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0)
|
|
pl = pad_left
|
|
pr = pad_right
|
|
while pl > 0 or pr > 0:
|
|
ww = int(t.shape[3])
|
|
sl = min(pl, max(ww - 1, 0))
|
|
sr = min(pr, max(ww - 1, 0))
|
|
t = F.pad(t, (sl, sr, 0, 0), mode="reflect")
|
|
pl -= sl
|
|
pr -= sr
|
|
out = (t.squeeze(0).permute(1, 2, 0).numpy() * 255.0).astype(np.uint8)
|
|
return Image.fromarray(out)
|
|
if bg_color == "edge":
|
|
left_color = get_edge_color(img, "left")
|
|
right_color = get_edge_color(img, "right")
|
|
|
|
# 创建填充图像
|
|
padded = Image.new("RGB", (target_width, target_height), (0, 0, 0))
|
|
# 填充左侧
|
|
if pad_left > 0:
|
|
left_pad = Image.new("RGB", (pad_left, target_height), left_color)
|
|
padded.paste(left_pad, (0, 0))
|
|
# 粘贴原图
|
|
padded.paste(img, (pad_left, 0))
|
|
# 填充右侧
|
|
if pad_right > 0:
|
|
right_pad = Image.new(
|
|
"RGB", (pad_right, target_height), right_color
|
|
)
|
|
padded.paste(right_pad, (pad_left + new_width, 0))
|
|
elif bg_color == "average":
|
|
avg_color = get_average_color(img)
|
|
padded = Image.new("RGB", (target_width, target_height), avg_color)
|
|
padded.paste(img, (pad_left, 0))
|
|
else:
|
|
padded = Image.new("RGB", (target_width, target_height), bg_color)
|
|
padded.paste(img, (pad_left, 0))
|
|
|
|
return padded
|
|
|
|
# 默认情况下直接调整大小
|
|
return img.resize((target_width, target_height), resize_sampler)
|
|
|
|
|
|
def tensor2pil(image):
|
|
"""将张量转换为PIL图像"""
|
|
return Image.fromarray(
|
|
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
|
|
)
|
|
|
|
|
|
def pil2tensor(image):
|
|
"""将PIL图像转换为张量"""
|
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
|
|
|
|
|
def image2mask(image):
|
|
"""将图像转换为遮罩"""
|
|
return torch.from_numpy(
|
|
np.array(image.convert("L")).astype(np.float32) / 255.0
|
|
).unsqueeze(0)
|
|
|
|
|
|
def is_valid_mask(mask):
|
|
"""检查遮罩是否有效"""
|
|
if mask is None:
|
|
return False
|
|
return torch.min(mask) < 0.9999
|
|
|
|
|
|
def num_round_up_to_multiple(num, multiple):
|
|
"""将数字向上取整到指定倍数"""
|
|
return math.ceil(num / multiple) * multiple
|
|
|
|
|
|
def log(message, message_type="info"):
|
|
"""输出日志信息"""
|
|
print(f"[{message_type.upper()}] {message}")
|
|
|
|
|
|
def adapt_fill_to_channels(fill_rgb, channels):
|
|
if isinstance(fill_rgb, str):
|
|
return fill_rgb
|
|
if not isinstance(fill_rgb, (tuple, list)):
|
|
return fill_rgb
|
|
values = list(fill_rgb)
|
|
if channels == 4:
|
|
if len(values) >= 4:
|
|
return tuple(values[:4])
|
|
if len(values) == 3:
|
|
return (values[0], values[1], values[2], 1.0)
|
|
if channels == 3 and len(values) >= 3:
|
|
return tuple(values[:3])
|
|
return tuple(values)
|
|
|
|
|
|
def ensure_channels(img, target_channels):
|
|
current_channels = int(img.shape[3])
|
|
if current_channels == target_channels:
|
|
return img
|
|
if current_channels == 3 and target_channels == 4:
|
|
alpha = torch.ones(
|
|
(img.shape[0], img.shape[1], img.shape[2], 1),
|
|
dtype=img.dtype,
|
|
device=img.device,
|
|
)
|
|
return torch.cat([img, alpha], dim=3)
|
|
if current_channels == 4 and target_channels == 3:
|
|
return img[:, :, :, :3]
|
|
return img
|
|
|
|
|
|
def parse_spacing_color(color_str):
|
|
if color_str is None:
|
|
return (1.0, 1.0, 1.0)
|
|
text = str(color_str).strip().lower()
|
|
if text.startswith("(") and text.endswith(")"):
|
|
text = text[1:-1].strip()
|
|
single = {
|
|
"r": "red",
|
|
"g": "lime",
|
|
"b": "blue",
|
|
"c": "cyan",
|
|
"m": "magenta",
|
|
"y": "yellow",
|
|
"k": "black",
|
|
"w": "white",
|
|
"o": "orange",
|
|
"p": "purple",
|
|
"n": "brown",
|
|
"s": "silver",
|
|
"l": "lime",
|
|
"i": "indigo",
|
|
"v": "violet",
|
|
"t": "turquoise",
|
|
"f": "fuchsia",
|
|
"h": "hotpink",
|
|
"d": "darkblue",
|
|
}
|
|
if len(text) == 1 and text in single:
|
|
text = single[text]
|
|
try:
|
|
v = float(text)
|
|
if 0.0 <= v <= 1.0:
|
|
return (v, v, v)
|
|
except Exception:
|
|
pass
|
|
if "," in text:
|
|
try:
|
|
parts = [p.strip() for p in text.split(",")]
|
|
if len(parts) >= 3:
|
|
r, g, b = [float(parts[i]) for i in range(3)]
|
|
if max(r, g, b) <= 1.0:
|
|
return (r, g, b)
|
|
return (r / 255.0, g / 255.0, b / 255.0)
|
|
except Exception:
|
|
pass
|
|
if text.startswith("#") and len(text) in (4, 7):
|
|
try:
|
|
hex_str = text[1:]
|
|
if len(hex_str) == 3:
|
|
hex_str = "".join(ch * 2 for ch in hex_str)
|
|
r = int(hex_str[0:2], 16) / 255.0
|
|
g = int(hex_str[2:4], 16) / 255.0
|
|
b = int(hex_str[4:6], 16) / 255.0
|
|
return (r, g, b)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
rgb = ImageColor.getrgb(text)
|
|
return (rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0)
|
|
except Exception:
|
|
return (1.0, 1.0, 1.0)
|
|
|
|
|
|
def parse_pad_color(color_str):
|
|
if color_str is None:
|
|
return (1.0, 1.0, 1.0)
|
|
text = str(color_str).strip().lower()
|
|
if text in ("edge", "e"):
|
|
return "edge"
|
|
if text in ("extend", "ex"):
|
|
return "extend"
|
|
if text in ("mirror", "mr"):
|
|
return "mirror"
|
|
if text in ("average", "a"):
|
|
return "average"
|
|
if text.startswith("(") and text.endswith(")"):
|
|
text = text[1:-1].strip()
|
|
single = {
|
|
"r": "red",
|
|
"g": "lime",
|
|
"b": "blue",
|
|
"c": "cyan",
|
|
"m": "magenta",
|
|
"y": "yellow",
|
|
"k": "black",
|
|
"w": "white",
|
|
"o": "orange",
|
|
"p": "purple",
|
|
"n": "brown",
|
|
"s": "silver",
|
|
"l": "lime",
|
|
"i": "indigo",
|
|
"v": "violet",
|
|
"t": "turquoise",
|
|
"q": "aqua",
|
|
"f": "fuchsia",
|
|
"h": "hotpink",
|
|
"d": "darkblue",
|
|
}
|
|
if len(text) == 1 and text in single:
|
|
text = single[text]
|
|
try:
|
|
v = float(text)
|
|
if 0.0 <= v <= 1.0:
|
|
return (v, v, v)
|
|
except Exception:
|
|
pass
|
|
if "," in text:
|
|
try:
|
|
parts = [p.strip() for p in text.split(",")]
|
|
if len(parts) >= 3:
|
|
r, g, b = [float(parts[i]) for i in range(3)]
|
|
if max(r, g, b) <= 1.0:
|
|
return (r, g, b)
|
|
return (r / 255.0, g / 255.0, b / 255.0)
|
|
except Exception:
|
|
pass
|
|
if text.startswith("#") and len(text) in (4, 7):
|
|
try:
|
|
hex_str = text[1:]
|
|
if len(hex_str) == 3:
|
|
hex_str = "".join(ch * 2 for ch in hex_str)
|
|
r = int(hex_str[0:2], 16) / 255.0
|
|
g = int(hex_str[2:4], 16) / 255.0
|
|
b = int(hex_str[4:6], 16) / 255.0
|
|
return (r, g, b)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
from PIL import ImageColor
|
|
|
|
rgb = ImageColor.getrgb(text)
|
|
return (rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0)
|
|
except Exception:
|
|
return (1.0, 1.0, 1.0)
|
|
|
|
|
|
def pad_to_rgb(img, target_h, target_w, fill_rgb):
|
|
"""居中填充到指定尺寸,自动适配当前图像通道数。"""
|
|
b, h, w, c = img.shape
|
|
top = max((target_h - h) // 2, 0)
|
|
left = max((target_w - w) // 2, 0)
|
|
h_end = min(top + h, target_h)
|
|
w_end = min(left + w, target_w)
|
|
if isinstance(fill_rgb, str) and fill_rgb == "extend":
|
|
pad_top = top
|
|
pad_bottom = max(target_h - (top + h), 0)
|
|
pad_left = left
|
|
pad_right = max(target_w - (left + w), 0)
|
|
nchw = img.permute(0, 3, 1, 2)
|
|
padded = F.pad(
|
|
nchw, (pad_left, pad_right, pad_top, pad_bottom), mode="replicate"
|
|
)
|
|
return padded.permute(0, 2, 3, 1)
|
|
if isinstance(fill_rgb, str) and fill_rgb == "mirror":
|
|
pad_top = top
|
|
pad_bottom = max(target_h - (top + h), 0)
|
|
pad_left = left
|
|
pad_right = max(target_w - (left + w), 0)
|
|
nchw = img.permute(0, 3, 1, 2)
|
|
while pad_top > 0 or pad_bottom > 0 or pad_left > 0 or pad_right > 0:
|
|
hh = int(nchw.shape[2])
|
|
ww = int(nchw.shape[3])
|
|
st = min(pad_top, max(hh - 1, 0))
|
|
sb = min(pad_bottom, max(hh - 1, 0))
|
|
sl = min(pad_left, max(ww - 1, 0))
|
|
sr = min(pad_right, max(ww - 1, 0))
|
|
nchw = F.pad(nchw, (sl, sr, st, sb), mode="reflect")
|
|
pad_top -= st
|
|
pad_bottom -= sb
|
|
pad_left -= sl
|
|
pad_right -= sr
|
|
return nchw.permute(0, 2, 3, 1)
|
|
out = torch.zeros((b, target_h, target_w, c), dtype=img.dtype, device=img.device)
|
|
if isinstance(fill_rgb, str) and fill_rgb == "edge":
|
|
if top > 0 or (target_h - h_end) > 0:
|
|
top_row = img[:, 0:1, :, :].mean(dim=(1, 2))
|
|
top_color = top_row.view(b, 1, 1, c)
|
|
bottom_row = img[:, -1:, :, :].mean(dim=(1, 2))
|
|
bottom_color = bottom_row.view(b, 1, 1, c)
|
|
if top > 0:
|
|
out[:, :top, :, :] = top_color.expand(b, top, target_w, c)
|
|
if (target_h - h_end) > 0:
|
|
bottom_pad = target_h - h_end
|
|
out[:, h_end:, :, :] = bottom_color.expand(b, bottom_pad, target_w, c)
|
|
if left > 0 or (target_w - w_end) > 0:
|
|
left_col = img[:, :, 0:1, :].mean(dim=(1, 2))
|
|
left_color = left_col.view(b, 1, 1, c)
|
|
right_col = img[:, :, -1:, :].mean(dim=(1, 2))
|
|
right_color = right_col.view(b, 1, 1, c)
|
|
if left > 0:
|
|
out[:, :, :left, :] = left_color.expand(b, target_h, left, c)
|
|
if (target_w - w_end) > 0:
|
|
right_pad = target_w - w_end
|
|
out[:, :, w_end:, :] = right_color.expand(b, target_h, right_pad, c)
|
|
elif isinstance(fill_rgb, str) and fill_rgb == "average":
|
|
avg = img.mean(dim=(1, 2))
|
|
avg_color = avg.view(b, 1, 1, c)
|
|
out[:] = avg_color.expand(b, target_h, target_w, c)
|
|
else:
|
|
fill_value = adapt_fill_to_channels(fill_rgb, c)
|
|
fill_t = torch.tensor(fill_value, dtype=img.dtype, device=img.device)
|
|
out[:] = fill_t
|
|
out[:, top:h_end, left:w_end, :] = img[:, : h_end - top, : w_end - left, :]
|
|
return out
|
|
|
|
|
|
def remove_small_components(mask: np.ndarray, min_area: int) -> np.ndarray:
|
|
if min_area <= 1:
|
|
return mask
|
|
h, w = int(mask.shape[0]), int(mask.shape[1])
|
|
visited = np.zeros((h, w), dtype=bool)
|
|
keep = np.zeros((h, w), dtype=bool)
|
|
neighbors = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
|
|
|
ys, xs = np.nonzero(mask)
|
|
for y0, x0 in zip(ys.tolist(), xs.tolist()):
|
|
if visited[y0, x0]:
|
|
continue
|
|
visited[y0, x0] = True
|
|
q: deque[tuple[int, int]] = deque()
|
|
q.append((y0, x0))
|
|
comp: list[tuple[int, int]] = [(y0, x0)]
|
|
|
|
while q:
|
|
y, x = q.popleft()
|
|
for dy, dx in neighbors:
|
|
ny = y + dy
|
|
nx = x + dx
|
|
if ny < 0 or ny >= h or nx < 0 or nx >= w:
|
|
continue
|
|
if visited[ny, nx] or (not mask[ny, nx]):
|
|
continue
|
|
visited[ny, nx] = True
|
|
q.append((ny, nx))
|
|
comp.append((ny, nx))
|
|
|
|
if len(comp) >= int(min_area):
|
|
for cy, cx in comp:
|
|
keep[cy, cx] = True
|
|
return keep
|
|
|
|
|
|
def apply_levels_numpy(
|
|
image_array: np.ndarray,
|
|
black_point: int,
|
|
white_point: int,
|
|
gray_point: float = 1.0,
|
|
output_black_point: int = 0,
|
|
output_white_point: int = 255,
|
|
) -> np.ndarray:
|
|
bp = int(max(0, min(255, black_point)))
|
|
wp = int(max(0, min(255, white_point)))
|
|
obp = int(max(0, min(255, output_black_point)))
|
|
owp = int(max(0, min(255, output_white_point)))
|
|
result = np.zeros_like(image_array)
|
|
for i in range(image_array.shape[2]):
|
|
ch = image_array[:, :, i].astype(np.float32)
|
|
ch = np.clip(ch, bp, wp)
|
|
denom = max(1, (wp - bp))
|
|
ch = (ch - bp) / float(denom) * 255.0
|
|
if gray_point != 1.0:
|
|
ch = 255.0 * (ch / 255.0) ** (1.0 / float(gray_point))
|
|
ch = (ch / 255.0) * (owp - obp) + obp
|
|
result[:, :, i] = np.clip(ch, 0, 255).astype(np.uint8)
|
|
return result
|