图片批量转LMDB:Caffe数据准备脚本原理与避坑指南
简介这是一份面向Caffe用户的图片转LMDB实用脚本包针对图像分类场景用于将原始图片批量转换为Caffe训练所需的LMDB格式并自动生成train.txt与test.txt列表。相较旧版新版更适合超过10类的多分类数据修正了此前存在的问题可直接运行无需额外干预。压缩包共13个文件以5个Shell脚本为主配合文本配置、日志记录以及compute_image_mean、convert_imageset两个工具和说明文档整体仅40KB轻量易部署。目前已有354人学习/下载。通过该脚本使用者可跳过手工整理图片列表与参数调试的繁琐环节快速完成数据准备并能借助日志排查转换过程中的异常适合中高级Caffe使用者提升数据预处理效率。1. 把图片批量转成 LMDB这份脚本解决的问题不止是格式做 CNN 分类任务时数据准备永远比模型结构更让人头大。某次模拟项目 X 里一个看起来完全正常的分类网络总是训练到一半就卡住,后来定位才发现问题不在网络层写法而是喂给 caffe 的图压根没转成 lmdb。数据进不了内存训练自然无从谈起。这份脚本专门解决这个环节从普通图片目录一次性生成 train.txt 和 test.txt再产出能被 caffe 数据层直接读取的 lmdb 文件没有多余依赖属于拿到手改改路径就能跑的直接运行版。适合正在入门 caffe、或者想把已有图片集快速搬进训练流程的开发者尤其是卡在数据准备这一步的人。2. LMDB 与 caffe 数据流先搞清楚为什么非要这个格式2.1 为什么是 LMDB把每次迭代的磁盘读取变成内存映射caffe 的 Data 层读取训练数据的标准后端之一就是 lmdb。它本质上是一个内存映射数据库把图片内容和标签以键值对的形式写在一个文件里。训练时 Data 层通过 key 随机取 sample而不是反复去磁盘上解码 jpg。直接读原始图片行不行caffe 里有 ImageDataLayer 可以做到但生产环境我一般不推荐。原因有三点。第一随机读取大量小图片磁盘 IO 会成为瓶颈。训练一个 epoch 可能要读几万张图每张图都走一次文件读取和解码CPU 占用会无规律飙升GPU 时常处于等待状态。第二图片解码尺寸不一致会引入大量隐性问题比如内存碎片、batch 填充逻辑复杂。第三lmdb 支持多进程共享读取caffe 的数据层预取线程能从 lmdb 里快速抓取 batch减少 IO 抖动。所以 lmdb 是 caffe 训练流程里很实用的数据容器。而这份脚本的价值就是把“收集图片 → 生成标注 txt → 写入 lmdb”这三件事合成一步避免自己手写一堆容易出错的胶水代码。2.2 脚本的预处理逻辑目录约定、标签生成和 txt 格式使用这类脚本之前最重要的不是急着跑而是先把图片目录结构整理好。我常用的一种约定是每个类别一个文件夹文件夹名就是标签。datasets/ ├── train/ │ ├── cat/ │ │ ├── cat_001.jpg │ │ └── cat_002.jpg │ └── dog/ │ ├── dog_001.jpg │ └── dog_002.jpg └── val/ ├── cat/ └── dog/脚本会扫描 train 目录下所有子文件夹按类别名排序后分配整数标签。生成的 train.txt 每一行内容是“图片路径 数字标签”比如train/cat/cat_001.jpg 0 train/dog/dog_001.jpg 1写 txt 时有几个细节值得注意。脚本里通常会设置一个shuffle参数决定是否打乱顺序。训练阶段建议打乱避免同类图片连续出现导致 loss 震荡验证集可以不打乱方便按类别观察结果。标签分配一般用字典常见做法是import os label_map {} label_id 0 with open(train.txt, w) as f: for class_name in sorted(os.listdir(train_root)): if not os.path.isdir(os.path.join(train_root, class_name)): continue label_map[class_name] label_id class_dir os.path.join(train_root, class_name) for img_name in sorted(os.listdir(class_dir)): img_path os.path.join(class_dir, img_name) f.write(f{img_path} {label_id}\n) label_id 1 print(label_map)这里编码标签的规则很关键label_map的顺序必须和后续转换时保持一致。如果训练脚本重新写了一份 label_map顺序对不上模型学到的标签就等于错乱。转换时脚本会同时读取 train.txt 和 test.txt并把每行转换成 lmdb 键值对。图片尺寸处理也是这一步的核心。脚本里常见配置是resize_height256, resize_width256或者直接取网络输入尺寸比如 224x224。转换过程中会用 OpenCV 对图片做缩放如果遇到无法解码的文件脚本通常会跳过并在终端打印警告。我不会让脚本直接崩溃退出因为几万张图里偶尔有一两张损坏是常态跳过比中断更符合实际使用习惯。3. 跑通脚本命令、关键参数与训练前的验证3.1 脚本入口与一次完整运行这份脚本是“直接运行版”意思是下载后不需要额外写代码直接在命令行跑即可。但前提是机器里已经装好了 caffe 所需的依赖特别是 OpenCV 和 lmdb 的 Python 包。安装依赖的常见做法pip install lmdb opencv-python numpy如果你的环境里已经有 caffe 的 Python 接口一般来说这两个包也不会缺。安装完以后先检查脚本目录下是否有 config 区域也就是开头那部分 define 参数的地方。典型的配置段长这样# config train_root datasets/train val_root datasets/val txt_output ./ train_txt train.txt test_txt test.txt train_lmdb train_lmdb test_lmdb test_lmdb resize_height 256 resize_width 256 shuffle True map_size 1 40 # 1TB 的地址空间防止写入中途空间不足配置里最有技术含量的是map_size这一项。lmdb 在写入前要预分配一段虚拟地址空间如果设置太小写几万张图很容易出现Map full错误。很多初学者直接不写这一行默认 map_size 只有 10MB 左右跑几十张图就报错。我一般习惯设成 1TB 的虚拟空间因为 lmdb 实际占用会按需增长虚拟设置大一点不会真的占满磁盘但能避免中途扩容失败。改好路径后直接运行python create_lmdb.py脚本会先扫描目录生成 train.txt 和 test.txt接着逐行读取图片写入 lmdb。整个过程快慢取决于图片数量和尺寸。如果命令行窗口里能看到类似10000 samples added的输出基本说明转换过程正常。3.2 转换后的产物data.mdb 与 lock.mdb 到底是什么转换完成后train_lmdb目录下会出现两个文件data.mdb真正的数据存储文件里面是所有图片和标签的键值对lock.mdb环境锁文件训练时 caffe 会以只读方式打开数据库锁是必要的我见过不少人只拷贝data.mdb而忘了lock.mdb结果换到另一台机器上训练报锁相关错误。实操中两个文件要一起拷贝或者直接用原始目录。检查 lmdb 是否可用的一个简单方式是看条目数import lmdb env lmdb.open(train_lmdb, readonlyTrue, lockFalse) with env.begin() as txn: print(txn.stat())txn.stat()返回的字典里entries字段就是数据库中的样本总数。这个数字应该和 train.txt 有效行数一致。如果不一致说明转换过程中有图片被跳过回头去查脚本打印的警告日志。3.3 反向读取 lmdb确认图片和标签真的对齐生成完 lmdb 不代表万事大吉。我习惯在训练之前先反向读取几个样本把灰度值还原成图片看看。import lmdb import cv2 import numpy as np def inspect_lmdb(lmdb_path, sample_count5): env lmdb.open(lmdb_path, readonlyTrue, lockFalse) with env.begin() as txn: for key, value in txn.cursor(): if sample_count 0: break # caffe 写入 lmdb 的 value 结构: 前4字节是 label, 后续是图片数据 label int(np.frombuffer(value[:4], dtypenp.int32)[0]) img_data np.frombuffer(value[4:], dtypenp.uint8) img cv2.imdecode(img_data, cv2.IMREAD_COLOR) if img is not None: print(fkey{key}, label{label}, img_shape{img.shape}) sample_count - 1 inspect_lmdb(train_lmdb, sample_count10)注意这里 label 的解析方式caffe 的 convert_imageset 写入 lmdb 时把 label 放在 value 的最前面占 4 个字节后面才是编码后的图片数据。如果解析不对打印出来的 label 会是一串乱码。这个读取函数也是后面排查训练异常时高频使用的一个工具。4. 避坑指南四个最常翻车的点以及对应的解决办法4.1 图片尺寸不一致导致转换中断现象脚本运行到某一张图时报错cv2.error: ... (-215:Assertion failed)或直接说decode or resize failed。原因数据集中存在单通道灰度图、四通道 RGBA 图或者损坏的 jpg 文件。OpenCV 默认按三通道读取遇到灰度图解码后是二维数组resize 的维度对不上就抛异常。解决转换前先加一个遍历函数把非 RGB 的图统一转成 RGB并跳过无法解码的文件。常见做法import cv2 def robust_loader(img_path, height, width): img cv2.imread(img_path, cv2.IMREAD_COLOR) if img is None: return None img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) return cv2.resize(img, (width, height))我还会在写 txt 的阶段就把无法读取的图片路径过滤掉避免中途才发现。这个方法虽简单但能省掉很多转换时的处理逻辑。4.2 路径含中文或空格导致 txt 路径失效现象脚本生成 txt 正常但 caffe 训练时一直报Image ... does not exist。原因Windows 下图片路径可能包含中文或空格。txt 里记录的路径和 lmdb 内部 key 虽然是字符串但在某些 caffe 版本中文件读取环节对路径的编码处理不够健壮空格也会被系统调用截断。解决目录和文件名统一改成 ASCII 字符串路径中不要有空格。换一台 Linux 机器训练时还要注意路径分隔符要转成/。如果已经生成了错误的 txt用下面这段代码清洗with open(train.txt, r, encodingutf-8) as f: lines f.readlines() with open(train_clean.txt, w, encodingutf-8) as f: for line in lines: line line.strip().replace(\\, /) if line: f.write(line \n)4.3 写入大量图片时内存被撑爆现象脚本跑到一半系统开始疯狂使用 swap最后进程被系统杀掉。原因脚本一次性把所有图片数组读进内存再统一写入 lmdb。几万张 256x256 的三通道图光 numpy 数组就是几十 GB。解决分批次写入每处理 1000 张图就 commit 一次事务。转换脚本里的写入结构一般是import lmdb env lmdb.open(train_lmdb, map_sizemap_size) txn env.begin(writeTrue) for idx, (key, img_data, label) in enumerate(sample_iter): # key 用 8 位定长数字字符串保证排序对齐 key_bytes f{idx:08d}.encode() value np.array([label], dtypenp.int32).tobytes() img_data.tobytes() txn.put(key_bytes, value) if idx % 1000 0: txn.commit() txn env.begin(writeTrue) env.sync()这里用idx:08d固定 key 的宽度也是很重要的细节。如果 key 写的是1、10、100这种变长字符串lmdb 会按字符串字典序排列打乱数据顺序。4.4 生成的 txt 结尾带换行符导致空行进入数据集现象txt 内容看起来完全正常但 caffe 读取时偶尔报路径错误且错误行是空字符串。原因在 Windows 上编辑过 txt行尾是\r\n脚本又用 strip 不彻底的方式处理导致空行残留在 txt 中。解决写入 txt 时统一使用\n读取时无论原文件是什么换行符都先 strip 再判断。转换脚本内部读取 txt 时我习惯用with open(txt_path, r) as f: for line in f: line line.strip() if not line: continue img_path, label_str line.split( ) # do something这种写法无论文件是 LF 还是 CRLF都能正常工作属于一种很省心的防御姿势。5. 进阶验证在训练的同时反向检查数据质量lmdb 生成之后还有一个很多人忽略的步骤验证数据分布和标签对应关系。我会在写完 lmdb 之后运行一段随机抽样程序从数据库中随机取 100 张图按标签分类统计每类的样本数然后画出一个简易的列表标签 ID类别名样本数0cat49521dog50312bird1204这一步能快速发现两类问题。第一标签 0 是不是你期望的那个类别。第二训练集是否存在严重的类别不平衡。如果某个类别的样本数只有别人的十分之一训练第一个 epoch 时 loss 往往会表现出诡异的震荡你会误以为是网络结构问题实际是数据分布问题。另一个更直观的验证方式是重建图像网格。从 lmdb 中读出若干张图片把 label 和图片一起保存到一张拼接图中import lmdb import cv2 import numpy as np env lmdb.open(train_lmdb, readonlyTrue, lockFalse) with env.begin() as txn: cursor txn.cursor() imgs [] for i, (key, value) in enumerate(cursor): label int(np.frombuffer(value[:4], dtypenp.int32)[0]) img_data np.frombuffer(value[4:], dtypenp.uint8) img cv2.imdecode(img_data, cv2.IMREAD_COLOR) img cv2.resize(img, (128, 128)) img cv2.putText(img, str(label), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) imgs.append(img) if len(imgs) 12: break grid np.hstack(imgs) cv2.imwrite(check_lmdb.png, grid)把生成的 check_lmdb.png 打开看一眼比看任何日志都直观。如果图的内容和标签对不上说明目录扫描顺序或标签映射写错了这时候重新生成比硬着头皮训练要划算得多。我后来养成了一个习惯每次准备新数据集不管时间多紧都会先走一遍这个抽样检查流程。从那次被标签错乱坑了一天之后我就强制自己在训练前把“生成 txt → 写 lmdb → 抽样读回”作为固定的三步曲。换环境、换数据集都如此哪怕是只有几百张的小测试集。这个习惯确实帮我避掉了不少回头找 bug 的时间希望也能帮到正在和数据准备较劲的你。检查无误后再开始训练你会发现自己省下的时间远比写这段验证逻辑多。本文还有配套的精品资源点击获取