How a folder of tiles becomes a pyramidal OME-TIFF or OME-ZARR. The TileConfigurationTxt
strategy is used as the running example; the others differ only in step 3.
MenuStartup -> StitchingGUI -> StitchingWorkflow.runDetailed(config)
|
+-- StitchingStrategyFactory.getStrategy(config)
+-- strategy.prepareStitching(...) -> List<TileMapping> (nominal positions)
+-- TileRegistrationStep.applyTo(...) -> List<TileMapping> (corrected; no-op by default)
+-- group by subdirName
+-- per subdir: DirectTileStitcher.stitch(...)
+-- TileSpatialIndex (positions only, no pixels)
+-- ChunkCompositor (reads tile sub-regions on demand)
+-- OME-TIFF: CompositorImageServer -> PyramidImageWriter -> DirectTiffOutputWriter
OME-ZARR: ZarrOutputWriter chunk loop -> PyramidLevelGenerator
MenuStartup.java registers the menu item that opens the dialog.
functions/StitchingGUI.java collects the folder, output format, compression, pixel size and
downsample, and builds a StitchingConfig. For MicroManager datasets it also offers a
"Try calculating pixel size..." button, which measures pixel size from tile overlap by normalized
cross-correlation rather than trusting the metadata.
stitching/StitchingStrategy.java has one method:
List<TileMapping> prepareStitching(String folderPath, double pixelSizeInMicrons,
double baseDownsample, String matchingString);Implementations:
| Strategy | Positions come from |
|---|---|
TileConfigurationTxtStrategy |
TileConfiguration.txt (stage microns); z/t from z{zz}/t{tt} directory names |
FileNameStitchingStrategy |
coordinates embedded in the filename |
VectraMetadataStrategy |
Vectra TIFF metadata |
MicroManagerMetadataStrategy |
MicroManager sidecar JSON (XPositionUm/YPositionUm) |
Each returns TileMapping(file, region, subdirName, seriesIndex), where region is an
ImageRegion in output-pixel space (stage microns divided by pixel size, with any
flipStitchingX/flipStitchingY already applied).
workflow/TileRegistrationStep.java. A no-op unless the caller sets a RegistrationMode on
the config.
Stage coordinates are nominal: real stages have backlash, finite encoder resolution, and thermal drift across a long acquisition. Registration measures where neighbouring tiles actually line up, by correlating the content in their overlap, and solves for globally consistent corrections.
Three modes:
| Mode | Behaviour |
|---|---|
Disabled (default) |
place tiles at nominal stage positions |
Solve(out, settings, reference) |
measure the reference subdirectory, solve, write TileRegistration.txt, apply |
Apply(in) |
reuse a previous solve |
Why two active modes. Polarization angles and fluorescence channels are captured at the same stage position for a given tile. Solving each independently would give each its own corrections and misregister the angles against each other -- worse than leaving them all on a shared nominal grid. So exactly one subdirectory is solved and every sibling reuses that result. The solution file is also durable: a re-stitch can reuse a solve instead of repeating it, and it can be inspected when a mosaic looks wrong.
Corrections are applied in memory. TileConfiguration.txt is never rewritten, so it stays the
nominal record and re-running is idempotent by construction.
See registration/ for the engine: NeighborGraphBuilder (4-connected grid; derives the overlap
from the nominal step rather than being told it), CoarseToFineNccRegistrar (bounded correlation
search behind the PairwiseRegistrar interface), GlobalPositionSolver (weighted least-squares
over all edges, plus a pull toward nominal), TileRegistrationSolution (the file format, whose
header refuses to be applied to a run it was not solved for).
assembly/direct/DirectTileStitcher.java. Every tile count routes through here.
The design constraint is bounded memory: roughly 40 MB regardless of tile count, against the
2-4+ GB the retired SparseImageServer path needed. Three mechanisms hold that:
TileSpatialIndexholds onlyTileMappingreferences -- a file handle and a rectangle. No pixels. Tiles are bucketed into chunk-sized cells and translated so the image starts at (0, 0).TileReaderPoolkeeps at most 64ImageReaders open, LRU-evicting beyond that, and reads sub-regions viaImageReadParam.setSourceRegionso only the pixels a chunk needs are decoded.getDimensionsreads the header without decoding pixels at all.- Streaming writes. There is never a full-image buffer. Zarr composites one 1024x1024 chunk,
writes it, and discards it. OME-TIFF wraps the compositor in
CompositorImageServer, which composites on demand as the writer pulls tiles.
ChunkCompositor.compositeChunk is where pixels land: query the index, allocate one chunk buffer,
and for each intersecting tile read its sub-region and transfer it in. Overlaps currently resolve
last-writer-wins.
| Format | Path |
|---|---|
| OME-TIFF | CompositorImageServer to PyramidImageWriter.write to DirectTiffOutputWriter (Bio-Formats TiffWriter, explicit clamped tile loop) |
| OME-ZARR | ZarrOutputWriter.writeChunk loop to PyramidLevelGenerator (2x2 box downsample of the level already written) |
Writes are serial by design: Bio-Formats TiffWriter is not thread-safe, and PyramidImageWriter
holds a global semaphore around OME-TIFF writes.
ChannelMerger.merge combines separately-stitched single-channel outputs into one multichannel
image via ChannelMergeImageServer. Callers that split channels import them individually instead.