图片处理是很多项目中绕不开的痛点。本质上是一个”分类+裁剪”的 ETL 流程,但真正做好需要考虑安全性、隔离性、一致性。本文记录完整的设计思路和代码实现。
设计目标
建立一个稳定、可靠且具备等幂性的图片处理系统:
- 安全性:原始图片在未完成最终归类前禁止任何修改
- 等幂性:脚本可重复执行,不会导致重复处理或数据污染
- 隔离性:所有中间处理过程在独立的临时沙箱目录中完成
- 一致性:分类与裁剪共用一套严格的乘法比例判定逻辑
核心组件
系统包含三个核心组件:
1. normalize_image_names.py - 扩展名规范化
处理图片扩展名的大小写问题,.JPEG → .jpg:
def normalize_dir(directory, verbose=False):
if not os.path.isdir(directory):
return False
for filename in files:
new_ext = ext.lower()
if new_ext == '.jpeg': new_ext = '.jpg'
if ext != new_ext:
# 备份 → 重命名 → 删除备份
backup_path = old_path + '.bak'
shutil.copy2(old_path, backup_path)
os.rename(old_path, new_path)
os.remove(backup_path)
使用备份机制保证重命名失败时可回滚。
2. classify_images_by_ratio.py - 分类主控
负责整体调度、扫描与物理移动:
- 环境初始化:启动时清空
~tmp/目录 - 非递归扫描:仅处理当前目录下的图片文件
- 比例判定:严格乘法公式
- 物理搬运:同名冲突自动重命名
3. resize_image__by_ratio.py - 裁剪工具
作为底层工具模块,不维护比例字典,仅根据传入参数执行裁剪:
def crop_and_save(src_path, dest_path, tw, th):
with Image.open(src_path) as img:
w, h = img.size
if w * th > h * tw:
new_w = round(h * tw / th)
left = (w - new_w) // 2
img = img.crop((left, 0, left + new_w, h))
else:
new_h = round(w * th / tw)
top = (h - new_h) // 2
img = img.crop((0, top, w, top + new_h))
img.save(dest_path, quality=95, subsampling=0)
关键数学逻辑
为避免浮点数除法产生的精度误差,系统统一采用交叉乘法判定:
# 分类判定:找到 diff 最小的比例
diff = abs(w * th - h * tw)
if diff < min_diff:
best_key = key
# 验证判定:允许 10 像素内的舍入误差
return abs(w * th - h * tw) <= 10
ETL 工作流程
1. Start: 清空 ~tmp/
2. Extract: 获取图片清单(白名单过滤扩展名)
3. Transform & Load:
IF 比例 == 预设比例:
MOVE 原图 → 目标目录/
ELSE:
COPY 原图 → ~tmp/副本
调用裁剪工具
IF 裁剪成功:
MOVE ~tmp/副本 → 目标目录/
REMOVE 原图
ELSE:
记录 Error
4. End: 清理 ~tmp/
核心原则:原图仅在操作成功后删除。
异常处理
| 场景 | 系统反应 | 安全保障 |
|---|---|---|
| 处理中断 | 原图未被删除 | 重启后原图完整 |
| 同名冲突 | 自动重命名 | 防止覆盖 |
| 裁剪失败 | 保留原图 | 坏图不会进入分类目录 |
| 重复执行 | 已无图片可处理 | 等幂性保证 |
实现细节
白名单过滤
使用白名单而非黑名单,避免误处理非图片文件:
IMAGE_EXTS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff'}
for filename in files:
ext = os.path.splitext(filename)[1].lower()
if ext not in IMAGE_EXTS:
continue
事务性文件操作
保证操作原子性:
def safe_move(src, dest):
"""先复制再删除,确保事务安全"""
shutil.copy2(src, dest)
os.remove(src)
脚本相对路径
要求从工具目录运行,使用相对路径调用子脚本:
RESIZE_SCRIPT = './resize_image__by_ratio.py'
输出结构
images/
├── 0101/ # 1:1 比例
├── 1609/ # 16:9 比例
├── classify_*.log
└── ~tmp/ # 运行后清空
总结
这类工具的核心挑战不在于”能做什么”,而在于”如何安全地做”:
- 等幂性需要每一步都可验证、可重试
- 事务安全需要先备份再操作,失败可回滚
- 隔离处理需要沙盒环境,原数据不受影响
作为工具脚本,可维护性同样重要:日志记录、白名单过滤、详细错误信息都是为了让调试更轻松。