|
9 | 9 | import os.path |
10 | 10 | from collections import deque |
11 | 11 | from enum import Enum |
12 | | -from typing import Dict, Optional, Union, Tuple |
| 12 | +from typing import Dict, Optional, Union, Tuple, Sequence |
13 | 13 | import yaml |
14 | 14 |
|
15 | 15 | import matplotlib.cm as cmap |
16 | 16 | import numpy as np |
17 | 17 | import sys |
| 18 | +from numpy.linalg import inv |
18 | 19 | from typing import Any, Dict |
19 | 20 |
|
20 | 21 | import rospy |
21 | 22 | from cras.distortion_models import PLUMB_BOB, RATIONAL_POLYNOMIAL, EQUIDISTANT |
| 23 | +from cras.geometry_utils import quat_msg_from_rpy |
22 | 24 | from cras.image_encodings import isColor, isMono, isBayer, isDepth, bitDepth, numChannels, MONO8, RGB8, BGR8,\ |
23 | 25 | TYPE_16UC1, TYPE_32FC1, YUV422 |
24 | 26 | from cras.log_utils import rosconsole_notifyLoggerLevelsChanged, rosconsole_set_logger_level, RosconsoleLevel |
|
28 | 30 | from cv_bridge import CvBridge, CvBridgeError |
29 | 31 | from dynamic_reconfigure.encoding import decode_config |
30 | 32 | from dynamic_reconfigure.msg import Config |
| 33 | +from geometry_msgs.msg import Quaternion, Transform, TransformStamped, Vector3 |
31 | 34 | from image_transport_codecs import decode, encode |
32 | 35 | from image_transport_codecs.compressed_depth_codec import has_rvl |
33 | 36 | from image_transport_codecs.parse_compressed_format import guess_any_compressed_image_transport_format |
34 | 37 | from kdl_parser_py.urdf import treeFromUrdfModel |
| 38 | +from ros_numpy import msgify, numpify |
35 | 39 | from sensor_msgs.msg import CameraInfo, CompressedImage, Image, JointState |
36 | 40 | from std_msgs.msg import Header |
37 | 41 | from tf2_msgs.msg import TFMessage |
| 42 | +from tf2_py import BufferCore |
38 | 43 | from urdf_parser_py import urdf, xml_reflection |
39 | 44 | from vision_msgs.msg import Detection2DArray, Detection2D, ObjectHypothesisWithPose |
40 | 45 |
|
41 | | -from .message_filter import DeserializedMessageFilter, RawMessageFilter, TopicSet |
| 46 | +from .message_filter import DeserializedMessageFilter, DeserializedMessageFilterWithTF, RawMessageFilter, TopicSet |
42 | 47 |
|
43 | 48 |
|
44 | 49 | def urdf_error(message): |
@@ -1075,6 +1080,173 @@ def _str_params(self): |
1075 | 1080 | return ", ".join(parts) |
1076 | 1081 |
|
1077 | 1082 |
|
| 1083 | +class FixStaticTF(DeserializedMessageFilterWithTF): |
| 1084 | + """Adjust some static transforms.""" |
| 1085 | + |
| 1086 | + def __init__(self, transforms=None, *args, **kwargs): |
| 1087 | + # type: (Optional[Sequence[Dict[STRING_TYPE, STRING_TYPE]]], Any, Any) -> None |
| 1088 | + """ |
| 1089 | + :param transforms: The new transforms. The dicts have to contain keys "frame_id", "child_frame_id", "transform". |
| 1090 | + The transform has to be a 6-tuple (x, y, z, roll, pitch, yaw) |
| 1091 | + or 7-tuple (x, y, z, qx, qy, qz, qw). |
| 1092 | + :param args: Standard include/exclude topics/types and min/max stamp args. |
| 1093 | + :param kwargs: Standard include/exclude topics/types and min/max stamp kwargs. |
| 1094 | + """ |
| 1095 | + super(FixStaticTF, self).__init__( |
| 1096 | + include_topics=["tf_static"], include_types=(TFMessage._type,), *args, **kwargs) # noqa |
| 1097 | + |
| 1098 | + self._transforms = dict() |
| 1099 | + for t in (transforms if transforms is not None else list()): |
| 1100 | + frame_id = t["frame_id"] |
| 1101 | + child_frame_id = t["child_frame_id"] |
| 1102 | + data = t["transform"] |
| 1103 | + if len(data) == 7: |
| 1104 | + transform = Transform(Vector3(*data[:3]), Quaternion(*data[3:])) |
| 1105 | + elif len(data) == 6: |
| 1106 | + transform = Transform(Vector3(*data[:3]), quat_msg_from_rpy(*data[3:])) |
| 1107 | + else: |
| 1108 | + raise RuntimeError("'transform' has to be either a 6-tuple or 7-tuple.") |
| 1109 | + |
| 1110 | + self._transforms[(frame_id, child_frame_id)] = transform |
| 1111 | + |
| 1112 | + def filter(self, topic, msg, stamp, header): |
| 1113 | + for transform in msg.transforms: |
| 1114 | + key = (transform.header.frame_id, transform.child_frame_id) |
| 1115 | + if key in self._transforms: |
| 1116 | + transform.transform = self._transforms[key] |
| 1117 | + print("Adjusted transform %s->%s." % (key[0], key[1])) |
| 1118 | + |
| 1119 | + return topic, msg, stamp, header |
| 1120 | + |
| 1121 | + def _str_params(self): |
| 1122 | + parts = [] |
| 1123 | + parts.append('transforms=' + repr(list(self._transforms.keys()))) |
| 1124 | + parent_params = self._default_str_params(include_types=False) |
| 1125 | + if len(parent_params) > 0: |
| 1126 | + parts.append(parent_params) |
| 1127 | + return ", ".join(parts) |
| 1128 | + |
| 1129 | + |
| 1130 | +class FixExtrinsicsFromIKalibr(DeserializedMessageFilterWithTF): |
| 1131 | + """Apply extrinsic calibrations from IKalibr.""" |
| 1132 | + |
| 1133 | + def __init__(self, param_file, ref_imu_frame="imu", frames=None, *args, **kwargs): |
| 1134 | + # type: (STRING_TYPE, STRING_TYPE, Optional[Dict[STRING_TYPE, Dict[STRING_TYPE, STRING_TYPE]]], Any, Any) -> None |
| 1135 | + """ |
| 1136 | + :param param_file: Path to ikalibr_param.yaml file - the result of extrinsic calibration. |
| 1137 | + :param ref_imu_frame: Frame of the reference IMU towards which everything is calibrated. |
| 1138 | + :param frames: The TF frames to fix. Keys are the topic names used in iKalibr. Values are dicts with keys |
| 1139 | + "sensor_frame" and optionally "adjust_frame" (if not specified, "sensor_frame" is used). |
| 1140 | + "adjust_frame" specifies the frame whose transform should be changed, which can be useful |
| 1141 | + if you want to change a transform somewhere in the middle of the TF chain and not the last one. |
| 1142 | + :param args: Standard include/exclude topics/types and min/max stamp args. |
| 1143 | + :param kwargs: Standard include/exclude topics/types and min/max stamp kwargs. |
| 1144 | + """ |
| 1145 | + super(FixExtrinsicsFromIKalibr, self).__init__( |
| 1146 | + include_topics=["tf", "tf_static"], include_types=(TFMessage._type,), *args, **kwargs) # noqa |
| 1147 | + |
| 1148 | + self._ref_imu_frame = ref_imu_frame |
| 1149 | + self._frames = frames if frames is not None else dict() |
| 1150 | + self._param_file = param_file |
| 1151 | + |
| 1152 | + with open(param_file, 'r') as f: |
| 1153 | + self._params = yaml.safe_load(f) |
| 1154 | + |
| 1155 | + extri = self._params["CalibParam"]["EXTRI"] |
| 1156 | + tfs = dict() |
| 1157 | + for key, values in extri.items(): |
| 1158 | + for item in values: |
| 1159 | + ikalibr_name = item["key"] |
| 1160 | + value = item["value"] |
| 1161 | + if ikalibr_name not in tfs: |
| 1162 | + tfs[ikalibr_name] = Transform() |
| 1163 | + if key.startswith("POS_"): |
| 1164 | + msg = Vector3(value["r0c0"], value["r1c0"], value["r2c0"]) |
| 1165 | + tfs[ikalibr_name].translation = msg |
| 1166 | + elif key.startswith("SO3_"): |
| 1167 | + msg = Quaternion(value["qx"], value["qy"], value["qz"], value["qw"]) |
| 1168 | + tfs[ikalibr_name].rotation = msg |
| 1169 | + |
| 1170 | + print("Read %i transforms from %s." % (len(tfs), param_file)) |
| 1171 | + |
| 1172 | + self._sensor_to_adjust_frame = dict() |
| 1173 | + self._adjust_frame_to_sensor = dict() |
| 1174 | + self._sensor_transforms = dict() |
| 1175 | + for ikalibr_name, item in self._frames.items(): |
| 1176 | + sensor_frame = item["sensor_frame"] |
| 1177 | + adjust_frame = item.get("adjust_frame", sensor_frame) |
| 1178 | + |
| 1179 | + if adjust_frame in self._adjust_frame_to_sensor: |
| 1180 | + raise RuntimeError( |
| 1181 | + "Duplicate appearance of adjust frame %s. This is invalid configuration!" % (adjust_frame,)) |
| 1182 | + |
| 1183 | + self._sensor_transforms[sensor_frame] = tfs[ikalibr_name] |
| 1184 | + self._sensor_to_adjust_frame[sensor_frame] = adjust_frame |
| 1185 | + self._adjust_frame_to_sensor[adjust_frame] = sensor_frame |
| 1186 | + |
| 1187 | + self._tf = BufferCore() |
| 1188 | + |
| 1189 | + @staticmethod |
| 1190 | + def fix_transform_type(t): |
| 1191 | + """Convert from the ad-hoc rosbag type to true Transform type (needed by ros_numpy).""" |
| 1192 | + return Transform( |
| 1193 | + Vector3(t.translation.x, t.translation.y, t.translation.z), |
| 1194 | + Quaternion(t.rotation.x, t.rotation.y, t.rotation.z, t.rotation.w)) |
| 1195 | + |
| 1196 | + @staticmethod |
| 1197 | + def fix_transform_stamped_type(t): |
| 1198 | + """Convert from the ad-hoc rosbag type to true TransformStamped type (needed by tf2_py).""" |
| 1199 | + return TransformStamped( |
| 1200 | + Header(t.header.seq, t.header.stamp, t.header.frame_id), |
| 1201 | + t.child_frame_id, |
| 1202 | + FixExtrinsicsFromIKalibr.fix_transform_type(t.transform)) |
| 1203 | + |
| 1204 | + def filter(self, topic, msg, stamp, header): |
| 1205 | + if topic in self._tf_static_topics: |
| 1206 | + for transform in msg.transforms: |
| 1207 | + self._tf.set_transform_static(self.fix_transform_stamped_type(transform), "filter_bag") |
| 1208 | + else: |
| 1209 | + for transform in msg.transforms: |
| 1210 | + self._tf.set_transform(self.fix_transform_stamped_type(transform), "filter_bag") |
| 1211 | + |
| 1212 | + latest = rospy.Time(0) |
| 1213 | + for transform in msg.transforms: |
| 1214 | + if transform.child_frame_id in self._adjust_frame_to_sensor: |
| 1215 | + adjust_frame = transform.child_frame_id |
| 1216 | + sensor_frame = self._adjust_frame_to_sensor[adjust_frame] |
| 1217 | + parent_frame = transform.header.frame_id |
| 1218 | + |
| 1219 | + sensor_tf = numpify(self._sensor_transforms[sensor_frame]) |
| 1220 | + t_parent_imu = numpify( |
| 1221 | + self._tf.lookup_transform_core(self._ref_imu_frame, parent_frame, latest).transform) |
| 1222 | + t_adjust_parent = numpify(self.fix_transform_type(transform.transform)) |
| 1223 | + t_sensor_adjust = numpify(self._tf.lookup_transform_core(adjust_frame, sensor_frame, latest).transform) |
| 1224 | + |
| 1225 | + t_correction = \ |
| 1226 | + np.matmul(np.matmul(inv(np.matmul(t_parent_imu, t_adjust_parent)), sensor_tf), inv(t_sensor_adjust)) |
| 1227 | + t_adjust_parent = np.matmul(t_adjust_parent, t_correction) |
| 1228 | + transform.transform = msgify(Transform, t_adjust_parent) |
| 1229 | + |
| 1230 | + print("Adjusted transform %s->%s by %.3f m (for sensor %s)." % ( |
| 1231 | + parent_frame, adjust_frame, np.linalg.norm(t_correction[:3, 3]), sensor_frame)) |
| 1232 | + |
| 1233 | + return topic, msg, stamp, header |
| 1234 | + |
| 1235 | + def reset(self): |
| 1236 | + self._tf.clear() |
| 1237 | + super(FixExtrinsicsFromIKalibr, self).reset() |
| 1238 | + |
| 1239 | + def _str_params(self): |
| 1240 | + parts = [] |
| 1241 | + parts.append('param_file=' + self._param_file) |
| 1242 | + parts.append('ref_imu_frame=' + self._ref_imu_frame) |
| 1243 | + parts.append('frames=' + ",".join(self._frames.keys())) |
| 1244 | + parent_params = self._default_str_params(include_types=False) |
| 1245 | + if len(parent_params) > 0: |
| 1246 | + parts.append(parent_params) |
| 1247 | + return ", ".join(parts) |
| 1248 | + |
| 1249 | + |
1078 | 1250 | class FixSpotCams(RawMessageFilter): |
1079 | 1251 | """Fix a problem with Spot robot cameras that publish a bit weird message header.""" |
1080 | 1252 |
|
|
0 commit comments