diff --git a/volume-cartographer/apps/src/vc_render_tifxyz.cpp b/volume-cartographer/apps/src/vc_render_tifxyz.cpp index 472d0928e..94cf5bd8d 100644 --- a/volume-cartographer/apps/src/vc_render_tifxyz.cpp +++ b/volume-cartographer/apps/src/vc_render_tifxyz.cpp @@ -1296,6 +1296,22 @@ int main(int argc, char *argv[]) printMat4x4(affineTransform.matrix, "Final composed affine:"); } + // Try to read voxelsize from meta.json to set TIFF DPI + float tifDpi = 0.f; + { + auto metaPath = vol_path / "meta.json"; + if (std::filesystem::exists(metaPath)) { + try { + auto meta = nlohmann::json::parse(std::ifstream(metaPath)); + if (meta.contains("voxelsize")) { + double vs = meta["voxelsize"].get(); + tifDpi = voxelSizeToDpi(vs); + } + } catch (...) { + } + } + } + // --- Open source volume --- std::shared_ptr remoteVolume; std::unique_ptr ownedDs; @@ -1567,7 +1583,7 @@ int main(int argc, char *argv[]) uint32_t tiffTileW = (uint32_t(outW) + 15u) & ~15u; uint16_t tifComp = quickTif ? COMPRESSION_PACKBITS : COMPRESSION_LZW; for (int z = 0; z < tifSlices; z++) - tifWriters.emplace_back(makePartPath(z), uint32_t(outW), uint32_t(outH), cvType, tiffTileW, tiffTileH, 0.0f, tifComp); + tifWriters.emplace_back(makePartPath(z), uint32_t(outW), uint32_t(outH), cvType, tiffTileW, tiffTileH, 0.0f, tifComp, tifDpi); } } diff --git a/volume-cartographer/apps/src/vc_zarr_to_tiff.cpp b/volume-cartographer/apps/src/vc_zarr_to_tiff.cpp index 63240fc6a..a5c0c3e0c 100644 --- a/volume-cartographer/apps/src/vc_zarr_to_tiff.cpp +++ b/volume-cartographer/apps/src/vc_zarr_to_tiff.cpp @@ -60,6 +60,24 @@ int main(int argc, char** argv) const uint16_t compression = parseCompression(compressionStr); + // Try to read voxelsize from meta.json to set TIFF DPI + float dpi = 0.f; + { + fs::path metaPath = fs::path(inputPath) / "meta.json"; + if (fs::exists(metaPath)) { + try { + auto meta = json::parse(std::ifstream(metaPath)); + if (meta.contains("voxelsize")) { + double vs = meta["voxelsize"].get(); + dpi = voxelSizeToDpi(vs); + if (dpi > 0.f) + std::cout << "Voxel size: " << vs << " µm → DPI: " << dpi << "\n"; + } + } catch (...) { + } + } + } + // Open zarr dataset fs::path inRoot(inputPath); std::string dsName = std::to_string(level); @@ -110,11 +128,7 @@ int main(int argc, char** argv) fs::path outPath = outDir / fname.str(); constexpr uint32_t tileSize = 256; - TiffWriter writer(outPath, - static_cast(X), - static_cast(Y), - cvType, tileSize, tileSize, - 0.0f, compression); + TiffWriter writer(outPath, static_cast(X), static_cast(Y), cvType, tileSize, tileSize, 0.0f, compression, dpi); for (uint32_t ty = 0; ty < Y; ty += tileSize) { for (uint32_t tx = 0; tx < X; tx += tileSize) { diff --git a/volume-cartographer/core/include/vc/core/util/QuadSurface.hpp b/volume-cartographer/core/include/vc/core/util/QuadSurface.hpp index cd50169f0..2e19f1dda 100644 --- a/volume-cartographer/core/include/vc/core/util/QuadSurface.hpp +++ b/volume-cartographer/core/include/vc/core/util/QuadSurface.hpp @@ -396,6 +396,10 @@ class QuadSurface : public Surface void refreshMaskTimestamp(); static std::optional readMaskTimestamp(const std::filesystem::path& dir); + // DPI for TIFF output (0 = don't set). Set via setDpi() or voxelSizeToDpi(). + float dpi() const { return dpi_; } + void setDpi(float d) { dpi_ = d; } + protected: std::unordered_map _channels; std::unique_ptr> _points; @@ -404,6 +408,7 @@ class QuadSurface : public Surface Rect3D _bbox = {{-1,-1,-1},{-1,-1,-1}}; std::set _overlappingIds; std::optional _maskTimestamp; + float dpi_ = 0.f; private: // Write surface data to directory without modifying state. skipChannel can be used to exclude a channel. diff --git a/volume-cartographer/core/include/vc/core/util/Tiff.hpp b/volume-cartographer/core/include/vc/core/util/Tiff.hpp index dec978f9c..1cc491756 100644 --- a/volume-cartographer/core/include/vc/core/util/Tiff.hpp +++ b/volume-cartographer/core/include/vc/core/util/Tiff.hpp @@ -7,18 +7,28 @@ #include #include +// Convert voxel size in micrometers to DPI (dots per inch) +// Returns 0 if voxelSize is <= 0 +inline float voxelSizeToDpi(double voxelSizeUm) +{ + return voxelSizeUm > 0 ? static_cast(25400.0 / voxelSizeUm) : 0.f; +} + // Write single-channel image (8U, 16U, 32F) as tiled TIFF // cvType: output type (-1 = same as input). If different, values are scaled: // 8U↔16U: scale by 257, 8U↔32F: scale by 1/255, 16U↔32F: scale by 1/65535 // compression: libtiff compression constant (e.g. COMPRESSION_LZW, COMPRESSION_PACKBITS) // padValue: value for padding partial tiles (default -1.0f, used for float; int types use 0) -void writeTiff(const std::filesystem::path& outPath, - const cv::Mat& img, - int cvType = -1, - uint32_t tileW = 1024, - uint32_t tileH = 1024, - float padValue = -1.0f, - uint16_t compression = COMPRESSION_LZW); +// dpi: resolution in dots per inch (0 = don't set). Use voxelSizeToDpi() to convert from µm. +void writeTiff( + const std::filesystem::path& outPath, + const cv::Mat& img, + int cvType = -1, + uint32_t tileW = 1024, + uint32_t tileH = 1024, + float padValue = -1.0f, + uint16_t compression = COMPRESSION_LZW, + float dpi = 0.f); // Class for incremental tiled TIFF writing // Useful for writing tiles in parallel or from streaming data @@ -27,13 +37,17 @@ class TiffWriter { // Open a new TIFF file for tiled writing // cvType: CV_8UC1, CV_16UC1, or CV_32FC1 // padValue: value for padding partial tiles (used for float; int types use 0) - TiffWriter(const std::filesystem::path& path, - uint32_t width, uint32_t height, - int cvType, - uint32_t tileW = 1024, - uint32_t tileH = 1024, - float padValue = -1.0f, - uint16_t compression = COMPRESSION_LZW); + // dpi: resolution in dots per inch (0 = don't set). Use voxelSizeToDpi() to convert from µm. + TiffWriter( + const std::filesystem::path& path, + uint32_t width, + uint32_t height, + int cvType, + uint32_t tileW = 1024, + uint32_t tileH = 1024, + float padValue = -1.0f, + uint16_t compression = COMPRESSION_LZW, + float dpi = 0.f); ~TiffWriter(); diff --git a/volume-cartographer/core/src/GrowPatch.cpp b/volume-cartographer/core/src/GrowPatch.cpp index 7a335209d..bd333f29d 100644 --- a/volume-cartographer/core/src/GrowPatch.cpp +++ b/volume-cartographer/core/src/GrowPatch.cpp @@ -3157,6 +3157,7 @@ QuadSurface *tracer(vc::VcDataset *ds, float scale, vc::cache::TieredChunkCache cv::Mat_ generations_crop = generations(used_area_safe); auto surf = new QuadSurface(points_crop, {1/T, 1/T}); + surf->setDpi(voxelSizeToDpi(voxelsize)); surf->setChannel("generations", generations_crop); if (params.value("vis_losses", false)) { diff --git a/volume-cartographer/core/src/GrowSurface.cpp b/volume-cartographer/core/src/GrowSurface.cpp index 2b0bcb842..f6feb99ab 100644 --- a/volume-cartographer/core/src/GrowSurface.cpp +++ b/volume-cartographer/core/src/GrowSurface.cpp @@ -2362,6 +2362,7 @@ QuadSurface *grow_surf_from_surfs(QuadSurface *seed, const std::vectorsetDpi(voxelSizeToDpi(voxelsize)); auto gen_channel = surftrack_generation_channel(generations, used_area, step); if (!gen_channel.empty()) diff --git a/volume-cartographer/core/src/QuadSurface.cpp b/volume-cartographer/core/src/QuadSurface.cpp index 170c675d9..aaf8c40c7 100644 --- a/volume-cartographer/core/src/QuadSurface.cpp +++ b/volume-cartographer/core/src/QuadSurface.cpp @@ -363,7 +363,7 @@ void QuadSurface::writeValidMask(const cv::Mat& img) cv::Mat_ mask = validMask(); if (img.empty()) { - writeTiff(maskPath, mask); + writeTiff(maskPath, mask, -1, 1024, 1024, -1.0f, COMPRESSION_LZW, dpi_); } else { std::vector layers = {mask, img}; cv::imwritemulti(maskPath.string(), layers); @@ -817,9 +817,9 @@ void QuadSurface::writeDataToDirectory(const std::filesystem::path& dir, const s cv::split((*_points), xyz); // Write x/y/z as 32-bit float tiled TIFF with LZW - writeTiff(dir / "x.tif", xyz[0]); - writeTiff(dir / "y.tif", xyz[1]); - writeTiff(dir / "z.tif", xyz[2]); + writeTiff(dir / "x.tif", xyz[0], -1, 1024, 1024, -1.0f, COMPRESSION_LZW, dpi_); + writeTiff(dir / "y.tif", xyz[1], -1, 1024, 1024, -1.0f, COMPRESSION_LZW, dpi_); + writeTiff(dir / "z.tif", xyz[2], -1, 1024, 1024, -1.0f, COMPRESSION_LZW, dpi_); // OpenCV compression params for fallback std::vector compression_params = { cv::IMWRITE_TIFF_COMPRESSION, 5 }; @@ -834,7 +834,7 @@ void QuadSurface::writeDataToDirectory(const std::filesystem::path& dir, const s (mat.type() == CV_8UC1 || mat.type() == CV_16UC1 || mat.type() == CV_32FC1)) { try { - writeTiff(dir / (name + ".tif"), mat); + writeTiff(dir / (name + ".tif"), mat, -1, 1024, 1024, -1.0f, COMPRESSION_LZW, dpi_); wrote = true; } catch (...) { wrote = false; // Fall back to OpenCV diff --git a/volume-cartographer/core/src/Tiff.cpp b/volume-cartographer/core/src/Tiff.cpp index d31b3a719..872e3de5f 100644 --- a/volume-cartographer/core/src/Tiff.cpp +++ b/volume-cartographer/core/src/Tiff.cpp @@ -79,13 +79,7 @@ cv::Mat convertWithScaling(const cv::Mat& img, int targetType) { // writeTiff implementation // ============================================================================ -void writeTiff(const std::filesystem::path& outPath, - const cv::Mat& img, - int cvType, - uint32_t tileW, - uint32_t tileH, - float padValue, - uint16_t compression) +void writeTiff(const std::filesystem::path& outPath, const cv::Mat& img, int cvType, uint32_t tileW, uint32_t tileH, float padValue, uint16_t compression, float dpi) { if (img.empty()) throw std::runtime_error("Empty image for " + outPath.string()); @@ -117,6 +111,11 @@ void writeTiff(const std::filesystem::path& outPath, TIFFSetField(tf, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL); TIFFSetField(tf, TIFFTAG_TILEWIDTH, tileW); TIFFSetField(tf, TIFFTAG_TILELENGTH, tileH); + if (dpi > 0.f) { + TIFFSetField(tf, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH); + TIFFSetField(tf, TIFFTAG_XRESOLUTION, dpi); + TIFFSetField(tf, TIFFTAG_YRESOLUTION, dpi); + } const size_t tileBytes = static_cast(tileW) * tileH * params.elemSize; std::vector tileBuf(tileBytes); @@ -158,15 +157,8 @@ void writeTiff(const std::filesystem::path& outPath, // TiffWriter implementation // ============================================================================ -TiffWriter::TiffWriter(const std::filesystem::path& path, - uint32_t width, uint32_t height, - int cvType, - uint32_t tileW, - uint32_t tileH, - float padValue, - uint16_t compression) - : _width(width), _height(height), _tileW(tileW), _tileH(tileH), - _cvType(cvType), _padValue(padValue), _path(path) +TiffWriter::TiffWriter(const std::filesystem::path& path, uint32_t width, uint32_t height, int cvType, uint32_t tileW, uint32_t tileH, float padValue, uint16_t compression, float dpi) + : _width(width), _height(height), _tileW(tileW), _tileH(tileH), _cvType(cvType), _padValue(padValue), _path(path) { const auto params = getTiffParams(cvType); _elemSize = params.elemSize; @@ -187,6 +179,11 @@ TiffWriter::TiffWriter(const std::filesystem::path& path, TIFFSetField(_tiff, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL); TIFFSetField(_tiff, TIFFTAG_TILEWIDTH, tileW); TIFFSetField(_tiff, TIFFTAG_TILELENGTH, tileH); + if (dpi > 0.f) { + TIFFSetField(_tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH); + TIFFSetField(_tiff, TIFFTAG_XRESOLUTION, dpi); + TIFFSetField(_tiff, TIFFTAG_YRESOLUTION, dpi); + } // Allocate reusable tile buffer _tileBuf.resize(static_cast(tileW) * tileH * _elemSize); @@ -298,6 +295,10 @@ bool mergeTiffParts(const std::string& outputPath, int numParts) TIFFGetField(first, TIFFTAG_SAMPLESPERPIXEL, &spp); TIFFGetField(first, TIFFTAG_SAMPLEFORMAT, &sf); TIFFGetField(first, TIFFTAG_COMPRESSION, &comp); + uint16_t resUnit = 0; + float xRes = 0, yRes = 0; + bool hasRes = TIFFGetField(first, TIFFTAG_RESOLUTIONUNIT, &resUnit) && TIFFGetField(first, TIFFTAG_XRESOLUTION, &xRes) && + TIFFGetField(first, TIFFTAG_YRESOLUTION, &yRes); TIFFClose(first); TIFF* out = TIFFOpen(finalPath.c_str(), "w"); @@ -307,6 +308,11 @@ bool mergeTiffParts(const std::string& outputPath, int numParts) TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, bps); TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, spp); TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, sf); TIFFSetField(out, TIFFTAG_COMPRESSION, comp); TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + if (hasRes) { + TIFFSetField(out, TIFFTAG_RESOLUTIONUNIT, resUnit); + TIFFSetField(out, TIFFTAG_XRESOLUTION, xRes); + TIFFSetField(out, TIFFTAG_YRESOLUTION, yRes); + } tmsize_t tileBytes = TIFFTileSize(out); std::vector buf(tileBytes, 0), zero(tileBytes, 0);