# -*- coding: utf-8 -*- """ 从 PPT 提取的原始素材生成网站用图: 1. logo 与 7 张黑底产品图:边缘 flood-fill 镂空(背景变透明,适配深色网页) 2. 水培架图背景复杂(39% 均匀度),不镂空,走统一描边容器展示 3. 截图/场景/照片转 WebP 压缩;二维码保持 PNG 无损 用法: uv run --with pillow python3 tools/process_images.py /ppt/media <输出目录> """ import sys from collections import deque from pathlib import Path from PIL import Image src = Path(sys.argv[1]) out = Path(sys.argv[2]) out.mkdir(parents=True, exist_ok=True) RENAME = { 'image2.png': 'logo.png', # 原样无损输出(见 PASS_THROUGH) 'image1.jpeg': 'hero-bg.webp', # v2 起不再做 hero 背景,仅作 og:image 'image3.png': 'about-main.webp', # v2 起弃用(PPT 拼贴图有白缝) 'image4.png': 'about-base.webp', 'image5.png': 'collar-sg03-mini.webp', 'image13.png': 'collar-sg03.webp', 'image10.png': 'ear-tag-st01.webp', 'image11.png': 'base-station-bg01.webp', 'image12.png': 'grazing-scene-1.webp', 'image18.png': 'grazing-scene-2.webp', 'image19.png': 'hydroponics-hs01.webp', 'image20.png': 'planting-scene.webp', 'image31.png': 'terminal-bb03.webp', 'image32.png': 'uwb-tag-sl01.webp', 'image33.png': 'bt-tag-st02.webp', 'image29.png': 'asset-scene-1.webp', 'image30.jpeg': 'asset-scene-2.webp', 'image34.png': 'platform-grazing-1.webp', 'image35.png': 'platform-grazing-app.webp', 'image36.png': 'platform-planting-1.webp', 'image37.png': 'platform-planting-2.webp', 'image38.png': 'platform-asset-1.webp', 'image39.png': 'platform-asset-2.webp', 'image40.png': 'custom-liangchang-1.webp', 'image41.png': 'custom-liangchang-2.webp', 'image42.png': 'custom-simulation.webp', 'image43.png': 'custom-agent.webp', 'image44.png': 'qrcode.png', } # 这些图做边缘 flood-fill 镂空(检测过:边缘色 96-100% 均匀的纯黑底) # 注意:logo 不镂空——其底色 #000020 与四角检测色差超容差,v2 曾清成斑驳残留导致看不清 CUTOUT = { 'image5.png', 'image13.png', 'image10.png', 'image11.png', 'image31.png', 'image32.png', 'image33.png', } CUTOUT_TOL = 30 # 曼哈顿距离阈值,宁小勿大:黑底纯度高,产品暗部 (#20 系) 距离约 96 不会被误清 def cutout_bg(path: Path, tol: int) -> Image.Image: """从画布四边缘 flood-fill 清除均匀底色,只清与边缘连通的背景,不动被包围的内容""" im = Image.open(path).convert('RGBA') w, h = im.size px = im.load() corners = [px[0, 0], px[w-1, 0], px[0, h-1], px[w-1, h-1]] bg = tuple(sum(c[i] for c in corners) // 4 for i in range(3)) def is_bg(p): if p[3] < 10: return True # 原本就是透明的像素直接视为背景 return abs(p[0]-bg[0]) + abs(p[1]-bg[1]) + abs(p[2]-bg[2]) <= tol seen = [[False]*w for _ in range(h)] q = deque() for x in range(w): q.extend(((x, 0), (x, h-1))) for y in range(h): q.extend(((0, y), (w-1, y))) cleared = 0 while q: x, y = q.popleft() if x < 0 or x >= w or y < 0 or y >= h or seen[y][x]: continue seen[y][x] = True p = px[x, y] if not is_bg(p): continue px[x, y] = (0, 0, 0, 0) cleared += 1 q.extend(((x+1, y), (x-1, y), (x, y+1), (x, y-1))) ratio = cleared / (w * h) # 镂空比例健全性检查:太低=没清干净,太高=误伤内容 if not 0.10 < ratio < 0.92: raise SystemExit(f'镂空异常 {path.name}: 清除 {ratio:.0%},需人工检查') return im # 原样无损拷贝:logo 色块文字经不起有损压缩,二维码必须无损 PASS_THROUGH = {'image2.png', 'image44.png'} for src_name, dst_name in RENAME.items(): src_path = src / src_name if not src_path.exists(): print(f' 跳过缺失: {src_name}') continue dst_path = out / dst_name if src_name in PASS_THROUGH: Image.open(src_path).save(dst_path) elif src_name in CUTOUT: im = cutout_bg(src_path, CUTOUT_TOL) # 镂空图必须带 alpha,webp 无损存 alpha(有损会弄脏半透明边缘) im.save(dst_path, 'WEBP', quality=100, lossless=True, method=6) else: im = Image.open(src_path).convert('RGB') if im.width > 1600: im = im.resize((1600, round(im.height * 1600 / im.width)), Image.LANCZOS) im.save(dst_path, 'WEBP', quality=82, method=6) kb = dst_path.stat().st_size // 1024 tag = ' [镂空]' if src_name in CUTOUT else '' print(f' {src_name:14s} -> {dst_name:28s} {kb}KB{tag}') print(f'完成,共 {len(RENAME)} 张 -> {out}')