Skip to content

Repository files navigation

VoxelScale Guard

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 main via PR #1831 on 30 September 2026 (merge commit d08fa94d56c5e197f23408d1a39c78419146f311). The September 2026 Progress Prize submission has been sent; the prize result is pending. The licensing arrangement documented in NOTICE.md was 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

What this repository contains

  1. A diagnosis, at file-and-line resolution, of why vc_render_tifxyz attached 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.
  2. 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.
  3. 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.
  4. A public test harness that reproduces the defect and pins the behaviour, including upstream's own test suite compiled unmodified as a control.

The problem in one paragraph

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.

What was found

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_seed half of issue #1403 is already fixed upstream. The std::ifstream(vol_path/"meta.json") it quotes is not on main. 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 evidence

From real compiled binaries, on real published volumes

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.

From the pinned revision's own code, against the live catalog

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.

Tests

37 cases / 249 assertions, plus the resolver suite 18 cases / 90 assertions compiled unmodified as a control. Transcripts in DOCS/RESULTS.md.

The patch

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.

Repository layout

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)

Development history, kept deliberately

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:

  1. 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
  2. A 1000× unit regression on the explicit-size path. --voxel-size 8640 --voxel-unit nanometer declared 8.64 nanometer instead of 8640 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
  3. 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.

Building and running the harness

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.exe

To re-fetch the live metadata (read-only, ~12 KB):

node research/fetch_volume_metadata.mjs research/raw_metadata

Building and running the renderer itself

That 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

Applying the patch

cd villa
git apply --check /path/to/patch/vc_render_tifxyz.patch   # exit 0
git apply         /path/to/patch/vc_render_tifxyz.patch

Verified 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.

What is still open

  1. Upstream integration is complete. PR #1831 was reviewed, corrected after two substantive findings, and merged into ScrollPrize/villa main on 30 September 2026 as d08fa94d56c5e197f23408d1a39c78419146f311. The review history is retained in RESULTS.md §16.
  2. The GUI path remains broken: SegmentationCommandHandler.cpp:2076-2078 suppresses --voxel-size for native-resolution remote volumes, so the CLI fix is not reachable from VC3D. Deliberate, separate follow-up.
  3. 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.
  4. 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 and LICENSING_PROPOSAL.md §4 remain unchanged.

Data and licences

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 under volume-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 "villa is 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 in DOCS/LICENSING_PROPOSAL.md §1.
  • The images contain third-party data. DOCS/evidence/before-after.png embeds rendered panels of the PHerc0009B volume, and the four documents under research/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 in DATA_ATTRIBUTION.md, linked from DOCS/evidence/README.md and from research/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.

Attribution

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.

Contributing

Read AGENTS.md first. It contains the verification rules, the attribution requirements, and the environment traps that will otherwise cost you an hour.

About

VoxelScaleGuard fixes incorrect physical voxel scaling in Vesuvius Challenge's CT renderer. It reuses existing volume metadata, validates voxel sizes, corrects output units, and provides reproducible tests for local and remote volumes.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages