labelImg安装、改软件后打包成exe、改软件功能
本文更改了软件功能,部分如下:
- 打包的labelImg.exe自动识别同目录的predefined_classes.txt,请标注前定义好具体的标签。保存目录里不再需要classes.txt。
- 默认YOLO格式,且勾选Auto Save mode。
- 去掉透明度,使用红色框,方便浏览。
- 标签界面高度依据标签数自动加大,避免标签多了每次下拉滑动条。当超过18个标签,出现滚动条。
- 不允许编辑标签,单击标签就可以保存,不需要双击。
一、labelImg安装
需要python3.9,不建议更高版本。
创建python3.9环境
conda create --name=labelImg python=3.9 conda activate labelImg
安装labelImg
pip install labelimg -i https://pypi.tuna.tsinghua.edu.cn/simple
二、改软件后打包成exe
安装pyinstaller
pip install pyinstaller
进入目录
cd /d D:\miniconda3\envs\labelImg\Lib\site-packages\labelImg
修改labelImg.py文件,在文件末尾
1557行附近
def load_yolo_txt_by_filename(self, txt_path):
if self.file_path is None:
return
if os.path.isfile(txt_path) is False:
return
self.set_format(FORMAT_YOLO)
if getattr(sys, 'frozen', False):# 打包环境下使用可执行文件同目录的predefined_classes.txt
default_class_file = os.path.join(os.path.dirname(sys.executable), "predefined_classes.txt")
else:# 开发环境下使用labelImg.py同目录的predefined_classes.txt
default_class_file = os.path.join(os.path.dirname(__file__), "predefined_classes.txt")
#t_yolo_parse_reader = YoloReader(txt_path, self.image, os.path.join(os.path.dirname(__file__), "predefined_classes.txt"))
t_yolo_parse_reader = YoloReader(txt_path, self.image, default_class_file)#修改下
shapes = t_yolo_parse_reader.get_shapes()
1620行附近
# Tzutalin 201705+: Accept extra agruments to change predefined class file
argparser = argparse.ArgumentParser()
argparser.add_argument("image_dir", nargs="?")
'''
argparser.add_argument("class_file",
default=os.path.join(os.path.dirname(__file__), "predefined_classes.txt"),
nargs="?")
'''
if getattr(sys, 'frozen', False):# 打包环境下使用可执行文件同目录的predefined_classes.txt
default_class_file = os.path.join(os.path.dirname(sys.executable), "predefined_classes.txt")
else:# 开发环境下使用labelImg.py同目录的predefined_classes.txt
default_class_file = os.path.join(os.path.dirname(__file__), "predefined_classes.txt")
argparser.add_argument("class_file",
default=default_class_file, #修改下
nargs="?")
argparser.add_argument("save_dir", nargs="?")
args = argparser.parse_args(argv[1:])
打包,--windowed不带小黑窗口
pyinstaller --onefile --windowed labelImg.py
得到的exe可以放到其他地方,使用时请先编辑好predefined_classes.txt

调试bug时,建议带小黑窗口调试,多打印print
三、改软件功能
1、默认使用同目录的predefined_classes.txt,不再需要classes.txt。默认YOLO格式,且勾选Auto Save mode




保存目录不再需要classes.txt。为了防止读取txt有问题,指定utf8

52行附近
def save(self, class_list=[], target_file=None): out_file = None # Update yolo .txt #out_class_file = None # Update class list .txt if target_file is None: out_file = open( self.filename + TXT_EXT, 'w', encoding=ENCODE_METHOD) #classes_file = os.path.join(os.path.dirname(os.path.abspath(self.filename)), "classes.txt") #out_class_file = open(classes_file, 'w') else: out_file = codecs.open(target_file, 'w', encoding=ENCODE_METHOD) #classes_file = os.path.join(os.path.dirname(os.path.abspath(target_file)), "classes.txt") #out_class_file = open(classes_file, 'w') for box in self.box_list: class_index, x_center, y_center, w, h = self.bnd_box_to_yolo_line(box, class_list) # print (classIndex, x_center, y_center, w, h) out_file.write("%d %.6f %.6f %.6f %.6f\n" % (class_index, x_center, y_center, w, h)) # print (classList) # print (out_class_file) ''' for c in class_list: out_class_file.write(c+'\n') out_class_file.close() ''' out_file.close()

2、设置颜色

纯透明、红框
DEFAULT_LINE_COLOR = QColor(0, 255, 0, 255) DEFAULT_FILL_COLOR = QColor(255, 0, 0, 0) DEFAULT_SELECT_LINE_COLOR = QColor(255, 0, 0) DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 0) DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255) DEFAULT_HVERTEX_FILL_COLOR = QColor(255, 0, 0)
打开看下效果,先删除 C:\Users\YourAccount\.labelImgSettings.pkl,重新打开labelImg
2种打开方式:
- 终端里直接输入labelImg,回车。即开篇的第1个图中labelImg.py
- 如下目录双击labelImg.exe

3、 不允许编辑标签,单击标签就可以保存。标签界面高度加大。


4、删除框后,如果图中没有任何框了,对应的标注文件也删除,如txt等。
这个功能不建议改,因为mmyolo与官方yolo,在背景数据集上的注意事项 - 夕西行 - 博客园
labelImg.py里save_labels方法,黄底字体为新增
print('Image:{0} -> Annotation:{1}'.format(self.file_path, annotation_file_path)) #保存后检查如果没有任何标签框,删除文件 if self.no_shapes() and os.path.exists(annotation_file_path): try: os.remove(annotation_file_path) self.status("已删除空标签文件: {}".format(os.path.basename(annotation_file_path))) except Exception as e: self.status("删除文件时出错: {}".format(str(e))) return True except LabelFileError as e:
5、删除当前图片,如果有标注文件,也一起删除。删除快捷键改为R
labelImg.py里delete_image函数
def delete_image(self): delete_path = self.file_path ''' if delete_path is not None: self.open_next_image() self.cur_img_idx -= 1 self.img_count -= 1 if os.path.exists(delete_path): os.remove(delete_path) self.import_dir_images(self.last_open_dir) ''' if delete_path is not None: # 记录当前索引 current_idx = self.cur_img_idx # 删除图像文件和对应的标注文件 if os.path.exists(delete_path): # 删除图像文件 os.remove(delete_path) #图像文件的全路径 # 删除对应的标注文件 base_name = os.path.splitext(os.path.basename(delete_path))[0] #不带后缀的文件名(无路径) # 删除所有可能的标注文件格式 annotation_files = [ base_name + '.txt', # YOLO格式 base_name + '.xml', # Pascal VOC格式 base_name + '.json', # Create ML格式 ] for ann_file in annotation_files: try: ann_file_path = os.path.join(self.default_save_dir, ann_file) #标签文件全路径 print(f"标注文件: {ann_file}") print(f"标注文件全路径: {ann_file_path}") if os.path.exists(ann_file_path): os.remove(ann_file_path) print(f"已删除标注文件: {os.path.basename(ann_file_path)}") except Exception as e: print(f"删除标注文件 {ann_file} 时出错: {str(e)}") # 删除文件 if os.path.exists(delete_path): os.remove(delete_path) # 手动执行 import_dir_images 的部分逻辑,但不调用 open_next_image self.last_open_dir = self.last_open_dir self.dir_name = self.last_open_dir self.file_list_widget.clear() self.m_img_list = self.scan_all_images(self.last_open_dir) self.img_count = len(self.m_img_list) # 重新填充文件列表 for imgPath in self.m_img_list: item = QListWidgetItem(imgPath) self.file_list_widget.addItem(item) # 如果还有图片,打开正确的图片 if self.m_img_list: # 确定要打开的索引 if current_idx < len(self.m_img_list): next_idx = current_idx # 打开下一张 else: next_idx = len(self.m_img_list) - 1 # 打开上一张 # 打开图片 self.cur_img_idx = next_idx filename = self.m_img_list[next_idx] if filename: self.load_file(filename) else: # 没有图片了,清空界面 self.file_path = None self.reset_state()
labelImg.py里__init__函数
delete_image = action(get_str('deleteImg'), self.delete_image, 'R', 'close', get_str('deleteImgDetail'))
6、选择标签路径后,为所有的图片创建空txt,已存在txt跳过
labelImg.py中change_save_dir_dialog函数,新增黄底代码
def change_save_dir_dialog(self, _value=False): if self.default_save_dir is not None: path = ustr(self.default_save_dir) else: path = '.' dir_path = ustr(QFileDialog.getExistingDirectory(self, '%s - Save annotations to the directory' % __appname__, path, QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)) if dir_path is not None and len(dir_path) > 1: self.default_save_dir = dir_path self.statusBar().showMessage('%s . Annotation will be saved to %s' % ('Change saved folder', self.default_save_dir)) self.statusBar().show() #创建空txt,如果已经存在则跳过 for imgPath in self.m_img_list: img_filename = os.path.basename(imgPath) #图片名 img_name_without_ext = os.path.splitext(img_filename)[0] #去掉图片名后缀,如.bmp txt_path = os.path.join(self.default_save_dir, img_name_without_ext + ".txt") #txt全路径 if not os.path.exists(txt_path): with open(txt_path, 'w', encoding='utf-8') as f: pass # 创建空文件 print(f"已创建: {txt_path}") else: print(f"已存在,跳过: {txt_path}")
具体操作:先选择图像目录(即Open Dir),再选择标签目录(即Change Save Dir)后,自动生成与图同名空txt,若同名txt已存在则跳过
7、关闭labelImg前,将已标注的剪切到指定目录
避免下次打开不知道标到哪了,以及人为剪切图与txt时产生的失误。


labelImg.py里的closeEvent函数
def closeEvent(self, event): if not self.may_continue(): #关闭前所有修改都已确认 event.ignore() #==1 添加提示是否剪切已标注样本到指定目录 if self.img_count>1: #软件默认参数self.img_count=1,一般也不会只标一张图 print(f"图像数量: {self.img_count}") # 弹出询问对话框 reply = QMessageBox.question( self, '提示', '已标注的剪切到其它文件夹?\n(会将当前及之前的图像移到指定目录)', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # 选择目标目录 target_dir = QFileDialog.getExistingDirectory( self, "选择目标文件夹", QDir.currentPath(), QFileDialog.ShowDirsOnly ) if target_dir: #执行剪切操作 # 创建目标子目录 img_target_dir = os.path.join(target_dir, 'images') label_target_dir = os.path.join(target_dir, 'labels') os.makedirs(img_target_dir, exist_ok=True) os.makedirs(label_target_dir, exist_ok=True) # 获取需要剪切的图像列表(当前图像及之前的所有图像) cut_img_list = self.m_img_list[:self.cur_img_idx + 1] # 显示进度对话框 progress = QProgressDialog("正在剪切文件...", "取消", 0, len(cut_img_list), self) progress.setWindowModality(Qt.WindowModal) progress.setMinimumDuration(0) # 执行剪切 cut_count = 0 error_count = 0 error_messages = [] for i, img_path in enumerate(cut_img_list): # 检查是否取消 if progress.wasCanceled(): break progress.setValue(i) progress.setLabelText(f"正在处理: {os.path.basename(img_path)}") try: # 1. 剪切图像文件 if os.path.exists(img_path): img_filename = os.path.basename(img_path) dest_img_path = os.path.join(img_target_dir, img_filename) # 直接移动,不检查是否存在(如果存在会报错或覆盖) shutil.move(img_path, dest_img_path) cut_count += 1 # 2. 剪切对应的txt文件 img_basename = os.path.splitext(img_filename)[0] txt_path = os.path.join(self.default_save_dir, img_basename + '.txt') if os.path.exists(txt_path): dest_txt_path = os.path.join(label_target_dir, img_basename + '.txt') shutil.move(txt_path, dest_txt_path) cut_count += 1 else: error_msg = f"未找到标注文件: {img_basename}.txt" print(f"警告: {error_msg}") error_messages.append(error_msg) error_count += 1 else: error_msg = f"图像文件不存在: {img_path}" print(f"警告: {error_msg}") error_messages.append(error_msg) error_count += 1 except Exception as e: error_msg = f"处理失败 {os.path.basename(img_path)}: {str(e)}" print(f"错误: {error_msg}") error_messages.append(error_msg) error_count += 1 progress.setValue(len(cut_img_list)) # 显示结果 if cut_count > 0 or error_count > 0: result_msg = f"操作完成!\n\n" result_msg += f"成功剪切: {cut_count} 个文件\n" result_msg += f"失败: {error_count} 个文件\n\n" result_msg += f"图像保存到: {img_target_dir}\n" result_msg += f"标注保存到: {label_target_dir}" if error_messages and len(error_messages) <= 5: result_msg += "\n\n错误详情:\n" + "\n".join(error_messages[:5]) elif error_messages: result_msg += f"\n\n有 {len(error_messages)} 个错误,请查看控制台输出" QMessageBox.information(self, '完成', result_msg) else: QMessageBox.warning(self, '失败', '没有文件被剪切') #==1 结束,之后是原有代码 settings = self.settings # If it loads images from dir, don't load it at the beginning if self.dir_name is None: settings[SETTING_FILENAME] = self.file_path if self.file_path else '' else: settings[SETTING_FILENAME] = ''
8、关闭软件时记录图片,方便下次打开软件选择文件夹后自动定位到该图像,而不是第1张
labelImg.py里的closeEvent函数
def closeEvent(self, event): if not self.may_continue(): #关闭前所有修改都已确认 event.ignore() # 记录当前打开的图片到目录 if self.dir_name and self.file_path: try: image_name = os.path.basename(self.file_path) last_image_file = os.path.join(self.dir_name, 'last_image.txt') with open(last_image_file, 'w', encoding='utf-8') as f: f.write(image_name) except Exception as e: print(f"保存最后图片记录失败: {e}") settings = self.settings # If it loads images from dir, don't load it at the beginning if self.dir_name is None:
labelImg.py里的open_next_image函数
def open_next_image(self, _value=False): # Proceeding prev image without dialog if having any label if self.auto_saving.isChecked(): if self.default_save_dir is not None: if self.dirty is True: self.save_file() else: self.change_save_dir_dialog() return if not self.may_continue(): return if self.img_count <= 0: return filename = None if self.file_path is None: #第一次打开图像目录 # 检查是否有上次打开的图片记录 if self.dir_name: last_image_file = os.path.join(self.dir_name, 'last_image.txt') if os.path.exists(last_image_file): try: with open(last_image_file, 'r', encoding='utf-8') as f: last_image_name = f.read().strip() # 在m_img_list中查找匹配的文件名 found = False for img_path in self.m_img_list: if os.path.basename(img_path) == last_image_name: self.cur_img_idx = self.m_img_list.index(img_path) filename = img_path found = True print(f"找到匹配的图片,索引: {self.cur_img_idx}") break if not found: print(f"未找到匹配的图片,使用第一张") filename = self.m_img_list[0] self.cur_img_idx = 0 except Exception as e: print(f"读取最后图片记录失败: {e}") filename = self.m_img_list[0] self.cur_img_idx = 0 else: filename = self.m_img_list[0] self.cur_img_idx = 0 else: #已经有图片在显示,用户点击了"下一张"按钮 if self.cur_img_idx + 1 < self.img_count: self.cur_img_idx += 1 filename = self.m_img_list[self.cur_img_idx] if filename: self.load_file(filename)

浙公网安备 33010602011771号