
简介一套面向机器学习数据预处理的轻量工具包用于将XML标注数据转换为CSV中间文件再进一步生成TensorFlow框架推荐的TFRecord格式。压缩包共2个Python脚本大小仅3KB覆盖数据格式转换核心链路适用于目标检测、图像分类等需要把XML标注送入TensorFlow模型的开发场景。已有251人学习下载。第一个脚本借助ElementTree解析XML节点并与pandas配合导出结构化CSV便于人工检查或做后续清洗第二个脚本基于TensorFlow的Example协议缓冲区把CSV逐行写入TFRecord文件从而利用TFRecordDataset实现高效批量读取。二者串联即可完成从原始标注到训练输入的转换省去手工编写转换逻辑的重复工作。包体小巧、职责清晰适合快速嵌入现有数据管线或作为初学者理解XML、CSV、TFRecord三种数据形态的参考实现。1. 为什么绕这一圈XML → CSV → TFRecord 的流水线价值拿到 scripts(xml-csv-tfrecord).rar 这个压缩包懂行的人一眼就认出这是目标检测训练前最经典的一条数据流水线XML 标注转 CSV再转 TensorFlow 官方推荐的 TFRecord 二进制格式。绕这一圈不是闲得慌——XML 解析慢、结构松散CSV 方便人眼检查和清洗TFRecord 则让训练时的数据 IO 快一个量级。包里的两个 Python 脚本分工明确xml_to_csv.py 把标注 XML 摊平成表格generate_tfrecord.py 再把表格序列化成 TFRecord。对正在用 LabelImg 标注、准备跑 TensorFlow Object Detection API 的开发者这套脚本能省掉最枯燥的格式转换步骤对手里堆着历史 XML、想统一迁到 tf.data 管线的老手它也是一个可以直接改用的模板。这篇拆解会把两个脚本的解析逻辑、参数含义、执行顺序和五类典型翻车现场分开讲。读完你能照着复现整套流程出问题时也知道先查哪里而不是对着报错瞎试。2. 拆解 xml_to_csv.pyElementTree 解析、列映射与 CSV 落盘用 7-Zip 解开 RAR 压缩包搜 rar 解压软件装个 7-Zip 就够了里面是两个 .py 文件。第一步先把 xml_to_csv.py 读一遍它负责把指定目录下所有 XML 解析成一张 CSV。想改对脚本先得知道它要吃的 XML 长什么样以及 xml 解析时用什么库、按什么路径取字段。2.1 VOC 标注 XML 的结构与 ElementTree 解析思路LabelImg 这类工具导出的 XML遵循的是 PASCAL VOC 标注格式去掉声明后结构基本如下annotation folderimages/folder filename000001.jpg/filename size width1920/width height1080/height depth3/depth /size object nameperson/name bndbox xmin312/xmin ymin289/ymin xmax912/xmax ymax891/ymax /bndbox /object /annotation解析它用的是 Python 标准库 xml.etree.ElementTree。常见做法是ET.parse(xml_file) 拿到整个树root.findall(object) 拿到所有目标框再逐层取 name 和 bndbox 下的四个坐标。这套路径在 VOC 格式下是固定写死的不需要任何配置。关键点在于ElementTree 的 findall 是按标签名精确匹配的。如果 XML 带了 xmlns 命名空间很多工业软件、传感器扩展库导出的 XML 都有标签名会带前缀findall(object) 会静默返回空列表。很多人搜「xml 格式文件没有标签怎么办」真相往往不是标签丢了是命名空间把解析路径遮住了。那为什么中间非要落一个 CSV因为 CSV 是可以用 Excel 或 WPS 直接打开的格式导入 CSV 文件后一屏能看几百行标注坐标合不合理、类别名有没有拼错人眼扫一遍就知道。这在数据准备里是极其重要的一步黑匣子不可怕可怕的是数据和标注全在黑匣子里从没人肉眼确认过。2.2 xml_to_csv.py 核心代码拆解下面这段是这类脚本最典型的实现逻辑和包里脚本一致import glob import xml.etree.ElementTree as ET import pandas as pd def xml_to_csv(xml_dir, output_csv): rows [] for xml_file in glob.glob(xml_dir /*.xml): tree ET.parse(xml_file) root tree.getroot() for obj in root.findall(object): row ( root.find(filename).text, # 图片文件名 int(root.find(size)[0].text), # widthsize 下第 0 个 int(root.find(size)[1].text), # heightsize 下第 1 个 obj.find(name).text, # 类别名 int(obj.find(bndbox)[0].text), # xmin int(obj.find(bndbox)[1].text), # ymin int(obj.find(bndbox)[2].text), # xmax int(obj.find(bndbox)[3].text), # ymax ) rows.append(row) header [filename, width, height, class, xmin, ymin, xmax, ymax] df pd.DataFrame(rows, columnsheader) df.to_csv(output_csv, indexFalse) return df if __name__ __main__: xml_to_csv(annotations, train_labels.csv)glob.glob(xml_dir /*.xml) 只匹配该目录一层子目录里的 XML 不会进来所以标注目录最好扁平化。root.find(size)[0] 这种写法依赖子节点顺序size 下依次是 width、height、depth下标 0 和 1 就是宽高。obj.find(bndbox) 同理四个坐标按 xmin、ymin、xmax、ymax 排列。df.to_csv 里的 indexFalse 必须带着否则 pandas 会把行号写进第一列后面生成 TFRecord 时列名对不上数据全部错位。编码方面只用英文类别名时默认 UTF-8 没问题有中文类名建议直接 to_csv(path, indexFalse, encodingutf-8-sig)这样 Excel 打开不乱码TFRecord 里也能正确 decode。顺带一提Java 体系里解析相同 XML 常用 dom4j步骤是创建 SAXReader、read 文件、getRootElement、selectNodes。思路一致但那是另一套生态。Python 这边用标准库 ElementTree 就够零依赖几千张标注文件的解析速度完全可接受。2.3 列名即协议八个字段为什么不能乱动CSV 落盘后列名就成了两个脚本之间的协议。pandas 读 CSV 是按列名取数的理论上列顺序不影响 generate_tfrecord.py但很多人会手动改表、加列、调顺序一旦别的脚本按位置索引取值立刻翻车。我的习惯是永远保持这八个字段的固定顺序:列名类型来源在 TFRecord 中的用途filenamestrannotation/filename定位原图路径widthintsize/width坐标归一化参考值heightintsize/height坐标归一化参考值classstrobject/name写入 class/text 与 label 映射xminintbndbox/xmin归一化后写入 bbox/xminyminintbndbox/ymin归一化后写入 bbox/yminxmaxintbndbox/xmax归一化后写入 bbox/xmaxymaxintbndbox/ymax归一化后写入 bbox/ymaxwidth 和 height 在这条流水线里比较特殊generate_tfrecord.py 如果读不到原图就只能靠 CSV 里的宽高做坐标归一化。所以这两列别精简掉也别用图片实际尺寸去覆盖它们——标注尺寸和图片尺寸不一致时后面的框就是错的。3. generate_tfrecord.py 的序列化逻辑从 DataFrame 到 tf.train.ExampleCSV 出来后真正决定训练能不能跑的是 generate_tfrecord.py。这一步把结构化表格变成 TensorFlow 的二进制记录也是新手最容易当黑匣子处理的一步。先搞清楚 TFRecord 的底层形态再改代码才有底气。3.1 TFRecord 文件与 tf.train.Example 的结构TFRecord 本质上是一个装着序列化数据的容器文件。文件里一条条记录每条记录是一个 tf.train.Example 实例的序列化字节流。Example 的内部结构是 Mapstring, Feature而 Feature 只有三种取值BytesList、FloatList、Int64List。这个设计对图片目标检测数据非常合适整张 JPEG 图片的编码字节塞进 BytesList四个归一化坐标塞进 FloatList类别 ID 塞进 Int64List类别文本塞进 BytesList。一个 Example 就对应一个样本的全部信息训练时用 tf.data.TFRecordDataset 顺序读取再在内存里解析IO 效率远超逐张读图加读 XML。从 CSV 到 TFRecord常见做法是逐行构造 Example。这里有个关键约定Object Detection API 要求的 bbox 字段是归一化坐标也就是 xmin / width 这种 0 到 1 的小数。第一次写生成脚本时直接填像素值的人不在少数训练 loss 直接爆炸还以为是模型问题。3.2 从 DataFrame 行到 Example 的编码代码下面这段是 generate_tfrecord.py 最核心的 create_tf_example 函数import os import io import tensorflow as tf import pandas as pd from PIL import Image def create_tf_example(row, image_dir, label_map): # 1. 读原图字节 img_path os.path.join(image_dir, row[filename]) with tf.io.gfile.GFile(img_path, rb) as f: encoded f.read() # 2. 用 PIL 拿真实宽高 image Image.open(io.BytesIO(encoded)) width, height image.size # 3. 坐标归一化到 0~1 xmin float(row[xmin]) / width ymin float(row[ymin]) / height xmax float(row[xmax]) / width ymax float(row[ymax]) / height feature_dict { image/encoded: tf.train.Feature( bytes_listtf.train.BytesList(value[encoded])), image/format: tf.train.Feature( bytes_listtf.train.BytesList(value[bjpeg])), image/width: tf.train.Feature( int64_listtf.train.Int64List(value[width])), image/height: tf.train.Feature( int64_listtf.train.Int64List(value[height])), image/object/bbox/xmin: tf.train.Feature( float_listtf.train.FloatList(value[xmin])), image/object/bbox/ymin: tf.train.Feature( float_listtf.train.FloatList(value[ymin])), image/object/bbox/xmax: tf.train.Feature( float_listtf.train.FloatList(value[xmax])), image/object/bbox/ymax: tf.train.Feature( float_listtf.train.FloatList(value[ymax])), image/object/class/text: tf.train.Feature( bytes_listtf.train.BytesList(value[row[class].encode(utf-8)])), image/object/class/label: tf.train.Feature( int64_listtf.train.Int64List(value[label_map[row[class]]])), } example tf.train.Example( featurestf.train.Features(featurefeature_dict)) return example几个参数层面的细节要说明。tf.io.gfile.GFile 是 TensorFlow 的文件抽象能统一读写本地和远端路径比原生 open 读写更省心小数据集上直接用 open 也没区别。PIL 打开图片后image.size 返回的是 (width, height)顺序别写反。归一化必须用真实图片宽高而不是 CSV 里记录的宽高——两者不一致时以实际图片为准否则画出来的框会整体偏移。class/text 必须 encode 成字节中文类名用 utf-8。如果 CSV 阶段用了 utf-8-sig 导出pandas 读回时会自动剥离 BOM编码不会出问题。label_map 是一个 dict例如 {person: 1, dog: 2, car: 3}。取值用 row[class] 做 keyCSV 里类别名和 label_map 的 key 有一个字符对不上就会 KeyError 中断整个转换。3.3 Label Map 与类别 ID从 1 开始是约定TensorFlow Object Detection API 的 label_map 有个不成文的规矩背景是 0第一个真实类别从 1 开始。所以 label_map 里的 id 不要出现 0。虽然没有强约束但预训练模型的类别头结构和迁移学习配置都默认这个约定踩了会吃暗亏。完整的写入主循环长这样writer tf.io.TFRecordWriter(train.record) for idx, row in df.iterrows(): example create_tf_example(row, images, label_map) writer.write(example.SerializeToString()) writer.close()tf.io.TFRecordWriter 在 TensorFlow 2.x 里替代了旧版的 tf.python_io.TFRecordWriter。TF2 环境下用旧 API 会直接 AttributeError老教程里这种写法已经失效统一用 tf.io 前缀就不翻车。SerializeToString() 是必须的Example 对象本身不能直接写进文件。4. 把流程跑通环境准备、执行命令与 TFRecord 验证理论拆完开始动手。这一章按实际操作顺序走环境、命令、验证三步每步都给出输出信号方便你确认自己没走偏。4.1 环境与目录结构准备依赖三个Python 3.7 以上、TensorFlow 2.x、pandas。读图那一步还需要 Pillow。建议先建独立虚拟环境避免和系统 Python 互相干扰python -m venv .venv source .venv/bin/activate pip install tensorflow2.10 pandas pillowTensorFlow 版本多说一句2.10 是 CPU 环境下踩坑最少的一版更高版本对 Python 版本和指令集有额外要求先跑通流程再考虑升级。Windows 上装完 import tensorflow 报 DLL 错误多半是缺 Visual C 运行库装上再试。pip 安装慢就换国内镜像源一行命令的事不值得耗二十分钟。目录结构建议扁平化组织project/ ├── scripts/ │ ├── xml_to_csv.py │ └── generate_tfrecord.py ├── annotations/ ├── images/ └── output/为什么强调扁平xml_to_csv.py 的 glob 只匹配一层子目录放标注会漏扫generate_tfrecord.py 里的 os.path.join 也只适合 filename 直接落在 image_dir 下的情况。嵌套目录不是不能改但改之前先想清楚值不值——不少项目就是被目录结构的小聪明坑掉了时间。RAR 解压用 7-Zip 就行注意有些压缩包内层还套了一层同名目录解压后先 ls 看结构别让脚本里的相对路径找不到文件。4.2 分步执行与参数说明第一步XML 转 CSV。脚本默认直接运行cd project python scripts/xml_to_csv.py如果脚本没有把路径做成参数直接改源码里的 xml_dir 和 output_csv 变量。我一般会在函数开头加一行 print(len(glob.glob(xml_dir /*.xml)))确认 glob 真的扫到了文件。这个打印在排错时价值极高扫到 0 个文件后面所有问题都不用查了。第二步CSV 转 TFRecord。常见版本支持如下命令行参数python scripts/generate_tfrecord.py \ --csv_inputoutput/train_labels.csv \ --output_pathoutput/train.record \ --image_dirimages参数含义参数取值说明csv_inputCSV 路径指向 2.2 生成的表格output_pathTFRecord 输出路径建议统一放 output 目录image_dir原图目录脚本内部用 filename 拼接图片路径执行完看两个信号终端没有异常output 目录里 train.record 文件大小和图片总量成正比。如果 CSV 有 5000 行但 record 只有几十 KB大概率只写入了少量样本回第 5 章查 XML 解析问题别急着调模型。有个容易忽略的点generate_tfrecord.py 用 pandas.read_csv 时默认把首行当列名。如果 CSV 被 Excel 二次编辑过、加过列后面所有字段都会错位。我一直建议CSV 只在两个脚本之间流动中间不要手动改表真要改就在脚本里加处理逻辑。提示TensorFlow 1.x 老环境里tf.io.TFRecordWriter 要改回 tf.python_io.TFRecordWriter字段结构完全一致。先确认环境版本再看报错。4.3 用 tf.data.TFRecordDataset 验证生成结果生成完不等于生成对。我习惯直接读一条记录看字段import tensorflow as tf dataset tf.data.TFRecordDataset([output/train.record]) for raw in dataset.take(1): example tf.train.Example() example.ParseFromString(raw.numpy()) for key, value in example.features.feature.items(): print(key, value)正常输出里应该看到image/encoded 的 bytes_list 长度与原图体积接近bbox 四个坐标都在 0 到 1 之间class/label 是整数。如果 bbox 出现大于 1 的数说明归一化没生效如果 class/text 为空说明 CSV 类别名没读进来。再统计样本数count sum(1 for _ in tf.data.TFRecordDataset([output/train.record])) print(TFRecord 样本数:, count)这个数要和 CSV 行数对上。对不上的时候先想清楚生成脚本是「一行一个 Example」还是「一张图一个 Example」前者要求 CSV 每行代表一个独立样本多框数据必须按 filename 分组5.5 有完整解法。如果 Dataset 读取时报 Checksum 错误或中途 OutOfRange说明 TFRecord 文件写入时被截断重新生成一遍最省事不要尝试修复。预训练 checkpoint 对输入张量尺寸有要求TFRecord 里的图片宽高不需要统一但 batch size 和 padding 策略会影响读取效率这部分属于模型侧调优别和数据处理脚本的问题混在一起排查。5. 避坑与排查XML 解析为空、CSV 中文乱码与 TFRecord 读不出的五个案例这一章是血泪经验汇总。五个案例都来自真实项目按「现象 → 原因 → 解决」写排序大致沿流水线方向。5.1 现象CSV 只有表头一行数据都没有CSV 生成后打开一看只有 filename、width、height 八个列名下面空荡荡。先看 glob 是否匹配到文件再看 findall(object) 是否返回空。九成情况是 XML 自带命名空间标签实际是ns0:object这种形式ElementTree 的 findall(object) 找不到。解决解析前剥离命名空间改用递归遍历加 local 名判断def get_local_name(tag): return tag.split(})[-1] for elem in root.iter(): if get_local_name(elem.tag) object: # 处理目标框 pass用 root.iter() 配合 local tag 判断就能绕开 namespace 干扰。这条经验同样适用于「xml 格式文件没有标签怎么办」这类搜索场景——不是标签丢了是解析路径没对上。5.2 现象CSV 里中文类别名乱码用 Excel 打开生成的 CSV类别显示成「浜烘」之类乱码或者 pandas 读取时直接报 UnicodeDecodeError。原因是 pandas 默认按 UTF-8 写文件而 Windows 的 Excel 默认按 GBK 打开两边编码不匹配。解决写 CSV 时指定 encodingutf-8-sig。这个编码会在文件头加 BOMExcel 识别成 UTF-8 打开不乱码pandas 读回来时 BOM 被自动剥离生成 TFRecord 时 row[class].encode(utf-8) 也不会出问题。如果脚本不支持这个参数手动在 to_csv 里加一下即可。5.3 现象TFRecord 生成成功但训练时提示图片张数为 0最常见原因是图片路径拼接错误。CSV 里 filename 写的是 C:/full/path/0001.jpg 这类绝对路径而 generate_tfrecord.py 里 os.path.join(image_dir, row[filename]) 拼出来一个不存在的路径tf.io.gfile.GFile 抛异常。有些脚本把异常吞掉继续跑结果就是 record 文件生成了但内容残缺。解决转换前在脚本里加一道存在性检查img_path os.path.join(image_dir, row[filename]) if not os.path.exists(img_path): print(图片缺失:, img_path) continue同时养成习惯在写 record 前后各打一次计数样本数对不上立刻能发现。这里最容易翻车的不是代码逻辑而是「没报错就以为成功」的错觉。5.4 现象TF2 环境下调用 TFRecordWriter 报 AttributeError报错内容是 AttributeError: module tensorflow has no attribute python_io。原因是 TF2 移除了旧的 tf.python_io 命名空间老教程的写法全部失效。解决统一改用 tf.io.TFRecordWriter 和 tf.io.gfile.GFile。搜索解决方案时关键词建议带「tensorflow 2.x tfrecord 写入」别再看旧版教程。另外 tf.train.Example 在 TF2 里仍然保留不用改。5.5 现象一张图多个框训练样本数量和标注框数量对不上CSV 里一张图有 4 个框就占 4 行按行生成 TFRecord 后一个 Example 只包含一个框另外 3 个框被当成独立样本。训练时同一张图被重复读取多次而且每个样本都缺框。原因是对「样本」的定义理解错了目标检测里一张图才是一个样本一张图的所有框必须放进同一个 Example 的 repeated 字段。解决生成前按 filename 分组再写grouped df.groupby(filename) for filename, group in grouped: example create_example_with_group(group, image_dir, label_map) writer.write(example.SerializeToString())在 create_example_with_group 里xmin 等字段用 FloatList(value[...]) 一次传多个归一化坐标class/text 用 BytesList 传多个类别名。3.2 的单行写法适合每图单框的数据集多框必须走分组写法。6. 进阶用法把这套脚本改造成自己的数据流水线两个脚本跑通之后大多数人会直接拿去训练。但作为一线工具它们还能再改一版让流水线更适合自己的数据。这里给两个性价比最高的改造方向。6.1 增加坐标合理性检查与类别过滤坐标越界是标注数据里最常见的脏数据。训练前加一段清洗把 xmin xmax 或坐标超出图片边界的行直接标记出来invalid df[(df[xmin] df[xmax]) | (df[ymin] df[ymax])] print(非法框数量:, len(invalid)) df df[~df.index.isin(invalid.index)]类别过滤同理。如果数据集有几百个 category但当前任务只关心其中 5 个在 CSV 阶段按类别过滤比在 TFRecord 阶段过滤轻松得多也省得反复重写 record 文件。6.2 用 OpenCV 把 TFRecord 里的框画回去验证TFRecord 是二进制看不见摸不着。我的习惯是每改一次生成脚本就随机抽几条做可视化验证——把 Example 解码、画框、存盘人眼确认框和物体是否对齐import cv2 import numpy as np import tensorflow as tf dataset tf.data.TFRecordDataset([output/train.record]) for raw in dataset.take(3): example tf.train.Example() example.ParseFromString(raw.numpy()) feats example.features.feature image np.frombuffer(feats[image/encoded].bytes_list.value[0], np.uint8) image cv2.imdecode(image, cv2.IMREAD_COLOR) h, w image.shape[:2] xmins feats[image/object/bbox/xmin].float_list.value ymins feats[image/object/bbox/ymin].float_list.value xmaxs feats[image/object/bbox/xmax].float_list.value ymaxs feats[image/object/bbox/ymax].float_list.value for x1, y1, x2, y2 in zip(xmins, ymins, xmaxs, ymaxs): pt1 (int(x1 * w), int(y1 * h)) pt2 (int(x2 * w), int(y2 * h)) cv2.rectangle(image, pt1, pt2, (0, 255, 0), 2) cv2.imwrite(check.jpg, image)这套验证方法把前面所有环节的问题一次性暴露坐标归一化错、宽高比错、编码格式错画出来的框全部不对。从那以后我每次改完生成脚本都强制自己先抽 10 张图可视化一遍再进训练绝不跳过。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取