diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 7c693819ae1..b9769572692 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -31,7 +31,8 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Updated antd UI library from version 4.24.8 to 4.24.15. [#7505](https://github.com/scalableminds/webknossos/pull/7505)
- Changed the default dataset search mode to also search in subfolders. [#7539](https://github.com/scalableminds/webknossos/pull/7539)
- When clicking a segment in the viewport, it is automatically focused in the segment list. A corresponding context menu entry was added as well. [#7512](https://github.com/scalableminds/webknossos/pull/7512)
-- Updated the isValidName route in the API to return 200 for valid and invalid names. With this, the API version was bumped up to 6. [#7550](https://github.com/scalableminds/webknossos/pull/7550)
+- Updated the isValidName route in the API to return 200 for valid and invalid names. With this, the API version was bumped up to 6. [#7550](https://github.com/scalableminds/webknossos/pull/7550)
+- The metadata for ND datasets and their annotation has changed: upper bound of additionalAxes is now stored as an exclusive value, called "end" in the NML format. [#7547](https://github.com/scalableminds/webknossos/pull/7547)
### Fixed
- Datasets with annotations can now be deleted. The concerning annotations can no longer be viewed but still be downloaded. [#7429](https://github.com/scalableminds/webknossos/pull/7429)
@@ -49,6 +50,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Fixed regression in proofreading tool when automatic mesh loading was disabled and a merge/split operation was performed. [#7534](https://github.com/scalableminds/webknossos/pull/7534)
- Fixed that last dimension value in ND dataset was not loaded. [#7535](https://github.com/scalableminds/webknossos/pull/7535)
- Fixed the initialization of the mapping list for agglomerate views if json mappings are present. [#7537](https://github.com/scalableminds/webknossos/pull/7537)
+- Fixed a bug where uploading ND volume annotations would lead to errors due to parsing of the chunk paths. [#7547](https://github.com/scalableminds/webknossos/pull/7547)
### Removed
- Removed several unused frontend libraries. [#7521](https://github.com/scalableminds/webknossos/pull/7521)
diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md
index 8c46c6251e7..1cccde5d142 100644
--- a/MIGRATIONS.unreleased.md
+++ b/MIGRATIONS.unreleased.md
@@ -12,6 +12,7 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).
- If your setup contains webknossos-workers, postgres evolution 110 introduces the columns `maxParallelHighPriorityJobs` and `maxParallelLowPriorityJobs`. Make sure to set those values to match what you want for your deployment. [#7463](https://github.com/scalableminds/webknossos/pull/7463)
- If your setup contains webknossos-workers, you may want to add the new available worker job `compute_segment_index_file` to the `supportedJobCommands` column of one or more of your workers. [#7493](https://github.com/scalableminds/webknossos/pull/7493)
- The WEBKNOSSOS api version has changed to 6. The `isValidNewName` route for datasets now returns 200 regardless of whether the name is valid or not. The body contains a JSON object with the key "isValid". [#7550](https://github.com/scalableminds/webknossos/pull/7550)
+- If your setup contains ND datasets, run the python3 script at `tools/migrate-axis-bounds/migration.py` on your datastores to update the datasource-properties.jsons of the ND datasets.
### Postgres Evolutions:
diff --git a/app/models/annotation/nml/NmlParser.scala b/app/models/annotation/nml/NmlParser.scala
index 452bfecc5b7..b830566c1dc 100755
--- a/app/models/annotation/nml/NmlParser.scala
+++ b/app/models/annotation/nml/NmlParser.scala
@@ -270,11 +270,11 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener
name <- getSingleAttributeOpt(additionalAxisNode, "name")
indexStr <- getSingleAttributeOpt(additionalAxisNode, "index")
index <- indexStr.toIntOpt
- minStr <- getSingleAttributeOpt(additionalAxisNode, "min")
- min <- minStr.toIntOpt
- maxStr <- getSingleAttributeOpt(additionalAxisNode, "max")
- max <- maxStr.toIntOpt
- } yield new AdditionalAxisProto(name, index, Vec2IntProto(min, max))
+ startStr <- getSingleAttributeOpt(additionalAxisNode, "start")
+ start <- startStr.toIntOpt
+ endStr <- getSingleAttributeOpt(additionalAxisNode, "end")
+ end <- endStr.toIntOpt
+ } yield new AdditionalAxisProto(name, index, Vec2IntProto(start, end))
}
)
)
diff --git a/app/models/annotation/nml/NmlWriter.scala b/app/models/annotation/nml/NmlWriter.scala
index 3629cd21b2b..32648e605ce 100644
--- a/app/models/annotation/nml/NmlWriter.scala
+++ b/app/models/annotation/nml/NmlWriter.scala
@@ -234,8 +234,8 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits {
Xml.withinElementSync("additionalAxis") {
writer.writeAttribute("name", a.name)
writer.writeAttribute("index", a.index.toString)
- writer.writeAttribute("min", a.bounds.x.toString)
- writer.writeAttribute("max", a.bounds.y.toString)
+ writer.writeAttribute("start", a.bounds.x.toString)
+ writer.writeAttribute("end", a.bounds.y.toString)
}
})
}
diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts
index e16871e6495..32dfc98b292 100644
--- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts
+++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts
@@ -232,7 +232,7 @@ class DataCube {
for (const coord of coords || []) {
if (coord.name in this.additionalAxes) {
const { bounds } = this.additionalAxes[coord.name];
- if (coord.value < bounds[0] || coord.value > bounds[1]) {
+ if (coord.value < bounds[0] || coord.value >= bounds[1]) {
return null;
}
}
diff --git a/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts b/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts
index 874068a06b0..73a491f3f43 100644
--- a/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts
+++ b/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts
@@ -273,14 +273,14 @@ function serializeParameters(
...(additionalAxes.length > 0
? serializeTagWithChildren(
- "additionalCoordinates",
+ "additionalAxes",
{},
additionalAxes.map((coord) =>
- serializeTag("additionalCoordinate", {
+ serializeTag("additionalAxis", {
name: coord.name,
index: coord.index,
- min: coord.bounds[0],
- max: coord.bounds[1],
+ start: coord.bounds[0],
+ end: coord.bounds[1],
}),
),
)
diff --git a/frontend/javascripts/oxalis/view/action_bar_view.tsx b/frontend/javascripts/oxalis/view/action_bar_view.tsx
index db2a07a6a6b..a8f66d92bec 100644
--- a/frontend/javascripts/oxalis/view/action_bar_view.tsx
+++ b/frontend/javascripts/oxalis/view/action_bar_view.tsx
@@ -110,7 +110,7 @@ function AdditionalCoordinatesInputView() {
label={coord.name}
key={coord.name}
min={bounds[0]}
- max={bounds[1]}
+ max={bounds[1] - 1}
value={coord.value}
spans={[2, 18, 4]}
onChange={(newCoord) => {
diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.md b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.md
index 672b873ace1..ff8522951eb 100644
--- a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.md
+++ b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.md
@@ -82,9 +82,9 @@ Generated by [AVA](https://avajs.dev).
␊
␊
␊
- ␊
- ␊
- ␊
+ ␊
+ ␊
+ ␊
␊
␊
␊
diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.snap b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.snap
index 94be274b569..046d13f7319 100644
Binary files a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.snap and b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/libs/nml.spec.js.snap differ
diff --git a/tools/migrate-axis-bounds/migration.py b/tools/migrate-axis-bounds/migration.py
new file mode 100644
index 00000000000..c3e6a0ddf4e
--- /dev/null
+++ b/tools/migrate-axis-bounds/migration.py
@@ -0,0 +1,51 @@
+from pathlib import Path
+import json
+import shutil
+import time
+import argparse
+
+# Traverses a binaryData directory and changes all datasource-properties.jsons
+# that include additionalAxes, increasing the upper bound by 1
+# This follows a change in the upper bound semantic to be exclusive
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--dry",
+ action="store_true",
+ )
+ parser.add_argument("binary_data_dir", type=Path, help="WEBKNOSSOS binary data dir")
+ args = parser.parse_args()
+ dry = args.dry
+ binary_data_dir = args.binary_data_dir
+
+ count = 0
+ print(f"Traversing datasets at {binary_data_dir.resolve()} ...")
+ for orga_dir in [item for item in binary_data_dir.iterdir() if item.is_dir()]:
+ for dataset_dir in [item for item in orga_dir.iterdir() if item.is_dir()]:
+ json_path = dataset_dir / "datasource-properties.json"
+ if json_path.exists():
+ changed = False
+ with open(json_path, 'r') as json_file:
+ content = json.load(json_file)
+ for layer in content.get("dataLayers", []):
+ for axis in layer.get("additionalAxes", []):
+ if "bounds" in axis:
+ bounds = axis["bounds"]
+ if len(bounds) >= 2:
+ bounds[1] = bounds[1] + 1
+ changed = True
+ if changed:
+ print(f"Updating {json_path} (dry={dry})...")
+ count += 1
+ backup_path = Path(f"{json_path}.{round(time.time() * 1000)}.bak")
+
+ if not dry:
+ shutil.copyfile(json_path, backup_path)
+ with open(json_path, 'w') as json_outfile:
+ json.dump(content, json_outfile, indent=4)
+
+ print(f"Updated {count} datasets (dry={dry})")
+
+if __name__ == '__main__':
+ main()
diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/explore/NgffExplorer.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/explore/NgffExplorer.scala
index 16192065d64..b12ef60a610 100644
--- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/explore/NgffExplorer.scala
+++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/explore/NgffExplorer.scala
@@ -130,7 +130,7 @@ class NgffExplorer(implicit val ec: ExecutionContext) extends RemoteLayerExplore
.filter(axis => !defaultAxes.contains(axis.name))
.zipWithIndex
.map(axisAndIndex =>
- createAdditionalAxis(axisAndIndex._1.name, axisAndIndex._2, Array(0, shape(axisAndIndex._2) - 1)).toFox))
+ createAdditionalAxis(axisAndIndex._1.name, axisAndIndex._2, Array(0, shape(axisAndIndex._2))).toFox))
duplicateNames = axes.map(_.name).diff(axes.map(_.name).distinct).distinct
_ <- Fox.bool2Fox(duplicateNames.isEmpty) ?~> s"Additional axes names (${duplicateNames.mkString("", ", ", "")}) are not unique."
} yield axes
diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala
index eacaf413182..ab155c3162a 100644
--- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala
+++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala
@@ -85,7 +85,7 @@ trait VolumeDataZipHelper extends WKWDataFormatHelper with ByteUtils with BoxImp
val additionalAxesNames: Seq[String] = dimensionNames.toSeq.drop(1).dropRight(3) // drop channel left, and xyz right
// assume additionalAxes,x,y,z
- val chunkPathRegex = s"(|.*/)(\\d+-\\d+-\\d+)/c\\.(.+)".r
+ val chunkPathRegex = s"(|.*/)(\\d+|\\d+-\\d+-\\d+)/c\\.(.+)".r
path match {
case chunkPathRegex(_, magStr, dimsStr) =>
@@ -99,7 +99,7 @@ trait VolumeDataZipHelper extends WKWDataFormatHelper with ByteUtils with BoxImp
val bucketY = dims(dims.length - 2)
val bucketZ = dims.last
- Vec3Int.fromMagLiteral(magStr).map { mag =>
+ Vec3Int.fromMagLiteral(magStr, allowScalar = true).map { mag =>
BucketPosition(
bucketX.toInt * mag.x * DataLayer.bucketLength,
bucketY.toInt * mag.y * DataLayer.bucketLength,