HLT Geometric Vision β€” rectangle extraction (ONNX)

A network plus explicit geometry for extracting rectangles and quadrilaterals (picture frames, windows, panels, screens) from an image: a UNet producing an edge map and a corner map, a differentiable Hough transform, polygon initialization and a neural geometric refiner that takes the vertices to sub-pixel accuracy.

This folder is the published release: the ONNX model, the full technical documentation and examples on public-domain photographs. Everything here is generated by scripts/build_hf_release.py in the source repository: every number below is read from the run's files or measured at build time on this artifact.

Read first: documentation.md β€” accepted inputs, output contract, preprocessing, and the algorithmic specification needed to reimplement inference in another language.

Honest TL;DR

What Measured value
Vertex error, test split (2000 unseen images, EMA weights) 0.384 px at 512Β² (median 0.346, p95 0.648)
Vertex error, val split (epoch 43, EMA weights) 0.813 px
Polygon IoU (test) 0.9719
Vertex recall / precision (test) 0.9762 / 1.0000
Vertices within 1 px / within 3 px (test) 1.0000 / 1.0000
PyTorch ↔ ONNXRuntime numerical parity 0.00198 px on the vertices
ONNX latency on CPU (4 threads), batch 1 137.9 ms median (155.9 ms p95)

⚠️ The training domain is synthetic (procedurally rendered convex polygons). On real photographs the edge map transfers, the corner map does not: for real-world cases use path B (network edge map + weightless geometric extractor), specified in documentation.md §6 and shown in the examples. See Limitations.

ONNX artifact

Field Value
File model.onnx (58.5 MB, single file, weights inlined)
SHA-256 a351dc1ff555a9e8e6fb2dacffadb2b92f91aa7c7e16fb06eed6aceb949e2e89
Opset 18 (effective: default:18)
Input input_image β†’ ('batch', 3, 512, 512) float32, RGB, [0,1], contiguous NCHW
Resolution fixed 512Γ—512 (the Hough accumulator is sized on H,W)
Batch axis 0 dynamic (exercised at B=2 during the build: 0.00000 px of difference between B=1 and B=2)
Outputs vertices, vertex_mask, confidence, coarse_vertices, coarse_mask, edge_map, keypoint_map
Export verification verified=true, reference input = clean drawn quadrilateral 512x512, batch 2

Maximum deviation per output (PyTorch vs ONNXRuntime, same input): vertices 0.021978, vertex_mask 0.000000, confidence 1.55e-06, coarse_vertices 0.021978, coarse_mask 0.000000, edge_map 7.75e-06, keypoint_map 2.80e-06

Full binding details, which masks to apply, and the pitfalls (noise inputs faking differences, the uncalibrated confidence): documentation.md Β§2–§5.

Inference in 20 lines

import cv2, numpy as np, onnxruntime as ort
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
img = cv2.imread("photo.jpg")
s = 512 / max(img.shape[:2])
small = cv2.resize(img, (round(img.shape[1] * s), round(img.shape[0] * s)),
                   interpolation=cv2.INTER_AREA)
ox, oy = (512 - small.shape[1]) // 2, (512 - small.shape[0]) // 2
frame = cv2.copyMakeBorder(small, oy, 512 - small.shape[0] - oy, ox, 512 - small.shape[1] - ox,
                           cv2.BORDER_REPLICATE)   # NOT a constant gray fill: it fakes lines in edge
x = np.ascontiguousarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB).astype(np.float32)
                         .transpose(2, 0, 1) / 255.0)[None]
names = [o.name for o in sess.get_outputs()]
out = dict(zip(names, sess.run(names, {sess.get_inputs()[0].name: x})))
mask = out["vertex_mask"][0, 0] > 0.5
corners = out["vertices"][0, 0][mask]              # (K,2) in 512-frame coordinates
# Rectangles on real photographs: edge_map + hlt_postproc_quads (see code/inference_example.py)

Ready-to-run script (path A and B, geometric extractor included): code/inference_example.py, which uses code/hlt_postproc_quads.py (the working copy of the extractor: numpy + OpenCV, no weights).

Examples on public-domain photographs

Two public-domain photographs from Wikimedia Commons (license re-read from the Commons metadata at build time). Each one runs path B: network edge map + geometric extractor. Scores, per-side coverage and coordinates in original pixels are in the JSON next to each image. In scenes dense in parallel lines (the window) the legitimate candidates run into the dozens β€” panes and muntins are rectangles β€” so max_quads: 12 keeps the highest-scoring ones, a value recorded in each JSON.

Image Content Rectangles Max score Side coverage of the best
example_1_picture_frame Portrait miniature in a gilded rectangular frame, photographed frontally: two nested rectangles, the outer edge of the velvet border and the edge of the painted panel (a scale bar sits in the lower right). 2 1.006 1.00, 1.00, 1.00, 1.00
example_2_window Double-hung window with white casing: outer casing, both sashes and the individual panes come out as nested rectangles. 12 1.003 1.00, 1.00, 1.00, 1.00

Portrait miniature in a gilded rectangular frame, photographed frontally: two nested rectangles, the outer edge of the velvet border and the edge of the painted panel (a scale bar sits in the lower right).

Image: example_1_picture_frame

Extracted rectangles: example_1_picture_frame overlay

Debug mosaic (network edge map | hybrid map | 512 frame with the extracted sides): example_1_picture_frame debug

Source: File:19th-century portrait miniature of sitting woman painting by Valentine Manchon Duchesne - with scale.jpg β€” license Public domain, artist: Valentine Manchon Duchesne. Scores and per-side coordinates: examples/example_1_picture_frame.json.

Double-hung window with white casing: outer casing, both sashes and the individual panes come out as nested rectangles.

Image: example_2_window

Extracted rectangles: example_2_window overlay

Debug mosaic (network edge map | hybrid map | 512 frame with the extracted sides): example_2_window debug

Source: File:Double hung window.jpeg β€” license CC0, artist: Tucker T. Scores and per-side coordinates: examples/example_2_window.json.

What the model does (architecture)

  • UNet, ResNet-18 encoder (ImageNet weights, pretrained: true) + decoder, normalization set to group in the config but BatchNorm during training (see the pitfall in documentation.md), 96-channel features, two heatmaps out: edges and corners.
  • Differentiable Hough: 180 theta bins, rho step 2.0 px, 1024 sample points, 12 lines extracted with a theta window of 3 / rho window of 5.
  • Polygon initialization from keypoints (init_source: keypoint): the peaks of the corner map define the initial vertices; merge radius 6.0 px, at most 1 polygon / 8 vertices.
  • Neural geometric refiner: MLP 3Γ—256, max offset 12.0 px per vertex, anchored to the local geometry of the edge map.
  • Losses: BCE + Dice on the two heatmaps (weights 1.00/1.00, positive weights 3.0/10.0), Smooth-L1 on the matched vertices (vertex_beta: 4.0), Chamfer (weight 0.50), convexity (0.20), parallelism/orthogonality (0.10), offset regularization (0.010). Curriculum on object scale (final scale 0.10).

Training statistics

Field Value
Run runs/g3_keypoint (the winning variant of the four G3 ablations)
Dataset procedurally rendered synthetic: 20000 train / 2000 val / 2000 test, seed 1337
Objects per sample 1 convex polygon, 4–6 vertices, relative radius 0.16–0.38, center margin 0.24
Supervision heatmaps rendered with supersampling 2, edge thickness 1.0 px, corner sigma 3.0 px
Augmenting geometric on (geometric_aug: true), photometric on, background bank (8 backgrounds)
Epoch / batch 50 epochs, batch 16 Γ— accum 3 = 48 effective
Optimizer AdamW (betas 0.9/0.999), lr 0.0002 β†’ 1e-06 (cosine), warmup 300 steps, weight decay 0.0001, grad clip 1.0
Precision AMP bf16, channels_last, TF32, cudnn.benchmark
EMA decay 0.999 β€” the reported metrics are the EMA weights
Wall time 2.52 h on GPU RTX 5070 Ti 16 GB (sum of the sec column of metrics.csv)
Best checkpoint selected on vertex_rmse_px β†’ epoch 43 (0.813 px)
Last epoch (49) on val RMSE 1.649 px β€” the tail of the curve is noisy: the best checkpoint is not the last one

Validation curve (20 epochs are stored in the restored run history β€” the early epochs are missing from summary.json, the weights are not):

validation curves

Initialization ablation (why this model)

All four runs: 50 epochs, same seed, same architecture β€” the polygon initialization is the only thing that changes. This is the most important result of the project: pure differentiable Hough never reaches the vertices, because the refiner has refiner_max_offset: 12.0 px and cannot recover an initialization that is off by tens of px.

Initialization best val RMSE [px] epoch test RMSE [px]
keypoint (the released model) 0.81 43 0.38
hybrid (Hough + keypoint) 39.64 20 n/a
keypoint, no structural losses 39.90 26 70.08
pure differentiable Hough 71.26 45 n/a

n/a = no report_test.json for that run (the test split was never evaluated). The comparison on val stays valid: the four runs share split and seed.

ablation

Reproducing / re-exporting

  • Source repository, export command (it produces this artifact: opset 18, dynamic batch, single file): python scripts/export_onnx.py --full --checkpoint runs/g3_keypoint/best.pt
  • Evaluation: python scripts/evaluate.py --checkpoint runs/g3_keypoint/best.pt --split test --weights ema
  • Rebuilding the release: python scripts/build_hf_release.py (reuses cached artifacts and images; --refresh-onnx / --refresh-examples regenerate them)

Limitations (what does not work)

  1. Domain gap: trained on synthetic polygons. On real photographs the model vertices (the vertices output) are not reliable: use path B, edge map + geometric extractor.
  2. One polygon per frame: with more than one object in the scene, path A merges the corners into a single "star" polygon.
  3. confidence is not calibrated (β‰ˆ0.5 on every vertex): it is not a probability of correctness.
  4. Fixed 512Β² resolution: an object scaled down too far becomes undetectable (the extractor drops sides shorter than 30 px).
  5. The hybrid() extractor thresholds were tuned on a handful of real photographs: the most delicate point of the whole chain (documentation.md Β§8).
  6. No perspective model: in scenes rich in parallel lines, one proposal in four comes out deformed or on the wrong sash (visible in the window example). Neither example image was retouched to hide false positives.
  7. The metrics above hold only on the synthetic distribution of the training dataset.

License and attributions

  • Weights and code in this release: Apache-2.0, the license declared in the front matter (license: apache-2.0); the full text is in the LICENSE file at the repository root.
  • Example images: public domain (CC0 / PD), read from the Wikimedia Commons metadata; artist and source page are in each example JSON and in examples/manifest.json.
  • Third-party architectures and weights: torchvision ResNet-18 (ImageNet weights, torchvision BSD-3-Clause license).

Citation

@misc{hlt_geometric_vision,
  title        = {HLT: heatmaps and differentiable geometry for rectangle extraction},
  author       = {tailot},
  year         = {2026},
  howpublished = {\url{https://huggingface.co/tailot/hlt-geometric-vision}},
  note         = {Release generated by scripts/build_hf_release.py, numbers read from the run files}
}

*Release generated on 2026-11-02 16:11 by scripts/build_hf_release.py on Python 3.14.5, Windows 11. Every number comes from runs/g3_keypoint/ or from a measurement taken during the build on this artifact.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support