
本文解析 python 批量转换 dota 格式为 yolo 格式时,因全局 `coordinateslist` 未在每次循环中清空,导致后续文件误用前序数据、坐标归一化异常(值 >1)及输出行数膨胀的核心问题,并提供健壮的修复方案。
在批量处理多文件的场景中,一个看似微小的状态管理疏忽——如未及时重置累积型列表——会引发连锁性错误。您遇到的问题正是典型例证:首个文件输出正确,而后续文件出现坐标值大于 1、行数异常增多、YOLO 格式失效。根本原因并非“文件被提前打开”,而是 coordinatesList 在循环外定义且未在每次处理新文件前清空,导致它持续追加所有输入文件的坐标点,最终使第 n 个输出文件不仅写入自身数据,还混入了前 n−1 个文件的残留坐标。
? 问题复现逻辑分析
观察原代码片段:
coordinatesList = list() # ← 定义在循环外部(如 for textFile in textFiles: 上方)
with open(textPath, "r") as f:
contentsSplitLine = f.read().splitlines()[2:]
for i in range(len(contentsSplitLine)):
splitLine = ' '.join(contentsSplitLine[i].rsplit(' ', 2)[:-2]).split()
coordinatesList.append(splitLine) # ← 每次都追加!不重置!
# ... 后续对 coordinatesList 的遍历与写入当处理第二个 .txt 文件时,coordinatesList 仍保留第一个文件的所有坐标,因此 for coordinates in coordinatesList: 实际遍历的是 全部历史数据,造成:
- 输出行数远超当前文件真实行数;
- 归一化计算使用了错误的 imageWidth/imageHeight(来自当前图像),但坐标却是前一图像的原始像素值 → centerX = ((maxX+minX)/2) * (1/imageWidth) 中 maxX 可能高达数千,除以当前图宽后仍 >1;
- 多个文件共用同一输出文件路径("a" 模式),进一步加剧内容污染。
✅ 正确做法:作用域隔离 + 显式初始化
应将 coordinatesList 严格限定在单文件处理作用域内,并在每次进入新文件时重新创建空列表:
立即学习“Python免费学习笔记(深入)”;
import os
import cv2
textDir = "./inputData"
imageDir = "./inputImages" # 假设图像目录
outputDir = "./outputData"
os.makedirs(outputDir, exist_ok=True)
for textFile in os.listdir(textDir):
if not textFile.endswith(".txt"):
continue
textPath = os.path.join(textDir, textFile)
# 匹配同名图像(如 P0000.txt → P0000.jpg)
imageFile = textFile.replace(".txt", ".jpg") # 或 .png,按实际调整
imagePath = os.path.join(imageDir, imageFile)
if not os.path.exists(imagePath):
print(f"Warning: image {imageFile} not found for {textFile}")
continue
img = cv2.imread(imagePath)
if img is None:
print(f"Failed to load image {imagePath}")
continue
imageHeight, imageWidth = img.shape[:2] # 忽略 channels 更安全
print(f"Processing {textFile}: {imageWidth}x{imageHeight}")
# ✅ 关键修复:coordinatesList 现在是局部变量,每次循环自动新建
coordinatesList = []
with open(textPath, "r") as f:
lines = f.read().splitlines()[2:] # 跳过前两行元数据
for line in lines:
# 安全分割:移除最后两个字段(类别、难度),再拆坐标
coords_str = ' '.join(line.rsplit(' ', 2)[:-2])
coords = coords_str.split()
if len(coords) < 8: # 至少需4个点(8个坐标)
continue
coordinatesList.append(coords)
# ✅ 使用 with 语句安全写入,避免手动 close 和异常中断风险
output_path = os.path.join(outputDir, textFile)
with open(output_path, "w") as out_f: # "w" 覆盖写入,非追加!
for coords in coordinatesList:
try:
coords_int = [int(x) for x in coords]
xs = coords_int[::2]
ys = coords_int[1::2]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
# YOLO 格式:class_id center_x center_y width height(全归一化到 [0,1])
center_x = ((max_x + min_x) / 2.0) / imageWidth
center_y = ((max_y + min_y) / 2.0) / imageHeight
width = (max_x - min_x) / imageWidth
height = (max_y - min_y) / imageHeight
# 防御性截断:确保归一化值在 [0,1] 内(极少数边界情况)
center_x = max(0.0, min(1.0, center_x))
center_y = max(0.0, min(1.0, center_y))
width = max(0.0, min(1.0, width))
height = max(0.0, min(1.0, height))
out_f.write(f"0 {center_x:.16f} {center_y:.16f} {width:.16f} {height:.16f}\n")
except (ValueError, ZeroDivisionError) as e:
print(f"Error processing line in {textFile}: {e}")
continue⚠️ 关键注意事项总结
- 永远避免跨迭代共享可变状态:列表、字典等聚合容器必须在循环体内初始化(或显式 .clear()),而非外部声明。
- 写入模式选择:使用 "w"(覆盖)而非 "a"(追加),除非明确需要增量写入;批量处理中 "a" 是常见陷阱。
- 资源管理:优先用 with open(...) as f: 确保文件自动关闭,防止句柄泄漏。
- 鲁棒性增强:添加 try/except 捕获 int() 转换异常、图像加载失败、空坐标等边界情况。
- 归一化校验:即使算法正确,也建议对 center_x, width 等结果做 [0,1] 截断,避免浮点误差导致的轻微越界(YOLO 训练器通常拒绝 >1 的值)。
遵循以上原则,您的 Dota→YOLO 批量转换脚本即可稳定、准确地处理任意数量的文件,彻底杜绝“只有第一个文件正确”的诡异现象。










