feat: Add support for ADAS project in labeling and export processes

- Implemented class name loading for ADAS campaigns in `_class_names_for_campaign`.
- Enhanced annotation parsing to support cuboid and 2D rectangle labels in `_parse_ls_annotations`.
- Updated catalog building to include ADAS packs and batches in `build_catalog_signature`.
- Added ADAS cuboid promotion logic in `adas_cuboid.py`, supporting both quaternion_json and YOLO HBB formats.
- Introduced validation for ADAS batches to check for YOLO HBB labels in `adas_cuboid.py`.
- Modified delivery scanning to include ADAS project deliveries in `scan.py`.
- Extended job execution to handle ADAS exports for YOLO format in `runner.py`.
- Updated labeling export logic to accommodate ADAS project in `export_cuboid_batch.py`.
- Enhanced format conversion to support rectangle labels in `format_converter.py`.
- Improved CVAT task handling in `service.py` to ensure task ID updates on each labeling job.
- Updated web API endpoints to reflect ADAS project changes in `hsap-api.ts`.
- Added ADAS catalog types and UI components for displaying ADAS packs and batches in `dmsCatalog.ts`, `CatalogPage.tsx`, and `ExportPage.tsx`.
- Adjusted workflow registry to align with new ADAS project structure.
This commit is contained in:
2026-07-16 16:03:04 +08:00
parent be8a885b07
commit 1bde0fe430
23 changed files with 472 additions and 83 deletions

View File

@@ -119,8 +119,8 @@ def on_labeling_export_job_succeeded(job: dict) -> None:
except Exception:
return
project = camp.project or "dms"
if _batch_has_export_labels(project, batch_dir):
_advance_campaign_stage(str(cid), "returned")
# job 已成功,无论是否有 CLI 导出产物,均进入 returned 阶段
_advance_campaign_stage(str(cid), "returned")
if project == "adas" and _batch_has_calib(batch_dir):
from as_platform.jobs.queue import enqueue_job

View File

@@ -102,8 +102,45 @@ def _resolve_image_for_ann(data: dict[str, Any], batch_dir: Path, task_id: str)
return None
def _rect_item_to_detection(item: dict[str, Any], class_map: dict[str, int]) -> dict[str, Any] | None:
"""2D rectanglelabels 条目 → quaternion_json detection无 3D 信息,仅 bbox2d"""
value = item.get("value") or {}
w_pct = float(value.get("width") or 0)
h_pct = float(value.get("height") or 0)
if w_pct <= 0 or h_pct <= 0:
return None
labels = value.get("rectanglelabels") or []
label = str(labels[0]) if labels else "unknown"
class_id = class_map.get(label)
if class_id is None:
for name, cid in class_map.items():
if name.lower() == label.lower():
class_id = cid
break
if class_id is None:
return None
img_w = int(item.get("original_width") or 1920)
img_h = int(item.get("original_height") or 1080)
x_pct = float(value.get("x") or 0)
y_pct = float(value.get("y") or 0)
x1 = int(x_pct / 100.0 * img_w)
y1 = int(y_pct / 100.0 * img_h)
x2 = int((x_pct + w_pct) / 100.0 * img_w)
y2 = int((y_pct + h_pct) / 100.0 * img_h)
return {
"class_id": class_id,
"class_name": label,
"score": 1.0,
"box2d_xyxy": [x1, y1, x2, y2],
"fit_ok": False,
"source": "2d_rect",
}
def export_batch(batch_dir: Path) -> dict[str, Any]:
"""导出 cuboid ls_annotations → quaternion_json。"""
"""导出 ls_annotationscuboid + 2D rectangle→ quaternion_json。"""
batch_dir = batch_dir.resolve()
class_map = _load_cuboid_class_map()
calib_path, K, calib_size = _find_calib(batch_dir)
@@ -124,7 +161,9 @@ def export_batch(batch_dir: Path) -> dict[str, Any]:
continue
regions = _extract_result_regions(data)
cuboids = [r for r in regions if r.get("type") == "cuboid"]
if not cuboids:
rectangles = [r for r in regions if r.get("type") == "rectanglelabels"]
if not cuboids and not rectangles:
skipped_empty += 1
continue
@@ -134,16 +173,24 @@ def export_batch(batch_dir: Path) -> dict[str, Any]:
continue
detections: list[dict[str, Any]] = []
# 处理 cuboid 标注
for item in cuboids:
det = cuboid_item_to_detection(item, class_map, K=K)
if det:
detections.append(det)
# 处理 2D 矩形标注 → quaternion_json detection
for item in rectangles:
det = _rect_item_to_detection(item, class_map)
if det:
detections.append(det)
if not detections:
skipped_empty += 1
continue
img_w = int((cuboids[0].get("original_width") or (calib_size or [1920, 1080])[0]))
img_h = int((cuboids[0].get("original_height") or (calib_size or [1920, 1080])[1]))
# 获取图片尺寸:优先 cuboid其次 rectangle
first_region = cuboids[0] if cuboids else rectangles[0]
img_w = int((first_region.get("original_width") or (calib_size or [1920, 1080])[0]))
img_h = int((first_region.get("original_height") or (calib_size or [1920, 1080])[1]))
payload: dict[str, Any] = {
"image": str(image_path),

View File

@@ -238,10 +238,14 @@ def cvat_shapes_to_export_regions(
}
if stype == "rectangle":
xtl = float(shape.get("xtl", 0))
ytl = float(shape.get("ytl", 0))
xbr = float(shape.get("xbr", 0))
ybr = float(shape.get("ybr", 0))
pts = shape.get("points") or []
if len(pts) >= 4:
xtl, ytl, xbr, ybr = float(pts[0]), float(pts[1]), float(pts[2]), float(pts[3])
else:
xtl = float(shape.get("xtl", 0))
ytl = float(shape.get("ytl", 0))
xbr = float(shape.get("xbr", 0))
ybr = float(shape.get("ybr", 0))
regions.append({
**base,
"type": "rectanglelabels",

View File

@@ -186,9 +186,9 @@ def open_campaign(
else:
camp.status = "in_progress"
camp.updated_at = now
if cvat_task_id and not camp.cvat_task_id:
camp.cvat_task_id = cvat_task_id
camp.cvat_job_url = cvat_job_url
# 每次开标都创建新 CVAT Task始终更新 task_id
camp.cvat_task_id = cvat_task_id
camp.cvat_job_url = cvat_job_url
if ann_types and not camp.annotation_types:
camp.annotation_types = ann_types
db.flush()
@@ -531,6 +531,7 @@ def _cvat_upload_thread(cvat_task_id: int, image_paths: list, campaign_id: str |
with session_scope() as db:
camp = db.get(LabelingCampaign, campaign_id)
if camp and camp.cvat_job_url is None:
camp.cvat_task_id = cvat_task_id
camp.cvat_job_url = f"_UPLOAD_FAILED_: {e}"
camp.status = "upload_failed"
updated = True
@@ -542,12 +543,13 @@ def _cvat_upload_thread(cvat_task_id: int, image_paths: list, campaign_id: str |
try:
with session_scope() as db:
camp = db.get(LabelingCampaign, campaign_id)
# 上传成功后,尝试回填 job_url
# 上传成功后,回填 job_url 和 task_id
if camp and camp.cvat_job_url is None:
from as_platform.labeling.cvat_client import get_cvat_client
cvat = get_cvat_client()
task = cvat.get_task(cvat_task_id)
if task.job_url:
camp.cvat_task_id = cvat_task_id
camp.cvat_job_url = task.job_url
camp.status = "in_progress"
updated = True
@@ -657,7 +659,11 @@ def _build_class_map(camp, reg: dict | None) -> dict[str, int]:
def get_cvat_status(campaign_id: str) -> dict[str, Any]:
"""查询 CVAT 侧 Task 状态,包含上传进度信息。"""
"""查询 CVAT 侧 Task 状态,包含上传进度信息。
自动恢复:当检测到 cvat_job_url 为 None 且 CVAT Task 无数据时,
说明上传线程已丢失(如容器重启),自动重新触发上传。
"""
with session_scope() as db:
camp = db.get(LabelingCampaign, campaign_id)
if not camp:
@@ -690,12 +696,31 @@ def get_cvat_status(campaign_id: str) -> dict[str, Any]:
images = _iter_batch_images(batch_dir)
image_count = len(images)
except Exception:
images = []
pass
# 自动恢复cvat_job_url 为空且 CVAT Task 无 Job → 上传线程已丢失
resolved_url = task.job_url or camp.cvat_job_url
if not resolved_url and image_count > 0:
# CVAT Task 存在但无数据,重新触发上传
import threading
_needs_recovery = True
try:
r = cvat._session.get(f"{cvat._api}/tasks/{camp.cvat_task_id}/data/meta")
_needs_recovery = r.status_code >= 400 # 400 = 未上传数据
except Exception:
pass
if _needs_recovery:
print(f"[CVAT] 检测到断线任务,自动恢复上传 task={camp.cvat_task_id} campaign={campaign_id}")
uploader = _cvat_upload_thread(camp.cvat_task_id, images, campaign_id=campaign_id)
threading.Thread(target=uploader, daemon=True).start()
camp.cvat_job_url = None # 重置,等待线程回填
return {
"cvat_available": True,
"campaign_id": campaign_id,
"cvat_task_id": camp.cvat_task_id,
"cvat_job_url": task.job_url or camp.cvat_job_url,
"cvat_job_url": resolved_url,
"cvat_status": task.status,
"image_count": image_count,
}