superpoint-p150 / code /models /server /smoke_test.py
changh95's picture
Python API (2026-10-04): pip install -e code/, from_pretrained() + model(...), Python-first quickstart
6ffd3f8 verified
Raw History Blame Contribute Delete
9.34 kB
# SPDX-License-Identifier: Apache-2.0
"""Smoke test for the SuperPoint tt-dit-server: one request, sanity checks, PASS/FAIL.
python code/models/server/smoke_test.py --url http://127.0.0.1:20000
Default input: ``sample_data/house_in_field_1080p.jpg`` (the port's natural-image
validation frame, 1600x900) resolved relative to this file (code/sample_data).
Only the standard library + numpy are needed (numpy decodes the descriptor NPZ).
Checks (the README reports ~500 strong keypoints on this frame at top-500;
we ask for 1024 and require a few hundred):
* /health is ok, /info answers, /v1/models lists the weights repo
* /predict returns 200 with num_keypoints in [200, max_keypoints]
* every keypoint lies inside the ORIGINAL image, scores in (threshold, 1]
and sorted-consistent with top-k, descriptors (N, 256) float16 finite and
unit-norm (|1 - ||d||| < 0.05), timing_ms present
* a second call with return_descriptors=false / max_keypoints=100 obeys both
* POST /predict_raw (the file bytes) and POST /predict_plane (the R plane, needs Pillow) return the
same response as /predict apart from timing_ms (skip with --no-binary-routes)
Exit code 0 on PASS, 1 on FAIL.
"""
from __future__ import annotations
import argparse
import base64
import io
import json
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
DEFAULT_IMAGE = Path(__file__).resolve().parents[2] / "sample_data" / "house_in_field_1080p.jpg"
def _get(url: str, timeout: float = 30.0):
with urllib.request.urlopen(url, timeout=timeout) as r:
return r.status, json.loads(r.read().decode("utf-8"))
def _post(url: str, payload: dict, timeout: float = 300.0):
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, json.loads(r.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", "replace")
return e.code, {"detail": body}
def _post_bytes(url: str, body: bytes, timeout: float = 300.0):
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/octet-stream"}, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, json.loads(r.read().decode("utf-8"))
except urllib.error.HTTPError as e:
return e.code, {"detail": e.read().decode("utf-8", "replace")}
def _fail(msg: str) -> int:
print(f"FAIL {msg}")
return 1
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--url", default="http://127.0.0.1:20000", help="server base URL (the port `serve` printed)")
ap.add_argument("--image", type=Path, default=DEFAULT_IMAGE)
ap.add_argument("--max-keypoints", type=int, default=1024)
ap.add_argument("--min-keypoints", type=int, default=200, help="lower bound for PASS on the default image")
ap.add_argument("--threshold", type=float, default=0.005)
ap.add_argument("--timeout", type=float, default=300.0)
ap.add_argument("--no-binary-routes", action="store_true", help="skip /predict_raw and /predict_plane")
ap.add_argument("--save-json", type=Path, default=None, help="write the first /predict response here")
args = ap.parse_args()
base = args.url.rstrip("/")
if not args.image.is_file():
return _fail(f"input image not found: {args.image}")
raw = args.image.read_bytes()
if raw[:4] not in (b"\xff\xd8\xff\xe0", b"\xff\xd8\xff\xe1", b"\x89PNG") and raw[:3] != b"\xff\xd8\xff":
return _fail(f"{args.image} is not a PNG/JPEG (git-lfs pointer? run `git lfs pull`)")
b64 = base64.b64encode(raw).decode("ascii")
try:
st, health = _get(f"{base}/health")
except Exception as e:
return _fail(f"GET /health failed: {e}")
if st != 200 or health.get("status") != "ok":
return _fail(f"/health not ok: {health}")
st, info = _get(f"{base}/info")
if st != 200 or info.get("model") != "superpoint-p150":
return _fail(f"/info unexpected: {info}")
st, models = _get(f"{base}/v1/models")
if st != 200 or not models.get("data"):
return _fail(f"/v1/models unexpected: {models}")
weights_repo = models["data"][0]["id"]
payload = {
"image": b64,
"max_keypoints": args.max_keypoints,
"keypoint_threshold": args.threshold,
"nms_radius": 4,
"return_descriptors": True,
}
t0 = time.perf_counter()
st, resp = _post(f"{base}/predict", payload, timeout=args.timeout)
wall_ms = (time.perf_counter() - t0) * 1000.0
if st != 200:
return _fail(f"/predict HTTP {st}: {str(resp)[:300]}")
if args.save_json:
args.save_json.write_text(json.dumps(resp))
n = resp.get("num_keypoints")
kps = resp.get("keypoints") or []
scores = resp.get("scores") or []
if not isinstance(n, int) or n != len(kps) or n != len(scores):
return _fail(f"inconsistent counts: num_keypoints={n} keypoints={len(kps)} scores={len(scores)}")
if n < args.min_keypoints or n > args.max_keypoints:
return _fail(f"num_keypoints={n} outside [{args.min_keypoints}, {args.max_keypoints}]")
osz = resp.get("original_size") or {}
W, H = osz.get("width"), osz.get("height")
if not W or not H:
return _fail(f"original_size missing: {osz}")
for x, y in kps:
if not (0.0 <= x < W and 0.0 <= y < H):
return _fail(f"keypoint ({x}, {y}) outside the {W}x{H} image")
if min(scores) <= args.threshold or max(scores) > 1.0 + 1e-6:
return _fail(f"scores out of range: min={min(scores)} max={max(scores)} threshold={args.threshold}")
if "timing_ms" not in resp or "device_forward" not in resp["timing_ms"]:
return _fail("timing_ms.device_forward missing")
d = resp.get("descriptors")
if not d or d.get("format") != "npz" or d.get("shape") != [n, 256]:
return _fail(f"descriptors missing or wrong shape: {None if not d else d.get('shape')}")
try:
import numpy as np
npz = np.load(io.BytesIO(base64.b64decode(d["data"])))
desc = npz[d.get("key", "descriptors")]
if desc.shape != (n, 256) or desc.dtype != np.float16:
return _fail(f"descriptor array is {desc.shape} {desc.dtype}, expected ({n}, 256) float16")
desc32 = desc.astype(np.float32)
if not np.isfinite(desc32).all():
return _fail("descriptors contain non-finite values")
norms = np.linalg.norm(desc32, axis=1)
if np.abs(1.0 - norms).max() > 0.05:
return _fail(f"descriptors not unit-norm: |1-norm| max {np.abs(1.0 - norms).max():.3f}")
norm_dev = float(np.abs(1.0 - norms).max())
except ImportError:
norm_dev = float("nan") # numpy unavailable: shape/format checks above still ran
# Second call: descriptors off + small top-k must be honoured.
st2, resp2 = _post(f"{base}/predict", {**payload, "return_descriptors": False, "max_keypoints": 100}, timeout=args.timeout)
if st2 != 200:
return _fail(f"second /predict HTTP {st2}: {str(resp2)[:200]}")
if resp2.get("num_keypoints") != 100 or "descriptors" in resp2:
return _fail(f"top-k/return_descriptors not honoured: n={resp2.get('num_keypoints')} desc={'descriptors' in resp2}")
binary = "skipped"
if not args.no_binary_routes:
from urllib.parse import urlencode
q = {k: v for k, v in payload.items() if k != "image"}
q["return_descriptors"] = "true"
ref = {k: v for k, v in resp.items() if k != "timing_ms"}
st3, resp3 = _post_bytes(f"{base}/predict_raw?{urlencode(q)}", raw, timeout=args.timeout)
if st3 != 200 or {k: v for k, v in resp3.items() if k != "timing_ms"} != ref:
return _fail(f"/predict_raw HTTP {st3}, response differs from /predict: {str(resp3)[:200]}")
binary = "raw=ok"
try:
from PIL import Image
except ImportError:
binary += " plane=skipped(no Pillow)"
else:
im = Image.open(io.BytesIO(raw))
im = im if im.mode == "RGB" else im.convert("RGB")
plane = im.getchannel(0).tobytes()
st4, resp4 = _post_bytes(f"{base}/predict_plane?{urlencode({**q, 'height': im.height, 'width': im.width})}",
plane, timeout=args.timeout)
if st4 != 200 or {k: v for k, v in resp4.items() if k != "timing_ms"} != ref:
return _fail(f"/predict_plane HTTP {st4}, response differs from /predict: {str(resp4)[:200]}")
binary += " plane=ok"
tm = resp["timing_ms"]
print(
f"PASS superpoint-p150: {n} keypoints on {args.image.name} ({W}x{H}), "
f"score max={max(scores):.4f} min={min(scores):.4f}, desc (N,256) f16 |1-norm|max={norm_dev:.3f}, "
f"device_forward={tm.get('device_forward')} ms postprocess={tm.get('postprocess')} ms "
f"server_total={tm.get('total')} ms wall={wall_ms:.0f} ms, binary routes {binary}, weights={weights_repo}"
)
return 0
if __name__ == "__main__":
sys.exit(main())