Skip to content

Camera intrinsics, normalised kernels, measured benchmarks - #4

Merged
cobanov merged 1 commit into
mainfrom
docs/benchmarks-and-demo
Aug 30, 2026
Merged

Camera intrinsics, normalised kernels, measured benchmarks#4
cobanov merged 1 commit into
mainfrom
docs/benchmarks-and-demo

Conversation

@cobanov

@cobanov cobanov commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Closes #1. Answers #2 in the README.

--focal, for metric depth (#1)

The default treats a depth map as a height field, which is what a shading normal map wants. --focal (in pixels) treats it as metric distance from a pinhole camera instead: each pixel is unprojected to P = ((u - cx) Z / f, (v - cy) Z / f, Z) and the normal is the cross product of the two surface tangents. A plane twice as far away then comes out with the same orientation.

Tested against analytic planes: the recovered normal matches the true plane normal to within 1e-4 for all three gradient methods, and a very long focal length converges on the height field form with the relief scaled f / Z.

Normalised kernels

Implementing that exposed a real bug. The Sobel and Scharr kernels had gains of 8 and 32, so they were not estimating a derivative at all. The height field path hid this inside strength, but the metric form needs the true slope, and a tilted plane came back at 0.63 of its correct orientation instead of 1.00.

All three methods are now normalised: a ramp of one unit per pixel reads as 1, whichever method is picked, and strength finally means the same thing across methods. Sobel and Scharr therefore produce a gentler relief than before at the same strength; gaussian, the default, is unchanged.

IR version 10

The exported graph carried IR version 13, which is what the newest onnx emits. ONNX Runtime 1.22 and earlier refuse anything above 10, so the models failed to load on a slightly older runtime, which was found while benchmarking on the CUDA box. Pinned, with a test.

Rings (#2)

Contour rings come from the source depth map's 8-bit steps, amplified by the derivative, not from the conversion. Measured against the same surface before quantisation, and written up in the README:

Source Mean error
8-bit, --sigma 0.5 7.1 levels
8-bit, --sigma 1 2.4 levels
8-bit, --sigma 3 0.2 levels
16-bit, --sigma 1 0.01 levels

Benchmarks and the demo

tools/benchmark.py produces the README tables: an M4 Pro and an i5-9600K with an RTX 3090, 2 to 37 megapixels, portrait and landscape. Depth estimation now uses CUDA when onnxruntime-gpu is installed: 32 ms against 231 ms on the M4 Pro CPU.

The browser demo is deployed at depth2normal.cobanov.dev.

The README's version history section is gone; it describes what the tool is now.

87 tests, ruff clean.

Closes #1: `--focal` treats the depth as metric distance from a pinhole
camera, unprojects each pixel and returns the normals of that 3-D surface, so
a plane keeps its orientation whatever its distance. Recovered against
analytic planes to within 1e-4.

That exposed a second problem: the Sobel and Scharr kernels had gains of 8 and
32, which the height field path hid inside `strength` but which made the
metric form wrong. All three methods are now normalised, so a ramp of one unit
per pixel reads as 1 and `strength` means the same thing whichever method is
picked.

Also:

- The exported graph is pinned to IR version 10. ONNX Runtime 1.22 and earlier
  refuse anything above it, which the newest `onnx` emits by default, so the
  models would not load on a slightly older runtime.
- `tools/benchmark.py`, and the README numbers now come from it: an M4 Pro and
  an i5-9600K with an RTX 3090, from 2 to 37 megapixels, portrait and
  landscape.
- Answers #2 in the README: contour rings are the source's 8-bit steps
  amplified by the derivative, measured against the unquantised surface.
- Depth estimation runs on CUDA when onnxruntime-gpu is installed: 32 ms on
  the 3090 against 231 ms on the M4 Pro CPU.
- The browser demo is live at depth2normal.cobanov.dev.
@cobanov
cobanov merged commit 9ac059e into main Aug 30, 2026
11 checks passed
@cobanov
cobanov deleted the docs/benchmarks-and-demo branch August 30, 2026 18:46
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.

intrinsic

1 participant