Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion volume-cartographer/apps/src/vc_render_tifxyz.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>();
tifDpi = voxelSizeToDpi(vs);
Comment on lines +1307 to +1308
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for group index when setting render DPI

In vc_render_tifxyz, DPI is derived from raw voxelsize only, but render resolution already changes with group_idx via ds_scale = 2^-group_idx (same file, lines 1258 and 1458), so any run with --group-idx > 0 gets TIFF resolution tags that are too high for the generated pixel grid. This produces inconsistent spatial metadata between rendered outputs from different OME-Zarr levels.

Useful? React with 👍 / 👎.

}
} catch (...) {
}
}
}

// --- Open source volume ---
std::shared_ptr<Volume> remoteVolume;
std::unique_ptr<vc::VcDataset> ownedDs;
Expand Down Expand Up @@ -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);
}
}

Expand Down
24 changes: 19 additions & 5 deletions volume-cartographer/apps/src/vc_zarr_to_tiff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>();
dpi = voxelSizeToDpi(vs);
Comment on lines +71 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scale DPI by selected pyramid level

vc_zarr_to_tiff reads from dataset input/<level> but computes DPI from the base meta.json voxel size without applying the level downsampling factor, so outputs from --level > 0 are tagged with overly high resolution. The repo’s volume docs explicitly define levels as 2x/4x/8x downsampled (volume-cartographer/docs/api/Volume.md, lines 135-137 and 173), so this writes incorrect physical calibration metadata for nonzero levels and can distort downstream measurements.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a good point.

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);
Expand Down Expand Up @@ -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<uint32_t>(X),
static_cast<uint32_t>(Y),
cvType, tileSize, tileSize,
0.0f, compression);
TiffWriter writer(outPath, static_cast<uint32_t>(X), static_cast<uint32_t>(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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ class QuadSurface : public Surface
void refreshMaskTimestamp();
static std::optional<std::filesystem::file_time_type> 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<std::string, cv::Mat> _channels;
std::unique_ptr<cv::Mat_<cv::Vec3f>> _points;
Expand All @@ -404,6 +408,7 @@ class QuadSurface : public Surface
Rect3D _bbox = {{-1,-1,-1},{-1,-1,-1}};
std::set<std::string> _overlappingIds;
std::optional<std::filesystem::file_time_type> _maskTimestamp;
float dpi_ = 0.f;

private:
// Write surface data to directory without modifying state. skipChannel can be used to exclude a channel.
Expand Down
42 changes: 28 additions & 14 deletions volume-cartographer/core/include/vc/core/util/Tiff.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@
#include <cstdint>
#include <tiffio.h>

// 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<float>(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
Expand All @@ -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();

Expand Down
1 change: 1 addition & 0 deletions volume-cartographer/core/src/GrowPatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3157,6 +3157,7 @@ QuadSurface *tracer(vc::VcDataset *ds, float scale, vc::cache::TieredChunkCache
cv::Mat_<uint16_t> 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)) {
Expand Down
1 change: 1 addition & 0 deletions volume-cartographer/core/src/GrowSurface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2362,6 +2362,7 @@ QuadSurface *grow_surf_from_surfs(QuadSurface *seed, const std::vector<QuadSurfa
/*inpaint=*/false, approved_weight_hr, prefer_approved_in_hr);

auto surf = new QuadSurface(points_hr(used_area_hr), {1/src_step,1/src_step});
surf->setDpi(voxelSizeToDpi(voxelsize));

auto gen_channel = surftrack_generation_channel(generations, used_area, step);
if (!gen_channel.empty())
Expand Down
10 changes: 5 additions & 5 deletions volume-cartographer/core/src/QuadSurface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ void QuadSurface::writeValidMask(const cv::Mat& img)
cv::Mat_<uint8_t> mask = validMask();

if (img.empty()) {
writeTiff(maskPath, mask);
writeTiff(maskPath, mask, -1, 1024, 1024, -1.0f, COMPRESSION_LZW, dpi_);
} else {
std::vector<cv::Mat> layers = {mask, img};
cv::imwritemulti(maskPath.string(), layers);
Expand Down Expand Up @@ -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<int> compression_params = { cv::IMWRITE_TIFF_COMPRESSION, 5 };
Expand All @@ -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
Expand Down
38 changes: 22 additions & 16 deletions volume-cartographer/core/src/Tiff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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<size_t>(tileW) * tileH * params.elemSize;
std::vector<uint8_t> tileBuf(tileBytes);
Expand Down Expand Up @@ -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;
Expand All @@ -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<size_t>(tileW) * tileH * _elemSize);
Expand Down Expand Up @@ -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");
Expand All @@ -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<uint8_t> buf(tileBytes, 0), zero(tileBytes, 0);
Expand Down
Loading