Files
smthemex-ComfyUI_InteractAv…/utils/img_utils.py
T
2026-02-06 09:34:36 +08:00

331 lines
13 KiB
Python

import os
import sys
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import torch, random
import torch.distributed as dist
from PIL import Image
import pandas as pd
import librosa
import json
import cv2
def get_image_from_path(image_path):
"""
读取图片文件并返回一个 PIL Image 对象。
:param image_path: 图片文件的路径 (例如 PNG, JPG 等)
:return: PIL Image 对象,如果读取失败则返回 None
"""
try:
# 使用 Pillow 的 Image.open() 直接打开图片文件
img = Image.open(image_path)
# 为了确保输出格式与原函数一致 (RGB),我们进行转换。
# 这么做可以处理带透明度通道的 PNG 图片 (RGBA -> RGB)
# 或者其他色彩模式的图片。
return img.convert('RGB')
except FileNotFoundError:
print(f"错误: 无法找到文件 {image_path}")
return None
except Exception as e:
print(f"错误: 打开图片文件时发生未知错误 {image_path}: {e}")
return None
def _center_crop_to_aspect_ratio(img: Image.Image, target_ratio: float) -> Image.Image:
"""
辅助函数:将图片通过中心裁剪的方式调整到目标长宽比。
target_ratio: 宽度 / 高度
"""
w, h = img.size
current_ratio = w / h
if abs(current_ratio - target_ratio) < 1e-4: # 如果比例已经很接近,则无需裁剪
return img
if current_ratio > target_ratio: # 当前图片比目标更“宽”,需要裁掉左右两边
new_w = int(target_ratio * h)
left = (w - new_w) // 2
right = left + new_w
top, bottom = 0, h
else: # 当前图片比目标更“高”,需要裁掉上下两边
new_h = int(w / target_ratio)
top = (h - new_h) // 2
bottom = top + new_h
left, right = 0, w
img_cropped = img.crop((left, top, right, bottom))
print(f"INFO: 图片从 {img.size} 裁剪到 {img_cropped.size} 以匹配目标长宽比 {target_ratio:.3f}")
return img_cropped
def process_images_final(img1, img2, reference_aspect_from = 'small'):
"""
处理两张图片,严格满足以下所有条件:
1. 所有最终图片的宽高都是32的倍数。
2. 一张图的短边固定为256px,另一张固定为704px。
3. 两张图最终的长宽比【完全相等】。
4. 通过中心裁剪来统一长宽比。
:param img1: 第一个Pillow图片对象。
:param img2: 第二个Pillow图片对象。
:param reference_aspect_from: 以哪张图的长宽比为基准, 'small' 或 'large'。
:return: 一个包含两张处理后图片的元组 (img1_processed, img2_processed)。
"""
# 1. 识别小图和大图
w1, h1 = img1.size
w2, h2 = img2.size
short1, short2 = min(w1, h1), min(w2, h2)
if short1 < short2:
img_small_orig, img_large_orig, is_img1_small = img1, img2, True
else:
img_small_orig, img_large_orig, is_img1_small = img2, img1, False
# 2. 确定基准长宽比并裁剪非基准图
if reference_aspect_from == 'small':
ref_img, other_img = img_small_orig, img_large_orig
else:
ref_img, other_img = img_large_orig, img_small_orig
ref_w, ref_h = ref_img.size
target_aspect_ratio = ref_w / ref_h
other_img_cropped = _center_crop_to_aspect_ratio(other_img, target_aspect_ratio)
if reference_aspect_from == 'small':
img_small_src, img_large_src = ref_img, other_img_cropped
else:
img_small_src, img_large_src = other_img_cropped, ref_img
# 3. 核心计算:基于严格比例推导尺寸
is_horizontal = target_aspect_ratio > 1
# 3.1 计算小图的理想长边
if is_horizontal:
ideal_long_small = 256 * target_aspect_ratio
else:
ideal_long_small = 256 / target_aspect_ratio
# 3.2 【关键修正】将小图理想长边调整到最接近的128的倍数
# 这是为了确保大图的长边也能成为32的倍数 (128 = 32 * 4)
final_long_small = int(round(ideal_long_small / 128.0) * 128)
final_long_small = max(128, final_long_small) # 确保不为0
# 3.3 计算大图的最终长边,它现在必然是32的倍数
final_long_large = int(final_long_small * (704 / 256.0))
# 4. 组合最终尺寸
if is_horizontal:
final_size_small = (final_long_small, 256)
final_size_large = (final_long_large, 704)
else:
final_size_small = (256, final_long_small)
final_size_large = (704, final_long_large)
print(f"INFO: 小图理想长边 {ideal_long_small:.2f} -> 调整到128的倍数 -> {final_long_small}")
print(f"FINAL: 小图尺寸: {final_size_small}, 大图尺寸: {final_size_large}")
# 验证长宽比是否相等
final_ratio_small = final_size_small[0] / final_size_small[1]
final_ratio_large = final_size_large[0] / final_size_large[1]
# 5. 缩放图片到最终尺寸
img_small_final = img_small_src.resize(final_size_small, Image.Resampling.LANCZOS)
img_large_final = img_large_src.resize(final_size_large, Image.Resampling.LANCZOS)
# 6. 按原始输入顺序返回结果
if is_img1_small:
return img_small_final, img_large_final
else:
return img_large_final, img_small_final
def resize_images(img1: Image.Image, img2: Image.Image):
"""
根据特定规则缩放两个Pillow图片对象。(已修正版本)
"""
w1, h1 = img1.size
w2, h2 = img2.size
short1, long1 = (w1, h1) if w1 < h1 else (h1, w1)
short2, long2 = (w2, h2) if w2 < h2 else (h2, w2)
# 情况一:两个短边都是256 (此部分逻辑正确,无需修改)
if short1 == 256 and short2 == 256:
print("检测到情况一:两张图片的短边均为256。")
if long1 < long2:
target_size = img1.size
img2_resized = img2.resize(target_size, Image.Resampling.LANCZOS)
return img1, img2_resized
elif long2 < long1:
target_size = img2.size
img1_resized = img1.resize(target_size, Image.Resampling.LANCZOS)
return img1_resized, img2
else:
return img1, img2
# 情况二:一个短边是256,另一个是512 (此部分已修正)
elif (short1 == 256 and short2 == 512) or (short1 == 512 and short2 == 256):
print("检测到情况二:一张图片短边为256,另一张为512。")
if short1 == 256:
img_small, img_large = img1, img2
long_small, long_large_orig = long1, long2
else:
img_small, img_large = img2, img1
long_small, long_large_orig = long2, long1
# 计算大图的目标长边
target_long_large = long_small * 2
# 如果大图当前的长边已经是目标长度,则无需处理
if target_long_large == long_large_orig:
return img1, img2
w_large, h_large = img_large.size
# --- 核心修正逻辑 ---
# 直接将新的长边与固定的短边(512)组合,而不是通过比例计算
if w_large > h_large: # 如果宽度是长边, 高度就是固定的512
target_size_large = (target_long_large, h_large)
else: # 如果高度是长边, 宽度就是固定的512
target_size_large = (w_large, target_long_large)
# --- 修正结束 ---
print(f"小图尺寸: {img_small.size}, 大图将从 {img_large.size} 缩放到 {target_size_large}")
img_large_resized = img_large.resize(target_size_large, Image.Resampling.LANCZOS)
if short1 == 256:
return img_small, img_large_resized
else:
return img_large_resized, img_small
else:
raise ValueError("输入的图片尺寸不符合预设的两种情况。")
def resize_short_side(img, short_side=512):
w, h = img.size
if h < w:
new_h = short_side
new_w = int(w * short_side / h)
else:
new_w = short_side
new_h = int(h * short_side / w)
new_h = int(new_h // 32 * 32)
new_w = int(new_w // 32 * 32)
img = img.resize((new_w, new_h), Image.LANCZOS)
return img
def get_first_frame_from_video(video_path):
"""
读取视频文件的第一帧并返回一个 PIL Image 对象。
:param video_path: 视频文件的路径
:return: PIL Image 对象,如果读取失败则返回 None
"""
# 打开视频文件
cap = cv2.VideoCapture(video_path)
# 检查视频是否成功打开
if not cap.isOpened():
print(f"错误: 无法打开视频文件 {video_path}")
return None
# 读取第一帧
# cap.read() 返回一个元组 (布尔值, 帧)
# 布尔值表示是否成功读取,帧是图像数据
ret, frame = cap.read()
# 释放视频捕获对象,这很重要
cap.release()
if ret:
# OpenCV 读取的图像格式是 BGR (蓝, 绿, 红)
# PIL 和大多数其他库使用的格式是 RGB (红, 绿, 蓝)
# 所以我们需要进行颜色空间转换
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 将 NumPy 数组格式的帧转换为 PIL Image 对象
return Image.fromarray(frame_rgb)
else:
print("错误: 无法从视频中读取第一帧")
return None
def get_all_frames_from_video(video_path: str):
"""
读取视频文件的所有帧并返回一个 PIL Image 对象的列表。
:param video_path: 视频文件的路径
:return: PIL Image 对象的列表,如果读取失败或视频为空则返回 None
"""
# 打开视频文件
cap = cv2.VideoCapture(video_path)
# 检查视频是否成功打开
if not cap.isOpened():
print(f"错误: 无法打开视频文件 {video_path}")
return None
frames = []
while True:
# 读取一帧
# cap.read() 返回一个元组 (布尔值, 帧)
# 布尔值表示是否成功读取,帧是图像数据
ret, frame = cap.read()
# 如果 ret 为 False,表示视频已结束或读取时发生错误
if not ret:
break
# OpenCV 读取的图像格式是 BGR (蓝, 绿, 红)
# PIL 和大多数其他库使用的格式是 RGB (红, 绿, 蓝)
# 所以我们需要进行颜色空间转换
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 将 NumPy 数组格式的帧转换为 PIL Image 对象并添加到列表中
frames.append(Image.fromarray(frame_rgb))
# 释放视频捕获对象,这很重要
cap.release()
if not frames:
print(f"错误: 未能从视频 {video_path} 中读取任何帧。")
return None
return frames
import torchvision.transforms.functional as TF
def read_obj_tensor_from_path(input_path=None, target_size=None, background_color=(255, 255, 255)):
if input_path is None:
final_background_color = background_color + (255,)
background = Image.new('RGBA', target_size, final_background_color)
elif not os.path.exists(input_path):
final_background_color = background_color + (255,)
background = Image.new('RGBA', target_size, final_background_color)
else:
with Image.open(input_path) as img:
# 确保图片是RGBA模式,以更好地处理透明度
img = img.convert("RGBA")
original_width, original_height = img.size
target_width, target_height = target_size
# 1. 计算缩放比例,确保图片能完整放入目标框内
ratio = min(target_width / original_width, target_height / original_height)
# 2. 计算缩放后的新尺寸
new_width = int(original_width * ratio)
new_height = int(original_height * ratio)
# 3. 高质量缩放图片
resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# 4. 创建一个新的纯色背景画布
# 注意:颜色需要是RGBA格式,所以白色是 (255, 255, 255, 255)
# 最后一个值255代表完全不透明
final_background_color = background_color + (255,)
background = Image.new('RGBA', target_size, final_background_color)
# 5. 计算粘贴位置,使其居中
paste_x = (target_width - new_width) // 2
paste_y = (target_height - new_height) // 2
# 6. 将缩放后的图片粘贴到背景画布上
# 第三个参数 `resized_img` 作为蒙版,可以正确处理PNG的透明通道
background.paste(resized_img, (paste_x, paste_y), resized_img)
return background.convert('RGB')