2. added unwarp map export in current cli, don't need to generate by precompute in sensor running
20 KiB
Reading the probe sensitivity report
This is the operator's guide to sensitivity_report.json and
optimizer_report.json. The interface itself is frozen in
probe-pipeline.md; this document explains what the numbers
mean, what they do not mean, and how to use the optimizer.
The bench deforms the sensing surface through a known positive x/y/z sweep
(0.01 mm .. 0.10 mm in 0.01 mm steps) in a local probe-average basis, renders what
cam_sensor sees, and measures DIS optical flow against the zero-displacement
reference at two scales (the real render and the runtime's processed resolution).
deform -> render -> track (DIS flow) -> sensitivity (score + report) -> optimise
Every stage writes JSON under build/probe/<sensor>/<revision>/. The sensitivity
stage reads render_manifest.json, flow_report.json and flow/<scale>/*.npy
and writes sensitivity_report.json. It never re-runs a stage; use the CLI's
--skip-deform/--skip-render/--skip-track to reuse existing artifacts.
The one thing to remember
Sensitivity here is a relative ranking under a fixed estimator (DIS) and a
fixed camera. It is not force calibration. The absolute gain_mm_per_mm
carries the measurement geometry with it, so only compare scores produced with the
same radius_mm, ROI, resolution and tracker settings.
The floor is not a noise model (read this first)
An earlier version of this document stated that the marker cell must match the
estimator's effective window, and that nx=31 was the best pattern. That is retracted.
The conclusion was driven by floor_px, and floor_px is not a valid noise model.
floor_px is measured as reference vs a re-render at seed + 1 — Cycles antialiasing
jitter, ~5 grey levels RMS on half the pixels, scaling with edge contrast. It cannot rank
patterns, for three measured reasons:
-
Its trend reverses under additive noise. p95 |flow| over the ROI, same DIS config:
pattern render-seed injected σ=2 σ=4 nx=80 0.0292 0.0161 0.0278 nx=31 0.0376 0.0084 0.0182 nx=56 0.0134 0.0102 0.0203 -
It understates the real floor, coarse patterns most. Measured on idle dd02 footage with the same estimator: 0.2267 px at 1280x720, dominant period 25.5 px, 0.0089 per period, with short baseline == long baseline (ratio 0.98), so it is white noise and the clip is idle. Simulated floors per cell sit ~2x (nx=80), ~4x (nx=31) and ~6x (nx=56) below it.
-
Signal is not noise-invariant, so gain is only meaningful with a stated model. Retention
gain(σ)/gain(clean)at +0.10 mm, x axis: nx=80 dm1 99% at σ=4 and 84% at σ=16, against nx=31 dm1 72% and 59%.
What the calibrated model supports: at the render scale, radius_mm=0.5,
dis_mode=1, nx=80 has the highest clean response on both axes (x 9.10 vs 4.74 px/mm;
y 11.69 vs 10.90) and the best retention, so the shipped 80x80 is competitive or better
on this evidence. The process scale and the radius_mm=1.5 block have not been
re-ranked.
So: report and compare noise_models (gain, floor and response_retention per σ, all
measured under one model), not gain / floor_px. Every scale records
null.floor_basis: render_seed_reproducibility so the distinction is machine-readable.
The quantities that are still worth watching
- The marker grid is authored across the camera image, so a cell is
render_width / nx. It is not a millimetre quantity: a millimetre cell times the probe-local px/mm understates it by about half, because the probe-local scale (~13.2 px/mm) differs from the camera-grid scale (~27 px/mm).cell_pxis the camera-space value and governs;nominal_cell_pxis the old millimetre-derived value. effective_patch_px = patch_px * 2**finest_scaleis the correlation window in input pixels (16 px on r01, becausedis_mode=1).cell_over_effective_patch_pxis a useful diagnostic — the ratio hovered around 1 for the patterns that scored well — but it is not a validated law, since the score was floor-driven.- The measured gain is confounded with the ROI size. The probe patch plus falloff taper spans only ~13 px, so DIS regularizes toward the static background; a synthetic rigid translation recovers ~17% at r=6.6 px (the ROI) but 81% at r=20. Compare patterns only at a fixed radius, ROI and scale.
- The optimum moves with
radius_mm(at r=1.5 the score ordering differs), andoverall.scoreis the unweighted mean of the two scales. Readscale_scoresandscale_ranksbefore trusting a winner.
Top-level fields
| Field | Meaning |
|---|---|
texture |
The marker pattern actually measured: PNG path + sha256, grid [nx, ny], and the physical cell cell_mm = [vector_width_mm/nx, vector_height_mm/ny]. grid/cell_mm are null rather than assumed when the pattern cannot be established. |
probe |
group, origin, basis, radius_mm, falloff, and affected_vertices (how many vertices outside the probe group actually move). |
tracker |
dis_preset, dis_mode, patch_px (the nominal patch) and finest_scale (the pyramid level dis_mode selects, which is what turns patch_px into effective_patch_px). |
overall |
score (mean over scales and axes), verdict (worst axis wins), best_scale, best_axis. |
warnings |
The honesty channel. Read it before the score. |
notes |
Measured context, including which pattern source was used and the ROI-size confound. |
Per-scale fields
| Field | Meaning |
|---|---|
px_per_mm |
The scale's probe-local scalar: render_manifest.probe_px_per_mm.p50, halved at the process scale (13.248 at render, 6.624 at process on r01). Used for the sub-pixel warnings and the marker cell in pixels. Never a gain divisor — the per-axis scale_px_per_mm is, and the two differ (the probe-local p50 is 13.248 while the mesh-wide p50 is 13.430). |
mesh_px_per_mm |
The mesh-wide p50 (13.430 at render): p50 over all 7341 mesh edges of projected px / 3D mm. Recorded for context only; never used as a gain divisor or as the probe scale. |
displacement_px_per_step |
step_mm * px_per_mm: how many pixels a single 0.01 mm step is at that scale. |
cell_px |
The camera-space marker cell, width / nx. The pattern grid is defined across the camera image, so this is the cell the estimator sees. cell_px_source records that. |
nominal_cell_px |
The old millimetre-derived cell (cell_mm[0] * px_per_mm) at the probe-local scale; about half of cell_px, kept only so an older expectation can be compared. Not the governing value. |
effective_patch_px |
patch_px * 2**finest_scale, the correlation window in input pixels. 16 px on r01, because dis_mode=1. |
cell_over_patch_px |
cell_px / patch_px, the nominal ratio. |
cell_over_effective_patch_px |
cell_px / effective_patch_px — the ratio to steer by. Near 1 is where the measured optima sit. |
null_floor_px |
The largest per-axis estimator floor at this scale (see below). |
local_scale_source |
per_axis (measured per-axis scales), p50_fallback (old manifest without per-axis keys), or unavailable. |
score, verdict |
Mean/aggregate of the axes. |
Per-axis fields
{"axis": "x", "direction_px": [-1.0, 0.000589], "direction_source": "centroid_regression",
"points": [{"step_mm": 0.01, "applied_px": 0.1325, "response_px": 0.081, "above_floor": true}],
"summary": {"gain_px_per_mm": 9.247, "gain_mm_per_mm": 0.698, "scale_px_per_mm": 13.248,
"gain_gate": "derived", "snr": 33.5, "spearman": 1.0,
"resolvable_step_mm": 0.01, "saturation_mm": null,
"cross_axis_leak": 0.038, "score": 32.25, "verdict": "resolvable",
"floor_px": 0.0276, "floor_source": "null_projection_p95",
"px_per_mm_axis": 13.248}}
direction_px— the unit image-space direction the probe moves for this axis, measured fromrender_manifest.frames[].centroid_pxby regressing the projected centroid onstep_mm(direction_source: "centroid_regression"). Fallbacks are a two-point finite difference and, when no centroids exist at all, the ROI-mean flow at the largest step. The per-frameroi.u/vis deliberately not used: the ROI is fixed at the reference centroid.points[].applied_px— the applied displacement in pixels for that axis (step_mm * px_per_mm_axis), notstep_mm * px_per_mm. The isotropic scalar would overstate z (1.519 px/mm) by ~8.7x.floor_px— p95 of the null-frame flow projected ondirection_px(floor_source: null_projection_p95). If the null array is missing the report falls back to the flow report's scalar magnitude p95 and says so (reported_magnitude_p95_fallback), which is a different quantity.gain_px_per_mm— the primary sensitivity: the OLS slope ofresponse_pxagainstapplied_mmover the steps whose|response| > floor_px(at least three such points, elsenull). Units: measured pixels of probe response per millimetre applied. This is the number to rank patterns by.scale_px_per_mm— the probe-local, per-axis scale fromrender_manifest.probe_px_per_mm[axis], rescaled to this scale's pixel grid.gain_mm_per_mm— derived asgain_px_per_mm / scale_px_per_mmonly whenscale_px_per_mm >= min_local_scale_px_per_mm(currently 1.0). Otherwise it isnullwithgain_gate=below_min_local_scale(orscale_unknown), plus a warning. Dividing by a near-zero scale would manufacture a huge ratio no camera could observe; the gate makes that structurally impossible. The current process-scale z axis (0.760 px/mm) is gated for exactly this reason.snr—|gain_px_per_mm| * max_mm / max(floor_px, eps). Both numerator and denominator are in pixels, so it is dimensionally consistent and comparable.spearman— rank correlation ofresponse_pxagainstapplied_mmover the axis's steps, clamped to[0, 1]. A 1.0 means the response grows monotonically.cross_axis_leak— mean perpendicular response over mean in-axis response, clamped to[0, 1].resolvable_step_mm— the smallest step whose|response| > floor_px, elsenull. This is the "is a single step measurable" answer, and it is a floor comparison, not a pixel-size comparison.saturation_mm— the first step where the response stops increasing (a decrease after a rise), elsenull.px_per_mm_axis— the probe-local px/mm measured independently from the centroid regression at this scale. It should agree withscale_px_per_mm; a25 % disagreement is reported as a note.
score—snr * spearman * (1 - min(leak, 0.5)). A dimensionless ranking number, not a percentage.verdict— in precedence order:ambiguous— the tracker'sreference_texture_known_answerfailed, i.e. a known shift applied to the actual rendered marker was not recovered. A periodic pattern aliases a sub-period shift onto its period, so direction and magnitude are untrustworthy and no score may promote the pattern. This outranks everything else.resolvable—gain_px_per_mmis notnullandsnr >= 3.marginal—gain_px_per_mmis notnullandsnr < 3.unresolvable—gain_px_per_mmisnull.
overall.verdict is resolvable only if every scale and axis is; ambiguous if
any scale is; otherwise the worst remaining verdict.
Honest limits (measured, not assumed)
- Sub-pixel steps, and why they still measure. At 500x500 the probe scale is
~13.25 px/mm, so 0.01 mm is ~0.13 px and the whole 0.1 mm sweep is ~1.33 px.
The measured zero-input floor is ~0.03 px, so a step is ~4.5x above the floor
and the ROI mean genuinely resolves single steps at the render scale
(
resolvable_step_mm = 0.01). "Below one pixel" and "below the estimator floor" are different statements; the report states the step and the floor separately in pixels. What is lost below one pixel is per-pixel precision, not the ROI-averaged response. At the process scale the same step is ~0.066 px and the floor comparison decides, not the pixel size. - The measured gain is confounded with the ROI size. The probe patch plus its
falloff taper spans only ~13 px (1-2 DIS patches of 8 px), so DIS regularises
toward the static background. Injecting a synthetic rigid translation of the
same 1.3248 px recovers ~17 % at a patch radius of 6.6 px (equal to the ROI),
51 % at r 10, 81 % at r 20 and 95 % at r 40. Relative ranking of marker patterns
is valid only while
radius_mm, the ROI and the scale are held fixed. That is whyrun_optimisevariesnx/ny/seedand never the radius. - The render-scale deficit is measurement-region resolution, not noise,
aliasing or a diluted average: the real flow's radial profile is flat inside the
ROI (-0.9162, -0.9096, -0.8725 px at r 0-2, 2-4, 4-6.6), the texture-level
known-answer passed (2 px recovered as 2.0000 px), and the coherent motion is
measured at about 0.67x of the applied displacement. Only synthetic periodic
textures alias, and that is what the
ambiguousverdict catches. - The process scale is much weaker. At 250x250 the region is ~6.5 px, under one DIS patch, and the response collapses while the forward-backward error grows. A low process score is a real result about the runtime resolution, not a bug in the scorer.
The three pre-registered hypotheses, as the artifact shows them
The report deliberately records the numbers needed to test these rather than asserting them:
- (a) the cell should be comparable to the DIS patch.
cell_pxandcell_over_patch_pxare per scale. For the shipped nx=80 pattern at 500x500 the cell is 3.06 px = 0.38 of the 8 px patch; at 250x250 it is 1.53 px, below Nyquist, and the report warns. Measured outcome: the hypothesis is scale-dependent, not a straight win — nx=80 is the best pattern at the render scale (score 36.77) and near the worst at the process scale (1.77), so no single nx is best on both. That is why the report ranks per scale. - (b) an 80x80 grid cannot track at 250x250. Supported at the process scale: that
is the
cell_px < 2warning plus the measured collapse of the process responses (nx=80 ranks 10/10 there). - (c) no single 0.01 mm step is resolvable. Falsified at the render scale by
measurement:
resolvable_step_mmis 0.01 on x/y (and 0.04 on z) because the ROI aggregation clears the ~0.03 px floor; it is not assumed either way.
Warnings you should expect
step_mm=... is ... px at <scale>, i.e. below one pixel— a per-pixel precision note that also states the per-axisfloor_px, so you can see whether the step clears the floor.marker cell is ... px at <scale> ... below 2 px— the cell is at or under the DIS patch Nyquist limit.no step is resolvable on axis ... at <scale>— every|response|is inside the estimator floor.the null floor (...) exceeds the full ... mm response— the aggregate response never rises out of the floor.axis ... has a probe-local scale of ... below min_local_scale_px_per_mm— the derivedgain_mm_per_mmwas refused.reference_texture_known_answer FAILED ...— the scale's axes areambiguous; the warning quotes measured vs expected shift.the probe ROI is not fully inside the ... frame— the ROI was truncated at that scale and only the pixels actually inside were scored.
Using the optimizer
orisys-design optimise <revision_dir> [--output-dir DIR] [--grid LIST]
[--seeds LIST] [--search-resolution N] [--top N] [--dry-run]
- The default grid is
[15, 20, 24, 28, 31, 36, 42, 48, 56, 64, 80]with seed7. It brackets the DIS-patch-matched cell (an 8 px patch is ~0.6 mm, i.e. nx ~ 28-36 for this marker page), and it includes nx=80 because that is the opposite case, not because it aliases: measured on the real run nx=80 wins the render scale (36.77 at 500x500) and loses the process scale (1.77 at 250x250, rank 10/10), because its cell is 3.06 px at 500x500 but 1.53 px at 250x250. The shipped random texture does not alias (a known 2 px shift is recovered as 2.0000 px); aliasing onto the DIS period was measured only for strongly periodic synthetic textures, which is why the report carries anambiguousverdict gate rather than a score penalty. - Candidates are
(nx, ny, seed), enumerated in a sorted order and ranked by(-score, nx, ny, seed), so two runs produce byte-identical rankings. - The search resolution defaults to
settings.resolution(the real render scale), not to a reduced250x250. A reduced search answers a different question: on the real data a 250-based search picks nx=48 (search score 8.94) while nx=80 is the best pattern at 500x500 (36.77) and near the worst at 250 (1.77). Use--search-resolution Nonly as a deliberate speed/correctness trade, and read the search-vs-confirm gap note: a large gap is the signal that the search resolution was wrong. - Each candidate regenerates the marker texture, then runs deform -> render ->
track -> sensitivity at the search resolution. Only the winner is re-run at the full
probe.resolutionand itsconfirmed_scorerecorded. --dry-runenumerates the candidates and writes the report without rendering anything (no candidate directories, no Blender, no cv2).- The winner is a measured ranking under a fixed estimator and camera; it is not
a global optimum. The report says so in
notes, and it also warns if the top-scoring candidate's verdict isambiguous. bestis scale-dependent.overall.scoreis the unweighted mean of the per-scale scores, which is a modelling choice: on the real data nx=80 wins the render scale while a mid-grid pattern balances both scales, so the overall winner need not be the best render-scale pattern. Every candidate row therefore carriesscale_scores,scale_verdictsandscale_ranks(1 = best on that scale), and the report carriesranked_by_scalealongside the overallrankedlist andwinner.scale_scores. Read those before trusting the winner.- Candidate rows carry
score,verdict,cell_px(reported at the full render scale),report,texture_sha256and the per-scale fields above. A candidate whose run raised is kept in the table with anullscore, anerrorstring, and ranks last.
Reproducing and testing
orisys-design sensitivity sensors/<sensor>/<revision> # score only
orisys-design optimise sensors/<sensor>/<revision> --dry-run
& "C:\Users\cheun\anaconda3\envs\py310\python.exe" -m pytest tests/test_sensitivity.py tests/test_optimise.py
tests/test_sensitivity.py includes the primary correctness evidence: a synthetic
flow field with a known injected gain (13.248 px/mm) whose OLS recovery is
asserted within 0.2 px/mm (measured recovery 13.2584 px/mm, floor 0.0459 px,
snr 28.9), plus the gated-gain_mm_per_mm path, the ambiguous verdict guard,
resolvable-step selection, the leak penalty, saturation detection, report schema
and determinism. tests/test_optimise.py covers enumeration, ranking stability,
winner selection, full-resolution re-confirmation, the dry run and the absence of
any global-optimum claim. Neither test needs Blender or cv2.
Measured example (this scorer, real finger-xynova/r01 artifacts)
| scale | axis | gain_px_per_mm |
gain_mm_per_mm |
floor_px |
resolvable_step_mm |
snr |
verdict |
|---|---|---|---|---|---|---|---|
| render 500x500, 13.248 px/mm, cell 3.06 px | x | 9.247 | 0.698 | 0.0276 | 0.01 | 33.5 | resolvable |
| y | 12.019 | 0.680 | 0.0160 | 0.01 | 75.3 | resolvable | |
| z | 0.677 | 0.445 | 0.0160 | 0.04 | 4.24 | resolvable | |
| process 250x250, 6.624 px/mm, cell 1.53 px | x | 1.427 | 0.215 | 0.0158 | 0.01 | 9.0 | resolvable |
| y | 0.931 | 0.105 | 0.0070 | 0.01 | 13.2 | resolvable | |
| z | 0.227 | null (gated, scale 0.760 < 1.0) |
0.0070 | 0.03 | 3.23 | resolvable |
overall.score 22.22, overall.verdict resolvable, best_scale render,
best_axis y. The render-scale gain_mm_per_mm of ~0.68-0.70 is the ~0.67x
coherent-motion shrinkage described above, not a noise floor. These numbers are
re-produced by re-running sensitivity after any change to the upstream stages.