Python 图像处理实战——Pillow、OpenCV 基础操作与 AI 预处理流程
Python 图像处理实战——Pillow、OpenCV 基础操作与 AI 预处理流程
适读人群:做 AI 视觉工程的 Python 开发者、需要处理图像数据的工程师 | 阅读时长:约17分钟 | 核心价值:掌握从图像采集到 AI 模型输入的完整预处理工程方案
从一个批量处理失误说起
两年前,我在一个 AI 识别项目里负责数据预处理,需要把几十万张原始图片处理成模型训练所需的格式。
当时有个实习生小王负责写预处理脚本,他不熟悉图像处理,直接用 Python 打开图片,然后用 PIL 的 resize 随便缩放到 224x224,就传给了模型训练。
结果第一版模型准确率比预期低了将近15个百分点。排查了很久,最后发现问题出在预处理上:小王的缩放没有保持宽高比,很多图片被拉伸变形了;另外颜色通道顺序搞错了(PIL 是 RGB,OpenCV 是 BGR);还有一个问题是没有做归一化,直接把 0-255 的像素值喂给了模型。
这三个问题叠在一起,导致模型学到了错误的特征分布,损失了大量准确率。重新处理数据、重新训练,多花了将近一周时间。
今天这篇,我就来系统讲讲 Python 图像处理的工程实践——从基础操作到 AI 预处理的完整流程。
一、Pillow——Python 图像处理的基础
pip install Pillow基础操作
from PIL import Image, ImageFilter, ImageEnhance, ImageDraw, ImageFont
from pathlib import Path
import numpy as np
def image_basic_ops(image_path: str):
"""Pillow 基础操作演示"""
img = Image.open(image_path)
print(f"格式: {img.format}")
print(f"尺寸: {img.size}") # (width, height)
print(f"模式: {img.mode}") # RGB, RGBA, L(灰度), etc.
# 转换模式
rgb_img = img.convert("RGB") # 统一转 RGB
gray_img = img.convert("L") # 转灰度
# 保持宽高比缩放(关键!)
def resize_with_aspect(img: Image.Image, max_size: int) -> Image.Image:
w, h = img.size
if w > h:
new_w, new_h = max_size, int(h * max_size / w)
else:
new_w, new_h = int(w * max_size / h), max_size
return img.resize((new_w, new_h), Image.LANCZOS)
resized = resize_with_aspect(img, 512)
# 居中裁剪(常用于制作正方形缩略图)
def center_crop(img: Image.Image, size: int) -> Image.Image:
w, h = img.size
left = (w - size) // 2
top = (h - size) // 2
return img.crop((left, top, left + size, top + size))
# 先缩放到比目标大一点,再中心裁剪
target = 224
scale_size = int(target * 256 / 224)
scaled = resize_with_aspect(img, scale_size)
cropped = center_crop(scaled, target)
# 图像增强
enhancer = ImageEnhance.Brightness(cropped)
bright = enhancer.enhance(1.2) # 提高亮度20%
contrast_enhancer = ImageEnhance.Contrast(cropped)
high_contrast = contrast_enhancer.enhance(1.3)
# 滤镜
blurred = cropped.filter(ImageFilter.GaussianBlur(radius=2))
sharpened = cropped.filter(ImageFilter.SHARPEN)
# 保存(控制质量)
cropped.save("output.jpg", quality=85, optimize=True)
cropped.save("output.webp", quality=80) # WebP 通常比 JPEG 小30%
return cropped批量处理工具
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm
def batch_process_images(
input_dir: str,
output_dir: str,
target_size: int = 224,
max_workers: int = 8,
) -> dict:
"""批量图像预处理"""
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
image_files = list(input_path.rglob("*.jpg")) + \
list(input_path.rglob("*.png")) + \
list(input_path.rglob("*.jpeg"))
stats = {"success": 0, "failed": 0, "skipped": 0}
def process_one(src: Path) -> tuple[bool, str]:
# 计算输出路径(保持目录结构)
rel_path = src.relative_to(input_path)
dst = output_path / rel_path.with_suffix(".jpg")
dst.parent.mkdir(parents=True, exist_ok=True)
if dst.exists():
return True, "skipped"
try:
img = Image.open(src).convert("RGB")
# 保持宽高比缩放 + 中心裁剪
scale = max(target_size / img.width, target_size / img.height)
new_w = int(img.width * scale)
new_h = int(img.height * scale)
img = img.resize((new_w, new_h), Image.LANCZOS)
left = (new_w - target_size) // 2
top = (new_h - target_size) // 2
img = img.crop((left, top, left + target_size, top + target_size))
img.save(str(dst), quality=90, optimize=True)
return True, "success"
except Exception as e:
return False, str(e)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(process_one, f): f for f in image_files}
for future in tqdm(as_completed(futures), total=len(futures), desc="处理图像"):
success, status = future.result()
if status == "skipped":
stats["skipped"] += 1
elif success:
stats["success"] += 1
else:
stats["failed"] += 1
return stats二、OpenCV——计算机视觉的核心工具
pip install opencv-python-headless # 服务器环境用这个,不包含 GUI踩坑实录1:PIL 和 OpenCV 颜色通道顺序不同
现象:用 PIL 打开图片,转成 numpy 数组,传给 OpenCV 处理,颜色变了(蓝红颠倒)。
原因:PIL 使用 RGB 顺序,OpenCV 使用 BGR 顺序,两者通道排列正好相反。
解法:在 PIL 和 OpenCV 之间转换时,始终做通道转换。
import cv2
import numpy as np
from PIL import Image
def pil_to_cv2(pil_img: Image.Image) -> np.ndarray:
"""PIL Image 转 OpenCV ndarray(RGB -> BGR)"""
return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
def cv2_to_pil(cv2_img: np.ndarray) -> Image.Image:
"""OpenCV ndarray 转 PIL Image(BGR -> RGB)"""
return Image.fromarray(cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB))
# OpenCV 常用操作
def opencv_operations(image_path: str):
img = cv2.imread(image_path) # 读取为 BGR
# 颜色空间转换
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 边缘检测
edges = cv2.Canny(gray, threshold1=100, threshold2=200)
# 高斯模糊
blurred = cv2.GaussianBlur(img, (5, 5), 0)
# 直方图均衡化(增强低对比度图像)
equalized = cv2.equalizeHist(gray)
# 旋转
h, w = img.shape[:2]
center = (w // 2, h // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, angle=15, scale=1.0)
rotated = cv2.warpAffine(img, rotation_matrix, (w, h))
# 调整大小
resized = cv2.resize(img, (224, 224), interpolation=cv2.INTER_LANCZOS4)
return resized三、AI 模型预处理——这才是重点
import numpy as np
from PIL import Image
import torch
from torchvision import transforms
# PyTorch/Torchvision 标准预处理
def get_imagenet_transforms(mode: str = "train"):
"""ImageNet 标准预处理,适用于大多数预训练 CNN 模型"""
if mode == "train":
return transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪+缩放
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.ColorJitter(
brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1
),
transforms.ToTensor(), # PIL -> Tensor,归一化到 [0,1]
transforms.Normalize(
mean=[0.485, 0.456, 0.406], # ImageNet 均值
std=[0.229, 0.224, 0.225], # ImageNet 标准差
),
])
else: # val / test
return transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
),
])
# 自定义增强管道(针对特定任务)
class MedicalImagePreprocessor:
"""
医学图像预处理
医学图像有特殊要求:不能随意做颜色增强,但需要处理类不平衡
"""
def __init__(self, target_size: int = 224, mode: str = "train"):
self.target_size = target_size
self.mode = mode
def __call__(self, img: Image.Image) -> torch.Tensor:
# 统一转灰度(很多医学图像是灰度的)
img = img.convert("L")
# 保持宽高比 padding 到正方形(医学图像不能拉伸)
img = self._pad_to_square(img)
if self.mode == "train":
# 轻度随机增强(医学图像不适合太大幅度的增强)
if np.random.random() < 0.5:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
if np.random.random() < 0.3:
from PIL import ImageFilter
img = img.filter(ImageFilter.GaussianBlur(radius=0.5))
img = img.resize((self.target_size, self.target_size), Image.LANCZOS)
# 转 Tensor 并归一化
arr = np.array(img, dtype=np.float32) / 255.0
# 灰度转 3 通道(复制3次),以兼容 RGB 预训练模型
arr = np.stack([arr, arr, arr], axis=0)
# 使用 ImageNet 统计值归一化
mean = np.array([0.485, 0.456, 0.406]).reshape(3, 1, 1)
std = np.array([0.229, 0.224, 0.225]).reshape(3, 1, 1)
arr = (arr - mean) / std
return torch.tensor(arr, dtype=torch.float32)
def _pad_to_square(self, img: Image.Image) -> Image.Image:
"""用黑色 padding 把图像补为正方形"""
w, h = img.size
size = max(w, h)
result = Image.new(img.mode, (size, size), 0)
result.paste(img, ((size - w) // 2, (size - h) // 2))
return result四、踩坑实录
踩坑实录2:归一化方式不对,模型推理结果异常
现象:模型训练时准确率很高,但推理时效果很差。
原因:训练时用的 mean=[0.485, 0.456, 0.406] 归一化,推理时直接除以255,没有做减均值操作,输入分布不一致。
解法:训练和推理必须用完全相同的预处理流程,最好把预处理封装成函数,两处都调用同一个函数。
踩坑实录3:大量小图片读取,IO 成为瓶颈
现象:训练时 GPU 利用率只有40%,大量时间在等待数据加载。
原因:每个 batch 都在实时从磁盘读取和处理图片,IO 速度跟不上 GPU 消耗速度。
解法:1)预先把图片处理好存为 numpy 格式(npy)或 HDF5;2)DataLoader 设置 num_workers=4-8、pin_memory=True;3)把小文件打包成 WebDataset 的 tar 格式。
五、选型建议
| 场景 | 推荐工具 |
|---|---|
| 日常图片处理(缩放/裁剪/格式转换) | Pillow |
| 计算机视觉算法(边缘检测/目标检测) | OpenCV |
| AI 模型训练数据预处理 | torchvision.transforms |
| 高性能批量处理 | Pillow + ThreadPoolExecutor |
| 视频帧处理 | OpenCV |
| 科学图像分析 | scikit-image |
图像预处理是 AI 工程里最容易被忽视、出问题却很难排查的环节。小王那次的教训让我们团队形成了一个规范:所有预处理流程必须有单元测试,测试内容包括:输出尺寸是否正确、像素值范围是否在预期区间、通道顺序是否正确。这些测试帮我们避开了好几次类似的坑。
