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:
Ubuntu
2026-05-11 11:05:37 +00:00
parent 1a4fffd2c1
commit 82f71fcc96
7 changed files with 625 additions and 0 deletions

72
scripts/viser_auv.py Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Open viser viewer with all PLYs from one AUV.
Usage:
viser_auv.py --ply-dir /path/to/auv/ply --port 9210
"""
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
import numpy as np
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--ply-dir", required=True)
ap.add_argument("--port", type=int, default=9210)
ap.add_argument("--point-size", type=float, default=0.01)
ap.add_argument("--max-points-per-ply", type=int, default=1_500_000)
args = ap.parse_args()
try:
import open3d as o3d
import viser
except ImportError as e:
sys.exit(f"missing dep: {e}")
ply_dir = Path(args.ply_dir)
plys = sorted(ply_dir.glob("**/*.ply"))
print(f"Found {len(plys)} PLY files in {ply_dir}", flush=True)
if not plys:
sys.exit("no PLY found")
server = viser.ViserServer(host="0.0.0.0", port=args.port)
palette = [
(1.0, 0.30, 0.30), (0.30, 1.0, 0.30), (0.30, 0.55, 1.0),
(1.0, 0.85, 0.20), (1.0, 0.30, 1.0), (0.30, 1.0, 1.0),
(1.0, 0.55, 0.20), (0.55, 0.30, 1.0),
]
for i, p in enumerate(plys):
pcd = o3d.io.read_point_cloud(str(p))
pts = np.asarray(pcd.points, dtype=np.float32)
if len(pts) == 0:
print(f" ! {p.name}: empty", flush=True)
continue
if pcd.has_colors():
cols = np.asarray(pcd.colors, dtype=np.float32)
else:
cols = np.tile(palette[i % len(palette)], (len(pts), 1)).astype(np.float32)
if len(pts) > args.max_points_per_ply:
idx = np.random.choice(len(pts), args.max_points_per_ply, replace=False)
pts = pts[idx]
cols = cols[idx]
# viser wants uint8 colors
cols_u8 = (cols * 255).clip(0, 255).astype(np.uint8)
name = f"/{p.parent.name}_{p.stem}"
server.scene.add_point_cloud(
name=name, points=pts, colors=cols_u8, point_size=args.point_size
)
print(f" + {p.name}: {len(pts):,} pts", flush=True)
print(f"Viser ready on port {args.port}", flush=True)
while True:
time.sleep(60)
if __name__ == "__main__":
main()