【深度学习项目实战·07】NumPy与图像处理:把一张图片正确送进模型
文章目录
- 01 本篇要解决什么问题
- 02 背景:图像进入程序后,就是有规则的数组
- 03 实操:检查图像处理的六个关键环节
- (1)先转换类型,再做减法与归一化
- (2)切片很方便,也可能共享原始数据
- (3)广播让逐通道处理变简单
- (4)调整维度顺序,用transpose而不是硬改shape
- (5)图片与类别掩膜,缩放规则不同
- (6)显示图片前,先还原它的数值含义
- 04 一个具体例子:同时处理RGB图像与分割掩膜
- 05 常见问题与自测
- 06 本篇回顾与下一步
01 本篇要解决什么问题
读图、缩放、除以255,看起来只是几行代码。但模型输入错通道、掩膜多出小数、图像显示发黑,往往就发生在这几步。
本篇从数组的形状、类型和数值范围入手,完成一个“生成图片→读取→缩放→归一化→调整维度→保存”的小流程。后面换成真实数据,也能沿用同一套检查方式。
- 本篇关键词:ndarray、shape、dtype、广播、视图、HWC/CHW、插值。
- 完成标志:能说明输入的每个维度代表什么,并保证图像与标签采用合适的处理方式。
- 适用范围:示例使用普通8位RGB图片和整数类别掩膜。医学影像、16位图像及浮点科学数据需根据物理含义另外设计强度处理。
02 背景:图像进入程序后,就是有规则的数组
对于一张高64、宽96的RGB图,常见NumPy形状是(64, 96, 3),依次表示高、宽、通道,即HWC。灰度图可能只有(64, 96);有些流程会显式增加一个通道维。
同样是(64, 96, 3),数值也可能是0~255整数、0~1浮点数,或标准化后包含负数的浮点数。只看shape,不能确定数据是否正确。
importnumpyasnp image=np.zeros((64,96,3),dtype=np.uint8)print(image.shape,image.dtype,image.min(),image.max())本篇统一通过Pillow读取RGB,避免让颜色通道约定成为隐含条件。其他图像库的默认顺序可能不同,混用时需要查清再转换。
03 实操:检查图像处理的六个关键环节
(1)先转换类型,再做减法与归一化
uint8只能表达0~255。用它做像素差值,结果可能发生回绕:
importnumpyasnp a=np.array([0],dtype=np.uint8)b=np.array([1],dtype=np.uint8)print(a-b)# [255]print(a.astype(np.float32)-b.astype(np.float32))# [-1.]所以差值、均值、标准化等运算应先转换到合适的浮点类型。固定宽度整数的边界说明可查NumPy数据类型文档。如果最后保存回8位图片,先恢复显示范围、裁剪,再转换类型,避免超出范围的值直接进入整数数组。
(2)切片很方便,也可能共享原始数据
importnumpyasnp image=np.zeros((8,8,3),dtype=np.float32)crop=image[:4,:4]# 基本切片通常是视图crop[:]=1.0# 原image对应位置也改变independent=image[:4,:4].copy()independent[:]=0.0assertimage[0,0,0]==1.0这是“裁剪后增强,为什么原图也变了”的常见原因。基本切片与高级索引的行为不同,不能把所有索引都当成复制;需要独立修改时主动.copy()。具体规则见NumPy copies与views。
(3)广播让逐通道处理变简单
对HWC图像做逐通道标准化,可以把均值和标准差组织为(1, 1, 3),让它们沿高宽维广播:
广播从末尾维度向前对齐,对应维度相同或其中一个为1时才能兼容。它不会判断轴的语义;如果已经变成CHW,统计量就应按通道轴组织,例如(3, 1, 1)。因此“代码能运行”还不够,也要确认运算发生在想要的轴上。
x h , w , c ′ = x h , w , c − μ c max ( σ c , ε ) . x'_{h,w,c}=\frac{x_{h,w,c}-\mu_c}{\max(\sigma_c,\varepsilon)}.xh,w,c′=max(σc,ε)xh,w,c−μc.
这里的均值、标准差必须与输入范围一致。若先除以255,统计量也要对应0~1范围。演示脚本使用固定常数方便观察;真实项目若自己估计统计量,只能用训练集,不能把验证集和测试集一起统计进去。
除以255只是针对8位强度范围的缩放;它不会自动完成均值方差标准化,也不适合直接套用到所有MRI或CT数据上。
(4)调整维度顺序,用transpose而不是硬改shape
importnumpyasnp hwc=np.zeros((64,96,3),dtype=np.float32)chw=hwc.transpose(2,0,1)chw=np.ascontiguousarray(chw)batch=chw[None,...]assertbatch.shape==(1,3,64,96)transpose表示交换轴;直接reshape(3, 64, 96)只按元素顺序重新分组,不能表达相同的通道移动。转置结果也可能不连续,ascontiguousarray在需要时整理内存布局,为后面交给PyTorch或其他接口提供清晰的边界。
(5)图片与类别掩膜,缩放规则不同
连续颜色图像可以使用双线性等插值。类别掩膜里的0、1、2代表类别编号,中间的0.5没有类别含义,所以本篇用最近邻插值保留编号。
Pillow的resize参数是(宽, 高),与数组shape里常见的(高, 宽)相反。处理分割任务时,图像和掩膜还必须采用一致的空间变换,不能各自随机裁剪。插值选项见Pillow Image.resize文档。
类别掩膜不要随手.convert("RGB")或除以255;本篇单通道掩膜保存类别ID,转成训练标签时再使用所需整数类型。
(6)显示图片前,先还原它的数值含义
标准化后的数组出现负数很正常。若直接乘255保存,显示会失真。应按相同的均值、标准差逆变换,再裁剪到显示范围。
排查图像问题时,可以固定检查四件事:shape是否正确、dtype是否合理、最小最大值是否符合约定、图像与标签能否对应。颜色正常只是第一步,不能代替数值检查。
图:本篇原创图像处理示意。数值缩放与维度调整是两件不同的事情。
04 一个具体例子:同时处理RGB图像与分割掩膜
保存为examples/image_pipeline.py。脚本自动生成合成图片和圆形掩膜,不需要下载数据。
"""Create synthetic RGB/mask data and verify an 8-bit image pipeline."""importargparseimportjsonfrompathlibimportPathimportnumpyasnpfromPILimportImagedefmain():parser=argparse.ArgumentParser(description=__doc__)parser.add_argument("--output",type=Path,default=Path("artifacts/image_pipeline"))args=parser.parse_args()args.output.mkdir(parents=True,exist_ok=True)yy,xx=np.mgrid[:64,:64]rgb=np.stack([xx*4,yy*4,np.full_like(xx,128)],axis=-1).astype(np.uint8)mask=((xx-32)**2+(yy-32)**2<16**2).astype(np.uint8)Image.fromarray(rgb).save(args.output/"image.png")Image.fromarray(mask).save(args.output/"mask.png")withImage.open(args.output/"image.png")asimage:resized=image.convert("RGB").resize((96,96),Image.Resampling.BILINEAR)hwc=np.array(resized,dtype=np.float32)/255.0withImage.open(args.output/"mask.png")aslabel:resized_mask=np.array(label.resize((96,96),Image.Resampling.NEAREST))# Demo constants, NOT statistics estimated from a real dataset.mean=np.array([0.5,0.5,0.5],dtype=np.float32).reshape(1,1,3)std=np.array([0.25,0.25,0.25],dtype=np.float32).reshape(1,1,3)normalized=(hwc-mean)/np.maximum(std,np.float32(1e-6))chw=np.ascontiguousarray(normalized.transpose(2,0,1))np.save(args.output/"image_chw.npy",chw)np.save(args.output/"mask_ids.npy",resized_mask.astype(np.int64))recovered=np.clip(normalized*std+mean,0.0,1.0)preview=np.rint(recovered*255.0).astype(np.uint8)Image.fromarray(preview).save(args.output/"preview.png")assertchw.shape==(3,96,96)andchw.dtype==np.float32assertchw.flags.c_contiguousandset(np.unique(resized_mask))=={0,1}assertnp.max(np.abs(recovered-hwc))<1e-6print(json.dumps({"shape":list(chw.shape),"dtype":str(chw.dtype),"mask_ids":np.unique(resized_mask).tolist(),"contiguous":bool(chw.flags.c_contiguous)},indent=2))if__name__=="__main__":main()在项目根目录执行,PowerShell与Bash均适用:
python examples/image_pipeline.py程序应报告shape=[3, 96, 96]、dtype=float32、mask_ids=[0, 1]和连续内存标志。输出位于artifacts/image_pipeline:PNG用于查看,NPY用于保留数组形状与类型。
注意mask.png的前景值是1,普通看图软件中会显得很暗。这不表示标签错误;若要做展示图,可以另存一份0/255可视化图,训练标签仍保留原类别ID。
05 常见问题与自测
问题1:HWC与CHW的区别只是shape不同吗?
轴的含义也不同。需要交换轴,不能只把原数组重新reshape成目标形状。
问题2:所有图像都除以255,可以吗?
不可以。先确认原始类型、范围及物理意义。16位数据和科学浮点数据通常需要另外设计缩放。
问题3:为什么掩膜缩放后类别数变多了?
检查是否用了连续插值,或者把类别ID当成灰度强度处理。本篇的整数掩膜采用最近邻插值。
问题4:图像标准化后包含负数,是不是出错了?
不一定。减去均值后出现负数很常见。应根据处理公式判断,而不是要求所有中间数组都落在0~1。
问题5:修改crop后原图也变化,应该怎么处理?
先确认是否共享内存。需要独立副本时使用.copy(),也可以用np.shares_memory辅助检查。
06 本篇回顾与下一步
图像预处理的目标,是把数据转换成模型约定的表示,同时保留标签与输入之间的对应关系。每一步都能说清形状、类型和范围,后面排查模型问题才有可靠起点。
| 中文 | 常见英文或写法 |
|---|---|
| 多维数组 | ndarray |
| 数值类型 | dtype |
| 广播 | Broadcasting |
| 通道优先 | Channel-first / CHW |
| 最近邻插值 | Nearest-neighbor Interpolation |
下一篇进入PyTorch:把数组变成Tensor,再看参数、梯度和优化器如何配合工作。
参考与延伸阅读
- NumPy快速入门
- NumPy广播规则
- Pillow图像模式与概念