本页聚焦「处理与变换」环节:像素归一化、数据扩增/增强、特征提取、颜色空间变换、几何/光度变换。质检与脏数据检测请见《图像数据清洗全流程》,本页不再重复。
原始图像像素是 0~255 的 uint8 整数,直接喂进神经网络会有两个问题:数值太大导致梯度爆炸、不同通道量纲不一致。两步处理几乎是深度学习图像任务的标配:
transforms.ToTensor() 自动完成,同时把 HWC 排成 CHW。from torchvision import transforms # ① 除以 255:ToTensor 自动把 [0,255] uint8 → [0,1] float,并 HWC → CHW # ② 标准化:减均值、除标准差(ImageNet 统计量,迁移学习几乎必用) transform = transforms.Compose([ transforms.ToTensor(), # PIL/ndarray → Tensor [0,1] transforms.Normalize( mean=[0.485, 0.456, 0.406], # ImageNet R/G/B 均值 std =[0.229, 0.224, 0.225], # ImageNet R/G/B 标准差 ) ]) # 公式:x_norm = (x - mean) / std,结果约在 [-2.1, 2.6] 之间 # 用自己的数据集时,应自行统计训练集的 mean/std,不要盲套 ImageNet
RGB 不总是最优表示。把图像转到其它颜色空间,可以让光照、色度、亮度解耦,特定任务更鲁棒:
| 颜色空间 | 通道含义 | 典型用途 |
|---|---|---|
| HSV | 色相 / 饱和度 / 明度 | 光照敏感任务(颜色追踪、阈值分割) |
| LAB | 亮度 / a 红绿 / b 蓝黄 | 感知均匀,颜色检索/聚类 |
| YCbCr | 亮度 / 色差蓝 / 色差红 | 肤色检测、JPEG 压缩域 |
| GRAY | 单通道灰度 | 传统特征 HOG/SIFT 前置、降维 |
import cv2 import numpy as np img_bgr = cv2.imread('cat.jpg') # OpenCV 默认读入 BGR img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # 先转 RGB,符合人类直觉 # RGB → HSV:色相追踪、光照鲁棒阈值 hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV) h, s, v = cv2.split(hsv) # 通道分离 # RGB → LAB:感知均匀,做颜色检索/聚类 lab = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2LAB) l, a, b = cv2.split(lab) # RGB → YCbCr:肤色检测常用 ycbcr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2YCrCb) y, cr, cb = cv2.split(ycbcr) # RGB → GRAY:传统手工特征前置 gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) # 形状 (H,W)
每个通道用纯色块示意其语义,例如 HSV 的 V 通道用亮度渐变表示明暗。实际工程中通道分离常用于按通道做阈值或独立处理。
几何变换改变像素的空间位置:旋转、翻转、缩放、裁剪、平移。训练时用随机版做增强,推理时只做确定性的 Resize + CenterCrop。
RandomRotation / RandomHorizontalFlip / RandomResizedCrop。每个 epoch 看到不同变体,提升平移/旋转/尺度不变性。Resize + CenterCrop + Normalize。关闭一切随机,保证结果可复现。from torchvision import transforms # ===== 训练 transform:随机几何增强 ===== train_tfm = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), # 随机缩放+裁剪到 224 transforms.RandomRotation(15), # 随机旋转 ±15° transforms.RandomHorizontalFlip(p=0.5), # 50% 概率水平翻转 transforms.ColorJitter(brightness=0.2, contrast=0.2), # 顺带光度抖动 transforms.ToTensor(), transforms.Normalize(mean=[0.485,0.456,0.406], std =[0.229,0.224,0.225]), ]) # ===== 推理 transform:只 Resize + CenterCrop + Normalize ===== infer_tfm = transforms.Compose([ transforms.Resize(256), # 短边缩到 256 transforms.CenterCrop(224), # 中心裁 224×224 transforms.ToTensor(), transforms.Normalize(mean=[0.485,0.456,0.406], std =[0.229,0.224,0.225]), ])
CSS transform 演示:rotate + scaleX(-1) + scale。真实训练中,这些变换在 PIL/numpy 上完成,标签同步处理。
光度变换改变像素的数值而不改变位置:亮度、对比度、饱和度、色相抖动(ColorJitter)、高斯噪声、模糊、JPEG 压缩模拟。训练时用提升光照鲁棒性,测试时必须关闭。
from torchvision import transforms import torch # ColorJitter:亮度/对比度/饱和度/色相 同时抖动 color_jitter = transforms.ColorJitter( brightness=0.3, # 亮度抖动 ±30% contrast=0.3, # 对比度抖动 ±30% saturation=0.3, # 饱和度抖动 ±30% hue=0.1, # 色相抖动 ±10%(过大颜色失真) ) # 高斯模糊:模拟轻微失焦,提升清晰度鲁棒性 gaussian_blur = transforms.GaussianBlur(kernel_size=5, sigma=(0.1, 2.0)) # 高斯噪声:模拟传感器噪声 def add_noise(x): return x + torch.randn_like(x) * 0.05 train_tfm = transforms.Compose([ transforms.RandomApply([color_jitter], p=0.8), transforms.RandomApply([gaussian_blur], p=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485,0.456,0.406], std =[0.229,0.224,0.225]), ]) # JPEG 压缩模拟:用 PIL 的 quality 参数模拟有损压缩伪影 from io import BytesIO from PIL import Image def jpeg_compress(img, quality=50): buf = BytesIO(); img.save(buf, 'JPEG', quality=quality) return Image.open(buf)
CSS filter 实时演示,对应 torchvision ColorJitter + GaussianBlur 的效果。拖到极端值即可理解「增强强度过大反而伤性能」。
增强分两类:单样本增强(一张图自己变)和批次混合增强(一个 batch 内多张图插值,标签也插值)。后者能显著提升泛化,尤其在小数据集上。
x̃ = λx_i + (1-λ)x_j,标签也按 λ 混合。平滑决策边界,提升泛化。import torch import numpy as np # ===== MixUp:batch 内两两混合,标签也插值 ===== def mixup_data(x, y, alpha=1.0): # 从 Beta(alpha,alpha) 采样混合系数 λ lam = np.random.beta(alpha, alpha) if alpha > 0 else 1.0 index = torch.randperm(x.size(0), device=x.device) # 随机打乱下标 mixed_x = lam * x + (1 - lam) * x[index] # 图像混合 y_a, y_b = y, y[index] # 标签也对应混合 return mixed_x, y_a, y_b, lam # 训练 step 里:loss 按 λ 加权 def mixup_criterion(crit, pred, y_a, y_b, lam): return lam * crit(pred, y_a) + (1 - lam) * crit(pred, y_b) # ===== CutMix:贴矩形块,标签按面积比例混合 ===== def cutmix_data(x, y, beta=1.0): lam = np.random.beta(beta, beta) index = torch.randperm(x.size(0), device=x.device) _, _, H, W = x.shape cut_h = int(H * np.sqrt(1 - lam)); cut_w = int(W * np.sqrt(1 - lam)) cy = np.random.randint(H - cut_h); cx = np.random.randint(W - cut_w) x[:, :, cy:cy+cut_h, cx:cx+cut_w] = x[index, :, cy:cy+cut_h, cx:cx+cut_w] lam = 1 - (cut_h * cut_w) / (H * W) # 按实际面积修正 λ return x, y, y[index], lam
「特征」是把图像变成可用于下游任务的向量。两条路线:手工特征(传统机器学习时代)和深度特征(CNN 中间层 / 预训练 backbone 的 embedding)。
from skimage.feature import hog from skimage.color import rgb2gray import torch import torch.nn as nn from torchvision import models, transforms # ===== ① 手工特征:HOG(行人检测经典特征)===== gray = rgb2gray(img) # HOG 在灰度图上算 fd, hog_img = hog( gray, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2), visualize=True, ) print(fd.shape) # 一维特征向量,喂给 SVM # ===== ② 深度特征:预训练 ResNet 取 embedding ===== model = models.resnet50(weights='IMAGENET1K_V2') model.fc = nn.Identity() # 去掉分类头,输出 2048 维 model.eval() tfm = 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]), ]) x = tfm(img).unsqueeze(0) # (1,3,224,224) with torch.no_grad(): embedding = model(x) # (1, 2048),用于检索/聚类/冷启动 print(embedding.shape)
model.eval() 关闭 BN/Dropout,否则结果不可复现;④ 用预训练模型必须配对应的 ImageNet 归一化参数。
图像特征工程里最容易踩的 5 个坑,每条都是真实项目血泪教训:
Resize + CenterCrop + Normalize。任何 Random* 都会让预测不稳定、指标不可复现。常见错误:复用 train_tfm 做 eval。把本页涉及的变换整理成一份「数据扩增 Pipeline 配置表」,每行一个变换:名称 / 参数 / 训练时启用 / 推理时启用 / 概率。点击下载,可用 pandas 直接 pd.read_csv 解析,作为实验记录或配置固化。
| 变换名称 | 参数 | 训练启用 | 推理启用 | 概率 |
|---|
CSV 含 BOM 头,Excel 打开不乱码;文本字段双引号包裹,pandas 可直接解析。