|
| 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() |
0 commit comments