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:
@@ -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
|
||||
|
||||
|
||||
@@ -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_annotations(cuboid + 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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user