feat: frame QC scoring + viser per-AUV button
Stage 04 frame extract:
- New lib_frame_qc.py: per-frame Laplacian/contrast/blue-dominance scoring
- Classes: bottom_visible / water_no_bottom / turbid_water / out_of_water
- Sample 1/5 frames after extraction, write qc.json per segment
- Record metrics (frames_total, frames_bottom_visible, bottom_visible_pct)
- Mark job degraded when bottom_visible_pct < 50%
Per-AUV viser view:
- scripts/viser_auv.py loads all PLYs of an AUV, color per file
- POST /pipeline/missions/{id}/auvs/{auv}/view rsyncs ply -> worker
- launches viser on hashed port 9300+, returns URL
- _pipeline.html exposes AUV list, JS handler opens viser tab
This commit is contained in:
85
app/main.py
85
app/main.py
@@ -320,12 +320,17 @@ async def partial_pipeline(request: Request):
|
||||
counts = {}
|
||||
for j in jobs:
|
||||
counts[j["status"]] = counts.get(j["status"], 0) + 1
|
||||
auvs: list[str] = []
|
||||
for j in jobs:
|
||||
if j["auv_id"] and j["auv_id"] not in auvs:
|
||||
auvs.append(j["auv_id"])
|
||||
data["missions"].append({
|
||||
"id": m["id"],
|
||||
"name": m["name"],
|
||||
"status": m["status"],
|
||||
"jobs": [dict(j) for j in jobs],
|
||||
"counts": counts,
|
||||
"auvs": auvs,
|
||||
})
|
||||
except Exception as e:
|
||||
data["error"] = str(e)[:200]
|
||||
@@ -571,6 +576,86 @@ async def live_job(job_id: int):
|
||||
return {"url": row["viser_url"]}
|
||||
|
||||
|
||||
VISER_AUV_BASE = 9300
|
||||
PIPELINE_DATA_BASE = Path(os.environ.get("COSMA_PIPELINE_DATA", "/cosma-pipeline/data"))
|
||||
|
||||
|
||||
@app.post("/pipeline/missions/{mission_id}/auvs/{auv_id}/view")
|
||||
async def view_auv(mission_id: int, auv_id: str):
|
||||
"""Launch viser showing all PLYs for one AUV from a mission."""
|
||||
if not PIPELINE_DB.exists():
|
||||
raise HTTPException(404, "state.db introuvable")
|
||||
import hashlib
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
shutil.copy2(str(PIPELINE_DB), tmp_path)
|
||||
try:
|
||||
with sqlite3.connect(tmp_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
m = conn.execute(
|
||||
"SELECT name FROM missions WHERE id=?", (mission_id,)
|
||||
).fetchone()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
if not m:
|
||||
raise HTTPException(404, "mission introuvable")
|
||||
mission_name = m["name"]
|
||||
ply_dir_local = PIPELINE_DATA_BASE / mission_name / "ply" / auv_id
|
||||
if not ply_dir_local.exists():
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": f"PLY dir {ply_dir_local} pas encore (stitch pas done)"},
|
||||
status_code=409,
|
||||
)
|
||||
|
||||
h = int(hashlib.md5(f"{mission_id}-{auv_id}".encode()).hexdigest()[:6], 16)
|
||||
port = VISER_AUV_BASE + (h % 100)
|
||||
worker = WORKERS[1] if len(WORKERS) > 1 else WORKERS[0]
|
||||
alias = worker["ssh_alias"]
|
||||
host = worker["host"]
|
||||
worker_dir = f"/tmp/cosma-viser-auv/{mission_name}/{auv_id}"
|
||||
|
||||
rsync = await asyncio.create_subprocess_exec(
|
||||
"rsync", "-az", "--delete",
|
||||
"-e", "ssh -o BatchMode=yes -o StrictHostKeyChecking=no",
|
||||
f"{ply_dir_local}/", f"{alias}:{worker_dir}/",
|
||||
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, err = await rsync.communicate()
|
||||
if rsync.returncode != 0:
|
||||
raise HTTPException(500, f"rsync failed: {err.decode()[:200]}")
|
||||
|
||||
local_script = Path(__file__).parent.parent / "scripts" / "viser_auv.py"
|
||||
scp = await asyncio.create_subprocess_exec(
|
||||
"scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no",
|
||||
str(local_script), f"{alias}:/tmp/viser_auv.py",
|
||||
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, err = await scp.communicate()
|
||||
if scp.returncode != 0:
|
||||
raise HTTPException(500, f"scp failed: {err.decode()[:200]}")
|
||||
|
||||
venv_py = f"{worker.get('lingbot_path', '/root/ai-video/lingbot-map')}/.venv/bin/python"
|
||||
launch_cmd = (
|
||||
f"pkill -f 'viser_auv.py.*--port {port}' 2>/dev/null ; sleep 1 ; "
|
||||
f"setsid nohup {venv_py} /tmp/viser_auv.py --ply-dir {worker_dir} --port {port} "
|
||||
f"</dev/null >/tmp/viser_auv_{port}.log 2>&1 & disown ; sleep 0.3"
|
||||
)
|
||||
launch = await asyncio.create_subprocess_exec(
|
||||
"ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", alias, launch_cmd,
|
||||
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
await launch.communicate()
|
||||
await asyncio.sleep(4)
|
||||
|
||||
return {"ok": True, "url": f"http://{host}:{port}/", "auv_id": auv_id, "port": port}
|
||||
|
||||
|
||||
@app.post("/stitches/{stitch_id}/view")
|
||||
async def view_stitch(stitch_id: int):
|
||||
with closing(db()) as conn:
|
||||
|
||||
Reference in New Issue
Block a user