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

image

调试bug时,建议带小黑窗口调试,多打印print

三、改软件功能

1、默认使用同目录的predefined_classes.txt,不再需要classes.txt。默认YOLO格式,且勾选Auto Save mode

image

image

image

image

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

image

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()

image

2、设置颜色

image

纯透明、红框

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、 不允许编辑标签,单击标签就可以保存。标签界面高度加大。

image

image

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时产生的失误。

image

image

 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)

 

posted @ 2025-07-24 12:32  夕西行  阅读(458)  评论(0)    收藏  举报