🖼️ 特征工程 · 图像数据

图像数据 · 特征工程与数据处理全流程

本页聚焦「处理与变换」环节:像素归一化、数据扩增/增强、特征提取、颜色空间变换、几何/光度变换。质检与脏数据检测请见《图像数据清洗全流程》,本页不再重复。

① 像素归一化与标准化

原始图像像素是 0~255 的 uint8 整数,直接喂进神经网络会有两个问题:数值太大导致梯度爆炸、不同通道量纲不一致。两步处理几乎是深度学习图像任务的标配:

🔢
除以 255 → [0,1]
最简单的归一化:把 0~255 压缩到 [0,1] 浮点数。配合 transforms.ToTensor() 自动完成,同时把 HWC 排成 CHW。
⚖️
减均值除标准差 → 0 均值
用 ImageNet 三通道均值方差做标准化,使每个通道近似 0 均值 1 方差,加速收敛、提升数值稳定性,也是迁移学习的默认参数。
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
💡 为什么要归一化:① 数值小且分布稳定,SGD 收敛更快、学习率更好调;② 与预训练权重对齐(ImageNet 预训练模型期望输入是 ImageNet 归一化后的分布),否则迁移效果会打折;③ 三通道量纲统一,避免某通道因数值大而主导梯度。
🧪 交互演示:拖动 R/G/B 滑块,实时看归一化后的数值与颜色
R 通道128
G 通道128
B 通道128
原始 [0,1]:(0.502, 0.502, 0.502)
ImageNet 标准化后:(0.075, 0.201, 0.063)
公式:(x/255 - mean) / std

② 颜色空间变换

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)
⚠️ 常见坑:① OpenCV 默认 BGR 而非 RGB,混用 plt.imshow 会颜色错乱;② HSV 中 H 范围因库而异(OpenCV 0~179,skimage 0~1),写阈值前先确认;③ 深度学习多数直接吃 RGB,颜色空间变换更多用于传统 CV 流程或数据扩增。
🧪 交互演示:选择颜色空间,查看三个通道的语义

每个通道用纯色块示意其语义,例如 HSV 的 V 通道用亮度渐变表示明暗。实际工程中通道分离常用于按通道做阈值或独立处理。

③ 几何变换(Geometry)

几何变换改变像素的空间位置:旋转、翻转、缩放、裁剪、平移。训练时用随机版做增强,推理时只做确定性的 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]),
])
⚠️ 常见坑:① 垂直翻转要慎用——行人/文字翻转后语义全错,只有天空/纹理类才适合;② RandomResizedCrop 的 scale 下限别设太小,否则裁出局部碎片反而引入噪声;③ 检测/分割任务做几何变换时, bbox 和 mask 必须同步变换,否则标签错位。
🧪 交互演示:旋转角度 / 水平翻转 / 缩放,对示意图的实时效果
旋转角度0°
缩放1.00
🐱

CSS transform 演示:rotate + scaleX(-1) + scale。真实训练中,这些变换在 PIL/numpy 上完成,标签同步处理。

④ 光度变换(Photometric)

光度变换改变像素的数值而不改变位置:亮度、对比度、饱和度、色相抖动(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)
⚠️ 常见坑:① 测试时一定要关——光度抖动是随机的,不关会让预测不稳定;② 色相 hue 抖动范围别超 0.2,否则颜色语义被破坏(红苹果变绿苹果);③ 模糊/噪声强度过大反而让模型学不到东西,找平衡点。
🧪 交互演示:亮度/对比度/饱和度/模糊,对示意图实时 CSS filter
亮度 brightness100%
对比度 contrast100%
饱和度 saturate100%
模糊 blur0px
🌸

CSS filter 实时演示,对应 torchvision ColorJitter + GaussianBlur 的效果。拖到极端值即可理解「增强强度过大反而伤性能」。

⑤ 数据扩增策略(Augmentation Policy)

增强分两类:单样本增强(一张图自己变)和批次混合增强(一个 batch 内多张图插值,标签也插值)。后者能显著提升泛化,尤其在小数据集上。

🧩
MixUp
两张图按 λ 线性混合:x̃ = λx_i + (1-λ)x_j,标签也按 λ 混合。平滑决策边界,提升泛化。
✂️
CutMix
把一张图的局部矩形块贴到另一张上,标签按面积比例混合。比 MixUp 保留更多局部纹理,检测/分割也适用。
🕳️
Cutout
随机遮挡一块矩形区域(填 0 或均值),强迫模型看局部。只改一张图、标签不变,实现最简单。
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
⚠️ 何时用 / 何时有害:✅ 该用:小数据集、泛化差、ImageNet 量级分类(MixUp/CutMix 是 SOTA 标配)。❌ 有害:细粒度分类(混合后类别语义模糊)、医学影像(病灶小且标签敏感)、检测/分割中 bbox 混合复杂慎用。MixUp 的 alpha 常取 0.2~1.0,越大越激进。

⑥ 特征提取与表示

「特征」是把图像变成可用于下游任务的向量。两条路线:手工特征(传统机器学习时代)和深度特征(CNN 中间层 / 预训练 backbone 的 embedding)。

✋ 手工特征(传统)
HOG:梯度方向直方图,行人检测经典;
SIFT:尺度不变特征点,图像配准;
LBP:局部二值模式,纹理分类。
适合小数据、可解释、无 GPU 场景,配合 SVM 用。
🧠 深度特征(现代)
CNN 中间层特征图:可视化模型学到的模式;
预训练 backbone 输出 embedding:去掉分类头取 2048/512 维向量,用于检索/聚类/冷启动;
多数任务端到端,但 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)
💡 选型建议:① 现代深度学习多数端到端训练,不单独提特征;② 但 embedding 仍用于检索/聚类/推荐冷启动(没有用户行为时用视觉相似度兜底);③ 取 embedding 时记得 model.eval() 关闭 BN/Dropout,否则结果不可复现;④ 用预训练模型必须配对应的 ImageNet 归一化参数。

⑦ 常见坑与最佳实践

图像特征工程里最容易踩的 5 个坑,每条都是真实项目血泪教训:

① 测试/推理阶段必须关闭随机增强
只做 Resize + CenterCrop + Normalize。任何 Random* 都会让预测不稳定、指标不可复现。常见错误:复用 train_tfm 做 eval。
② 归一化统计量必须用训练集计算
mean/std 只能在训练集上统计,测试集只能 transform。用全量或测试集统计 = 数据泄露,指标虚高。自己的数据集别盲套 ImageNet 参数。
③ 增强强度过大反而伤性能
旋转 360°、hue 抖到 0.5、模糊到看不清……会让模型学到错误语义。建议小步调参,用验证集曲线找「刚好够用」的强度。
④ MixUp/CutMix 不适用于细粒度/标签敏感任务
细粒度分类(区分 200 种鸟)、医学影像(病灶小且标签敏感),混合后类别语义模糊。检测/分割中 bbox 混合实现复杂,慎用。
⑤ 预训练模型用对应 ImageNet 归一化参数
用 ImageNet 预训练权重,输入就必须用 mean=[0.485,0.456,0.406]/std=[0.229,0.224,0.225] 归一化。否则输入分布与预训练不匹配,迁移效果打折。

⑧ 下载扩增配置清单 CSV

把本页涉及的变换整理成一份「数据扩增 Pipeline 配置表」,每行一个变换:名称 / 参数 / 训练时启用 / 推理时启用 / 概率。点击下载,可用 pandas 直接 pd.read_csv 解析,作为实验记录或配置固化。

变换名称 参数 训练启用 推理启用 概率

CSV 含 BOM 头,Excel 打开不乱码;文本字段双引号包裹,pandas 可直接解析。