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).
Debug mosaic (network edge map | hybrid map | 512 frame with the extracted sides): 
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.
Debug mosaic (network edge map | hybrid map | 512 frame with the extracted sides): 
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 indocumentation.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):
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.
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-examplesregenerate them)
Limitations (what does not work)
- Domain gap: trained on synthetic polygons. On real photographs the model vertices (the
verticesoutput) are not reliable: use path B, edge map + geometric extractor. - One polygon per frame: with more than one object in the scene, path A merges the corners into a single "star" polygon.
confidenceis not calibrated (β0.5 on every vertex): it is not a probability of correctness.- Fixed 512Β² resolution: an object scaled down too far becomes undetectable (the extractor drops sides shorter than 30 px).
- The
hybrid()extractor thresholds were tuned on a handful of real photographs: the most delicate point of the whole chain (documentation.md Β§8). - 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.
- 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 theLICENSEfile 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.





