@@ -4,12 +4,9 @@ import com.pivovarit.function.ThrowingConsumer
44import gnu.trove.map.TLongLongMap
55import gnu.trove.map.hash.TLongLongHashMap
66import io.github.oshai.kotlinlogging.KotlinLogging
7- import net.imglib2.FinalInterval
87import net.imglib2.Interval
98import net.imglib2.RandomAccessibleInterval
109import net.imglib2.algorithm.util.Grids
11- import net.imglib2.img.array.ArrayImgs
12- import net.imglib2.loops.LoopBuilder
1310import net.imglib2.algorithm.util.Singleton
1411import net.imglib2.algorithm.util.Singleton.ThrowingSupplier
1512import net.imglib2.converter.Converter
@@ -36,7 +33,9 @@ import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadataPars
3633import org.janelia.saalfeldlab.n5.zarr.ZarrKeyValueWriter
3734import org.janelia.saalfeldlab.n5.zarr.v3.ZarrV3DatasetAttributes
3835import org.janelia.saalfeldlab.n5.zarr.v3.ZarrV3KeyValueWriter
36+ import org.janelia.saalfeldlab.n5.spark.downsample.N5LabelDownsamplerSpark
3937import org.janelia.saalfeldlab.n5.spark.supplier.N5ReaderSupplier
38+ import org.janelia.saalfeldlab.n5.spark.util.ShardedBlocks
4039import org.janelia.saalfeldlab.n5.spark.supplier.N5WriterSupplier
4140import org.janelia.scicomp.n5.zstandard.ZstandardCompression
4241import picocli.CommandLine
@@ -70,9 +69,11 @@ object ExtractHighestResolutionLabelDataset {
7069 considerFragmentSegmentAssignment : Boolean ,
7170 assignment : TLongLongMap ,
7271 xyzUnit : Array <String >,
73- chunksPerShard : IntArray? = null
72+ chunksPerShard : IntArray? = null,
73+ scales : Array <IntArray > = emptyArray(),
74+ downsampleBlockSizes : Array <IntArray > = emptyArray()
7475 ) {
75- extract(sc, n5in, n5out, datasetIn, datasetOut, blockSizeOut, considerFragmentSegmentAssignment, assignment, xyzUnit, chunksPerShard)
76+ extract(sc, n5in, n5out, datasetIn, datasetOut, blockSizeOut, considerFragmentSegmentAssignment, assignment, xyzUnit, chunksPerShard, scales, downsampleBlockSizes )
7677 }
7778
7879 @JvmStatic
@@ -87,7 +88,9 @@ object ExtractHighestResolutionLabelDataset {
8788 considerFragmentSegmentAssignment : Boolean ,
8889 assignment : TLongLongMap ,
8990 xyzUnit : Array <String >,
90- chunksPerShard : IntArray? = null
91+ chunksPerShard : IntArray? = null,
92+ scales : Array <IntArray > = emptyArray(),
93+ downsampleBlockSizes : Array <IntArray > = emptyArray()
9194 ) where IN : NativeType<IN>?, IN : IntegerType<IN>? {
9295 extract<IN , UnsignedLongType >(
9396 sc,
@@ -103,7 +106,9 @@ object ExtractHighestResolutionLabelDataset {
103106 considerFragmentSegmentAssignment,
104107 assignment,
105108 xyzUnit,
106- chunksPerShard
109+ chunksPerShard,
110+ scales,
111+ downsampleBlockSizes
107112 )
108113 }
109114
@@ -120,7 +125,9 @@ object ExtractHighestResolutionLabelDataset {
120125 considerFragmentSegmentAssignment : Boolean ,
121126 assignment : TLongLongMap ,
122127 xyzUnit : Array <String >,
123- chunksPerShard : IntArray? = null
128+ chunksPerShard : IntArray? = null,
129+ scales : Array <IntArray > = emptyArray(),
130+ downsampleBlockSizes : Array <IntArray > = emptyArray()
124131 ) where IN : NativeType<IN>?, IN : IntegerType<IN>?, OUT : NativeType<OUT>?, OUT : IntegerType<OUT>? {
125132 val n5InLocal = n5in.get()
126133 if (! n5InLocal.exists(datasetIn)) {
@@ -140,7 +147,7 @@ object ExtractHighestResolutionLabelDataset {
140147 }
141148 extract(
142149 sc, n5in, n5out, " $datasetIn /data" , datasetOut, blockSizeOut, outputTypeSupplier, updatedAdditionalEntries,
143- considerFragmentSegmentAssignment, assignment, xyzUnit, chunksPerShard
150+ considerFragmentSegmentAssignment, assignment, xyzUnit, chunksPerShard, scales, downsampleBlockSizes
144151 )
145152 return
146153 } catch (e: NoValidDatasetException ) {
@@ -150,7 +157,7 @@ object ExtractHighestResolutionLabelDataset {
150157 try {
151158 extract(
152159 sc, n5in, n5out, " $datasetIn /s0" , datasetOut, blockSizeOut, outputTypeSupplier, additionalAttributes,
153- considerFragmentSegmentAssignment, assignment, xyzUnit, chunksPerShard
160+ considerFragmentSegmentAssignment, assignment, xyzUnit, chunksPerShard, scales, downsampleBlockSizes
154161 )
155162 return
156163 } catch (e: NoValidDatasetException ) {
@@ -186,24 +193,33 @@ object ExtractHighestResolutionLabelDataset {
186193 val keys = assignment.keys()
187194 val values = assignment.values()
188195
189- val resolution = n5InLocal.getAttribute(datasetIn, " resolution" , DoubleArray ::class .java)
190- val offset = n5InLocal.getAttribute(datasetIn, " offset" , DoubleArray ::class .java)
191- /* resolution/offset become the OME-NGFF scale/translation transforms on the multiscale group */
196+ val resolution = n5InLocal.getAttribute(datasetIn, " resolution" , DoubleArray ::class .java) ? : DoubleArray (dimensions.size) { 1.0 }
197+ val offset = n5InLocal.getAttribute(datasetIn, " offset" , DoubleArray ::class .java) ? : DoubleArray (dimensions.size) { 0.0 }
198+ /* resolution/offset become the OME-NGFF scale/translation transforms; one dataset per pyramid level, with the
199+ * per-level scale accumulating the relative downsampling factors */
192200 val axes = arrayOf(
193201 Axis (Axis .SPACE , " x" , xyzUnit[0 ], false ),
194202 Axis (Axis .SPACE , " y" , xyzUnit[1 ], false ),
195203 Axis (Axis .SPACE , " z" , xyzUnit[2 ], false )
196204 )
205+ val scalePaths = Array (scales.size + 1 ) { " s$it " }
206+ val levelResolutions = arrayListOf (resolution.copyOf())
207+ val levelTranslations = arrayListOf (offset.copyOf())
208+ val cumulativeFactor = DoubleArray (dimensions.size) { 1.0 }
209+ scales.forEach { factor ->
210+ for (d in cumulativeFactor.indices) cumulativeFactor[d] = cumulativeFactor[d] * factor[d]
211+ levelResolutions.add(DoubleArray (dimensions.size) { resolution[it] * cumulativeFactor[it] })
212+ levelTranslations.add(offset.copyOf())
213+ }
197214 val metadata = OmeNgffMetadata .buildForWriting(
198215 dimensions.size,
199216 datasetOut,
200217 ngffVersion,
201218 axes,
202- arrayOf( " s0 " ) ,
203- arrayOf(resolution ? : DoubleArray (dimensions.size) { 1.0 } ),
204- arrayOf(offset ? : DoubleArray (dimensions.size) { 0.0 } )
219+ scalePaths ,
220+ levelResolutions.toTypedArray( ),
221+ levelTranslations.toTypedArray( )
205222 )
206- OmeNgffMetadataParser (outWriter).writeMetadata(metadata, outWriter, datasetOut)
207223
208224 n5InLocal.getAttribute(datasetIn, " maxId" , Long ::class .javaPrimitiveType)?.let { maxId ->
209225 outWriter.setAttribute(dataDataset, " maxId" , maxId)
@@ -267,29 +283,11 @@ object ExtractHighestResolutionLabelDataset {
267283 val attributes = writer.getDatasetAttributes(dataDataset)
268284
269285 val fillValue = (attributes as ? ZarrV3DatasetAttributes )?.run { ByteBuffer .wrap(fillBytes).long } ? : 0L
270- var blockIsEmpty = true
271286 if (attributes.isSharded) {
272- /* materialize a full shard-sized block (0-padded past the edge) and write it at the shard grid position.*/
273- val shardBlockSize = attributes.blockSize
274- val data = LongArray (shardBlockSize.fold(1 ) { acc, s -> acc * s })
275- val shardImg = ArrayImgs .unsignedLongs(data, * shardBlockSize.map { it.toLong() }.toLongArray())
276- LoopBuilder .setImages(
277- Views .zeroMin(converted),
278- Views .interval(shardImg, FinalInterval (* Intervals .dimensionsAsLongArray(converted)))
279- ).forEachPixel { source, target ->
280- val value = source!! .integerLong
281- target.setInteger(value)
282- /* set blockIsEmpty = false first value that is not the fill value. */
283- if (blockIsEmpty && value != fillValue)
284- blockIsEmpty = false
285- }
286- if (! blockIsEmpty)
287- writer.writeBlock(
288- dataDataset,
289- attributes,
290- LongArrayDataBlock (shardBlockSize, blockWithPosition._2 (), data)
291- )
287+ val fillType = outputTypeSupplier.get()!! .also { it.setInteger(fillValue) }
288+ ShardedBlocks .saveNonEmptyShard(writer, dataDataset, attributes, blockWithPosition._2 (), converted, fillType)
292289 } else {
290+ var blockIsEmpty = true
293291 val blockDims = Intervals .dimensionsAsIntArray(converted)
294292 val data = LongArray (Intervals .numElements(converted).toInt())
295293 val cursor = Views .flatIterable(converted).cursor()
@@ -308,6 +306,20 @@ object ExtractHighestResolutionLabelDataset {
308306 )
309307 }
310308 }
309+
310+ /* build the downsampled pyramid s1..sN from s0; each level is sharded when chunksPerShard is set */
311+ var previousLevel = dataDataset
312+ scales.forEachIndexed { level, factor ->
313+ val nextLevel = " $datasetOut /s${level + 1 } "
314+ N5LabelDownsamplerSpark .downsampleLabel<UnsignedLongType >(
315+ sc, n5out, previousLevel, nextLevel, factor,
316+ downsampleBlockSizes.getOrElse(level) { blockSize }, chunksPerShard, false
317+ )
318+ previousLevel = nextLevel
319+ }
320+
321+ /* write the multiscale metadata once every level exists */
322+ OmeNgffMetadataParser (outWriter).writeMetadata(metadata, outWriter, datasetOut)
311323 }
312324
313325 private fun <IN : IntegerType <IN >? , OUT : IntegerType <OUT >? > getAppropriateConverter (map : TLongLongMap ? ): Converter <IN , OUT > {
0 commit comments