Skip to content

Commit 8efb86f

Browse files
authored
PR #14420 from ttkhuong: add test-fps-manual-exposure.py
2 parents e5faf0b + ce615f7 commit 8efb86f

File tree

2 files changed

+167
-52
lines changed

2 files changed

+167
-52
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# License: Apache 2.0. See LICENSE file in root directory.
2+
# Copyright(c) 2025 RealSense, Inc. All Rights Reserved.
3+
4+
# Currently, we exclude D555 as it's failing
5+
# test:device each(D400*)
6+
# test:device each(D500*) !D555
7+
# test:donotrun:!nightly
8+
9+
import pyrealsense2 as rs
10+
from rspy.stopwatch import Stopwatch
11+
from rspy import test, log
12+
import time
13+
import platform
14+
import fps_helper
15+
16+
# This test mirrors test-fps.py but forces manual exposure where supported
17+
# Start depth + color streams and measure frame frequency using sensor API.
18+
# Verify that actual fps is as requested
19+
20+
def set_exposure_half_frame_time(sensor, requested_fps):
21+
"""Set sensor exposure to half the frame time for the given requested_fps.
22+
Returns the exposure value that was set (or None if not supported/failed).
23+
"""
24+
if not sensor.supports(rs.option.exposure):
25+
return None
26+
try:
27+
r = sensor.get_option_range(rs.option.exposure)
28+
# frame time in seconds
29+
frame_time_s = 1.0 / float(requested_fps)
30+
desired_s = frame_time_s / 2.0
31+
32+
# Decide units based on sensor type: depth -> microseconds; RGB -> 100 microseconds units
33+
is_color = False
34+
try:
35+
for p in sensor.profiles:
36+
if p.stream_type() == rs.stream.color:
37+
is_color = True
38+
break
39+
except Exception:
40+
# fallback: assume depth unless explicitly color
41+
is_color = False
42+
43+
if is_color:
44+
# RGB sensors: many backends treat exposure as units of 100 microseconds
45+
desired_units = desired_s * 1e6 / 100.0 # convert seconds -> 100-microsecond units
46+
else:
47+
# Depth sensors: use microseconds directly
48+
desired_units = desired_s * 1e6
49+
50+
# clamp to reported range
51+
if desired_units < r.min:
52+
desired_units = r.min
53+
if desired_units > r.max:
54+
desired_units = r.max
55+
56+
# Set the option (use float/int as appropriate)
57+
try:
58+
sensor.set_option(rs.option.exposure, desired_units)
59+
except Exception:
60+
sensor.set_option(rs.option.exposure, int(desired_units))
61+
return desired_s * 1e6 # return in microseconds
62+
except Exception:
63+
return None
64+
65+
66+
delta_Hz = 1
67+
tested_fps = [5, 6, 15, 30, 60, 90]
68+
time_to_test_fps = [25, 20, 13, 10, 5, 4]
69+
test.check_equal( len(tested_fps), len(time_to_test_fps) )
70+
71+
dev, _ = test.find_first_device_or_exit()
72+
product_line = dev.get_info(rs.camera_info.product_line)
73+
camera_name = dev.get_info(rs.camera_info.name)
74+
firmware_version = dev.get_info(rs.camera_info.firmware_version)
75+
serial = dev.get_info(rs.camera_info.serial_number)
76+
log.i(f"Device: {camera_name}, product_line: {product_line}, serial: {serial}, firmware: {firmware_version}")
77+
78+
#####################################################################################################
79+
test.start("Testing depth fps (manual exposure) " + product_line + " device - "+ platform.system() + " OS")
80+
81+
ds = dev.first_depth_sensor()
82+
# Prepare depth sensor for manual exposure if supported
83+
if product_line == "D400":
84+
if ds.supports(rs.option.enable_auto_exposure):
85+
# disable auto exposure to force manual exposure
86+
ds.set_option(rs.option.enable_auto_exposure, 0)
87+
88+
for i in range(len(tested_fps)):
89+
requested_fps = tested_fps[i]
90+
try:
91+
dp = next(p for p in ds.profiles
92+
if p.fps() == requested_fps
93+
and p.stream_type() == rs.stream.depth
94+
and p.format() == rs.format.z16
95+
and ((p.as_video_stream_profile().height() == 720 and p.fps() != 60) if "D585S" in camera_name else True))
96+
97+
except StopIteration:
98+
log.i("Requested fps: {:.1f} [Hz], not supported".format(requested_fps))
99+
else:
100+
# set exposure to half frame time for this requested fps if supported
101+
exposure_val = set_exposure_half_frame_time(ds, requested_fps)
102+
# use shared fps helper which expects a dict of {sensor: [profile]}
103+
fps_helper.TIME_TO_COUNT_FRAMES = time_to_test_fps[i]
104+
fps_dict = fps_helper.measure_fps({ds: [dp]})
105+
fps = fps_dict.get(dp.stream_name(), 0)
106+
log.i("Exposure: {:.1f} [msec], requested fps: {:.1f} [Hz], actual fps: {:.1f} [Hz]".format((exposure_val or 0)/1000, requested_fps, fps))
107+
delta_Hz = requested_fps * 0.05 # Validation KPI is 5%
108+
test.check(fps <= (requested_fps + delta_Hz) and fps >= (requested_fps - delta_Hz))
109+
test.finish()
110+
111+
112+
#####################################################################################################
113+
test.start("Testing color fps (manual exposure) " + product_line + " device - "+ platform.system() + " OS")
114+
115+
product_name = dev.get_info(rs.camera_info.name)
116+
cs = None
117+
try:
118+
cs = dev.first_color_sensor()
119+
except RuntimeError as rte:
120+
if 'D421' not in product_name and 'D405' not in product_name: # Cameras with no color sensor may fail.
121+
test.unexpected_exception()
122+
123+
if cs:
124+
# Try to force manual exposure on color sensor
125+
if product_line == "D400":
126+
if cs.supports(rs.option.enable_auto_exposure):
127+
cs.set_option(rs.option.enable_auto_exposure, 0)
128+
if cs.supports(rs.option.auto_exposure_priority):
129+
cs.set_option(rs.option.auto_exposure_priority, 0) # AE priority should be 0 for constant FPS
130+
131+
for i in range(len(tested_fps)):
132+
requested_fps = tested_fps[i]
133+
try:
134+
# collect matching color profiles and pick median resolution
135+
candidates = [p for p in cs.profiles
136+
if p.fps() == requested_fps
137+
and p.stream_type() == rs.stream.color
138+
and p.format() == rs.format.rgb8]
139+
if not candidates:
140+
raise StopIteration
141+
candidates.sort(key=lambda pr: pr.as_video_stream_profile().width() * pr.as_video_stream_profile().height())
142+
cp = candidates[len(candidates)//2]
143+
144+
except StopIteration:
145+
log.i("Requested fps: {:.1f} [Hz], not supported".format(requested_fps))
146+
else:
147+
# set exposure to half frame time for this requested fps if supported
148+
exposure_val = set_exposure_half_frame_time(cs, requested_fps)
149+
fps_dict = fps_helper.measure_fps({cs: [cp]})
150+
fps = fps_dict.get(cp.stream_name(), 0)
151+
log.i("Exposure: {:.1f} [msec], requested fps: {:.1f} [Hz], actual fps: {:.1f} [Hz]".format((exposure_val or 0)/1000, requested_fps, fps))
152+
delta_Hz = requested_fps * (0.10 if requested_fps == 5 else 0.05) # Validation KPI is 5% for all non 5 FPS rate
153+
test.check(fps <= (requested_fps + delta_Hz) and fps >= (requested_fps - delta_Hz))
154+
155+
test.finish()
156+
157+
#####################################################################################################
158+
test.print_results_and_exit()

unit-tests/live/frames/test-fps.py

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,58 +11,11 @@
1111
from rspy import test, log
1212
import time
1313
import platform
14+
import fps_helper
1415

1516
# Start depth + color streams and measure frame frequency using sensor API.
1617
# Verify that actual fps is as requested
1718

18-
global first_frame_seconds
19-
20-
def measure_fps(sensor, profile, seconds_to_count_frames = 10):
21-
"""
22-
Wait a few seconds to be sure that frames are at steady state after start
23-
Count number of received frames for seconds_to_count_frames seconds and compare actual fps to requested fps
24-
"""
25-
seconds_till_steady_state = 4
26-
27-
steady_state = False
28-
first_frame_received = False
29-
frames_received = 0
30-
first_frame_stopwatch = Stopwatch()
31-
prev_frame_number = 0
32-
33-
def frame_cb(frame):
34-
global first_frame_seconds
35-
nonlocal steady_state, frames_received, first_frame_received, prev_frame_number
36-
current_frame_number = frame.get_frame_number()
37-
if not first_frame_received:
38-
first_frame_seconds = first_frame_stopwatch.get_elapsed()
39-
first_frame_received = True
40-
else:
41-
if current_frame_number > prev_frame_number + 1:
42-
log.w( f'Frame drop detected. Current frame number {current_frame_number} previous was {prev_frame_number}' )
43-
if steady_state:
44-
frames_received += 1
45-
prev_frame_number = current_frame_number
46-
47-
sensor.open(profile)
48-
sensor.start(frame_cb)
49-
first_frame_stopwatch.reset()
50-
51-
time.sleep(seconds_till_steady_state)
52-
53-
steady_state = True
54-
55-
time.sleep(seconds_to_count_frames) # Time to count frames
56-
57-
steady_state = False # Stop counting
58-
59-
sensor.stop()
60-
sensor.close()
61-
62-
fps = frames_received / seconds_to_count_frames
63-
return fps
64-
65-
6619
delta_Hz = 1
6720
tested_fps = [5, 6, 15, 30, 60, 90]
6821
time_to_test_fps = [25, 20, 13, 10, 5, 4]
@@ -96,8 +49,10 @@ def frame_cb(frame):
9649
except StopIteration:
9750
log.i("Requested fps: {:.1f} [Hz], not supported".format(requested_fps))
9851
else:
99-
fps = measure_fps(ds, dp, time_to_test_fps[i])
100-
log.i("Requested fps: {:.1f} [Hz], actual fps: {:.1f} [Hz]. Time to first frame {:.6f}".format(requested_fps, fps, first_frame_seconds))
52+
fps_helper.TIME_TO_COUNT_FRAMES = time_to_test_fps[i]
53+
fps_dict = fps_helper.measure_fps({ds: [dp]})
54+
fps = fps_dict.get(dp.stream_name(), 0)
55+
log.i("Requested fps: {:.1f} [Hz], actual fps: {:.1f} [Hz]".format(requested_fps, fps))
10156
delta_Hz = requested_fps * 0.05 # Validation KPI is 5%
10257
test.check(fps <= (requested_fps + delta_Hz) and fps >= (requested_fps - delta_Hz))
10358
test.finish()
@@ -133,8 +88,10 @@ def frame_cb(frame):
13388
except StopIteration:
13489
log.i("Requested fps: {:.1f} [Hz], not supported".format(requested_fps))
13590
else:
136-
fps = measure_fps(cs, cp, time_to_test_fps[i])
137-
log.i("Requested fps: {:.1f} [Hz], actual fps: {:.1f} [Hz]. Time to first frame {:.6f}".format(requested_fps, fps, first_frame_seconds))
91+
fps_helper.TIME_TO_COUNT_FRAMES = time_to_test_fps[i]
92+
fps_dict = fps_helper.measure_fps({cs: [cp]})
93+
fps = fps_dict.get(cp.stream_name(), 0)
94+
log.i("Requested fps: {:.1f} [Hz], actual fps: {:.1f} [Hz]".format(requested_fps, fps))
13895
delta_Hz = requested_fps * (0.10 if requested_fps == 5 else 0.05) # Validation KPI is 5% for all non 5 FPS rate
13996
test.check(fps <= (requested_fps + delta_Hz) and fps >= (requested_fps - delta_Hz))
14097

0 commit comments

Comments
 (0)