Forum

Author Topic: 2.3.1 Python API: rig slave-sensor reference.rotation convention inverted  (Read 1446 times)

Frédéric Devernay

  • Newbie
  • *
  • Posts: 6
    • View Profile
2.3.1 Python API: rig slave-sensor reference.rotation convention inverted (transposed) vs 2.2

Environment
  • Metashape Professional 2.3.1 (build 22580), standalone Python module
  • macOS (also reproduced on Linux); wheel metashape-2.3.1-*-abi3
  • Reproduced on CPython 3.11, 3.12 and 3.13 (not Python-version dependent)
  • Compared against Metashape 2.2.3 (build 21752)
Summary

A camera rig's slave sensor stores its orientation relative to the master in sensor.reference.rotation, as fixed OPK (omega/phi/kappa) Euler angles. Between 2.2 and 2.3 the direction of that rotation was inverted:
  • 2.2.3: sensor.reference.rotation is the OPK of the master->slave rotation R — you set it with mat2opk(R).
  • 2.3.1: the same field is interpreted as the OPK of the inverse rotation R^T — you must set mat2opk(R.transpose()) to express the same physical slave offset.
So code that sets a slave rotation reference the 2.2 way places the slave in the inverse orientation on 2.3: you intend R but 2.3 applies R^T, so the slave is wrong by R^2about twice the offset angle (this is exactly why the 20 deg test offset below drifts ~40 deg). The error is silent. The angle-conversion utilities (Metashape.Utils.mat2opk, opk2mat, ...) are unchanged; only how optimizeCameras interprets the stored slave reference flipped.

How this was measured. optimizeCameras is the only operation that consumes a slave sensor's rotation reference, so the test is behavioral: build a rig with a known slave offset, solve it to get the rotation matrix solved, then set the reference to an OPK encoding of a candidate matrix (with tight accuracy) and re-optimize. If the encoding matches Metashape's interpretation the prior agrees with the solution and the slave does not move (0 deg); otherwise the high-accuracy prior drags it away from solved. Feeding back solved itself vs. its transpose isolates the direction:

Code: [Select]
                       reference set to mat2opk(solved)   reference set to mat2opk(solved^T)
Metashape 2.2.3:            0.000 deg (slave stays put)       39.998 deg (dragged away)
Metashape 2.3.1:           39.998 deg (dragged away)           0.000 deg (slave stays put)

=> 2.2 wants mat2opk(solved); 2.3 wants mat2opk(solved^T). (chunk.euler_angles is YPR here, yet the reference is still read as OPK on both versions — it is not being reinterpreted as YPR; only the rotation direction flipped.)

Minimal reproduction

Self-contained (Metashape + Pillow); builds a 10-station x 2-face master/slave rig with blank tiles (pixels are never read; camera poses and marker observations are set analytically). Core:

Code: [Select]
import Metashape
# ... build_rig(): 2 sensors, sensor[0].makeMaster(); slave = sensor[1];
#     known offset R_TRUE = opk2mat([0, 20, 0]); exact marker projections ...
ch.euler_angles = Metashape.EulerAnglesYPR
ch.optimizeCameras(fit_f=False, fit_cx=False, fit_cy=False, adaptive_fitting=False)
solved = slave.rotation                      # the solved slave-offset rotation

def move_after(angles):                      # geodesic drift of slave from `solved`
    slave.rotation = solved
    slave.reference.rotation = angles
    slave.reference.rotation_accuracy = Metashape.Vector([1e-4, 1e-4, 1e-4])
    slave.reference.rotation_enabled = slave.reference.enabled = True
    slave.fixed_rotation = False
    ch.optimizeCameras(fit_f=False, fit_cx=False, fit_cy=False, adaptive_fitting=False)
    return geodesic_angle(slave.rotation, solved)

print(Metashape.version)
print("mat2opk(solved)    :", move_after(Metashape.Utils.mat2opk(solved)))
print("mat2opk(solved.t()):", move_after(Metashape.Utils.mat2opk(solved.t())))

Output:

Code: [Select]
# Metashape 2.2.3.21752
mat2opk(solved)    : 0.000 deg
mat2opk(solved.t()): 39.998 deg

# Metashape 2.3.1.22580
mat2opk(solved)    : 39.998 deg
mat2opk(solved.t()): 0.000 deg

Sweeping all four Euler encodings of solved on 2.3.1 confirms no non-transposed standard encoding round-trips (all should be ~0 deg if the 2.2 convention held):

Code: [Select]
mat2opk(solved) -> 39.998    mat2ypr(solved) -> 28.211    mat2pok(solved) -> 28.210    mat2ank(solved) -> 123.944

The encoder did not change — only the interpretation

Metashape.Utils.mat2opk / mat2ypr return identical values on 2.2.3 and 2.3.1 for the same matrix (verified numerically), and Metashape.Utils.opk2mat is unchanged. The regression is purely in how optimizeCameras interprets a slave sensor's reference.rotation (it now expects the transposed rotation), not in the angle-conversion utilities.

Expected vs. actual
  • Expected (as in 2.2.3, and matching the GUI/manual, which show Omega/Phi/Kappa for rig slave offsets): sensor.reference.rotation = mat2opk(R) sets the slave-offset rotation to R; feeding back mat2opk(solved) leaves the solved slave untouched.
  • Actual (2.3.1): feeding back mat2opk(solved) rotates the slave ~40 deg away from its own solution; only mat2opk(solved.transpose()) is self-consistent.
Why this reads as a regression, not a documented change
  • It is undocumented: neither the GUI changelog nor the Python API Change Log for 2.3.0 / 2.3.1 mentions a change to the rig slave-sensor rotation-reference convention. (The 2.3 reference-CSV Euler additions — rotation_angles/load_rotation on Chunk.importReference/exportReference — concern camera/marker reference CSVs, not the rig slave-sensor offset.)
  • The community/GUI convention for slave offsets is OPK of the master->slave rotation (e.g. forum topic 15021), i.e. the 2.2 behavior.
  • Any code that sets a rig slave sensor's rotation reference assuming the 2.2 convention (from a stored calibration or a hardcoded offset) is silently mis-oriented by R^2 on 2.3.1 — the slave lands at R^T instead of the intended R, i.e. off by about twice the offset angle.
Impact

Code paths that set a rig slave sensor's reference.rotation directly (cubemap / multiplane rig calibration importers) produce a mis-oriented rig on 2.3.1 while reporting success, unless they transpose the rotation before OPK-encoding.

Workaround (version-gated)

Because the encoder is unchanged and only the interpreted direction flipped, a deterministic fix is to transpose the rotation before encoding on >= 2.3.0:

Code: [Select]
R_for_ref = solved if Metashape.version < "2.3.0" else solved.t()
slave.reference.rotation = Metashape.Utils.mat2opk(R_for_ref)

This must be gated on Metashape.version: at the point of setting the reference there is no cheap local signal to distinguish the conventions without a solve-and-check probe, so the 2.3.0 boundary is the pragmatic gate. Verify the result against held-out check points, since a wrong convention still "succeeds".

Suggested fix

Restore the 2.2 slave-offset rotation-reference convention (OPK of the master->slave rotation, non-transposed), or document the change explicitly in the Python API Change Log with the exact frame/direction so callers can adapt deterministically.