3、图像基础:图像张量表示、TensorFlow图像读取与解码、图像基本变换
好,咱们正式开始聊图像。图像处理这块,说白了就是跟像素打交道。你想想看,一张照片在计算机眼里是什么?就是一串数字。我刚开始接触时也觉得挺神奇的——一张猫的照片,底层居然是个三维数组。
3.1 图像张量表示
图像在TensorFlow里,通常用张量(Tensor)来表示。一张彩色图片,形状是 (height, width, channels)。height和width就是像素的行列数,channels呢,RGB图就是3,灰度图就是1。
举个例子。一张224x224的彩色猫图,张量形状就是 (224, 224, 3)。每个像素点的值范围是0到255,代表红绿蓝的强度。嗯,这里要注意:TensorFlow里默认的数据类型是float32,但图像读进来往往是uint8。我个人习惯先转成float32再归一化到[0,1]区间,这样模型训练更稳定。
核心概念:图像张量的维度顺序。TensorFlow用的是HWC(高度、宽度、通道),而有些框架用CHW。别搞混了,我在项目中就踩过这个坑——模型推理结果全乱套,查了半天才发现是维度顺序不对。
import tensorflow as tf
# 模拟一张224x224的彩色图像
image_tensor = tf.random.uniform(shape=(224, 224, 3), minval=0, maxval=255, dtype=tf.int32)
print("图像张量形状:", image_tensor.shape)
print("数据类型:", image_tensor.dtype)
# 转float32并归一化
image_float = tf.cast(image_tensor, tf.float32) / 255.0
print("归一化后范围:", tf.reduce_min(image_float).numpy(), "~", tf.reduce_max(image_float).numpy())
3.2 TensorFlow图像读取与解码
实际项目中,图像不会凭空生成,得从文件读。TensorFlow提供了两套方案:tf.io.read_file + tf.image.decode_*,或者直接用tf.keras.utils.load_img。我个人更推荐前者,因为更底层、更可控。
为什么会这样?因为load_img虽然方便,但它默认会做一些预处理(比如调整大小),有时候你并不想要这些“隐形操作”。
我的经验:用tf.io.read_file读取原始字节,再手动解码,这样每一步都清清楚楚。我曾经在做一个OCR项目时,发现图像读进来后文字变模糊了,排查半天才发现是load_img默认做了插值缩放。从那以后,我坚持用底层API。
import tensorflow as tf
# 假设有一张图片路径
image_path = "cat.jpg"
# 方案一:底层API
raw = tf.io.read_file(image_path)
image = tf.image.decode_jpeg(raw, channels=3) # 或decode_png
print("解码后形状:", image.shape)
# 方案二:高层API(不推荐用于生产)
# from tensorflow.keras.utils import load_img
# img = load_img(image_path, target_size=None) # 不指定target_size会保持原尺寸
解码后,图像张量的值还是uint8的0~255。如果你要送入神经网络,记得归一化。我习惯用tf.image.convert_image_dtype,它会自动处理缩放和类型转换。
# 安全归一化
image_normalized = tf.image.convert_image_dtype(image, dtype=tf.float32)
# 等价于:image_float = tf.cast(image, tf.float32) / 255.0
3.3 图像基本变换
图像变换是数据增强的基础。说白了,就是给图片“整容”——旋转、裁剪、翻转、调亮度。目的是让模型见过更多样的情况,不至于换个角度就认不出来。
3.3.1 调整大小
模型输入通常要求固定尺寸。比如ResNet要224x224,EfficientNet要240x240。用tf.image.resize搞定。
# 调整到224x224
resized = tf.image.resize(image_normalized, [224, 224])
print("调整后形状:", resized.shape)
注意:resize默认用双线性插值。如果你做的是分割或检测任务,标签图(mask)要用最近邻插值,否则类别边界会模糊。我曾经在语义分割项目里忘了改插值方式,结果mask边缘出现了一堆“中间类别”,损失函数怎么都降不下去。
3.3.2 翻转与旋转
翻转是最简单的增强手段。水平翻转(左右镜像)几乎不会改变语义——猫翻过来还是猫。垂直翻转要慎用,因为现实世界很少倒着看东西。
# 水平翻转
flipped = tf.image.flip_left_right(image_normalized)
# 随机翻转(训练时常用)
random_flip = tf.image.random_flip_left_right(image_normalized)
旋转呢?tf.image.rot90只能做90度的整数倍旋转。如果你需要任意角度,得用tfa.image.rotate(TensorFlow Addons)。嗯,这里要注意:旋转会引入黑色填充区域,训练时可能会让模型学到“黑色=无效区域”的偏见。
3.3.3 裁剪与填充
中心裁剪是经典操作。比如一张大图,只取中间区域。或者用随机裁剪做数据增强。
# 中心裁剪到200x200
cropped = tf.image.central_crop(image_normalized, central_fraction=0.8)
# 随机裁剪(需要先resize到稍大尺寸)
# 先放大到256x256,再随机裁剪224x224
larger = tf.image.resize(image_normalized, [256, 256])
random_crop = tf.image.random_crop(larger, size=[224, 224, 3])
避坑指南:我曾经在训练分类模型时,直接用random_crop裁剪原图,结果有些裁剪区域只包含背景,没有目标物体。模型学了半天,准确率卡在60%不动。后来我改用tf.image.sample_distorted_bounding_box,先检测目标区域再裁剪,效果好多了。
3.3.4 亮度、对比度、饱和度
这些是颜色空间上的变换。调整亮度模拟不同光照,调整对比度模拟不同显示设备。
# 调整亮度(delta范围[-1,1],实际是像素值加减)
bright = tf.image.adjust_brightness(image_normalized, delta=0.2)
# 调整对比度(factor>1增强,<1减弱)
contrast = tf.image.adjust_contrast(image_normalized, contrast_factor=2.0)
# 随机组合(训练时常用)
random_bright = tf.image.random_brightness(image_normalized, max_delta=0.3)
random_contrast = tf.image.random_contrast(image_normalized, lower=0.5, upper=1.5)
饱和度调整一般用tf.image.adjust_saturation。色相调整用tf.image.adjust_hue,但要注意色相是循环的,delta范围是[-0.5, 0.5]。我一般只调亮度和对比度,色相和饱和度改太多会让图像看起来不自然。
3.4 实战:构建一个图像预处理流水线
把上面这些操作串起来,就是一个标准的预处理流程。我通常这样写:
def preprocess_image(image_path, target_size=(224, 224)):
# 1. 读取
raw = tf.io.read_file(image_path)
image = tf.image.decode_jpeg(raw, channels=3)
# 2. 转float并归一化
image = tf.image.convert_image_dtype(image, tf.float32)
# 3. 调整大小
image = tf.image.resize(image, target_size)
# 4. 数据增强(训练时启用)
# image = tf.image.random_flip_left_right(image)
# image = tf.image.random_brightness(image, max_delta=0.2)
return image
# 测试
img = preprocess_image("cat.jpg")
print("预处理后形状:", img.shape, "范围:", tf.reduce_min(img).numpy(), "~", tf.reduce_max(img).numpy())
总结一下:图像处理的核心就三件事——读进来、转成张量、做变换。读文件用tf.io.read_file+解码,变换用tf.image系列函数。记住:归一化到[0,1]是标准做法,数据增强要随机且合理。嗯,这些基础打牢了,后面处理多模态数据时才能游刃有余。