项目文件夹

文件
wehub-resource-sync 7d6672c63e
Deploy Documentation / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:19:20 +08:00

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