ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CVAT 导出崩溃排查实录:一个空壳轨迹引发的连锁反应

CVAT 导出崩溃排查实录:一个空壳轨迹引发的连锁反应 一、遇到问题导出任务时的神秘崩溃在使用老版本 CVATComputer Vision Annotation Tool导出标注数据时我们遇到了一个导致导出任务直接中断的致命报错。报错信息指向了dataset_manager模块具体的异常堆栈如下Traceback (most recent call last): ... File /home/django/cvat/apps/dataset_manager/annotation.py, line 393, in _get_objects_by_frame shape obj[shapes][-1] # optimization for old tracks IndexError: list index out of range此外在网页端导出时也遇到了同样的报错Could not export dataset for the task 573 Error: Request failed with status code 500. “IndexError: list index out of range\n”。错误信息明确指向Task 573说明问题并非随机发生而是与特定任务的数据状态密切相关。二、分析问题从异常堆栈推导业务逻辑面对这个IndexError: list index out of range错误我们进行了深入的代码逻辑分析代码行为报错行obj[shapes][-1]试图获取某个轨迹Track的最后一个形状Shape。异常原因在 Python 中对一个空列表执行[-1]索引操作会抛出越界异常。这说明在数据库中存在至少一条“空壳轨迹”——即该轨迹在数据库中有记录但其对应的形状列表shapes为空。业务场景这通常是因为标注人员在操作时创建了轨迹但忘记画框或者在删除某一帧的标注框时只删除了形状而没有彻底删除轨迹本身从而产生了这种“脏数据”。理解了异常的本质后下一步就是要在数据库层面把这条“空壳轨迹”找出来。# 容器内部 Supervisor 记录的完整日志文件django02dd1763fd8d:~$cat/var/log/supervisor/cvat_server-stderr.log|grep-A30IndexErrorcat: /var/log/supervisor/cvat_server-stderr.log: No suchfileor directory django02dd1763fd8d:~$cat/var/log/apache2/error.log|grep-A30IndexErrorcat: /var/log/apache2/error.log: Permission denied django02dd1763fd8d:~$cat/var/log/mod_wsgi/error.log|grep-A30IndexErrorcat: /var/log/mod_wsgi/error.log: No suchfileor directory django02dd1763fd8d:~$# 当前在容器内使用的是 django 普通用户而 Apache 的日志文件属于系统级文件普通用户没有权限读取所以报了 Permission denied# 退出当前容器以 root 身份重新进入容器dockerexec-uroot-itcvat_serverbashroot02dd1763fd8d:~# cat /var/log/apache2/error.log | grep -A 30 IndexErrorroot02dd1763fd8d:~# grep -rnw /var/log -e IndexErrorroot02dd1763fd8d:~## 既然常规日志走不通我们就用终极绝招直接在容器内运行一段 Python 脚本绕过 Web 服务强制调用 CVAT 底层的导出逻辑。# 于你的 CVAT 版本比较老调用 CVAT 自带的命令行工具来触发导出。python manage.py export_task573coco root02dd1763fd8d:~# python manage.py export_task 573 cocoUnknown command:export_taskTypemanage.py helpforusage. root02dd1763fd8d:~# ^Croot02dd1763fd8d:~## 找到老版本 CVAT 真实的导出函数。从你的 grep 结果来看最底层的真实入口是 /home/django/cvat/apps/dataset_manager/task.py 第 741 行的 export_task 函数。root02dd1763fd8d:~# grep -rn def export /home/django/cvat/apps/dataset_manager//home/django/cvat/apps/dataset_manager/formats/registry.py:68:def exporter(name, version, ext,display_nameNone,enabledTrue,dimensionDimensionType.DIM_2D): /home/django/cvat/apps/dataset_manager/project.py:19:def export_project(project_id, dst_file, format_name, /home/django/cvat/apps/dataset_manager/project.py:124: def export(self, dst_file: str, exporter: Callable, host:str, **options): /home/django/cvat/apps/dataset_manager/views.py:46:def export(dst_format,project_idNone,task_idNone,job_idNone,server_urlNone,save_imagesFalse): /home/django/cvat/apps/dataset_manager/views.py:107:def export_job_annotations(job_id,dst_formatNone,server_urlNone): /home/django/cvat/apps/dataset_manager/views.py:110:def export_job_as_dataset(job_id,dst_formatNone,server_urlNone): /home/django/cvat/apps/dataset_manager/views.py:113:def export_task_as_dataset(task_id,dst_formatNone,server_urlNone): /home/django/cvat/apps/dataset_manager/views.py:116:def export_task_annotations(task_id,dst_formatNone,server_urlNone): /home/django/cvat/apps/dataset_manager/views.py:119:def export_project_as_dataset(project_id,dst_formatNone,server_urlNone): /home/django/cvat/apps/dataset_manager/views.py:123:def export_project_annotations(project_id,dst_formatNone,server_urlNone): /home/django/cvat/apps/dataset_manager/task.py:543: def export(self, dst_file, exporter,host, **options): /home/django/cvat/apps/dataset_manager/task.py:630: def export(self, dst_file, exporter,host, **options): /home/django/cvat/apps/dataset_manager/task.py:691:def export_job(job_id, dst_file, format_name, /home/django/cvat/apps/dataset_manager/task.py:741:def export_task(task_id, dst_file, format_name, root02dd1763fd8d:~## 捕获到了完整的 Python 报错堆栈Tracebackpython-c import os import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, cvat.settings.production) django.setup() from cvat.apps.dataset_manager.task import export_task # 强制触发 Task 573 的 COCO 格式导出 # 如果你实际导出的是 YOLO 格式请把下面的 coco 换成 yolo export_task(573, /tmp/test_export.zip, coco) Traceback(most recent call last): Filestring, line11,inmoduleFile/home/django/cvat/apps/dataset_manager/task.py, line750,inexport_task task.init_from_db()File/home/django/cvat/apps/dataset_manager/task.py, line628,ininit_from_db self._merge_data(annotation.ir_data, start_frame, overlap)File/home/django/cvat/apps/dataset_manager/task.py, line599,in_merge_data annotation_manager.merge(data, start_frame, overlap)File/home/django/cvat/apps/dataset_manager/annotation.py, line158,inmerge tracks.merge(data.tracks, start_frame, overlap)File/home/django/cvat/apps/dataset_manager/annotation.py, line214,inmerge old_objects_by_frameself._get_objects_by_frame(self.objects, start_frame)File/home/django/cvat/apps/dataset_manager/annotation.py, line393,in_get_objects_by_frame shapeobj[shapes][-1]# optimization for old tracksIndexError: list index out of range root02dd1763fd8d:~#三、排查问题老版本 CVAT 数据库模型的“连环坑”为了安全起见我们决定编写 Python 脚本在数据库层面进行排查。但在执行过程中我们遭遇了老版本 CVAT 复杂的底层模型关联带来的挑战。第一坑字段名变更初次尝试使用shapes__isnullTrue查询时Django 抛出FieldError。通过排查发现老版本 CVAT 中轨迹的形状字段名为trackedshape而非新版本的shapes。第二坑Task 与 Job 的关联断裂修正字段名后尝试使用LabeledTrack.objects.filter(tasktask)查询再次报错。查阅字段列表后发现老版本的LabeledTrack并不直接关联Task而是关联Job。第三坑Job 与 Task 的中间层当我们试图通过task.job_set或Job.objects.filter(task_idtask.id)获取 Job 时依然报错。最终通过 Django 的报错提示我们彻底摸清了老版本 CVAT 的真实数据拓扑结构Task - Segment - Job - LabeledTrack原来 Task 和 Job 之间还隔着一个 Segment 层这是老版本 CVAT 特有的架构设计。四、解决问题精准定位与数据清理在理清了Task - Segment - Job - LabeledTrack的关联链路后我们编写了终极排查脚本成功绕过了所有反向关联属性的坑# 核心排查逻辑segment_idsSegment.objects.filter(task_idtask.id).values_list(id,flatTrue)job_idsJob.objects.filter(segment_id__insegment_ids).values_list(id,flatTrue)tracksLabeledTrack.objects.filter(job_id__injob_ids)fortrackintracks:ifnottrack.trackedshape_set.exists():# 找到空壳轨迹print(f轨迹 ID:{track.id}| 帧数:{track.frame})脚本运行后成功在 Task 573 中精准揪出了导致崩溃的罪魁祸首轨迹 ID 36956位于 Frame 2003。在确认该轨迹确实为空壳后我们执行了精准的删除操作trackLabeledTrack.objects.get(id36956)track.delete()删除后重新触发导出任务Task 573 的数据顺利导出完成问题彻底解决。# Python 脚本直接在 Task 573 中找出这些“空壳轨迹”并把它们删掉。# 显式导入了 Segment 模型完全顺应了老版本 CVAT Task - Segment - Job - LabeledTrack 的真实数据库结构避开了所有可能报错的反向关联属性。python-c import os import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, cvat.settings.production) django.setup() from cvat.apps.engine.models import Task, Segment, Job, LabeledTrack # 1. 获取 Task 573 task Task.objects.get(id573) # 2. 通过 Segment 找到该任务下所有的 Job ID segment_ids Segment.objects.filter(task_idtask.id).values_list(id, flatTrue) job_ids Job.objects.filter(segment_id__insegment_ids).values_list(id, flatTrue) print(f正在排查并清理 Task 573 (包含 {len(job_ids)} 个 Job)...) # 3. 在这些 Job 中查找所有的轨迹 tracks LabeledTrack.objects.filter(job_id__injob_ids) count 0 for track in tracks: # 检查是否存在关联的形状 (trackedshape) if not track.trackedshape_set.exists(): print(f发现空壳轨迹正在删除 - 轨迹 ID: {track.id} | 帧数: {track.frame} | 标签 ID: {track.label_id}) track.delete() count 1 print(- * 50) if count 0: print(f清理完成共成功删除了 {count} 个导致崩溃的空轨迹。) print(现在你可以回到 CVAT 网页端重新尝试导出了) else: print(排查完成没有找到空轨迹。如果仍然无法导出可能是其他数据问题。) # 精准定位到了罪魁祸首——轨迹 ID: 36956 正在排查 Task 573 (包含 9 个 Job)... 诊断完成发现了 1 个有问题的空轨迹 -------------------------------------------------- 轨迹 ID: 36956 | 所在帧数 (Frame): 2003 | 标签 ID: 84 -------------------------------------------------- # 直接把这个导致崩溃的“空壳轨迹”从数据库中彻底删除。 python -c importosimportdjango os.environ.setdefault(DJANGO_SETTINGS_MODULE,cvat.settings.production)django.setup()from cvat.apps.engine.modelsimportLabeledTrack# 精准定位并删除 ID 为 36956 的空轨迹track_id_to_delete36956try: trackLabeledTrack.objects.get(idtrack_id_to_delete)print(f正在删除问题轨迹 - ID: {track.id} | 帧数: {track.frame} | 标签 ID: {track.label_id})track.delete()print(删除成功现在你可以回到 CVAT 网页端重新尝试导出了。)except LabeledTrack.DoesNotExist: print(f未找到 ID 为 {track_id_to_delete} 的轨迹可能已被删除。)正在删除问题轨迹 -ID:36956|帧数:2003|标签 ID:84删除成功现在你可以回到 CVAT 网页端重新尝试导出了。五、经验总结与反思这次排查过程不仅成功修复了导出崩溃的问题还为我们留下了宝贵的经验老版本架构的复杂性在维护老旧系统时不能想当然地套用新版本的数据模型。Django 的FieldError报错信息Choices are: ...是探索未知数据库结构的最佳指南针。脏数据的产生与防御在复杂的标注工具中级联删除Cascade Delete如果不彻底极易产生孤儿数据。在编写数据清理脚本时应遵循“先诊断打印后确认删除”的安全原则。版本确认问题解决后可通过pip show cvat | grep Version确认当前环境的具体版本以便在未来的运维中建立版本档案。通过这次实战我们不仅清理了数据库中的“毒瘤”也彻底摸清了该老版本 CVAT 的底层数据流转逻辑为后续的系统维护和升级打下了坚实基础。
返回列表