- W26: NestJS API + Next.js Web + Prisma + SQLite scaffold - W26: Lead 模型 + Crawler + 询价发布助手 (线索池 + 一键发布) - W29.x: MCP 集成 (Integration 模型 + connectMcp + publish_rfq 5 工具端到端跑通) - W30: 3 字段补充 (notes / quotationType / quotationDemand) - W31 P1: 自动填 enterpriseId / userId (Prisma companyUserId 字段 + get_app_identity tool + lead.service.ts publishArgs) - W31: detail 详情页 + admin UI 集成管理 (OpenAppListView + AuditLogListView) 关联: ~/Documents/02-平台开发/产品开发/F002-W31-交接文档.md GIT-AUDIT-REPORT-2026-06-24.md
242 lines
6.7 KiB
Python
242 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
图片处理脚本
|
||
裁剪、缩放、合成背景
|
||
|
||
用法:
|
||
python3 process_image.py input.jpg output.jpg --ratio 1:1 --style white
|
||
python3 process_image.py input.jpg output.jpg --ratio 4:3 --style minimal
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
from PIL import Image, ImageFilter, ImageDraw
|
||
|
||
# 目标尺寸(长边)
|
||
TARGET_SIZE = 800
|
||
|
||
# 背景色
|
||
BG_COLORS = {
|
||
"white": (255, 255, 255),
|
||
"light_gray": (245, 245, 245),
|
||
"transparent": None,
|
||
}
|
||
|
||
# 比例映射
|
||
RATIO_MAP = {
|
||
"1:1": (1, 1),
|
||
"4:3": (4, 3),
|
||
"3:4": (3, 4),
|
||
"16:9": (16, 9),
|
||
}
|
||
|
||
# 水印检测阈值:像素亮度 > 此值且 < 255 的区域可能是水印
|
||
WATERMARK_BRIGHT_MIN = 200
|
||
WATERMARK_BRIGHT_MAX = 254
|
||
# 边缘裁剪比例(去除角标水印)
|
||
EDGE_CROP_RATIO = 0.03
|
||
|
||
|
||
def crop_edges(img, ratio=EDGE_CROP_RATIO):
|
||
"""裁剪边缘,去除角落水印/标识"""
|
||
w, h = img.size
|
||
cx = int(w * ratio)
|
||
cy = int(h * ratio)
|
||
return img.crop((cx, cy, w - cx, h - cy))
|
||
|
||
|
||
def remove_semi_transparent_watermark(img):
|
||
"""去除半透明文字水印(常见于素材网站)"""
|
||
if img.mode == "RGB":
|
||
img = img.convert("RGBA")
|
||
|
||
pixels = img.load()
|
||
w, h = img.size
|
||
|
||
# 扫描检测亮度异常高的像素(接近白色但不是纯白 → 可能是半透明白色水印)
|
||
for y in range(h):
|
||
for x in range(w):
|
||
r, g, b, a = pixels[x, y]
|
||
# 计算亮度
|
||
brightness = (r + g + b) / 3
|
||
# 如果亮度很高但不是纯白,且 RGB 三通道接近(灰白色),可能是水印
|
||
if WATERMARK_BRIGHT_MIN <= brightness <= WATERMARK_BRIGHT_MAX:
|
||
if abs(r - g) < 15 and abs(g - b) < 15 and abs(r - b) < 15:
|
||
# 取周围像素的平均值来替换
|
||
pixels[x, y] = _get_neighbor_avg(pixels, x, y, w, h)
|
||
|
||
return img.convert("RGB")
|
||
|
||
|
||
def _get_neighbor_avg(pixels, x, y, w, h, radius=3):
|
||
"""获取周围非水印像素的平均颜色"""
|
||
total_r, total_g, total_b, count = 0, 0, 0, 0
|
||
|
||
for dy in range(-radius, radius + 1):
|
||
for dx in range(-radius, radius + 1):
|
||
nx, ny = x + dx, y + dy
|
||
if 0 <= nx < w and 0 <= ny < h and (dx != 0 or dy != 0):
|
||
r, g, b, a = pixels[nx, ny]
|
||
brightness = (r + g + b) / 3
|
||
# 只统计不是水印的像素
|
||
if brightness < WATERMARK_BRIGHT_MIN or brightness > WATERMARK_BRIGHT_MAX:
|
||
total_r += r
|
||
total_g += g
|
||
total_b += b
|
||
count += 1
|
||
|
||
if count > 0:
|
||
return (total_r // count, total_g // count, total_b // count, 255)
|
||
# 如果周围都是水印,用白色填充
|
||
return (255, 255, 255, 255)
|
||
|
||
|
||
def remove_watermark(img):
|
||
"""综合水印去除:边缘裁剪 + 半透明水印清理"""
|
||
# 1. 裁剪边缘(去除角落 logo/水印)
|
||
img = crop_edges(img)
|
||
|
||
# 2. 去除半透明文字水印
|
||
img = remove_semi_transparent_watermark(img)
|
||
|
||
return img
|
||
|
||
|
||
def crop_to_ratio(img, ratio_str):
|
||
"""按比例居中裁剪"""
|
||
w, h = img.size
|
||
rw, rh = RATIO_MAP.get(ratio_str, (1, 1))
|
||
target_ratio = rw / rh
|
||
current_ratio = w / h
|
||
|
||
if current_ratio > target_ratio:
|
||
# 图片太宽,裁剪左右
|
||
new_w = int(h * target_ratio)
|
||
left = (w - new_w) // 2
|
||
img = img.crop((left, 0, left + new_w, h))
|
||
elif current_ratio < target_ratio:
|
||
# 图片太高,裁剪上下
|
||
new_h = int(w / target_ratio)
|
||
top = (h - new_h) // 2
|
||
img = img.crop((0, top, w, top + new_h))
|
||
|
||
return img
|
||
|
||
|
||
def resize_to_target(img, target_size=TARGET_SIZE):
|
||
"""缩放到目标尺寸(长边为 target_size)"""
|
||
w, h = img.size
|
||
if w >= h:
|
||
ratio = target_size / w
|
||
else:
|
||
ratio = target_size / h
|
||
|
||
new_w = int(w * ratio)
|
||
new_h = int(h * ratio)
|
||
return img.resize((new_w, new_h), Image.LANCZOS)
|
||
|
||
|
||
def add_white_bg(img):
|
||
"""添加白色背景(处理透明通道)"""
|
||
if img.mode in ("RGBA", "LA", "PA"):
|
||
bg = Image.new("RGB", img.size, (255, 255, 255))
|
||
if img.mode == "RGBA":
|
||
bg.paste(img, mask=img.split()[3])
|
||
else:
|
||
bg.paste(img)
|
||
return bg
|
||
elif img.mode != "RGB":
|
||
return img.convert("RGB")
|
||
return img
|
||
|
||
|
||
def add_colored_bg(img, color):
|
||
"""添加纯色背景"""
|
||
bg = Image.new("RGB", img.size, color)
|
||
if img.mode in ("RGBA", "LA", "PA"):
|
||
if img.mode == "RGBA":
|
||
bg.paste(img, mask=img.split()[3])
|
||
else:
|
||
bg.paste(img)
|
||
else:
|
||
bg.paste(img)
|
||
return bg
|
||
|
||
|
||
def sharpen(img):
|
||
"""轻微锐化"""
|
||
return img.filter(ImageFilter.SHARPEN)
|
||
|
||
|
||
def process_image(input_path, output_path, ratio="1:1", style="white"):
|
||
"""完整处理流程"""
|
||
try:
|
||
img = Image.open(input_path)
|
||
|
||
# 1. 转为 RGB(如果是 CMYK 等)
|
||
if img.mode not in ("RGB", "RGBA", "LA", "PA"):
|
||
img = img.convert("RGB")
|
||
|
||
# 2. 去水印
|
||
img = remove_watermark(img)
|
||
|
||
# 3. 按比例裁剪
|
||
img = crop_to_ratio(img, ratio)
|
||
|
||
# 4. 缩放
|
||
img = resize_to_target(img, TARGET_SIZE)
|
||
|
||
# 5. 合成背景
|
||
if style == "white":
|
||
img = add_white_bg(img)
|
||
elif style == "minimal":
|
||
img = add_white_bg(img) # minimal 也用白底
|
||
elif style == "scene":
|
||
# 场景图保留原背景,只做裁剪缩放
|
||
if img.mode != "RGB":
|
||
img = add_white_bg(img)
|
||
elif style == "realistic":
|
||
if img.mode != "RGB":
|
||
img = add_white_bg(img)
|
||
else:
|
||
img = add_white_bg(img)
|
||
|
||
# 6. 锐化
|
||
img = sharpen(img)
|
||
|
||
# 7. 保存
|
||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||
img.save(output_path, "JPEG", quality=90)
|
||
|
||
return output_path
|
||
except Exception as e:
|
||
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
||
return None
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="图片处理")
|
||
parser.add_argument("input", help="输入图片路径")
|
||
parser.add_argument("output", help="输出图片路径")
|
||
parser.add_argument("--ratio", default="1:1", choices=RATIO_MAP.keys(), help="目标比例")
|
||
parser.add_argument("--style", default="white", choices=BG_COLORS.keys(), help="背景风格")
|
||
args = parser.parse_args()
|
||
|
||
result = process_image(args.input, args.output, args.ratio, args.style)
|
||
|
||
output = {
|
||
"input": args.input,
|
||
"output": result,
|
||
"ratio": args.ratio,
|
||
"style": args.style,
|
||
"success": result is not None,
|
||
}
|
||
print(json.dumps(output, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|