test_camera.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- python -*-
3 
4 # SPDX-License-Identifier: Apache-2.0
5 # Copyright (C) 2021 ifm electronic, gmbh
6 
7 
8 import sys
9 import time
10 import unittest
11 import rospy
12 import rostest
13 import numpy as np
14 import tf2_ros
15 import tf2_geometry_msgs
16 from sensor_msgs.msg import Image
17 from geometry_msgs.msg import PointStamped
18 from cv_bridge import CvBridge, CvBridgeError
19 from ifm3d.msg import Extrinsics
20 
21 # XXX: TP debugging
22 #np.set_printoptions(threshold=np.nan)
23 
24 class TestCamera(unittest.TestCase):
25  """
26  TP: 14 May 2018 - heavily adapted from my old o3d3xx-ros test of the same
27  name.
28 
29  This class tests the following:
30  - Getting data from the camera
31  - Computing the Cartesian data and comparing it to ground truth
32  - To do the comparison to ground truth, the computed cartesian data
33  must be transformed, to do that we use the tf2 API. It follows that
34  this indirectly is testing our tf2 static publisher which is publishing
35  the transform between the optical and camera frames
36 
37  NOTE: The camera h/w is needed to run this test.
38  """
39 
40  def __init__(self, *args):
41  super(TestCamera, self).__init__(*args)
42  self.success_ = False
43 
44  self.cloud_ = None # ground truth for cartesian computation
45  self.rdis_ = None
46  self.uvec_ = None
47  self.extrinsics_ = None
48  self.conf_ = None
49 
50  def test_camera(self):
51  time.sleep(5.0) # <-- wait for rosmaster and ifm3d nodelet
52  rospy.init_node('test_camera')
53 
54  self.bridge_ = CvBridge()
55  self.rdis_sub_ = \
56  rospy.Subscriber("/ifm3d/camera/distance", Image, self.image_cb,
57  queue_size=None, callback_args="rdis")
58  self.cloud_sub_ = \
59  rospy.Subscriber("/ifm3d/camera/xyz_image", Image, self.image_cb,
60  queue_size=None, callback_args="cloud")
61  self.uvec_sub_ = \
62  rospy.Subscriber("/ifm3d/camera/unit_vectors", Image, self.image_cb,
63  queue_size=None, callback_args="uvec")
64  self.extrinsics_sub_ = \
65  rospy.Subscriber("/ifm3d/camera/extrinsics", Extrinsics,
66  self.extrinsics_cb, queue_size=None)
67  self.conf_sub_ = \
68  rospy.Subscriber("/ifm3d/camera/confidence", Image, self.image_cb,
69  queue_size=None, callback_args="conf")
70 
71  # If it takes more than 10 secs to run our test, something is wrong.
72  timeout_t = time.time() + 10.0
73  rate = rospy.Rate(10.0)
74  while ((not rospy.is_shutdown()) and
75  (not self.success_) and
76  (time.time() < timeout_t)):
77  # Make sure we have all the data we need to compute
78  if ((self.rdis_ is not None) and
79  (self.cloud_ is not None) and
80  (self.uvec_ is not None) and
81  (self.extrinsics_ is not None) and
82  (self.conf_ is not None)):
83 
84  # Make sure the cloud, conf, and rdis are from the same image
85  # acquisition.
86  d = self.rdis_.header.stamp - self.cloud_.header.stamp
87  if d.to_sec() == 0:
88  d2 = self.rdis_.header.stamp - self.conf_.header.stamp
89  if d2.to_sec() == 0:
90  self.success_ = self.compute_cartesian()
91  break
92  else:
93  # get new data
94  self.rdis_ = None
95  self.cloud_ = None
96  self.conf_ = None
97  else:
98  # get new data
99  self.rdis_ = None
100  self.cloud_ = None
101  self.conf_ = None
102 
103  rate.sleep()
104 
105  self.assertTrue(self.success_)
106 
107  def extrinsics_cb(self, data, *args):
108  if self.extrinsics_ is None:
109  self.extrinsics_ = data
110 
111  def image_cb(self, data, *args):
112  im_type = args[0]
113  if im_type == "rdis":
114  if self.rdis_ is None:
115  self.rdis_ = data
116  elif im_type == "cloud":
117  if self.cloud_ is None:
118  self.cloud_ = data
119  elif im_type == "uvec":
120  if self.uvec_ is None:
121  self.uvec_ = data
122  elif im_type == "conf":
123  if self.conf_ is None:
124  self.conf_ = data
125 
126  def compute_cartesian(self):
127  """
128  Computes the Cartesian data from the radial distance image, unit
129  vectors, and extrinsics. Then transforms it to the camera frame using
130  the tf2 api. Once transformed, it will do a pixel-by-pixel comparision
131  to ground truth.
132 
133  Returns a bool indicating the the success of the computation.
134  """
135  rdis = np.array(self.bridge_.imgmsg_to_cv2(self.rdis_))
136  uvec = np.array(self.bridge_.imgmsg_to_cv2(self.uvec_))
137  cloud = np.array(self.bridge_.imgmsg_to_cv2(self.cloud_))
138  conf = np.array(self.bridge_.imgmsg_to_cv2(self.conf_))
139 
140  # split out the camera-computed image planes
141  x_cam = cloud[:,:,0]
142  y_cam = cloud[:,:,1]
143  z_cam = cloud[:,:,2]
144  if ((cloud.dtype == np.float32) or
145  (cloud.dtype == np.float64)):
146  # convert to mm
147  x_cam *= 1000.
148  y_cam *= 1000.
149  z_cam *= 1000.
150 
151  # cast to int16_t
152  x_cam = x_cam.astype(np.int16)
153  y_cam = y_cam.astype(np.int16)
154  z_cam = z_cam.astype(np.int16)
155  else:
156  # camera data are already in mm
157  pass
158 
159  # Get the unit vectors
160  ex = uvec[:,:,0]
161  ey = uvec[:,:,1]
162  ez = uvec[:,:,2]
163 
164  # translation vector from extrinsics
165  tx = self.extrinsics_.tx
166  ty = self.extrinsics_.ty
167  tz = self.extrinsics_.tz
168 
169  # Cast the radial distance image to float
170  rdis_f = rdis.astype(np.float32)
171  if (rdis.dtype == np.float32):
172  # assume rdis was in meters, convert to mm
173  rdis_f *= 1000.
174 
175  # Compute Cartesian
176  x_ = ex * rdis_f + tx
177  y_ = ey * rdis_f + ty
178  z_ = ez * rdis_f + tz
179 
180  # mask out bad pixels from our computed cartesian values
181  bad_mask = (np.bitwise_and(conf, 0x1) == 0x1)
182  x_[bad_mask] = 0.
183  y_[bad_mask] = 0.
184  z_[bad_mask] = 0.
185 
186  # Transform to target frame
187  #
188  # NOTE: This could obviously be vectorized if we exploit our apriori
189  # knowledge of the transform, but we want to test the transform we are
190  # broadcasting via tf and the tf2 api for doing the transformation.
191  #
192  # I agree, this loop is slow and ugly :-( -- mostly b/c using the tf2
193  # interface with our data structures here is kind of wonky
194  #
195  tf_buffer = tf2_ros.Buffer()
196  tf_listener = tf2_ros.TransformListener(tf_buffer)
197  n_rows = x_.shape[0]
198  n_cols = x_.shape[1]
199  x_f = np.zeros((n_rows, n_cols), dtype=np.float32)
200  y_f = np.zeros((n_rows, n_cols), dtype=np.float32)
201  z_f = np.zeros((n_rows, n_cols), dtype=np.float32)
202  for i in range(n_rows):
203  for j in range(n_cols):
204  p = PointStamped()
205  p.header = self.rdis_.header
206  p.point.x = x_[i,j]
207  p.point.y = y_[i,j]
208  p.point.z = z_[i,j]
209  pt = tf_buffer.transform(p, self.cloud_.header.frame_id,
210  rospy.Duration(2.0))
211  x_f[i,j] = pt.point.x
212  y_f[i,j] = pt.point.y
213  z_f[i,j] = pt.point.z
214 
215  # cast to the data type of the point cloud
216  x_i = x_f.astype(np.int16)
217  y_i = y_f.astype(np.int16)
218  z_i = z_f.astype(np.int16)
219 
220  tol = 10 # milli-meters
221  x_mask = np.fabs(x_i - x_cam) > tol
222  y_mask = np.fabs(y_i - y_cam) > tol
223  z_mask = np.fabs(z_i - z_cam) > tol
224 
225  # we are asserting that no pixels are out-of-tolerance
226  # per the computation of the x_,y_,z_mask variables above.
227  self.assertTrue(x_mask.sum() == 0)
228  self.assertTrue(y_mask.sum() == 0)
229  self.assertTrue(z_mask.sum() == 0)
230 
231  # if any of the above asserts fail, the test will error out,
232  # so, we return True here carte blanche
233  return True
234 
235 def main():
236  rostest.rosrun('ifm3d', 'test_camera', TestCamera, sys.argv)
237  return 0
238 
239 if __name__ == '__main__':
240  sys.exit(main())
def extrinsics_cb(self, data, args)
Definition: test_camera.py:107
def __init__(self, args)
Definition: test_camera.py:40
def image_cb(self, data, args)
Definition: test_camera.py:111
def compute_cartesian(self)
Definition: test_camera.py:126


ifm3d_ros_driver
Author(s): CSR ifm sytron
autogenerated on Tue Feb 21 2023 03:13:25