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:
72
scripts/viser_auv.py
Normal file
72
scripts/viser_auv.py
Normal 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()
|
||||
Reference in New Issue
Block a user