ath-maas--pixelle-video
432 行
16 KiB
Python
432 行
16 KiB
Python
import os
|
|
import requests
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from datetime import datetime, timedelta
|
|
from PIL import Image
|
|
import logging
|
|
|
|
|
|
class ImageProcessor:
|
|
"""
|
|
图片处理和上传集合类
|
|
支持:图片处理、分割、拼接,以及上传到阿里云OSS
|
|
"""
|
|
|
|
# 阿里云DashScope上传配置
|
|
UPLOAD_API_URL = "https://dashscope.aliyuncs.com/api/v1/uploads"
|
|
|
|
def __init__(self,
|
|
image_path='',
|
|
api_key: str = "sk-bcab316d69a7414faa9dc29737019333",
|
|
model_name: str = "wan2.6-i2v-flash",
|
|
local_proxy: str | None = None):
|
|
"""
|
|
初始化图片处理器
|
|
|
|
Args:
|
|
image_path: 图片文件路径(可选,用于处理已有图片)
|
|
api_key: DashScope API Key(用于上传,可从环境变量 DASHSCOPE_API_KEY 读取)
|
|
model_name: 模型名称,默认使用 wan2.6-i2v-flash
|
|
"""
|
|
# 图片处理部分
|
|
if image_path != '':
|
|
self.image_path = image_path
|
|
self.image = Image.open(image_path)
|
|
self.image_np = np.array(self.image)
|
|
self.width, self.height = self.image_np.shape[1], self.image_np.shape[0]
|
|
else:
|
|
self.image_path = None
|
|
self.image = None
|
|
self.image_np = None
|
|
self.width = None
|
|
self.height = None
|
|
|
|
# 上传功能部分
|
|
self.api_key = api_key or os.getenv("DASHSCOPE_API_KEY")
|
|
self.model_name = model_name
|
|
self.local_proxy = local_proxy
|
|
|
|
def _proxies(self):
|
|
if not self.local_proxy:
|
|
return None
|
|
return {"http": self.local_proxy, "https": self.local_proxy}
|
|
|
|
@staticmethod
|
|
def check_column_white(column_pixels):
|
|
"""检查列是否几乎全白"""
|
|
is_almost_white = np.logical_or(column_pixels == 254, column_pixels == 255)
|
|
white_pixels_ratio = np.mean(np.all(is_almost_white, axis=-1))
|
|
return white_pixels_ratio >= 0.98 # 至少98%的像素为白色
|
|
|
|
def find_white_section(self, start, end):
|
|
"""查找指定范围内的白色区间"""
|
|
white_sections = []
|
|
in_white_section = False
|
|
start_index = 0
|
|
|
|
for col in range(start, end):
|
|
column_pixels = self.image_np[:, col, :]
|
|
if self.check_column_white(column_pixels):
|
|
if not in_white_section:
|
|
start_index = col
|
|
in_white_section = True
|
|
else:
|
|
if in_white_section:
|
|
white_sections.append((start_index, col))
|
|
in_white_section = False
|
|
|
|
if in_white_section:
|
|
white_sections.append((start_index, end))
|
|
|
|
return white_sections
|
|
|
|
def split_image(self):
|
|
"""将图片从中间分割为左右两部分"""
|
|
start_col = self.width * 2 // 5
|
|
end_col = self.width * 3 // 5
|
|
white_sections = self.find_white_section(start_col, end_col)
|
|
|
|
if white_sections:
|
|
middle_section = white_sections[len(white_sections) // 2]
|
|
mid_col = (middle_section[0] + middle_section[1]) // 2
|
|
else:
|
|
raise ValueError("No suitable white column found within the specified range")
|
|
|
|
left_box = (0, 0, mid_col, self.height)
|
|
right_box = (mid_col, 0, self.width, self.height)
|
|
left_image = self.image.crop(left_box)
|
|
right_image = self.image.crop(right_box)
|
|
|
|
save_dir, filename = os.path.split(self.image_path)
|
|
base, extension = os.path.splitext(filename)
|
|
|
|
left_image_path = os.path.join(save_dir, base + '_front' + extension)
|
|
right_image_path = os.path.join(save_dir, base + '_back' + extension)
|
|
left_image.save(left_image_path)
|
|
right_image.save(right_image_path)
|
|
|
|
return left_image_path, right_image_path
|
|
|
|
def stitch_images(self, image_paths, output_path):
|
|
"""拼接多张图片"""
|
|
if not image_paths:
|
|
raise ValueError("No image paths provided")
|
|
sample_image = Image.open(image_paths[0])
|
|
single_width, single_height = sample_image.size
|
|
num_images = len(image_paths)
|
|
total_desired_width = single_width
|
|
total_current_width = single_width * num_images
|
|
total_width_to_cut = max(0, total_current_width - total_desired_width)
|
|
width_to_cut_per_image = total_width_to_cut // num_images
|
|
stitched_image = Image.new('RGB', (total_desired_width, single_height), "white")
|
|
current_x = 0
|
|
|
|
for path in image_paths:
|
|
image = Image.open(path)
|
|
if width_to_cut_per_image > 0:
|
|
left_margin = width_to_cut_per_image // 2
|
|
right_margin = image.width - width_to_cut_per_image + left_margin
|
|
image = image.crop((left_margin, 0, right_margin, image.height))
|
|
stitched_image.paste(image, (current_x, 0))
|
|
current_x += image.width
|
|
|
|
output_dir = os.path.dirname(output_path)
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
stitched_image.save(output_path)
|
|
return output_path
|
|
|
|
def download_image(self, image_url, save_path, max_retries=3):
|
|
"""
|
|
下载图片,带有重试机制和SSL错误处理
|
|
|
|
Args:
|
|
image_url: 图片URL
|
|
save_path: 本地保存路径
|
|
max_retries: 最大重试次数
|
|
"""
|
|
import time
|
|
import urllib3
|
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
response = requests.get(
|
|
image_url,
|
|
timeout=(10, 30),
|
|
stream=True,
|
|
verify=True,
|
|
proxies=self._proxies(),
|
|
headers={
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
with open(save_path, 'wb') as file:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
if chunk:
|
|
file.write(chunk)
|
|
print(f"✓ 图片下载成功: {save_path}")
|
|
return True
|
|
else:
|
|
print(f"下载失败,状态码: {response.status_code}")
|
|
|
|
except requests.exceptions.SSLError as e:
|
|
print(f"SSL错误 (尝试 {attempt + 1}/{max_retries}): {str(e)[:100]}")
|
|
if attempt < max_retries - 1:
|
|
wait_time = (attempt + 1) * 2
|
|
print(f"等待 {wait_time} 秒后重试...")
|
|
time.sleep(wait_time)
|
|
else:
|
|
print("尝试禁用SSL验证重新下载...")
|
|
try:
|
|
response = requests.get(
|
|
image_url,
|
|
timeout=(10, 30),
|
|
stream=True,
|
|
verify=False,
|
|
proxies=self._proxies(),
|
|
headers={
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
}
|
|
)
|
|
if response.status_code == 200:
|
|
with open(save_path, 'wb') as file:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
if chunk:
|
|
file.write(chunk)
|
|
print(f"✓ 图片下载成功(已禁用SSL验证): {save_path}")
|
|
return True
|
|
except Exception as fallback_error:
|
|
print(f"禁用SSL验证后仍然失败: {fallback_error}")
|
|
raise
|
|
|
|
except requests.exceptions.Timeout as e:
|
|
print(f"超时错误 (尝试 {attempt + 1}/{max_retries}): {e}")
|
|
if attempt < max_retries - 1:
|
|
time.sleep((attempt + 1) * 2)
|
|
else:
|
|
raise
|
|
|
|
except Exception as e:
|
|
print(f"下载错误 (尝试 {attempt + 1}/{max_retries}): {e}")
|
|
if attempt < max_retries - 1:
|
|
time.sleep((attempt + 1) * 2)
|
|
else:
|
|
raise
|
|
|
|
return False
|
|
|
|
def resize_image(self, image_path):
|
|
"""调整图片大小(添加顶部空白)"""
|
|
original_image = Image.open(image_path)
|
|
width, height = original_image.size
|
|
top_blank_height = height // 2
|
|
final_height = height + top_blank_height
|
|
final_width = int(final_height * 5 / 3)
|
|
new_image = Image.new("RGB", (final_width, final_height), color="white")
|
|
left = (final_width - width) // 2
|
|
top = top_blank_height
|
|
new_image.paste(original_image, (left, top))
|
|
new_image.save(image_path)
|
|
return image_path
|
|
|
|
def has_black_borders(self, image_path, threshold=10, black_limit=20):
|
|
"""检查图片是否有黑色边框"""
|
|
img = Image.open(image_path)
|
|
pixels = img.load()
|
|
width, height = img.size
|
|
|
|
def is_black_pixel(pixel):
|
|
return all(x <= black_limit for x in pixel)
|
|
|
|
# 检查顶部和底部边框
|
|
for y in range(threshold):
|
|
if all(is_black_pixel(pixels[x, y]) for x in range(width)):
|
|
return True
|
|
if all(is_black_pixel(pixels[x, height - 1 - y]) for x in range(width)):
|
|
return True
|
|
|
|
# 检查左右边框
|
|
for x in range(threshold):
|
|
if all(is_black_pixel(pixels[x, y]) for y in range(height)):
|
|
return True
|
|
if all(is_black_pixel(pixels[width - 1 - x, y]) for y in range(height)):
|
|
return True
|
|
|
|
return False
|
|
|
|
# ===== 图片上传功能 =====
|
|
|
|
def get_upload_policy(self):
|
|
"""
|
|
获取文件上传凭证
|
|
|
|
Returns:
|
|
policy_data: 包含上传所需凭证的字典
|
|
|
|
Raises:
|
|
Exception: 获取上传凭证失败时
|
|
"""
|
|
if not self.api_key:
|
|
raise RuntimeError("DASHSCOPE_API_KEY 未设置,无法使用图片上传服务")
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
params = {
|
|
"action": "getPolicy",
|
|
"model": self.model_name
|
|
}
|
|
|
|
response = requests.get(
|
|
self.UPLOAD_API_URL,
|
|
headers=headers,
|
|
params=params,
|
|
proxies=self._proxies(),
|
|
)
|
|
if response.status_code != 200:
|
|
raise Exception(f"Failed to get upload policy: {response.text}")
|
|
|
|
return response.json()['data']
|
|
|
|
def upload_file_to_oss(self, policy_data: dict, file_path: str) -> str:
|
|
"""
|
|
将文件上传到临时存储OSS
|
|
|
|
Args:
|
|
policy_data: 上传凭证数据
|
|
file_path: 本地文件路径
|
|
|
|
Returns:
|
|
oss_url: OSS URL (格式: oss://...)
|
|
|
|
Raises:
|
|
Exception: 上传失败时
|
|
"""
|
|
file_name = Path(file_path).name
|
|
# Sanitize filename for upload to avoid issues with spaces/characters
|
|
safe_file_name = "".join([c if c.isalnum() or c in ('-','_','.') else '_' for c in file_name])
|
|
|
|
key = f"{policy_data['upload_dir']}/{safe_file_name}"
|
|
|
|
with open(file_path, 'rb') as file:
|
|
files = {
|
|
'OSSAccessKeyId': (None, policy_data['oss_access_key_id']),
|
|
'Signature': (None, policy_data['signature']),
|
|
'policy': (None, policy_data['policy']),
|
|
'x-oss-object-acl': (None, policy_data['x_oss_object_acl']),
|
|
'x-oss-forbid-overwrite': (None, policy_data['x_oss_forbid_overwrite']),
|
|
'key': (None, key),
|
|
'success_action_status': (None, '200'),
|
|
'file': (safe_file_name, file)
|
|
}
|
|
|
|
response = requests.post(
|
|
policy_data['upload_host'],
|
|
files=files,
|
|
proxies=self._proxies(),
|
|
)
|
|
if response.status_code != 200:
|
|
raise Exception(f"Failed to upload file: {response.text}")
|
|
|
|
# Construct OSS URL correctly: oss://<bucket>/<key>
|
|
# Extract bucket from upload_host (e.g., https://dashscope-instant.oss-cn-beijing.aliyuncs.com)
|
|
upload_host = policy_data['upload_host']
|
|
bucket_name = ""
|
|
if '://' in upload_host:
|
|
domain = upload_host.split('://')[1]
|
|
bucket_name = domain.split('.')[0]
|
|
|
|
if bucket_name:
|
|
return f"oss://{bucket_name}/{key}"
|
|
else:
|
|
# Fallback if parsing fails (though unlikely for standard OSS hosts)
|
|
# If the original code's assumption that key was self-sufficient was somehow valid, logic is here.
|
|
# But normally, oss://<key> is wrong if key doesn't have bucket.
|
|
return f"oss://{key}"
|
|
|
|
def upload(self, file_path: str) -> str:
|
|
"""
|
|
上传文件到阿里云OSS并获取URL(统一接口方法)
|
|
|
|
Args:
|
|
file_path: 本地文件路径
|
|
|
|
Returns:
|
|
oss_url: OSS URL,可在48小时内使用
|
|
|
|
Raises:
|
|
FileNotFoundError: 文件不存在时
|
|
RuntimeError: API Key未设置时
|
|
Exception: 上传失败时
|
|
"""
|
|
# 检查文件是否存在
|
|
if not os.path.exists(file_path):
|
|
raise FileNotFoundError(f"文件不存在: {file_path}")
|
|
|
|
if not self.api_key:
|
|
raise RuntimeError("DASHSCOPE_API_KEY 未设置,无法使用图片上传服务")
|
|
|
|
# 1. 获取上传凭证(注意:上传凭证接口有限流)
|
|
policy_data = self.get_upload_policy()
|
|
|
|
# 2. 上传文件到OSS
|
|
oss_url = self.upload_file_to_oss(policy_data, file_path)
|
|
|
|
# 3. 计算过期时间
|
|
expire_time = datetime.now() + timedelta(hours=48)
|
|
|
|
logging.info(f"文件上传成功: {file_path}")
|
|
logging.info(f" OSS URL: {oss_url}")
|
|
logging.info(f" 过期时间: {expire_time.strftime('%Y-%m-%d %H:%M:%S')} (48小时)")
|
|
|
|
return oss_url
|
|
|
|
def collage_images(self, image_paths, output_path):
|
|
"""
|
|
拼图功能:将多张图片水平拼接
|
|
Args:
|
|
image_paths: 图片路径列表
|
|
output_path: 输出文件路径
|
|
"""
|
|
if not image_paths:
|
|
return None
|
|
|
|
images = []
|
|
for p in image_paths:
|
|
try:
|
|
img = Image.open(p)
|
|
images.append(img)
|
|
except Exception as e:
|
|
logging.error(f"Cannot open image {p}: {e}")
|
|
|
|
if not images:
|
|
return None
|
|
|
|
# 统一高度,按第一张图片的高度调整其他图片
|
|
base_height = images[0].height
|
|
resized_images = []
|
|
for img in images:
|
|
if img.height != base_height:
|
|
ratio = base_height / img.height
|
|
new_width = int(img.width * ratio)
|
|
resized_images.append(img.resize((new_width, base_height)))
|
|
else:
|
|
resized_images.append(img)
|
|
|
|
total_width = sum(img.width for img in resized_images)
|
|
new_im = Image.new('RGB', (total_width, base_height))
|
|
|
|
x_offset = 0
|
|
for img in resized_images:
|
|
new_im.paste(img, (x_offset, 0))
|
|
x_offset += img.width
|
|
|
|
new_im.save(output_path)
|
|
return output_path
|