From 2417b6fda8d901e55f384b98d10d0ba43f341924 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 1 Apr 2026 15:19:58 +0200 Subject: [PATCH 1/3] vc_render_tifxyz: provide voxel size scale to ome-zarr --- .../apps/src/vc_render_tifxyz.cpp | 60 ++++++++++++++++++- .../core/include/vc/core/util/Zarr.hpp | 4 +- volume-cartographer/core/src/Zarr.cpp | 20 ++++--- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/volume-cartographer/apps/src/vc_render_tifxyz.cpp b/volume-cartographer/apps/src/vc_render_tifxyz.cpp index bdb04ce3d..5ee35c2e3 100644 --- a/volume-cartographer/apps/src/vc_render_tifxyz.cpp +++ b/volume-cartographer/apps/src/vc_render_tifxyz.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1054,6 +1055,34 @@ static void renderTiles( } +// ============================================================ +// readVolumeVoxelSize – read voxelsize from volume metadata +// ============================================================ + +static std::optional readVolumeVoxelSize(const std::filesystem::path& volPath) +{ + using json = nlohmann::json; + auto tryFile = [](const std::filesystem::path& p, const char* key) -> std::optional { + if (!std::filesystem::exists(p)) return std::nullopt; + try { + auto j = json::parse(std::ifstream(p)); + if (key) { + if (j.contains(key) && j[key].is_object()) + j = j[key]; + else + return std::nullopt; + } + if (j.contains("voxelsize") && j["voxelsize"].is_number()) + return j["voxelsize"].get(); + } catch (...) {} + return std::nullopt; + }; + if (auto v = tryFile(volPath / "meta.json", nullptr)) return v; + if (auto v = tryFile(volPath / "metadata.json", "scan")) return v; + if (auto v = tryFile(volPath / "metadata.json", nullptr)) return v; + return std::nullopt; +} + // ============================================================ // main // ============================================================ @@ -1111,7 +1140,9 @@ int main(int argc, char *argv[]) ("merge-tiff-parts", po::bool_switch()->default_value(false), "Merge partial TIFFs from multi-VM render") ("pyramid", po::value()->default_value(true), "Build pyramid levels L1-L5 (default: true)") ("resume", po::bool_switch()->default_value(false), "Skip chunks that already exist on disk") - ("pre", po::bool_switch()->default_value(false), "Create zarr + all level datasets"); + ("pre", po::bool_switch()->default_value(false), "Create zarr + all level datasets") + ("voxel-size", po::value(), "Physical voxel size for OME-Zarr scale metadata (reads from volume metadata if omitted)") + ("voxel-unit", po::value()->default_value("nanometer"), "Physical unit for OME-Zarr axes (e.g. nanometer, micrometer)"); // clang-format on po::options_description all("Usage"); @@ -1357,6 +1388,27 @@ int main(int argc, char *argv[]) logPrintf(stdout, "chunk shape [%s]\n", oss.str().c_str()); } + // --- Resolve voxel size for OME-Zarr metadata --- + const std::string voxel_unit = parsed["voxel-unit"].as(); + double base_voxel_size = 1.0; + if (parsed.count("voxel-size")) { + base_voxel_size = parsed["voxel-size"].as(); + if (!std::isfinite(base_voxel_size) || base_voxel_size <= 0.0) { + logPrintf(stderr, "Error: --voxel-size must be a positive finite number\n"); + return EXIT_FAILURE; + } + logPrintf(stdout, "Voxel size (from CLI): %g %s\n", base_voxel_size, voxel_unit.c_str()); + } else if (auto mv = readVolumeVoxelSize(vol_path); mv.has_value()) { + if (std::isfinite(*mv) && *mv > 0.0) { + base_voxel_size = *mv; + logPrintf(stdout, "Voxel size (from volume metadata): %g %s\n", base_voxel_size, voxel_unit.c_str()); + } else { + logPrintf(stderr, "Warning: ignoring invalid metadata voxelsize; using default 1.0\n"); + } + } else { + logPrintf(stdout, "Voxel size: 1.0 (no metadata found; override with --voxel-size)\n"); + } + int rotQuadGlobal = -1; if (std::abs(rotate_angle) > 1e-6) { rotQuadGlobal = normalizeQuadrantRotation(rotate_angle); @@ -1506,7 +1558,8 @@ int main(int argc, char *argv[]) cv::Size attrXY = tgt_size; if (rotQuad >= 0 && (rotQuad % 2) == 1) std::swap(attrXY.width, attrXY.height); writeZarrAttrs(outFilePath, vol_path, group_idx, baseZ, slice_step, accum_step, - accum_type_str, accumOffsets.size(), attrXY, baseZ, CH, CW); + accum_type_str, accumOffsets.size(), attrXY, baseZ, CH, CW, + base_voxel_size, voxel_unit); return true; } else if (numParts > 1) { if (!std::filesystem::exists(std::filesystem::path(zarrOutputArg) / "0" / ".zarray")) { @@ -1705,7 +1758,8 @@ int main(int argc, char *argv[]) cv::Size attrXY = tgt_size; if (rotQuad >= 0 && (rotQuad % 2) == 1) std::swap(attrXY.width, attrXY.height); writeZarrAttrs(outFilePath, vol_path, group_idx, baseZ, slice_step, accum_step, - accum_type_str, accumOffsets.size(), attrXY, baseZ, CH, CW); + accum_type_str, accumOffsets.size(), attrXY, baseZ, CH, CW, + base_voxel_size, voxel_unit); } } return true; diff --git a/volume-cartographer/core/include/vc/core/util/Zarr.hpp b/volume-cartographer/core/include/vc/core/util/Zarr.hpp index 17b129a81..7405cc644 100644 --- a/volume-cartographer/core/include/vc/core/util/Zarr.hpp +++ b/volume-cartographer/core/include/vc/core/util/Zarr.hpp @@ -85,7 +85,9 @@ void writeZarrAttrs(const std::filesystem::path& outFile, const std::filesystem::path& volPath, int groupIdx, size_t baseZ, double sliceStep, double accumStep, const std::string& accumTypeStr, size_t accumSamples, - const cv::Size& canvasSize, size_t CZ, size_t CH, size_t CW); + const cv::Size& canvasSize, size_t CZ, size_t CH, size_t CW, + double baseVoxelSize = 1.0, + const std::string& voxelUnit = ""); // Write a dense uint8 ZYX subregion into a freshly created dataset via // writeChunk(). Chunks overlapping the region are materialized; untouched diff --git a/volume-cartographer/core/src/Zarr.cpp b/volume-cartographer/core/src/Zarr.cpp index 2452bbffa..a1439e9d5 100644 --- a/volume-cartographer/core/src/Zarr.cpp +++ b/volume-cartographer/core/src/Zarr.cpp @@ -338,7 +338,8 @@ void writeZarrAttrs(const std::filesystem::path& outDir, const std::filesystem::path& volPath, int groupIdx, size_t baseZ, double sliceStep, double accumStep, const std::string& accumTypeStr, size_t accumSamples, - const cv::Size& canvasSize, size_t CZ, size_t CH, size_t CW) + const cv::Size& canvasSize, size_t CZ, size_t CH, size_t CW, + double baseVoxelSize, const std::string& voxelUnit) { json attrs; attrs["source_zarr"] = volPath.string(); @@ -356,19 +357,20 @@ void writeZarrAttrs(const std::filesystem::path& outDir, json ms; ms["version"] = "0.4"; ms["name"] = "render"; - ms["axes"] = json::array({ - json{{"name","z"},{"type","space"}}, - json{{"name","y"},{"type","space"}}, - json{{"name","x"},{"type","space"}} - }); + auto makeAxis = [&](const char* name) -> json { + json ax = {{"name", name}, {"type", "space"}}; + if (!voxelUnit.empty()) ax["unit"] = voxelUnit; + return ax; + }; + ms["axes"] = json::array({makeAxis("z"), makeAxis("y"), makeAxis("x")}); ms["datasets"] = json::array(); for (int l = 0; l <= 5; l++) { - double s = std::pow(2.0, l); - const double sz = 1.0; + const double sYX = baseVoxelSize * std::pow(2.0, l); + const double sZ = baseVoxelSize; ms["datasets"].push_back({ {"path", std::to_string(l)}, {"coordinateTransformations", json::array({ - json{{"type","scale"},{"scale",json::array({sz,s,s})}}, + json{{"type","scale"},{"scale",json::array({sZ, sYX, sYX})}}, json{{"type","translation"},{"translation",json::array({0.0,0.0,0.0})}} })} }); From c2a2afca4b476f718277df5ff00dccc46edd521d Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 1 Apr 2026 16:36:10 +0200 Subject: [PATCH 2/3] vc_render_tifxyz: resurrect anisotropic scaling This regressed in 0fe91589f --- volume-cartographer/core/src/Zarr.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volume-cartographer/core/src/Zarr.cpp b/volume-cartographer/core/src/Zarr.cpp index a1439e9d5..c2c0b4314 100644 --- a/volume-cartographer/core/src/Zarr.cpp +++ b/volume-cartographer/core/src/Zarr.cpp @@ -322,7 +322,8 @@ void createPyramidDatasets(const std::filesystem::path& outDir, std::vector prevShape = shape0; for (int level = 1; level <= 5; level++) { - std::vector shape = {(prevShape[0]+1)/2, (prevShape[1]+1)/2, (prevShape[2]+1)/2}; + // Keep Z fixed and halve only Y/X at each level (anisotropic scaling). + std::vector shape = {prevShape[0], (prevShape[1]+1)/2, (prevShape[2]+1)/2}; size_t chZ = std::min(shape[0], shape0[0]); std::vector chunks = {chZ, std::min(CH, shape[1]), std::min(CW, shape[2])}; vc::createZarrDataset(outDir, std::to_string(level), shape, chunks, dtype, "blosc"); From afaebb666f9213519bbfebe5392445ddad79e6c5 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 2 Apr 2026 10:16:49 +0200 Subject: [PATCH 3/3] vc_render_tifxyz: fail when merge-tiff step failed --- volume-cartographer/core/src/Tiff.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/volume-cartographer/core/src/Tiff.cpp b/volume-cartographer/core/src/Tiff.cpp index d31b3a719..424c55696 100644 --- a/volume-cartographer/core/src/Tiff.cpp +++ b/volume-cartographer/core/src/Tiff.cpp @@ -285,10 +285,11 @@ bool mergeTiffParts(const std::string& outputPath, int numParts) } std::cout << "Merging " << groups.size() << " TIFF(s) from " << numParts << " parts..." << std::endl; + size_t failures = 0; for (auto& [finalPath, partFiles] : groups) { std::sort(partFiles.begin(), partFiles.end()); TIFF* first = TIFFOpen(partFiles[0].c_str(), "r"); - if (!first) { std::cerr << "Cannot open " << partFiles[0] << "\n"; continue; } + if (!first) { std::cerr << "Cannot open " << partFiles[0] << "\n"; failures++; continue; } uint32_t w, h, tw, th; uint16_t bps, spp, sf, comp; TIFFGetField(first, TIFFTAG_IMAGEWIDTH, &w); TIFFGetField(first, TIFFTAG_IMAGELENGTH, &h); @@ -301,7 +302,7 @@ bool mergeTiffParts(const std::string& outputPath, int numParts) TIFFClose(first); TIFF* out = TIFFOpen(finalPath.c_str(), "w"); - if (!out) { std::cerr << "Cannot create " << finalPath << "\n"; continue; } + if (!out) { std::cerr << "Cannot create " << finalPath << "\n"; failures++; continue; } TIFFSetField(out, TIFFTAG_IMAGEWIDTH, w); TIFFSetField(out, TIFFTAG_IMAGELENGTH, h); TIFFSetField(out, TIFFTAG_TILEWIDTH, tw); TIFFSetField(out, TIFFTAG_TILELENGTH, th); TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, bps); TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, spp); @@ -330,6 +331,10 @@ bool mergeTiffParts(const std::string& outputPath, int numParts) for (auto& pf : partFiles) std::filesystem::remove(pf); std::cout << " " << finalPath.filename().string() << ": " << merged << " tiles from " << partFiles.size() << " parts\n"; } + if (failures > 0) { + std::cerr << "Merge failed: " << failures << " of " << groups.size() << " TIFF(s) could not be created.\n"; + return false; + } std::cout << "Merge complete.\n"; return true; }