From 722ec6e4469c9abd69857d39b3348985041b52ca Mon Sep 17 00:00:00 2001 From: SuperOptimizer <156155735+SuperOptimizer@users.noreply.github.com> Date: Thu, 5 Mar 2026 07:57:32 -0600 Subject: [PATCH 1/3] fix non power of 2 chunks and allow for opening a bare ome zarr --- .../apps/VC3D/MenuActionController.cpp | 49 ++++++++ .../apps/VC3D/MenuActionController.hpp | 2 + volume-cartographer/core/src/Slicing.cpp | 115 +++++++++++------- 3 files changed, 119 insertions(+), 47 deletions(-) diff --git a/volume-cartographer/apps/VC3D/MenuActionController.cpp b/volume-cartographer/apps/VC3D/MenuActionController.cpp index 85819ea18..a04aad87c 100644 --- a/volume-cartographer/apps/VC3D/MenuActionController.cpp +++ b/volume-cartographer/apps/VC3D/MenuActionController.cpp @@ -129,6 +129,9 @@ void MenuActionController::populateMenus(QMenuBar* menuBar) _openAct->setShortcut(vc3d::keybinds::sequenceFor(vc3d::keybinds::shortcuts::OpenVolpkg)); connect(_openAct, &QAction::triggered, this, &MenuActionController::openVolpkg); + _openLocalZarrAct = new QAction(QObject::tr("Open Local &Zarr..."), this); + connect(_openLocalZarrAct, &QAction::triggered, this, &MenuActionController::openLocalZarr); + _openRemoteAct = new QAction(QObject::tr("Open &Remote Volume..."), this); connect(_openRemoteAct, &QAction::triggered, this, &MenuActionController::openRemoteVolume); @@ -183,6 +186,7 @@ void MenuActionController::populateMenus(QMenuBar* menuBar) // Build menus _fileMenu = new QMenu(QObject::tr("&File"), qWindow); _fileMenu->addAction(_openAct); + _fileMenu->addAction(_openLocalZarrAct); _fileMenu->addAction(_openRemoteAct); _recentMenu = new QMenu(QObject::tr("Open &recent volpkg"), _fileMenu); @@ -350,6 +354,51 @@ void MenuActionController::openVolpkg() _window->UpdateView(); } +void MenuActionController::openLocalZarr() +{ + if (!_window) return; + + QSettings settings(vc3d::settingsFilePath(), QSettings::IniFormat); + QString dir = QFileDialog::getExistingDirectory( + _window, + QObject::tr("Open Local OME-Zarr Directory"), + settings.value(vc3d::settings::volpkg::DEFAULT_PATH).toString(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks | + QFileDialog::ReadOnly | QFileDialog::DontUseNativeDialog); + + if (dir.isEmpty()) return; + + auto path = std::filesystem::path(dir.toStdString()); + + // Validate that this looks like a zarr directory + if (!Volume::checkDir(path)) { + QMessageBox::warning( + _window, QObject::tr("Not a Zarr Volume"), + QObject::tr("The selected directory does not appear to be an " + "OME-Zarr volume (no .zgroup, .zattrs, or meta.json found).")); + return; + } + + try { + auto vol = Volume::New(path); + _window->CloseVolume(); + _window->setVolume(vol); + _window->UpdateView(); + + if (_window->statusBar()) { + _window->statusBar()->showMessage( + QObject::tr("Opened local zarr: %1") + .arg(QString::fromStdString(vol->id())), + 5000); + } + } catch (const std::exception& e) { + QMessageBox::critical( + _window, QObject::tr("Error Opening Zarr"), + QObject::tr("Failed to open zarr volume:\n%1") + .arg(QString::fromStdString(e.what()))); + } +} + void MenuActionController::openRecentVolpkg() { if (!_window) { diff --git a/volume-cartographer/apps/VC3D/MenuActionController.hpp b/volume-cartographer/apps/VC3D/MenuActionController.hpp index cd1db50b1..5ad32cb29 100644 --- a/volume-cartographer/apps/VC3D/MenuActionController.hpp +++ b/volume-cartographer/apps/VC3D/MenuActionController.hpp @@ -33,6 +33,7 @@ class MenuActionController : public QObject private slots: void openVolpkg(); void openRecentVolpkg(); + void openLocalZarr(); void openRemoteVolume(); void openRecentRemoteVolume(); void showSettingsDialog(); @@ -76,6 +77,7 @@ private slots: QMenu* _recentRemoteMenu{nullptr}; QAction* _openAct{nullptr}; + QAction* _openLocalZarrAct{nullptr}; QAction* _openRemoteAct{nullptr}; std::array _recentActs{}; std::array _recentRemoteActs{}; diff --git a/volume-cartographer/core/src/Slicing.cpp b/volume-cartographer/core/src/Slicing.cpp index 5c9419a44..78e4fe991 100644 --- a/volume-cartographer/core/src/Slicing.cpp +++ b/volume-cartographer/core/src/Slicing.cpp @@ -28,6 +28,7 @@ struct CacheParams { int cz, cy, cx, sz, sy, sx; int czShift, cyShift, cxShift, czMask, cyMask, cxMask; + bool pow2; // true when all chunk dims are powers of two (fast path) int chunksZ, chunksY, chunksX; // Chunk index bounds from logical data extent. @@ -42,10 +43,17 @@ struct CacheParams { cz = cs[0]; cy = cs[1]; cx = cs[2]; auto shape = cache->levelShape(level); sz = shape[0]; sy = shape[1]; sx = shape[2]; + auto isPow2 = [](int v) { return v > 0 && (v & (v - 1)) == 0; }; - assert(isPow2(cz) && isPow2(cy) && isPow2(cx) && "Chunk dimensions must be powers of two"); - czShift = log2_pow2(cz); cyShift = log2_pow2(cy); cxShift = log2_pow2(cx); - czMask = cz - 1; cyMask = cy - 1; cxMask = cx - 1; + pow2 = isPow2(cz) && isPow2(cy) && isPow2(cx); + if (pow2) { + czShift = log2_pow2(cz); cyShift = log2_pow2(cy); cxShift = log2_pow2(cx); + czMask = cz - 1; cyMask = cy - 1; cxMask = cx - 1; + } else { + czShift = cyShift = cxShift = 0; + czMask = cyMask = cxMask = 0; + } + chunksZ = (sz + cz - 1) / cz; chunksY = (sy + cy - 1) / cy; chunksX = (sx + cx - 1) / cx; @@ -57,16 +65,36 @@ struct CacheParams { auto db = cache->dataBounds(); if (db.valid) { float scale = 1.0f / static_cast(1 << level); - dbMinCx = std::max(0, (static_cast(std::floor(db.minX * scale)) >> cxShift) - 1); - dbMaxCx = std::min(chunksX - 1, (static_cast(std::ceil(db.maxX * scale)) >> cxShift) + 1); - dbMinCy = std::max(0, (static_cast(std::floor(db.minY * scale)) >> cyShift) - 1); - dbMaxCy = std::min(chunksY - 1, (static_cast(std::ceil(db.maxY * scale)) >> cyShift) + 1); - dbMinCz = std::max(0, (static_cast(std::floor(db.minZ * scale)) >> czShift) - 1); - dbMaxCz = std::min(chunksZ - 1, (static_cast(std::ceil(db.maxZ * scale)) >> czShift) + 1); + dbMinCx = std::max(0, chunkIdx(static_cast(std::floor(db.minX * scale)), cx, cxShift) - 1); + dbMaxCx = std::min(chunksX - 1, chunkIdx(static_cast(std::ceil(db.maxX * scale)), cx, cxShift) + 1); + dbMinCy = std::max(0, chunkIdx(static_cast(std::floor(db.minY * scale)), cy, cyShift) - 1); + dbMaxCy = std::min(chunksY - 1, chunkIdx(static_cast(std::ceil(db.maxY * scale)), cy, cyShift) + 1); + dbMinCz = std::max(0, chunkIdx(static_cast(std::floor(db.minZ * scale)), cz, czShift) - 1); + dbMaxCz = std::min(chunksZ - 1, chunkIdx(static_cast(std::ceil(db.maxZ * scale)), cz, czShift) + 1); dbValid = true; } } + // Chunk index: shift for pow2, divide otherwise + inline int chunkIdx(int v, int c, int shift) const { + return pow2 ? (v >> shift) : (v / c); + } + + // Local offset within chunk: mask for pow2, modulo otherwise + inline int localOff(int v, int c, int mask) const { + return pow2 ? (v & mask) : (v % c); + } + + // Convenience: chunk indices from voxel coords + inline int chunkZ(int iz) const { return pow2 ? (iz >> czShift) : (iz / cz); } + inline int chunkY(int iy) const { return pow2 ? (iy >> cyShift) : (iy / cy); } + inline int chunkX(int ix) const { return pow2 ? (ix >> cxShift) : (ix / cx); } + + // Convenience: local offsets from voxel coords + inline int localZ(int iz) const { return pow2 ? (iz & czMask) : (iz % cz); } + inline int localY(int iy) const { return pow2 ? (iy & cyMask) : (iy % cy); } + inline int localX(int ix) const { return pow2 ? (ix & cxMask) : (ix % cx); } + static int log2_pow2(int v) { int r = 0; while ((v >> r) > 1) r++; @@ -148,20 +176,20 @@ struct ChunkSampler { if (iy >= p.sy) iy = p.sy - 1; if (ix >= p.sx) ix = p.sx - 1; - updateChunk(iz >> p.czShift, iy >> p.cyShift, ix >> p.cxShift); + updateChunk(p.chunkZ(iz), p.chunkY(iy), p.chunkX(ix)); if (!data) return 0; - return data[(iz & p.czMask) * s0 + (iy & p.cyMask) * s1 + (ix & p.cxMask)]; + return data[p.localZ(iz) * s0 + p.localY(iy) * s1 + p.localX(ix)]; } T sampleInt(int iz, int iy, int ix) { if (iz < 0 || iy < 0 || ix < 0 || iz >= p.sz || iy >= p.sy || ix >= p.sx) return 0; - updateChunk(iz >> p.czShift, iy >> p.cyShift, ix >> p.cxShift); + updateChunk(p.chunkZ(iz), p.chunkY(iy), p.chunkX(ix)); if (!data) return 0; - return data[(iz & p.czMask) * s0 + (iy & p.cyMask) * s1 + (ix & p.cxMask)]; + return data[p.localZ(iz) * s0 + p.localY(iy) * s1 + p.localX(ix)]; } float sampleTrilinear(float vz, float vy, float vx) { @@ -203,11 +231,9 @@ struct ChunkSampler { iz + 1 >= p.sz || iy + 1 >= p.sy || ix + 1 >= p.sx) return sampleTrilinear(vz, vy, vx); - // Local copies of shifts for register allocation - const int lczShift = p.czShift, lcyShift = p.cyShift, lcxShift = p.cxShift; - int ciz0 = iz >> lczShift, ciz1 = (iz + 1) >> lczShift; - int ciy0 = iy >> lcyShift, ciy1 = (iy + 1) >> lcyShift; - int cix0 = ix >> lcxShift, cix1 = (ix + 1) >> lcxShift; + int ciz0 = p.chunkZ(iz), ciz1 = p.chunkZ(iz + 1); + int ciy0 = p.chunkY(iy), ciy1 = p.chunkY(iy + 1); + int cix0 = p.chunkX(ix), cix1 = p.chunkX(ix + 1); if (ciz0 == ciz1 && ciy0 == ciy1 && cix0 == cix1) { updateChunk(ciz0, ciy0, cix0); @@ -215,8 +241,7 @@ struct ChunkSampler { // Local stride copies — s2 is always 1, eliminated const size_t ls0 = s0, ls1 = s1; - const int lczMask = p.czMask, lcyMask = p.cyMask, lcxMask = p.cxMask; - int lz0 = iz & lczMask, ly0 = iy & lcyMask, lx0 = ix & lcxMask; + int lz0 = p.localZ(iz), ly0 = p.localY(iy), lx0 = p.localX(ix); int lz1 = lz0 + 1, ly1 = ly0 + 1, lx1 = lx0 + 1; float c000 = data[lz0*ls0 + ly0*ls1 + lx0]; @@ -317,9 +342,9 @@ static void readVolumeImpl( if (iz >= p.sz) iz = p.sz - 1; if (iy >= p.sy) iy = p.sy - 1; if (ix >= p.sx) ix = p.sx - 1; - int ciz = iz >> p.czShift; - int ciy = iy >> p.cyShift; - int cix = ix >> p.cxShift; + int ciz = p.chunkZ(iz); + int ciy = p.chunkY(iy); + int cix = p.chunkX(ix); // Skip chunks in zero-padded regions if (p.dbValid && (ciz < p.dbMinCz || ciz > p.dbMaxCz || ciy < p.dbMinCy || ciy > p.dbMaxCy || @@ -731,9 +756,9 @@ static void readMultiSliceImpl( auto markVoxel = [&](int iz, int iy, int ix) { if (iz < 0 || iy < 0 || ix < 0 || iz >= p.sz || iy >= p.sy || ix >= p.sx) return; - int ciz = iz >> p.czShift; - int ciy = iy >> p.cyShift; - int cix = ix >> p.cxShift; + int ciz = p.chunkZ(iz); + int ciy = p.chunkY(iy); + int cix = p.chunkX(ix); size_t idx = static_cast(ciz) * p.chunksY * p.chunksX + ciy * p.chunksX + cix; if (!needed[idx]) { needed[idx] = 1; @@ -793,8 +818,6 @@ static void readMultiSliceImpl( constexpr float maxVal = std::is_same_v ? 65535.f : 255.f; const float firstOff = offsets[0]; const float lastOff = offsets[numSlices - 1]; - const int lczShift = p.czShift, lcyShift = p.cyShift, lcxShift = p.cxShift; - const int lczMask = p.czMask, lcyMask = p.cyMask, lcxMask = p.cxMask; const int lsz = p.sz, lsy = p.sy, lsx = p.sx; { @@ -833,14 +856,14 @@ static void readMultiSliceImpl( izMin >= 0 && iyMin >= 0 && ixMin >= 0; bool singleChunk = allInBounds && - (izMin >> lczShift) == (izMax >> lczShift) && - (iyMin >> lcyShift) == (iyMax >> lcyShift) && - (ixMin >> lcxShift) == (ixMax >> lcxShift); + p.chunkZ(izMin) == p.chunkZ(izMax) && + p.chunkY(iyMin) == p.chunkY(iyMax) && + p.chunkX(ixMin) == p.chunkX(ixMax); if (singleChunk) { - int ciz = izMin >> lczShift; - int ciy = iyMin >> lcyShift; - int cix = ixMin >> lcxShift; + int ciz = p.chunkZ(izMin); + int ciy = p.chunkY(iyMin); + int cix = p.chunkX(ixMin); sampler.updateChunk(ciz, ciy, cix); const T* __restrict__ d = sampler.data; if (!d) continue; @@ -855,7 +878,7 @@ static void readMultiSliceImpl( int iy = static_cast(vy); int ix = static_cast(vx); - size_t base = (iz & lczMask)*ls0 + (iy & lcyMask)*ls1 + (ix & lcxMask); + size_t base = p.localZ(iz)*ls0 + p.localY(iy)*ls1 + p.localX(ix); float c000 = d[base]; float c100 = d[base + ls0]; float c010 = d[base + ls1]; @@ -960,9 +983,9 @@ static void sampleTileSlicesImpl( seen.reserve(16); auto markVoxel = [&](int iz, int iy, int ix) { if (iz < 0 || iy < 0 || ix < 0 || iz >= p.sz || iy >= p.sy || ix >= p.sx) return; - int ciz = iz >> p.czShift; - int ciy = iy >> p.cyShift; - int cix = ix >> p.cxShift; + int ciz = p.chunkZ(iz); + int ciy = p.chunkY(iy); + int cix = p.chunkX(ix); uint64_t key = (uint64_t(ciz) << 40) | (uint64_t(ciy) << 20) | uint64_t(cix); if (!seen.insert(key).second) return; neededChunks.push_back({ciz, ciy, cix}); @@ -1000,8 +1023,6 @@ static void sampleTileSlicesImpl( constexpr float maxVal = std::is_same_v ? 65535.f : 255.f; const float firstOff = offsets[0]; const float lastOff = offsets[numSlices - 1]; - const int lczShift = p.czShift, lcyShift = p.cyShift, lcxShift = p.cxShift; - const int lczMask = p.czMask, lcyMask = p.cyMask, lcxMask = p.cxMask; const int lsz = p.sz, lsy = p.sy, lsx = p.sx; ChunkSampler sampler(p, cache, level); @@ -1039,14 +1060,14 @@ static void sampleTileSlicesImpl( izMin >= 0 && iyMin >= 0 && ixMin >= 0; bool singleChunk = allInBounds && - (izMin >> lczShift) == (izMax >> lczShift) && - (iyMin >> lcyShift) == (iyMax >> lcyShift) && - (ixMin >> lcxShift) == (ixMax >> lcxShift); + p.chunkZ(izMin) == p.chunkZ(izMax) && + p.chunkY(iyMin) == p.chunkY(iyMax) && + p.chunkX(ixMin) == p.chunkX(ixMax); if (singleChunk) { - int ciz = izMin >> lczShift; - int ciy = iyMin >> lcyShift; - int cix = ixMin >> lcxShift; + int ciz = p.chunkZ(izMin); + int ciy = p.chunkY(iyMin); + int cix = p.chunkX(ixMin); sampler.updateChunk(ciz, ciy, cix); const T* __restrict__ d = sampler.data; if (!d) continue; @@ -1061,7 +1082,7 @@ static void sampleTileSlicesImpl( int iy = static_cast(vy); int ix = static_cast(vx); - size_t base = (iz & lczMask)*ls0 + (iy & lcyMask)*ls1 + (ix & lcxMask); + size_t base = p.localZ(iz)*ls0 + p.localY(iy)*ls1 + p.localX(ix); float c000 = d[base]; float c100 = d[base + ls0]; float c010 = d[base + ls1]; From 36844abc5909642602ab6419135139431f41a868 Mon Sep 17 00:00:00 2001 From: SuperOptimizer <156155735+SuperOptimizer@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:19:07 -0600 Subject: [PATCH 2/3] bug fixes for enable editing button, r key fix, reload surfaces for remote volume --- volume-cartographer/apps/VC3D/CWindow.cpp | 46 +++++++++++++++++-- .../apps/VC3D/SurfacePanelController.cpp | 5 ++ .../apps/VC3D/SurfacePanelController.hpp | 1 + volume-cartographer/apps/VC3D/VCMain.ui | 4 +- .../VC3D/segmentation/SegmentationWidget.cpp | 1 + .../apps/VC3D/tiled/CTiledVolumeViewer.hpp | 5 +- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/volume-cartographer/apps/VC3D/CWindow.cpp b/volume-cartographer/apps/VC3D/CWindow.cpp index f3c1ef7a0..551957bcc 100644 --- a/volume-cartographer/apps/VC3D/CWindow.cpp +++ b/volume-cartographer/apps/VC3D/CWindow.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -731,11 +732,20 @@ CWindow::CWindow(size_t cacheSizeGB) : settings.sync(); }); } + // Ensure right-side tabified docks have a usable minimum size + for (QDockWidget* dock : { ui.dockWidgetSegmentation, + ui.dockWidgetDistanceTransform, + ui.dockWidgetDrawing }) { + if (dock) { + dock->setMinimumWidth(250); + dock->setMinimumHeight(120); + } + } if (!restoredState) { // No saved state - set sensible default sizes for dock widgets - // The Volume Package dock (left side) should have a reasonable width and height resizeDocks({ui.dockWidgetVolumes}, {300}, Qt::Horizontal); resizeDocks({ui.dockWidgetVolumes}, {400}, Qt::Vertical); + resizeDocks({ui.dockWidgetSegmentation}, {350}, Qt::Horizontal); } for (QDockWidget* dock : { ui.dockWidgetSegmentation, @@ -1683,10 +1693,11 @@ bool CWindow::attachVolumeToCurrentPackage(const std::shared_ptr& volume return false; } + const bool needSurfaceLoad = _surfacePanel && !_surfacePanel->hasSurfaces(); refreshCurrentVolumePackageUi(preferredVolumeId.isEmpty() ? QString::fromStdString(volume->id()) : preferredVolumeId, - false); + needSurfaceLoad); UpdateView(); return true; } @@ -1869,10 +1880,32 @@ bool CWindow::centerFocusAt(const cv::Vec3f& position, const cv::Vec3f& normal, bool CWindow::centerFocusOnCursor() { - if (!_state) { + if (!_state || !mdiArea) { return false; } + // Get fresh volume position from the active viewer's current cursor + // location, rather than relying on the stale "cursor" POI which is only + // updated on mouse move and becomes outdated after the view shifts. + auto* subWindow = mdiArea->activeSubWindow(); + if (subWindow) { + if (auto* viewer = qobject_cast(subWindow->widget())) { + auto* gv = viewer->fGraphicsView; + if (gv && gv->viewport()) { + QPoint globalPos = QCursor::pos(); + QPoint viewportPos = gv->viewport()->mapFromGlobal(globalPos); + if (gv->viewport()->rect().contains(viewportPos)) { + QPointF scenePos = gv->mapToScene(viewportPos); + cv::Vec3f p, n; + if (viewer->sceneToVolumePN(p, n, scenePos)) { + return centerFocusAt(p, n, viewer->surfName(), true); + } + } + } + } + } + + // Fallback to stored cursor POI if no active viewer or cursor is outside POI* cursor = _state->poi("cursor"); if (!cursor) { return false; @@ -2104,6 +2137,11 @@ void CWindow::CreateWidgets(void) return; } + // Delete any existing widget from the .ui file to prevent ghosting + if (auto* oldWidget = dock->widget()) { + delete oldWidget; + } + auto* container = new QWidget(dock); container->setObjectName(objectName); auto* layout = new QVBoxLayout(container); @@ -2113,6 +2151,7 @@ void CWindow::CreateWidgets(void) layout->addStretch(1); auto* scrollArea = new QScrollArea(dock); + scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setWidgetResizable(true); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); @@ -2132,6 +2171,7 @@ void CWindow::CreateWidgets(void) _segmentationWidget->setNormalGridPathHint(initialHint); attachScrollAreaToDock(ui.dockWidgetSegmentation, _segmentationWidget, QStringLiteral("dockWidgetSegmentationContent")); + _segmentationEdit = std::make_unique(this); _segmentationEdit->setViewerManager(_viewerManager.get()); _segmentationOverlay = std::make_unique(_state, this); diff --git a/volume-cartographer/apps/VC3D/SurfacePanelController.cpp b/volume-cartographer/apps/VC3D/SurfacePanelController.cpp index 70edae6c5..a618c681c 100644 --- a/volume-cartographer/apps/VC3D/SurfacePanelController.cpp +++ b/volume-cartographer/apps/VC3D/SurfacePanelController.cpp @@ -105,6 +105,11 @@ void SurfacePanelController::clear() } } +bool SurfacePanelController::hasSurfaces() const +{ + return _ui.treeWidget && _ui.treeWidget->topLevelItemCount() > 0; +} + void SurfacePanelController::loadSurfaces(bool reload) { if (!_volumePkg) { diff --git a/volume-cartographer/apps/VC3D/SurfacePanelController.hpp b/volume-cartographer/apps/VC3D/SurfacePanelController.hpp index 3dff4a3af..8e2376825 100644 --- a/volume-cartographer/apps/VC3D/SurfacePanelController.hpp +++ b/volume-cartographer/apps/VC3D/SurfacePanelController.hpp @@ -82,6 +82,7 @@ class SurfacePanelController : public QObject void setVolumePkg(const std::shared_ptr& pkg); void clear(); + bool hasSurfaces() const; void loadSurfaces(bool reload); void loadSurfacesIncremental(); diff --git a/volume-cartographer/apps/VC3D/VCMain.ui b/volume-cartographer/apps/VC3D/VCMain.ui index 6c9654af3..3f37e1526 100644 --- a/volume-cartographer/apps/VC3D/VCMain.ui +++ b/volume-cartographer/apps/VC3D/VCMain.ui @@ -465,8 +465,8 @@ - 0 - 0 + 250 + 120 diff --git a/volume-cartographer/apps/VC3D/segmentation/SegmentationWidget.cpp b/volume-cartographer/apps/VC3D/segmentation/SegmentationWidget.cpp index 1ef75613c..3a8e8f02c 100644 --- a/volume-cartographer/apps/VC3D/segmentation/SegmentationWidget.cpp +++ b/volume-cartographer/apps/VC3D/segmentation/SegmentationWidget.cpp @@ -57,6 +57,7 @@ void SegmentationWidget::buildUi() layout->addWidget(_neuralTracerPanel); _lasagnaPanel = new SegmentationLasagnaPanel(QStringLiteral("segmentation_edit"), this); + _lasagnaPanel->setVisible(false); // Not in layout; hosted in a separate dock when available _correctionsPanel = new SegmentationCorrectionsPanel(QStringLiteral("segmentation_edit"), this); layout->addWidget(_correctionsPanel); diff --git a/volume-cartographer/apps/VC3D/tiled/CTiledVolumeViewer.hpp b/volume-cartographer/apps/VC3D/tiled/CTiledVolumeViewer.hpp index 4f5d566ce..b15740569 100644 --- a/volume-cartographer/apps/VC3D/tiled/CTiledVolumeViewer.hpp +++ b/volume-cartographer/apps/VC3D/tiled/CTiledVolumeViewer.hpp @@ -172,6 +172,8 @@ class CTiledVolumeViewer : public QWidget, public VolumeViewerBase QPointF volumeToScene(const cv::Vec3f& vol_point); // Transform from canvas scene coordinates to volume (world) coordinates cv::Vec3f sceneToVolume(const QPointF& scenePoint) const; + // Scene-to-volume coordinate conversion (returns position + normal) + bool sceneToVolumePN(cv::Vec3f& p, cv::Vec3f& n, const QPointF& scenePos) const; QPointF lastScenePosition() const { return _lastScenePos; } // --- BBox tool --- @@ -245,9 +247,6 @@ public slots: // Build TileRenderParams for a given tile key TileRenderParams buildRenderParams(const WorldTileKey& wk) const; - // Scene-to-volume coordinate conversion (returns position + normal) - bool sceneToVolumePN(cv::Vec3f& p, cv::Vec3f& n, const QPointF& scenePos) const; - void markActiveSegmentationDirty(); // Mark overlays dirty on the render controller AND notify external listeners From cf63f528fde051d4cb77b43addea052dca993b04 Mon Sep 17 00:00:00 2001 From: SuperOptimizer <156155735+SuperOptimizer@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:51:28 -0600 Subject: [PATCH 3/3] Set TIFF DPI from volume voxel size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Derive DPI from voxelsize metadata (25400 µm/inch ÷ voxelSize µm = DPI) and embed it in TIFF output via XRESOLUTION/YRESOLUTION tags. - Add voxelSizeToDpi() helper and optional dpi parameter to writeTiff/TiffWriter - Propagate resolution tags through mergeTiffParts - Set dpi on QuadSurface from tracer/grower voxelsize - Read voxelsize from meta.json in vc_zarr_to_tiff and vc_render_tifxyz Co-Authored-By: Claude Opus 4.6 --- .../apps/src/vc_render_tifxyz.cpp | 18 +++++++- .../apps/src/vc_zarr_to_tiff.cpp | 24 ++++++++--- .../core/include/vc/core/util/QuadSurface.hpp | 5 +++ .../core/include/vc/core/util/Tiff.hpp | 42 ++++++++++++------- volume-cartographer/core/src/GrowPatch.cpp | 1 + volume-cartographer/core/src/GrowSurface.cpp | 1 + volume-cartographer/core/src/QuadSurface.cpp | 10 ++--- volume-cartographer/core/src/Tiff.cpp | 38 ++++++++++------- 8 files changed, 98 insertions(+), 41 deletions(-) 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);