资讯动态

rolabelimg文件列表排序混乱?用自然排序彻底解决数字编号问题

发布时间:2026/10/20 14:48:15 来源:尧图企业网站定制
做了几年旋转目标检测的标注我一直在用rolabelimg这个YOLO旋转框标注工具。它是在LabelImg基础上改出来的旋转框版本核心用途是给遥感图像、工业质检图像这些带角度的目标画旋转矩形框保存成DOTA格式或者再转成YOLO OBB能直接用的标签。工具本身蛮好用但有个细节一直让我很烦躁——打开一个图片文件夹的时候左侧文件列表的顺序完全不是人话scan_2.png排在scan_10.png后面img_9.png排在img_100.png后面。文件数量一多想按编号跳到某一张图得来回翻好几次都找不到。后来翻了源码才发现这是典型的字典序排序问题。这篇就从问题现象、根源、到具体修改方案一次说清楚。1. 问题现象与根源定位1.1 先复现一遍字典序排序长什么样把这个现象说准确一点。假设文件夹里是img_1.png、img_2.png、img_3.png、img_10.png、img_100.png。你心里的顺序当然是1、2、3、10、100但工具左侧列表显示的是img_1.png、img_10.png、img_100.png、img_2.png、img_3.png这种。有人第一次看到还以为是随机排序刷新几次也一样。问题本质是字符串比较规则。字符串是一个字符一个字符比较的img_10.png和img_2.png比较时前半部分img_相同到第一个不同字符时一个是1一个是21比2小所以img_10.png排在img_2.png前面。词典里单词排序也是这个规则所以叫字典序或者说词典序。标题里的“字典类型排序”不是Python里的dict类型别被名字带偏。这个规则本身没什么对错但放到图片列表里就很不符合直觉。尤其当图片名是tile_1_256.png、tile_10_1024.png这种带坐标信息的切片时字典序会让你完全找不到规律tile_10_2.png可能紧挨着tile_1_512.png出现如果是人工按编号找图效率会非常低。1.2 为什么会按字典序排sort和QListWidget的默认行为rolabelimg打开文件夹的时候源码干的活大致是用文件对话框选目录遍历目录下图片扩展名生成一个文件列表然后把这个列表add到界面左侧的QListWidget里最后调用一次sortItems()。QListWidget是Qt里非常常见的列表控件addItem是按插入顺序往列表里加的但sortItems()会立刻按列表项文本做一次排序。如果不调用sortItems()列表会保持文件系统枚举文件的原始顺序。问题在于这个原始顺序往往不稳定——os.listdir在Linux上按目录项返回顺序在Windows上按文件系统枚举顺序既不保证按数字大小也不保证每次都一样。工具作者加上sortItems()本意是让列表看起来“有序”只是这个“有序”是按字符串的字典序来的。我去翻了源码发现rolabelimg沿用了LabelImg的UI结构sortItems()直接调用了Qt默认的字符串比较。Qt的列表项、Python的sorted()、shell里的sort命令默认都是这个字典序行为。也就是说不是工具故意按乱七八糟的顺序排而是它用了“最通用”的排序规则恰好不适合带数字编号的图片名。1.3 标注场景里为什么特别难受也许有人觉得排错就排错呗不过是个列表。但标注场景不是看图浏览是要按切片编号找图的。遥感切片图文件名往往是tile_x_y.pngx和y代表坐标位置你要根据外业记录快速定位到tile_3_15.png这张图字典序排完tile_10_2.png挤在很前面你得肉眼一条一条过。还有一个更实际的场景是连续标注。你昨天标到第280张今天要继续你希望点一下列表就能看到下一张在哪字典序下280后面可能直接是3或者干脆排到很后面几百张图里找上次断点每天浪费几分钟积累下来非常影响节奏。标注工作本身已经够费眼了这种“列表顺序和脑子对不上”的摩擦属于完全没必要的损耗。2. 两条解决路线改文件名还是改工具2.1 不碰代码批量重命名补零法最不动脑子也最安全的方案是改文件名本身把数字部分补零补到同样长度。img_2.png改成img_0002.pngimg_100.png改成img_0100.png。补零之后字符串比较时每一位都是同长度的数字字符在比字典序自然等价于数字大小序。脚本很简单Python几行就能处理import os import re root E:/dataset/images for old_name in os.listdir(root): if not old_name.lower().endswith((.png, .jpg, .jpeg)): continue new_name re.sub(r(\d), lambda m: m.group(1).zfill(6), old_name) if new_name ! old_name: old_path os.path.join(root, old_name) new_path os.path.join(root, new_name) if os.path.exists(new_path): print(f跳过目标已存在: {new_name}) continue os.rename(old_path, new_path) print(f{old_name} - {new_name})zfill(6)是左补零到6位1000张以内的图都能覆盖。如果你的图是tile_1_256.png这种多个数字的正则会把每一个数字段都补零变成tile_000001_000256.png排序时也是按数字大小走。这个方案优点是零风险不用动工具代码缺点是影响所有文件本身。已经标过的图文件夹里还有同名txt标注文件改图名前必须同步改txt否则打开新文件名后找不到同名txt标签全部失配。另外补零后的文件名显示出来很丑在界面里看到的是img_0000002.png而不是img_2.png对追求清爽的人来说很难受。还有一个硬伤如果这批图是从别处拷来的或者同事那边还没有重命名你这边改了名字后面数据交换又要来回解释。注意如果目录里已经有同名txt标注文件改图名前必须用同样的规则把txt也改掉否则标注文件会彻底失配。2.2 从根上改自然排序算法比补零更优雅的做法是让工具自己用自然排序。自然排序把文件名里的数字部分识别出来转成整数参与比较其余部分按普通字符串比较。核心代码就这么几行import re def natural_key(name): parts re.split(r(\d), name) return [int(part) if part.isdigit() else part.lower() for part in parts]re.split(r(\d), name)是关键括号不能去掉。re.split里带括号分组时返回结果会保留分隔符本身img_10.png会被拆成[img_, 10, .png]。如果把括号去掉数字部分不会出现在结果里key里就只剩字符串片段整段排序直接退回字典序等于白改。拆完以后对每个片段判断如果是纯数字转成整数否则转成小写字符串。这样比较时数字按数值大小比字符串部分按小写字典序比。用法files [img_2.png, img_10.png, img_1.png] print(sorted(files, keynatural_key)) # [img_1.png, img_2.png, img_10.png]顺便说一句sorted()是稳定排序如果两个文件名的自然key完全相等会保留它们在原列表里的相对顺序一般不会出问题。2.3 两种方案怎么选对比项改文件名补零改工具源码是否影响数据文件影响原图和同名txt都要改不影响只影响列表展示是否一劳永逸每来一批新图都要再跑一遍打开任何文件夹都生效风险点改错名、txt失配排序逻辑改错影响列表行为适合人群不想碰工具代码能打开源码、愿意维护补丁我自己的选择是改源码。原因很简单我手里的数据集有几百个文件夹每批新图回来都重新补零不现实而工具改一次之后就一直生效。改源码的“风险”其实很低因为排序只影响列表展示顺序不碰标注数据本身。3. 实操给rolabelimg打上数字大小排序补丁3.1 找到打开文件夹的代码入口rolabelimg的源码结构不算复杂主窗口逻辑一般在labelImg.py这个文件里。打开文件夹的处理流程大致是通过QFileDialog选择目录遍历目录下图片扩展名生成filelistaddItems到fileListWidget然后调用sortItems()。你在源码里搜sortItems基本一两分钟内就能定位到那一行。注意搜到的地方可能不止一处比如清空列表后重新打开文件夹也会调用。定位后不要急着改先看看filelist里的元素是纯文件名还是完整路径。不同版本写法略有差异有的版本在打开文件夹时只收集文件名有的版本收集的是os.path.join(dir, name)后的完整路径。这一步判断决定了排序key怎么写如果列表里是完整路径直接对路径字符串排序会先比较上级目录名同一目录下的文件顺序反而可能不对所以要用os.path.basename()把文件名单独拎出来。3.2 替换排序逻辑的具体实现推荐改法把排序前移到addItems之前基于basename排序然后按排好的顺序逐条addItem不再调用sortItems()。示意代码# 打开文件夹处理中找到类似下面的片段 self.fileListWidget.clear() self.fileListWidget.addItems(filelist) self.fileListWidget.sortItems() # 就是这行搞的鬼 # 改成 self.fileListWidget.clear() filelist.sort(keylambda p: natural_key(os.path.basename(p))) for file_path in filelist: self.fileListWidget.addItem(file_path) # 原来的 sortItems() 删掉natural_key函数放在文件头部和import放一起import os import re def natural_key(name): parts re.split(r(\d), name) return [int(part) if part.isdigit() else part.lower() for part in parts]为什么不保留sortItems()因为QListWidget的sortItems()只会用Qt默认的字符串规则再排一次会把刚排好的自然序重新打乱。删掉它列表就老老实实按插入顺序显示而插入顺序已经在前面用filelist.sort()控制好了。如果你不想动打开文件夹的逻辑还有一条偏门路线继承QListWidgetItem重写__lt__方法让Qt排序时使用自然序。但说实话我在一个PyQt5分支上这样改有效在另一个基于PySide6的分支上却不生效Qt对Python子类比较方法的调用在不同的绑定实现里并不一致。最可控的还是直接排好序再逐条addItem代码直观出了问题也好排查。注意修改后必须彻底重启工具。Python会生成__pycache__缓存如果启动后还是旧现象把工具目录下的__pycache__清掉再启动一次。3.3 实测验证与注意事项为了验证效果我用脚本生成了带编号的测试图片for i in range(1, 121): open(fimg_{i}.png, w).close()改源码前打开这个文件夹列表前几条长这样顺序改前文件名改后文件名1img_1.pngimg_1.png2img_10.pngimg_2.png3img_100.pngimg_3.png4img_11.pngimg_4.png5img_12.pngimg_5.png改完后从img_1一路顺到img_120没有跳变。我又测了多数字文件名比如生成tile_1_256.png、tile_1_32.png、tile_10_64.png自然排序会先比较第一个数字段再比较后续段整体顺序符合“列号优先、行号其次”的直觉。还有一个必须验证的点切换图片和保存标注文件是否正常。rolabelimg加载标签是按当前图片的basename找同名txt和列表显示顺序没有关系所以排序修改不影响保存路径。我连续标了几十张保存、切换、再打开标注数据都没丢。这点可以放心。4. 改完踩过的坑与排查技巧4.1 补丁没生效的常见原因最常见的原因是不止一个地方调用了sortItems()。你改了打开文件夹的方法但工具在初始化、清空画布后重新加载列表时也可能调用sortItems()把顺序又冲掉了。解决方法是全局搜一遍sortItems()把所有影响文件列表的地方一并处理或者把自然排序写成一个独立函数在每个加列表的操作之前都调用。另一个可能是文件没保存就启动了旧缓存。Python的__pycache__会缓存编译后的字节码你改了.py文件但启动的还是旧缓存表现就是“代码明明改了效果没变”。这种情况把__pycache__目录删掉再启动就好。如果你给了同事新代码他们那边也出现同样的诡异现象优先怀疑缓存。4.2 多数字文件名和多级目录文件名自然排序并不是万能的。像v1.10和v1.2按自然排序v1.2在前符合“版本号”的直觉没问题。但如果是语义上的严格版本号比如v2.0.1和v2.0.10自然排序对比的是各段数字的值大多数情况下也是对的但如果段数、位数非常规仍然可能出现偏差。标注场景很少碰版本号大多数是纯序号切片图所以自然排序够用。多级目录路径的情况也要注意。如果filelist里存的是完整路径排序key必须用os.path.basename(p)提取文件名。我之前就踩过这个坑直接对完整路径排序同一目录下的文件顺序可能被上级目录名的相同前缀干扰看起来像是“随机排列”。用basename后就正常了。4.3 中文文件名的排序问题如果你的标注文件名是中文比如道路_1.png、道路_2.png、道路_10.png自然排序依然能解决数字部分但中文部分会按Unicode码点排序也就是按字符编码值排和拼音没有关系。好在同前缀中文文件名数字部分解决以后顺序就是对的。如果文件名是中文开头且前缀不同想按拼音排序那就得动用locale.strxfrm或者第三方拼音库。但标注场景一般没必要折腾这个因为配套的标签文件名也是中文改了排序规则反而可能让标注文件匹配逻辑变复杂。我的建议是能用英文前缀就用英文前缀省得在排序上浪费时间。4.4 工具升级后补丁丢失与维护方法工具版本更新后拉新代码补丁很可能被覆盖。建议把改过的代码单独存一个diff文件升级后重打。更粗犷一点的办法是用sitecustomize.py在解释器启动时做猴子补丁但这对PyQt应用有风险我一般不推荐。最省心的方案是维护一个自己用的分支官方更新时同步merge冲突一般就一两行很好处理。最后说两个实际操作中的体会。第一个改排序这种小问题先确认自己有没有改错层次。很多人第一反应是去设置界面找排序选项但这类工具往往没有暴露这个配置最直接的还是源码层。第二个如果你身边有同事也在用这个工具改完源码后把同目录下的__pycache__清掉或者干脆把整个新目录复制给他们不然会出现一个人效果正常、另一个人还是旧顺序的诡异现象。排序问题看起来小但在几百张切片图里翻找目标的时候它每天都会扎你一下。趁早改了后面什么都顺。

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

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

免费获取报价 →
↑