Forum

Recent Posts

Pages: [1] 2 3 ... 10
1
General / Re: Change Image *File* names after alignment?
« Last post by James on July 13, 2026, 11:59:29 AM »
Not a solution, but maybe a temporary workaround/tip, you can sort the photos pane by 'Date & Time' column, which would be better than by 'Label' in this case.

You can also create image groups which appear like folders for images in the workspace pane, which also helps keep things organised.
2
General / The section construction tool no longer works [MS v2.3.1]
« Last post by Gstudio on July 11, 2026, 10:05:52 AM »
I don't understand why, but I can't build cross-sections anymore, and I think it's a bug in version 2.3.1.

I have a terrain survey from a drone and need to extract some cross-sections.

The profile construction function works, but since it only follows the highest points of the model or cloud. I need to generate some cross-sections that also extract the points under the trees (in the cross-section, I want to see both the tree and the ground below).

In previous versions of Metashape I drew the polyline, then went to Measurements (right click on polyline) and, next to the Coordinates tab, I could access the Profile tool, from which I could set the section thickness and offset. Now it's simply disabled, whether I'm working on a 3D model or the Dense Cloud.

Has something changed, or is it actually a bug that needs to be fixed?

I suggest not calling this function Profile but "Cross-Section" (or not use the same word Profile for two different functions), it is different from the "Measure Profile" function I described before.
3
Feature Requests / Re: Agisoft Metashape Icon on Mac
« Last post by ser1993 on July 09, 2026, 09:10:29 PM »
Hi,

I have the latest version of both.
From a little bit of investigation done by myself, it seems is the .icns inside the application which has a transparent padding. I think that’s the image that get rendered.

Thank you,
Matteo
4
Feature Requests / manual XYZ editing of shape vertices
« Last post by Lcici on July 09, 2026, 07:14:02 PM »
Hello Agisoft team,

I would like to suggest a small improvement for future Metashape versions: the ability to manually edit the X, Y and Z coordinates of shape vertices directly inside Metashape.

This would be very useful for volume measurements, especially in complex cases where the contour points need to be adjusted manually to better define the reference surface.

At the moment, my workaround is to export the shape, edit the vertices in QGIS, and then re-import it into Metashape. It works, but it adds unnecessary steps. Does anybody have another workaround ?

In Pix4Dmatic, it is possible to manually adjust the coordinates of contour points, including the Z value. Having a similar option in Metashape would make volume workflows much more efficient.

A simple editable table for the selected shape vertices, with X/Y/Z fields, would already be very helpful.

Best regards,
5
Feature Requests / Re: Agisoft Metashape Icon on Mac
« Last post by Alexey Pasumansky on July 09, 2026, 05:47:14 PM »
Hello Matteo,

What Metashape version (including the build number) you are using and what is macOS version?
6
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.
7
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)
  • Not reproducible on Metashape 2.2.3 (build 21752)
Summary

After assigning a loaded Metashape.Mask to camera.mask, the chunk-level mask collection accessors return empty placeholders instead of the mask:

Code: [Select]
Metashape 2.2.3:  chunk.mask_sets == [{<Camera>: <Mask>}]   chunk.masks == {<Camera>: <Mask>}
Metashape 2.3.1:  chunk.mask_sets == [None]                 chunk.masks == None

camera.mask itself is non-None and camera.mask.image() is valid on both versions — only the chunk-level mask_sets / masks accessors regressed. This breaks any Python code that enumerates masks through chunk.mask_sets / chunk.masks (the multi-mask-set API added in 2.2.0).

Minimal reproduction

Code: [Select]
import tempfile
from pathlib import Path

import Metashape
from PIL import Image

W = H = 64
with tempfile.TemporaryDirectory() as td:
    tmp = Path(td)
    img = tmp / "img0.jpg"
    Image.new("RGB", (W, H)).save(img)               # a blank photo
    doc = Metashape.Document()
    chunk = doc.addChunk()
    chunk.addPhotos([str(img)])

    mpath = tmp / "mask0.png"
    Image.new("L", (W, H), 255).save(mpath)           # a white (all-visible) mask
    mask = Metashape.Mask()
    mask.load(str(mpath))
    chunk.cameras[0].mask = mask                       # assign per-camera mask

    print("Metashape", Metashape.version)
    print("camera.mask set   :", chunk.cameras[0].mask is not None)  # True on both
    print("chunk.mask_sets   :", chunk.mask_sets)      # 2.2: [{cam: mask}]   2.3: [None]
    print("chunk.masks       :", chunk.masks)          # 2.2: {cam: mask}     2.3: None

Output:

Code: [Select]
# Metashape 2.2.3.21752
camera.mask set   : True
chunk.mask_sets   : [{<Camera 'img0'>: <Metashape.Metashape.Mask object at ...>}]
chunk.masks       : {<Camera 'img0'>: <Metashape.Metashape.Mask object at ...>}

# Metashape 2.3.1.22580
camera.mask set   : True
chunk.mask_sets   : [None]
chunk.masks       : None

Expected vs. actual
  • Expected (as in 2.2.3): assigning camera.mask materializes / updates the active Masks set, so chunk.mask_sets[0] is a Masks object mapping the camera to its mask and chunk.masks is that same active set.
  • Actual (2.3.1): chunk.mask_sets stays [None] (a phantom slot holding no object) and chunk.masks stays None, even though the mask is present and valid on camera.mask.
Scope: the underlying data model and native save are correct

This is a Python read-accessor regression, not data loss:
  • The native doc.save() serializes the mask correctly on 2.3.1 — the saved .psz doc.xml is byte-for-byte equivalent to 2.2.3's, containing <mask camera_id="0" path="mask0.png"/> and the mask0.png payload.
  • Cross-version proof: a project saved by 2.3.1 (with camera.mask set) reopened in 2.2.3 exposes fully-populated chunk.mask_sets / chunk.masks; the same archive reopened in 2.3.1 shows [None] / None. Same bytes, two API versions, opposite reads — the defect is confined to the 2.3.x mask_sets / masks getters (and to the implicit build-on-assign), while the stored data and serialization are intact.
Consequently, code that consumes masks via chunk.mask_sets / chunk.masks (rather than camera.mask) silently sees zero masks on 2.3.1.

Also observed (likely the same root cause)
  • Assigning a plain dict, chunk.masks = {camera: mask}, raises TypeError('expected Masks object') on 2.2.3 but is silently accepted (no error, no effect) on 2.3.1.
  • chunk.masks = chunk.mask_sets[0] assigns None on 2.3.1 (since the slot is None), so the "activate a mask set" idiom is a no-op.
  • Metashape.Masks() cannot be constructed directly on either version (TypeError("cannot create 'Masks' instances")), so there is no public way to build/attach a mask set from Python.
Workaround

Read per-camera masks directly from camera.mask by iterating chunk.cameras, instead of relying on chunk.mask_sets / chunk.masks:

Code: [Select]
masks = {cam: cam.mask for cam in chunk.cameras if cam.mask is not None}

This works on both 2.2.3 and 2.3.1. It is preferable to gate on Metashape.version: fall through to chunk.mask_sets when it is populated and reconstruct from camera.mask only when it is empty/all-None, so the code auto-recovers if the accessor is fixed.

Suggested fix

Restore the 2.2 behavior where assigning camera.mask materializes/updates the active Masks set, and where chunk.mask_sets / chunk.masks reflect the per-camera masks that doc.save() already serializes and that reopen correctly in 2.2.
8
Environment
  • Metashape Professional 2.3.1 (build 22580), standalone Python module
  • Linux x86_64, wheel metashape-2.3.1-cp39.cp310.cp311.cp312.cp313-abi3-linux_x86_64.whl
  • CPython 3.11 (see "CPython version dependency" below — 3.12+ masks it)

Summary
Comparing None (or any non-Metashape object) with a Metashape object using == / != — for example None == chunk.cameras[0].group — returns the correct boolean but leaks a decref on the NotImplemented singleton. Each such comparison lowers NotImplemented's reference count by one; once it reaches zero the interpreter aborts:
Code: [Select]
Fatal Python error: notimplemented_dealloc: deallocating NotImplemented(SIGABRT, process exit 134.)

Minimal reproduction
Run in a Metashape 2.3.1 environment on CPython < 3.12 (e.g. 3.11) — on CPython >= 3.12 the crash is masked (see "CPython version dependency" below):
Code: [Select]
import sys
import Metashape

g = Metashape.Document().addChunk().addCameraGroup()
print("Metashape", Metashape.version, "| Python", sys.version.split()[0])
print("NotImplemented refcount:", sys.getrefcount(NotImplemented))
for i in range(1, 21):
    _ = None == g  # returns False, but leaks one reference on NotImplemented
    print(f"after {i}: NotImplemented refcount = {sys.getrefcount(NotImplemented)}")
On CPython 3.11 the printed refcount decreases by one per iteration and the process aborts with notimplemented_dealloc when it reaches zero (typically within ~5 iterations). The comparison result itself is correct (False) — the crash comes purely from the refcount corruption.

Example output (3.11):
Code: [Select]
Metashape 2.3.1.22580 | Python 3.11.15
NotImplemented refcount: 5
after 1: NotImplemented refcount = 4
after 2: NotImplemented refcount = 3
after 3: NotImplemented refcount = 2
Fatal Python error: notimplemented_dealloc: deallocating NotImplemented

Expected vs. actual
  • Expected: None == <Metashape object> returns False and leaves NotImplemented's refcount unchanged (the rich-comparison protocol borrows the reference to NotImplemented; it must not be stolen).
  • Actual: the refcount is decremented on every such comparison, eventually deallocating the singleton and aborting the interpreter.

Root cause (analysis)
None == g evaluates None.__eq__(g), which returns NotImplemented; CPython then tries the reflected g.__eq__(None). The Metashape C-extension's reflected comparison appears to over-decref the NotImplemented it handles (treating a borrowed reference as owned). It reproduces with any Metashape object type compared against None (tested with CameraGroup; the same pattern applies to markers, sensors, components, cameras, etc.). The module also over-decrefs None itself in some comparison paths (same failure mode as none_dealloc).

CPython version dependency
  • CPython < 3.12NotImplemented is an ordinary singleton with a small refcount, so the leak reaches zero and the interpreter aborts. A single comparison only makes it off-by-one, so the abort is deferred and can surface at a much later, unrelated point in the program.
  • CPython >= 3.12 (immortal objects, PEP 683) — NotImplemented is immortal (sys.getrefcount(NotImplemented) reports 4294967295), so incref/decref are no-ops and the leak is masked (no crash). The underlying refcount bug is still present, just harmless.

Impact
On CPython < 3.12, any code that compares a Metashape object with == / != where an operand is None will intermittently abort. This is easy to hit unintentionally — e.g. iterating cameras and testing camera.group == some_group while some cameras are ungrouped (camera.group is None), which produces None == some_group.

Workarounds

On CPython 3.11 (where the crash occurs)
  • Avoid == / != on Metashape objects in your own code. Use is / is not for None checks and compare entities by their integer .key:
    # instead of:  if camera.group == group:
    if camera.group is not None and camera.group.key == group.key:

    Removes the leak triggers in code you control (not any comparison inside the module itself).
  • Re-add the dropped reference via ctypes right after a known-leaking comparison (brittle; only if you can pinpoint every call):
    import ctypes
    _ = None == some_metashape_obj
    ctypes.pythonapi.Py_IncRef(ctypes.py_object(NotImplemented))
  • Immortalize the singletons at startup (last resort) — mimic CPython >= 3.12 by inflating the refcounts once so the leak can never reach zero:
    import ctypes
    for obj in (None, NotImplemented):
        ctypes.c_ssize_t.from_address(id(obj)).value += 1 << 30

    Effective but pokes at CPython internals and only masks the bug — use with care.

Recommended: run on CPython >= 3.12
On CPython >= 3.12, None and NotImplemented are immortal (PEP 683), so the leaked decrefs are no-ops and no code changes are needed.

The real fix is for the Metashape C-extension to stop stealing the borrowed NotImplemented (and None) reference in its comparison paths.
9
Feature Requests / Re: Agisoft Metashape Icon on Mac
« Last post by ser1993 on July 08, 2026, 07:02:29 AM »
Hello,

Icons are broken again. They looks alright when the software is launched but they are small and inside a gray box when not used and just sit in the dock. Could you please fix it?

Cheers,
Matteo
10
Thank you.
It has been successfully resolved.
You were a great help.
Pages: [1] 2 3 ... 10