Symlink filename — not target — controls the camera label¶
- Status: unverified
- Applies to: Metashape Pro 2.x and Standard 2.x — and unchanged from PhotoScan 1.x
- Edition: Standard
- Diátaxis: how-to
- Confidence: medium
- Last reviewed: 2026-05-22
Confidence: medium. The behaviour is API-introspectable in principle but requires file-system testing to confirm fully. Tier 3 reproduction recommended on multiple platforms (macOS, Linux, Windows) before promoting to observed — Windows symlink semantics differ enough that the behaviour may not be identical there.
A non-obvious but useful behaviour of Chunk.addPhotos:
Camera.photo.pathis the resolved canonical path — symlinks have been followed.Camera.labelis the basename of the path you passed in — derived from the symlink's filename, not from the target's filename.
In other words, the symlink's basename determines the camera's
label and its sort position in chunk.cameras (which is
alphabetically ordered by label), independent of where the
actual image file lives on disk.
The use case¶
Force a specific camera to sort first in the GUI, or group cameras under a coherent naming scheme, without renaming source data. Three operational scenarios:
Scenario 1 — Anchor camera in a multi-camera layout¶
Choosing the master sensor in a multi-camera layout
documents that the master sensor is determined by alphabetical
filename ordering at import. If the desired master is not
alphabetically-first, you can rename the folder containing it
to 1 — but only if you can rename source data. If the source
images are read-only, on a network share, or in a structure you
shouldn't modify, build symlinks under your project's working
directory with whatever filename prefix you need:
import os
from pathlib import Path
source = Path("/data/shared/aerial_2025/IMG_0042.jpg")
sym = Path("/path/to/working_dir/0_anchor.jpg")
sym.symlink_to(source.resolve()) # use absolute target
chunk.addPhotos([str(sym)])
camera = chunk.cameras[-1]
assert camera.label == "0_anchor"
assert camera.photo.path == str(source.resolve())
The symlink basename 0_anchor becomes the label; the source
image stays where it is on disk; the camera sorts first because
0_anchor precedes anything starting with a higher digit or
letter.
Scenario 2 — Unify naming across disparate source locations¶
A project assembling images from multiple capture sessions, drives,
or shared mounts can build a single working-directory tree of
symlinks with a unified naming scheme. The labels in
chunk.cameras then follow the working-directory scheme, not the
source-disk paths.
session_a = Path("/data/2024_q4/session_a")
session_b = Path("/data/2025_q1/session_b")
working = Path("/path/to/project_workdir/images")
working.mkdir(parents=True, exist_ok=True)
for i, src in enumerate(sorted(session_a.glob("*.jpg"))):
(working / f"a_{i:04d}.jpg").symlink_to(src.resolve())
for i, src in enumerate(sorted(session_b.glob("*.jpg"))):
(working / f"b_{i:04d}.jpg").symlink_to(src.resolve())
chunk.addPhotos([str(p) for p in sorted(working.glob("*.jpg"))])
# Cameras sort: a_0000, a_0001, …, b_0000, b_0001, …
# Their photo.path values still reference the original data
Re-running the project doesn't require copying source data; the working-directory symlinks are all that needs to be rebuilt.
Scenario 3 — Test ordering hypotheses without disturbing source¶
When debugging multi-camera-layout behaviour or other ordering-dependent issues, build a throwaway working directory of symlinks with controlled filenames; iterate quickly without touching source data.
What is verifiable, what isn't¶
The basic API surface is documented:
Camera.label(str, "Camera label.") — confirmed by introspection on Metashape 2.2.2.Camera.photo(Photo) — haspathattribute (canonical string path).Chunk.addPhotos(filenames=…, …)— accepts a list of paths.
What's not documented is the derivation rule for
Camera.label (where it comes from when the path is a symlink).
The article documents the observed behaviour: symlink basename →
label; symlink target → photo.path.
Caveats¶
- Tested on macOS and Linux. Windows symlink semantics differ
(symlinks require admin permission on most installations,
shortcut
.lnkfiles are entirely different from POSIX symlinks). Behaviour on Windows is unverified by this article. - Use absolute paths in
symlink_to. Relative-target symlinks become brittle if the working directory moves; usesource.resolve()when constructing the symlink to capture an absolute target. strip_extensions=True(the default foraddPhotos) trims the file extension from the label. SoaddPhotos(["/path/0_anchor.jpg"])produces a camera withlabel="0_anchor", not"0_anchor.jpg". The article's examples reflect this.- The
Camera.labelis editable. If post-import renaming is appropriate, setcamera.label = "new_name"directly. The symlink trick is for projects where the label needs to be controlled at import time (e.g., for the multi-camera master selection that happens at import). - Source data must remain accessible at the symlink target's
resolved path. Moving or renaming the source breaks all
symlinks; the working-directory symlinks then point to
non-existent files and
addPhotosfails. Use this technique only when source data has stable absolute paths.
Runnable demonstration¶
The demo below is a one-line API check that any installation can run; no sample dataset required as long as you have any image file.
Demo verified: ✗ — pending Tier 3 reproduction on multiple platforms (macOS, Linux, Windows) to confirm the symlink basename → label rule. The demo's API surface is verified; the file-system behaviour requires platform-specific testing.
"""Confirm: Camera.label derives from symlink basename;
Camera.photo.path is the resolved target."""
import os
from pathlib import Path
import Metashape
# Pick any image file you have access to.
SOURCE = Path("/path/to/any_image.jpg")
WORKDIR = Path("/tmp/symlink_test")
WORKDIR.mkdir(exist_ok=True)
sym = WORKDIR / "0_force_first.jpg"
if sym.exists() or sym.is_symlink():
sym.unlink()
sym.symlink_to(SOURCE.resolve())
doc = Metashape.Document()
chunk = doc.addChunk()
chunk.addPhotos([str(sym)])
cam = chunk.cameras[0]
print(f"Camera.label = {cam.label!r}")
print(f"Camera.photo.path = {cam.photo.path!r}")
print(f"Source was = {SOURCE.resolve()!r}")
# Cleanup.
sym.unlink()
WORKDIR.rmdir()
Expected output: Camera.label = '0_force_first' (symlink
basename, extension stripped) and Camera.photo.path =
'<absolute path of original image>' (resolved). If the label
matches the source basename instead, the version of Metashape on
this machine is doing path-resolution differently than tested,
and the article needs revision.
References¶
- Metashape Python Reference (2.3.1),
Camera.label,Camera.photo,Chunk.addPhotos— documents the API surface but not the symlink-basename-vs-target derivation rule. - Choosing the master sensor in a multi-camera layout — the operational scenario this technique supports.