Download code/inference_example.py from tailot/hlt-geometric-vision: direct link, hf CLI and curl.
- Browser
- Download file 5.08 kB
-
https://huggingface.co/tailot/hlt-geometric-vision/resolve/main/code/inference_example.py
- Command line
-
hf download hf://tailot/hlt-geometric-vision/code/inference_example.py
-
curl -L -o inference_example.py https://huggingface.co/tailot/hlt-geometric-vision/resolve/main/code/inference_example.py
5.08 kB
| """Inference with the hlt-geometric-vision release (ONNXRuntime + OpenCV, no PyTorch). | |
| Path A: the vertices come out of the model (1 polygon, training domain = clean rectangles). | |
| Path B: model edge_map + geometric extractor -> N rectangles on real photographs. | |
| python inference_example.py --model ../model.onnx --image photo.jpg [--nested] | |
| The extractor is a copy of hlt/postproc/quads.py from the source repository (numpy + OpenCV, | |
| no weights): see documentation.md section 6 for the specification, section 8 for the parameters. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| import onnxruntime as ort | |
| from hlt_postproc_quads import QuadConfig, extract_quads, hybrid_edge_map | |
| def preprocess(bgr, size=512): | |
| """Letterbox to a square, aspect preserved, with a REPLICATED border (a constant gray fill | |
| creates artificial lines in the edge map). Also returns the real-image-area mask.""" | |
| h0, w0 = bgr.shape[:2] | |
| scale = float(size) / float(max(h0, w0)) | |
| nw, nh = max(1, round(w0 * scale)), max(1, round(h0 * scale)) | |
| small = cv2.resize(bgr, (nw, nh), interpolation=cv2.INTER_AREA) | |
| ox, oy = (size - nw) // 2, (size - nh) // 2 | |
| disp = cv2.copyMakeBorder(small, oy, size - nh - oy, ox, size - nw - ox, cv2.BORDER_REPLICATE) | |
| valid = np.zeros((size, size), bool) | |
| valid[oy:oy + nh, ox:ox + nw] = True | |
| x = cv2.cvtColor(disp, cv2.COLOR_BGR2RGB).astype(np.float32).transpose(2, 0, 1) / 255.0 | |
| return np.ascontiguousarray(x)[None], disp, valid, scale, ox, oy | |
| def to_original(corners, scale, ox, oy): | |
| """512 frame -> pixels of the original image. The half-pixel correction is not cosmetic: on | |
| large photographs the mapping error reaches ~1.6 px.""" | |
| return (corners + 0.5 - np.array([ox, oy], np.float64)) / scale - 0.5 | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--model", default="model.onnx") | |
| ap.add_argument("--image", required=True) | |
| ap.add_argument("--nested", action="store_true", help="also keep nested rectangles") | |
| ap.add_argument("--only-net", action="store_true", help="path A only (model vertices)") | |
| ap.add_argument("--no-canny", action="store_true", help="network edge map only (no Canny)") | |
| ap.add_argument("--threads", type=int, default=0, help="ONNXRuntime threads (0 = default)") | |
| ap.add_argument("--max-quads", type=int, default=64, | |
| help="cap on kept rectangles (QuadConfig default: 64; the release examples use 12)") | |
| args = ap.parse_args() | |
| so = ort.SessionOptions() | |
| if args.threads: | |
| so.intra_op_num_threads = args.threads | |
| sess = ort.InferenceSession(args.model, sess_options=so, providers=["CPUExecutionProvider"]) | |
| in_name = sess.get_inputs()[0].name | |
| out_names = [o.name for o in sess.get_outputs()] | |
| img = cv2.imread(args.image, cv2.IMREAD_COLOR) | |
| if img is None: | |
| raise SystemExit("image not decoded: " + args.image) | |
| x, disp, valid, scale, ox, oy = preprocess(img) | |
| out = dict(zip(out_names, sess.run(out_names, {in_name: x}))) | |
| # ---- path A: a single polygon, strong on the training domain | |
| mask = out["vertex_mask"][0, 0] > 0.5 | |
| net_corners = to_original(out["vertices"][0, 0][mask], scale, ox, oy) | |
| result = {"image": args.image, "size_wh": [img.shape[1], img.shape[0]], | |
| "path_A_model_polygon": np.round(net_corners, 2).tolist(), | |
| "path_A_active_vertices": int(mask.sum()), | |
| "note": "the model confidence is not calibrated: do not use it as a threshold"} | |
| if not args.only_net: | |
| # ---- path B: edge_map + geometry (rectangles on real photographs) | |
| # The model edge map lives in the 512 letterbox frame: the helper Canny must be computed on | |
| # THAT frame, not on the original image (different areas and different scales). | |
| g512 = cv2.cvtColor(disp, cv2.COLOR_BGR2GRAY) | |
| er = cv2.erode(valid.astype(np.uint8), np.ones((5, 5), np.uint8)).astype(bool) | |
| if args.no_canny: | |
| edge = out["edge_map"][0, 0] * er | |
| cfg = QuadConfig(nested=args.nested, max_quads=args.max_quads) | |
| else: | |
| edge = hybrid_edge_map(out["edge_map"][0, 0], g512, valid=er) | |
| cfg = QuadConfig.hybrid(nested=args.nested, max_quads=args.max_quads) | |
| quads = extract_quads(edge, cfg, gray=g512.astype(np.float32)) | |
| result["path_B_rectangles"] = [ | |
| {"corners_xy": to_original(q["corners"], scale, ox, oy).round(2).tolist(), | |
| "score": round(float(q["score"]), 3), | |
| "side_edge_coverage": [round(float(v), 2) for v in q["edge_cov"]]} | |
| for q in quads] | |
| result["path_B_count"] = len(quads) | |
| print(json.dumps(result, ensure_ascii=False, indent=2)) | |
| Path(args.image).with_suffix(".quads.json").write_text( | |
| json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") | |
| if __name__ == "__main__": | |
| main() | |