Femto Bolt: depth-to-color extrinsic rotation is not a rotation matrix (3 units, firmware 1.1.3)

Summary

On three Femto Bolt cameras, the depth-to-color rotation returned by the device is not orthonormal. The first element is close to the cosine of the tilt between the two cameras, where a rotation matrix needs a value close to 1. The matrix therefore scales one axis by about 0.5%.

I get the same values through two independent host-side paths, so I believe the value comes from the device and is not computed on the host. I could not determine whether it was written at factory calibration or is generated by the firmware.

Devices

Model Femto Bolt
Firmware 1.1.3 (all three units, read from the devices)
Hardware 1.1
Serial numbers CL86541038L, CL8F25300JZ, CL86541006P
pyorbbecsdk2 2.1.3 (bundles OrbbecSDK 2.7.6)
OS Windows 11 x64

What I see

Unit R[0][0] reported R[0][0] required Norm of row 0 Norm of rows 1, 2
CL86541038L 0.9947249 0.999968 0.994757 1.000000
CL8F25300JZ 0.9946091 0.999974 0.994635 1.000000
CL86541006P 0.9942945 0.999987 0.994308 1.000000

“Required” is sqrt(1 - R[0][1]^2 - R[0][2]^2). The determinant equals the norm of row 0. The other eight elements are consistent with a proper rotation: a tilt of about 6 degrees between the depth and color cameras.

Full matrix for CL86541038L, row-major, as stored in the color camera’s Rt.Rotation:

 0.9947248697   0.0080308039   0.0005517029
-0.0080450317   0.9946922660   0.1025793031
 0.0002750196  -0.1025804207   0.9947246313

In all three units R[0][0] is nearly equal to R[2][2] (they differ in the seventh digit).

Where the value comes from

Both paths return identical values:

  1. Calibration JSON stored on the device, read with k4a_device_get_raw_calibration (OrbbecSDK-K4A-Wrapper): color camera, "Rt" → "Rotation".
  2. OrbbecSDK through pyorbbecsdk2, pipeline.get_camera_param().transform.rot. In the current OrbbecSDK_v2 source (2.10.6) this value is copied without computation from the OB_RAW_DATA_ALIGN_CALIB_PARAM block read from the device (TOFDeviceCommonAlgParamManager::fetchParamFromDevice, AlgParseHelper::alignCalibParamParse).

How to reproduce

import numpy as np
import pyorbbecsdk as ob

pipeline = ob.Pipeline()
config = ob.Config()
config.enable_stream(pipeline.get_stream_profile_list(ob.OBSensorType.COLOR_SENSOR).get_video_stream_profile(1920, 1080, ob.OBFormat.BGRA, 15))
config.enable_stream(pipeline.get_stream_profile_list(ob.OBSensorType.DEPTH_SENSOR).get_video_stream_profile(1024, 1024, ob.OBFormat.Y16, 15))
pipeline.start(config)
rotation = np.asarray(pipeline.get_camera_param().transform.rot, dtype=np.float64).reshape(3, 3)
pipeline.stop()

print(rotation)
print("row norms:", np.linalg.norm(rotation, axis=1))
print("max |R R^T - I|:", np.abs(rotation @ rotation.T - np.eye(3)).max())

Output on CL86541038L: row norms [0.994757 1. 1.], max |R R^T - I| = 0.0105. A rotation matrix gives row norms of 1 and an error near 1e-7.

Impact

Any code that treats this matrix as rigid is affected, for example by inverting it with a transpose. In my multi-camera calibration (ChArUco boards, 1920x1080 color), replacing the matrix with the nearest rotation (SVD) reduced the board reprojection error from 1.3-4.7 px to 0.25-0.42 px.

Questions

  1. Is this a known issue for Femto Bolt?
  2. Is the fix in the firmware, in the factory calibration data, or should the SDK correct the matrix when reading it?
  3. Do other firmware versions return a correct matrix?

Attachments

Raw calibration JSON of the three units, as returned by k4a_device_get_raw_calibration:

CL8F25300JZ_raw_calibration.txt (4.0 KB)
CL86541006P_raw_calibration.txt (4.0 KB)
CL86541038L_raw_calibration.txt (4 KB)

GitHub issue

Update: root cause identified in firmware 1.1.3

I investigated the Femto Bolt v1.1.3 firmware package and found what appears to be the exact source of the invalid rotation matrices.

1. Location of the bug

After extracting customer.ubifs , I disassembled the following ARM32 executable:

mi_demo/oem/usr/bin/observice

The binary contains a function with the symbol:

camera_read_param_from_bin_file (address 0x1D130 )

This function reads calibration parameters and constructs the 3×3 rotation matrix from three angles using sinf and cosf .

2. Incorrect rotation formula

The instructions around 0x1DB38–0x1DB82 calculate the first rotation-matrix element, R[0][0] .

Defining the three angles as A , B , and C , the firmware implements:

R[0][0] = cos(A) * cos(B) - sin(A) * sin(B) * sin(C);

However, the formula consistent with the other eight matrix elements is:

R[0][0] = cos(C) * cos(B) - sin(A) * sin(B) * sin(C);

The first cosine uses the wrong angle: cos(A) instead of cos(C) .

The relevant ARM instructions are:

0x1DB38: vldr     s14, [r3]      ; cos(A)
0x1DB44: vldr     s15, [r3]      ; cos(B)
0x1DB48: vmul.f32 s14, s14, s15
...
0x1DB78: vsub.f32 s15, s14, s15
0x1DB82: vstr     s15, [r3]      ; R[0][0]

The cos(A) value is loaded from the local stack offset 0x1C , whereas cos(C) is available at offset 0x14 .

3. Verification against the three devices

This error explains the characteristic pattern observed in all three calibration files:

Unit Reported R[0][0] Expected R[0][0]
CL86541038L 0.99472487 0.99996760
CL8F25300JZ 0.99460912 0.99997407
CL86541006P 0.99429452 0.99998695

In particular, the erroneous expression explains why R[0][0] is almost identical to R[2][2] in every device.

The remaining eight matrix elements are consistent with the corrected formula. Replacing only R[0][0] restores orthonormality to approximately float32 precision.

4. Conclusion and requested fix

The firmware binary contains a specific mathematical error that reproduces the observed calibration defect. This strongly indicates that the problem originates in the firmware’s conversion of calibration parameters into a rotation matrix, rather than in the host SDK or the individual factory measurements.

Could Orbbec confirm this finding and correct the expression in camera_read_param_from_bin_file ?

It would also be useful to establish whether already-stored calibration JSON or alignment parameters need to be regenerated after the firmware correction.

For reproducibility, the SHA-256 of the inspected observice executable is:

594cfec2eb66ec094f02c86cbfe8bc699a48d3fcaec846f12da82160ad7a3a82