A verified diagnosis and a compiled, tested fix for a silently wrong physical
voxel size in Vesuvius Challenge villa's
renderer, vc_render_tifxyz.
Status: the fix is written, compiled, run, tested, reviewed upstream, and merged into ScrollPrize/villa
mainvia PR #1831 on 30 September 2026 (merge commitd08fa94d56c5e197f23408d1a39c78419146f311). The September 2026 Progress Prize submission has been sent; the prize result is pending. The licensing arrangement documented inNOTICE.mdwas described to the organisers and publicly confirmed on Discord as not problematic for the prize.
Reviewers, start here:
| If you want | Read |
|---|---|
| The problem in 30 seconds | The problem, below |
| Proof that it is fixed | DOCS/CI_VALIDATION.md — the run record |
| The before/after figure | DOCS/evidence/before-after.png |
| The same result as terminal output | DOCS/evidence/terminal-before-after.png — the run's log lines, drawn verbatim, and labelled as CI output rather than an interactive session |
| What the evidence figures are, and how they were built | DOCS/evidence/README.md |
| What is not verified | DOCS/RESULTS.md §7 — kept deliberately, and mostly closed |
| The reading order for everything | DOCS/INDEX.md |
| The PR text and review follow-up notes | DOCS/PR_DRAFT.md |
| The Progress Prize submission text | DOCS/SUBMISSION_DRAFT.md |
| What you may reuse, and under what terms | NOTICE.md — the authoritative path-by-path map. This repository is not under one licence |
| The third-party data and its required citations | DATA_ATTRIBUTION.md |
| How the licence position was reached, and what legal questions remain | DOCS/LICENSING_PROPOSAL.md |
- A diagnosis, at file-and-line resolution, of why
vc_render_tifxyzattached a physically wrong voxel size to everything it rendered — established by reading the pinned revision and by executing its real code against live catalog metadata. - A fix, a patch against the pinned revision. It is three files: the renderer, plus the OME-Zarr attribute writer whose contract had to change so that an unknown size is not published as a measurement.
- Evidence on real published data: the patch compiled and run on GitHub-hosted runners against two public catalog volumes, with a deliberate control, comparing the declared metadata, the TIFF tags, and the rendered pixels.
- A public test harness that reproduces the defect and pins the behaviour, including upstream's own test suite compiled unmodified as a control.
vc_render_tifxyz writes the physical scale of its output into the OME-Zarr axis
metadata (.zattrs) and into TIFF resolution tags. To do that it needs the volume's
voxel size. It looked for that number in a local file, using a reader that
recognises only a top-level voxelsize key — while the process had already fetched
the correct value over the network and was holding it. For three of the four
published volumes probed here, the renderer therefore fell back to a scale of 1.0
and declared it as nanometres. The declared physical voxel size was wrong by
×2400, ×8640 or ×45532. The rendered image was fine; every physical number
attached to it was not.
Established by reading main @ 757f70c and by executing the pinned revision's
real code against live catalog metadata. Full detail in
DOCS/ROOT_CAUSE_ANALYSIS.md.
| # | Finding |
|---|---|
| 1 | The renderer resolves the size before it opens the remote volume, so it can never see the remote metadata. It grew its own filesystem-only reader as a result. |
| 2 | That reader knows only a top-level voxelsize. Most of the catalog publishes its resolution only as scan.tomo.acquisition.detector.samplePixelSize (mm). core already has a shared resolver for exactly this — vc::metadata::resolveLocalStoreVoxelSize — which vc_grow_seg_from_segments uses and the renderer does not. |
| 3 | The reader validates the field's type but not its value: {"voxelsize": 0} yields 0.0 and {"voxelsize": -3} yields -3.0, both returned as if they were measurements. |
| 4 | The placeholder 1.0 is emitted as a physical scale, with the unit defaulting to nanometer while the number is a micrometre quantity. The error is the product of two independent defects. |
| 5 | The GUI never passes the size for a native-resolution remote volume (SegmentationCommandHandler.cpp:2076), so the CLI fix would not reach most users. Diagnosed, not fixed, and deliberately outside this patch — see FEASIBILITY.md §8. |
Two claims worth flagging because they contradict common assumptions:
- The
vc_grow_seg_from_seedhalf of issue #1403 is already fixed upstream. Thestd::ifstream(vol_path/"meta.json")it quotes is not onmain. This project did not fix it and does not claim it. - Even where the old reader succeeds, the output is ×1000 wrong, because of the nanometre default. Fixing discovery alone would turn ×8640 wrong into ×1000 wrong.
The patched renderer has been compiled and run [exec]. Both the baseline and the
patched binary were built from the same commit, in the same configuration, by
.github/workflows/renderer-validation.yml
on a free ubuntu-24.04 runner, and run against public catalog data with identical
arguments.
Run: https://github.com/BioMarco/VoxelScaleGuard/actions/runs/35249590299
baseline (main) |
patched | |
|---|---|---|
PHerc0009B .zattrs |
nanometer / [1, 1, 1] |
micrometer / [8.64, 8.64, 8.64] |
PHerc0009B TIFF |
no resolution tag | XResolution 2939.8147 px/inch |
PHerc0172 .zattrs (control) |
nanometer / [1, 1, 1] |
micrometer / [7.91, 7.91, 7.91] |
PHerc0172 TIFF (control) |
no resolution tag | XResolution 3211.1252 px/inch |
| decoded pixels | — | identical on both volumes |
25400 / 8.64 = 2939.8148… and 25400 / 7.91 = 3211.1252…, so the emitted
resolution is the correct physical scale. The pixels are unchanged: the file bytes
differ only because the resolution tag is the fix, which is why the regression check
is a hash of the decoded pixel array and not of the file.
Against four real published volumes, running the pinned revision's reader and the fixed chain side by side:
| Published store | Old reader | Store actually says | Declared scale error |
|---|---|---|---|
PHerc0009B/…-8.640um-… |
not found | 8.64 µm | ×8640 |
PHercParis4/…-45.532um-… |
not found | 45.532 µm | ×45532 |
PHercParis4/…-2.400um-… |
not found | 2.4 µm | ×2400 |
PHerc0172/…-7.910um-… |
7.91 µm | 7.91 µm | ×1000 (unit only) — the control |
The last row is the reason the bug survived: it is the only catalog entry the old
reader handles, and it is the volume core/test/test_volume_live_s3.cpp pins.
37 cases / 249 assertions, plus the resolver suite 18 cases / 90 assertions
compiled unmodified as a control. Transcripts in
DOCS/RESULTS.md.
patch/vc_render_tifxyz.patch — 3 files, +358 / −102:
This standalone patch artefact covers the original three-file renderer/Zarr
correction. The accepted upstream contribution touched seven files because the
review round also required changes to the shared voxel-size resolver and regression
tests. Those additional changes are documented in
RESULTS.md §16, and the final result was merged upstream in
PR #1831.
| File | Why |
|---|---|
volume-cartographer/apps/src/vc_render_tifxyz.cpp |
the fix |
volume-cartographer/core/src/Zarr.cpp |
when the voxel size is unknown, writeZarrAttrs() declares no physical size — no axis unit, no fabricated placeholder — while still writing the OME-Zarr multiscales block with relative pyramid scaling |
volume-cartographer/core/include/vc/core/util/Zarr.hpp |
the documented contract for the above, and the new buildMultiscales() entry point |
It applies cleanly to the pinned revision. The standalone raw patch should not be
assumed to apply directly to later upstream revisions because main moved during
review. The maintained seven-file contribution was rebased/merged forward by the
maintainer and was accepted into upstream main as
PR #1831, merge commit
d08fa94d56c5e197f23408d1a39c78419146f311, on 30 September 2026. The GUI change
is not included.
The unknown-size behaviour was corrected after review of the open PR — the first
revision removed the whole multiscales block, which is the image's discovery
metadata. DOCS/RESULTS.md §16 records the correction and what it invalidated.
README.md this file
AGENTS.md operating rules for agents working in this repository
LICENSE MIT — this project's ORIGINAL work only
LICENSE-GPL-3.0.txt the GPL text for the derived patch material
NOTICE.md which paths fall under which terms (authoritative)
DATA_ATTRIBUTION.md the third-party data, its terms and its citations
DOCS/ all project documentation
INDEX.md reading order and the evidence map
PROJECT_STATUS.md state, gaps, isolation rules, environment notes
CI_VALIDATION.md the build-and-run record: environment, commands, results
RESULTS.md everything executed, and everything that was not (§7)
ROOT_CAUSE_ANALYSIS.md the cause, at file-and-line resolution
ARCHITECTURE.md the fix's design and the rejected alternatives
FEASIBILITY.md GO decision, resources, alternatives, out-of-scope items
TEST_PLAN.md what was tested, what is blocked, what would falsify this
RESEARCH.md sources, exact commits, licences, assumptions that failed
PRIZE_REQUIREMENTS.md Progress Prize rules vs. this project
PROGRESS_PRIZE_QUESTION.md the licensing question sent to the organisers
PR_DRAFT.md the pull request text and review follow-up notes
SUBMISSION_DRAFT.md Progress Prize submission text
PROGRESS_PRIZE_CHECKLIST.md what is prepared, and what only the author can attest
LICENSING_PROPOSAL.md how the licence position was reached, and what is uncertain
evidence/
before-after.png the before/after figure (contains third-party data)
terminal-before-after.png the run's log lines, drawn verbatim
README.md where every value comes from, and the data's licence
patch/vc_render_tifxyz.patch the proposed fix (three files)
patch/README.md the patch's GPL notice, modification dates and status
harness/ the reproducer: real upstream code + tests
ci/ the validation workflow's helpers and self-tests
research/ live catalog probe + the raw documents it fetched
raw_metadata/PROVENANCE.md URLs, hashes and terms for those documents
tools/ small scripts (git wrapper, fork-branch staging)
villa/ read-only clone of villa @ 757f70c (git-ignored)
The project's earlier state was wrong in ways that were only found by executing, and the record is kept rather than tidied, because it is what makes the current claims checkable. In order:
- The patch did not compile. The first real compile failed on a
use-before-declaration error: the new block read variables declared ~60 lines
below it. The harness could not have caught this, because it never compiled the
file — and its "pristine" copy was itself contaminated, copied from the patched
working tree. Both are fixed, and both now have tests.
RESULTS.md§9 - A 1000× unit regression on the explicit-size path.
--voxel-size 8640 --voxel-unit nanometerdeclared8.64 nanometerinstead of8640 nanometer. Found by review of the published output, not by the tooling. Fixed, with tests that check physical values rather than exit codes.RESULTS.md§10 - An adversarial pre-PR review found three more issues: the unknown-size branch
told the operator "no physical scale will be written" while still writing
scale = 1.0; the warning's own advice would have produced a 1000× error; and the opened volume was consulted after a possibly-stale local cache. All three fixed.RESULTS.md§11
Two of the three earlier defects were invisible to reading and to the test suite of the day. That is the reason the evidence in this repository is execution-based.
No installs. Uses the MSVC toolchain already on the machine (found: MSVC 14.34.31933, Windows SDK 10.0.22000.0).
# 1. copy the pinned upstream sources + fetch two header-only libs (~1.2 MB)
pwsh -File harness/setup.ps1
node harness/fetch_deps.mjs
# 2. build (invokes cl.exe directly; CMake cannot spawn subprocesses here)
pwsh -File harness/build.ps1 -Configuration Release
# 3. tests
cd harness/build/Release
./test_upstream_voxel_size_metadata.exe # upstream's suite, unmodified: 13/13
./test_render_voxel_size.exe # this project's: 37/37
# 4. the before/after demonstration over the real documents in research/raw_metadata
./probe_render_voxel_size.exeTo re-fetch the live metadata (read-only, ~12 KB):
node research/fetch_volume_metadata.mjs research/raw_metadataThat needs OpenCV, libtiff, Boost program_options and more, which are not
installable on the machine this was developed on, and neither CMake nor Ninja can
execute a compiler there (RESULTS.md §8.4). It is done in CI
instead, on a free GitHub runner, from the public apt package list:
https://github.com/BioMarco/VoxelScaleGuard/actions/workflows/renderer-validation.yml
cd villa
git apply --check /path/to/patch/vc_render_tifxyz.patch # exit 0
git apply /path/to/patch/vc_render_tifxyz.patchVerified to apply: git apply --check --reverse succeeds against the patched tree,
i.e. the patch describes the pinned revision exactly. It must not be assumed to
apply directly to a later upstream main; use PR #1831 for current integration.
- Upstream integration is complete. PR #1831
was reviewed, corrected after two substantive findings, and merged into
ScrollPrize/villamainon 30 September 2026 asd08fa94d56c5e197f23408d1a39c78419146f311. The review history is retained inRESULTS.md§16. - The GUI path remains broken:
SegmentationCommandHandler.cpp:2076-2078suppresses--voxel-sizefor native-resolution remote volumes, so the CLI fix is not reachable from VC3D. Deliberate, separate follow-up. - Coverage is two volumes, one crop, one slice each, in one build configuration. That demonstrates the correction and the absence of a pixel regression; it is not a survey.
- The September 2026 Progress Prize submission has been made; the result is
pending. The licensing arrangement was described to the organisers and
publicly confirmed on Discord as not problematic for the prize. The separate
legal uncertainties documented in
NOTICE.md§2.4 andLICENSING_PROPOSAL.md§4 remain unchanged.
This repository is not under a single licence, and the difference matters.
NOTICE.md is the authoritative path-by-path map; the summary is:
| Terms | Applies to | File |
|---|---|---|
| MIT, Copyright (c) 2026 Marco Pontesilli | the original work of this repository — the investigation, harness, CI, figure generators, documentation | LICENSE |
| GNU GPL v3.0-or-later, Copyright (C) 2023 EduceLab | the patch and everything derived from or copied out of Volume Cartographer | LICENSE-GPL-3.0.txt, NOTICE.md §2 |
| CC BY-NC 4.0 unless otherwise noted | the third-party tomographic data and the images rendered from it | DATA_ATTRIBUTION.md |
Three things worth stating plainly, because they are easy to get wrong:
- The patch is not MIT.
villa's root is MIT, Copyright (c) 2024 Vesuvius Challenge — but the three files the patch modifies live undervolume-cartographer/, which is GPL-3.0-or-later. The MIT licence here covers this project's own work and does not reach them. An earlier revision of this section said "villais MIT … the patch is a diff against MIT-licensed files", which was wrong; corrected 2026-09-18. The full inventory, including the rest of villa's per-subproject licence patchwork, is inDOCS/LICENSING_PROPOSAL.md§1. - The images contain third-party data.
DOCS/evidence/before-after.pngembeds rendered panels of thePHerc0009Bvolume, and the four documents underresearch/raw_metadata/are verbatim copies of published metadata. Those are CC BY-NC 4.0 unless otherwise noted, and the required attribution — authors, source links, both dataset citations, and an indication of the modifications made — is inDATA_ATTRIBUTION.md, linked fromDOCS/evidence/README.mdand fromresearch/raw_metadata/PROVENANCE.md. - Two licence questions remain genuinely open and are documented rather than
smoothed over: whether this repository is an aggregate in the sense of
GPL-3.0 §5 (the MIT grant for the documentation and CI rests on that reading),
and whether the
harness/src/vsguard/files that include GPL headers are derivative or combined works. They are treated conservatively as GPL-3.0-or-later regardless.NOTICE.md§2.4 states both, and nothing here claims they are settled.
Also: nlohmann/json (MIT) and doctest (MIT) are fetched at build time, not
vendored; the eight Volume Cartographer sources that harness/setup.ps1 copies are
not tracked in git and so are not redistributed by a clone; and the Open Data
bucket is accessed anonymously and read-only.
The reasoning, the checks behind it and the two claims that were wrong before them
are recorded in DOCS/LICENSING_PROPOSAL.md.
The approach of having the renderer consult the volume's remote voxel size is
NicolasHuberty's, from PR #1417, which lapsed to an inactivity bot rather than
being rejected. The samplePixelSize schema handling,
resolveLocalStoreVoxelSize, and Volume::voxelSize()'s semantics are
Bullo27's and the villa maintainers' (PRs #1227, #1229, #1454). The VC3D
enable-predicate diagnosis is Bullo27's, from PR #1228. The issue reports are
DarthCeltic's (#1403) and Bullo27's (#1226). See
FEASIBILITY.md §7.
Read AGENTS.md first. It contains the verification rules, the
attribution requirements, and the environment traps that will otherwise cost you an
hour.