8. ROS1-Depth Camera Applications
8.1 Depth Map Pseudo-Color Processing
8.1.1 Program Flow
First, initialize the node and obtain the RGB image and depth map topic messages.
Next, process the image to generate the pseudo-color image.
Finally, transmit both the color image and pseudo-color image.
8.1.2 Operation Steps
Note
The input command should be case sensitive, and the keywords can be complemented using Tab key.
(1) Double-click
to open the command line terminal, and execute the command below to terminate the auto-start service.
~/.stop_ros.sh
(2) Execute the following command and hit Enter to run the game.
roslaunch jetarm_6dof_rgbd_cam 1_get_depth_rgb_img.launch
(3) If you need to terminate the running program, use short-cut Ctrl+C.
(4) After completing the game experience, you must start the app service (if not started, it will affect the functionality of subsequent app features). In the terminal interface, enter the command and press Enter to launch the app service.
sudo systemctl start start_app_node.service
(5) After the service is initiated successfully, the robot will emit a beeping sound.
8.1.3 Program Outcome
Once the game starts, the robot arm will send the RGB color image and depth image to the terminal. The color will change corresponding to the distance indicated in the depth map.
8.1.4 Launch File Analysis
The Launch file is saved in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /launch/1_get_depth_rgb_img.launch
Initiate base.launch file to invoke the robot arm basic settings.
2 | <include file="$(find jetarm_bringup)/launch/base.launch"/> |
Launch the source code file 1_get_depth_rgb_img.py using the node.
4 5 6 | <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="1_get_depth_rgb_img.py" output="screen" respawn="true"> </node> </launch> |
8.1.5 Python Source Code Analysis
The source code is saved in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /scripts/1_get_depth_rgb_img.py
The program flowchart is as below:
The program primarily involves capturing the RGB image and depth image from the camera. After processing the image, the program will then transmit it to the screen.
Import Feature Pack
To import the necessary modules through the import statement:
cv2 is used for OpenCV image processing.
rospy is utilized for ROS communication.
numpy is employed for array operations.
message_filters is incorporated for implementing message filtering and synchronization, handling sensor data and messages in ROS.
The Image message type is imported and renamed as RosImage.
The SetBool service message type is imported from the std_srvs.srv module.
The fps function module is imported from the vision_utils module for frame rate calculation.
queue is used for queue-related operations in multi-threading.
The ROS message type named MultiRawIdPosDur is imported from hiwonder_interfaces.msg for defining custom ROS messages to facilitate data transmission and communication within the ROS system.
The servo control module is imported from jetarm_sdk.
7 8 9 10 11 12 13 14 15 16 | import cv2 import rospy import numpy as np import message_filters from sensor_msgs.msg import Image as RosImage from std_srvs.srv import SetBool from vision_utils import fps import queue from hiwonder_interfaces.msg import MultiRawIdPosDur from jetarm_sdk import pid, bus_servo_control |
Initiate RgbDepthImageNode Class(RGB Depth Image Class)
Display pertinent prompts in the terminal. Initiate the RgbDepthImageNode class, and within the loop, call the node.image_proc() method until Ctrl+C is triggered or the ROS node is shut down. Provide log information indicating shutdown2.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | if __name__ == "__main__": print(''' ********************************************* * * * 此程序需要订阅深度摄像头节点,开启前请确保(The depth camera node needs to be subscribed for this program, please ensure that before starting) * * 已开启摄像头节点, 通过rostopic list可查看(The camera node has been started, please check via rostopic list) * * 是否有usb_cam相关节点,成功运行可看到终端(Whether there are usb_cam related nodes, you can see the terminal if it runs successfully) * * running ... * * * ********************************************* ''') try: node = RgbDepthImageNode() while not rospy.is_shutdown(): node.image_proc() except KeyboardInterrupt: rospy.loginfo("shutdown2") |
Initialization Function of RgbDepthImageNode Class
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class RgbDepthImageNode: def __init__(self): rospy.init_node('g1et_rgb_and_depth_image', anonymous=True) self.fps = fps.FPS() self.servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(3) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 700), (3, 85), (4, 350), (5, 500), (10, 200))) rospy.sleep(2) rospy.wait_for_service('/rgbd_cam/set_ldp') rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', RosImage, queue_size=1) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', RosImage, queue_size=1) rospy.ServiceProxy('/rgbd_cam/set_ldp', SetBool)(False) # 同步时间戳, 时间允许有误差在0.03s(synchronize timestamps, allowing for a time error of up to 0.03 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub], 2, 0.02) sync.registerCallback(self.multi_callback) #执行反馈函数(execute feedback function) self.queue = queue.Queue(maxsize=1) |
Initialize the node g1et_rgb_and_depth_image.
19 20 21 | class RgbDepthImageNode: def __init__(self): rospy.init_node('g1et_rgb_and_depth_image', anonymous=True) |
Initialize the frame rate counter.
22 | self.fps = fps.FPS() |
Create Servo Publisher
The
servos_pubcreates a servo publisher, sending messages to the/controllers/multi_id_pos_durtopic. Here,MultiRawIdPosDuris the message type used to control the positions and durations of multiple servos.The
set_servosfunction from thebus_servo_controlmodule is utilized to set the initial position and duration of the servos. The parameters include the publisher instance, servo runtime (in milliseconds), and a tuple containing the IDs and rotation angles (ranging from 1-1000) of multiple servos.24 25 26 27
self.servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(3) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 700), (3, 85), (4, 350), (5, 500), (10, 200))) rospy.sleep(2)
Wait for the /rgbd_cam/set_ldp service to start. A service proxy is created, using the SetBool message type to specify the request and response types for the service. False is then passed to the /rgbd_cam/set_ldp service.
29 30 31 32 | rospy.wait_for_service('/rgbd_cam/set_ldp') rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', RosImage, queue_size=1) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', RosImage, queue_size=1) rospy.ServiceProxy('/rgbd_cam/set_ldp', SetBool)(False) |
Synchronize Timestamp
Establish a time synchronizer to synchronize messages from the rgb_sub and depth_sub topics. The 2 signifies a maximum buffer size of 2 messages, and 0.03 implies that messages with a timestamp difference of no more than 0.03 seconds will be considered synchronized.
34 35 | # 同步时间戳, 时间允许有误差在0.03s(synchronize timestamps, allowing for a time error of up to 0.03 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub], 2, 0.02) |
Call the multi_callback function.
36 | sync.registerCallback(self.multi_callback) #执行反馈函数(execute feedback function) |
A queue object, self.queue, has been created to store synchronized messages, with a maximum queue size set to 1.
37 | self.queue = queue.Queue(maxsize=1) |
multi_callback Function
The
multi_callbackfunction receives ROS image messages,ros_rgb_imageandros_depth_image.It checks whether
self.queueis empty. If the queue is empty, it places a tuple containing synchronizedros_rgb_imageandros_depth_imageinto the queue. This tuple represents two successfully synchronized image messages.39 40 41
def multi_callback(self, ros_rgb_image, ros_depth_image): if self.queue.empty(): self.queue.put_nowait((ros_rgb_image, ros_depth_image))
image_proc Function
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | def image_proc(self): ros_rgb_image, ros_depth_image = self.queue.get(block=True) try: rgb_image = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) rgb_image = rgb_image[40:440,] depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) h, w = depth_image.shape[:2] depth = np.copy(depth_image).reshape((-1, )) depth[depth<=0] = 0 sim_depth_image = np.clip(depth_image, 0, 4000).astype(np.float64) sim_depth_image = sim_depth_image / 2000.0 * 255.0 bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) depth_color_map = cv2.applyColorMap(sim_depth_image.astype(np.uint8), cv2.COLORMAP_JET) result_image = np.concatenate([cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR), depth_color_map], axis=1) cv2.imshow("depth", result_image) key = cv2.waitKey(1) if key != -1: rospy.signal_shutdown('shutdown1') except Exception as e: rospy.logerr('callback error:', str(e)) |
Retrieve synchronized RGB and depth image messages from the self.queue queue.
43 44 | def image_proc(self): ros_rgb_image, ros_depth_image = self.queue.get(block=True) |
Create a NumPy array, rgb_image, to store RGB image data. Extract data from ros_rgb_image and construct it based on image height, width, and data type. Crop the RGB image, retaining only the portion of rows from 40 to 440 pixels in height.
Create another NumPy array, depth_image, to store depth image data. Extract data from ros_depth_image and construct it based on image height and width. Obtain the height and width of the depth image.
45 46 47 48 49 50 | try: rgb_image = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) rgb_image = rgb_image[40:440,] depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) h, w = depth_image.shape[:2] |
Copy depth image data, rearranging it into a one-dimensional array. Set pixel values with depth values less than or equal to 0 to 0, eliminating invalid depth values.
Perform some processing on the depth image, truncating depth values between 0 and 4000, and converting the result to the np.float64 type.
Normalize and scale the depth image, mapping depth values to the range of 0 to 255 for display purposes.
52 53 54 55 | depth = np.copy(depth_image).reshape((-1, )) depth[depth<=0] = 0 sim_depth_image = np.clip(depth_image, 0, 4000).astype(np.float64) sim_depth_image = sim_depth_image / 2000.0 * 255.0 |
Convert the RGB image from the RGB color space to the BGR color space.
Utilize a Jet pseudocolor mapping to transform the depth image into a pseudocolored image.
Horizontally concatenate the RGB image and pseudocolored depth image into a single image.
Display the final image on the depth window using cv2.imshow.
56 57 58 59 | bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) depth_color_map = cv2.applyColorMap(sim_depth_image.astype(np.uint8), cv2.COLORMAP_JET) result_image = np.concatenate([cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR), depth_color_map], axis=1) cv2.imshow("depth", result_image) |
Check for keyboard input, and if any key is pressed, close the node.
60 61 62 | key = cv2.waitKey(1) if key != -1: rospy.signal_shutdown('shutdown1') |
In the event of an exception in the code, capture the exception and record the error information as a ROS error log.
64 65 | except Exception as e: rospy.logerr('callback error:', str(e)) |
8.2 Distance Ranging
8.2.1 Program Flow
First, initialize the node and obtain the RGB image and depth map topic messages.
Next, process the image to obtain the pixel distance.
Finally, transmit both the color image and pseudo-color image.
8.2.2 Operation Steps
Note
the input command should be case sensitive, and the keywords can be complemented using Tab key.
(1) Double-click
to open the command line terminal, and execute the following command to close the auto-start service.
~/.stop_ros.sh
(2) Run the following command and hit Enter to run the game program.
roslaunch jetarm_6dof_rgbd_cam 2_distance_measure.launch
(3) If you need to terminate the running program, use short-cut Ctrl+C.
(4) Following the above function, it is essential to enable the app service; otherwise, the app functions may be impacted. Execute the command below to initiate the app service.
sudo systemctl start start_app_node.service
(4) After the service is initiated successfully, the robot will emit a beeping sound.
8.2.3 Program Outcome
When the game begins, the robot arm will transmit both the RGB color image and depth image to the screen. The depth image will indicate the point closest to the screen along with its corresponding distance. You can generate a click point by left-clicking the mouse, and obtain the distance from this point. By double-clicking or pressing the mouse scroll, the marked point can be canceled, allowing the robot arm to resume measuring the minimal distance.
8.2.4 Launch File Analysis
The Launch file is saved in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /launch/2_distance_measure.launch
1 2 3 4 5 6 | <launch> <include file="$(find jetarm_bringup)/launch/base.launch"/> <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="2_distance_measure.py" output="screen" respawn="true"> </node> </launch> |
Launch base.launch file and invoke the robot arm basic settings.
2 | <include file="$(find jetarm_bringup)/launch/base.launch"/> |
Launch 2_distance_measure.py source code file using the node.
4 5 | <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="2_distance_measure.py" output="screen" respawn="true"> </node> |
8.2.5 Python Source Code Analysis
The source code file locates in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /scripts/2_distance_measure.py
The program flowchart is as below:
When the game begins, the robot arm will transmit both the RGB color image and depth image to the screen. The depth image will indicate the point closest to the screen along with its corresponding distance. You can generate a click point by left-clicking the mouse, and obtain the distance from this point. By double-clicking or pressing the mouse scroll, the marked point can be canceled, allowing the robot arm to resume measuring the minimal distance.
Import Feature Pack
To import the necessary modules through the import statement:
cv2 is used for OpenCV image processing.
rospy is utilized for ROS communication.
numpy is employed for array operations.
message_filters is incorporated for implementing message filtering and synchronization, handling sensor data and messages in ROS.
The Image message type is imported and renamed as RosImage.
The SetBool service message type is imported from the std_srvs.srv module.
The fps function module is imported from the vision_utils module for frame rate calculation.
queue is used for queue-related operations in multi-threading.
The ROS message type named MultiRawIdPosDur is imported from hiwonder_interfaces.msg for defining custom ROS messages to facilitate data transmission and communication within the ROS system.
The servo control module is imported from jetarm_sdk.
10 11 12 13 14 15 16 17 18 19 | import cv2 import rospy import numpy as np import message_filters from sensor_msgs.msg import Image as RosImage from std_srvs.srv import SetBool from vision_utils import fps import queue from hiwonder_interfaces.msg import MultiRawIdPosDur from jetarm_sdk import pid, bus_servo_control |
Initiate RgbDepthImageNode Class(RGB Depth Image Class)
Display pertinent prompts in the terminal. Initiate the RgbDepthImageNode class, and within the loop, call the node.image_proc() method until Ctrl+C is triggered or the ROS node is shut down. Provide log information indicating shutdown2.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | if __name__ == "__main__": print(''' ********************************************* * * * 此程序需要订阅深度摄像头节点,开启前请确保(The depth camera node needs to be subscribed for this program, please ensure that before starting) * * 已开启摄像头节点, 通过rostopic list可查看(The camera node has been started, please check via rostopic list) * * 是否有usb_cam相关节点,成功运行可看到终端(Whether there are usb_cam related nodes, you can see the terminal if it runs successfully) * * running ... * * * ********************************************* ''') try: node = RgbDepthImageNode() while not rospy.is_shutdown(): node.image_proc() except KeyboardInterrupt: rospy.loginfo("shutdown2") |
Initialization Function of RgbDepthImageNode Class
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | class RgbDepthImageNode: def __init__(self): rospy.init_node('g1et_rgb_and_depth_image', anonymous=True) self.fps = fps.FPS() self.servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(3) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 700), (3, 85), (4, 350), (5, 500), (10, 200))) rospy.sleep(2) rospy.wait_for_service('/rgbd_cam/set_ldp') rospy.ServiceProxy('/rgbd_cam/set_ldp', SetBool)(False) rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', RosImage, queue_size=1) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', RosImage, queue_size=1) # 同步时间戳, 时间允许有误差在0.03s(synchronize timestamps, allowing for a time error of up to 0.03 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub], 2, 0.03) sync.registerCallback(self.multi_callback) #执行反馈函数(execute feedback function) self.queue = queue.Queue(maxsize=1) self.target_point = None self.last_event = 0 cv2.namedWindow("depth") cv2.setMouseCallback('depth', self.click_callback) |
Initialize g1et_rgb_and_depth_image node
22 23 24 | class RgbDepthImageNode: def __init__(self): rospy.init_node('g1et_rgb_and_depth_image', anonymous=True) |
Initialize the frame rate counter.
25 | self.fps = fps.FPS() |
Create Servo Publisher
The servos_pub creates a servo publisher, sending messages to the /controllers/multi_id_pos_dur topic. Here, MultiRawIdPosDur is the message type used to control the positions and durations of multiple servos.
The set_servos function from the bus_servo_control module is utilized to set the initial position and duration of the servos. The parameters include the publisher instance, servo runtime (in milliseconds), and a tuple containing the IDs and rotation angles (ranging from 1-1000) of multiple servos.
27 28 29 30 | self.servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(3) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 700), (3, 85), (4, 350), (5, 500), (10, 200))) rospy.sleep(2) |
Wait for /rgbd_cam/set_ldp Service to Start
A service proxy is created, using the SetBool message type to specify the request and response types for the service. False is then passed to the /rgbd_cam/set_ldp service.
32 33 | rospy.wait_for_service('/rgbd_cam/set_ldp') rospy.ServiceProxy('/rgbd_cam/set_ldp', SetBool)(False) |
Receive Image Information
(1) Create rgb_sub image information subscriber, and use message_filters.Subscriber to create a message subscriber.
The first parameter /rgbd_cam/color/image_raw represents the image topic name.
The second parameter RosImage is the message type.
The third parameter queue_size=1 specifies the size of the message queue.
(2) Create the depth_sub image information subscriber by utilizing message_filters.Subscriber. This subscriber is defined with the following specifications:
The first parameter /rgbd_cam/depth/image_raw represents the image topic name.
The second parameter indicates the message type as RosImage.
The third parameter queue_size=1 defines the message queue size.
35 36 | rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', RosImage, queue_size=1) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', RosImage, queue_size=1) |
Synchronize Timestamp
Establish a time synchronizer to synchronize messages from the rgb_sub and depth_sub topics. The 2 signifies a maximum buffer size of 2 messages, and 0.03 implies that messages with a timestamp difference of no more than 0.03 seconds will be considered synchronized.
38 39 | # 同步时间戳, 时间允许有误差在0.03s(synchronize timestamps, allowing for a time error of up to 0.03 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub], 2, 0.03) |
Invoke multi_callback function.
40 | sync.registerCallback(self.multi_callback) #执行反馈函数(execute feedback function) |
A queue object, self.queue, has been created to store synchronized messages, with a maximum queue size set to 1.
41 | self.queue = queue.Queue(maxsize=1) |
Initialize self.target_point and self.last_event.
41 42 43 | self.queue = queue.Queue(maxsize=1) self.target_point = None self.last_event = 0 |
cv2.namedWindow is used to create an OpenCV window named depth.
cv2.setMouseCallback invokes self.click_callback callback function to process the mouse event on the depth window.
44 45 | cv2.namedWindow("depth") cv2.setMouseCallback('depth', self.click_callback) |
click_callback Function
47 48 49 50 51 52 53 54 55 | def click_callback(self, event, x, y, flags, params): if event == cv2.EVENT_RBUTTONDOWN or event == cv2.EVENT_MBUTTONDOWN or event == cv2.EVENT_LBUTTONDBLCLK: self.target_point = None if event == cv2.EVENT_LBUTTONDOWN and self.last_event != cv2.EVENT_LBUTTONDBLCLK: if x >= 640: self.target_point = (x - 640, y) else: self.target_point = (x, y) self.last_event = event |
Firstly, check the type of mouse event, utilizing the mouse event constants from OpenCV. If the event is one of EVENT_RBUTTONDOWN (right button click), EVENT_MBUTTONDOWN (middle button click), or EVENT_LBUTTONDBLCLK (left button double click), set self.target_point to None.
47 48 49 | def click_callback(self, event, x, y, flags, params): if event == cv2.EVENT_RBUTTONDOWN or event == cv2.EVENT_MBUTTONDOWN or event == cv2.EVENT_LBUTTONDBLCLK: self.target_point = None |
If the detected mouse event is EVENT_LBUTTONDOWN (left button click) and the previous mouse event was not EVENT_LBUTTONDBLCLK (left button double click), check the mouse click position x and y. If x is greater than or equal to 640, set self.target_point to (x - 640, y); otherwise, set self.target_point to (x, y), recording the coordinates of the target point.
Finally, update the self.last_event variable to store the type of the current event.
50 51 52 53 54 55 | if event == cv2.EVENT_LBUTTONDOWN and self.last_event != cv2.EVENT_LBUTTONDBLCLK: if x >= 640: self.target_point = (x - 640, y) else: self.target_point = (x, y) self.last_event = event |
image_proc Function
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | def image_proc(self): ros_rgb_image, ros_depth_image = self.queue.get(block=True) try: rgb_image = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) h, w = depth_image.shape[:2] depth = np.copy(depth_image).reshape((-1, )) depth[depth<=0] = 55555 min_index = np.argmin(depth) min_y = min_index // w min_x = min_index - min_y * w if self.target_point is not None: min_x, min_y = self.target_point sim_depth_image = np.clip(depth_image, 0, 2000).astype(np.float64) / 2000 * 255 depth_color_map = cv2.applyColorMap(sim_depth_image.astype(np.uint8), cv2.COLORMAP_JET) txt = 'Dist: {}mm'.format(depth_image[min_y, min_x]) cv2.circle(depth_color_map, (int(min_x), int(min_y)), 8, (32, 32, 32), -1) cv2.circle(depth_color_map, (int(min_x), int(min_y)), 6, (255, 255, 255), -1) cv2.putText(depth_color_map, txt, (11, 200), cv2.FONT_HERSHEY_PLAIN, 2.0, (32, 32, 32), 6, cv2.LINE_AA) cv2.putText(depth_color_map, txt, (10, 200), cv2.FONT_HERSHEY_PLAIN, 2.0, (240, 240, 240), 2, cv2.LINE_AA) bgr_image = cv2.cvtColor(rgb_image[40:440, ], cv2.COLOR_RGB2BGR) cv2.circle(bgr_image, (int(min_x), int(min_y)), 8, (32, 32, 32), -1) cv2.circle(bgr_image, (int(min_x), int(min_y)), 6, (255, 255, 255), -1) cv2.putText(bgr_image, txt, (11, h - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (32, 32, 32), 6, cv2.LINE_AA) cv2.putText(bgr_image, txt, (10, h - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (240, 240, 240), 2, cv2.LINE_AA) self.fps.update() bgr_image = self.fps.show_fps(bgr_image) result_image = np.concatenate([bgr_image, depth_color_map], axis=1) cv2.imshow("depth", result_image) key = cv2.waitKey(1) if key != -1: rospy.signal_shutdown('shutdown1') except Exception as e: rospy.logerr('callback error:', str(e)) |
Retrieve synchronized RGB and depth image messages from the self.queue.
62 63 | def image_proc(self): ros_rgb_image, ros_depth_image = self.queue.get(block=True) |
Create a NumPy array, rgb_image, to store RGB image data. Extract data from ros_rgb_image and construct it based on the image’s height, width, and data type.
Create another NumPy array, depth_image, to store depth image data. Extract data from ros_depth_image and construct it based on the image’s height and width.
Retrieve the height and width of the depth image.
64 65 66 67 68 | try: rgb_image = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) h, w = depth_image.shape[:2] |
Acquire Minimal Depth Pixel Point Coordinate
Copy the depth image data and reassemble it into a one-dimensional array.
Set pixel values with depth less than or equal to 0 to 55555, eliminating invalid depth values.
Find the index of the pixel with the minimum depth value in the depth image using np.argmin(depth).
70 71 72 | depth = np.copy(depth_image).reshape((-1, )) depth[depth<=0] = 55555 min_index = np.argmin(depth) |
Calculate the row coordinate min_y of the pixel with the minimum depth value by performing integer division using min_index divided by w (image width). Integer division truncates the decimal part, resulting in an integer representing the row position of the pixel with the minimum depth value in the depth image.
Multiply min_y by w (image width) and subtract the result from min_index to obtain the column coordinate min_x of the pixel with the minimum depth value.
73 74 | min_y = min_index // w min_x = min_index - min_y * w |
If target_point is not empty, assign its values to min_x and min_y, respectively.
75 76 | if self.target_point is not None: min_x, min_y = self.target_point |
Limit the depth values in the depth image to the range of 0 to 2000, then scale them to the range of 0 to 255 for visualizing depth information as a pseudocolored image.
Convert the depth image to a pseudocolored image using the Jet pseudocolor mapping.
78 79 | sim_depth_image = np.clip(depth_image, 0, 2000).astype(np.float64) / 2000 * 255 depth_color_map = cv2.applyColorMap(sim_depth_image.astype(np.uint8), cv2.COLORMAP_JET) |
Mark Corresponding Depth Values On Deep Pseudo-Color Image
Utilizing the depth values from the depth image (depth_image) at the corresponding coordinates (min_y, min_x), draw filled black and white circles on the deep pseudo-color image. These circles serve to highlight the position of the minimum depth value. The parameters include the image name, circle center coordinates, circle radius, circle colors (with -1 indicating filling the entire circle). The white circle is slightly smaller than the black circle for better emphasis.
Additionally, display text on the deep pseudo-color image representing the depth values. This involves drawing a larger black text and a slightly offset smaller gray text on the image. The parameters include the image being processed, text content, starting position, font, size, color, line width, and an option to enable anti-aliasing for smoother line rendering.
81 82 83 84 85 | txt = 'Dist: {}mm'.format(depth_image[min_y, min_x]) cv2.circle(depth_color_map, (int(min_x), int(min_y)), 8, (32, 32, 32), -1) cv2.circle(depth_color_map, (int(min_x), int(min_y)), 6, (255, 255, 255), -1) cv2.putText(depth_color_map, txt, (11, 200), cv2.FONT_HERSHEY_PLAIN, 2.0, (32, 32, 32), 6, cv2.LINE_AA) cv2.putText(depth_color_map, txt, (10, 200), cv2.FONT_HERSHEY_PLAIN, 2.0, (240, 240, 240), 2, cv2.LINE_AA) |
Mark Corresponding Depth Values On bgr Image
Crop a region from the original RGB image (rgb_image) spanning from pixel coordinates 40 rows to 440 rows. Subsequently, convert the cropped RGB image to the BGR color space.
On the BGR image, draw filled black and white circles to highlight the position of the minimum depth value. The parameters include the image name, circle center coordinates, circle radius, circle colors (with -1 indicating filling the entire circle). The white circle is slightly smaller than the black circle for better emphasis.
Additionally, display text on the BGR image representing the depth values. This involves drawing a larger black text and a slightly offset smaller gray text on the image. The parameters include the image being processed, text content, starting position, font, size, color, line width, and an option to enable anti-aliasing for smoother line rendering.
87 88 89 90 91 | bgr_image = cv2.cvtColor(rgb_image[40:440, ], cv2.COLOR_RGB2BGR) cv2.circle(bgr_image, (int(min_x), int(min_y)), 8, (32, 32, 32), -1) cv2.circle(bgr_image, (int(min_x), int(min_y)), 6, (255, 255, 255), -1) cv2.putText(bgr_image, txt, (11, h - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (32, 32, 32), 6, cv2.LINE_AA) cv2.putText(bgr_image, txt, (10, h - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (240, 240, 240), 2, cv2.LINE_AA) |
Update image frame rate.
93 | self.fps.update() |
Concatenate the RGB image and pseudo-color depth image horizontally to create a single image.
Utilize cv2.imshow to display the final image on the depth window.
95 96 | result_image = np.concatenate([bgr_image, depth_color_map], axis=1) cv2.imshow("depth", result_image) |
Continuously check for keyboard inputs; if there is any key pressed, terminate the process.
98 99 | if key != -1: rospy.signal_shutdown('shutdown1') |
In the event of code exceptions, capture and log the exception information as an error in the ROS error logs.
101 102 | except Exception as e: rospy.logerr('callback error:', str(e)) |
8.3 Depth Map Conversion
8.3.1 Program Flow
Firstly, initialize the node and obtain the topic message of the RGB image and depth image.
Next, process the image to integrate the RGB image and depth map into RGBD image. Then convert the RGBD image into point cloud.
Lastly, crop and transmit the point cloud data.
8.3.2 Operation Steps
Note
the input command should be case sensitive, and the keywords can be complemented using Tab key.
(1) Double-click
to open the command line terminal, and execute the command below to close the auto-start service.
~/.stop_ros.sh
(2) Run the following command and hit Enter to run the program.
roslaunch jetarm_6dof_rgbd_cam 3_rgb_depth_to_pointcloud.launch
(3) If you need to terminate the running program, use short-cut Ctrl+C.
(4) Following the above function, it is essential to enable the app service
; otherwise, the app functions may be impacted. Execute the command below.
sudo systemctl start start_app_node.service
(5) After the service is initiated successfully, the robot will emit a beeping sound.
8.3.3 Program Outcome
When the game starts, the robotic arm will transform the pre-cropped point cloud data into a depth image, which is subsequently displayed in the visualization window. In the feedback display, users can examine the depth image. For different perspectives of the point cloud, users can either hold down the left mouse button or scroll to drag and navigate through the point cloud. Furthermore, scrolling the mouse wheel enables zooming in on the point cloud, providing a more detailed view.
8.3.4 Launch File Analysis
The Launch file is saved in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /launch/3_rgb_depth_image_to_point_cloud.launch
{lineno-start=}
<launch>
<include file="$(find jetarm_bringup)/launch/base.launch"/>
<node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="3_rgb_depth_image_to_point_cloud.py" output="screen" respawn="true">
</node>
</launch>
Initiate base.launch file to invoke the robot arm basic configuration.
2 | <include file="$(find jetarm_bringup)/launch/base.launch"/> |
Launch 3_rgb_depth_image_to_point_cloud.pysource code file using the node.
4 5 | <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="3_rgb_depth_image_to_point_cloud.py" output="screen" respawn="true"> </node> |
8.3.5 Python Source Code Analysis
The source code file locates in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /scripts/3_rgb_depth_image_to_point_cloud.py
The program flowchart is as below:
The program’s primary logic, as depicted in the diagram above, includes acquiring RGB and depth images from the camera. Subsequently, the program processes this data and employs mouse click events to measure the distance from the clicked point and determine the nearest point distance.
Import Feature Pack
Import the necessary modules using the import statements:
cv2 for OpenCV image processing.
rospy for ROS communication.
numpy for array operations.
message_filters for implementing message filtering and synchronization, particularly for handling sensor data and messages in ROS.
Import the Image message type and rename it as RosImage.
Import the SetBool service message type from the std_srvs.srv module.
Import the fps functionality module from the vision_utils module for frame rate calculation.
Use the queue module for executing queue-related operations in multithreading.
Import the ROS message type named MultiRawIdPosDur from the hiwonder_interfaces.msg module. This message type is used for defining custom ROS messages for data transmission and communication within the ROS system.
Import the servo control module from jetarm_sdk.
Import the Open3D library.
9 10 11 12 13 14 15 16 17 18 19 | import cv2 import rospy import numpy as np import message_filters from sensor_msgs.msg import Image from std_srvs.srv import SetBool from vision_utils import fps import queue from hiwonder_interfaces.msg import MultiRawIdPosDur from jetarm_sdk import pid, bus_servo_control import open3d as o3d |
Initialize the variable have_add、get_point
21 22 | haved_add = False get_point = False |
Store the point cloud to be displayed.
23 | target_cloud = o3d.geometry.PointCloud() # 要显示的点云(the point cloud to be displayed) |
Utilize cuda to accelerate the open3d program.
26 | device = o3d.core.Device('CUDA:0') |
Define Intrinsic and Extrinsic Parameters
The intrinsic and extrinsic matrices for the camera have been defined. These matrices are instrumental in converting color images and depth images into point cloud data.
29 30 31 32 33 34 35 36 | intrinsic = o3d.core.Tensor([[477, 0, 316], [0, 477, 189], [0, 0, 1]]) extrinsic = o3d.core.Tensor([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.17], [0.0, 0.0, 1.0, 0.433], [0.0, 0.0, 0.0, 1.0]]) |
Define roi Area
A matrix defining the region of interest (ROI) has been established to delineate the specific area for point cloud cropping. This matrix configures the coordinates of four vertices, with each vertex represented by a three-dimensional coordinate [x, y, z]. In this context, x corresponds to the horizontal direction, y to the vertical direction, and z is set to 0 (indicating a planar area). The data type for these coordinates is floating-point.
48 49 50 51 52 53 | roi = np.array([ [-0.05, -0.20, 0], [-0.05, -0.05, 0], [0.1, -0.05, 0], [0.1, -0.20, 0]], dtype = np.float64) |
Specify Cropping Region
We’ve created a vol object to define a specific polygonal volume. This object is responsible for specifying the cropping region, indicating the axes for cropping, and establishing the depth range for cropping.
55 | vol = o3d.visualization.SelectionPolygonVolume() |
Specifically, the cropping is carried out along the Z-axis, with a range set from -0.26 to 0.26 meters. The polygonal boundaries of the chosen volume align with the ROI area. Additionally, an intrinsic object named intrinsic has been generated for the Pinhole camera. These intrinsic parameters encompass the image’s width and height, along with the horizontal and vertical focal lengths and the principal point. These parameters will play a crucial role in transforming point cloud data from the camera coordinate system to the world coordinate system.
56 57 58 59 60 61 62 | # 裁剪z轴,范围(crop z-axis, range) vol.orthogonal_axis = 'Z' vol.axis_max = 0.26 vol.axis_min = -0.26 vol.bounding_polygon = o3d.utility.Vector3dVector(roi) intrinsic = o3d.camera.PinholeCameraIntrinsic(640, 400, 477, 477, 316, 189)#356, 260 FPS = None |
Initialize Node
Print the related info on the terminal and initialize the nodes.
110 111 112 113 114 115 116 117 118 119 120 121 | if __name__ == "__main__": print(''' ********************************************* * * * 此程序需要订阅深度摄像头节点,开启前请确保 * * 已开启摄像头节点,通过rostopic list可查看 * * 是否有usb_cam相关节点,成功运行可看到终端 * * running ... * * * ********************************************* ''') rospy.init_node('point_cloud', anonymous=True) |
Create Servo Publisher
The
servos_pubcreates a servo publisher, sending messages to the/controllers/multi_id_pos_durtopic. Here,MultiRawIdPosDuris the message type used to control the positions and durations of multiple servos.The
set_servosfunction from thebus_servo_controlmodule is utilized to set the initial position and duration of the servos. The parameters include the publisher instance, servo runtime (in milliseconds), and a tuple containing the IDs and rotation angles (ranging from 1-1000) of multiple servos.122 123 124 125
servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(2) bus_servo_control.set_servos(servos_pub, 1000, ((1, 500), (2, 560), (3, 160), (4, 80), (5, 500), (10, 200))) rospy.sleep(3)
Wait for /rgbd_cam/set_ldp Service to Initiate
Create a service proxy, and use
SetBoolmessage type to specify the service request and response type.Convey
Falseto/rgbd_cam/set_ldpservice.127 128 129
rospy.wait_for_service('/rgbd_cam/set_ldp') rospy.ServiceProxy('/rgbd_cam/set_ldp', SetBool)(False) rospy.sleep(1)
Create Visualization Window
Create a window and configure the following parameters: window name (point cloud), width, height, and visibility status.
133 134 135 | # 创建可视化窗口(create a visualization window) vis = o3d.visualization.Visualizer() vis.create_window(window_name='point cloud', width=640, height=480, visible=1) |
Receive Image Info
(1) Create rgb_sub image subscriber, and use message_filters.Subscriber’ to create a message subscriber.
The first parameter /rgbd_cam/color/image_raw represents the image topic name.
The second parameter RosImage refers to the message type.
The third parameter queue_size=1 specify the size of message queue.
(2) Create depth_sub image subscriber, and use message_filters.Subscriber to create a message subscriber.
The first parameter /rgbd_cam/depth/image_raw denotes the image topic name.
The second parameter RosImage is the message type.
The third parameter queue_size=1 specifies the size of the message queue.
137 138 | rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', Image) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', Image) |
multi_callback Function
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
def multi_callback(ros_rgb_image, ros_depth_image): global get_point, t0 try: # ros格式转为numpy(convert ros format to numpy) image_rgb = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) # 统一分辨率为640x400(standardize the resolution to 640x400) image_rgb = image_rgb[40:440, ] o3d_image_rgb = o3d.geometry.Image(image_rgb) o3d_image_depth = o3d.geometry.Image(np.ascontiguousarray(depth_image)) # rgbd --> point_cloud rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth(o3d_image_rgb, o3d_image_depth, convert_rgb_to_intensity=False) # cpu占用大 (high CPU usage) pc = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd_image, intrinsic)#, extrinsic=extrinsic) ''' # numpy格式转为open3d格式(convert numpy format to open3d format) o3d_image_rgb = o3d.t.geometry.Image(o3d.core.Tensor(image_rgb, dtype=o3d.core.Dtype.UInt8, device=device)) o3d_image_depth = o3d.t.geometry.Image(o3d.core.Tensor(np.ascontiguousarray(depth_image), dtype=o3d.core.Dtype.Float32, device=device)) # rgb depth ---> rgbd rgbd = o3d.t.geometry.RGBDImage(o3d_image_rgb, o3d_image_depth) # rgbd ---> point cloud point_cloud = o3d.t.geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsic)#, extrinsic) # 取出gpu的数据(retrieve data from the GPU) pc = point_cloud.to_legacy() ''' # 裁剪(crop) roi_pc = pc#vol.crop_point_cloud(pc) target_cloud.points = roi_pc.points target_cloud.colors = roi_pc.colors # 转180度方便查看(rotate 180 degrees for easier viewing) target_cloud.transform(np.asarray([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]])) get_point = True fps = int(1.0/(rospy.get_time() - t0)) print('\r', 'FPS: ' + str(fps), end='') except BaseException as e: print('callback error:', e) t0 = rospy.get_time()
Convert ros-Formatted Image into numpy
Arrays in NumPy, namely
image_rgbanddepth_image, have been created to store RGB and depth image data, facilitating subsequent processing of both types of images.66 67 68 69
try: # ros格式转为numpy(convert ros format to numpy) image_rgb = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data)
A region is cropped from the original RGB image (
rgb_image), specifically from pixel coordinates 40 rows to 440 rows.The NumPy arrays
image_rgbanddepth_imageare then converted into Open3D image objects (o3d_image_rgb). This conversion allows for processing and visualization within Open3D, establishing a foundational data basis for subsequent point cloud creation and processing.71 72 73 74
image_rgb = image_rgb[40:440, ] o3d_image_rgb = o3d.geometry.Image(image_rgb) o3d_image_depth = o3d.geometry.Image(np.ascontiguousarray(depth_image))
Generate rgbd Image
An RGB-D (Color-Depth) image object, named rgbd_image, has been created, comprising the color image (o3d_image_rgb) and the depth image (o3d_image_depth).
Within this function call, the parameter convert_rgb_to_intensity is set to False. This indicates that color information will not be converted into intensity information.
76 77 | # rgbd --> point_cloud rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth(o3d_image_rgb, o3d_image_depth, convert_rgb_to_intensity=False) |
Convert rgbd Image into Point Cloud Data
Utilize rgbd_image and camera intrinsic parameter to generate a point cloud object.
Convert rgbd image into three dimensional point cloud data. Each point contains three dimensional coordinate information and color information.
79 | pc = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd_image, intrinsic)#, extrinsic=extrinsic) |
The point cloud data from the variable pc has been assigned to roi_pc. Subsequently, the points and color data of roi_pc have been allocated to the variable target_cloud.
96 97 98 99 | roi_pc = pc#vol.crop_point_cloud(pc) target_cloud.points = roi_pc.points target_cloud.colors = roi_pc.colors |
The transform function has been invoked to perform a transformation on target_cloud, involving a rotation around both the Y-axis and Z-axis, inverting the point cloud.
100 101 | # 转180度方便查看(rotate 180 degrees for easier viewing) target_cloud.transform(np.asarray([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]])) |
Set get_point to True.
The frame rate is estimated by calculating the time interval between two consecutive callbacks. rospy.get_time() is used to obtain the current timestamp, and the reciprocal of the time difference between the two timestamps represents the frame rate. This frame rate, stored as an integer in the variable fps, is printed to the terminal.
103 104 105 | get_point = True fps = int(1.0/(rospy.get_time() - t0)) print('\r', 'FPS: ' + str(fps), end='') |
If the program encounters an exception, the error information is printed.
The current timestamp is stored in t0 for subsequent frame rate calculations. This way, the program continuously computes and updates frame rate information, allowing users to monitor the performance of the program in real-time.
106 107 108 | except BaseException as e: print('callback error:', e) t0 = rospy.get_time() |
Synchronize Timestamp
Establish a time synchronizer to synchronize messages from the rgb_sub and depth_sub topics. The 2 signifies a maximum buffer size of 2 messages, and 0.03 implies that messages with a timestamp difference of no more than 0.03 seconds will be considered synchronized.
140 141 | # 同步时间戳, 时间允许有误差在0.02s(synchronize timestamps, allowing for a time error of up to 0.02 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub], 2, 0.015) |
Call the multi_callback function.
142 | sync.registerCallback(multi_callback) #执行反馈函数(execute feedback function) |
Add Point Cloud Data to Visualization Window
Check if the node is running, and if so, enter the While loop.
If haved_add is False and get_point is True, use vis.add_geometry(target_cloud) to add the point cloud data to the visualization window and set haved_add to True.
If haved_add is already True, then check if get_point is also True. If it is, it indicates that new point cloud data is ready. Perform the following actions:
set get_point to False, update the point cloud data, handle events in the visualization window using vis.poll_events(), update the renderer using vis.update_renderer().
If haved_add is still False, it means that the point cloud data has not been added to the visualization window yet. In this case, the code sleeps for a short duration (rospy.sleep(0.01)) to reduce CPU usage.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | while not rospy.is_shutdown(): if not haved_add and get_point: vis.add_geometry(target_cloud) haved_add = True if haved_add: if get_point: get_point = False # 刷新(refresh) vis.update_geometry(target_cloud) vis.poll_events() vis.update_renderer() else: rospy.sleep(0.001) else: rospy.sleep(0.01) |
8.4 Height Detection and Gripping
8.4.1 Program Flow
Start by initializing the node and fetching topic information related to RGB and depth images.
Then, perform image processing to determine the pixel coordinates of the tallest object within the field of view.
Afterward, convert these pixel coordinates into the world coordinates of the robotic arm through a coordinate system transformation.
Finally, conclude the process by planning the trajectory of the robotic arm for grasping and placing the large wooden block.
8.4.2 Operation Steps
Note
The input command should be case sensitive, and the keywords can be complemented using Tab key.
(1) Double-click
to open the command line terminal, and execute the command below to close the auto-start service.
~/.stop_ros.sh
(2) Run the following command and hit Enter to run the game program.
roslaunch jetarm_6dof_rgbd_cam remove_too_high.launch
(3) If you need to terminate the running program, use short-cut Ctrl+C.
(4) Following the above function, it is essential to enable the app service; otherwise, the app functions may be impacted. Execute the command below to enable the app service.
sudo systemctl start start_app_node.service
(5) After the service is initiated successfully, the robot will emit a beeping sound.
8.4.3 Program Outcome
Once the game begins, place a 30x30mm block and a 40x40mm block within the recognition area on the map. Subsequently, the robotic arm will assess their heights and pick up the taller of the two.
8.4.4 Launch File Analysis
The Launch file is saved in : /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /launch/remove_too_high.launch
1 2 3 4 5 6 | <launch> <include file="$(find jetarm_bringup)/launch/base.launch"/> <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="remove_too_high.py" output="screen" respawn="true"> </node> </launch> |
Initiate base.launch file, and invoke some robot arm basic settings.
2 | <include file="$(find jetarm_bringup)/launch/base.launch"/> |
Initiate remove_too_high.py source code file using the node.
4 5 | <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="remove_too_high.py" output="screen" respawn="true"> </node> |
8.4.5 Python Source Code Analysis
The source code file is saved in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam/scripts/remove_too_high.py
The program flowchart is as below:
The program’s core logic, as depicted in the above diagram, revolves around obtaining RGB and depth images from the camera. Following image processing, the program then moves on to acquiring the pose of the tallest object within the field of view. Subsequently, it initiates the grasp operation to relocate the large block.
Import Feature Pack
Use import statements to bring in the necessary modules:
cv2 for OpenCV image processing.
rospy for ROS communication.
numpy for array operations.
math for mathematical calculations.
message_filters for implementing message filtering and synchronization, specifically for handling sensor data and messages in ROS.
Import the Image message type and rename it as RosImage.
Import the SetBool service message type from the std_srvs.srv module.
Import the fps functionality module from the vision_utils module for frame rate calculation.
Utilize the queue module for executing queue-related operations in multithreading.
Import the ROS message type named MultiRawIdPosDur from the hiwonder_interfaces.msg module. This type is used to define custom ROS messages for data transmission and communication within the ROS system.
Import modules from vision_utils for coordinate system transformation calculations.
Import modules such as pid, bus_servo_control, sdk_client, and tone from jetarm_sdk.
Import the library for kinematics control from jetarm_kinematics.
Import the action_group_controller for action group control.
Define a variable named CONFIG_NAME.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import cv2 import rospy import numpy as np import math import message_filters import time from sensor_msgs.msg import Image as RosImage from sensor_msgs.msg import CameraInfo from std_srvs.srv import SetBool from vision_utils import fps from hiwonder_interfaces.srv import GetRobotPose import threading import queue from hiwonder_interfaces.msg import MultiRawIdPosDur from vision_utils import xyz_quat_to_mat, xyz_euler_to_mat, mat_to_xyz_euler from jetarm_sdk import pid, bus_servo_control, sdk_client, tone from jetarm_kinematics import kinematics_control |
depth_pixel_to_camera Function
This function is employed to convert pixel coordinates from the depth image into three-dimensional point coordinates within the camera coordinate system.
pixel_coords: A tuple (px, py) representing pixel coordinates in the depth image, indicating the pixel’s position.
depth: The depth value corresponding to the pixel.
intrinsics: Camera intrinsics, presented as a tuple (fx, fy, cx, cy), representing the camera’s focal lengths and optical center.
The formula is applied to calculate and return a NumPy array containing x, y, and z values, which represent the three-dimensional point coordinates.
30 31 32 33 34 35 36 | def depth_pixel_to_camera(pixel_coords, depth, intrinsics): fx, fy, cx, cy = intrinsics px, py = pixel_coords x = (px - cx) * depth / fx y = (py - cy) * depth / fy z = depth return np.array([x, y, z]) |
Initiate RgbDepthImageNode Class (RGB Depth Image Class)
Display pertinent messages in the terminal. Initiate the RgbDepthImageNode class and repeatedly invoke the node.image_proc() method within a loop until Ctrl+C is pressed or the ROS node is closed. Output the log message shutdown2 upon the conclusion of the loop.
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | if __name__ == "__main__": print(''' ********************************************* * * * 此程序需要订阅深度摄像头节点,开启前请确保(The depth camera node needs to be subscribed for this program, please ensure that before starting) * * 已开启摄像头节点, 通过rostopic list可查看(The camera node has been started, please check via rostopic list) * * 是否有usb_cam相关节点,成功运行可看到终端(Whether there are usb_cam related nodes, you can see the terminal if it runs successfully) * * running ... * * * ********************************************* ''') try: node = RgbDepthImageNode() while not rospy.is_shutdown(): node.image_proc() except KeyboardInterrupt: rospy.loginfo("shutdown2") |
Initialization Function of RgbDepthImageNode Class
39 40 41 42 43 44 45 46 47 48 49 50 51 | class RgbDepthImageNode: def __init__(self): rospy.init_node('remove_too_high_node', anonymous=True) config = rospy.get_param(CONFIG_NAME) self.endpoint = None self.hand2cam_tf_matrix = config['hand2cam_tf_matrix'] self.fps = fps.FPS() self.last_shape = "none" self.moving = False self.stamp = time.time() self.count = 0 self.last_position = (0, 0, 0) self.sdk = sdk_client.JetArmSDKClient() |
Initialize remove_too_high_node node.
39 40 41 | class RgbDepthImageNode: def __init__(self): rospy.init_node('remove_too_high_node', anonymous=True) |
Initialize the related variable.
43 44 45 46 47 48 49 50 51 | self.endpoint = None self.hand2cam_tf_matrix = config['hand2cam_tf_matrix'] self.fps = fps.FPS() self.last_shape = "none" self.moving = False self.stamp = time.time() self.count = 0 self.last_position = (0, 0, 0) self.sdk = sdk_client.JetArmSDKClient() |
Create Servo Publisher
The servos_pub creates a servo publisher, sending messages to the /controllers/multi_id_pos_dur topic. Here, MultiRawIdPosDur is the message type used to control the positions and durations of multiple servos.
The set_servos function from the bus_servo_control module is utilized to set the initial position and duration of the servos. The parameters include the publisher instance, servo runtime (in milliseconds), and a tuple containing the IDs and rotation angles (ranging from 1-1000) of multiple servos.
53 54 55 56 | self.servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(3) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 540), (3, 220), (4, 50), (5, 500), (10, 200))) rospy.sleep(2) |
Wait for /rgbd_cam/set_ldp Service to Initiate
Create a service proxy, and use SetBool message type to specify the service request and response type.
Convey False to /rgbd_cam/set_ldp service.
58 59 60 | rospy.wait_for_service('/rgbd_cam/set_ldp') rospy.ServiceProxy('/rgbd_cam/set_ldp', SetBool)(False) threading.Thread(target=self.get_endpoint, args=()).start() |
Create a new thread, and invoke get_endpoint function.
60 | threading.Thread(target=self.get_endpoint, args=()).start() |
Receive Image Information
(1) Create rgb_sub image information subscriber, and utilize message_filters.Subscriber to create a message subscriber.
The first parameter /rgbd_cam/color/image_raw represents the image topic name.
The second parameter RosImage refers to the message type.
The third parameter queue_size=1 specify the size of message queue.
(2) Create depth_sub image subscriber, and use message_filters.Subscriber to create a message subscriber.
The first parameter /rgbd_cam/depth/image_raw denotes the image topic name.
The second parameter RosImage is the message type.
The third parameter queue_size=1 specifies the size of the message queue.
(3) Create info_sub information subscriber, and utilize message_filters.Subscriber to create a message subscriber.
The first parameter /rgbd_cam/depth/camera_info denotes the camera information topic name.
The second parameter CameraInfo is the message type.
The third parameter queue_size=1 specifies the size of the message queue.
62 63 64 | rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', RosImage, queue_size=1) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', RosImage, queue_size=1) info_sub = message_filters.Subscriber('/rgbd_cam/depth/camera_info', CameraInfo, queue_size=1) |
Synchronize Timestamp
Establish a time synchronizer to synchronize messages from the rgb_sub and depth_sub topics. The 3 signifies a maximum buffer size of 3 messages, and 0.03 implies that messages with a timestamp difference of no more than 0.03 seconds will be considered synchronized.
66 67 68 69 | # 同步时间戳, 时间允许有误差在0.03s(synchronize timestamps, allowing for a time error of up to 0.03 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub, info_sub], 3, 0.03) sync.registerCallback(self.multi_callback) #执行反馈函数(execute feedback function) self.queue = queue.Queue(maxsize=1) |
get_endpoint Function
In the absence of a node shutdown signal, the program enters a While loop. Inside this loop, a rospy.ServiceProxy is established to call the /kinematics/get_current_pose service, retrieving the robot’s current pose information encompassing position and orientation. The acquired data is stored in the endpoint variable.
Subsequently, the xyz_quat_to_mat function is invoked to transform the position and orientation details from endpoint into a matrix. The outcome is then stored in the self.endpoint member variable, serving as a foundation for subsequent coordinate system transformations.
71 72 73 74 75 76 | def get_endpoint(self): while not rospy.is_shutdown(): endpoint = rospy.ServiceProxy('/kinematics/get_current_pose', GetRobotPose)().pose self.endpoint = xyz_quat_to_mat([endpoint.position.x, endpoint.position.y, endpoint.position.z], [endpoint.orientation.w, endpoint.orientation.x, endpoint.orientation.y, endpoint.orientation.z]) rospy.sleep(0.5) |
multi_callback Function
The multi_callback function receives messages from ros_rgb_image, ros_depth_image, and camera_info. It checks if self.queue is empty. If the queue is empty, it adds a tuple to the queue. This tuple contains synchronized messages of ros_rgb_image, ros_depth_image, and camera_info, indicating the successful synchronization of these three images.
117 118 119 | def multi_callback(self, ros_rgb_image, ros_depth_image, camera_info): if self.queue.empty(): self.queue.put_nowait((ros_rgb_image, ros_depth_image, camera_info)) |
Pick Function
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | def pick(self, position, angle): angle = angle % 90 angle = angle - 90 if angle > 40 else (angle + 90 if angle < -45 else angle) print('ang', angle) position[2] += 0.05 ret = kinematics_control.set_pose_target(position, 80) if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, ret[1][4]))) rospy.sleep(1.5) angle = 500 + int(1000 * (angle + ret[3][-1]) / 240) print('ret',ret) bus_servo_control.set_servos(self.servos_pub, 500, ((5, angle),)) rospy.sleep(0.5) position[2] -= 0.05 ret = kinematics_control.set_pose_target(position, 80) if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, angle))) rospy.sleep(1.5) bus_servo_control.set_servos(self.servos_pub, 1000, ((10, 700),)) rospy.sleep(1) position[2] += 0.05 ret = kinematics_control.set_pose_target(position, 80) if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, angle))) rospy.sleep(1.5) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 740), (3, 100), (4, 260), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 150), (2, 635), (3, 100), (4, 260), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 150), (2, 600), (3, 125), (4, 175), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 150), (2, 600), (3, 125), (4, 175), (5, 500), (10, 200))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 540), (3, 220), (4, 50), (5, 500), (10, 200))) rospy.sleep(2) self.stamp = time.time() self.moving = False |
Move Above The Block
Determine the rotation angle for the gripping operation, ensuring it stays within the range of ±45°.
79 80 | angle = angle % 90 angle = angle - 90 if angle > 40 else (angle + 90 if angle < -45 else angle) |
Increase the Z-axis coordinate of the target position (position) by 0.05m. Specify the gripping target position and set the pitch angle of the robotic arm to 80°.
98 99 | position[2] += 0.05 ret = kinematics_control.set_pose_target(position, 80) |
If ret[1] is not empty, employ the set_servos function to transmit information to each servo, prompting the robotic arm to move to a position 0.05m above the specified target location.
100 101 102 | if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, angle))) rospy.sleep(1.5) |
Grip the Block
Compute and set the target angle of servo 5.
87 88 89 90 | angle = 500 + int(1000 * (angle + ret[3][-1]) / 240) print('ret',ret) bus_servo_control.set_servos(self.servos_pub, 500, ((5, angle),)) rospy.sleep(0.5) |
Raise the Z-axis coordinate of the target position (position) by 0.05m. Specify the gripping target position and set the pitch angle of the robotic arm to 80°.
91 92 | position[2] -= 0.05 ret = kinematics_control.set_pose_target(position, 80) |
If ret[1] is not empty, employ the set_servos function to transmit information to each servo, prompting the robotic arm to move to a position 0.05m below the target location and grip the block.
100 101 102 103 104 | if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, angle))) rospy.sleep(1.5) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 740), (3, 100), (4, 260), (5, 500))) rospy.sleep(1) |
Put Down Block
Elevate the Z-axis coordinate of the target position (position) by 0.05m. Define the gripping target position and set the pitch angle of the robotic arm to 80°.
98 99 | position[2] += 0.05 ret = kinematics_control.set_pose_target(position, 80) |
Subsequent to gripping the wooden block, retreat by 0.05m. Employ set_servos to transmit servo control messages for the trajectory planning of the robotic arm, facilitating the placement of the wooden block at the specified location.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, angle))) rospy.sleep(1.5) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 740), (3, 100), (4, 260), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 150), (2, 635), (3, 100), (4, 260), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 150), (2, 600), (3, 125), (4, 175), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 150), (2, 600), (3, 125), (4, 175), (5, 500), (10, 200))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 540), (3, 220), (4, 50), (5, 500), (10, 200))) rospy.sleep(2) self.stamp = time.time() self.moving = False |
image_proc Function
Retrieve synchronized RGB images, depth images, and camera messages from the self.queue.
121 122 | def image_proc(self): ros_rgb_image, ros_depth_image, camera_info = self.queue.get(block=True) |
Create a NumPy array named rgb_image to store RGB image data. Extract data from ros_rgb_image and construct it based on the image height, width, and data type.
Create a NumPy array named depth_image to store depth image data. Extract data from ros_depth_image and construct it based on the image height and width.
Obtain camera parameters and the height and width of the depth image.
123 124 125 126 127 128 129 | try: rgb_image = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) K = camera_info.K ih, iw = depth_image.shape[:2] |
Acquire Coordinate of The Smallest Depth Pixel
Create a NumPy array named rgb_image to store RGB image data. Extract data from ros_rgb_image and construct it based on the image height, width, and data type.
Create a NumPy array named depth_image to store depth image data. Extract data from ros_depth_image and construct it based on the image height and width.
Obtain camera parameters and the height and width of the depth image.
132 133 134 135 136 | depth = depth_image.copy() depth[380:400, :] = np.array([[55555,]*640]*20) depth = depth.reshape((-1, )).copy() depth[depth<=100] = 55555 min_index = np.argmin(depth) |
Calculate the row coordinate min_y of the pixel with the minimum depth value by using integer division. Divide min_index by w (image width), resulting in the row coordinate min_y. Integer division truncates the decimal part, so the result is an integer representing the row position of the pixel with the minimum depth value in the depth image.
Multiply min_y by w (image width), then subtract the result from min_index. This way, you can obtain the column coordinate min_x of the pixel with the minimum depth value.
136 137 138 | min_index = np.argmin(depth) min_y = min_index // iw min_x = min_index - min_y * iw |
Preprocess Image
Assign the values of target_point to min_x and min_y, respectively.
Use np.clip to constrain the values in the depth image between 0 and 2000; values exceeding this range will be truncated.
Convert the data type of the depth image to np.float64. Normalize the depth values to a range between 0 and 255 for subsequent visualization.
Set pixel values in the depth image greater than min_dist + 10 to 0.
Re-normalize and convert the updated depth image to a range between 0 and 255.
140 141 142 143 144 | min_dist = depth_image[min_y, min_x] sim_depth_image = np.clip(depth_image, 0, 2000).astype(np.float64) / 2000 * 255 depth_image = np.where(depth_image > min_dist + 10, 0, depth_image) sim_depth_image_sort = np.clip(depth_image, 0, 2000).astype(np.float64) / 2000 * 255 depth_gray = sim_depth_image_sort.astype(np.uint8) |
Apply Gaussian blur to the depth image for smoothing and noise reduction.
Convert the depth image to a binary image, where pixels with depth values less than 1 are set to 0, and the rest are set to 255.
Perform an erosion operation on the binary image to remove small noise or fill small holes in the image.
Dilate the eroded image to restore the shape of the original objects.
Visualize the depth image as a colored image using the JET colormap.
144 145 146 147 148 149 | depth_gray = sim_depth_image_sort.astype(np.uint8) depth_gray = cv2.GaussianBlur(depth_gray, (5, 5), 0) _, depth_bit = cv2.threshold(depth_gray, 1, 255, cv2.THRESH_BINARY) depth_bit = cv2.erode(depth_bit, np.ones((5, 5), np.uint8)) depth_bit = cv2.dilate(depth_bit, np.ones((3, 3), np.uint8)) depth_color_map = cv2.applyColorMap(sim_depth_image.astype(np.uint8), cv2.COLORMAP_JET) |
Convert Coordinate
Utilizing OpenCV to identify contours in the depth image and initializing variables to store the shape, text information, and numerical information of detected objects.
151 152 153 154 155 | contours, hierarchy = cv2.findContours(depth_bit, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) shape = 'none' txt = "" z = 0 largest = None |
Iterating through each contour in the list of contours obtained from the cv2.findContours function using a for loop.
If the contour value is less than 500, using the cv2.minEnclosingCircle function to find the minimum enclosing circle for the contour. The returned (cx, cy) represents the center coordinates of the circle, and radius is the radius of the circle.
Drawing a red circle on the depth image with the center at (int(cx), int(cy)) and a radius of int(radius) to mark the detected object.
156 157 158 159 160 161 162 | for obj in contours: area = cv2.contourArea(obj) if area < 500 or self.moving is True: continue #cv2.drawContours(depth_color_map, obj, -1, (255, 255, 0), 4) # 绘制轮廓线(draw contour line) (cx, cy), radius = cv2.minEnclosingCircle(obj) cv2.circle(depth_color_map, (int(cx), int(cy)), int(radius), (0, 0, 255), 2) |
Using depth_pixel_to_camera to convert the pixel coordinates (cx, cy) in the depth image to a three-dimensional coordinate point position in the camera coordinate system, dividing it by 1000.0 to convert the depth value from millimeters (mm) to meters (m). (K[0], K[4], K[2], K[5]) is a tuple containing camera intrinsic parameters for coordinate transformation.
Increasing the Z coordinate of the calculated three-dimensional coordinate point position by 0.03 meters. This is done to elevate the position of the target slightly for sufficient operational space.
The np.matmul function performs matrix multiplication, multiplying self.hand2cam_tf_matrix by the coordinate transformation matrix generated by xyz_euler_to_mat to obtain pose_end, representing the end-effector relative coordinates.
Using np.matmul again to multiply self.endpoint by pose_end, transforming the end-effector relative coordinates to the robotic arm’s world coordinate system.
Extracting the pose information of the calculated robotic arm end-effector from the coordinate transformation matrix world_pose and storing it separately in pose_t (translation vector) and pose_R (rotation matrix).
164 165 166 167 168 169 | position = depth_pixel_to_camera((cx, cy), depth_image[int(cy), int(cx)] / 1000.0, (K[0], K[4], K[2], K[5])) position[2] = position[2] + 0.03 pose_end = np.matmul(self.hand2cam_tf_matrix, xyz_euler_to_mat(position, (0, 0, 0))) # 转换的末端相对坐标(relative coordinates of the converted end) world_pose = np.matmul(self.endpoint, pose_end) # 转换到机械臂世界坐标(convert to the robotic arm's world coordinates) pose_t, pose_R = mat_to_xyz_euler(world_pose) pose_t[1] += 0.01 |
Checking if the Z coordinate of the calculated object position is greater than z; if the condition is met, assigning the current contour object obj and its corresponding position information pose_t to the largest variable. Updating min_x, min_y, and txt variables.
170 171 172 173 174 | if pose_t[2] > z: largest = obj, pose_t min_x = cx min_y = cy txt = 'Dist: {}mm'.format(depth_image[int(cy), int(cx)]) |
Detect Distance from Object and Invoke Gripping Action
Check if largest is empty, calculate the distance dist between the current detected position pose_t and the last detected position self.last_position, and print the distance.
If the distance between the detected position and the previous position is less than 0.002 meters and the Z coordinate is greater than 0.009 meters, check if more than 0.5 seconds have elapsed since the last operation. If true, update the timestamp self.stamp and perform the following actions:
Assign cv2.minAreaRect(obj) to ret and set self.moving to True.
Create a new thread, invoke the self.pick method, and pass the object’s position pose_t and rotation angle rect[2] as parameters. Format the X, Y, Z coordinates of the object’s position into a string and store it in the txt variable.
If the above conditions are not met, update the timestamp self.stamp to the current time.
176 177 178 179 180 181 182 183 184 185 186 187 188 189 | if largest is not None: obj, pose_t = largest dist = math.sqrt((self.last_position[0] - pose_t[0]) ** 2 + (self.last_position[1] - pose_t[1])** 2 + (self.last_position[2] - pose_t[2])**2) print(dist) self.last_position = pose_t if dist < 0.002 and pose_t[2] > 0.009: if time.time() - self.stamp > 0.5: self.stamp = time.time() rect = cv2.minAreaRect(obj) self.moving = True threading.Thread(target=self.pick, args=(pose_t, rect[2])).start() txt = 'X:{:.3f} Y:{:.3f} Z:{:.3f}'.format(pose_t[0], pose_t[1], pose_t[2]) else: self.stamp = time.time() |
Mark the Position of Depth Value on Depth Pseudocolored Image
Draw a filled black circle and a smaller white circle on the depth pseudocolored image, marking the position of the minimum depth value. The parameters correspond to the processed image name, center coordinates, circle radius, circle color, and -1 indicating filling the entire circle. The white circle is slightly smaller than the black circle to serve as a marker.
Display text indicating the depth values on the depth pseudocolored image, with a larger black text and a slightly offset smaller gray text. The parameters correspond to the processed image, text content, starting position, font, size, color, line width, and -1 indicating antialiased line drawing mode for smoother lines.
190 191 192 193 194 | self.last_shape = shape cv2.circle(depth_color_map, (int(min_x), int(min_y)), 8, (32, 32, 32), -1) cv2.circle(depth_color_map, (int(min_x), int(min_y)), 6, (255, 255, 255), -1) cv2.putText(depth_color_map, txt, (11, ih-20), cv2.FONT_HERSHEY_PLAIN, 2.0, (32, 32, 32), 6, cv2.LINE_AA) cv2.putText(depth_color_map, txt, (10, ih-20), cv2.FONT_HERSHEY_PLAIN, 2.0, (240, 240, 240), 2, cv2.LINE_AA) |
Mark the Position of Depth Value on BGR Image
Crop a region from the original RGB image rgb_image, specifically the portion between pixel coordinates 40 rows to 440 rows. Subsequently, convert the cropped RGB image to the BGR color space.
Display text indicating the depth values on the BGR image, with a larger black text and a slightly offset smaller gray text. The parameters correspond to the processed image, text content, starting position, font, size, color, line width, and -1 indicating antialiased line drawing mode for smoother lines.
196 197 198 199 200 201 202 | bgr_image = cv2.cvtColor(rgb_image[40:440, ], cv2.COLOR_RGB2BGR) #cv2.circle(bgr_image, (int(min_x), int(min_y)), 8, (32, 32, 32), -1) #cv2.circle(bgr_image, (int(min_x), int(min_y)), 6, (255, 255, 255), -1) cv2.putText(bgr_image, txt, (11, ih - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (32, 32, 32), 6, cv2.LINE_AA) cv2.putText(bgr_image, txt, (10, ih - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (240, 240, 240), 2, cv2.LINE_AA) self.fps.update() |
Update image frame rate.
202 | self.fps.update() |
Concatenate the RGB image and the pseudocolored depth image horizontally to create a single image.
Display the final image on the depth window using cv2.imshow.
204 205 | result_image = np.concatenate([bgr_image, depth_color_map], axis=1) cv2.imshow("depth", result_image) |
Check for any keyboard input, and if a key is pressed, close the node.
208 209 | if key != -1: rospy.signal_shutdown('shutdown1') |
In the case of an exception in the code, capture and log the exception information as an error in the ROS log.
211 212 | except Exception as e: rospy.logerr('callback error:', str(e)) |
8.5 3D Space Gripping
8.5.1 Program Flow
Firstly, initialize the node and obtain the topic message of the RGB image and depth image.
Next, process the image to obtain the coordinate of the target pixel so as to track the target.
Subsequently, convert the obtained pixel coordinate into the robot arm world coordinate.
Lastly, plan the trajectory for the robot arm enabling the robot arm to place the item to corresponding position.
8.5.2 Operation Steps
Note
The input command should be case sensitive, and the keywords can be complemented using Tab key.
(1) Double-click
to open the command-line terminal, and execute the following command to disable the auto-start service.
~/.stop_ros.sh
(2) Run the command and hit Enter to run the game program.
roslaunch jetarm_6dof_rgbd_cam track_and_grab.launch
(3) If you need to terminate the running program, use short-cut Ctrl+C.
(4) After completing the game experience, you must start the app service
(if not started, it will affect the functionality of subsequent app features). In the terminal interface, enter the following command and press Enter to start the APP service:
sudo systemctl start start_app_node.service
(5) Once the service is started, the robotic arm will return to its initial position, and the buzzer will emit a beep.
8.5.3 Program Outcome
Upon initiating the game, the robot arm will align the camera to face forward for detecting the colored block (the program defaults to recognizing the red color). Subsequently, the camera initiates tracking of the item. Once the item’s pose is acquired, the robot arm proceeds to pick it up and relocate it to the designated position.
Note
If the item is placed within the minimal detection range, the camera cannot measure the item’s distance. In this scenario, the measurement result will be set to 0, and the terminal will display TOO CLOSE !!!. To resolve this issue, ensure the target block is positioned away from the camera for proper gripping.
8.5.4 Launch File Analysis
Launch file is saved in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam /launch/track_and_grab.launch
1 2 3 4 5 6 7 8 | <launch> <arg name="target_color" default="red" /> <include file="$(find jetarm_bringup)/launch/base.launch"/> <node name="track_and_grab" pkg="jetarm_6dof_rgbd_cam" type="track_and_grab.py" output="screen" respawn="true"> <param name="target_color" value="$(arg target_color)" /> </node> </launch> |
Set the target color to red for example.
2 | <arg name="target_color" default="red" /> |
Launch the base.launch file, and invoke the robot arm basic settings.
3 | <include file="$(find jetarm_bringup)/launch/base.launch"/> |
Launch track_and_grab.py source code using the node.
5 6 7 | <node name="track_and_grab" pkg="jetarm_6dof_rgbd_cam" type="track_and_grab.py" output="screen" respawn="true"> <param name="target_color" value="$(arg target_color)" /> </node> |
8.5.5 Python Source Code Analysis
The source code file is saved in /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof/jetarm_6dof_rgbd_cam /scripts/track_and_grab.py
The program flowchart is as below:
As depicted in the above diagram, the logical flow of the program primarily involves obtaining RGB and depth images from the camera, synchronizing their timestamps, processing the acquired images, tracking the target based on pixel coordinates, calculating the actual object position when the target comes to a halt, and employing trajectory planning for grasping and placing the object.
Import Feature Pack
Import the required modules through import statements:
cv2 for OpenCV image processing.
rospy for ROS communication.
numpy for array operations.
math for mathematical calculations.
message_filters for implementing message filtering and synchronization, crucial for handling sensor data and messages in ROS.
Import the Image message type and rename it as RosImage.
Import the SetBool service message type from the std_srvs.srv module.
Import the fps function module from the vision_utils module for calculating frame rates.
Use the queue module for executing queue-related operations in a multi-threaded context.
Import the MultiRawIdPosDur ROS message type from the hiwonder_interfaces.msg module. This type is utilized for defining custom ROS messages, facilitating data transmission and communication within the ROS system.
Import the necessary modules for coordinate system transformation from the vision_utils module.
Import the pid, bus_servo_control, sdk_client, and tone modules from the jetarm_sdk module.
Import the library for kinematic control from the jetarm_kinematics module.
Define a variable named CONFIG_NAME.
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import os import cv2 import rospy import numpy as np import threading import math import time from sensor_msgs.msg import Image as RosImage from sensor_msgs.msg import CameraInfo from visualization_msgs.msg import Marker from hiwonder_interfaces.msg import MultiRawIdPosDur from hiwonder_interfaces.srv import GetRobotPose from vision_utils import fps, colors, get_area_max_contour from vision_utils import xyz_quat_to_mat, xyz_euler_to_mat, pixels_to_world, box_center, mat_to_xyz_euler, distance, extristric_plane_shift from jetarm_sdk import pid, bus_servo_control, common import message_filters from jetarm_kinematics import kinematics_control from sensor_msgs.msg import Image as RosImage from geometry_msgs.msg import PoseStamped from std_srvs.srv import SetBool import queue CONFIG_NAME="/config" |
depth_pixel_to_camera Function
Function for converting pixel coordinates from the depth image to three-dimensional coordinates in the camera coordinate system.
pixel_coords: Pixel coordinates in the depth image, represented as a tuple (px, py), indicating the pixel’s position in the depth image.
depth: Depth value corresponding to the pixel.
intrinsics: Camera intrinsics, a tuple containing (fx, fy, cx, cy), representing the camera’s focal length and optical center.
The function calculates the three-dimensional point coordinates (x, y, z) using a formula and returns a NumPy array containing these values.
31 32 33 34 35 36 37 | def depth_pixel_to_camera(pixel_coords, depth, intrinsics): fx, fy, cx, cy = intrinsics px, py = pixel_coords x = (px - cx) * depth / fx y = (py - cy) * depth / fy z = depth return np.array([x, y, z]) |
Initiate TrackAndGrapNode Class (Tracking and Gripping Class)
Print relevant prompt messages in the terminal. Initiate the RgbDepthImageNode class, and within the loop, call the node.image_proc() method until Ctrl+C is pressed or the ROS node is closed, then output the log information shutdown2.
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | if __name__ == "__main__": print(''' ********************************************* * * * 此程序需要订阅深度相机节点,开启前请确保已(before starting this program, make sure to subscribe to the depth camera node) * * 开启摄像头节点, 通过rostopic list可查看是看(the camera node has been started, you can verify this by using "rostopic list") * * 否有rgbd_cam相关节点,成功运行可看到终端(is there a rgbd_cam related node? You should see the terminal output upon successful execution) * * running ... * * * ********************************************* ''') try: node = TrackAndGrapNode() while not rospy.is_shutdown(): node.image_proc() except KeyboardInterrupt: rospy.loginfo("shutdown2") |
Initialization Function of RgbDepthImageNode Class
Initialize track_and_grap node.
106 107 108 | class TrackAndGrapNode: def __init__(self): rospy.init_node("track_and_grap", anonymous=True, log_level=rospy.INFO) |
Initialize the related variable.
109 110 111 112 113 114 115 116 117 | self.fps = fps.FPS() self.moving = False self.last_pitch_yaw = (0, 0) self.first_time = time.time() self.enable_disp = rospy.get_param('~enable_disp', True) self.color_ranges = rospy.get_param('/config/lab', {}) self.last_position = (0, 0, 0) self.stamp = time.time() |
Create Servo Publisher
The servos_pub creates a servo publisher, sending messages to the /controllers/multi_id_pos_dur topic. Here, MultiRawIdPosDur is the message type used to control the positions and durations of multiple servos.
The set_servos function from the bus_servo_control module is utilized to set the initial position and duration of the servos. The parameters include the publisher instance, servo runtime (in milliseconds), and a tuple containing the IDs and rotation angles (ranging from 1-1000) of multiple servos.
118 119 120 121 | self.servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(3) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 720), (3, 100), (4, 120), (5, 500), (10, 200))) rospy.sleep(2) |
Retrieve the parameter named CONFIG_NAME from the ROS Parameter Server using rospy.get_param, and store it in the config variable. Subsequently, extract the parameter named hand2cam_tf_matrix from the config variable and store it in self.hand2cam_tf_matrix for subsequent use in the class methods.
123 124 | config = rospy.get_param(CONFIG_NAME) self.hand2cam_tf_matrix = config['hand2cam_tf_matrix'] |
(1) Set target_colorto red, and print the information of the tracked target.
(2) Create a color tracker named self.tracker. Utilize the self.target_color parameter obtained in the last line to initialize the color tracker.
(3) Call the color processing class and configure the tracked target.
(4) Use rospy.wait_for_service function to wait for the /rgbd_cam/set_ldp service to initiate.
126 127 128 129 | self.target_color = rospy.get_param('~target_color', 'red') rospy.loginfo("正在设置将要追踪的目标认识为" + self.target_color) self.tracker = ColorTracker(self.target_color) rospy.wait_for_service('/rgbd_cam/set_ldp') |
Color Processing Class
(1) Configure the tracking color with the provided parameter target_color.
(2) Set the values of the PID controllers for yaw (pitch angle) and pitch (rotation angle).
(3) Set the initial values for the yaw (pitch angle) and pitch (rotation angle) of the robotic arm.
40 41 42 43 44 45 46 | class ColorTracker: def __init__(self, target_color): self.target_color = target_color self.pid_yaw = pid.PID(20.5, 1.0, 1.2) self.pid_pitch = pid.PID(20.5, 1.0, 1.2) self.yaw = 500 self.pitch = 150 |
Image Processing and Tracking Function porc
(1) Initialize image.
48 49 50 51 52 | def proc (self, source_image, result_image, color_ranges): h, w = source_image.shape[:2] color = color_ranges[self.target_color] img = cv2.resize(source_image, (int(w/2), int(h/2))) |
Obtain the height and width of the image.
Obtain the color threshold of the tracked color from color_ranges.
Adjust the image size and scale both its height and width by half.
(2) Apply Gaussian blur to the image using the GaussianBlur() function from the cv2 library to filter out noise.
The first parameter img is the input image that has already been resized.
The second parameter (3, 3) specifies the size of the Gaussian convolution kernel, where both the height and width of the kernel must be positive and odd.
The third parameter 3 indicates the standard deviation of the Gaussian kernel in the horizontal direction.
53 | img_blur = cv2.GaussianBlur(img, (3, 3), 3) # 高斯模糊(Gaussian blur) |
(3) Convert the RGB color space into LAB color space.
54 | img_lab = cv2.cvtColor(img_blur, cv2.COLOR_RGB2LAB) # 转换到 LAB 空间(convert to the LAB space) |
(4) Perform thresholding, and utilize inRange() function in cv2 library to apply the thresholding processing on the image.
55 | mask = cv2.inRange(img_lab, tuple(color['min']), tuple(color['max'])) # 二值化(binarization) |
(5) Implement erosion and dilation operations on the image to smooth the contours and facilitate the subsequent search for target contours.
57 58 59 | # 平滑边缘,去除小块,合并靠近的块(smooth edges, remove small blocks, and merge adjacent blocks) eroded = cv2.erode(mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) |
The erode() function is utilized for image erosion. In the provided code eroded = cv2.erode(mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))), the parameters in the parentheses are explained as follows:
The first parameter mask represents the input image.
The second parameter cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) determines the nature of the operation, serving as the structuring element or kernel. Within the parentheses, the first parameter denotes the kernel shape, while the second parameter specifies the kernel size.
The dilate() function is employed for image dilation. The parameters inside the parentheses have the same meanings as those in the erode() function.
(6) Retrieve the contour with the maximum area by invoking the findContours() function from the cv2 library. This function is used to locate the largest contour corresponding to the color identified as the target in the image.
61 62 63 | # 找出最大轮廓(find out the largest contour) contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] #max_contour_area = get_area_max_contour(contours, 10) |
The first parameter dilatedrepresents the input image.
The second parameter cv2.RETR_EXTERNAL is contour retrieving mode.
The third parameter cv2.CHAIN_APPROX_NONE is contour approximation method.
(7) Circle the tracked target.
76 77 78 79 80 81 | if min_c is not None: (center_x, center_y), radius = cv2.minEnclosingCircle(min_c[0]) # 最小外接圆(the minimum circumcircle) # 圈出识别的的要追踪的色块(outline the identified color blocks to be tracked) circle_color = colors.rgb[self.target_color] if self.target_color in colors.rgb else (0x55, 0x55, 0x55) cv2.circle(result_image, (int(center_x * 2), int(center_y * 2)), int(radius * 2), circle_color, 2) |
(8) Update PID data by applying the PID algorithm to refresh the coordinates along the X and Y axes.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | center_x = center_x * 2 center_x_1 = center_x / w if abs(center_x_1 - 0.5) > 0.02: # 相差范围小于一定值就不用再动了(if the difference range is smaller than a certain value, there's no need to move anymore) self.pid_yaw.SetPoint = 0.5 # 我们的目标是要让色块在画面的中心, 就是整个画面的像素宽度的 1/2 位置(our goal is to make the color block appear in the center of the frame, which is at half the pixel width of the entire frame) self.pid_yaw.update(center_x_1) self.yaw = min(max(self.yaw + self.pid_yaw.output, 0), 1000) else: self.pid_yaw.clear() # 如果已经到达中心了就复位一下 pid 控制器(if it has already reached the center, reset the PID controller) center_y = center_y * 2 center_y_1 = center_y / h if abs(center_y_1 - 0.5) > 0.02: self.pid_pitch.SetPoint = 0.5 self.pid_pitch.update(center_y_1) self.pitch = min(max(self.pitch + self.pid_pitch.output, 100), 720) else: self.pid_pitch.clear() # rospy.loginfo("x:{:.2f}\ty:{:.2f}".format(self.x , self.y)) return (result_image, (self.pitch, self.yaw), (center_x, center_y), radius * 2) else: |
When the x-axis coordinate of the center point of the target color (center_x) minus the x-axis coordinate of the screen center (0.5) is greater than 0.02, it means the target is not at the center of the screen. When it is less than 0.02, it indicates that the target is at the center of the screen.
If the target is not at the center of the screen, adjust the yaw angle (ID4) of the robotic arm using the pid_yaw. After the adjustment, output the value of self.yaw using the function min(max(self.yaw + self.pid_yaw.output, 0), 1000), which sets the yaw angle to be between 0 and 1000. This is achieved by taking the maximum value between the target value and 0, and then the minimum value between the target value and 1000.
If the target is at the center of the screen, reset the PID controller directly using the clear() function. The same logic applies to the pitch angle.
(9) Return the image info, and the offset of pitch angle and yaw angle.
103 | return (result_image, None, None, 0) |
Receive Image Info
(1) Create rgb_sub image subscriber, and use message_filters.Subscriber to create a message subscriber.
The first parameter /rgbd_cam/color/image_raw represents the image topic name.
The second parameter RosImage refers to the message type.
The third parameter queue_size=1 specify the size of message queue.
(2) Create depth_sub image subscriber, and use message_filters.Subscribe to create a message subscriber.
The first parameter, /rgbd_cam/depth/image_raw, denotes the topic name for the image.
The second parameter specifies the message type as RosImage.
The third parameter, queue_size=1 ,specifies the size of the message queue.
(3) Create info_sub image subscriber, and use message_filters.Subscriber to create an image subscriber.
The first parameter /rgbd_cam/depth/camera_info represents the image topic name.
The second parameter CameraInfo is the message type.
The third parameter queue_size=1 specifies the size of the message queue.
136 137 138 | rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', RosImage, queue_size=1) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', RosImage, queue_size=1) info_sub = message_filters.Subscriber('/rgbd_cam/depth/camera_info', CameraInfo, queue_size=1) |
Synchronize Timestamp
Establish a time synchronizer to synchronize messages from the rgb_sub and depth_sub topics. The 3 signifies a maximum buffer size of 3 messages, and 0.03 implies that messages with a timestamp difference of no more than 0.03 seconds will be considered synchronized.
142 143 144 145 146 147 148 | # 同步时间戳, 时间允许有误差在0.03s(synchronize timestamps, allowing for a time error of up to 0.03 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub, info_sub], 3, 0.03) sync.registerCallback(self.multi_callback) #执行反馈函数(execute feedback function) self.queue = queue.Queue(maxsize=1) self.endpoint = None self.ttt = time.time() + 10 common.loginfo("TrackAndGrapNode initailized") |
multi_callback Feedback Function
The multi_callback receives messages from ros_rgb_image, ros_depth_image, and camera_info. It checks if self.queue is empty. If the queue is empty, it places a tuple containing synchronized ros_rgb_image, ros_depth_image, and camera_info into the queue. This tuple signifies the successful synchronization of the three images.
150 151 152 | def multi_callback(self, ros_rgb_image, ros_depth_image, depth_camera_info): if self.queue.empty(): self.queue.put_nowait((ros_rgb_image, ros_depth_image, depth_camera_info)) |
get_endpoint Function
If the node shutdown signal is not received, the code enters a while loop. It creates a rospy.ServiceProxy to call the /kinematics/get_current_pose service, obtaining the current pose information of the robot, including position and orientation, which is then stored in the endpoint variable. The xyz_quat_to_mat function is then invoked to convert the position and orientation information in endpoint into a matrix, and the result is stored in the self.endpoint member variable for subsequent coordinate transformations.
154 155 156 157 158 | def get_endpoint(self): endpoint = rospy.ServiceProxy('/kinematics/get_current_pose', GetRobotPose)().pose self.endpoint = xyz_quat_to_mat([endpoint.position.x, endpoint.position.y, endpoint.position.z], [endpoint.orientation.w, endpoint.orientation.x, endpoint.orientation.y, endpoint.orientation.z]) return self.endpoint |
Gripping & Placing Function
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | def pick(self, position): ret = kinematics_control.set_pose_target(position, 30) if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1000, ((1, ret[1][0]), )) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, ret[1][4]))) rospy.sleep(1.5) bus_servo_control.set_servos(self.servos_pub, 500, ((10, 700),)) rospy.sleep(1) position[2] += 0.03 ret = kinematics_control.set_pose_target(position, 30) if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1000, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, ret[1][4]))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 720), (3, 100), (4, 120), (5, 500), (10, 700))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 125), (2, 635), (3, 120), (4, 200), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 500, ((1, 125), (2, 550), (3, 120), (4, 150), (5, 500))) rospy.sleep(0.5) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 125), (2, 550), (3, 120), (4, 170), (5, 500), (10, 200))) rospy.sleep(1) #bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 125), (2, 720), (3, 120), (4, 170), (5, 500), (10, 200))) #rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 720), (3, 100), (4, 150), (5, 500), (10, 200))) rospy.sleep(2) self.tracker.yaw = 500 self.tracker.pitch = 150 self.tracker.pid_yaw.clear() self.tracker.pid_pitch.clear() self.stamp = time.time() self.first_time = time.time() + 2 self.moving = False |
(1) Obtain the inverse kinematics solution by utilizing kinematics_control.set_pose_target to achieve the target position of the colored block, storing it in the ret variable.
161 | ret = kinematics_control.set_pose_target(position, 30) |
(2) If the length of the second element in the ret variable is greater than 0, pass the obtained inverse kinematics solution to the bus_servo_control.set_servos function, controlling the robotic arm to grasp the colored block.
162 163 164 165 166 | if len(ret[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1000, ((1, ret[1][0]), )) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret[1][0]),(2, ret[1][1]), (3, ret[1][2]),(4, ret[1][3]), (5, ret[1][4]))) rospy.sleep(1.5) |
(3) For placing the colored block, perform trajectory planning by setting the pulse widths for each servo in the bus_servo_control.set_servos function, guiding the robotic arm to position the block at the target location.
175 176 177 178 179 180 181 182 183 184 185 186 | bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 720), (3, 100), (4, 120), (5, 500), (10, 700))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 125), (2, 635), (3, 120), (4, 200), (5, 500))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 500, ((1, 125), (2, 550), (3, 120), (4, 150), (5, 500))) rospy.sleep(0.5) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 125), (2, 550), (3, 120), (4, 170), (5, 500), (10, 200))) rospy.sleep(1) #bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 125), (2, 720), (3, 120), (4, 170), (5, 500), (10, 200))) #rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 720), (3, 100), (4, 150), (5, 500), (10, 200))) rospy.sleep(2) |
(4) Set the yaw (yaw angle) of the self.tracker object to 500.
(5) Set the pitch (pitch angle) of the self.tracker object to 260.
(6) Invoke the clear() method on the pid_yaw attribute of the self.tracker object to clear control parameters or variables related to yaw.
(7) Clear control parameters or variables related to pitch by calling the clear() method on the pid_pitch attribute of the self.tracker object.
(8) Set the self.stamp variable to the current timestamp, obtained using the time.time() function, for time recording purposes.
187 188 189 190 191 192 193 | self.tracker.yaw = 500 self.tracker.pitch = 150 self.tracker.pid_yaw.clear() self.tracker.pid_pitch.clear() self.stamp = time.time() self.first_time = time.time() + 2 self.moving = False |
image_process Function
Obtain the synchronized RGB image and depth image from the self.queue queue.
195 196 | def image_proc(self): ros_rgb_image, ros_depth_image, depth_camera_info = self.queue.get(block=True) |
A NumPy array named rgb_image is created to store RGB image data. Data is extracted from ros_rgb_image, and the array is constructed based on image height, width, and data type.
Similarly, a NumPy array named depth_image is created to store depth image data. Data is extracted from ros_depth_image, and the array is constructed based on image height and width.
Image data is copied from rgb_image to create result_image.
The height and width of the depth image are obtained.
197 198 199 200 201 202 | try: rgb_image = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data)[40:440,] depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) result_image = np.copy(rgb_image) h, w = depth_image.shape[:2] |
Convert Depth Image into Color Depth Image
(1) Replace pixels with depth values less than or equal to 0 with 55555 to mark invalid depth values.
(2) Restrict the depth values in the depth image to the range of 0 to 2000 and convert their data type to float64. This is done to map the depth values to a specific range for visualization.
(3) Map the depth values to the range of 0 to 255 to create a colored depth map.
(4) Convert rgb_image from the RGB color space to the BGR color space. Utilize the cv2.applyColorMap function from OpenCV to apply the mapped depth values as a colored depth map using the JET color mapping. The result is stored in depth_color_map.
203 204 205 206 207 208 209 210 211 | depth = np.copy(depth_image).reshape((-1, )) depth[depth<=0] = 55555 sim_depth_image = np.clip(depth_image, 0, 2000).astype(np.float64) sim_depth_image = sim_depth_image / 2000.0 * 255.0 bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) depth_color_map = cv2.applyColorMap(sim_depth_image.astype(np.uint8), cv2.COLORMAP_JET) |
Determine Whether The Target is Tracked
Firstly, check for the availability of the target tracker self.tracker and ensure that the robotic arm self.moving is not in motion. Invoke the proc method of the target tracker to process the colored image. Return the updated image result_image, the position information of the target p_y, the center coordinates of the target center, and the radius of the target r.
213 214 | if self.tracker is not None and self.moving == False and time.time() > self.ttt: result_image, p_y, center, r = self.tracker.proc(rgb_image, result_image, self.color_ranges) |
Track the Target and Acquire Coordinate of Target Center
Check if the position information p_y of the target is not None. If the target is successfully tracked, perform the following operations:
(1) Call the set_servos method of the bus_servo_control object to control the angles of the servos. Set the angle of servo 1 to p_y[1] and the angle of servo 4 to p_y[0].
(2) Calculate center_x and center_y: Make minor adjustments to the coordinates of the target’s center, moving the target’s center 5 pixels to the right and (r - 20) pixels up. Then, check if center_x and center_y exceed the boundaries of the image. If they do, restrict them to the maximum size of the image, which is (639, 399). Print the x-coordinate of the target’s center for debugging purposes.
215 216 217 218 219 220 221 222 223 224 225 | if p_y is not None: bus_servo_control.set_servos(self.servos_pub, 20, ((1, p_y[1]), (4, p_y[0]))) # print(p_y) center_x, center_y = center center_x += 5 center_y += r - 20 if center_x > 639: center_x = 639 if center_y > 399: center_y = 399 #print("center_x:", center_x) |
Control Robotic Arm to Move Towards the Target Object and Perform the Grasping Operation
Check if the current target position p_y and the deviation from the previously recorded position self.last_pitch_yaw are both less than 1. If the deviations are small, indicating little change in the target’s position, perform the following steps:
If the time elapsed since the last action is more than 2 seconds, execute the following:
Update the timestamp: self.stamp = time.time() to record the current time.
Get the depth information of the target from the depth image: dist = depth_image[int(center_y), int(center_x)] / 1000.0 and convert it to meters.
Compensate for depth information: dist += 0.015, a small adjustment typically caused by the object’s radius.
Retrieve the intrinsic matrix K of the depth camera: K = depth_camera_info.K.
Call the get_endpoint method to obtain the position of the robotic arm’s end effector.
Use the depth_pixel_to_camera method to transform the target’s image coordinates (center_x, center_y), depth information dist, and the intrinsic matrix of the depth camera (K[0], K[4], K[2], K[5]) into coordinates in the camera coordinate system, denoted as position.
Adjust the X-axis coordinate slightly: position[0] -= 0.01, reducing it by 0.01 meters to account for a 1cm offset between the RGB and depth cameras.
Transform the coordinates position and pose (0, 0, 0) into the end effector’s relative coordinates: pose_end using the xyz_euler_to_mat method.
Multiply pose_end by the transformation matrix of the robotic arm’s world coordinates to obtain the end effector’s position in the robotic arm’s world coordinate system: world_pose.
Convert world_pose into position pose_t and orientation pose_R using the mat_to_xyz_euler method.
Make slight adjustments to pose_t, including displacements in the X, Y, and Z-axis coordinates.
Adjust the Y-axis coordinate: pose_t[1] = pose_t[1] * factor_y, considering deviations from the central line to maintain the robotic arm’s posture.
Compensate for the Z-axis coordinate height, accounting for gravity effects. The code uses linear compensation: no height compensation when the end effector is less than 0.2 meters from the center of the robotic arm. Beyond 0.2 meters, the greater the distance, the more height compensation is applied, regulated by the factor_z parameter.
Set the robotic arm state to moving: self.moving = True.
Use a thread to execute the self.pick method, passing the position pose_t as a parameter to perform the grasping operation.
Finally, print the robotic arm’s pose information as a string to the console for debugging.
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | if abs(self.last_pitch_yaw[0] - p_y[0]) < 5 and abs(self.last_pitch_yaw[1] - p_y[1]) < 5: if time.time() - self.stamp > 2: self.stamp = time.time() dist = depth_image[int(center_y),int(center_x)]/1000.0 dist += 0.015 # 物体半径补偿(object radius compensation) K = depth_camera_info.K self.get_endpoint() position = depth_pixel_to_camera((center_x, center_y), dist, (K[0], K[4], K[2], K[5])) position[0] -= 0.01 # rgb相机和深度相机tf有1cm偏移(the RGB camera and the depth camera have a 1cm offset in their transforms) pose_end = np.matmul(self.hand2cam_tf_matrix, xyz_euler_to_mat(position, (0, 0, 0))) # 转换的末端相对坐标(transform to end effector relative coordinates) world_pose = np.matmul(self.endpoint, pose_end) # 转换到机械臂世界坐标(transform to the robotic arm's world coordinates) pose_t, pose_R = mat_to_xyz_euler(world_pose) offset_x = 0.03 offset_y = 0.00 offset_z = 0.015 factor_y = 1.05 factor_z = 0.035 pose_t[0] += offset_x pose_t[1] += offset_y pose_t[2] += offset_z # 左右(Y轴), 左右幅度不足,机械臂离中心线越远偏离目标远越远的情况增加factor_y(left and right (Y-axis), insufficient left and right amplitudes, when the robotic arm is further from the centerline, the deviation from the target increases as well, increasing factor_y) pose_t[1] = pose_t[1] * factor_y |
Process Target Tracking Result
If the deviation in the target position exceeds 1, indicating a significant change in the target position or incorrect tracking, perform the following steps:
Update the timestamp: self.stamp = time.time() to record the current time.
Obtain the depth information of the target from the depth image (in millimeters): dist = depth_image[int(center_y), int(center_x)].
If dist < 100 (indicating the target is too close to the camera), execute the following:
Set the text message: txt = "TOO CLOSE !!!" to indicate that the target is too close.
Otherwise, if the target is at a distance from the camera, set the text message txt to Dist: xx mm, where xx is the distance to the target in millimeters.
Draw a white circle on the image to display the target position, with a radius of 5 pixels.
Use OpenCV’s cv2.putText to add text on the depth image, displaying the distance information of the target.
Update the last recorded target position for future iterations: self.last_pitch_yaw = p_y.
299 300 301 302 303 304 305 306 307 308 309 310 311 312 | else: self.stamp = time.time() dist = depth_image[int(center_y),int(center_x)] if dist < 100: txt = "TOO CLOSE !!!" else: txt = "Dist: {}mm".format(dist) cv2.circle(result_image, (int(center_x), int(center_y)), 5, (255, 255, 255), -1) cv2.circle(depth_color_map, (int(center_x), int(center_y)), 5, (255, 255, 255), -1) cv2.putText(depth_color_map, txt, (10, 400 - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (0, 0, 0), 10, cv2.LINE_AA) cv2.putText(depth_color_map, txt, (10, 400 - 20), cv2.FONT_HERSHEY_PLAIN, 2.0, (255, 255, 255), 2, cv2.LINE_AA) self.last_pitch_yaw = p_y else: self.stamp = time.time() |
Display Image & Handle Exception
Update the frame rate statistics using self.fps.update(), typically employed for monitoring program performance.
Display the frame rate information on the image using bgr_image = self.fps.show_fps(bgr_image), a helpful feature for debugging and performance monitoring.
Combine the RGB image result_image with the depth image depth_color_map horizontally, creating a composite image for simultaneous display in a single window.
Show the depth image in a window titled depth using cv2.imshow().
Wait for keyboard input with key = cv2.waitKey(1), anticipating the user to press a key. If any key is pressed, invoke rospy.signal_shutdown('shutdown1') to close the ROS node.
In case of an exception, log the error information using rospy.logerr for error analysis and debugging purposes.
316 317 318 319 320 321 322 323 324 325 326 | if self.enable_disp: self.fps.update() bgr_image = self.fps.show_fps(bgr_image) result_image = np.concatenate([cv2.cvtColor(result_image, cv2.COLOR_RGB2BGR), depth_color_map, ], axis=1) cv2.imshow("depth", result_image) key = cv2.waitKey(1) if key != -1: rospy.signal_shutdown('shutdown1') except Exception as e: rospy.logerr('callback error:', str(e)) |
8.6 3D Shape Sorting
8.6.1 Program Flow
First, initialize the node and obtain information about topics related to RGB and depth images.
Next, perform image processing by analyzing the number of corner points in the detected object contours to determine the shape of objects within the field of view.
Subsequently, acquire the pixel coordinates of the highest object within the field of view, perform coordinate system transformation on the obtained coordinates, converting them into the world coordinates of the robotic arm.
Finally, execute trajectory planning on the robotic arm to grasp the object and place it at the designated position.
8.6.2 Operation Steps
Note
The input command is case-sensitive, and the keywords can be autocompleted using the Tab key.
(1) Double-click
to open the command-line terminal, and execute the command below to disable the auto-start service.
~/.stop_ros.sh
(2) Run the following command, and hit Enter to execute the game program.
roslaunch jetarm_6dof_rgbd_cam shape_recognition_down.launch
(3) If you need to terminate the running program, use short-cut Ctrl+C.
(4) Following the above function, it is essential to enable the app service; otherwise, the app functions may be impacted. Execute the command below.
sudo systemctl start start_app_node.service
(5) After the robot arm returns to the initial pose, the robot arm will emit a beeping sound.
8.6.3 Program Outcome
Upon initiating the game, the robotic arm will identify rectangular prisms, cylinders, and spheres within the field of view. It will then proceed to grasp and categorize these objects, placing them in their respective positions.
8.6.4 Launch File Analysis
The Launch file is saved in : /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof/jetarm_6dof_rgbd_cam /launch/shape_recognition_down.launch
1 2 3 4 5 6 | <launch> <include file="$(find jetarm_bringup)/launch/base.launch"/> <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="shape_recognition_down.py" output="screen" respawn="true"> </node> </launch> |
Initiate base.launch file, and invoke the robot arm basic settings.
2 | <include file="$(find jetarm_bringup)/launch/base.launch"/> |
Initiate the shape_recognition_down.py source code file using the node.
4 5 6 | <node name="get_depth_rgb" pkg="jetarm_6dof_rgbd_cam" type="shape_recognition_down.py" output="screen" respawn="true"> </node> </launch> |
8.6.5 Python Source Code Analysis
The source code file is saved in: /home/ubuntu/jetarm/src/jetarm_example/src/jetarm_6dof_rgbd_cam/scripts/shape_recognition_down.py
The program flowchart is as below:
Based on the diagram above, the program’s logical flow primarily involves obtaining RGB and depth images from the camera, synchronizing their timestamps, processing the acquired images, determining object shapes based on detected contour corners, calculating object coordinates from the corresponding positions, and finally, utilizing trajectory planning for object grasping and placement.
Import Feature Pack
Import the required modules using the import statements:
cv2 for image processing with OpenCV.
rospy for ROS communication.
numpy for array operations.
message_filters for implementing message filtering and synchronization, essential for handling sensor data and messages in ROS.
Import Image message type from sensor_msgs.msg and rename it as RosImage.
Import SetBool service message type from the std_srvs.srv module.
Import the fps feature module from the vision_utils module for frame rate calculation.
Use queue for queue-related operations in multithreading.
Import the ROS message type named MultiRawIdPosDur from hiwonder_interfaces.msg for defining custom ROS messages, facilitating data transmission and communication within the ROS system.
Import modules from vision_utils for coordinate system transformation calculations.
Import pid, bus_servo_control, sdk_client, and tone modules from jetarm_sdk.
Import the library for kinematic control from jetarm_kinematics.
Import action_group_controller for action group control.
{lineno-start=}
import cv2
import rospy
import numpy as np
import message_filters
from sensor_msgs.msg import Image as RosImage
from sensor_msgs.msg import CameraInfo
from std_srvs.srv import SetBool
from vision_utils import fps
import threading
import queue
from hiwonder_interfaces.msg import MultiRawIdPosDur
from vision_utils import xyz_quat_to_mat, xyz_euler_to_mat, mat_to_xyz_euler
from jetarm_sdk import pid, bus_servo_control, sdk_client, tone
from jetarm_sdk.servo_controller import controller as actiongroup
from jetarm_kinematics import kinematics_control
Initiate RgbDepthImageNode Class (RGB Depth Image Class)
Print relevant prompt messages to the terminal. Initiate the RgbDepthImageNode class and within a loop, call the node.image_proc() method until either Ctrl+C is pressed or the ROS node is closed. Output log information indicating the shutdown (shutdown2).
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | if __name__ == "__main__": print(''' ********************************************* * * * 此程序需要订阅深度摄像头节点,开启前请确保 * * 已开启摄像头节点, 通过rostopic list可查看 * * 是否有rgbd_cam 相关节点,成功运行可看到终端 * * running ... * * * ********************************************* ''') try: node = RgbDepthImageNode() while not rospy.is_shutdown(): node.image_proc() except KeyboardInterrupt: rospy.loginfo("shutdown2") |
Initialization Function of RgbDepthImageNode Class
Initialize shape_recognition node and related variable.
38 39 40 41 42 43 44 45 46 47 48 | class RgbDepthImageNode: def __init__(self): rospy.init_node('shape_recognition', anonymous=True) self.fps = fps.FPS() self.last_shape = "none" self.moving = False self.count = 0 self.endpoint = None self.sdk = sdk_client.JetArmSDKClient() config = rospy.get_param(CONFIG_NAME) self.hand2cam_tf_matrix = config['hand2cam_tf_matrix'] |
Create Servo Publisher
The servos_pub creates a servo publisher, sending messages to the /controllers/multi_id_pos_dur topic. Here, MultiRawIdPosDur is the message type used to control the positions and durations of multiple servos.
The set_servos function from the bus_servo_control module is utilized to set the initial position and duration of the servos. The parameters include the publisher instance, servo runtime (in milliseconds), and a tuple containing the IDs and rotation angles (ranging from 1-1000) of multiple servos.
50 51 52 53 54 | self.servos_pub = rospy.Publisher('/controllers/multi_id_pos_dur', MultiRawIdPosDur, queue_size=1) rospy.sleep(3) rospy.wait_for_service('/kinematics/set_joint_value_target') self.goto_default() rospy.sleep(2) |
Wait /rgbd_cam/set_ldp Service to Initiate
Established a service proxy and utilized the SetBool message type to specify the service’s request and response formats. Sent False as the argument to the /rgbd_cam/set_ldp service.
55 56 | rospy.wait_for_service('/rgbd_cam/set_ldp') rospy.ServiceProxy('/rgbd_cam/set_ldp', SetBool)(False) |
Receive Image Info
(1) Create rgb_sub image subscriber, and use message_filters.Subscriber to create a message subscriber.
The first parameter /rgbd_cam/color/image_raw represents the image topic name.
The second parameter RosImage refers to the message type.
The third parameter queue_size=1 specify the size of message queue.
(2) Create depth_sub image subscriber, and use message_filters.Subscriber to create a message subscriber.
The first parameter, /rgbd_cam/depth/image_raw, denotes the topic name for the image.
The second parameter specifies the message type as RosImage.
The third parameter, queue_size=1, specifies the size of the message queue.
(3) Create info_sub image subscriber, and use message_filters.Subscriber to create an image subscriber.
The first parameter /rgbd_cam/depth/camera_info represents the image topic name.
The second parameter CameraInfo is the message type.
The third parameter queue_size=1 specifies the size of the message queue.
58 59 60 | rgb_sub = message_filters.Subscriber('/rgbd_cam/color/image_raw', RosImage, queue_size=1) depth_sub = message_filters.Subscriber('/rgbd_cam/depth/image_raw', RosImage, queue_size=1) info_sub = message_filters.Subscriber('/rgbd_cam/depth/camera_info', CameraInfo, queue_size=1) |
Synchronize Timestamp
Establish a time synchronizer to synchronize messages from the rgb_sub and depth_sub topics. The 3 signifies a maximum buffer size of 3 messages, and 0.03 implies that messages with a timestamp difference of no more than 0.03 seconds will be considered synchronized.
62 63 64 | # 同步时间戳, 时间允许有误差在0.03s(synchronize timestamps, allowing for a time error of up to 0.03 seconds) sync = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub, info_sub], 3, 0.03) sync.registerCallback(self.multi_callback) #执行反馈函数(execute feedback function) |
move Function
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | def move(self, shape, pose_t, angle): self.sdk.set_buzzer(int(tone.G4), 200, 100, 1) rospy.sleep(0.5) pose_t[2] += 0.02 ret1 = kinematics_control.set_pose_target(pose_t, 85) if len(ret1[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1500, ((1, ret1[1][0]), (2, ret1[1][1]), (3, ret1[1][2]), (4, ret1[1][3]),(5, ret1[1][4]))) rospy.sleep(1.5) pose_t[2] -= 0.05 ret2 = kinematics_control.set_pose_target(pose_t, 85) if angle != 0: angle = angle % 180 angle = angle - 180 if angle > 90 else (angle + 180 if angle < -90 else angle) angle = 500 + int(1000 * (angle + ret2[3][-1]) / 240) else: angle = 500 if len(ret2[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 500, ((5, angle),)) rospy.sleep(0.5) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, ret2[1][0]), (2, ret2[1][1]), (3, ret2[1][2]), (4, ret2[1][3]),(5, angle))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 600, ((10, 550),)) rospy.sleep(0.6) if len(ret1[1]) > 0: bus_servo_control.set_servos(self.servos_pub, 1000, ((1, ret1[1][0]), (2, ret1[1][1]), (3, ret1[1][2]), (4, ret1[1][3]),(5, angle))) rospy.sleep(1) bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 540), (3, 220), (4, 50), (5, 500), (10, 550))) rospy.sleep(1) print("shape: ", shape) if shape == "sphere": actiongroup.runAction("target_1") if shape == "cylinder": actiongroup.runAction("target_2") if shape == "cuboid": actiongroup.runAction("target_3") self.goto_default() rospy.sleep(2) self.moving = False |
Invoke the buzzer with parameters such as tone, duration (in milliseconds), volume, and 1 to enable the buzzer.
76 | self.sdk.set_buzzer(int(tone.G4), 200, 100, 1) |
Determine the shape of the recognized object, namely sphere, cylinder, and square, corresponding to three action groups: target_1, target_2, and target_3. Place the object in the respective position.
104 105 106 107 108 109 | if shape == "sphere": actiongroup.runAction("target_1") if shape == "cylinder": actiongroup.runAction("target_2") if shape == "cuboid": actiongroup.runAction("target_3") |
After placing the object, control the robotic arm to return to its initial state.
101 102 | bus_servo_control.set_servos(self.servos_pub, 1000, ((1, 500), (2, 540), (3, 220), (4, 50), (5, 500), (10, 550))) rospy.sleep(1) |
multi_callback Function
The multi_callback function receives messages for ros_rgb_image, ros_depth_image, and camera_info. It checks if self.queue is empty. If the queue is empty, it puts a tuple containing synchronized messages for ros_rgb_image, ros_depth_image, and camera_info into the queue. This tuple signifies that the three images have been successfully synchronized.
114 115 116 | def multi_callback(self, ros_rgb_image, ros_depth_image, depth_camera_info): if self.queue.empty(): self.queue.put_nowait((ros_rgb_image, ros_depth_image, depth_camera_info)) |
image_proc Function
Retrieve synchronized RGB and depth images from the queue self.queue.
119 120 | def image_proc(self): ros_rgb_image, ros_depth_image, depth_camera_info = self.queue.get(block=True) |
Create a NumPy array rgb_image to store RGB image data. Extract data from ros_rgb_image and construct the array based on image height, width, and data type.
Create a NumPy array depth_image to store depth image data. Extract data from ros_depth_image and construct the array based on image height and width.
Obtain camera intrinsics along with the height and width of the depth image.
121 122 123 | try: rgb_image = np.ndarray(shape=(ros_rgb_image.height, ros_rgb_image.width, 3), dtype=np.uint8, buffer=ros_rgb_image.data) depth_image = np.ndarray(shape=(ros_depth_image.height, ros_depth_image.width), dtype=np.uint16, buffer=ros_depth_image.data) |
Acquire the Coordinate of the Smallest Pixel Point
Copy the data from the depth image and reshape it into a one-dimensional array. Set pixel values less than or equal to 0 to 55555 to eliminate invalid depth values.
Calculate the row coordinate min_y of the pixel with the minimum depth value by performing integer division using the min_index divided by the image width (w). Integer division truncates the decimal part, yielding an integer representing the row position of the pixel with the minimum depth value in the depth image.
Multiply min_y by w (image width) and subtract the result from min_index. This yields the column coordinate min_x of the pixel with the minimum depth value.
133 134 135 136 137 | depth = np.copy(depth_image).reshape((-1, )) depth[depth<=0] = 55555 # 距离为0可能是进入死区,或者颜色问题识别不到,将距离赋一个大值(a distance of 0 could indicate an area of no return or a color recognition issue; assign a large value to distance) min_index = np.argmin(depth) # 距离最小的像素(the pixel with the minimum distance) min_y = min_index // iw min_x = min_index - min_y * iw |
Process Image
Assign the values of target_point to min_x and min_y.
Use np.clip to constrain the values in the depth image between 0 and 2000, with values exceeding this range truncated.
Convert the data type of the depth image to np.float64. Normalize the depth values to the range of 0 to 255 for subsequent visualization.
Set pixel values in the depth image greater than min_dist + 10 to 0.
Normalize and transform the updated depth image back to the range of 0 to 255.
139 140 141 142 143 | min_dist = depth_image[min_y, min_x] # 获取最小距离值(get the minimum distance measurement) sim_depth_image = np.clip(depth_image, 0, 300).astype(np.float64) / 300 * 255 depth_image = np.where(depth_image > min_dist + 15, 0, depth_image) # 将深度值大于最小距离15mm的像素置0(set the pixels with depth values greater than the minimum distance of 15mm to 0) sim_depth_image_sort = np.clip(depth_image, 0, 2000).astype(np.float64) / 2000 * 255 # depth_gray = sim_depth_image_sort.astype(np.uint8) |
Convert the depth image into a binary image where pixels with depth values less than 1 are set to 0, and the rest are set to 255.
Perform erosion on the binary image, which helps eliminate small noise or connect small holes in the image.
Apply dilation to the eroded image to restore the original shape of the objects.
Visualize the depth image as a colored image using JET colormap.
144 145 146 147 148 | depth_gray = cv2.GaussianBlur(depth_gray, (3, 3), 0) _, depth_bit = cv2.threshold(depth_gray, 1, 255, cv2.THRESH_BINARY) #depth_bit = cv2.erode(depth_bit, np.ones((3, 3), np.uint8)) #depth_bit = cv2.dilate(depth_bit, np.ones((3, 3), np.uint8)) depth_color_map = cv2.applyColorMap(sim_depth_image.astype(np.uint8), cv2.COLORMAP_JET) |
Utilize cv2.findContours to locate contours in the binary image depth_bit.
Set cv2.RETR_EXTERNAL as the contour retrieval mode, which exclusively retrieves external contours.
Choose cv2.CHAIN_APPROX_NONE as the contour approximation method.
Initialize the variable shape with the string value none.
150 151 | contours, hierarchy = cv2.findContours(depth_bit, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) shape = 'none' |
Iterate through each contour in the list of found contours.
Calculate the area of the current contour and store the result in the variable area.
If the area of the current contour is less than 2000 (or self.moving is true), skip the current contour and proceed to the next one.
Draw the contour lines, representing the current contour (obj), in yellow on the depth_color_map image.
Compute the perimeter of the current contour and store the result in the variable perimeter.
Use cv2.approxPolyDP to approximate the contour with a polygon, reducing the number of corners. The coordinates of the vertices of the approximated polygon are stored in the variable approx.
Draw the lines of the polygon approximation, representing it in blue on the depth_color_map image.
Calculate the number of vertices in the polygon approximation and store the result in the variable CornerNum.
Compute the minimum bounding rectangle around the polygon approximation and return its top-left coordinates (x, y), width w, and height h.
153 154 155 156 157 158 159 160 161 162 163 164 165 | for obj in contours: if min_dist > 225: break area = cv2.contourArea(obj) if area < 2000 or self.moving is True: continue cv2.drawContours(depth_color_map, obj, -1, (255, 255, 0), 4) # 绘制轮廓线(draw contour line) perimeter = cv2.arcLength(obj, True) # 计算轮廓周长(calculate contour perimeter) approx = cv2.approxPolyDP(obj, 0.035 * perimeter, True) # 获取轮廓角点坐标(get contour angular point coordinates) cv2.drawContours(depth_color_map, approx, -1, (255, 0, 0), 4) # 绘制轮廓线(draw contour line) CornerNum = len(approx) x, y, w, h = cv2.boundingRect(approx) |
Determine the shape type based on the detected standard deviation of depth values and the number of corners. When the standard deviation is greater than 1, it is considered a circular shape or a more complex figure. In this case, if the number of corners is greater than 4, it is recognized as a sphere; otherwise, if it is less than or equal to 4, it is considered a cylinder.
If the standard deviation is less than or equal to 1, it indicates that the shape may not be very complex, possibly a cylindrical pattern or a planar pattern. In this scenario, if the number of corners is equal to 4, it is identified as a rectangular prism; otherwise, it is considered a cylinder.
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | if depth_std > 1: if CornerNum > 4: objType = "sphere_1" # 球体(sphere) elif CornerNum == 4: objType = "cylinder_1" # 圆柱体(cylinder) else: objType = "none" else: if CornerNum == 4: if min_y > (y + int(h / 10 * 7)): objType="cuboid_1" # 立方体/长方体(cube/cuboid) else: objType="cylinder_2" # 圆柱体(cylinder) else: if 4 < CornerNum < 10: objType="cylinder_2" # 圆柱体(cylinder) else: objType = "none" |
Check if the current shape in the current frame is the same as the shape in the previous frame and not equal to none. If true, print the current counter value (self.count) and increment the counter by 1.
If the shape has changed but is not none, reset the counter value.
226 227 228 229 230 | if self.last_shape == shape and shape != 'none': #print(self.count) self.count += 1 else: self.count = 0 |
Estimate Pose and Execute Corresponding Action
Examine contour information. If contour is not
None, calculate the minimum enclosing circle of the object contour and retrieve the center point, camera intrinsics for that target.
231 232 233 | if contour is not None : (cx, cy), r = cv2.minEnclosingCircle(obj) K = depth_camera_info.K |
Convert pixel coordinates and depth values into coordinates in the camera coordinate system. Transform the information within the position function into a pose matrix and make slight adjustments.
234 235 | position = depth_pixel_to_camera((cx, cy), min_dist / 1000, (K[0], K[4], K[2], K[5])) position[0] -= 0.01 |
Subsequently, convert the parameters from the camera coordinate system to the world coordinate system. If the conditions are met, initiate a new thread, invoke the self.move function to grasp objects of a specific shape, and finally update the information for the current shape.
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | pose_end = np.matmul(self.hand2cam_tf_matrix, xyz_euler_to_mat(position, (0, 0, 0))) world_pose = np.matmul(self.endpoint, pose_end) pose_t, pose_r = mat_to_xyz_euler(world_pose) print(pose_t[2]) min_x, min_y = cx, cy print(pose_t) angle = 0 offset_x = 0.008 offset_y = 0 offset_z = 0.00 pose_t[0] += offset_x pose_t[1] += offset_y pose_t[2] += offset_z if shape == "cylinder_1" or shape == "cuboid_1": center, (width, height), angle = cv2.minAreaRect(contour) if angle < -45: angle += 90 if width > height and width / height > 1.5: print("wh: ", width, height) angle = angle + 90 cv2.drawContours(depth_color_map, [np.int0(cv2.boxPoints((center, (width,height), angle)))], -1, (0, 0, 255), 2, cv2.LINE_AA) if self.count > 5: self.count = 0 self.moving = True threading.Thread(target=self.move, args=(shape[:-2], pose_t, angle)).start() self.last_shape = shape |
Marking Corresponding Depth Values on Pseudocolored and BGR Images
Displaying depth values as text on pseudocolored and BGR images involves drawing larger black text and slightly offset smaller gray text. The parameters correspond to the processed image, text content, starting position, font, size, color, line width, and an anti-aliasing line drawing mode for smoother lines.
222 223 224 | cv2.rectangle(depth_color_map, (x, y), (x + w, y + h), (255, 255, 255), 2) cv2.putText(depth_color_map, objType[:-2], (x + w // 2, y + (h //2) - 10), cv2.FONT_HERSHEY_COMPLEX, 1.0, (0, 0, 0), 2, cv2.LINE_AA) cv2.putText(depth_color_map, objType[:-2], (x + w // 2, y + (h //2) - 10), cv2.FONT_HERSHEY_COMPLEX, 1.0, (255, 255, 255), 1) |
Update image frame rate.
266 | self.fps.update() |
Concatenate the RGB image and pseudocolored depth image horizontally to create a single image. Then, use cv2.imshow to display the final image on the depth window.
268 269 | result_image = np.concatenate([depth_color_map, bgr_image], axis=1) cv2.imshow("depth", result_image) |
Check for keyboard input, and if any key is pressed, close the node.
271 272 273 | key = cv2.waitKey(1) if key != -1: rospy.signal_shutdown('shutdown1') |
In the event of an exception in the code, capture the exception and log the error information as a ROS error message.
275 276 | except Exception as e: rospy.logerr('callback error:', str(e)) |