Skip to content

realsense-viewer: adapt depth color-map ruler to visible range (RSDEV-14227) - #15664

Merged
remibettan merged 6 commits into
realsenseai:developmentfrom
remibettan:rsdev-14227-dynamic-depth-ruler
Sep 17, 2026
Merged

remibettan merged 6 commits into
realsenseai:developmentfrom
remibettan:rsdev-14227-dynamic-depth-ruler

Conversation

@remibettan

@remibettan remibettan commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Tracked by: RSDEV-14227

Overview

On close-range D401 scenes the color-map ruler kept labelling 0-4 m even when everything visible was at 30-50 cm. The old calculate_ruler_max_distance was data-driven in principle (mean + 1.5·σ of visible depths), but then snapped up to the next 4 m grid multiple, so any scene with mean+1.5·σ ≤ 4 m always displayed exactly 0..4 m. The bottom of the ruler was a hardcoded "0" regardless of the actual minimum depth.

This change makes the ruler read the actual measured range, on a fine step ladder, without introducing SKU-specific behavior. The colored strip already sampled a subset of pixels (skip_pixels_factor = 30) to color the bar — the new bounds computation reuses that same distances vector, so per-frame cost stays effectively unchanged.

What changed

  • Dynamic bounds (common/viewer.cpp, new calculate_ruler_bounds): p05 / p95 of visible-depth samples with 5% headroom, snapped to a "nice" step from {0.05, 0.10, 0.25, 0.5, 1, 2, 5, 10, 20, 50, 100} m. Replaces calculate_ruler_max_distance and its hardcoded 4 m grid.
  • Attack/release hysteresis: asymmetric EMA (α_expand = 0.25, α_contract = 0.03) plus asymmetric deadband (½·step to expand, 1½·step to contract). A new farther object grows the ruler quickly; a brief close-up doesn't collapse it. Bounds near a tick edge don't oscillate.
  • Ruler labels rewrite (draw_color_ruler): the ruler now takes {min, max} instead of a single length. Labels come only from nice-step multiples inside the range — no forced min/max, no rounded-off duplicate at the top. Capped at 5 total ticks. Decimals derived from the step (≥1 → 0, ≥0.1 → 1, else 2), consistent across all labels. Depth-to-y clips pixels that fall outside the current window.
  • Right-click popover on the ruler button (common/stream-model.cpp): mode selector with three options, persisted through config_file:
    • Auto (default): the new adaptive behavior.
    • Fixed 0-4 m (legacy): bit-for-bit old behavior for anyone who needs it.
    • Fixed custom: user-typed min/max.
  • Ruler mode state (common/stream-model.h): new ruler_range_mode enum and depth_ruler_state (EMA + snapped bounds) live on stream_model, so each depth stream keeps its own smoothing.
  • Config keys (common/device-model.h): viewer_model.ruler_range_mode, viewer_model.ruler_fixed_min, viewer_model.ruler_fixed_max.

Not in scope

  • No SKU/PID branching — the decision is purely data-driven from the pixel percentiles.
  • No changes to the depth colorizer itself; the hues on the strip were already scene-adaptive (histogram equalization). This PR only makes the numeric labels match.
  • No changes to the FW or the librealsense pipeline; viewer-only.

Test plan

  • D401 close-range scene (all pixels ~0.15-0.40 m): ruler settles to a 0..0.5 m or 0..0.25 m window; labels are 0.25 and 0.5 (or similar) rather than 0-4.
  • D455 room scene (mixed depths ~0.5-4 m): ruler shows a 0..5 m window with labels 1, 2, 3, 4, 5.
  • Wave a hand into a close scene and back out: ruler expands within ~4 frames, contracts over ~1 s, no visible ping-pong at boundaries.
  • Right-click the ruler button → popover appears; Auto / Fixed 0-4 m / Fixed custom all switch behavior; min/max inputs enabled only in Fixed custom; settings persist across viewer restarts.
  • Fixed 0-4 m mode reproduces the pre-PR ruler pixel-for-pixel.

🤖 Generated with Claude Code

RSDEV-14227: on close-range D401 scenes the color-map ruler kept
labelling 0-4 m because calculate_ruler_max_distance snapped the
mean+1.5σ estimate up to a hardcoded 4 m grid. Replace it with a
percentile-driven bounds pair (p05..p95 with headroom) fed through
an asymmetric EMA (fast expand, slow contract) and snapped to a
"nice" step (0.05/0.1/0.25/0.5/1/2/5/…) only when the smoothed
value crosses an asymmetric deadband — expand at half a step, shrink
at one and a half — so a bound near a tick edge doesn't oscillate.

Ruler labels are drawn only at nice-step multiples that fall inside
the range (no forced min/max, no rounded-off duplicate at the top),
capped at five ticks; decimals derive from the step so 0.5 shows as
"0.5" and 5 shows as "5". draw_color_ruler now takes {min,max} and
maps depths accordingly, clipping pixels that fall outside the
window.

Right-clicking the ruler button opens a small popover with the mode
selector (Auto / Fixed 0-4 m legacy / Fixed custom) and, for the
custom mode, min/max inputs. All three settings persist through
config_file, so the legacy 0-4 m ruler remains one click away.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@sysrsbuild-gh-agentic sysrsbuild-gh-agentic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-generated review by rs-agentic bot

Overall the approach is clean — reusing the existing distances vector, asymmetric EMA hysteresis, and the per-stream depth_ruler_state are all solid choices. A few items flagged below.

Comment thread common/viewer.cpp Outdated
Comment thread common/viewer.cpp Outdated
Comment thread common/stream-model.cpp Outdated
Comment thread common/stream-model.cpp Outdated
remibettan and others added 2 commits September 9, 2026 14:12
Follow-ups on the previous ruler commit, all from a self code-review:

- Cache the p95-p05 span before mutating raw_lo/raw_hi so the ruler
  gets symmetric 5% headroom on both ends; before, the upper padding
  rode the already-shrunk lower endpoint and grew slightly larger
  every frame of steady input.
- Merge nice_step_for_range's hardcoded threshold table and
  draw_color_ruler's step_ladder array into one k_step_ladder
  constant that both consume. Reformulate nice_step_for_range as
  "coarsest step with <=10 grid cells across the range" — verified
  to match the previous thresholds through 20 m and extrapolate
  sensibly beyond (100 m now uses a 10 m step instead of 5 m).
- Drop the const qualifier on calculate_ruler_bounds — it writes
  ruler_state through the non-const stream_model&, so the const
  claim was misleading. Also take distances by value and std::move
  at the call site, eliminating the per-frame copy.
- Introduce k_min_ruler_gap in stream-model.h so the load path, the
  popover, and the fixed_user branch in calculate_ruler_bounds all
  enforce the same 0.1 m minimum span between user min and max.
- Validate the persisted ruler_range_mode against
  static_cast<int>(ruler_range_mode::fixed_user) instead of the
  magic 2, so adding a new mode won't silently rewrite it to auto.
- Remove the ruler_was_shown local left behind from an earlier
  iteration of the popover code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bot review on the previous commit flagged the ImGui popover id as a
std::string reconstructed every render frame. Switch to a static
const char* — same value, zero per-frame cost.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@remibettan
remibettan requested a review from Nir-Az September 15, 2026 07:16
@Nir-Az

Nir-Az commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Please share once you have the changes discussed

  1. missing steps
  2. ruller right click custom range option

…mals

Post-review feedback on the previous ruler commits:

1. The "Fixed 0-4 m (legacy)" radio has been asked to go — Auto covers
   the same case for anyone who really wants a 0-4 m bar, they can pin
   it via Fixed custom range. The enum entry is removed; the numeric
   value 1 is left commented out so any config that had it set silently
   falls back to Auto via the load-path validation.
2. In narrow depth scenes the label picker could pick a snap-step that
   gave only one tick — the reported case was seeing just "5" floating
   at the middle of the bar. Two changes:
   - Split the step ladder in two. k_snap_ladder stays as it was
     (finest 0.05 m) so the smoothing deadband is not defeated by
     sub-cm jitter. k_label_ladder extends down to 0.001/0.002/0.005/
     0.01/0.02 for label picking only — labels can safely be finer
     than the snap grid.
   - Walk k_label_ladder fine→coarse, take the smallest step with
     tick_count <= 6. If that step yields fewer than 3 labels, fall
     back one entry (finer) so a ~1 cm ruler shows sub-cm labels
     instead of a single lonely tick.
   - Decimals now derive from the picked step: >=1 -> 0, >=0.1 -> 1,
     >=0.01 -> 2, smaller -> 3. A ruler over [4.99, 5.01] labels as
     4.990, 4.995, 5.000, 5.005, 5.010.
   - Add an overlap guard: skip a tick whose glyph would collide with
     the previously placed one (font_size + 2 px). Prevents labels
     from stacking on short bars, which was the likely cause of the
     "only 5 visible" symptom.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@remibettan

remibettan commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

@Nir-Az — both landed in 3b8572a54 on top of the previous commits. Quoting your list:

  1. missing steps

The label picker used a single "nice step" ladder starting at 0.05 m and capped at ≤5 labels. On a narrow ruler that could pick a step whose only tick multiple inside the range was one number in the middle (the reported "only 5" case). Fix in common/viewer.cpp:

  • Split the ladder in two. k_snap_ladder stays as before (finest 0.05 m) so the smoothing deadband still catches sub-cm jitter. k_label_ladder is extended down to 0.001 / 0.002 / 0.005 / 0.01 / 0.02 — labels can safely be finer than the snap grid.
  • Walk k_label_ladder fine→coarse, take the smallest step with tick_count ≤ 6. If that step yields fewer than 3 labels, fall back one entry (finer). So a ~1 cm ruler shows sub-cm labels instead of a single lonely tick.
  • Decimals now derive from the picked step: ≥1 → 0, ≥0.1 → 1, ≥0.01 → 2, smaller → 3. A ruler over [4.99, 5.01] labels as 4.990, 4.995, 5.000, 5.005, 5.010.
  • Added an overlap guard: skip a tick whose glyph would collide with the previously placed one (font_size + 2 px). Prevents labels from stacking on short bars — likely the actual cause of the "only 5 visible" symptom.
  1. ruller right click custom range option

The right-click popover on the color-map ruler button had three modes: Auto, Fixed 0-4 m (legacy), and Fixed custom range. The legacy option is gone — Auto covers what it was for, and Fixed custom can pin 0..4 if anyone really wants that number. Changes in common/stream-model.{h,cpp} and common/viewer.cpp:

  • Removed the middle radio from the popover. Only Auto (adaptive) and Fixed custom range remain.
  • Removed the fixed_4m entry from the enum (numeric value 1 left commented so the on-disk numbering doesn't shift).
  • Load-path validation now accepts only 0 (Auto) and 2 (Fixed custom); any saved 1 from an older build silently falls back to Auto.
  • Removed the corresponding branch in calculate_ruler_bounds.

Nir-Az
Nir-Az previously approved these changes Sep 17, 2026

@Nir-Az Nir-Az left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Nir's feedback: a fixed ruler range saved on a D455 shouldn't follow
you to a D435 or a D585. Switch the three ruler settings to the same
per-device layout the post_processing entries already use:

  viewer_model.ruler.<device name>.<sensor name>.range_mode
  viewer_model.ruler.<device name>.<sensor name>.fixed_min
  viewer_model.ruler.<device name>.<sensor name>.fixed_max

- Removed the ruler load from stream_model's ctor (still loads the
  global show_map_ruler there — that stays a viewer-wide preference).
- Compute the key root in begin_stream once the subdevice is
  attached; store it on the stream_model.
- Popover writes route through the per-device root and no-op if the
  root is empty (defensive; a depth stream always has one after
  begin_stream).
- Reseed the smoothing state after loading so switching between two
  cameras doesn't inherit the previous scene's bounds.

The old flat keys (viewer_model.ruler_range_mode / _fixed_min /
_fixed_max) are no longer read or written; existing configs with
those set are silently ignored, and each SKU starts from the Auto
default until the user pins a custom range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@sysrsbuild-gh-agentic sysrsbuild-gh-agentic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [rs-agentic] BUG — shared popup ID in multi-stream mode

static const char* const popup_id = "##ColorMapRulerPopup" is the same literal for every stream_model instance. When two depth streams are visible simultaneously (e.g. two devices), both ruler buttons share this ImGui popup ID inside the same window. Whichever stream_model::draw_stream_footer runs last in the frame "owns" the popup; right-clicking either button opens that instance's popup and mutates the other stream's ruler_mode/ruler_fixed_* — wrong stream gets its settings changed.

Fix: make the ID unique per stream, e.g.:

// use the stream_model address as a discriminator
const std::string popup_id = std::string("##ColorMapRulerPopup") + std::to_string(reinterpret_cast<uintptr_t>(this));

or wrap the whole ruler block in ImGui::PushID(this) / ImGui::PopID() and use a bare "##ColorMapRulerPopup" — ImGui scopes it automatically.

Bot review flagged a real collision: with two visible depth streams
(two devices), both ruler buttons share the same "##Color map" ID and
both popovers share "##ColorMapRulerPopup". ImGui matches OpenPopup
and BeginPopup by ID within the current stack, so right-clicking one
button would open the OTHER stream's popover and mutate its
ruler_mode / ruler_fixed_* — wrong stream gets the settings changed.

Wrap the entire depth-stream ruler block in PushID(this)/PopID() so
both the button ID and the popup ID hash with the stream_model
pointer. No functional change with a single stream; multi-device
setups now correctly target the clicked tile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@remibettan
remibettan requested a review from Nir-Az September 17, 2026 09:28

@Nir-Az Nir-Az left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@remibettan
remibettan merged commit 13ab1d5 into realsenseai:development Sep 17, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants