Skip to content

Commit c98afe9

Browse files
authored
Merge pull request #63 from saalfeldlab/1.3.1
1.3.1
2 parents 3620f22 + eea34fc commit c98afe9

7 files changed

Lines changed: 180 additions & 35 deletions

File tree

.fileglancer/runnables.yaml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,25 +69,31 @@ runnables:
6969
name: Scale factors
7070
type: string
7171
required: true
72-
default: "2,2,2 2,2,2"
73-
description: >-
74-
Relative downsampling factor per level as whitespace-separated x,y,z triples
75-
(a single integer u means u,u,u). Example: `2,2,1 2,2,1 2`.
72+
default: "2 2"
73+
description: |
74+
Relative downsampling factor per level as whitespace-separated x,y,z triples (a single integer u means u,u,u).
75+
Example: `2,2,1 2,2,1 2`.
7676
- flag: --block-size
7777
name: Block size
7878
type: string
79-
default: "64,64,64"
79+
default: "32,32,32"
8080
- flag: --reverse-array-attributes
8181
name: Reverse array attributes
82+
hidden: true
8283
type: boolean
8384
default: false
84-
description: "Reverse resolution/offset arrays, i.e. [x,y,z] -> [z,y,x]. NOTE: N5-Zarr interop generally handles this automatically, especially for Zarr3. Only Reverse if you are sure it's necessary."
85+
description: |
86+
Reverse resolution/offset arrays, i.e. [x,y,z] -> [z,y,x].
87+
NOTE: N5-Zarr interop generally handles this automatically, especially for Zarr3.
88+
Only Reverse if you are sure it's necessary.
8589
- flag: --winner-takes-all-downsampling
8690
name: Winner-takes-all downsampling
8791
hidden: true
8892
type: boolean
8993
default: false
90-
description: "Use a scalar label type with winner-takes-all downsampling. If 'false' or omitted, data will be LabelMultisetType"
94+
description: |
95+
In most cases this should be left "false" or omitted, which defaults the conversion to LabelMultisetType, which is useful for Paintera.
96+
If true, uses a scalar label type with winner-takes-all downsampling.
9197
- flag: --overwrite-existing
9298
name: Overwrite existing
9399
hidden: true
@@ -145,12 +151,13 @@ runnables:
145151
- ZARR3
146152
required: false
147153
default: ZARR
148-
description: >-
154+
description: |
149155
Output storage format of the generated dataset. Explicit support for N5, ZARR2, and ZARR3.
150156
ZARR without the version will default to ZARR3 if no container exists, but if a container
151-
exists, it will keep the version of the container. All zarr containers contain OME-Zarr
152-
metadata, and ZARR3 supports sharding. If left unset, the format is taken from the output
153-
container's path, else inferred on write.
157+
exists, it will keep the version of the container.
158+
All zarr containers contain OME-Zarr metadata.
159+
ZARR3 supports sharding.
160+
If not provided a best-attempt is made to guess the format from the path, existing contain, or lastly a fallback to ZARR3.
154161
- flag: --scale
155162
name: Scale factors
156163
type: string

src/main/kotlin/org/janelia/saalfeldlab/conversion/DatasetInfo.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ data class DatasetInfo(
1414
val inputContainer: String,
1515
val inputDataset: String,
1616
val outputContainer: URI,
17-
val outputFormat: StorageFormat? = null,
17+
/* Paintera datasets are currently N5-only; the label multiset type and the adjacent index dataset are varlen */
18+
val outputFormat: StorageFormat = StorageFormat.N5,
1819
val outputGroup: String = inputDataset
1920
) : Serializable {
2021

22+
/* label-utilities-spark opens containers from a plain string; qualify it with the
23+
* storage scheme so it cannot guess a different format than the one we write with */
24+
val outputContainerUri: String
25+
get() = "${outputFormat.name.lowercase()}:$outputContainer"
26+
2127
val type: String
2228
get() {
2329
val attrs = attributes

src/main/kotlin/org/janelia/saalfeldlab/conversion/to/paintera/DatasetConverterLabel.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ private fun handleLabelDataset(
141141

142142
val maxId = ExtractUniqueLabelsPerBlock.extractUniqueLabels(
143143
sc,
144-
info.outputContainer.toString(),
145-
info.outputContainer.toString(),
144+
info.outputContainerUri,
145+
info.outputContainerUri,
146146
originalResolutionOutputDataset,
147147
Paths.get(uniqueLabelsGroup, "s0").toString()
148148
)
@@ -153,7 +153,7 @@ private fun handleLabelDataset(
153153
if (scales.isNotEmpty())
154154
// TODO refactor this to be nicer
155155
{
156-
LabelListDownsampler.donwsampleMultiscale(sc, info.outputContainer.toString(), uniqueLabelsGroup, scales, downsampleBlockSizes)
156+
LabelListDownsampler.donwsampleMultiscale(sc, info.outputContainerUri, uniqueLabelsGroup, scales, downsampleBlockSizes)
157157
}
158158
} else {
159159
// TODO pass compression and reverse array as parameters
@@ -167,7 +167,7 @@ private fun handleLabelDataset(
167167
slicedSupplier,
168168
initialBlockSize,
169169
initialBlockSize,
170-
info.outputContainer.toString(),
170+
info.outputContainerUri,
171171
originalResolutionOutputDataset,
172172
ZstandardCompression()
173173
)
@@ -177,7 +177,7 @@ private fun handleLabelDataset(
177177
info.inputContainer,
178178
info.inputDataset,
179179
initialBlockSize,
180-
info.outputContainer.toString(),
180+
info.outputContainerUri,
181181
originalResolutionOutputDataset,
182182
ZstandardCompression(),
183183
reverse
@@ -189,33 +189,33 @@ private fun handleLabelDataset(
189189

190190
ExtractUniqueLabelsPerBlock.extractUniqueLabels(
191191
sc,
192-
info.outputContainer.toString(),
193-
info.outputContainer.toString(),
192+
info.outputContainerUri,
193+
info.outputContainerUri,
194194
originalResolutionOutputDataset,
195195
"$uniqueLabelsGroup/s0"
196196
)
197197
LabelListDownsampler.addMultiScaleTag(writer, uniqueLabelsGroup)
198198

199199
if (scales.isNotEmpty()) {
200200
// TODO pass compression as parameter
201-
SparkDownsampler.downsampleMultiscale(sc, info.outputContainer.toString(), dataGroup, scales, downsampleBlockSizes, maxNumEntriesArray, ZstandardCompression())
202-
LabelListDownsampler.donwsampleMultiscale(sc, info.outputContainer.toString(), uniqueLabelsGroup, scales, downsampleBlockSizes)
201+
SparkDownsampler.downsampleMultiscale(sc, info.outputContainerUri, dataGroup, scales, downsampleBlockSizes, maxNumEntriesArray, ZstandardCompression())
202+
LabelListDownsampler.donwsampleMultiscale(sc, info.outputContainerUri, uniqueLabelsGroup, scales, downsampleBlockSizes)
203203
}
204204
}
205205

206206
if (labelBlockLookupN5BlockSize != null) {
207207
LabelToBlockMapping.createMappingWithMultiscaleCheckN5(
208208
sc,
209-
info.outputContainer.toString(),
209+
info.outputContainerUri,
210210
uniqueLabelsGroup,
211-
info.outputContainer.toString(),
211+
info.outputContainerUri,
212212
info.outputGroup,
213213
labelBlockMappingGroupBasename,
214214
labelBlockLookupN5BlockSize
215215
)
216216

217217
} else {
218-
LabelToBlockMapping.createMappingWithMultiscaleCheck(sc, info.outputContainer.toString(), uniqueLabelsGroup, labelBlockMappingGroupDirectory)
218+
LabelToBlockMapping.createMappingWithMultiscaleCheck(sc, info.outputContainerUri, uniqueLabelsGroup, labelBlockMappingGroupDirectory)
219219
}
220220
/* write the group-level lookup metadata */
221221
writer.setAttribute(info.outputGroup, LABEL_BLOCK_LOOKUP_KEY, LabelBlockLookupFromN5Relative("$labelBlockMappingGroupBasename/s%d"))

src/main/kotlin/org/janelia/saalfeldlab/conversion/to/paintera/ToPainteraData.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,19 @@ class ToPainteraData {
7575
@CommandLine.Option(names = ["--output-format"], required = false, defaultValue = "", paramLabel = "OUTPUT_FORMAT")
7676
var _outputFormat: String = ""
7777

78-
val outputFormat: StorageFormat?
79-
get() = runCatching {
78+
/* the format the user asked for, via --output-format or a scheme/extension on the container; null if unspecified */
79+
val requestedOutputFormat: StorageFormat?
80+
get() = runCatching {
8081
StorageFormat.valueOf(_outputFormat)
8182
}.getOrNull()
8283
?: StorageFormat.parseUri(_outputContainer).a
8384

85+
/* Only N5 supports Paintera datasets right now, as both the label multiset type
86+
* and the adjacent index dataset(s) are varlen; an ambiguous container path would
87+
* otherwise be created in the N5Factory default format */
88+
val outputFormat: StorageFormat
89+
get() = StorageFormat.N5
90+
8491
@CommandLine.Option(
8592
names = ["--spark-master"],
8693
required = false,
@@ -102,13 +109,11 @@ class ToPainteraData {
102109
if (helpRequested)
103110
return 0
104111

105-
/* Only N5 Format supports Paintera datasets right now, as both the
106-
* label multiset type and the adjacent index dataset are varlen */
107-
val resolvedOutputFormat = outputFormat ?: StorageFormat.guessStorageFromUri(outputContainer)
108-
if (resolvedOutputFormat != null && resolvedOutputFormat != StorageFormat.N5) {
112+
val requested = requestedOutputFormat
113+
if (requested != null && requested != StorageFormat.N5) {
109114
val error = InvalidOutputContainer(
110115
_outputContainer,
111-
"to-paintera only supports output-format=N5, but got `$resolvedOutputFormat'"
116+
"to-paintera only supports output-format=N5, but got `$requested'"
112117
)
113118
LOG.error { error.message }
114119
return error.exitCode

src/test/kotlin/org/janelia/saalfeldlab/conversion/PainteraConvertTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,39 @@ class PainteraConvertTest {
337337
})
338338
}
339339

340+
/* an output container without a recognizable extension used to be created in the N5Factory
341+
* default format (zarr3), which cannot hold the varlen label multiset blocks */
342+
@Test
343+
fun `ambiguous output container should be n5`() {
344+
val inputPath = "${Files.createTempDirectory("no-ext-in")}.n5"
345+
N5Utils.save(LABELS, createWriter(inputPath), LABEL_SOURCE_DATASET, blockSize, RawCompression())
346+
val painteraPath = Files.createTempDirectory("no-ext-out").resolve("data").toString()
347+
val target = "volumes/labels"
348+
System.setProperty("spark.master", "local[1]")
349+
main(
350+
arrayOf(
351+
"to-paintera",
352+
"--container=$inputPath",
353+
"--output-container=$painteraPath",
354+
"-d", LABEL_SOURCE_DATASET,
355+
"--type=label",
356+
"--target-dataset=$target",
357+
"--block-size=2,2,2"
358+
)
359+
)
360+
361+
assertTrue(Files.exists(java.nio.file.Paths.get(painteraPath, "attributes.json")))
362+
val painteraN5 = createWriter("n5:$painteraPath")
363+
assertEquals(DataType.UINT8, painteraN5.getDatasetAttributes("$target/data/s0").dataType)
364+
365+
/* the unique-label extraction reads the multiset back; it threw NegativeArraySizeException on zarr3 */
366+
assertTrue(painteraN5.datasetExists("$target/unique-labels/s0"))
367+
LoopBuilder.setImages(LABELS, N5LabelMultisets.openLabelMultiset(painteraN5, "$target/data/s0"))
368+
.forEachPixel(BiConsumer { e: UnsignedLongType, a: LabelMultisetType ->
369+
assertTrue(a.entrySet().size == 1 && a.entrySet().iterator().next().element.id() == e.get())
370+
})
371+
}
372+
340373
@Test
341374
fun `to-paintera only supports n5 output format`() {
342375
val inputPath = "${Files.createTempDirectory("guard-in")}.n5"

startup-scripts/flintstone-paintera-convert.sh

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,40 @@ fi
9191
LIB_DIR="${LIB_DIR:-$PROJECT_ROOT/target/dependency}"
9292

9393
find_main_jar() {
94-
ls "$LIB_DIR"/paintera-conversion-helper-*.jar 2>/dev/null \
95-
| grep -vE '\-(sources|javadoc)\.jar$' | head -1 || true
94+
local jar
95+
for jar in "$LIB_DIR"/paintera-conversion-helper-*.jar; do
96+
[[ -f "$jar" ]] || continue
97+
case "$jar" in
98+
*-sources.jar|*-javadoc.jar|*-tests.jar) continue ;;
99+
esac
100+
printf '%s\n' "$jar"
101+
return 0
102+
done
96103
}
97104

105+
# a bundled Spark jar means a prior local build populated the dir with compile-scope Spark; prepend
106+
# `clean` to the build so it starts fresh with -Pspark-provided, since a bundled Spark would conflict
107+
# with the cluster's SPARK_HOME
108+
has_bundled_spark() {
109+
local jar
110+
for jar in "$LIB_DIR"/spark-core_2.12-*.jar; do
111+
[[ -f "$jar" ]] && return 0
112+
done
113+
return 1
114+
}
115+
clean_first=0
116+
if has_bundled_spark; then
117+
echo "cleaning a prior local (Spark-bundled) build before the cluster build" 1>&2
118+
clean_first=1
119+
fi
120+
98121
MAIN_JAR="${MAIN_JAR:-$(find_main_jar)}"
99122

100-
if [[ ! -f "$MAIN_JAR" ]]; then
123+
if [[ "$clean_first" -eq 1 || ! -f "$MAIN_JAR" ]]; then
101124
export MAVEN_OPTS="-XX:ActiveProcessorCount=4 -XX:MaxRAMPercentage=25 ${MAVEN_OPTS:-}"
102-
"$PROJECT_ROOT/mvnw" package -DskipTests -Pspark-provided ${MAVEN_ARGS:-}
125+
goals=(package)
126+
[[ "$clean_first" -eq 1 ]] && goals=(clean package)
127+
"$PROJECT_ROOT/mvnw" "${goals[@]}" -DskipTests -Pspark-provided ${MAVEN_ARGS:-}
103128
MAIN_JAR="$(find_main_jar)"
104129
fi
105130

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bash
2+
3+
# paintera-convert wrapper script.
4+
#
5+
# This script builds the jar once, and explicitly excludes the default `spark-provided` profile (`-P=-spark-provided`).
6+
# This ensure Spark is bundled onto the classpath instead of being supplied by a cluster SPARK_HOME.
7+
# paintera-convert then runs on a local Spark master (local[*] by default).
8+
#
9+
# All arguments are passed directly to paintera-convert.
10+
#
11+
# usage: local-paintera-convert.sh <paintera-convert args>
12+
# e.g. local-paintera-convert.sh to-paintera --container=in.zarr -d labels/s0 \
13+
# --output-container=out.n5 --target-dataset=labels --type=label \
14+
# --scale 2,2,2 2,2,2 --block-size=32,32,32
15+
#
16+
17+
set -euo pipefail
18+
19+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
21+
CLASS="${CLASS:-org.janelia.saalfeldlab.conversion.PainteraConvert}"
22+
LIB_DIR="${LIB_DIR:-$PROJECT_ROOT/target/dependency}"
23+
24+
if [[ "$#" -eq 0 ]]; then
25+
echo "usage: $(basename "$0") <paintera-convert args>" 1>&2
26+
echo " e.g. $(basename "$0") to-scalar -i in.n5 -I labels -o out.zarr --xyz-unit nm --scale 2,2,2" 1>&2
27+
exit 1
28+
fi
29+
30+
find_main_jar() {
31+
local jar
32+
for jar in "$LIB_DIR"/paintera-conversion-helper-*.jar; do
33+
[[ -f "$jar" ]] || continue
34+
case "$jar" in
35+
*-sources.jar|*-javadoc.jar|*-tests.jar) continue ;;
36+
esac
37+
printf '%s\n' "$jar"
38+
return 0
39+
done
40+
}
41+
42+
has_bundled_spark() {
43+
local jar
44+
for jar in "$LIB_DIR"/spark-core_2.12-*.jar; do
45+
[[ -f "$jar" ]] && return 0
46+
done
47+
return 1
48+
}
49+
50+
MAIN_JAR="$(find_main_jar)"
51+
52+
# build once; also (re)build if a previous cluster build left the lib dir without a bundled Spark
53+
if [[ ! -f "$MAIN_JAR" ]] || ! has_bundled_spark; then
54+
export MAVEN_OPTS="-XX:ActiveProcessorCount=4 -XX:MaxRAMPercentage=25 ${MAVEN_OPTS:-}"
55+
( cd "$PROJECT_ROOT" && ./mvnw package -DskipTests -P=-spark-provided ${MAVEN_ARGS:-} )
56+
MAIN_JAR="$(find_main_jar)"
57+
fi
58+
59+
if [[ ! -f "$MAIN_JAR" ]]; then
60+
echo "error: main jar not found in $LIB_DIR after build" 1>&2
61+
exit 1
62+
fi
63+
64+
JAVACMD="java"
65+
[[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/java" ]] && JAVACMD="$JAVA_HOME/bin/java"
66+
67+
JVM_ARGS="${JVM_ARGS:---add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED -XX:SoftRefLRUPolicyMSPerMB=1}"
68+
69+
exec "$JAVACMD" $JVM_ARGS -cp "$LIB_DIR/*" "$CLASS" "$@"

0 commit comments

Comments
 (0)