Skip to content

Commit df869e8

Browse files
committed
cras_bag_tools: Added filters to manipulate TFs.
1 parent 9ab0969 commit df869e8

File tree

3 files changed

+247
-2
lines changed

3 files changed

+247
-2
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# SPDX-FileCopyrightText: Czech Technical University in Prague
3+
4+
# Recomputes TF from robot model and joint states
5+
out_format: '{name}.caminfo_fixed{ext}'
6+
compression: lz4
7+
filters:
8+
- MergeInitialStaticTf: {}
9+
- FixStaticTF:
10+
transforms:
11+
- frame_id: camera_fisheye_front
12+
child_frame_id: pylon_camera_fisheye_front
13+
transform: [0.0, 0.0, 0.0, -0.5, 0.5, -0.5, 0.5]
14+
- frame_id: camera_fisheye_rear
15+
child_frame_id: pylon_camera_fisheye_rear
16+
transform: [0.0, 0.0, 0.0, -0.5, 0.5, -0.5, 0.5]
17+
- frame_id: camera_front
18+
child_frame_id: pylon_camera_front
19+
transform: [0.0, 0.0, 0.0, -0.5, 0.5, -0.5, 0.5]
20+
- frame_id: camera_left
21+
child_frame_id: pylon_camera_left
22+
transform: [0.0, 0.0, 0.0, -0.5, 0.5, -0.5, 0.5]
23+
- frame_id: camera_rear
24+
child_frame_id: pylon_camera_rear
25+
transform: [0.0, 0.0, 0.0, -0.5, 0.5, -0.5, 0.5]
26+
- frame_id: camera_right
27+
child_frame_id: pylon_camera_right
28+
transform: [0.0, 0.0, 0.0, -0.5, 0.5, -0.5, 0.5]
29+
- frame_id: camera_up
30+
child_frame_id: pylon_camera_up
31+
transform: [0.0, 0.0, 0.0, -0.5, 0.5, -0.5, 0.5]
32+
- FixExtrinsicsFromIKalibr:
33+
param_file: "/media/data/bags/outdoor-dataset/calib/marv/extrinsic/ikalibr_param-final.yaml"
34+
ref_imu_frame: "imu"
35+
frames:
36+
"/camera_fisheye_front/image_color/compressed":
37+
sensor_frame: "pylon_camera_fisheye_front"
38+
adjust_frame: "camera_fisheye_front"
39+
"/camera_fisheye_rear/image_color/compressed":
40+
sensor_frame: "pylon_camera_fisheye_rear"
41+
adjust_frame: "camera_fisheye_rear"
42+
"/camera_front/image_color/compressed":
43+
sensor_frame: "pylon_camera_front"
44+
adjust_frame: "camera_front"
45+
"/camera_left/image_color/compressed":
46+
sensor_frame: "pylon_camera_left"
47+
adjust_frame: "camera_left"
48+
"/camera_rear/image_color/compressed":
49+
sensor_frame: "pylon_camera_rear"
50+
adjust_frame: "camera_rear"
51+
"/camera_right/image_color/compressed":
52+
sensor_frame: "pylon_camera_right"
53+
adjust_frame: "camera_right"
54+
"/camera_up/image_color/compressed":
55+
sensor_frame: "pylon_camera_up"
56+
adjust_frame: "camera_up"
57+
"/os_cloud_node/points":
58+
sensor_frame: "os_lidar"
59+
adjust_frame: "os_calib"
60+
# These transforms depend on the previous ones, so they have to be processed later; if there is an unfortunate order
61+
# of tf_static messages, you would even need to recompute some of the transforms again; thus, it is better to merge
62+
# initial static TFs to have all static TFs in one message.
63+
- FixExtrinsicsFromIKalibr:
64+
param_file: "/media/data/bags/outdoor-dataset/calib/marv/extrinsic/ikalibr_param-final.yaml"
65+
ref_imu_frame: "imu"
66+
frames:
67+
"/camera_front_thermo/image8/compressed":
68+
sensor_frame: "camera_front_thermo"
69+
"/os_cloud_node/imu":
70+
sensor_frame: "os_imu"

cras_bag_tools/package.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<exec_depend>cras_py_common</exec_depend>
2222
<exec_depend>cv_bridge</exec_depend>
2323
<exec_depend>genpy</exec_depend>
24+
<exec_depend>geometry_msgs</exec_depend>
2425
<exec_depend>image_transport_codecs</exec_depend>
2526
<exec_depend>kdl_parser_py</exec_depend>
2627
<exec_depend condition="$ROS_PYTHON_VERSION == 2">python-imageio</exec_depend>
@@ -43,8 +44,10 @@
4344
<exec_depend>rosbag</exec_depend>
4445
<exec_depend>roslib</exec_depend>
4546
<exec_depend>rospy</exec_depend>
47+
<exec_depend>ros_numpy</exec_depend>
4648
<exec_depend>sensor_msgs</exec_depend>
4749
<exec_depend>tf2_msgs</exec_depend>
50+
<exec_depend>tf2_py</exec_depend>
4851
<exec_depend>urdfdom_py</exec_depend>
4952
<exec_depend>vision_msgs</exec_depend>
5053

cras_bag_tools/src/cras_bag_tools/message_filters.py

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99
import os.path
1010
from collections import deque
1111
from enum import Enum
12-
from typing import Dict, Optional, Union, Tuple
12+
from typing import Dict, Optional, Union, Tuple, Sequence
1313
import yaml
1414

1515
import matplotlib.cm as cmap
1616
import numpy as np
1717
import sys
18+
from numpy.linalg import inv
1819
from typing import Any, Dict
1920

2021
import rospy
2122
from cras.distortion_models import PLUMB_BOB, RATIONAL_POLYNOMIAL, EQUIDISTANT
23+
from cras.geometry_utils import quat_msg_from_rpy
2224
from cras.image_encodings import isColor, isMono, isBayer, isDepth, bitDepth, numChannels, MONO8, RGB8, BGR8,\
2325
TYPE_16UC1, TYPE_32FC1, YUV422
2426
from cras.log_utils import rosconsole_notifyLoggerLevelsChanged, rosconsole_set_logger_level, RosconsoleLevel
@@ -28,17 +30,20 @@
2830
from cv_bridge import CvBridge, CvBridgeError
2931
from dynamic_reconfigure.encoding import decode_config
3032
from dynamic_reconfigure.msg import Config
33+
from geometry_msgs.msg import Quaternion, Transform, TransformStamped, Vector3
3134
from image_transport_codecs import decode, encode
3235
from image_transport_codecs.compressed_depth_codec import has_rvl
3336
from image_transport_codecs.parse_compressed_format import guess_any_compressed_image_transport_format
3437
from kdl_parser_py.urdf import treeFromUrdfModel
38+
from ros_numpy import msgify, numpify
3539
from sensor_msgs.msg import CameraInfo, CompressedImage, Image, JointState
3640
from std_msgs.msg import Header
3741
from tf2_msgs.msg import TFMessage
42+
from tf2_py import BufferCore
3843
from urdf_parser_py import urdf, xml_reflection
3944
from vision_msgs.msg import Detection2DArray, Detection2D, ObjectHypothesisWithPose
4045

41-
from .message_filter import DeserializedMessageFilter, RawMessageFilter, TopicSet
46+
from .message_filter import DeserializedMessageFilter, DeserializedMessageFilterWithTF, RawMessageFilter, TopicSet
4247

4348

4449
def urdf_error(message):
@@ -1075,6 +1080,173 @@ def _str_params(self):
10751080
return ", ".join(parts)
10761081

10771082

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+
10781250
class FixSpotCams(RawMessageFilter):
10791251
"""Fix a problem with Spot robot cameras that publish a bit weird message header."""
10801252

0 commit comments

Comments
 (0)