Skip to content

Commit 09cf0f6

Browse files
authored
PR #14348 from AviaAv: Create iq_helper.py, detect page on depth test
2 parents a389ca2 + 8b1c59b commit 09cf0f6

File tree

3 files changed

+151
-102
lines changed

3 files changed

+151
-102
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# License: Apache 2.0. See LICENSE file in root directory.
2+
# Copyright(c) 2025 RealSense, Inc. All Rights Reserved.
3+
4+
from rspy import log, test
5+
import numpy as np
6+
import cv2
7+
import time
8+
import pyrealsense2 as rs
9+
10+
11+
# standard size to display / process the target
12+
WIDTH = 1280
13+
HEIGHT = 720
14+
15+
# transformation matrix from frame to aligned region of interest
16+
M = None
17+
18+
def compute_homography(pts):
19+
"""
20+
Given 4 points (the detected ArUco marker centers), find the 3×3 matrix that stretches/rotates
21+
the four ArUco points so they become the corners of an A4 page (used to "flatten" the page in an image)
22+
"""
23+
pts_sorted = sorted(pts, key=lambda p: (p[1], p[0]))
24+
top_left, top_right = sorted(pts_sorted[:2], key=lambda p: p[0])
25+
bottom_left, bottom_right = sorted(pts_sorted[2:], key=lambda p: p[0])
26+
27+
src = np.array([top_left, top_right, bottom_right, bottom_left], dtype=np.float32)
28+
dst = np.array([[0,0],[WIDTH-1,0],[WIDTH-1,HEIGHT-1],[0,HEIGHT-1]], dtype=np.float32)
29+
M = cv2.getPerspectiveTransform(src, dst)
30+
return M # we later use M to get our roi
31+
32+
33+
def detect_a4_page(img, required_ids):
34+
"""
35+
Detect ArUco markers and return center of each one
36+
Returns None if not all required markers are found
37+
"""
38+
# init aruco detector
39+
aruco = cv2.aruco
40+
dict_type = cv2.aruco.DICT_4X4_1000
41+
dictionary = aruco.getPredefinedDictionary(dict_type)
42+
try:
43+
# new API (OpenCV >= 4.7)
44+
parameters = aruco.DetectorParameters()
45+
detector = aruco.ArucoDetector(dictionary, parameters)
46+
corners, ids, _ = detector.detectMarkers(img)
47+
except AttributeError:
48+
# legacy API (OpenCV <= 4.6) - used on some of our machines
49+
parameters = aruco.DetectorParameters_create()
50+
corners, ids, _ = aruco.detectMarkers(img, dictionary, parameters=parameters)
51+
52+
if ids is None or not all(rid in ids for rid in required_ids):
53+
return None
54+
55+
id_to_corner = dict(zip(ids.flatten(), corners)) # map id to corners
56+
values = [id_to_corner[rid][0].mean(axis=0) for rid in required_ids] # for each required id, get center of marker coords
57+
58+
return np.array(values, dtype=np.float32)
59+
60+
61+
def find_roi_location(pipeline, required_ids, DEBUG_MODE=False):
62+
"""
63+
Returns a matrix that transforms from frame to region of interest
64+
This matrix will later be used with cv2.warpPerspective()
65+
"""
66+
global M
67+
# stream until page found
68+
page_pts = None
69+
start_time = time.time()
70+
while page_pts is None and time.time() - start_time < 5:
71+
frames = pipeline.wait_for_frames()
72+
aruco_detectable_streams = (rs.stream.color, rs.stream.infrared) # we need one of those streams to detect ArUco markers
73+
frame = next(f for f in frames if f.get_profile().stream_type() in aruco_detectable_streams)
74+
img_bgr = np.asanyarray(frame.get_data())
75+
76+
if DEBUG_MODE:
77+
cv2.imshow("PageDetect - waiting for page", img_bgr)
78+
cv2.waitKey(1)
79+
80+
page_pts = detect_a4_page(img_bgr, required_ids)
81+
82+
if page_pts is None:
83+
log.e("Failed to detect page within timeout")
84+
test.fail()
85+
raise Exception("Page not found")
86+
87+
# page found - use it to calculate transformation matrix from frame to region of interest
88+
M = compute_homography(page_pts)
89+
cv2.destroyAllWindows()
90+
return M, page_pts
91+
92+
def get_roi_from_frame(frame):
93+
"""
94+
Apply the previously computed transformation matrix to the given frame
95+
to get the region of interest (A4 page)
96+
"""
97+
global M
98+
if M is None:
99+
raise Exception("Transformation matrix not computed yet")
100+
101+
np_frame = np.asanyarray(frame.get_data())
102+
warped = cv2.warpPerspective(np_frame, M, (WIDTH, HEIGHT)) # using A4 size for its ratio
103+
return warped

unit-tests/live/image-quality/test-basic-color.py

Lines changed: 9 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,13 @@
77
from rspy import log, test
88
import numpy as np
99
import cv2
10-
import time
10+
from iq_helper import find_roi_location, get_roi_from_frame, WIDTH, HEIGHT
1111

1212
NUM_FRAMES = 100 # Number of frames to check
1313
COLOR_TOLERANCE = 60 # Acceptable per-channel deviation in RGB values
1414
FRAMES_PASS_THRESHOLD =0.8 # Percentage of frames that needs to pass
1515
DEBUG_MODE = False
1616

17-
# A4 size in pixels at 96 DPI
18-
A4_WIDTH = 794
19-
A4_HEIGHT = 1123
20-
2117
# expected colors (insertion order -> mapped row-major to 3x3 grid)
2218
expected_colors = {
2319
"red": (132, 60, 60),
@@ -35,38 +31,23 @@
3531

3632
# we are given a 3x3 grid, we split it using 2 vertical and 2 horizontal separators
3733
# we also calculate the center of each grid cell for sampling from it for the test
38-
xs = [1.5 * A4_WIDTH / 6.0, A4_WIDTH / 2.0, 4.5 * A4_WIDTH / 6.0]
39-
ys = [1.5 * A4_HEIGHT / 6.0, A4_HEIGHT / 2.0, 4.5 * A4_HEIGHT / 6.0]
34+
xs = [1.5 * WIDTH / 6.0, WIDTH / 2.0, 4.5 * WIDTH / 6.0]
35+
ys = [1.5 * HEIGHT / 6.0, HEIGHT / 2.0, 4.5 * HEIGHT / 6.0]
4036
centers = [(x, y) for y in ys for x in xs]
4137

4238
dev, ctx = test.find_first_device_or_exit()
4339

4440
def is_color_close(actual, expected, tolerance):
4541
return all(abs(int(a) - int(e)) <= tolerance for a, e in zip(actual, expected))
4642

47-
def compute_homography(pts):
48-
"""
49-
Given 4 points (the detected ArUco marker centers), find the 3×3 matrix that stretches/rotates
50-
the four ArUco points so they become the corners of an A4 page (used to "flatten" the page in an image)
51-
"""
52-
pts_sorted = sorted(pts, key=lambda p: (p[1], p[0]))
53-
top_left, top_right = sorted(pts_sorted[:2], key=lambda p: p[0])
54-
bottom_left, bottom_right = sorted(pts_sorted[2:], key=lambda p: p[0])
55-
56-
src = np.array([top_left, top_right, bottom_right, bottom_left], dtype=np.float32)
57-
dst = np.array([[0,0],[A4_WIDTH-1,0],[A4_WIDTH-1,A4_HEIGHT-1],[0,A4_HEIGHT-1]], dtype=np.float32)
58-
M = cv2.getPerspectiveTransform(src, dst)
59-
return M # we later use M to get our roi
60-
61-
6243
def draw_debug(frame_bgr, a4_page_bgr):
6344
"""
6445
Simple debug view:
6546
- left: camera frame
6647
- right: focused view on the A4 page with grid and color names
6748
"""
68-
vertical_lines = [A4_WIDTH / 3.0, 2.0 * A4_WIDTH / 3.0]
69-
horizontal_lines = [A4_HEIGHT / 3.0, 2.0 * A4_HEIGHT / 3.0]
49+
vertical_lines = [WIDTH / 3.0, 2.0 * WIDTH / 3.0]
50+
horizontal_lines = [HEIGHT / 3.0, 2.0 * HEIGHT / 3.0]
7051
H, W = a4_page_bgr.shape[:2]
7152

7253
# draw grid on a4 page image
@@ -93,63 +74,6 @@ def draw_debug(frame_bgr, a4_page_bgr):
9374
return np.hstack([left, right])
9475

9576

96-
def detect_a4_page(img, dict_type=cv2.aruco.DICT_4X4_1000, required_ids=(0,1,2,3)):
97-
"""
98-
Detect ArUco markers and return center of each one
99-
Returns None if not all required markers are found
100-
"""
101-
# init aruco detector
102-
aruco = cv2.aruco
103-
dictionary = aruco.getPredefinedDictionary(dict_type)
104-
try:
105-
# new API (OpenCV >= 4.7)
106-
parameters = aruco.DetectorParameters()
107-
detector = aruco.ArucoDetector(dictionary, parameters)
108-
corners, ids, _ = detector.detectMarkers(img)
109-
except AttributeError:
110-
# legacy API (OpenCV <= 4.6) - used on some of our machines
111-
parameters = aruco.DetectorParameters_create()
112-
corners, ids, _ = aruco.detectMarkers(img, dictionary, parameters=parameters)
113-
114-
if ids is None or not all(rid in ids for rid in required_ids):
115-
return None
116-
117-
id_to_corner = dict(zip(ids.flatten(), corners)) # map id to corners
118-
values = [id_to_corner[rid][0].mean(axis=0) for rid in required_ids] # for each required id, get center of marker coords
119-
120-
return np.array(values, dtype=np.float32)
121-
122-
123-
def find_roi_location(pipeline):
124-
"""
125-
Returns a matrix that transforms from frame to region of interest
126-
This matrix will later be used with cv2.warpPerspective()
127-
"""
128-
# stream until page found
129-
page_pts = None
130-
start_time = time.time()
131-
while page_pts is None and time.time() - start_time < 5:
132-
frames = pipeline.wait_for_frames()
133-
color_frame = frames.get_color_frame()
134-
img_bgr = np.asanyarray(color_frame.get_data())
135-
136-
if DEBUG_MODE:
137-
cv2.imshow("PageDetect - waiting for page", img_bgr)
138-
cv2.waitKey(1)
139-
140-
page_pts = detect_a4_page(img_bgr)
141-
142-
if page_pts is None:
143-
log.e("Failed to detect page within timeout")
144-
test.fail()
145-
raise Exception("Page not found")
146-
147-
# page found - use it to calculate transformation matrix from frame to region of interest
148-
M = compute_homography(page_pts)
149-
cv2.destroyAllWindows()
150-
return M, page_pts
151-
152-
15377
def is_cfg_supported(resolution, fps):
15478
color_sensor = dev.first_color_sensor()
15579
for p in color_sensor.get_stream_profiles():
@@ -170,35 +94,32 @@ def run_test(resolution, fps):
17094
for i in range(30): # skip initial frames
17195
pipeline.wait_for_frames()
17296
try:
173-
17497
# find region of interest (page) and get the transformation matrix
175-
# page_pts is only used for debug display
176-
M, page_pts = find_roi_location(pipeline)
98+
find_roi_location(pipeline, (0, 1, 2, 3), DEBUG_MODE) # markers in the lab are 0,1,2,3
17799

178100
# sampling loop
179101
for i in range(NUM_FRAMES):
180102
frames = pipeline.wait_for_frames()
181103
color_frame = frames.get_color_frame()
182104
img_bgr = np.asanyarray(color_frame.get_data())
183105

184-
# use M to get the region of interest - our colored grid printed in the lab
185-
a4_bgr = cv2.warpPerspective(img_bgr, M, (A4_WIDTH, A4_HEIGHT))
106+
color_frame_roi = get_roi_from_frame(color_frame)
186107

187108
# sample each grid center and compare to expected color by row-major insertion order
188109
for idx, (x, y) in enumerate(centers):
189110
color = color_names[idx] if idx < len(color_names) else str(idx)
190111
expected_rgb = expected_colors[color]
191112
x = int(round(x))
192113
y = int(round(y))
193-
b, g, r = (int(v) for v in a4_bgr[y, x]) # stream is BGR, convert to RGB
114+
b, g, r = (int(v) for v in color_frame_roi[y, x]) # stream is BGR, convert to RGB
194115
pixel = (r, g, b)
195116
if is_color_close(pixel, expected_rgb, COLOR_TOLERANCE):
196117
color_match_count[color] += 1
197118
else:
198119
log.d(f"Frame {i} - {color} at ({x},{y}) sampled: {pixel} too far from expected {expected_rgb}")
199120

200121
if DEBUG_MODE:
201-
dbg = draw_debug(img_bgr, a4_bgr)
122+
dbg = draw_debug(img_bgr, color_frame_roi)
202123
cv2.imshow("PageDetect - camera | A4", dbg)
203124
cv2.waitKey(1)
204125

unit-tests/live/image-quality/test-basic-depth.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,18 @@
33

44
# test:device D400*
55
# test:donotrun
6+
67
import pyrealsense2 as rs
78
from rspy import log, test
89
import numpy as np
10+
import cv2
911
import time
12+
from iq_helper import find_roi_location, get_roi_from_frame
1013

11-
NUM_FRAMES = 10 # Number of frames to check
14+
NUM_FRAMES = 100 # Number of frames to check
1215
DEPTH_TOLERANCE = 0.05 # Acceptable deviation from expected depth in meters
1316
FRAMES_PASS_THRESHOLD =0.8 # Percentage of frames that needs to pass
14-
15-
# Known pixel positions and expected depth values (in meters)
16-
depth_points = {
17-
"front": ((538, 315), 0.524 ),
18-
"middle": ((437, 252), 0.655),
19-
"back": ((750, 63), 1.058 )
20-
}
17+
DEBUG_MODE = False
2118

2219
test.start("Basic Depth Image Quality Test")
2320

@@ -26,24 +23,35 @@
2623

2724
pipeline = rs.pipeline(ctx)
2825
cfg = rs.config()
29-
cfg.enable_stream(rs.stream.depth, 848, 480, rs.format.z16, 30)
26+
cfg.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30)
27+
cfg.enable_stream(rs.stream.infrared, 1, 640, 480, rs.format.y8, 30) # needed for finding the ArUco markers
3028
profile = pipeline.start(cfg)
31-
frames = pipeline.wait_for_frames()
3229
time.sleep(2)
3330

3431
depth_sensor = profile.get_device().first_depth_sensor()
3532
depth_scale = depth_sensor.get_depth_scale()
3633

37-
depth_passes = {name: 0 for name in depth_points}
38-
34+
# find region of interest (page) and get the transformation matrix
35+
find_roi_location(pipeline, (4,5,6,7), DEBUG_MODE) # markers in the lab are 4,5,6,7
36+
depth_passes = {}
3937
for i in range(NUM_FRAMES):
4038
frames = pipeline.wait_for_frames()
4139
depth_frame = frames.get_depth_frame()
40+
infrared_frame = frames.get_infrared_frame()
4241
if not depth_frame:
4342
continue
4443

45-
depth_image = np.asanyarray(depth_frame.get_data())
44+
depth_image = get_roi_from_frame(depth_frame)
4645

46+
# Known pixel positions and expected depth values (in meters)
47+
# Using temporary values until setup in lab is completed
48+
h, w = depth_image.shape
49+
depth_points = {
50+
"cube": ((h // 2, w // 2), 0.45), # center of page, cube at 0.45m
51+
"background": ((h // 2, int(w * 0.1)), 0.6), # left edge, background at 0.6m
52+
}
53+
if not depth_passes:
54+
depth_passes = {name: 0 for name in depth_points}
4755
for point_name, ((x, y), expected_depth) in depth_points.items():
4856
raw_depth = depth_image[y, x]
4957
depth_value = raw_depth * depth_scale # Convert to meters
@@ -53,14 +61,31 @@
5361
else:
5462
log.d(f"Frame {i} - {point_name} at ({x},{y}): {depth_value:.3f}m ≠ {expected_depth:.3f}m")
5563

64+
if DEBUG_MODE:
65+
# display IR image along with transformed view of IR, get_roi_from_frame(infrared_frame)
66+
infrared_np = np.asanyarray(infrared_frame.get_data())
67+
w, h = infrared_np.shape
68+
dbg_resized = cv2.resize(get_roi_from_frame(infrared_frame), (h, w))
69+
70+
dbg = np.hstack([infrared_np, dbg_resized])
71+
cv2.imshow("Depth IQ - IR | Depth", dbg)
72+
cv2.waitKey(1)
73+
74+
# wait for close
75+
if DEBUG_MODE:
76+
cv2.waitKey(0)
77+
5678
# Check that each point passed the threshold
5779
min_passes = int(NUM_FRAMES * FRAMES_PASS_THRESHOLD)
5880
for point_name, count in depth_passes.items():
5981
log.i(f"{point_name.title()} passed in {count}/{NUM_FRAMES} frames")
6082
test.check(count >= min_passes)
6183

6284
except Exception as e:
63-
test.unexpected_exception(e)
85+
test.fail()
86+
raise e
87+
finally:
88+
cv2.destroyAllWindows()
6489

6590
pipeline.stop()
6691
test.finish()

0 commit comments

Comments
 (0)