• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarlab-gatech / robotdataprocess / 20172884498

12 Dec 2025 04:18PM UTC coverage: 74.672% (+1.2%) from 73.457%
20172884498

push

github

web-flow
(v0.1.2) Path Metrics & Transformations, Python >=3.8 Support

269 of 364 new or added lines in 8 files covered. (73.9%)

6 existing lines in 4 files now uncovered.

1082 of 1449 relevant lines covered (74.67%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.13
/src/robotdataprocess/data_types/OdometryData.py
1
from __future__ import annotations
1✔
2

3
from ..conversion_utils import col_to_dec_arr
1✔
4
import csv
1✔
5
from .Data import CoordinateFrame, Data
1✔
6
import decimal
1✔
7
from decimal import Decimal
1✔
8
from evo.core import geometry
1✔
9
import matplotlib.pyplot as plt
1✔
10
import numpy as np
1✔
11
from numpy.typing import NDArray
1✔
12
import os
1✔
13
import pandas as pd
1✔
14
from .PathData import PathData
1✔
15
from pathlib import Path
1✔
16
from ..rosbag.Ros2BagWrapper import Ros2BagWrapper
1✔
17
from rosbags.rosbag2 import Reader as Reader2
1✔
18
from rosbags.typesys import Stores, get_typestore
1✔
19
from rosbags.typesys.store import Typestore
1✔
20
from scipy.spatial.transform import Rotation as R
1✔
21
from typeguard import typechecked
1✔
22
from typing import Union, List, Tuple
1✔
23
import tqdm
1✔
24

25
class OdometryData(PathData):
1✔
26

27
    # Define odometry-specific data attributes
28
    child_frame_id: str
1✔
29
    poses: list # Saved nav_msgs/msg/Pose
1✔
30

31
    @typechecked
1✔
32
    def __init__(self, frame_id: str, child_frame_id: str, timestamps: Union[np.ndarray, list], 
1✔
33
                 positions: Union[np.ndarray, list], orientations: Union[np.ndarray, list], frame: CoordinateFrame):
34
        
35
        # Copy initial values into attributes
36
        super().__init__(frame_id, timestamps, positions, orientations, frame)
1✔
37
        self.child_frame_id: str = child_frame_id
1✔
38
        self.poses: list = []
1✔
39

40
        # Check to ensure that all arrays have same length
41
        if len(self.timestamps) != len(self.positions) or len(self.positions) != len(self.orientations):
1✔
UNCOV
42
            raise ValueError("Lengths of timestamp, position, and orientation arrays are not equal!")
×
43

44
    # =========================================================================
45
    # ============================ Class Methods ============================== 
46
    # =========================================================================  
47

48
    @classmethod
1✔
49
    @typechecked
1✔
50
    def from_ros2_bag(cls, bag_path: Union[Path, str], odom_topic: str, frame: CoordinateFrame):
1✔
51
        """
52
        Creates a class structure from a ROS2 bag file with an Odometry topic.
53

54
        Args:
55
            bag_path (Path | str): Path to the ROS2 bag file.
56
            odom_topic (str): Topic of the Odometry messages.
57
        Returns:
58
            OdometryData: Instance of this class.
59
        """
60

61
        # Get topic message count and typestore
62
        bag_wrapper = Ros2BagWrapper(bag_path, None)
1✔
63
        typestore: Typestore = bag_wrapper.get_typestore()
1✔
64
        num_msgs: int = bag_wrapper.get_topic_count(odom_topic)
1✔
65
        
66
        # Pre-allocate arrays (memory-mapped or otherwise)
67
        timestamps_np = np.zeros(num_msgs, dtype=Decimal)
1✔
68
        positions_np = np.zeros((num_msgs, 3), dtype=Decimal)
1✔
69
        orientations_np = np.zeros((num_msgs, 4), dtype=Decimal)
1✔
70

71
        # Setup tqdm bar & counter
72
        pbar = tqdm.tqdm(total=num_msgs, desc="Extracting Odometry...", unit=" msgs")
1✔
73
        i = 0
1✔
74

75
        # Extract the odometry information
76
        frame_id, child_frame_id = None, None
1✔
77
        with Reader2(str(bag_path)) as reader:
1✔
78

79
            # Extract frames from first message
80
            connections = [x for x in reader.connections if x.topic == odom_topic]
1✔
81
            for conn, timestamp, rawdata in reader.messages(connections=connections):  
1✔
82
                msg = typestore.deserialize_cdr(rawdata, conn.msgtype)
1✔
83
                frame_id = msg.header.frame_id
1✔
84
                child_frame_id = msg.child_frame_id
1✔
85
                break
1✔
86

87
            # Extract individual message data
88
            connections = [x for x in reader.connections if x.topic == odom_topic]
1✔
89
            for conn, timestamp, rawdata in reader.messages(connections=connections):
1✔
90
                msg = typestore.deserialize_cdr(rawdata, conn.msgtype)
1✔
91
                
92
                timestamps_np[i] = bag_wrapper.extract_timestamp(msg)
1✔
93
                pos = msg.pose.pose.position
1✔
94
                positions_np[i] = np.array([Decimal(pos.x), Decimal(pos.y), Decimal(pos.z)])
1✔
95
                ori = msg.pose.pose.orientation
1✔
96
                orientations_np[i] = np.array([Decimal(ori.x), Decimal(ori.y), Decimal(ori.z), Decimal(ori.w)])
1✔
97

98
                # NOTE: Doesn't support Twist information currently
99

100
                # Increment the count
101
                i += 1
1✔
102
                pbar.update(1)
1✔
103

104
        # Create an OdometryData class
105
        return cls(frame_id, child_frame_id, timestamps_np, positions_np, orientations_np, frame)
1✔
106
    
107
    @classmethod
1✔
108
    @typechecked
1✔
109
    def from_csv(cls, csv_path: Union[Path, str], frame_id: str, child_frame_id: str, frame: CoordinateFrame,          
1✔
110
                 header_included: bool, column_to_data: Union[List[int], None] = None, 
111
                 separator: Union[str, None] = None, filter: Union[Tuple[str, str], None] = None,
112
                 ts_in_ns: bool = False):
113
        """
114
        Creates a class structure from a csv file.
115

116
        Args:
117
            csv_path (Path | str): Path to the CSV file.
118
            frame_id (str): The frame that this odometry is relative to.
119
            child_frame_id (str): The frame whose pose is represented by this odometry.
120
            frame (CoordinateFrame): The coordinate system convention of this data.
121
            header_included (bool): If this csv file has a header, so we can remove it.
122
            column_to_data (list[int]): Tells the algorithms which columns in the csv contain which
123
                of the following data: ['timestamp', 'x', 'y', 'z', 'qw', 'qx', 'qy', 'qz']. Thus, 
124
                index 0 of column_to_data should be the column that timestamp data is found in the 
125
                csv file. Set to None to use [0,1,2,3,4,5,6,7].
126
            separator (str | None): The separator used in the csv file. If None, will use a comma by default.
127
            filter: A tuple of (column_name, value), where only rows with column_name == value will be kept. If
128
                csv file has no headers, then `column_name` should be the index of the column as a string.
129
            ts_in_ns (bool): If True, assumes timestamps are in nanoseconds and converts to seconds. Otherwise,
130
                assumes timestamps are already in seconds.
131

132
        Returns:
133
            OdometryData: Instance of this class.
134
        """
135

136
        # If column_to_data is None, assume default
137
        if column_to_data is None:
1✔
138
            column_to_data = [0,1,2,3,4,5,6,7]
1✔
139
        else:
140
            # Check column_to_data values are valid
141
            assert np.all(np.array(column_to_data) >= 0)
1✔
142
            assert len(column_to_data) == 8
1✔
143

144
        # Read the csv file
145
        header = 0 if header_included else None
1✔
146
        df1 = pd.read_csv(str(csv_path), header=header, index_col=False, sep=separator, engine='python')
1✔
147

148
        # Rename columns to standard names
149
        rename_dict = {}
1✔
150
        desired_data = ['timestamp', 'x', 'y', 'z', 'qw', 'qx', 'qy', 'qz']
1✔
151
        for j, ind in enumerate(column_to_data):
1✔
152
            rename_dict[df1.columns[ind]] = desired_data[j]
1✔
153
        df1 = df1.rename(columns=rename_dict)
1✔
154

155
        # Using the filter if provided, remove unwanted rows
156
        if filter is not None:
1✔
157
            df1 = df1[df1[filter[0]] == filter[1]]
1✔
158

159
        # Convert columns to NDArray[Decimal]
160
        timestamps_np = np.array([Decimal(str(ts)) for ts in df1['timestamp']], dtype=object)
1✔
161
        positions_np = np.array([[Decimal(str(x)), Decimal(str(y)), Decimal(str(z))] 
1✔
162
                                 for x, y, z in zip(df1['x'], df1['y'], df1['z'])], dtype=object)
163
        orientations_np = np.array([[Decimal(str(qx)), Decimal(str(qy)), Decimal(str(qz)), Decimal(str(qw))]
1✔
164
                                    for qx, qy, qz, qw in zip(df1['qx'], df1['qy'], df1['qz'], df1['qw'])], dtype=object)
165
        
166
        # If timestamps are in ns, convert to s
167
        if ts_in_ns:
1✔
168
            timestamps_np = timestamps_np / Decimal('1e9')
1✔
169

170
        # Create an OdometryData class
171
        return cls(frame_id, child_frame_id, timestamps_np, positions_np, orientations_np, frame)
1✔
172
    
173
    @classmethod
1✔
174
    @typechecked
1✔
175
    def from_txt_file(cls, file_path: Union[Path, str], frame_id: str, child_frame_id: str, frame: CoordinateFrame):
1✔
176
        """
177
        Creates a class structure from a text file, where the order of values
178
        in the files follows ['timestamp', 'x', 'y', 'z', 'qw', 'qx', 'qy', 'qz'].
179

180
        Args:
181
            file_path (Path | str): Path to the file containing the odometry data.
182
            frame_id (str): The frame where this odometry is relative to.
183
            child_frame_id (str): The frame whose pose is represented by this odometry.
184
            frame (CoordinateFrame): The coordinate system convention of this data.
185
        Returns:
186
            OdometryData: Instance of this class.
187
        """
188
        
189
        # Count the number of lines in the file
190
        line_count = 0
1✔
191
        with open(str(file_path), 'r') as file:
1✔
192
            for _ in file: 
1✔
193
                line_count += 1
1✔
194

195
        # Setup arrays to hold data
196
        timestamps_np = np.zeros((line_count), dtype=object)
1✔
197
        positions_np = np.zeros((line_count, 3), dtype=object)
1✔
198
        orientations_np = np.zeros((line_count, 4), dtype=object)
1✔
199

200
        # Open the txt file and read in the data
201
        with open(str(file_path), 'r') as file:
1✔
202
            for i, line in enumerate(file):
1✔
203
                line_split = line.split(' ')
1✔
204
                timestamps_np[i] = line_split[0]
1✔
205
                positions_np[i] = line_split[1:4]
1✔
206
                orientations_np[i] =  line_split[5:8] + [line_split[4]]
1✔
207
        
208
        # Create an OdometryData class
209
        return cls(frame_id, child_frame_id, timestamps_np, positions_np, orientations_np, frame)
1✔
210
    
211
    # =========================================================================
212
    # ========================= Manipulation Methods ========================== 
213
    # =========================================================================  
214
    
215
    def add_folded_guassian_noise_to_position(self, xy_noise_std_per_frame: float,
1✔
216
            z_noise_std_per_frame: float):
217
        """
218
        This method simulates odometry drift by adding folded gaussian noise
219
        to the odometry positions on a per frame basis. It also accumulates
220
        it over time. NOTE: It completely ignores the timestamps, and the "folded
221
        guassian noise" distribution stds might not align with the stds of the 
222
        guassian used internally, so this is not a robust function at all.
223

224
        Args:
225
            xy_noise_std_per_frame (float): Standard deviation of the gaussian 
226
                distribution for xy, whose output is then run through abs().
227
            z_noise_std_per_frame (float): Same as above, but for z.
228
        """
229

230
        # Track cumulative noise for each field
231
        cumulative_noise_pos = {'x': 0.0, 'y': 0.0, 'z': 0.0}
×
232

233
        # For each position
234
        for i in range(len(self.timestamps)):
×
235

236
            # Sample noise and accumulate
237
            noise_pos = {'x': np.random.normal(0, xy_noise_std_per_frame),
×
238
                        'y': np.random.normal(0, xy_noise_std_per_frame),
239
                        'z': np.random.normal(0, z_noise_std_per_frame)}
240
            for key in cumulative_noise_pos:
×
241
                cumulative_noise_pos[key] += abs(noise_pos[key])
×
242

243
            # Update positions
244
            self.positions[i][0] += Decimal(cumulative_noise_pos['x'])
×
245
            self.positions[i][1] += Decimal(cumulative_noise_pos['y'])
×
246
            self.positions[i][2] += Decimal(cumulative_noise_pos['z'])
×
247

248
    @typechecked
1✔
249
    def shift_position(self, x_shift: float, y_shift: float, z_shift: float):
1✔
250
        """
251
        Shifts the positions of the odometry.
252

253
        Args:
254
            x_shift (float): Shift in x-axis.
255
            y_shift (float): Shift in y_axis.
256
            z_shift (float): Shift in z_axis.
257
        """
258
        self.positions[:,0] += Decimal(x_shift)
×
259
        self.positions[:,1] += Decimal(y_shift)
×
260
        self.positions[:,2] += Decimal(z_shift)
×
261

262
    def shift_to_start_at_identity(self):
1✔
263
        """
264
        Alter the positions and orientations based so that the first pose 
265
        starts at Identity.
266
        """
267

268
        # Get pose of first robot position w.r.t world
269
        R_o = R.from_quat(self.orientations[0]).as_matrix()
1✔
270
        T_o = np.expand_dims(self.positions[0], axis=1)
1✔
271
        
272
        # Calculate the inverse (pose of world w.r.t first robot location)
273
        R_inv = R_o.T
1✔
274

275
        # Rotate positions and orientations
276
        self.positions = (R_inv @ (self.positions.T - T_o).astype(float)).T
1✔
277
        for i in range(self.len()):
1✔
278
            self.orientations[i] = R.from_matrix((R_inv @ R.from_quat(self.orientations[i]).as_matrix())).as_quat()
1✔
279

280
        # Convert back to decimal array
281
        self.positions = col_to_dec_arr(self.positions)
1✔
282
        self.orientations = col_to_dec_arr(self.orientations)
1✔
283

284
    def crop_data(self, start: Decimal, end: Decimal):
1✔
285
        """ Will crop the data so only values within [start, end] inclusive are kept. """
286

287
        # Create boolean mask of data to keep
288
        mask = (self.timestamps >= start) & (self.timestamps <= end)
1✔
289

290
        # Apply mask
291
        self.timestamps = self.timestamps[mask]
1✔
292
        self.positions = self.positions[mask]
1✔
293
        self.orientations = self.orientations[mask]
1✔
294

295
        # Empty poses as they might need to be recalculated
296
        self.poses = []
1✔
297

298
    # =========================================================================
299
    # ============================ Export Methods ============================= 
300
    # =========================================================================  
301

302
    @typechecked
1✔
303
    def to_csv(self, csv_path: Union[Path, str]):
1✔
304
        """
305
        Writes the odometry data to a .csv file. Note that data will be
306
        saved in the following order: timestamp, pos.x, pos.y, pos.z,
307
        ori.w, ori.x, ori.y, ori.z. Timestamp is in seconds.
308

309
        Args:
310
            csv_path (Path | str): Path to the output csv file.
311
            odom_topic (str): Topic of the Odometry messages.
312
        Returns:
313
            OdometryData: Instance of this class.
314
        """
315

316
        # setup tqdm 
317
        pbar = tqdm.tqdm(total=None, desc="Saving to csv... ", unit=" frames")
1✔
318

319
        # Check that file path doesn't already exist
320
        file_path = Path(csv_path)
1✔
321
        if os.path.exists(file_path):
1✔
322
            raise ValueError(f"Output file already exists: {file_path}")
×
323
        
324
        # Open csv file
325
        with open(file_path, 'w', newline='') as csvfile:
1✔
326
            writer = csv.writer(csvfile, delimiter=',')
1✔
327

328
            # Write the first row
329
            writer.writerow(['timestamp', 'x', 'y', 'z', 'qw', 'qx', 'qy', 'qz'])
1✔
330
                
331
            # Write message data to the csv file
332
            for i in range(len(self.timestamps)):
1✔
333
                writer.writerow([str(self.timestamps[i]), 
1✔
334
                    str(self.positions[i][0]), str(self.positions[i][1]), str(self.positions[i][2]),
335
                    str(self.orientations[i][3]), str(self.orientations[i][0]), str(self.orientations[i][1]), 
336
                    str(self.orientations[i][2])])
337
                pbar.update(1)
1✔
338

339
    # =========================================================================
340
    # =========================== Frame Conversions =========================== 
341
    # ========================================================================= 
342
    def to_FLU_frame(self):
1✔
343
        # If we are already in the FLU frame, return
344
        if self.frame == CoordinateFrame.FLU:
1✔
345
            print("Data already in FLU coordinate frame, returning...")
1✔
346
            return
1✔
347

348
        # If in NED, run the conversion
349
        elif self.frame == CoordinateFrame.NED:
1✔
350
            # Define the rotation matrix
351
            R_NED = np.array([[1,  0,  0],
1✔
352
                              [0, -1,  0],
353
                              [0,  0, -1]])
354

355
            # Do a change of basis to update the frame
356
            self._convert_frame(R_NED)
1✔
357

358
            # Update frame
359
            self.frame = CoordinateFrame.FLU
1✔
360

361
        # Otherwise, throw an error
362
        else:
363
            raise RuntimeError(f"OdometryData class is in an unexpected frame: {self.frame}!")
1✔
364
    
365
    @typechecked
1✔
366
    def _convert_frame(self, R_frame: np.ndarray):
1✔
367
        """ Uses a change of basis to update the positions and orientations. """
368
        R_frame_Q = R.from_matrix(R_frame)
1✔
369
        self.positions = (R_frame @ self.positions.T).T
1✔
370
        self._ori_change_of_basis(R_frame_Q)
1✔
371

372
    @typechecked
1✔
373
    def _ori_apply_rotation(self, R_i: R):
1✔
374
        """ Applies a rotation (not a change of basis) to orientations, thus stays in the same frame. """
375
        for i in range(self.len()):
1✔
376
            self.orientations[i] = (R_i * R.from_quat(self.orientations[i])).as_quat()
1✔
377

378
    @typechecked
1✔
379
    def _ori_change_of_basis(self, R_i: R):
1✔
380
        """ Applies a change of basis to orientations """
381
        for i in range(self.len()):
1✔
382
            self.orientations[i] = (R_i * R.from_quat(self.orientations[i]) * R_i.inv()).as_quat()
1✔
383

384
    # =========================================================================
385
    # =========================== Conversion to ROS =========================== 
386
    # ========================================================================= 
387

388
    @typechecked
1✔
389
    @staticmethod
1✔
390
    def get_ros_msg_type(msg_type: str = "Odometry") -> str:
1✔
391
        """ Return the __msgtype__ for an Imu msg. """
392
        typestore = get_typestore(Stores.ROS2_HUMBLE)
1✔
393
        if msg_type == "Odometry":
1✔
394
            return typestore.types['nav_msgs/msg/Odometry'].__msgtype__
1✔
395
        elif msg_type == "Path":
1✔
396
            return typestore.types['nav_msgs/msg/Path'].__msgtype__
1✔
397
        else:
398
            raise ValueError(f"Unsupported msg_type for OdometryData: {msg_type}")
×
399
    
400
    @typechecked
1✔
401
    def extract_seconds_and_nanoseconds(self, i: int):
1✔
402
        seconds = int(self.timestamps[i])
1✔
403
        nanoseconds = (self.timestamps[i] - self.timestamps[i].to_integral_value(rounding=decimal.ROUND_DOWN)) \
1✔
404
                        * Decimal("1e9").to_integral_value(decimal.ROUND_HALF_EVEN)
405
        return seconds, nanoseconds
1✔
406
    
407
    def calculate_stamped_poses(self):
1✔
408
        """ Pre-calculates all stamped poses if they aren't calculated yet."""
409

410
        if len(self.poses) != self.len():
1✔
411
            # Get ROS2 message types
412
            typestore = get_typestore(Stores.ROS2_HUMBLE)
1✔
413
            Header = typestore.types['std_msgs/msg/Header']
1✔
414
            Time = typestore.types['builtin_interfaces/msg/Time']
1✔
415
            PoseStamped = typestore.types['geometry_msgs/msg/PoseStamped']
1✔
416
            Pose = typestore.types['geometry_msgs/msg/Pose']
1✔
417
            Point = typestore.types['geometry_msgs/msg/Point']
1✔
418
            Quaternion = typestore.types['geometry_msgs/msg/Quaternion']
1✔
419

420
            # Pre-calculate all the poses
421
            for i in range(self.len()):
1✔
422
                seconds, nanoseconds = self.extract_seconds_and_nanoseconds(i)
1✔
423
                self.poses.append(PoseStamped(Header(stamp=Time(sec=int(seconds), 
1✔
424
                                                                nanosec=int(nanoseconds)),
425
                                                    frame_id=self.frame_id),
426
                                            pose=Pose(position=Point(x=self.positions[i][0],
427
                                                                    y=self.positions[i][1],
428
                                                                    z=self.positions[i][2]),
429
                            orientation=Quaternion(x=self.orientations[i][0],
430
                                                    y=self.orientations[i][1],
431
                                                    z=self.orientations[i][2],
432
                                                    w=self.orientations[i][3]))))
433

434
    @typechecked
1✔
435
    def get_ros_msg(self, i: int, msg_type: str = "Odometry"):
1✔
436
        """
437
        Gets an Image ROS2 Humble message corresponding to the odometry in index i.
438
        
439
        Args:
440
            i (int): The index of the odometry data to convert.
441
        Raises:
442
            ValueError: If i is outside the data bounds.
443
        """
444

445
        # Calculate the Stamped Poses
446
        self.calculate_stamped_poses()
1✔
447

448
        # Make sure our data is in the FLU frame, otherwise throw an error
449
        if self.frame != CoordinateFrame.FLU:
1✔
450
            raise RuntimeError("Convert this Odometry Data to a FLU frame before writing to a ROS2 bag!")
×
451

452
        # Check to make sure index is within data bounds
453
        if i < 0 or i >= self.len():
1✔
454
            raise IndexError(f"Index {i} is out of bounds!")
×
455

456
        # Get ROS2 message classes
457
        typestore = get_typestore(Stores.ROS2_HUMBLE)
1✔
458
        Odometry = typestore.types['nav_msgs/msg/Odometry']
1✔
459
        Header = typestore.types['std_msgs/msg/Header']
1✔
460
        Time = typestore.types['builtin_interfaces/msg/Time']
1✔
461
        PoseWithCovariance = typestore.types['geometry_msgs/msg/PoseWithCovariance']
1✔
462
        TwistWithCovariance = typestore.types['geometry_msgs/msg/TwistWithCovariance']
1✔
463
        Twist = typestore.types['geometry_msgs/msg/Twist']
1✔
464
        Vector3 = typestore.types['geometry_msgs/msg/Vector3']
1✔
465
        Path = typestore.types['nav_msgs/msg/Path']
1✔
466
        Pose = typestore.types['geometry_msgs/msg/Pose']
1✔
467
        Point = typestore.types['geometry_msgs/msg/Point']
1✔
468
        Quaternion = typestore.types['geometry_msgs/msg/Quaternion']
1✔
469

470
        # Write the data into the new msg
471
        if msg_type == "Odometry":
1✔
472
            seconds, nanoseconds = self.extract_seconds_and_nanoseconds(i)
1✔
473
            return Odometry(Header(stamp=Time(sec=int(seconds), 
1✔
474
                                              nanosec=int(nanoseconds)), 
475
                            frame_id=self.frame_id),
476
                            child_frame_id=self.child_frame_id,
477
                            pose=PoseWithCovariance(pose=Pose(position=Point(x=self.positions[i][0],
478
                                                                 y=self.positions[i][1],
479
                                                                 z=self.positions[i][2]),
480
                                                    orientation=Quaternion(x=self.orientations[i][0],
481
                                                                            y=self.orientations[i][1],
482
                                                                            z=self.orientations[i][2],
483
                                                                            w=self.orientations[i][3])),
484
                                                    covariance=np.zeros(36)),
485
                            twist=TwistWithCovariance(twist=Twist(linear=Vector3(x=0, # Currently doesn't support Twist
486
                                                                                y=0,
487
                                                                                z=0,),
488
                                                                angular=Vector3(x=0,
489
                                                                                y=0,
490
                                                                                z=0,)),
491
                                                    covariance=np.zeros(36)))
492
        elif msg_type == "Path":
1✔
493
            seconds, nanoseconds = self.extract_seconds_and_nanoseconds(i)
1✔
494
            return Path(Header(stamp=Time(sec=int(seconds), 
1✔
495
                                          nanosec=int(nanoseconds)),
496
                               frame_id=self.frame_id),
497
                               poses=self.poses[0:i+1:10])
498
        else:
UNCOV
499
            raise ValueError(f"Unsupported msg_type for OdometryData: {msg_type}")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc