资讯动态

JPG批量转NPY:数据预处理提速与训练缓存实战指南

发布时间:2026/10/16 14:56:34 来源:尧图企业网站定制
把文件夹里的jpg图片批量转成对应的.npy格式文件听起来就是个不起眼的小脚本但我做视觉相关的数据处理时这套转换永远是第一个写、也是最常被复用的一段代码。早几年我偷懒训练脚本里直接读原始jpg几百张图测着玩没感觉后来真上了几万张的数据集每次训练启动前光图片解码和预处理就要磨二十多分钟实在忍不了才把这一步抽出来固化成了.npy缓存。做完之后同样的预处理工作变成了几秒钟的事整个实验迭代节奏完全不一样了。这篇文章就把整套批量转换的思路、代码和坑完整摊开讲清楚为什么转、怎么转、转完怎么验证适合准备视觉数据集、做模型复现、或者搭训练管道的朋友参考。1. 为什么要把jpg批量转成.npy先算清这笔账1.1 解码开销是数据准备里最容易被忽略的硬成本很多人觉得图片读进来不就是一个数组吗反正cv2.imread一句就搞定了何必多存一份.npy。问题恰恰出在这句一句就搞定上。jpg是压缩格式磁盘上的一张224×224的图片可能只有二三十KB但解码成RGB像素数组之后是224×224×3150528个字节。每次imread都要走一遍熵解码、逆离散余弦变换、色彩空间转换这些计算开销是纯CPU密集型的读一万张就是重复一万遍。.npy文件则完全不一样。它存的本来就是内存中数组的原始字节头部只有一小段描述shape和dtype的元数据np.load几乎等于直接做磁盘到内存的拷贝中间没有任何解压和重算的过程。我实测的数据大致是批量读一万张224×224的jpg用cv2.imread解码加resize单线程大约200秒换成np.load读提前转好的.npy同样一万张只需要15秒左右。差了一个数量级不是因为哪份代码写得更聪明而是单纯省掉了整个解码环节。1.2 什么场景值得做什么场景没必要折腾先说适合的场景。最典型的一种你有一批固定的数据集后面要对多个模型做对比实验或者反复调参。这时候图片预处理逻辑——统一尺寸、转RGB、归一化——只做一次存成.npy之后每次实验加载都是纯I/O操作不用再重复解码。第二种场景是数据需要喂给对输入格式有硬性要求的模型比如固定224×224、固定float32范围把预处理结果缓存下来能省掉训练代码里一堆冗余的if-else判断。不推荐折腾的情况也有数据集只有几百张一次脚本跑完就丢转换的时间可能比直接读还慢或者你的管道里每一步都在做随机裁剪、随机翻转这类在线增强那缓存的意义主要落在基础图而非增强后的图。即便在这种场景下我依然会把resize和通道统一先做了缓存增强只在缓存图的基础上做速度提升依然明显。2. 动手前的准备依赖选型和图片摸底2.1 OpenCV还是Pillow我的默认答案转换第一步要选读图库。市面上常用的是OpenCV和Pillow两家我默认用OpenCV因为它解码速度快、resize方便而且imread遇到损坏文件不会抛异常打断脚本而是返回None批量任务里这个行为对容错很友好。Pillow也不是不行甚至在某些格式兼容性上更强但需要多写几行。对比项OpenCV (cv2)Pillow读取效率更高一般返回通道顺序BGRRGBresize等几何变换内置且快可用但稍慢EXIF方向处理默认忽略需配合exif_transpose坏文件行为返回None抛异常环境依赖其实就两个核心库加一个辅助库numpy、opencv-python、tqdm纯粹为了进度条。安装命令很简单pip install numpy opencv-python tqdm2.2 转换之前先花两分钟摸清文件夹的底拿到一个图片文件夹别急着写转换脚本。先做一次摸底看看里面到底有什么。我用的是这样一段快速侦察代码from pathlib import Path import cv2 src Path(./images) jpg_list list(src.glob(*.jpg)) print(fjpg数量: {len(jpg_list)}) for p in jpg_list[:5]: img cv2.imread(str(p)) if img is None: print(p.name, 读不出来) else: print(p.name, shape:, img.shape, dtype:, img.dtype)这一步往往会有惊喜你以为全是RGB三通道图结果混进来几张灰度图你以为尺寸都是统一的结果有横版有竖版。这些信息直接决定转换时要不要统一到RGB、要不要统一resize。我在实际项目中就遇到过文件夹里混着几千张RGBA的png和jpg的情况如果摸底时没发现后面一堆图片的通道数对不上踩坑成本很高。3. 单张图片转换的完整链路从解码到落盘3.1 每一步代码在做什么为什么要这么做单张转换的核心逻辑就五步把它吃透了批量只是加一层循环而已。import cv2 import numpy as np img cv2.imread(images/img_001.jpg) # 1. 解码成ndarray img cv2.resize(img, (224, 224), interpolationcv2.INTER_AREA) # 2. 统一尺寸 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 3. 调通道顺序 arr img.astype(np.float32) / 255.0 # 4. 归一化到[0,1] np.save(npy_data/img_001.npy, arr) # 5. 落盘第一步imread返回的是一个H×W×C的uint8数组H是高、W是宽、C是通道数。注意它返回的顺序是BGR不是RGB这个坑后面专门讲。第二步统一尺寸为什么用INTER_AREA因为缩小图片时区域插值会做像素平均抗锯齿效果好不容易出现纹理失真如果是放大图片用INTER_LINEAR更顺滑。第三步如果你希望下游拿到的数据是RGB标准顺序这里就必须显式转换。第四步开始涉及一个关键决策——到底要不要归一化下面单独说。第五步np.save会自动补.npy后缀它是numpy自带的二进制格式跨平台、跨版本兼容性都很好。3.2 dtype与归一化的取舍别在不该内存翻倍的地方翻倍这一步是整个转换里最容易被忽略、也最容易埋雷的决策点。jpg解码出来是uint8取值范围0到255归一化后会变成float32取值0到1。两者文件大小差了整整4倍一张224×224×3的uint8数组约147KB转成float32直接变成588KB。我的建议是如果你只是把.npy当作中间缓存后面训练代码里本来就要做归一化那缓存就保留uint8训练时一行.astype(np.float32) / 255.0就完事了。文件更小加载更快肉眼验证也方便。如果你用的预训练模型要求输入就是float32的[0,1]或者[-1,1]那就在转换阶段归一化好免得每次训练都重复算。关键原则是这个决策一旦定下来就把它写进一个meta文件记下来而不是靠记忆。4. 批量脚本设计目录遍历、自然排序与命名一一对应4.1 目录遍历没有你想的那么简单批量转换第一步是拿到所有jpg的路径列表。最简单的写法是list(Path(./images).glob(*.jpg))但直接排序会有个经典问题字符串排序是字典序img10.jpg会排在img2.jpg前面顺序完全乱掉。如果你的图片名不是固定长度补零格式这个顺序混乱会直接影响后续的数据对齐。解决办法是自然排序把文件名里的数字部分按数值比较import re def natural_key(path): return [int(part) if part.isdigit() else part for part in re.split(r(\d), path.stem)]这个函数会把img_2拆成[img_, 2]把img_10拆成[img_, 10]排序时数字按大小排结果就是img_2、img_10而不是img_10、img_2。批量处理前先这样排一遍能保证转换顺序、后续加载顺序、标签生成顺序全部一致。4.2 主循环脚本稳定、可续传、有报错日志对应的.npy是这个项目的核心要求——每张img_001.jpg都生成一个img_001.npy命名一一对应方便追溯。目录结构上我习惯打成这样images/ img_001.jpg img_002.jpg ... npy_data/ img_001.npy img_002.npy ...完整的批量脚本我一般这么写import re from pathlib import Path import cv2 import numpy as np from tqdm import tqdm SRC_DIR Path(./images) DST_DIR Path(./npy_data) DST_DIR.mkdir(parentsTrue, exist_okTrue) TARGET_SIZE (224, 224) USE_RGB True KEEP_UINT8 True # False则转float32并归一化 def natural_key(p: Path): return [int(part) if part.isdigit() else part for part in re.split(r(\d), p.stem)] def convert_one(path: Path): try: img cv2.imread(str(path)) if img is None: raise ValueError(imread返回None文件可能损坏) img cv2.resize(img, TARGET_SIZE, interpolationcv2.INTER_AREA) if USE_RGB: img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if not KEEP_UINT8: img img.astype(np.float32) / 255.0 out_path DST_DIR / f{path.stem}.npy np.save(out_path, img) return path.name, ok except Exception as e: return path.name, str(e) paths sorted(SRC_DIR.glob(*.jpg), keynatural_key) errors [] for p in tqdm(paths, desc转换中): name, status convert_one(p) if status ! ok: errors.append((name, status)) print(f完成 {len(paths) - len(errors)}/{len(paths)} 张) if errors: print(失败列表最多显示20条:) for name, reason in errors[:20]: print(f {name}: {reason})这个脚本有几点是实际跑出来的经验每个文件都套了try/except单个文件失败不会中断整批任务最后统一打印失败清单方便回头手动处理tqdm进度条在几万张图时非常重要至少你知道它在动、还差多少转换函数独立成convert_one为后面的并行改造留了余地。5. 上万张图片时的内存与性能策略5.1 逐张保存还是合并成一个大数组前面讲的是每张jpg生成一个.npy这也是标题里对应的三个字决定的方向。但实际工程里你还会面临另一个选择要不要把所有图片合并成一个大的.npy数组比如dataset.npy加上labels.npy。先算内存账。一万张224×224×3的uint8图片合并成一个数组是10000×224×224×31505280000字节约1.4GB如果转成float32直接翻倍到5.6GB。这个数据在单机训练场景下能接受但如果你有十万张甚至百万张图合并成单文件就不现实了。逐张保存的优势是灵活想随机采样、想按标签批量加载、想用生成器逐个读入都很自然。缺点是会产生大量小文件机械硬盘上读写会慢固态硬盘问题不大。合并成大数组的优势是加载快、内存布局紧凑但前提是所有图片尺寸必须一致否则没法stack。另外如果你确实需要超大数组做随机访问可以考虑np.memmap它把磁盘上的大数组映射成内存地址空间按需读取不用一次性全部装进内存。5.2 并行处理与断点续传批量转换的瓶颈在解码解码是CPU密集型而且每张图之间互不影响天然适合多进程。改造很简单把主循环换成ProcessPoolExecutorfrom concurrent.futures import ProcessPoolExecutor import os with ProcessPoolExecutor(max_workersos.cpu_count()) as pool: results list(pool.map(convert_one, paths, chunksize100))我的经验是进程数不要贪多CPU核心数就够了解码本身已经能打满单核chunksize设成100减少进程间通信调度开销。四核机器实测能到三倍多加速八核能到五倍左右收益非常明显。断点续传是我在转换大文件夹时一定会加的小功能循环里先判断目标.npy是否已存在存在就直接跳过。这样万一中间断电、机器重启、或者数据源目录加了新图重新跑一遍脚本就行不会重复转换已有文件for p in tqdm(paths, desc转换中): if (DST_DIR / f{p.stem}.npy).exists(): continue name, status convert_one(p) ...6. 三个实测最容易翻车的细节6.1 通道顺序错位红蓝互换的诡异画面BGR和RGB这个问题字面上谁都懂实际操作时几乎每个人都至少翻车一次。症状就是转换结果里天空变成了橙色人的脸变成蓝紫色整体色调诡异。原因很简单cv2.imread读出来的是BGR顺序如果你直接把这个数组交给按RGB训练的模型或者直接用matplotlib的imshow显示前两个通道的反转会被完全暴露。我在早期一次项目里就因此浪费过半天时间模型在验证集上表现异常排查了很久才发现是缓存数据里通道顺序错了模型看到的所有图都是红蓝颠倒的。从那以后我在转换脚本里固定写一行cvtColor(img, cv2.COLOR_BGR2RGB)并且在输出目录放一个README.txt里面记录通道顺序、尺寸、dtype这些元信息。这个习惯后来帮我省了无数次沟通成本别人拿到这批数据也不会猜来猜去。6.2 EXIF方向被忽略手机竖拍照片全躺平另一个容易被忽略的坑是EXIF方向信息。手机拍的照片尤其是竖拍的会在文件头里写一个orientation标签告诉显示软件这张图要旋转90度才是正着看的。然而cv2.imread默认完全无视这个标签直接按文件里的原始像素读。结果就是你转换出来的.npy里所有竖拍照片都是横躺的。我遇到过整批图片转完训练出的模型对方向特别敏感准确率始终上不去。排查后才发现问题不在模型在数据方向全是乱的。解决办法是读图时主动处理EXIF用Pillow会更顺from PIL import Image, ImageOps import numpy as np pil_img ImageOps.exif_transpose(Image.open(str(path))) img np.array(pil_img.convert(RGB)) # 这里拿到的是RGB顺序不需要再cvtColor如果你必须用OpenCV可以先用Pillow处理完方向再转成OpenCV能用的numpy数组。至少心里要有数cv2不处理EXIF方向问题得自己兜底。6.3 文件名大小写、路径问题与坏图拦截glob(*.jpg)只会匹配小写后缀但现实中图片可能是.JPG、.jpeg甚至.JPEG。我的习惯是干脆匹配所有文件再按后缀过滤paths [p for p in src.iterdir() if p.is_file() and p.suffix.lower() in {.jpg, .jpeg}]路径方面有中文目录名的场景下尽量全程用pathlib的Path对象不要手动拼字符串路径。Windows上还容易踩路径过长的问题文件夹层级不要套太深我用脚本处理前会把数据集拷到单独的、短的根目录下。坏图问题前面说过imread返回None时直接跳过并记录路径如果整批成功率低于98%建议先排查源图是不是有系统性损坏。7. 转换完成后的验证别急着拿去训练7.1 像素级对比抽检转换完成不代表数据没问题。我的习惯是随机抽几张图把原始jpg重新读一遍和.npy内容做像素级对比。验证代码大致长这样import cv2 import numpy as np SRC_PATH images/img_001.jpg DST_PATH npy_data/img_001.npy TARGET_SIZE (224, 224) src cv2.imread(SRC_PATH) src_rgb cv2.cvtColor(src, cv2.COLOR_BGR2RGB) src_rgb cv2.resize(src_rgb, TARGET_SIZE, interpolationcv2.INTER_AREA) data np.load(DST_PATH) if data.dtype np.float32: data_u8 (data * 255).round().astype(np.uint8) else: data_u8 data diff np.abs(data_u8.astype(np.int16) - src_rgb.astype(np.int16)) print(shape:, data.shape, dtype:, data.dtype) print(最大像素差:, diff.max(), 平均像素差:, diff.mean())正常情况下最大像素差应该是0因为resize和cvtColor都是确定性操作。如果你看到最大像素差是几十甚至一百多就去查前面6.1里说的通道顺序问题或者dtype换算有没有出错。这里有个小技巧astype(np.int16)是为了防止uint8相减时下溢成巨大的错误值unsigned减unsigned在numpy里会绕回不用int16中间类型很容易误判。7.2 完整性与命名校验像素验证之外还得确认数量和命名一一对应。这个校验逻辑很简单但没做过的同学下次一定用得上src_names {p.stem for p in paths} dst_names {p.stem for p in DST_DIR.glob(*.npy)} missing src_names - dst_names extra dst_names - src_names print(缺失:, missing if missing else 无) print(多余:, extra if extra else 无)到这里整套批量转换流程就闭环了摸底、单张转换、批量处理、性能优化、踩坑规避、结果验证。我自己后来在这套脚本上加了一个小功能转换完成后自动生成一个meta.txt记录通道顺序、目标尺寸、dtype、图片数量和转换时间。就这一个文件后面不管是谁接手这批数据、或者换了一台机器重新跑都不用再对着代码猜配置。如果这套流程能帮你省下第一次那二十分钟的等待我觉得这篇就值了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑