|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Methods to numpify LaserScan message. |
| 4 | +""" |
| 5 | + |
| 6 | +from .registry import converts_from_numpy, converts_to_numpy |
| 7 | + |
| 8 | +import numpy as np |
| 9 | +from sensor_msgs.msg import LaserScan |
| 10 | + |
| 11 | + |
| 12 | +@converts_to_numpy(LaserScan) |
| 13 | +def laserscan_to_array( |
| 14 | + scan, remove_invalid_ranges=False, include_ranges_and_intensities=False |
| 15 | +): |
| 16 | + """ |
| 17 | + Takes a sensor_msgs/msg/LaserScan msg and returns a structered array with |
| 18 | + fields x, y and z that correspond to cartesian position data. Optionally, |
| 19 | + ranges and intensities fields that correspond to the range and intensity |
| 20 | + of a point are also included if include_ranges_and_intensities is True. |
| 21 | +
|
| 22 | + Parameters |
| 23 | + ---------- |
| 24 | + scan : ROS2 LaserScan message |
| 25 | + Input laser scan message to get numpyed |
| 26 | + remove_invalid_ranges : bool, optional |
| 27 | + whether to remove invalid ranges from the input scan, by default False |
| 28 | + include_ranges_and_intensities : bool, optional |
| 29 | + whether to also return the ranges & intensities along with the cartesian |
| 30 | + position. |
| 31 | +
|
| 32 | + Returns |
| 33 | + ------- |
| 34 | + pts : numpy k-array where each element will be a structured record that |
| 35 | + contains either 3 or 5 fields. If include_ranges_and_intensities is |
| 36 | + False, each element of output array is a structured record that has |
| 37 | + 3 fields ['x', 'y', 'z'] of type float 32. Else, it has 5 fields ['x', |
| 38 | + 'y', 'z', 'ranges', 'intensities']. Since output is a structured array, |
| 39 | + all the x-coordinates of the points can be accessed as out_array['x']. |
| 40 | + Similarly, the y and z coodinates can be accessed as out_array['y'] |
| 41 | + and out_array['z'] respectively. |
| 42 | + """ |
| 43 | + n_points = len(scan.ranges) |
| 44 | + angles = np.linspace( |
| 45 | + scan.angle_min, |
| 46 | + scan.angle_max, |
| 47 | + n_points, |
| 48 | + ) |
| 49 | + ranges = np.array(scan.ranges, dtype="f4") |
| 50 | + intensities = np.array(scan.intensities, dtype="f4") |
| 51 | + if remove_invalid_ranges: |
| 52 | + indices_invalid_range = ( |
| 53 | + np.isinf(ranges) |
| 54 | + | (ranges < scan.range_min) |
| 55 | + | (ranges > scan.range_max) |
| 56 | + ) |
| 57 | + ranges = ranges[~indices_invalid_range] |
| 58 | + angles = angles[~indices_invalid_range] |
| 59 | + intensities = intensities[~indices_invalid_range] |
| 60 | + |
| 61 | + x = np.array(ranges * np.cos(angles), dtype="f4") |
| 62 | + y = np.array(ranges * np.sin(angles), dtype="f4") |
| 63 | + z = np.zeros(ranges.shape[0], dtype="f4") |
| 64 | + if include_ranges_and_intensities: |
| 65 | + dtype = np.dtype( |
| 66 | + [ |
| 67 | + ("x", "f4"), |
| 68 | + ("y", "f4"), |
| 69 | + ("z", "f4"), |
| 70 | + ("ranges", "f4"), |
| 71 | + ("intensities", "f4"), |
| 72 | + ] |
| 73 | + ) |
| 74 | + out_array = np.empty(len(x), dtype=dtype) |
| 75 | + out_array["x"] = x |
| 76 | + out_array["y"] = y |
| 77 | + out_array["z"] = z |
| 78 | + out_array["ranges"] = ranges |
| 79 | + out_array["intensities"] = intensities |
| 80 | + return out_array |
| 81 | + else: |
| 82 | + dtype = np.dtype([("x", "f4"), ("y", "f4"), ("z", "f4")]) |
| 83 | + out_array = np.empty(len(x), dtype=dtype) |
| 84 | + out_array["x"] = x |
| 85 | + out_array["y"] = y |
| 86 | + out_array["z"] = z |
| 87 | + return out_array |
| 88 | + |
| 89 | + |
| 90 | +@converts_from_numpy(LaserScan) |
| 91 | +def array_to_laserscan(arr, header, scan_time=0.0, time_increment=0.0): |
| 92 | + """ |
| 93 | + Takes a structured array(created from LaserScan msg) and returns a |
| 94 | + LaserScan message. Fields that cannot be determined from the numpy |
| 95 | + array are provided as inputs. Since the LaserScan message relies |
| 96 | + on a consistent angular increment, structured array in which the |
| 97 | + points have been omitted will result in a LaserScan message that is |
| 98 | + not correct. |
| 99 | +
|
| 100 | + Parameters |
| 101 | + ---------- |
| 102 | + arr : Structured numpy array with fields x, y and z. |
| 103 | + Input numpy array that was created from LaserScan message. |
| 104 | + header : std_msgs::msg::Header, |
| 105 | + The header to be written to the output LaserScan message. |
| 106 | + scan_time : float, optional |
| 107 | + time between scans [seconds] of LaserScan |
| 108 | + time_increment : float, optional |
| 109 | + time between measurements [seconds] - if lidar is moving, |
| 110 | + this will be used in interpolating position |
| 111 | +
|
| 112 | + Returns |
| 113 | + ------- |
| 114 | + scan_msg : sensor_msgs::msg::LaserScan message |
| 115 | + If the input array does not contain an `intensities` field, |
| 116 | + this message has the intensities list filled to zeros. |
| 117 | +
|
| 118 | + """ |
| 119 | + n_points = arr.shape[0] |
| 120 | + |
| 121 | + if "intensities" in arr.dtype.names: |
| 122 | + intensities = arr["intensities"] |
| 123 | + else: |
| 124 | + intensities = np.zeros(n_points).astype(float) |
| 125 | + |
| 126 | + if "ranges" in arr.dtype.names: |
| 127 | + ranges = arr["ranges"] |
| 128 | + else: |
| 129 | + ranges = np.sqrt(arr["x"] ** 2 + arr["y"] ** 2).astype(float) |
| 130 | + |
| 131 | + angles = np.arctan2(arr["y"], arr["x"]).astype(float) |
| 132 | + |
| 133 | + # Create a LaserScan message |
| 134 | + scan_msg = LaserScan() |
| 135 | + |
| 136 | + scan_msg.header = header |
| 137 | + |
| 138 | + scan_msg.intensities = intensities.tolist() |
| 139 | + scan_msg.ranges = ranges.tolist() |
| 140 | + |
| 141 | + # Compute min and max of the ranges |
| 142 | + scan_msg.range_min = np.min(ranges) |
| 143 | + scan_msg.range_max = np.max(ranges) |
| 144 | + |
| 145 | + # Compute min and max of the angles |
| 146 | + scan_msg.angle_min = np.min(angles) |
| 147 | + scan_msg.angle_max = np.max(angles) |
| 148 | + |
| 149 | + # Use the time_increment and scan_time from input arguments |
| 150 | + scan_msg.time_increment = time_increment |
| 151 | + scan_msg.scan_time = scan_time |
| 152 | + |
| 153 | + # Compute angle increment. Since the angle_max is not exclusive, omit |
| 154 | + # the last point. |
| 155 | + scan_msg.angle_increment = (scan_msg.angle_max - scan_msg.angle_min) / ( |
| 156 | + n_points - 1 |
| 157 | + ) |
| 158 | + |
| 159 | + return scan_msg |
0 commit comments