Releases: leiverkus/effigies
v0.7.2
Patch release focused on license and compliance metadata.
Changes:
- Relicense repository metadata and source headers to AGPL-3.0-or-later.
- Clarify THIRD_PARTY_LICENSES.md: Effigies' own source is AGPL-3.0-or-later; bundled/orchestrated components keep their own licenses.
- Add Docker image dependencies to the third-party license table, including Obj2Tiles, OpenPointClass, VCGlib, CGAL, Entwine, and LightGBM.
- Bump CITATION.cff to 0.7.2 with release date 2026-06-18.
v0.7.1
Added
- Citation metadata for Zenodo archival.
CITATION.cff(CFF 1.2.0) with author, keywords and upstream references (COLMAP, OpenMVS, NodeODM), plus.zenodo.jsonso Zenodo draws release metadata from the repository.
This is the first Zenodo-archived release; the concept DOI it mints will be added to CITATION.cff.
v0.7.0 — semantic field v0 + change-detection accuracy
The semantic-field bridge to Structura plus a hardening pass on change-detection accuracy. Mechanism only — Effigies ships the field machinery; the fine-class archaeological material model is a downstream Structura deliverable and is never baked into the MIT image.
Added
- Semantic orthophoto v0 (
helpers/semantic_ortho.py, opt-in--semantic). The
geometry-derived first increment of the semantic field (ROADMAP v0.7.0): it
rasterises the OpenPointClass point classes already written into the LAZ onto the
orthophoto grid — pixel-aligned withodm_dem/dsm.tif— taking the per-cell
majority class, folded into ground / vegetation / structure, as
odm_semantic/orthophoto_semantic.tif(Byte GeoTIFF + colour table) plus a legend
JSON. No trained model: it ships what the cloud classification already knows; the
fine archaeological material classes (stone/earth/paving/ceramic/mortar) are a
downstream 2D-model deliverable. Needs a classified cloud (enable--classify);
self-skips, non-fatally, otherwise. Runs after the orthophoto/DSM so the grid exists.
Unit-tested (majority + ASPRS→v0 mapping + GeoTIFF round-trip) and image-validated
end-to-end. This is the bridge's v0 to Structura's vectorisation. - Multi-epoch semantic propagation (
helpers/semantic_propagate.py). Carries the
semantic field across daily epochs (runs under--semanticwhen--align-togives a
reference epoch). Because change detection re-lands this epoch into the reference frame,
its semantic ortho is already co-registered with the reference epoch's; the step writes
(1) a carry-forward field —odm_semantic/orthophoto_semantic_propagated.tif, where
this epoch's unobserved cells inherit the reference epoch's class (temporally consistent;
honest "unobserved = unchanged" assumption) — and (2) a semantic-change raster —
odm_semantic/semantic_change.tifwith per-pixel class transitions + per-transition area
inodm_report/semantic_change.json(the class complement of the DoD/M3C2 geometric
change: e.g. structure→ground = a feature removed, vegetation→ground = clearing). The
reference is resampled nearest-neighbour (categorical). Self-skips without both semantic
orthos; non-fatal. Unit-tested (carry-forward + transition + stats) and end-to-end
validated.
Changed
- Change detection — M3C2 level-of-detection now includes the co-registration
residual (change_detect.py). The per-point LoD previously reflected local
roughness only; it now passes the post-ICP cloud-to-cloud residual to py4dgeo as
registration_error(Lague 2013), so a cm-level alignment error is folded into the
significance test instead of being treated as zero — small (few-cm) changes are no
longer over-reported as significant. Recorded asregistration_error_min
odm_report/change_detection.json. Unit-tested: a 5 cm residual lifts the LoD
median (≈ 0.3 cm → 10 cm). Stable-area-masked ICP remains v2 (see ROADMAP). - Change detection — DoD now thresholded at a minimum level-of-detection
(change_detect.py). The DEM-of-Difference changed area and fill/cut volumes now
count only cells whose |Δz| exceeds a minimum LoD (Wheaton 2010 — a robust noise
floor of the difference distribution, floored by the co-registration residual;
min_lod_from_dod), so sub-LoD noise is no longer booked as excavation / back-fill.
A raw un-thresholded net volume is kept as a cross-check, and the minLoD is recorded
asmin_lod_minodm_report/change_detection.json. Unit-tested. - Change detection — co-registration is now stable-area-masked (
change_detect.py).
ICP runs in two passes: a whole-cloud fit, then a re-fit on only the unchanged
ground (changed cells dropped viastable_mask), so a localised excavation change no
longer biases the rigid transform, and the residual over the stable area is a clean
registration-only error that now feeds the M3C2 LoD and the DoD minLoD
(coreg_reg_error) instead of the conservative full-cloud C2C. Reports
stable_fraction+registration_errorinchange_detection.json; degrades to the
whole-cloud fit (and records why) when too little stable ground remains. Unit-tested
(stable_maskseparation; the two-pass ICP smoke is pdal-gated). - Change detection —
--align-tonow re-lands the deliverables into the reference
frame by default (change_detect.py), ODM--alignparity. The recovered ICP
transform is applied in place to the delivered mesh OBJ (offset-aware,transform_obj)
and the LAZ (PDAL; EPT rebuilt); because re-landing runs before the raster stages, the
DSM/DTM, orthophoto, contours, glTF and 3D Tiles inherit the reference frame natively
(no re-warp). Previously the alignment was additive-only (deliverables untouched) —
pass--no-relandto keep that.report["relanded"]lists what moved; non-fatal per
asset. Camera assets (shots.geojson) are a known re-land gap (v2). Offset-exact
transform unit-tested; the full raster re-derivation is Docker-validated. - Change detection —
--align-tonow accepts a DEM GeoTIFF as the reference, not
only a point cloud (change_detect.py). A reference.tif(a prior DSM/DEM) is
read as cell-centre points for the ICP co-registration and M3C2, and used directly
(resampled onto the shared grid) as the reference DSM for the DEM-of-Difference —
so a prior epoch's DSM, or any external reference DEM, can drive the comparison.
is_dem/dem_to_xyz/resample_dem; unit-tested. - Change detection — camera assets are re-landed with the rest (
camera_exports.py).
When an--align-torun re-landed this epoch into the reference frame,shots.geojson
camera centres and orientations are now transformed by the recorded re-land transform
(read fromodm_report/change_detection.json, gated on therelandedmarker), so the
camera positions stay consistent with the re-landed mesh / cloud / orthophoto instead
of sitting in the old frame.cameras.jsonis unchanged (intrinsics are
frame-independent). The re-land gate is unit-tested; the pyproj WGS84 path is
Docker-validated. Closes the last v2 gap of the change-detection item. - Change detection — fix: M3C2 no longer misses deep changes (
change_detect.py).
py4dgeo's M3C2max_distance(the cylinder search depth along the normal) defaulted
to 0, which on the auto-scaled small cylinder of a dense cloud is too shallow — a deep
excavation (a change larger than the cylinder scale) had no matching surface in range
and came back NaN / not-significant, even where the DoD clearly measured it. It is now
set generously (max(30·cyl_radius, 3 m)). Found by the new end-to-end smoke
(scripts/smoke_change_detect.py), which the fix takes from M3C2 0 %→significant on a
0.4 m block.
Carried forward (post-v0.7.0)
- The full mesh-classify → z-buffer
--semanticpath (v0 approximates it via per-cell cloud majority). - The fine-class material model (stone / earth / ceramic / mortar) — Structura's deliverable, itself data-blocked.
- Cross-project contract finalisation.
Test gate: pure-Python unit suite green; the only scripts/test.sh failure is the pre-existing macOS bash-3.2 issue in test_autoscale.sh (passes on the bash-5 Docker image).
v0.6.0 — capability parity (buildable gaps vs ODM / Metashape / RealityCapture)
v0.6.0 — capability parity (buildable gaps vs ODM / Metashape / RealityCapture)
Closes the buildable capability gaps to the established tools: vector contour
lines (GPKG + DXF), OGC 3D Tiles export (Obj2Tiles), ML multi-class point
classification (OpenPointClass), multi-epoch change detection (ICP
co-registration + DEM-of-difference + M3C2), and orthomosaic radiometric
colour-balance finishing. Plus engine hardening: the dead OpenSfM sparse path
removed and InterfaceCOLMAP resolved through a fail-loud alias lookup, and the
Docker images slimmed to a multi-stage builder->runtime layout (CPU 3.24->1.65 GB,
runtime exercise gate; GPU mirrored in lock-step).
v0.5.0 — scaling to large image sets (split-merge tiling)
v0.5.0 — scaling to large image sets (split-merge tiling)
Split-merge spatial tiling for sets that exceed the single-machine RAM wall:
cameras partitioned into overlapping spatial tiles, each run through the full
dense chain independently, then the per-tile meshes/clouds merged back. Enabled
by a streaming, memory-flat multi-view blend (top-K view selection streamed in
one pass; view-major bake keeps at most one source image resident), so peak RSS
is governed by mesh/atlas size, not the image count. Auto-scaling of
matcher/mapper/densify for large sets and a dense worker-thread cap.
v0.3.0 — georeferencing accuracy
v0.3.0 — georeferencing accuracy
Multi-view GCP triangulation: marked pixels undistorted through the full COLMAP
lens model (SIMPLE_RADIAL/RADIAL/OPENCV/FULL_OPENCV/OPENCV_FISHEYE) into viewing
rays and intersected in least squares across all images a GCP is marked in, with
parallax + cheirality checks; single-view GCPs fall back to the nearest sparse
point. Solve-quality residuals (RMS 3D/horizontal/vertical, max, count) recorded
in georef_transform.json and surfaced in the log and the quality-report PDF.
Named CRS presets (Israeli TM, Palestine 1923, ETRS89 UTM 32N/33N = Germany's
official grid, OSGB, Swiss LV95) — presets, not defaults; an explicit CRS wins.
v0.2.0 — reproducible source-built engine (COLMAP 4.0.4 + OpenMVS 2.4.0)
Added
- First full end-to-end run on the CPU image. The complete chain — COLMAP
sparse →image_undistorter→ OpenMVSDensifyPointCloud→ReconstructMesh
→RefineMesh→TextureMesh— now runs to completion on the CPU/arm64 image
against a real 70-image dataset, producing a textured OBJ. This closes the main
open 0.2.0 validation item (the engine had never been run through on a dataset). - Both images unified on COLMAP 4.0.4 + OpenMVS v2.4.0. The CUDA/production
Dockerfileand the CPUDockerfile.cpunow build the identical engine from
the identical pinned sources and recipe — the only differences are the CUDA
base image and the three-D*CUDA*flags. No image builds COLMAP 3.x any more. - OpenMVS bumped to v2.4.0 (both images). v2.3.0's
DensifyPointCloud
corrupts the heap and aborts on arm64 (it falls back off SSE); v2.4.0 swaps the
FLANN nearest-neighbour code for nanoflann and runs the full dense+mesh chain
cleanly. 2.4.0 needs two libs newer than Ubuntu 22.04 ships — nanoflann ≥1.5
and CGAL ≥6.0 — both header-only, so both Dockerfiles vendor pinned releases
(NANOFLANN_VERSION,CGAL_VERSION) rather than bumping the base off 22.04
(which would lose PDAL, dropped from 24.04). Two small source patches keep it
building against jammy's OpenCV (disable the hard libjxl requirement; map the
one OpenCV-4.7-only JXL write constant to the JPEG one — we emit no JPEG-XL). matcher=vocab_treenow works. Image-retrieval matching for large sets:
each image bakes in a SHA256-pinned vocabulary tree in the format its COLMAP
expects (FAISS for COLMAP 4). Previously the option was selectable but always
aborted with the opaque "Cannot process dataset".
Fixed
- CPU pipeline could not start: opaque "Cannot process dataset". A chain of
failures, each masking the next, all surfacing only as NodeODM's generic error:run.shapplied the defaultuse-gpu=trueeven on the CUDA-less CPU image,
so COLMAP's SIFT aborted ("Cannot use Sift GPU without CUDA or OpenGL"). It
now probes for a usable GPU (nvidia-smi -L) and falls back to CPU with a
loud warning when none is present.- COLMAP's CPU SIFT extractor OOM-killed itself fanning out over all cores on
large images; the CPU SIFT/match thread count is now capped
(EFFIGIES_CPU_THREADS, default 4). - COLMAP's CPU FLANN matcher segfaulted holding a full match block in memory;
the exhaustive block size is capped on the CPU path
(EFFIGIES_CPU_MATCH_BLOCK, default 10). InterfaceCOLMAPwas fed the rawsparse/0model instead of an undistorted
workspace;sparse_colmap.shnow runscolmap image_undistorterand
InterfaceCOLMAPreads$WORK/densewith the correct--image-folder.dense_openmvs.shpassed--cuda-deviceunconditionally; the CPU OpenMVS
build rejects it. The flag is now probed and passed only on CUDA builds.
- Source-built, pinned Dockerfile. COLMAP (
4.0.4) and OpenMVS (v2.4.0)
are now compiled from upstream source with CUDA, replacing the distro packages;
Eigen/CGAL/Boost/OpenCV come from Ubuntu 22.04. Versions are declared as build
ARGs and a build-timewhichgate fails the build loudly ifcolmap,
DensifyPointCloud,ReconstructMesh,RefineMesh,TextureMesh,
InterfaceCOLMAPorpdalis missing. - Georeferenced point cloud output. New
helpers/pointcloud_to_laz.pyapplies
the georef similarity toscene_dense.plyand writes
odm_georeferenced_model.lazvia PDAL (full projected coordinates, LAS
scale/offset for precision), and optionally builds an EPT tileset
(entwine_pointcloud/) for the Potree viewer whenentwine/untwineis
present.map_outputs.pymaps the LAZ + EPT into the WebODM paths, with the raw
PLY kept as a documented fallback. - CPU test image (
Dockerfile.cpu) — same pinned engine built without CUDA,
for local integration testing on machines without an NVIDIA GPU (e.g. Apple
Silicon). Plusdocs/DEPLOYMENT.md(local CPU + GPU host recipes, WebODM node
wiring) and a.dockerignore. - Unit tests for the point-cloud transform matrix (
tests/test_pointcloud.py) and
the NodeODM options translation (tests/test_options.py); the local runner and
CI now execute everytests/test_*.py.
Fixed
- Options were incompatible with NodeODM.
options.jsonwas a flat list, but
NodeODM (libs/odmInfo.js) expects an argparse-style descriptor object keyed by
--flag; it was serving every option asname="0".."12"with the wrong types.
helpers/optionsToJson.pynow translates our list into NodeODM's schema (enum
choices,<class 'int'>/float, bool viadefault, validmetavardomains),
so WebODM builds the correct task UI. (Found by actually running the node.) - Options shim could not find
options.json. It resolved the path with
abspath(__file__), which does not follow the NodeODM symlink — it looked in
/opt/NodeODM. Now prefersODM_PATHand falls back torealpath. mesh-decimatedomain changed tofloat: 0 <= x <= 1so it passes NodeODM's
checkDomainvalidation on task submission.
Notes
- NodeODM hard-skips a
--gcpUI option (WebODM handles GCP upload natively), so
thegcpoption is intentionally not shown;run.shstill auto-detects
gcp_list.txt. The node is verified to build, run, serve all options and be
reachable on the WebODM network; a full processing run on a dataset is the
remaining 0.2.0 validation.
Planned
See ROADMAP.md. Still open for 0.2.0: a full end-to-end processing
run on a real dataset, a verified VCGlib commit pin, and confirming the
InterfaceCOLMAP/InterfaceOpenSfM binary names across OpenMVS builds. Beyond
that: multi-view GCP triangulation (0.3.0).
v0.1.0 — first public release
First public release of Effigies — a NodeODM-compatible alternative engine for WebODM.
It runs the full OpenMVS chain (Densify → ReconstructMesh → RefineMesh → TextureMesh) on a COLMAP sparse reconstruction, recovering the mesh-refinement steps that stock ODM skips, and bridges the result into the WebODM asset contract.
This is an alpha / working scaffold — see the README status section and ROADMAP.md for what still needs hardening (source-built Docker image with binary verification, .laz/EPT point clouds, multi-view GCP triangulation).
Full notes: see CHANGELOG.md.