Moving MetriCal Metrics to MCAPs

MCAPs aren't just for inputs anymore

Jeremy Steward
,
Senior 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:

  • MCAP has an open-specification. Likewise, a number of different client libraries are available for various languages which makes it easy for users to work with regardless of whether they use Rust or Python or otherwise.

  • MCAP being the default ROS2 bag format gives it some (empirical) credibility in being both extensible to different use-cases and not subject to itself breaking in backwards incompatible ways.

  • Regardless of the encoding picked, MCAP supports compression at the chunk level, which meant that even text-based message encodings benefit from compression.

  • MCAP channels typically contain full schema definitions of the data they carry, which means that even if a message schema were to change (either in a backwards compatible way or otherwise) the most up-to-date version of the schema can be loaded and verified against what your software is currently expecting.

  • Metadata, attachments, and messages inside of the format can be managed independently of the format itself.

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 reprecussions 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 it 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:

  • Serializing metrics in a text-based format (which are often floating point values) meant that we were writing very large text files (compared to a binary equivalent representation of the metrics)

  • Additionally, JSON does not properly support “floats” the same way that e.g. Rust does. Completely valid floats like NAN, INFINITY and such are not representable in the JSON standard

  • Schema conformance was routinely an issue. JSON schema is extant to the JSON format itself, and unlike other schema formats (Protobuf, Cap’n Proto, Flatbuffers, etc.) does not have mechanisms or syntax in place to enforce backwards compatible growth of the format

  • The use of serde meant that we could not reliably uncouple our internal definitions of the type hierarchy from the representation at rest. This couples directly with managing JSON schema, which rather than being thought out and hand-crafted was instead derived downstream from the serde-compatible type implementations

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, was MCAP. We knew the advantages clearly from our time working with the format for data ingress; those same advantagese 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:

  • The method through which MCAP encodes messages (either as binary or through some text-based well-known encoding) is configurable on a per-channel basis. This allowed us to avoid serializing a lot of floating-point numbers as text.

  • Closely related to the last point: that compression factor really made a difference. Having witnessed some very-large results JSON files in the past (≥300MiB!), any ability to compress these artefacts is welcome.

  • Foxglove (the company that produced MCAP) already provides a number of helpful message definitions that are widely accepted and used across the industry. While Tangram still had to define custom messages for our metrics types, it made it easier to not have to reinvent the wheel for e.g. Vector3 or Pose or Quaternion messages.

  • Our schemas are now self-documenting due to MCAP’s structure.

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 was providing a layer of indirection between our internal representation (in our Rust code) of the various metrics versus how this was serialized at rest. What’s more, MCAP made 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 roughly 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:

  • The results JSON contains the optimized plex JSON

  • The results JSON contains the optimized object-space JSON

  • The results JSON could be used to produce various different graphs or visualizations of metrics that Tangram may not itself provide (either via Rerun or our console output)

  • Internally, Tangram could use the results JSON as a debugging tool — the serialized form of this contained information on the version of MetriCal used when the file was generated

  • The results JSON could be combined with MetriCal’s report-mode to reproduce the calibration report / console output that was created during the calibrate-mode run

    • This in some ways subsumes all other uses, as report-mode will utilize all of the data in a results file (JSON or MCAP) if provided

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:

  • command: the name of the software (metrical)

  • version: the version of the software that produced the results MCAP

  • arguments: the arguments passed to the process when it was invoked

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

$ mcap get 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, attachments differ from messages on channels in that they:

  • Do not have an associated schema embedded into the MCAP

  • Have listed offsets in the MCAP’s summary section so that they can be quickly retrieved

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:

  • The input plex

  • The input object-space

  • The optimized plex (post-calibration)

  • The optimized object-space (post-calibration)

For example:

$ mcap list attachments results.mcap
name                  	media type      	log time           	creation time	content length	offset 	
input-plex            	application/json	1756998937638220277	0            	6148          	291    	
input-object-space    	application/json	1756998937638237581	0            	931           	6510   	
association           	application/json	1756998937661339340	0            	2038494       	4382014	
optimized-object-space	application/json	1756998937663787485	0            	28972         	6420580	
optimized-plex        	application/json	1756998937663867444	0            	16197         	6449635

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:

$ mcap get attachment --name

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 a “distinct” file relative to e.g. the dataset or results. In this way, using attachments connotes a closer connection to this mental model.

To note, however, that this step of extracting the plex is often totally unnecessary! MetriCal is also smart enough to pull the resultant plex or object-space from the results in context. For example, if you were to run metrical-shape:

$ metrical

then, despite the input expecting a Plex, MetriCal will happily interpret the provided MCAP and extract the correct result plex for you.

Tangram Newsletter

Subscribe to our newsletter and keep up with latest calibration insights and Tangram Vision news.

Tangram Newsletter

Subscribe to our newsletter and keep up with latest calibration insights and Tangram Vision news.

Tangram Newsletter

Subscribe to our newsletter and keep up with latest calibration insights and Tangram Vision news.