Skip to content

Commit e872032

Browse files
author
Evgeni Raikhel
committed
Address review feedback: typed Python composite-option access, feature demo
Rework wrappers/python/examples/embedded_filters.py to use the typed composite-option accessors (get_improved_close_range_control(), get_temporal_filter_dpp_config()) registered per option id, reading fields back by introspecting the returned typed object - no struct.pack/unpack format string or hand-maintained byte offsets on the Python side anymore. Fold composite_option_hello.py's demo into this same file and remove it, so there's one Python composite-option example instead of two. Add an "Improved Close Range Depth" section to examples/embedded-filters/rs-embedded-filters.cpp that demos the actual feature rather than SDK plumbing: find the filter across all connected devices (it's currently D500-USB-only, unlike the rest of this DDS-only file), read its typed config, stream briefly with it off then on (Downscale x2), report the minimum valid depth seen each way, and restore the original configuration.
1 parent 3b28c5f commit e872032

3 files changed

Lines changed: 168 additions & 103 deletions

File tree

examples/embedded-filters/rs-embedded-filters.cpp

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// Copyright(c) 2025 RealSense, Inc. All Rights Reserved.
33

44
#include <librealsense2/rs.hpp>
5+
#include <librealsense2/h/rs_hkr_improved_close_range_control.h>
56

67
#include <iostream>
78
#include <string>
89
#include <thread>
10+
#include <vector>
911

1012

1113
rs2::device get_dds_device()
@@ -138,6 +140,138 @@ try
138140
std::cout << "Setting toggle back to initial value: " << enabled << std::endl;
139141
dec_filter.set_option(RS2_OPTION_EMBEDDED_FILTER_ENABLED, enabled);
140142

143+
std::cout << std::endl;
144+
std::cout << "Improved Close Range Depth (composite option)" << std::endl;
145+
std::cout << "=========================================" << std::endl;
146+
147+
// Unlike Decimation above, this filter has no RS2_OPTION_EMBEDDED_FILTER_ENABLED scalar
148+
// option at all - its whole configuration, enable included, is one atomically-exchanged
149+
// struct (see rs_hkr_improved_close_range_control.h). It's currently registered on the D500
150+
// USB path only (see improved_close_range_filter_feature in
151+
// src/ds/features/improved-close-range-filter-feature.cpp), not over DDS, so - unlike the
152+
// rest of this file - it's looked up across every connected device rather than the one DDS
153+
// device selected above.
154+
// rs2::embedded_filter/rs2::depth_sensor have no default constructor - a 0-or-1-element
155+
// vector stands in for "found or not" instead of a nullable local.
156+
std::vector<rs2::embedded_filter> found_filter;
157+
std::vector<rs2::depth_sensor> found_sensor;
158+
for (auto&& d : rs2::context().query_devices())
159+
{
160+
auto ds = d.first<rs2::depth_sensor>();
161+
if (!ds)
162+
continue;
163+
for (auto&& f : ds.query_embedded_filters())
164+
{
165+
if (f.supports_composite_option(RS2_COMPOSITE_OPTION_HKR_IMPROVED_CLOSE_RANGE_CONTROL))
166+
{
167+
found_filter.push_back(f);
168+
found_sensor.push_back(ds);
169+
break;
170+
}
171+
}
172+
if (!found_filter.empty())
173+
break;
174+
}
175+
176+
if (found_filter.empty())
177+
{
178+
std::cout << "No connected device exposes HKR Improved Close Range Control - skipping." << std::endl;
179+
}
180+
else
181+
{
182+
rs2::embedded_filter& close_range_filter = found_filter.front();
183+
rs2::depth_sensor& close_range_depth_sensor = found_sensor.front();
184+
const auto id = RS2_COMPOSITE_OPTION_HKR_IMPROVED_CLOSE_RANGE_CONTROL;
185+
186+
// Typed get - the SDK's own bound struct (get_composite_option_as<T>()), not raw bytes;
187+
// the same struct set_composite_option_from() below takes to write it back atomically.
188+
rs2_improved_close_range_control original{};
189+
try
190+
{
191+
original = close_range_filter.get_composite_option_as<rs2_improved_close_range_control>(id);
192+
}
193+
catch (const rs2::error& e)
194+
{
195+
// Registered but not actually functional on this device/FW is a real, expected
196+
// outcome - supports_composite_option() only reflects static registration, never a
197+
// live capability check.
198+
std::cout << "Registered but not functional on this device/FW: " << e.what() << std::endl;
199+
original.header.version = 0;
200+
}
201+
202+
if (original.header.version == 0)
203+
{
204+
// get_composite_option_as() failed above - nothing more to demo.
205+
}
206+
else
207+
{
208+
auto close_range_profile = get_depth_profile(close_range_depth_sensor, nominal_width, nominal_height);
209+
if (!close_range_profile)
210+
{
211+
std::cout << "No " << nominal_width << "x" << nominal_height
212+
<< " depth profile available on this device for the demo - skipping." << std::endl;
213+
}
214+
else
215+
{
216+
// Streams a short burst on close_range_depth_sensor's own current configuration
217+
// and returns the minimum non-zero (i.e. valid) depth value seen - a simple,
218+
// honest way to show the effect without asserting anything about what's actually
219+
// in front of the camera right now.
220+
auto capture_min_valid_depth = [&](int frames_to_skip, int frames_to_measure) -> uint16_t
221+
{
222+
rs2::frame_queue queue(1);
223+
close_range_depth_sensor.open(close_range_profile);
224+
close_range_depth_sensor.start(queue);
225+
226+
for (int i = 0; i < frames_to_skip; ++i)
227+
queue.wait_for_frame();
228+
229+
uint16_t min_depth = 0;
230+
for (int i = 0; i < frames_to_measure; ++i)
231+
{
232+
auto depth = queue.wait_for_frame().as<rs2::depth_frame>();
233+
auto data = reinterpret_cast<const uint16_t*>(depth.get_data());
234+
size_t pixel_count = (size_t)depth.get_width() * depth.get_height();
235+
for (size_t p = 0; p < pixel_count; ++p)
236+
{
237+
if (data[p] != 0 && (min_depth == 0 || data[p] < min_depth))
238+
min_depth = data[p];
239+
}
240+
}
241+
242+
close_range_depth_sensor.stop();
243+
close_range_depth_sensor.close();
244+
return min_depth;
245+
};
246+
247+
std::cout << "Streaming with Improved Close Range Depth OFF..." << std::endl;
248+
auto cfg = original;
249+
cfg.enable = 0;
250+
close_range_filter.set_composite_option_from(id, cfg);
251+
auto min_depth_off = capture_min_valid_depth(5, 10);
252+
std::cout << " Minimum valid depth (filter off): " << min_depth_off << " (depth units)" << std::endl;
253+
254+
std::cout << "Streaming with Improved Close Range Depth ON (Downscale x2)..." << std::endl;
255+
cfg = original;
256+
cfg.enable = 1;
257+
cfg.filter_type = 0; // Downscale
258+
cfg.downscale_ratio = 1; // x2
259+
close_range_filter.set_composite_option_from(id, cfg);
260+
auto min_depth_on = capture_min_valid_depth(5, 10);
261+
std::cout << " Minimum valid depth (filter on): " << min_depth_on << " (depth units)" << std::endl;
262+
263+
if (min_depth_on > 0 && (min_depth_off == 0 || min_depth_on < min_depth_off))
264+
std::cout << "Improved Close Range Depth reported a closer minimum valid depth, as expected." << std::endl;
265+
else
266+
std::cout << "No closer minimum valid depth observed this run - depends on what's actually "
267+
"in front of the camera." << std::endl;
268+
269+
std::cout << "Restoring original configuration." << std::endl;
270+
close_range_filter.set_composite_option_from(id, original);
271+
}
272+
}
273+
}
274+
141275
return EXIT_SUCCESS;
142276
}
143277
catch( const rs2::error & e )

wrappers/python/examples/composite_option_hello.py

Lines changed: 0 additions & 72 deletions
This file was deleted.

wrappers/python/examples/embedded_filters.py

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
#####################################################
77

88

9-
import struct
109
import sys
1110
import pyrealsense2 as rs
1211

@@ -21,10 +20,13 @@
2120
5. For each embedded filter: show supported COMPOSITE options and print current values too -
2221
these are a completely separate identity space from the ordinary (scalar) options above (see
2322
rs2_composite_option_id in rs_composite_option.h): a single multi-field control exchanged
24-
atomically, in ONE UVC transaction. get/set_composite_option() hand back/take raw bytes rather
25-
than a typed value - Python has no templates, so (mirroring the C++/C99 samples under
26-
examples/composite-option and examples/C/composite-ctl) the caller unpacks/packs those bytes
27-
against the option's documented wire layout with the `struct` module.
23+
atomically, in ONE UVC transaction. Each id known to this example is registered below against
24+
its typed accessor method (e.g. get_improved_close_range_control()) - the Python equivalent of
25+
the C++ wrapper's get_composite_option_as<T>() template call (see
26+
wrappers/python/pyrs_options.cpp) - so what comes back is a real object bound field-for-field
27+
against the actual C struct, not raw bytes. Fields are then read off that object by
28+
introspection, so this example carries no hand-maintained wire layout of its own: no struct
29+
format string, no byte offsets, no field list to keep in sync by hand.
2830
'''
2931

3032
def list_embedded_filter_options(embedded_filter):
@@ -35,25 +37,27 @@ def list_embedded_filter_options(embedded_filter):
3537
print("\n")
3638

3739

38-
# Known wire layouts for composite options this example knows how to interpret - there is no
39-
# generic "any composite option" cast (the SDK ships no per-id dispatch), so a new composite
40-
# option needs its layout added here too, same as every other composite-option sample's typed
41-
# dispatch. See rs_hkr_improved_close_range_control.h / rs_hkr_temporal_filter_dpp.h for the documented layouts.
42-
_COMPOSITE_OPTION_LAYOUTS = {
43-
rs.composite_option_id.hkr_improved_close_range_control: (
44-
# dppc_header (version, flags, ctl_id, param_count, param_type) shared by the whole HKR
45-
# DPP control family, then Improved Close Range's 7 logical fields, then 1 reserved (always 0) slot.
46-
'<BBHBBiiiiiiii',
47-
['version', 'flags', 'ctl_id', 'param_count', 'param_type',
48-
'enable', 'filter_type', 'downscale_ratio', 'shift_mode', 'shift_pixels',
49-
'threshold_mode', 'threshold_mm', 'reserved0']),
50-
rs.composite_option_id.hkr_temporal_filter_dpp: (
51-
# No shared header on this one - just its own 4 fields, tightly packed.
52-
'<ifii',
53-
['enabled', 'smooth_alpha', 'smooth_delta', 'persistency_index']),
40+
# Typed accessor method registered per known composite-option id - the SDK ships no generic "any
41+
# composite option" cast (there is no per-id dispatch table, in C++ or Python), so a new composite
42+
# option still needs an entry here, same as every other composite-option sample in this repo
43+
# dispatches by known id. Unlike a raw wire-layout table though, nothing here names a byte offset
44+
# or format string: the registered method returns a real typed object (see
45+
# rs.improved_close_range_control / rs.temporal_filter_dpp_config in pyrs_options.cpp), and
46+
# _typed_fields() below reads its fields back by introspection.
47+
_COMPOSITE_OPTION_ACCESSORS = {
48+
rs.composite_option_id.hkr_improved_close_range_control: 'get_improved_close_range_control',
49+
rs.composite_option_id.hkr_temporal_filter_dpp: 'get_temporal_filter_dpp_config',
5450
}
5551

5652

53+
def _typed_fields(value):
54+
'''The real, public fields on a typed composite-option object, discovered from the object
55+
itself rather than a hand-maintained name list. `header` is skipped - it's wire-transport
56+
framing (version/ctl_id/param_count/...), not one of the control's own logical fields.'''
57+
return [name for name in dir(value)
58+
if not name.startswith('_') and name != 'header' and not callable(getattr(value, name))]
59+
60+
5761
def list_embedded_filter_composite_options(embedded_filter):
5862
composite_ids = embedded_filter.get_supported_composite_options()
5963
if not composite_ids:
@@ -65,8 +69,14 @@ def list_embedded_filter_composite_options(embedded_filter):
6569
print(" {}:".format(repr(id)))
6670
print(" read-only: {}".format(embedded_filter.is_composite_option_read_only(id)))
6771
print(" description: \"{}\"".format(embedded_filter.get_composite_option_description(id)))
72+
73+
accessor_name = _COMPOSITE_OPTION_ACCESSORS.get(id)
74+
if accessor_name is None:
75+
print(" (no typed accessor registered for this id in this example - see "
76+
"get_composite_option()/get_composite_option_range() for the untyped, raw-bytes API)")
77+
continue
6878
try:
69-
raw = embedded_filter.get_composite_option(id)
79+
value = getattr(embedded_filter, accessor_name)(id)
7080
except RuntimeError as e:
7181
# Registered but not actually functional on this device/FW is a real, expected
7282
# outcome (supports_composite_option()/get_supported_composite_options() only
@@ -75,15 +85,8 @@ def list_embedded_filter_composite_options(embedded_filter):
7585
print(" SKIPPED (registered but not functional on this device/FW): {}".format(e))
7686
continue
7787

78-
layout = _COMPOSITE_OPTION_LAYOUTS.get(id)
79-
if layout is None:
80-
print(" raw bytes ({}): {}".format(len(raw), raw.hex()))
81-
continue
82-
83-
fmt, field_names = layout
84-
values = struct.unpack(fmt, raw)
85-
for name, value in zip(field_names, values):
86-
print(" {} = {}".format(name, value))
88+
for name in _typed_fields(value):
89+
print(" {} = {}".format(name, getattr(value, name)))
8790
print("\n")
8891

8992

0 commit comments

Comments
 (0)