Tangram Vision Blog | Moving MetriCal Metrics to MCAPs

Moving MetriCal Metrics to MCAPs

MCAPs aren't just for inputs anymore
Jeremy Steward
,
Staff Perception Engineer

Sep 16, 2025

In a few weeks, Tangram Vision will be releasing version 15.0.0 of MetriCal. This release is probably our biggest yet (though we say that every release). It represents a refinement of MetriCal’s processes and workflow, based directly on customer conversations and experience.

As part of this refinement, we are doing something a little radical: MetriCal’s outputs will no longer be JSON. Instead, all outputs will be formatted into an MCAP.

This choice makes a lot of sense to us, and represents a quality-of-life improvement to our customers. However, we haven’t seen other platforms do anything similar before. In that light, we wanted to use this space to explain our rationale and explore the ramifications of the move.

Note: This post is about the motivation for the switch. We’ve left the implementation notes on the change to the end; be sure to skip ahead if that's what you're looking for.

What’s an MCAP?

MCAP, for those unaware, is a relatively young data storage format in the robotics space. Despite this, it has become the default recording format for ROS2 since its debut and supplants any uses of the old ROS1 bag format. As such, MetriCal has read MCAP datasets for quite some time.

Using MCAPs over folders or ROSbags presents a laundry list of advantages:

It’s for these reasons that we strongly recommend anyone working with MetriCal today use MCAPs to record and play back data. In fact, we plan to deprecate other modes of data intake in the near future (but not for 15.0.0, don’t worry).

On Backwards Compatibility

Despite the many ways you can input data into MetriCal today, it only writes results one way: JSON. Unfortunately, when first designing these outputs in 2021, there was no obvious alternative to JSON. It fit our needs and got the job done. It wasn’t until MetriCal started being integrated into real production lines, for real use cases and customers, that we started realizing the repercussions of adopting the format for our outputs.

One of the largest consequences of this decision, and a large driver of this recent work, was a lack of backwards compatibility. In particular, the results.json file produced at the end of calibration runs have never been guaranteed backwards compatible. This causes problems when trying to investigate an older results.json with a newer version of MetriCal than what generated the file.

We kept it this way for a long time. It was both a product decision as well as a technological limitation.

As Product Decision: What’s Useful?

Before MetriCal reached today’s semi-stable state, output metrics changed rapidly and, as a business, we had not fully materialized which metrics were useful. Given that these metrics are often the output of a complex, multi-faceted optimization process, it was not always an easy decision to articulate what every metric meant, or whether it was something that could be interpreted in context of the calibration. Even today, there are metrics that are easy-to-compute but hard-to-interpret. Put all together, we weren’t in a logistic position to make something like results.json backwards-compatible in any way.

As Technical Decision: What’s Feasible?

Technically, the choice of JSON was done for expediency: It was simple enough to use Rust’s serde and serde-json crates to serialize any type we wanted to JSON with very little to change other than the type definition itself. However, we knew early on that JSON has its own set of limitations and was not in itself a great choice:

We kicked the proverbial can down the road until we couldn’t anymore. MetriCal is now expected to deliver not just accuracy and consistency in its metrics, but also its procedures. It’s relied upon by some of the largest automation companies in the world. We needed a solution to providing backwards-compatible outputs that would serve those customers long-term.

MCAP To The Rescue

And that solution, you already know, is MCAP. We knew the advantages clearly from our time working with the format for data ingress; those same advantages made it a clear choice for data output as well. Specifically, the format directly solved a lot of our technical concerns with serde and the serde-json crate:

There were also reasons of convenience. For instance, MetriCal already supports reading MCAP files, and we could preserve backwards compatibility even with our JSON structs via MCAP’s attachments paradigm.

Fundamentally, using MCAP provides a layer of indirection between our internal representation (in our Rust code) of the various metrics versus how they're serialized at rest. What’s more, MCAP makes sense from an operational standpoint due to its heavy use in the ROS ecosystem as well as due to the fact that many high-quality libraries exist in multiple languages.

Benefits to MetriCal Users Today

Besides having a backwards compatible output from our calibration process, there are other benefits that MetriCal users will experience today by switching to MCAP.

First off, the compression benefits cannot be understated. In some of the larger IMU and LiDAR datasets we have seen calibrated, this provides on average 3× compression (although the full amount of savings will depend largely on the type of calibration being performed).

In addition, Result MCAPs allow reconstruction of the exact inputs to a specific version of MetriCal. Before, it was often difficult to pin down the exact plex and object-space that produced poor results; users often forgot the exact command invocation used to produce a given set of results. By organizing all of that information into a single artefact, we can pull everything from the results MCAP, rather than try to rely on memory alone.

Get Ready for 15.0.0!

We realize that this blog post may sound like we’re making the case for MCAPs over JSON, but there’s really no case to be made; the advantages are clear. MCAP makes a ton of sense, it solves a lot of user problems, and makes our lives easier. We’re happy about the results (no pun intended), and we know you will be, too.

This was one of the biggest changes in 15.0.0, but it’s not all. The next edition of MetriCal will fulfill a ton of promises we’ve been making to users for literal years, and we couldn’t be more excited to release it. So don’t touch that dial! Stay tuned to Tangram for more great announcements in the coming weeks.

Implementation Discussion

If you’re interested in the implementation notes for this change, keep reading!

Tangram’s Results MCAP

The use of MetriCal’s results JSON usually serves (one of) several purposes:

What this meant was that we needed to organize the data within a given results MCAP such that it corresponded with the appropriate use. To give a brief introduction of the MCAP format, you can typically write data to three different kinds of places:

  1. A metadata record, which holds a key-value map of various string values

  2. Messages written to channels. Data written to a channel can be in any one of the well-known encodings

  3. Attachments of separate, single files that are related to the data, but not part of any specific channel stream

Since you might find yourself using this sometime soon, we’ll explain why data was placed in each section below. If you are reading this after getting a results MCAP yourself, we highly recommend installing the MCAP CLI tool to aid in understanding what’s being written under the hood.

Metadata

Tangram writes a metrical metadata record to the MCAP file. The metadata is primarily meant for debugging purposes, as it contains fairly sparse data about how MetriCal was run when the results MCAP was written. Specifically, we encode:

If you ever need to grab this information, any MCAP compatible software should work. For example with the MCAP CLI tool:

$ mcapget metadata --name metrical results.mcap

{

"arguments": "calibrate --camera-motion-threshold 3.5 -o results.mcap --report-path report.html camera_imu_box.mcap plex.json camera_imu_box_object.json --override-diagnostics",

"program": "metrical",

"version": "15.0.0-rc.0"

}

Channels

Tangram writes all of our metrics data out to channels. The individual metrics can be broken down into three rough categories:

  1. Pre-Calibration data. These metrics are any heuristics or data that we derive purely from the feature detection and data ingestion process.

  2. Residual metrics, abbreviated as just “metrics.” These are directly related to the various optimization costs constructed during the calibration, and usually give some sense of the residuals that get minimized during a calibration run.

  3. Summary metrics. These are typically a summary of other residual metrics that may be useful to reference as a global view of the optimization.

Using the MCAP CLI tool:

$ mcap info results.mcap

library: mcap-rs-0.23.3

profile:

messages: 1096

duration: 18.145069ms

start: 2025-09-04T09:15:37.638397288-06:00 (1756998937.638397288)

end: 2025-09-04T09:15:37.656542357-06:00 (1756998937.656542357)

compression:

lz4: [9/9 chunks] [6.11 MiB/4.15 MiB (31.99%)] [228.95 MiB/sec]

channels:

(1) /pre_calibration/topic_filter_statistics 4 msgs (220.45 Hz) : tangram.pre_cal.TopicFilterStatistics [protobuf]

(2) /pre_calibration/binned_feature_counts 3 msgs (165.33 Hz) : tangram.pre_cal.BinnedFeatureCount [protobuf]

(3) /metrics/image_reprojection 1072 msgs (59079.41 Hz) : tangram.metrics.ImageReprojection [protobuf]

(4) /metrics/composed_relative_extrinsics 6 msgs (330.67 Hz) : tangram.metrics.ComposedExtrinsics [protobuf]

(5) /metrics/imu_preintegration_error 3 msgs (165.33 Hz) : tangram.metrics.ImuPreintegrationError [protobuf]

(6) /metrics/object_inertial_extrinsics_error 3 msgs (165.33 Hz) : tangram.metrics.ObjectInertialExtrinsicsError [protobuf]

(7) /summary/overall 1 msgs : tangram.summary.OverallSummary [protobuf]

(8) /summary/camera 3 msgs (165.33 Hz) : tangram.summary.CameraSummary [protobuf]

(9) /summary/imu 1 msgs : tangram.summary.ImuSummary [protobuf]

channels: 9

attachments: 5

metadata: 1

As can be seen, each of the above channels is prefixed with either /pre_calibration, /metrics, or /summary, for each of the three sections above. As can be seen in the output above, we now have a collection of published schemata for each message kind. These can be found here. Tangram has chosen to use Protobuf as the encoding kind for these messages because it provides a binary encoding of each of these messages and because Protobuf messages can be evolved in a backwards-compatible manner.

Attachments

Last but certainly not least are the attachments. Attachments are a bit of an odd duck in the MCAP space: while one can attach as many unrelated files to an MCAP as one chooses, attachments differ from messages on channels in that they:

In terms of deciding what should be an attachment versus what should be a message on a channel, we opted to look at the typical data relationship that attachments imply. Notably, while you can have multiple attachments with the same name inside of a given MCAP, attachments are primarily optimized for the use-case where there is either zero or one attachment related to the topics listed in the MCAP. Conversely, channels tend to be optimized for the use-case where there may be 0 to N-many messages associated with a particular schema or topic.

Given that, we opted to attach some of the following entities to the MCAP itself:

For example:

$ mcap list attachments results.mcap

name media type log time creation time content length offset

input-plex application/json 175699893763822027706148291

input-object-space application/json 175699893763823758109316510

association application/json 1756998937661339340020384944382014

optimized-object-space application/json 17569989376637874850289726420580

optimized-plex application/json 17569989376638674440161976449635

It was important in particular as part of these efforts to preserve the usage of both plex and object-space JSON files as part of our workflow. Not only do we often hand-edit these internally, these files can serve as (mostly) human-readable configurations for a given calibration run. By adding the plex and object-space as attachments to the MCAP rather than as channels proper, we have the advantage of being able to extract these entities easily without performing a search across chunks:

$ mcapget attachment --name optimized-plex results.mcap > optimized-plex.json

It is not always often that one needs to explicitly grab the optimized plex out of the results, since we usually advise users towards something like MetriCal’s shape-mode after calibration has succeeded. However, in many cases a plex or object JSON is conceptualized as a “distinct” file relative to e.g. the dataset or results. In this way, using attachments connotes a closer connection to this mental model.