Skip to content

Commit 0c5ecd0

Browse files
committed
cras_bag_tools: Added filters to update and dump robot model URDF.
1 parent 312f9b8 commit 0c5ecd0

File tree

6 files changed

+211
-15
lines changed

6 files changed

+211
-15
lines changed

cras_bag_tools/config/add_subtitles.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
out_format: '{name}.subs{ext}'
55
compression: lz4
66
filters:
7+
- UpdateRobotModel:
8+
urdf_file: "marv.urdf"
9+
param: "marv/robot_description"
10+
- DumpRobotModel:
11+
urdf_file: "marv2.urdf"
12+
param: "marv/robot_description"
13+
remove_comments: True
14+
pretty_print: True
715
- AddSubtitles:
8-
subtitles_file: '/media/data/bags/outdoor-dataset/marv_2024-10-05-12-34-53.small.bag.srt'
9-
topic: /events
16+
subtitles_file: 'marv_2024-10-05-12-34-53.small.bag.srt'
17+
topic: /commentary

cras_bag_tools/package.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
<exec_depend>kdl_parser_py</exec_depend>
2727
<exec_depend condition="$ROS_PYTHON_VERSION == 2">python-imageio</exec_depend>
2828
<exec_depend condition="$ROS_PYTHON_VERSION == 3">python3-imageio</exec_depend>
29+
<exec_depend condition="$ROS_PYTHON_VERSION == 2">python-lxml</exec_depend>
30+
<exec_depend condition="$ROS_PYTHON_VERSION == 3">python3-lxml</exec_depend>
2931
<exec_depend condition="$ROS_PYTHON_VERSION == 2">python-marisa</exec_depend>
3032
<exec_depend condition="$ROS_PYTHON_VERSION == 3">python3-marisa</exec_depend>
3133
<exec_depend condition="$ROS_PYTHON_VERSION == 2">python-matplotlib</exec_depend>

cras_bag_tools/scripts/filter_bag

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import absolute_import, division, print_function
99

1010
import argparse
11+
import copy
1112

1213
import os
1314
import sys
@@ -141,9 +142,20 @@ def filter_bags(bags, out_format, compression, copy_params, filter, default_para
141142
except Exception as e:
142143
print("Error loading ROS parameters from file %s: %s" % (params_file, str(e)), file=sys.stderr)
143144

145+
orig_params = copy.deepcopy(params) if params is not None else None
144146
with rosbag.Bag(out_bag_path, 'w', compression=compression) as out:
145147
bag.read_index()
146148
filter_bag(bag, out, filter, params, start_time, end_time, time_ranges)
149+
150+
if copy_params and params != orig_params:
151+
print('Saving ROS parameters to', out_bag_path + '.params')
152+
try:
153+
with open(out_bag_path + '.params', 'w+') as f:
154+
yaml.safe_dump(params, f)
155+
except Exception as ex:
156+
print('Error saving ROS params to %s: %s' % (out_bag_path + '.params', str(ex)),
157+
file=sys.stderr)
158+
147159
except Exception as e:
148160
print('Error processing bag file %s: %s' % (bags_path, str(e)), file=sys.stderr)
149161
import traceback

cras_bag_tools/src/cras_bag_tools/bag_filter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ def filter_bag(bags, out, bag_filter=Passthrough(), params=None, start_time=None
6767
# apply connection filters
6868
topics = [c.topic for c in bags._get_connections(topics, bag_filter.connection_filter)] # noqa
6969

70+
bag_filter.on_filtering_start()
71+
7072
# get stamp from the tuple
7173
def get_stamp_fn(x):
7274
return x[2]
@@ -116,4 +118,6 @@ def get_stamp_fn(x):
116118
_topic, _raw_msg, _stamp, _connection_header = ret[0]
117119
out.write(_topic, _raw_msg, _stamp, connection_header=_connection_header, raw=True)
118120

121+
bag_filter.on_filtering_end()
122+
119123
bag_filter.reset()

cras_bag_tools/src/cras_bag_tools/message_filter.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55

66
from __future__ import absolute_import, division, print_function
77

8+
import os.path
9+
import re
810
import sys
911
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
1012

1113
import genpy
14+
import rospkg
1215
import rospy
1316
from cras.message_utils import raw_to_msg, msg_to_raw
1417
from cras.plugin_utils import get_plugin_implementations
@@ -113,6 +116,8 @@ def __init__(self, is_raw, include_topics=None, exclude_topics=None, include_typ
113116
self._exclude_time_ranges = self._parse_time_ranges(exclude_time_ranges)
114117
"""Time ranges that specify which regions of the bag should be skipped (but passed further)."""
115118

119+
self.__rospack = None
120+
116121
@staticmethod
117122
def _parse_time_ranges(ranges):
118123
if ranges is None:
@@ -161,6 +166,25 @@ def __get_param(self, params, param, default=None):
161166
key, rest = param.split("/", 1)
162167
return self.__get_param(params.get(key, None), rest, default)
163168

169+
def _set_param(self, param, value):
170+
"""Set parameter `param` to the parameters set by :meth:`set_params`.
171+
172+
:param str param: Name of the parameter.
173+
:param value: The value to set.
174+
"""
175+
self.__set_param(self._params, param, value)
176+
177+
def __set_param(self, params, param, value):
178+
if param.startswith("/"):
179+
param = param[1:]
180+
if "/" in param:
181+
key, rest = param.split("/", 1)
182+
if key not in params:
183+
params[key] = {}
184+
self.__set_param(params[key], rest, value)
185+
else:
186+
params[param] = value
187+
164188
def __call__(self, *args, **kwargs):
165189
"""Do the filtering.
166190
@@ -278,6 +302,39 @@ def extra_time_ranges(self, bags):
278302
"""
279303
return None
280304

305+
def __get_rospack(self):
306+
if self.__rospack is None:
307+
self.__rospack = rospkg.RosPack()
308+
return self.__rospack
309+
310+
def resolve_file(self, filename):
311+
"""Resolve `filename` relative to the bag set by :meth:`set_bag`.
312+
313+
:note: This is ideally called from :meth:`on_filtering_start` or :meth:`filter` because earlier, the `_bag`
314+
member variable is not set.
315+
"""
316+
matches = re.match(r'\$\(find ([^)]+)\)', filename)
317+
if matches is not None:
318+
package_path = self.__get_rospack().get_path(matches[1])
319+
filename = filename.replace('$(find %s)' % (matches[1],), package_path)
320+
321+
if self._bag is None or os.path.isabs(filename) or len(self._bag.filename) == 0:
322+
return filename
323+
324+
return os.path.join(os.path.dirname(self._bag.filename), filename)
325+
326+
def on_filtering_start(self):
327+
"""This function is called right before the first message is passed to filter().
328+
329+
:note: Specifically, :meth:`set_params` and :meth:`set_bag` are already called at this stage.
330+
:note: :meth:`extra_initial_messages` will be called after calling this method.
331+
"""
332+
pass
333+
334+
def on_filtering_end(self):
335+
"""This function is called right after the last message is processed by filter()."""
336+
pass
337+
281338
def reset(self):
282339
"""Reset the filter. This should be called e.g. before starting a new bag."""
283340
self._bag = None
@@ -483,6 +540,15 @@ def filter(self, topic, datatype, data, md5sum, pytype, stamp, header):
483540
return topic, datatype, data, md5sum, pytype, stamp, header
484541

485542

543+
class NoMessageFilter(RawMessageFilter):
544+
"""
545+
Ignore all messages. Good base for message generators or other helpers.
546+
"""
547+
548+
def consider_message(self, topic, datatype, stamp, header, is_from_extra_time_range=False):
549+
return False
550+
551+
486552
class FilterChain(RawMessageFilter):
487553
"""
488554
A chain of message filters.
@@ -586,6 +652,16 @@ def extra_final_messages(self):
586652
for m in f.extra_final_messages():
587653
yield m
588654

655+
def on_filtering_start(self):
656+
for f in self.filters:
657+
f.on_filtering_start()
658+
super(FilterChain, self).on_filtering_start()
659+
660+
def on_filtering_end(self):
661+
for f in self.filters:
662+
f.on_filtering_end()
663+
super(FilterChain, self).on_filtering_end()
664+
589665
def reset(self):
590666
for f in self.filters:
591667
f.reset()

cras_bag_tools/src/cras_bag_tools/message_filters.py

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77

88
import copy
99
import os.path
10-
from collections import deque
11-
from enum import Enum
12-
from typing import Dict, Optional, Union, Tuple, Sequence
13-
import yaml
14-
1510
import matplotlib.cm as cmap
1611
import numpy as np
1712
import sys
13+
import yaml
14+
from collections import deque
15+
from enum import Enum
16+
from typing import Dict, Optional, Union, Tuple, Sequence
17+
from lxml import etree
1818
from numpy.linalg import inv
1919
from typing import Any, Dict
2020

@@ -43,8 +43,8 @@
4343
from urdf_parser_py import urdf, xml_reflection
4444
from vision_msgs.msg import Detection2DArray, Detection2D, ObjectHypothesisWithPose
4545

46-
from .message_filter import DeserializedMessageFilter, DeserializedMessageFilterWithTF, Passthrough, RawMessageFilter, \
47-
TopicSet
46+
from .message_filter import DeserializedMessageFilter, DeserializedMessageFilterWithTF, NoMessageFilter, Passthrough, \
47+
RawMessageFilter, TopicSet
4848

4949

5050
def urdf_error(message):
@@ -1248,7 +1248,7 @@ def _str_params(self):
12481248
return ", ".join(parts)
12491249

12501250

1251-
class AddSubtitles(Passthrough):
1251+
class AddSubtitles(NoMessageFilter):
12521252
"""Read subtitles from a text file and add them to the output as std_msgs/String messages."""
12531253

12541254
def __init__(self, subtitles_file, topic, latch=False, time_offset=0.0):
@@ -1265,7 +1265,14 @@ def __init__(self, subtitles_file, topic, latch=False, time_offset=0.0):
12651265
self._latch = latch
12661266
self._time_offset = rospy.Duration(time_offset)
12671267

1268+
self._subtitles = None
1269+
1270+
def on_filtering_start(self):
1271+
super(AddSubtitles, self).on_filtering_start()
1272+
self._start_time = rospy.Time(self._bag.get_start_time()) + self._time_offset
1273+
subtitles_file = self.resolve_file(self._subtitles_file)
12681274
self._subtitles = self._parse_subtitles(subtitles_file)
1275+
print("Read", len(self._subtitles), "subtitles from file", subtitles_file)
12691276

12701277
def _parse_subtitles(self, subtitles_file):
12711278
if subtitles_file.endswith(".srt"):
@@ -1317,10 +1324,6 @@ def parse_time_str(time_str):
13171324

13181325
return subtitles
13191326

1320-
def set_bag(self, bag):
1321-
super(AddSubtitles, self).set_bag(bag)
1322-
self._start_time = rospy.Time(bag.get_start_time()) + self._time_offset
1323-
13241327
def extra_initial_messages(self):
13251328
connection_header = {
13261329
"callerid": "/bag_filter",
@@ -1338,7 +1341,7 @@ def extra_initial_messages(self):
13381341
yield self._topic, msg, abs_time, connection_header
13391342

13401343
def _str_params(self):
1341-
params = ["topic=" + self._topic, "subtitles_file=" + self._subtitles_file]
1344+
params = ["topic=" + self._topic, "subtitles_file=" + self.resolve_file(self._subtitles_file)]
13421345
if self._time_offset != rospy.Duration(0, 0):
13431346
params.append("time_offset=%f" % (self._time_offset.to_sec(),))
13441347
parent_params = super(AddSubtitles, self)._str_params()
@@ -1347,6 +1350,97 @@ def _str_params(self):
13471350
return ",".join(params)
13481351

13491352

1353+
class DumpRobotModel(NoMessageFilter):
1354+
"""Read robot model URDF from ROS params and store it in a file."""
1355+
1356+
def __init__(self, urdf_file, param="robot_description", remove_comments=False, pretty_print=False,
1357+
run_on_start=True):
1358+
"""
1359+
:param urdf_file: Path to the URDF file. If relative, it will be resolved relative to the bag set by set_bag.
1360+
:param param: The parameter where robot model should be read.
1361+
:param remove_comments: Whether comments should be removed from the output URDF file.
1362+
:param pretty_print: Whether to pretty-print the URDF file (if False, the original layout is preserved).
1363+
:param run_on_start: If true, the model will be exported before messages are processed. If false, the model will
1364+
be exported after processing all messages.
1365+
"""
1366+
super(DumpRobotModel, self).__init__()
1367+
self._urdf_file = urdf_file
1368+
self._param = param
1369+
self._remove_comments = remove_comments
1370+
self._pretty_print = pretty_print
1371+
self._run_on_start = run_on_start
1372+
1373+
def on_filtering_start(self):
1374+
super(DumpRobotModel, self).on_filtering_start()
1375+
1376+
if self._run_on_start:
1377+
self.dump_model()
1378+
1379+
def on_filtering_end(self):
1380+
super(DumpRobotModel, self).on_filtering_end()
1381+
1382+
if not self._run_on_start:
1383+
self.dump_model()
1384+
1385+
def dump_model(self):
1386+
urdf = self._get_param(self._param)
1387+
if urdf is None:
1388+
print('Robot model not found on parameter', self._param, file=sys.stderr)
1389+
return
1390+
1391+
if self._remove_comments or self._pretty_print:
1392+
parser = etree.XMLParser(remove_comments=self._remove_comments, encoding='utf-8')
1393+
tree = etree.fromstring(urdf.encode('utf-8'), parser=parser)
1394+
urdf = etree.tostring(tree, encoding='utf-8', xml_declaration=True,
1395+
pretty_print=self._pretty_print).decode('utf-8')
1396+
1397+
dest = self.resolve_file(self._urdf_file)
1398+
with open(dest, 'w+') as f:
1399+
print(urdf, file=f)
1400+
print("Robot model from parameter", self._param, "exported to", dest)
1401+
1402+
def _str_params(self):
1403+
params = ["param=" + self._param, "urdf_file=" + self.resolve_file(self._urdf_file)]
1404+
parent_params = super(DumpRobotModel, self)._str_params()
1405+
if len(parent_params) > 0:
1406+
params.append(parent_params)
1407+
return ",".join(params)
1408+
1409+
1410+
class UpdateRobotModel(NoMessageFilter):
1411+
"""Read robot model from URDF file and update it in the ROS parameters."""
1412+
1413+
def __init__(self, urdf_file, param="robot_description"):
1414+
"""
1415+
:param urdf_file: Path to the URDF file. If relative, it will be resolved relative to the bag set by set_bag.
1416+
:param param: The parameter where robot model should be stored.
1417+
"""
1418+
super(UpdateRobotModel, self).__init__()
1419+
self._urdf_file = urdf_file
1420+
self._param = param
1421+
1422+
def on_filtering_start(self):
1423+
super(UpdateRobotModel, self).on_filtering_start()
1424+
1425+
src = self.resolve_file(self._urdf_file)
1426+
if not os.path.exists(src):
1427+
print('Cannot find robot URDF file', src, file=sys.stderr)
1428+
return
1429+
1430+
with open(src, 'r', encoding='utf-8') as f:
1431+
urdf = f.read()
1432+
1433+
self._set_param(self._param, urdf)
1434+
print('Robot model from %s set to parameter %s' % (src, self._param))
1435+
1436+
def _str_params(self):
1437+
params = ["param=" + self._param, "urdf_file=" + self.resolve_file(self._urdf_file)]
1438+
parent_params = super(UpdateRobotModel, self)._str_params()
1439+
if len(parent_params) > 0:
1440+
params.append(parent_params)
1441+
return ",".join(params)
1442+
1443+
13501444
class FixSpotCams(RawMessageFilter):
13511445
"""Fix a problem with Spot robot cameras that publish a bit weird message header."""
13521446

0 commit comments

Comments
 (0)