
开场白:为什么图像处理这么重要?
上节课我们把模型搭起来了,有同学在群里问:”老师,我的训练精度怎么一直上不去?”翻了下他的代码,问题出在数据预处理上——直接把原图扔进模型,连归一化都没做。
这个问题太典型了。很多人觉得图像处理就是”调调参数、裁裁图”,实际上这是深度学习流程里最容易被低估、但影响最大的环节。今天这节课,咱们就把TensorFlow的图像处理从头到尾过一遍。
第一部分:图像在计算机眼里长什么样
先搞清楚数据结构
咱们人眼看到的照片,在计算机里其实是一堆数字。拿灰度图来说,就是个二维数组,每个位置存的是0-255之间的数字,代表那个像素点的亮度。
# 假设这是一张3x3的灰度图
gray_image = [
[0, 128, 255],
[64, 200, 100],
[255, 50, 180]
]
彩色图片呢?多了两个维度——RGB三个通道。红色通道、绿色通道、蓝色通道,三个数组叠在一起。所以一张256×256的彩色照片,在TensorFlow里表示成[256, 256, 3]这样的张量。
这里有个坑要注意:有些库(比如OpenCV)用的是BGR顺序,TensorFlow用的是RGB。混着用容易出问题,我之前就因为这个调了半天颜色不对。
为什么要做预处理?
主要四个原因:
- 尺寸不统一:你收集的照片可能有1920×1080的,有800×600的,模型只能接受固定尺寸输入
- 数值范围太大:像素值0-255,直接喂给神经网络会让梯度爆炸
- 数据太少:一千张图片根本不够训练,得想办法”变出”更多
- 特征不明显:有时候需要强化对比度、调整亮度,帮助模型看清关键信息
第二部分:TensorFlow图像处理的工具箱
tf.image这个模块得熟悉
TensorFlow把图像操作都封装在tf.image里了。我给大家列个常用功能清单:
import tensorflow as tf
# 最常用的几个
tf.image.decode_jpeg() # 解码JPEG图片
tf.image.resize() # 调整大小
tf.image.rgb_to_grayscale() # RGB转灰度
tf.image.flip_left_right() # 水平翻转
tf.image.adjust_brightness() # 调亮度
tf.image.adjust_contrast() # 调对比度
tf.image.rot90() # 旋转90度
这些方法基本能覆盖80%的日常需求。
实战:写个图像标准化函数
最基础的操作——把图片归一化到[0,1]:
def normalize_image(image):
"""
把uint8类型的图像转成float32,然后除以255
"""
# 先转类型,不然uint8除法会出错
image = tf.cast(image, tf.float32)
# 归一化
image = image / 255.0
return image
# 试试看
test_img = tf.random.uniform([224, 224, 3], 0, 255, dtype=tf.uint8)
normalized = normalize_image(test_img)
print(f"原始范围: 0-255")
print(f"归一化后: {tf.reduce_min(normalized).numpy():.2f} - {tf.reduce_max(normalized).numpy():.2f}")
为什么是除以255而不是其他数? 因为uint8的最大值就是255。归一化的目的是把数据压缩到神经网络喜欢的小范围内,避免梯度下降时出现数值问题。
第三部分:数据增强——用一张图”变出”一百张
什么是数据增强
假设你只有500张猫的照片,想训练个猫狗分类器。数据太少怎么办?
数据增强就是通过各种变换——翻转、旋转、调色、裁剪——从原图生成新的训练样本。对模型来说,翻转后的猫还是猫,但它看到的”样本”变多了。
手写一个增强函数
def augment_image(image, label):
"""
随机应用多种增强方式
"""
# 1. 50%概率水平翻转
image = tf.image.random_flip_left_right(image)
# 2. 随机调亮度(±20%)
image = tf.image.random_brightness(image, max_delta=0.2)
# 3. 随机调对比度(0.8-1.2倍)
image = tf.image.random_contrast(image, lower=0.8, upper=1.2)
# 4. 随机旋转(-10°到+10°)
# 这个稍微复杂点,需要把角度转成弧度
angle = tf.random.uniform([], -10, 10) * (3.1415926 / 180)
image = tf.image.rotate(image, angle)
# 5. 别忘了把值限制在[0,1]范围内
image = tf.clip_by_value(image, 0.0, 1.0)
return image, label
这里有几个注意点:
random_flip是随机的,每次调用结果可能不同- 亮度和对比度调整可能让像素值超出范围,所以最后要clip一下
- 旋转会产生黑边,这是正常的,模型能学会忽略
什么时候该用增强、什么时候不用
经验之谈:
适合用增强的场景:
- 数据集小(几千张以下)
- 通用物体识别(猫狗、车辆、人脸等)
- 分类任务
谨慎使用的场景:
- 医学影像(翻转可能改变病理特征)
- 文字识别(旋转可能让字变形)
- 工业缺陷检测(尺度变化可能影响判断)
我之前做过一个项目,客户要识别电路板上的焊点缺陷。一开始用了旋转增强,结果模型把正常的也判成缺陷了,后来才发现焊点的方向性很重要,不能随便转。
第四部分:构建完整的数据处理流水线
从文件到模型的完整流程
真实项目里,数据处理不是单独一步,而是和数据加载、批处理、预取连在一起的。来看个完整例子:
def build_dataset(file_paths, labels, batch_size=32, is_training=True):
"""
构建一个端到端的数据集
file_paths: 图片文件路径列表
labels: 对应的标签
"""
# 1. 创建基础Dataset
dataset = tf.data.Dataset.from_tensor_slices((file_paths, labels))
# 2. 打乱数据(只在训练时)
if is_training:
dataset = dataset.shuffle(buffer_size=1000)
# 3. 定义加载和预处理函数
def load_and_preprocess(path, label):
# 读取文件
image = tf.io.read_file(path)
# 解码(自动识别格式)
image = tf.image.decode_image(image, channels=3)
# 调整大小
image = tf.image.resize(image, [224, 224])
# 归一化
image = image / 255.0
# 训练时做增强
if is_training:
image = tf.image.random_flip_left_right(image)
image = tf.image.random_brightness(image, 0.2)
image = tf.clip_by_value(image, 0.0, 1.0)
return image, label
# 4. 应用预处理(并行化)
dataset = dataset.map(
load_and_preprocess,
num_parallel_calls=tf.data.AUTOTUNE # 自动调整并行度
)
# 5. 创建批次
dataset = dataset.batch(batch_size)
# 6. 预取下一批数据(提速关键)
dataset = dataset.prefetch(tf.data.AUTOTUNE)
return dataset
这个流水线的关键点:
num_parallel_calls=AUTOTUNE:让TensorFlow自动决定用多少CPU核心并行处理prefetch:GPU在处理当前批次时,CPU提前准备下一批,避免等待- 顺序很重要:先shuffle再map再batch,顺序错了会影响效率或结果
性能对比:看看优化前后的差距
我做过一个测试,同样的数据集:
没优化的版本:
- 单纯for循环读取 → 18秒/epoch
加了map并行:
- num_parallel_calls=4 → 8秒/epoch
再加prefetch:
- 加上prefetch(AUTOTUNE) → 5秒/epoch
三倍速度提升,就靠这几行代码。
第五部分:高级技巧——用Keras预处理层
为什么要用Keras的预处理层
TensorFlow 2.x之后,引入了更高级的API:tf.keras.layers.preprocessing。
好处是什么?它能直接嵌入模型里。
传统方法是在Dataset里做预处理,Keras层是在模型forward的时候做。这样的优势:
- 预处理成为模型的一部分,导出模型时一起打包
- 训练和推理时自动切换行为(训练时做增强,推理时不做)
- 可以利用GPU加速
实战代码
from tensorflow.keras import layers
# 构建一个预处理模型
data_augmentation = tf.keras.Sequential([
layers.RandomFlip("horizontal"),
layers.RandomRotation(0.1), # ±10%的旋转范围
layers.RandomZoom(0.1), # ±10%的缩放
layers.RandomContrast(0.2),
])
# 把它嵌入到主模型里
model = tf.keras.Sequential([
# 第一层就是数据增强
data_augmentation,
# 标准化层(Rescaling等价于除以255)
layers.Rescaling(1./255),
# 后面接正常的卷积层
layers.Conv2D(32, 3, activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(64, 3, activation='relu'),
layers.Flatten(),
layers.Dense(10, activation='softmax')
])
这种写法的好处:
- 训练时自动应用增强
- 推理时自动跳过增强
- 模型保存时,预处理逻辑也保存了,部署时不用单独写预处理代码
自己写一个预处理层
有时候内置的层不够用,可以自定义:
class CustomColorJitter(layers.Layer):
"""
自定义色彩抖动层
"""
def __init__(self, brightness=0.2, contrast=0.2, saturation=0.2, **kwargs):
super().__init__(**kwargs)
self.brightness = brightness
self.contrast = contrast
self.saturation = saturation
def call(self, images, training=None):
# 只在训练时应用
if not training:
return images
# 随机亮度
images = tf.image.random_brightness(images, self.brightness)
# 随机对比度
images = tf.image.random_contrast(
images,
1 - self.contrast,
1 + self.contrast
)
# 随机饱和度
images = tf.image.random_saturation(
images,
1 - self.saturation,
1 + self.saturation
)
# 限制范围
images = tf.clip_by_value(images, 0.0, 1.0)
return images
# 用法
model = tf.keras.Sequential([
CustomColorJitter(brightness=0.3, contrast=0.3, saturation=0.3),
# ...其他层
])
第六部分:实践练习(作业时间)
练习1:对比不同的标准化方法
任务:加载一张测试图片,分别应用以下三种标准化,保存并观察效果:
- 简单除以255:
image / 255.0 - ImageNet标准化:
mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225] normalized = (image - mean) / std - 缩放到[-1, 1]:
(image / 127.5) - 1.0
提示:用matplotlib画出三张图,看看视觉上有什么区别。
练习2:生成增强样本可视化
任务:选一张图,用你写的增强函数生成10个不同版本,用subplot排列显示。
参考代码框架:
import matplotlib.pyplot as plt
original_image = load_your_image() # 自己写加载函数
fig, axes = plt.subplots(2, 5, figsize=(15, 6))
for i, ax in enumerate(axes.flat):
if i == 0:
ax.imshow(original_image)
ax.set_title("Original")
else:
augmented, _ = augment_image(original_image, label=0)
ax.imshow(augmented)
ax.set_title(f"Aug {i}")
ax.axis('off')
plt.tight_layout()
plt.show()
练习3:完整流水线实现
任务:构建一个从TFRecord读取数据的完整流水线,要求:
- 解析TFRecord格式
- 解码图像
- 随机裁剪到256×256
- 训练集做增强(翻转+色彩调整)
- 归一化到[-1, 1]
- 批次大小32
- 预取优化
参考结构:
def parse_tfrecord(example):
# 定义TFRecord的格式
feature_description = {
'image': tf.io.FixedLenFeature([], tf.string),
'label': tf.io.FixedLenFeature([], tf.int64),
}
parsed = tf.io.parse_single_example(example, feature_description)
# 解码图像
image = tf.image.decode_jpeg(parsed['image'])
label = parsed['label']
# 你的预处理代码...
return image, label
# 构建Dataset
dataset = tf.data.TFRecordDataset(tfrecord_files)
dataset = dataset.map(parse_tfrecord, num_parallel_calls=tf.data.AUTOTUNE)
# ...后续步骤
第七部分:常见问题答疑
Q1:我的图片大小不一样,必须统一吗?
A:看情况。
CNN模型要求固定输入尺寸,所以必须统一。但统一的方法有两种:
- 直接resize:
tf.image.resize(image, [224, 224])- 优点:简单
- 缺点:可能拉伸变形
- 保持宽高比裁剪/填充:
tf.image.resize_with_pad或resize_with_crop_or_pad- 优点:不变形
- 缺点:可能损失信息或引入黑边
我一般的做法是:先resize到稍大一点(比如256×256),然后随机裁剪到目标大小(224×224),这样既统一了尺寸,又做了一定的增强。
Q2:图像处理应该在CPU还是GPU上做?
A:大部分情况在CPU。
原因:
- 图像解码、resize这些操作,CPU更擅长
- GPU留给模型训练,不要抢占资源
- 用
num_parallel_calls可以充分利用多核CPU
但也有例外:如果用Keras预处理层,它会在GPU上执行,这时候反而更快。
我的建议:
- Dataset.map的预处理 → CPU(用AUTOTUNE并行)
- Keras层的预处理 → GPU(自动处理)
Q3:数据增强会不会让模型学到错误信息?
A:合理设置参数就不会。
几个原则:
- 翻转:左右翻转对大多数任务都安全,但上下翻转要谨慎(比如人脸识别就不能上下翻转)
- 旋转:角度别太大,±15度以内比较安全
- 色彩调整:幅度控制在20-30%以内
- 裁剪:别裁掉关键部分,保证至少80%的目标物体可见
如果是医学影像、文档识别这种对细节敏感的任务,增强要更保守,甚至只用翻转和微调亮度。
Q4:处理超大图像(比如4K照片)有什么技巧?
A:别一口气全加载。
几种方案:
- 分块处理:
tf.image.extract_patches把大图切成小块 - 降采样:先resize到小尺寸训练,fine-tune时再用大图
- 渐进式训练:先用128×128训练,再用256×256,最后用512×512
我之前做卫星图像分类,原图5000×5000,直接处理显存爆炸。后来改成256×256的滑动窗口,每个窗口单独预测,最后再合并结果。
第八部分:课后总结
今天这节课信息量挺大,我总结几个关键点:
- 图像在TensorFlow里就是个多维数组,灰度图[H,W,1],彩色图[H,W,3]
- 预处理的三大目的:统一尺寸、归一化数值、增强数据
- 核心工具:
tf.image模块 +tf.data.Dataset - 增强技术:翻转、旋转、色彩调整、裁剪,但要适度
- 性能优化:并行化map + prefetch,能提速好几倍
- 高级玩法:Keras预处理层,把预处理嵌入模型
下节课预告:我们要进入模型训练了,讲讲损失函数、优化器、学习率调度这些东西。记得把今天的练习做完,因为下节课要用到今天的数据流水线。
有问题随时在群里问,我看到会回复。这节课就到这儿,下课!
原创文章,作者:自动驾驶编程扛把子,如若转载,请注明出处:https://www.key-iot.cn/zj/programming/1632.html