公司最近找了家深圳的专业摄影机构(沐王府形象摄影)拍了一批团队形象照,交付了50个人的原片和精修片。拿到手后发现一个问题:不同使用场景需要不同尺寸和比例的图片。
官网团队页要400x500竖版、工牌系统要300x300正方形、LinkedIn要1584x396的Banner底图... 一张张手动裁不现实。
作为IT,当然选择写脚本解决。分享一下我的批量处理方案。
需求分析
收到的形象照原片是统一的3:4竖版(3000x4000px),需要输出以下规格:
| 用途 | 尺寸(px) | 比例 | 特殊要求 |
|---|---|---|---|
| 官网团队页 | 400x500 | 4:5 | 白色底 |
| 工牌/系统头像 | 300x300 | 1:1 | 居中裁面部 |
| 名片 | 600x800 | 3:4 | 原始比例缩放 |
| LinkedIn横幅 | 1584x396 | 4:1 | 模糊背景延伸 |
| 微信头像 | 500x500 | 1:1 | 圆形蒙版 |
核心代码:批量裁剪+智能居中
from PIL import Image, ImageFilter, ImageDraw
import os
from pathlib import Path
class PortraitProcessor:
"""企业形象照批量处理工具"""
def __init__(self, input_dir: str, output_dir: str):
self.input_dir = Path(input_dir)
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
# 定义输出规格
self.specs = {
'website': {'size': (400, 500), 'mode': 'cover'},
'badge': {'size': (300, 300), 'mode': 'face_center'},
'card': {'size': (600, 800), 'mode': 'cover'},
'linkedin': {'size': (1584, 396), 'mode': 'blur_extend'},
'wechat': {'size': (500, 500), 'mode': 'circle'},
}
def smart_crop(self, img: Image.Image, target_size: tuple, mode: str) -> Image.Image:
"""智能裁剪:根据模式选择裁剪策略"""
if mode == 'cover':
return self._crop_cover(img, target_size)
elif mode == 'face_center':
return self._crop_face_center(img, target_size)
elif mode == 'blur_extend':
return self._blur_extend(img, target_size)
elif mode == 'circle':
return self._crop_circle(img, target_size)
else:
return img.resize(target_size, Image.LANCZOS)
def _crop_cover(self, img: Image.Image, target_size: tuple) -> Image.Image:
"""等比缩放后居中裁剪(类似CSS object-fit: cover)"""
img_ratio = img.width / img.height
target_ratio = target_size[0] / target_size[1]
if img_ratio > target_ratio:
new_height = target_size[1]
new_width = int(new_height * img_ratio)
else:
new_width = target_size[0]
new_height = int(new_width / img_ratio)
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
# 居中裁剪(形象照重点在上半部分,所以垂直方向偏上裁剪)
left = (new_width - target_size[0]) // 2
top = int((new_height - target_size[1]) * 0.3) # 偏上30%
return img_resized.crop((left, top, left + target_size[0], top + target_size[1]))
def _crop_face_center(self, img: Image.Image, target_size: tuple) -> Image.Image:
"""以面部为中心的正方形裁剪"""
face_center_y = img.height * 0.3
face_center_x = img.width * 0.5
crop_size = min(img.width, img.height) * 0.7
left = int(face_center_x - crop_size / 2)
top = int(face_center_y - crop_size / 3)
right = int(left + crop_size)
bottom = int(top + crop_size)
left = max(0, left)
top = max(0, top)
right = min(img.width, right)
bottom = min(img.height, bottom)
cropped = img.crop((left, top, right, bottom))
return cropped.resize(target_size, Image.LANCZOS)
def _blur_extend(self, img: Image.Image, target_size: tuple) -> Image.Image:
"""模糊背景延伸(适合横幅类超宽比例)"""
bg = img.resize(target_size, Image.LANCZOS)
bg = bg.filter(ImageFilter.GaussianBlur(radius=20))
img_ratio = img.width / img.height
new_height = target_size[1]
new_width = int(new_height * img_ratio)
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
paste_x = (target_size[0] - new_width) // 2
bg.paste(img_resized, (paste_x, 0))
return bg
def _crop_circle(self, img: Image.Image, target_size: tuple) -> Image.Image:
"""圆形蒙版裁剪"""
square = self._crop_face_center(img, target_size)
mask = Image.new('L', target_size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse([0, 0, target_size[0]-1, target_size[1]-1], fill=255)
result = Image.new('RGBA', target_size, (255, 255, 255, 0))
result.paste(square, mask=mask)
return result
def process_all(self):
"""批量处理所有图片"""
image_files = list(self.input_dir.glob('*.jpg')) + list(self.input_dir.glob('*.png'))
print(f"找到 {len(image_files)} 张图片,开始处理...")
for img_path in image_files:
img = Image.open(img_path).convert('RGB')
name = img_path.stem
for spec_name, spec in self.specs.items():
output_subdir = self.output_dir / spec_name
output_subdir.mkdir(exist_ok=True)
result = self.smart_crop(img, spec['size'], spec['mode'])
if spec['mode'] == 'circle':
result.save(output_subdir / f"{name}.png", 'PNG')
else:
result.save(output_subdir / f"{name}.jpg", 'JPEG', quality=92)
print(f" ✓ {name} -> 5种规格已生成")
print(f"\n全部完成!输出目录:{self.output_dir}")
# 使用示例
if __name__ == '__main__':
processor = PortraitProcessor(
input_dir='./portraits/original',
output_dir='./portraits/processed'
)
processor.process_all()
进阶:批量重命名(按工号)
摄影机构交付的文件通常是 DSC_0001.jpg 这种命名。我们需要按工号重命名方便系统导入:
import csv
def rename_by_mapping(photo_dir: str, mapping_file: str):
"""
根据映射表批量重命名
mapping.csv格式:原文件名,工号,姓名
"""
with open(mapping_file, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
next(reader) # 跳过表头
for row in reader:
original_name, emp_id, emp_name = row[0], row[1], row[2]
src = Path(photo_dir) / original_name
dst = Path(photo_dir) / f"{emp_id}_{emp_name}.jpg"
if src.exists():
src.rename(dst)
print(f" {original_name} -> {emp_id}_{emp_name}.jpg")
else:
print(f" [跳过] {original_name} 不存在")
# mapping.csv 示例:
# 原文件名,工号,姓名
# DSC_0001.jpg,EMP001,张三
# DSC_0002.jpg,EMP002,李四
补充:背景色统一化
有时候虽然是同一批拍的,但由于灯光微调,不同人的背景白可能有色差。用以下脚本做批量背景色统一:
import numpy as np
from PIL import Image
def normalize_background(img: Image.Image, target_bg=(255, 255, 255), threshold=30):
"""
将接近白色的背景统一为纯白
threshold: 与纯白的RGB距离阈值
"""
img_array = np.array(img)
white = np.array([255, 255, 255])
distance = np.sqrt(np.sum((img_array.astype(float) - white) ** 2, axis=2))
mask = distance < threshold
img_array[mask] = target_bg
return Image.fromarray(img_array)
# 批量处理
def batch_normalize_bg(input_dir: str, output_dir: str):
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
for img_file in input_path.glob('*.jpg'):
img = Image.open(img_file)
result = normalize_background(img)
result.save(output_path / img_file.name, quality=95)
print(f" ✓ {img_file.name} 背景已统一")
实际效果
跑完这套脚本,50个人的照片在30秒内完成了5种规格的裁剪输出。总共生成250张图片,按用途分好了文件夹。
IT直接导入OA系统替换头像,市场部拿走官网尺寸的图上了团队页,行政拿着工牌尺寸的去制卡。整个过程从"拿到照片"到"全部场景覆盖"只花了不到1小时。
顺便说一句,沐王府形象摄影的交付物做得挺规范,原片都是统一的sRGB色彩空间、3000x4000px、命名也有规律,省去了很多前期整理的功夫。如果你们IT部门也有类似需求,选摄影供应商时可以提前沟通好交付规格。
总结
对于有技术能力的企业IT,拍完形象照后的批量处理完全可以自动化。核心就是:
- 用Pillow处理裁剪和缩放
- 根据使用场景定义好输出规格
- 面部居中算法保证裁剪后人物不会偏移
- 背景统一保证视觉一致性
代码已开源,需要的自取。