Skip to content

Introduces unproject() lookup tables#29

Open
csp256 wants to merge 18 commits into
Robertleoj:mainfrom
csp256:unproject_LUT
Open

Introduces unproject() lookup tables#29
csp256 wants to merge 18 commits into
Robertleoj:mainfrom
csp256:unproject_LUT

Conversation

@csp256
Copy link
Copy Markdown

@csp256 csp256 commented Apr 11, 2026

This introduces the ability to precompute and save to disk a sparse (or dense) lookup table of normalize_points() results. It also introduces a way to analyze the accuracy of this lookup table.

There is a cpp_runtime/ folder, which is intended to be copy-pasted into the user's C++ project to read and use the .unproject_LUT file. This code in this folder is not connected to any of the extant cpp_src/ code.

I added tests and they all pass.

Building a dense lookup table can take quite a while on a large image, so that part is parallelized. The analysis code can still be quite slow, but it's usable and was resistant to initial efforts to parallelize it. I stopped myself from lowering that to C++.

I added docs, as well as an example Marimo notebook, which you can run by simply going ./example/unproject_lut_mo.py. The _mo suffix on the stem enables Jupyter to recognize it as a marimo notebook with https://github.com/marimo-team/marimo-jupyter-extension This is a bit of what it looks like:

Screenshot 2026-04-11 at 12 49 06 AM Screenshot 2026-04-11 at 12 54 28 AM Screenshot 2026-04-11 at 12 51 53 AM Screenshot 2026-04-11 at 12 31 57 AM Screenshot 2026-04-11 at 12 52 06 AM

Maybe you want to convert this to a Jupyter notebook? I don't even have Jupyter installed any more. It might be a good idea to provide examples as both Jupyter and Marimo notebooks. I can open another PR to convert the Jupyter notebooks to Marimo, if you'd like.

I moved the contents of CLAUDE.md to AGENTS.md and pointed CLAUDE.md at it, as I understand is the standard in projects where people use multiple coding agents.

I know its not radically simple, but hopefully it doesn't violate the KISS too much for your taste.

Some of the old logic about previous file format versions has stuck around, but I can clean that up on Monday. I just wanted to get this PR to you before the weekend.

@Robertleoj
Copy link
Copy Markdown
Owner

Thanks for the PR, it's a great addition. I'm looking over it and making some changes, I'll get back when I've gone through it properly.

Robertleoj and others added 15 commits April 12, 2026 21:06
Replace the Python adaptive-quadtree analyzer with a per-cell C++ optimiser
that maximises sin²(angular error) over each LUT cell using gradient ascent
in normalised camera-frame coordinates with a ReLU penalty enforcing pixel-cell
membership. Backed by ceres::Jet autodiff, OpenMP across cells, and works for
both PinholeSplined and OpenCV models.

The heatmap and accuracy report both use this single C++ entry. The dense
sampling path (sample_lut_accuracy / UnprojectLUTSampleAccuracy) is removed —
the per-cell maximiser supersedes it. Heatmap computation goes from ~6s to
~0.2s on typical LUTs.

Angular error formula switched to atan2(‖cross‖, dot) for full precision near
zero. Adds heatmap correctness tests covering optimiser/analyzer agreement,
peak-pixel reproduction, cell-membership, delta consistency, brute-force
matching on a realistic LUT, and exact zero error for a linear pinhole.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure UnprojectLUTErrorHeatmap to store the three primary 2D arrays
(peak_pixel_xy, exact_xy, approx_xy) and expose max_angular_error_deg,
error_delta_xy, and error_direction_xy as @Property derivations. The C++
output column layout changes from (N, 5) [angular, peak, delta] to
(N, 6) [peak, exact, approx]; Python derives the rest. Save format drops
3 redundant entries.

Also drop max_iters / grad_tol from the public Python API and from both
result dataclasses — they're now hardcoded as private constants
(_OPTIMISER_MAX_ITERS, _OPTIMISER_GRAD_TOL). Users never need to tune
them in practice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `vmax` parameter for capping the colorbar in the chosen angular_unit.
Default arrow length now scales with per-cell error magnitude (brightest
cell maps to `arrow_scale * arrow_spacing`); pass `constant_arrow_length=True`
for the previous fixed-length behaviour.

Drop the `Path | str` overload from the heatmap argument — callers always
pass a heatmap object now. Trims the unused Path import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reject grids smaller than 2x2 or with zero-span extents in the
constructor instead of papering over them with scale=0 branches.
Move each interpolation mode into its own private method so the
dispatch in query() is a plain switch. Replace the string_view +
char-storage trick with std::string for lensboy_version, and make
the invalid-result path explicit via UnprojectLUTQueryResult::invalid().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Wgrid/Hgrid -> grid_width/grid_height (lut_max_cell_error.cpp)
- nx/ny family -> normalized_x/normalized_y across 4 files
- px/py family -> pixel_x/pixel_y across all .cpp files; py:: namespace preserved
- Drop > 1 defensive guards already guaranteed by entry-point require >= 2
  (bilinear interpolator, scale + pixel-span calculations)
- Indent OpenMP pragmas with // clang-format off/on scaffolding

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants