From b3b3990602f3b52650b084d3ed3360fcf21cc56e Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Tue, 7 Jul 2026 16:40:12 -0500 Subject: [PATCH 01/22] Initial API to allow readers to report axis orientation --- .../loci/formats/AnatomicalOrientation.java | 74 +++++++++++++++++++ .../loci/formats/IAxisOrientationReader.java | 47 ++++++++++++ .../src/loci/formats/Orientation.java | 65 ++++++++++++++++ .../src/loci/formats/OrientationTerm.java | 39 ++++++++++ .../src/loci/formats/OrientationType.java | 56 ++++++++++++++ 5 files changed, 281 insertions(+) create mode 100644 components/formats-api/src/loci/formats/AnatomicalOrientation.java create mode 100644 components/formats-api/src/loci/formats/IAxisOrientationReader.java create mode 100644 components/formats-api/src/loci/formats/Orientation.java create mode 100644 components/formats-api/src/loci/formats/OrientationTerm.java create mode 100644 components/formats-api/src/loci/formats/OrientationType.java diff --git a/components/formats-api/src/loci/formats/AnatomicalOrientation.java b/components/formats-api/src/loci/formats/AnatomicalOrientation.java new file mode 100644 index 00000000000..9fee0f975cc --- /dev/null +++ b/components/formats-api/src/loci/formats/AnatomicalOrientation.java @@ -0,0 +1,74 @@ +/* + * #%L + * Top-level reader and writer APIs + * %% + * Copyright (C) 2023 Open Microscopy Environment: + * - Board of Regents of the University of Wisconsin-Madison + * - Glencoe Software, Inc. + * - University of Dundee + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package loci.formats; + +/** + * Enum of anatomical orientation terms defined in: + * https://ngff.openmicroscopy.org/rfc/4/#permissible-values + */ +public enum AnatomicalOrientation implements OrientationTerm { + LEFT_TO_RIGHT("left-to-right"), + RIGHT_TO_LEFT("right-to-left"), + ANTERIOR_TO_POSTERIOR("anterior-to-posterior"), + POSTERIOR_TO_ANTERIOR("posterior-to-anterior"), + INFERIOR_TO_SUPERIOR("inferior-to-superior"), + SUPERIOR_TO_INFERIOR("superior-to-inferior"), + DORSAL_TO_VENTRAL("dorsal-to-ventral"), + VENTRAL_TO_DORSAL("ventral-to-dorsal"), + DORSAL_TO_PALMAR("dorsal-to-palmar"), + PALMAR_TO_DORSAL("palmar-to-dorsal"), + DORSAL_TO_PLANTAR("dorsal-to-plantar"), + PLANTAR_TO_DORSAL("plantar-to-dorsal"), + ROSTRAL_TO_CAUDAL("rostral-to-caudal"), + CAUDAL_TO_ROSTRAL("caudal-to-rostral"), + CRANIAL_TO_CAUDAL("cranial-to-caudal"), + CAUDAL_TO_CRANIAL("caudal-to-cranial"), + PROXIMAL_TO_DISTAL("proximal-to-distal"), + DISTAL_TO_PROXIMAL("distal-to-proximal"), + SUPERFICIAL_TO_DEEP("superficial-to-deep"), + DEEP_TO_SUPERFICIAL("deep-to-superficial"), + APICAL_TO_BASAL("apical-to-basal"), + BASAL_TO_APICAL("basal-to-apical"), + APEX_TO_BASE("apex-to-base"), + BASE_TO_APEX("base-to-apex"); + + private String definedTerm; + + private AnatomicalOrientation(String term) { + definedTerm = term; + } + + public String getDefinedTerm() { + return definedTerm; + } +} diff --git a/components/formats-api/src/loci/formats/IAxisOrientationReader.java b/components/formats-api/src/loci/formats/IAxisOrientationReader.java new file mode 100644 index 00000000000..e118839f5c9 --- /dev/null +++ b/components/formats-api/src/loci/formats/IAxisOrientationReader.java @@ -0,0 +1,47 @@ +/* + * #%L + * Top-level reader and writer APIs + * %% + * Copyright (C) 2023 Open Microscopy Environment: + * - Board of Regents of the University of Wisconsin-Madison + * - Glencoe Software, Inc. + * - University of Dundee + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package loci.formats; + +/** + * Interface for reading axis orientation metadata. + */ +public interface IAxisOrientationReader { + + /** + * Get one orientation object per axis (e.g. "X", "Y"), for the current series. + * Order matches getDimensionOrder(). Array elements may be null. + */ + default Orientation[] getAxisOrientations() { + return new Orientation[5]; + } +} diff --git a/components/formats-api/src/loci/formats/Orientation.java b/components/formats-api/src/loci/formats/Orientation.java new file mode 100644 index 00000000000..4334b6bd4ad --- /dev/null +++ b/components/formats-api/src/loci/formats/Orientation.java @@ -0,0 +1,65 @@ +/* + * #%L + * Top-level reader and writer APIs + * %% + * Copyright (C) 2023 Open Microscopy Environment: + * - Board of Regents of the University of Wisconsin-Madison + * - Glencoe Software, Inc. + * - University of Dundee + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package loci.formats; + +/** + * Defines an orientation that can be associated with an axis. + */ +public class Orientation { + + private OrientationType type; + + private OrientationTerm value; + + public Orientation(OrientationType type, OrientationTerm term) { + this.type = type; + this.value = term; + } + + public void setOrientationType(OrientationType type) { + this.type = type; + } + + public OrientationType getOrientationType() { + return type; + } + + public void setOrientationTerm(OrientationTerm term) { + value = term; + } + + public OrientationTerm getOrientationTerm() { + return value; + } + +} diff --git a/components/formats-api/src/loci/formats/OrientationTerm.java b/components/formats-api/src/loci/formats/OrientationTerm.java new file mode 100644 index 00000000000..6fb26d4a75d --- /dev/null +++ b/components/formats-api/src/loci/formats/OrientationTerm.java @@ -0,0 +1,39 @@ +/* + * #%L + * Top-level reader and writer APIs + * %% + * Copyright (C) 2023 Open Microscopy Environment: + * - Board of Regents of the University of Wisconsin-Madison + * - Glencoe Software, Inc. + * - University of Dundee + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package loci.formats; + +/** + * Common interface for orientation values. + */ +public interface OrientationTerm { +} diff --git a/components/formats-api/src/loci/formats/OrientationType.java b/components/formats-api/src/loci/formats/OrientationType.java new file mode 100644 index 00000000000..3a6c1958b06 --- /dev/null +++ b/components/formats-api/src/loci/formats/OrientationType.java @@ -0,0 +1,56 @@ +/* + * #%L + * Top-level reader and writer APIs + * %% + * Copyright (C) 2023 Open Microscopy Environment: + * - Board of Regents of the University of Wisconsin-Madison + * - Glencoe Software, Inc. + * - University of Dundee + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package loci.formats; + +/** + * Enum of recognized axis orientation types. + */ +public enum OrientationType { + ANATOMICAL("anatomical", AnatomicalOrientation.class); + + private String definedType; + private Class terms; + + private OrientationType(String type, Class terms) { + definedType = type; + this.terms = terms; + } + + public String getDefinedType() { + return definedType; + } + + public Class getOrientationTermClass() { + return terms; + } +} From f9f17d75d598abb680bf7c53247615fb3b59f156 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 13 Jul 2026 18:28:10 -0500 Subject: [PATCH 02/22] Switch version to 9.0.0-SNAPSHOT --- components/bio-formats-plugins/pom.xml | 2 +- components/bio-formats-tools/pom.xml | 2 +- components/bundles/bioformats_package/pom.xml | 2 +- components/forks/turbojpeg/pom.xml | 2 +- components/formats-api/pom.xml | 2 +- components/formats-bsd/pom.xml | 2 +- components/formats-gpl/pom.xml | 2 +- components/test-suite/pom.xml | 2 +- pom.xml | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/components/bio-formats-plugins/pom.xml b/components/bio-formats-plugins/pom.xml index e2b74e34551..378e4efff07 100644 --- a/components/bio-formats-plugins/pom.xml +++ b/components/bio-formats-plugins/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../.. diff --git a/components/bio-formats-tools/pom.xml b/components/bio-formats-tools/pom.xml index 60f46b1858d..3fb44beb111 100644 --- a/components/bio-formats-tools/pom.xml +++ b/components/bio-formats-tools/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../.. diff --git a/components/bundles/bioformats_package/pom.xml b/components/bundles/bioformats_package/pom.xml index 58a0fce4463..933dce46f6d 100644 --- a/components/bundles/bioformats_package/pom.xml +++ b/components/bundles/bioformats_package/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../../../ diff --git a/components/forks/turbojpeg/pom.xml b/components/forks/turbojpeg/pom.xml index 93e0fef2417..e06eb82eb1f 100644 --- a/components/forks/turbojpeg/pom.xml +++ b/components/forks/turbojpeg/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../../../ diff --git a/components/formats-api/pom.xml b/components/formats-api/pom.xml index 3fb8b395a8e..27d65cb699c 100644 --- a/components/formats-api/pom.xml +++ b/components/formats-api/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../.. diff --git a/components/formats-bsd/pom.xml b/components/formats-bsd/pom.xml index 9f0fb709eed..bd2cf61ee6f 100644 --- a/components/formats-bsd/pom.xml +++ b/components/formats-bsd/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../.. diff --git a/components/formats-gpl/pom.xml b/components/formats-gpl/pom.xml index c12255e73ce..aeda69ab3b0 100644 --- a/components/formats-gpl/pom.xml +++ b/components/formats-gpl/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../.. diff --git a/components/test-suite/pom.xml b/components/test-suite/pom.xml index 6392ab1b24c..5d685a70c99 100644 --- a/components/test-suite/pom.xml +++ b/components/test-suite/pom.xml @@ -8,7 +8,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ../.. diff --git a/pom.xml b/pom.xml index 7fd4abe7de2..06a7b5c5b48 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ome pom-bio-formats - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT pom Bio-Formats projects @@ -34,7 +34,7 @@ When possible, we advise using the relevant groupId and version properties for your dependencies rather than hardcoding them. --> - 8.6.0-SNAPSHOT + 9.0.0-SNAPSHOT ${maven.build.timestamp} 2017 ${basedir} From 3e14c9b3c2ab9e8222b46438d26c5e2e27b66e95 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 13 Jul 2026 18:28:43 -0500 Subject: [PATCH 03/22] Make sure defined orientation term can always be retrieved --- .../formats-api/src/loci/formats/AnatomicalOrientation.java | 1 + components/formats-api/src/loci/formats/OrientationTerm.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/components/formats-api/src/loci/formats/AnatomicalOrientation.java b/components/formats-api/src/loci/formats/AnatomicalOrientation.java index 9fee0f975cc..21c08fa1869 100644 --- a/components/formats-api/src/loci/formats/AnatomicalOrientation.java +++ b/components/formats-api/src/loci/formats/AnatomicalOrientation.java @@ -68,6 +68,7 @@ private AnatomicalOrientation(String term) { definedTerm = term; } + @Override public String getDefinedTerm() { return definedTerm; } diff --git a/components/formats-api/src/loci/formats/OrientationTerm.java b/components/formats-api/src/loci/formats/OrientationTerm.java index 6fb26d4a75d..54c7e660c1d 100644 --- a/components/formats-api/src/loci/formats/OrientationTerm.java +++ b/components/formats-api/src/loci/formats/OrientationTerm.java @@ -36,4 +36,7 @@ * Common interface for orientation values. */ public interface OrientationTerm { + + /** @return the defined vocabulary term for the orientation */ + String getDefinedTerm(); } From e31a3920056f04206ae80adb19e5ed5b3f92df8c Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 13 Jul 2026 18:29:14 -0500 Subject: [PATCH 04/22] Implement IAxisOrientationReader in InveonReader --- .../src/loci/formats/in/InveonReader.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/components/formats-gpl/src/loci/formats/in/InveonReader.java b/components/formats-gpl/src/loci/formats/in/InveonReader.java index 457cc1896e9..9296cf096bc 100644 --- a/components/formats-gpl/src/loci/formats/in/InveonReader.java +++ b/components/formats-gpl/src/loci/formats/in/InveonReader.java @@ -34,11 +34,15 @@ import loci.common.DateTools; import loci.common.Location; import loci.common.RandomAccessInputStream; +import loci.formats.AnatomicalOrientation; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; +import loci.formats.Orientation; +import loci.formats.OrientationType; import loci.formats.meta.MetadataStore; import ome.xml.model.primitives.Timestamp; import ome.units.quantity.Length; @@ -46,7 +50,7 @@ /** * InveonReader is the file format reader for Inveon files. */ -public class InveonReader extends FormatReader { +public class InveonReader extends FormatReader implements IAxisOrientationReader { // -- Constants -- @@ -56,6 +60,7 @@ public class InveonReader extends FormatReader { private String datFile; private ArrayList dataPointers = new ArrayList(); + private Orientation zAxis = null; // -- Constructor -- @@ -68,6 +73,17 @@ public InveonReader() { datasetDescription = "One .hdr file plus one similarly-named file"; } + // -- IAxisOrientationReader API methods -- + + /* @see loci.formats.IAxisOrientationReader#getAxisOrientations() */ + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + Orientation[] axes = new Orientation[5]; + axes[getDimensionOrder().indexOf("Z") - 2] = zAxis; + return axes; + } + // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ @@ -133,6 +149,7 @@ public void close(boolean fileOnly) throws IOException { if (!fileOnly) { datFile = null; dataPointers.clear(); + zAxis = null; } } @@ -681,20 +698,28 @@ private String transformSubjectOrientation(String value) { int orientation = Integer.parseInt(value); switch (orientation) { case 1: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); return "Feet first, prone"; case 2: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); return "Head first, prone"; case 3: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); return "Feet first, supine"; case 4: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); return "Head first, supine"; case 5: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); return "Feet first, right"; case 6: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); return "Head first, right"; case 7: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); return "Feet first, left"; case 8: + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); return "Head first, left"; } return "Unknown"; From cc5bb13dd88f16531ea8aecadcc63ea35bbf0632 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Tue, 14 Jul 2026 18:12:07 -0500 Subject: [PATCH 05/22] Allow fake reader to set axis orientation (for testing) --- .../src/loci/formats/OrientationType.java | 9 ++++ .../src/loci/formats/in/FakeReader.java | 45 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/components/formats-api/src/loci/formats/OrientationType.java b/components/formats-api/src/loci/formats/OrientationType.java index 3a6c1958b06..8d103fbcc1e 100644 --- a/components/formats-api/src/loci/formats/OrientationType.java +++ b/components/formats-api/src/loci/formats/OrientationType.java @@ -53,4 +53,13 @@ public String getDefinedType() { public Class getOrientationTermClass() { return terms; } + + public static OrientationType fromString(String type) { + for (OrientationType t : OrientationType.class.getEnumConstants()) { + if (t.getDefinedType().equals(type)) { + return t; + } + } + return null; + } } diff --git a/components/formats-bsd/src/loci/formats/in/FakeReader.java b/components/formats-bsd/src/loci/formats/in/FakeReader.java index 2a38cdb4604..0e8e493e393 100644 --- a/components/formats-bsd/src/loci/formats/in/FakeReader.java +++ b/components/formats-bsd/src/loci/formats/in/FakeReader.java @@ -67,7 +67,11 @@ import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; +import loci.formats.Orientation; +import loci.formats.OrientationTerm; +import loci.formats.OrientationType; import loci.formats.ResourceNamer; import loci.formats.gui.AWTImageTools; import loci.formats.meta.MetadataStore; @@ -108,7 +112,7 @@ *
  • showinf 'pyramid&sizeX=10000&sizeY=10000&resolutions=5&resolutionScale=2.fake' -noflat -resolution 4
  • *

    */ -public class FakeReader extends FormatReader { +public class FakeReader extends FormatReader implements IAxisOrientationReader { // -- Constants -- private static final long ANN_LONG_VALUE = 365; @@ -215,6 +219,8 @@ public class FakeReader extends FormatReader { /** List of used files if the fake is a SPW structure */ private List fakeSeries = new ArrayList(); + private List orientations = new ArrayList(); + private OMEXMLMetadata omeXmlMetadata; private OMEXMLService omeXmlService; @@ -317,6 +323,14 @@ public FakeReader() { hasCompanionFiles = true; } + // -- IAxisOrientation API methods -- + + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + return orientations.toArray(new Orientation[orientations.size()]); + } + // -- IFormatReader API methods -- @Override @@ -703,6 +717,8 @@ protected void initFile(String id) throws FormatException, IOException { // add properties file values to list of tokens. if (iniFile != null) { + orientations.clear(); + IniParser parser = new IniParser(); IniList list = parser.parseINI(new File(iniFile)); @@ -1359,6 +1375,33 @@ private void parseSeriesTable(IniTable table, MetadataStore store, int newSeries int s = getSeries(); setSeries(newSeries); + // axis count should have been set to 5 or more (for modulo dims) + // order should match dimension order + String axisCount = table.get("AxisCount"); + if (axisCount != null) { + int axes = Integer.parseInt(axisCount); + for (int a=0; a Date: Tue, 14 Jul 2026 18:12:35 -0500 Subject: [PATCH 06/22] Update Analyze, MINC, and Nifti readers to report axis orientation --- .../src/loci/formats/in/AnalyzeReader.java | 71 ++++++++++++++++++- .../src/loci/formats/in/MINCReader.java | 20 +++++- .../src/loci/formats/in/NiftiReader.java | 22 +++++- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java b/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java index ae3b487314e..d43f8b48504 100644 --- a/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java +++ b/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java @@ -29,11 +29,15 @@ import loci.common.Location; import loci.common.RandomAccessInputStream; +import loci.formats.AnatomicalOrientation; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; +import loci.formats.Orientation; +import loci.formats.OrientationType; import loci.formats.meta.MetadataStore; import ome.units.quantity.Length; @@ -45,7 +49,7 @@ * * @author Melissa Linkert melissa at glencoesoftware.com */ -public class AnalyzeReader extends FormatReader { +public class AnalyzeReader extends FormatReader implements IAxisOrientationReader { // -- Constants -- @@ -61,6 +65,10 @@ public class AnalyzeReader extends FormatReader { private String pixelsFilename; + private Orientation xAxis; + private Orientation yAxis; + private Orientation zAxis; + // -- Constructor -- /** Constructs a new Analyze reader. */ @@ -71,6 +79,18 @@ public AnalyzeReader() { datasetDescription = "One .img file and one similarly-named .hdr file"; } + // -- IAxisOrientationReader API methods -- + + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + Orientation[] axes = new Orientation[5]; + axes[getDimensionOrder().indexOf("X")] = xAxis; + axes[getDimensionOrder().indexOf("Y")] = yAxis; + axes[getDimensionOrder().indexOf("Z")] = zAxis; + return axes; + } + // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ @@ -158,6 +178,9 @@ public void close(boolean fileOnly) throws IOException { pixelOffset = 0; pixelFile = null; pixelsFilename = null; + xAxis = null; + yAxis = null; + zAxis = null; } } @@ -241,6 +264,7 @@ protected void initFile(String id) throws FormatException, IOException { description = in.readString(80); String auxFile = in.readString(24); char orient = (char) in.readByte(); + String originator = in.readString(10); String generated = in.readString(10); String scannum = in.readString(10); @@ -259,6 +283,51 @@ protected void initFile(String id) throws FormatException, IOException { int smax = in.readInt(); int smin = in.readInt(); + // see page 4 of https://afni.nimh.nih.gov/pub/dist/doc/nifti/ANALYZE75.pdf + // further reference: + // http://www.grahamwideman.com/gw/brain/analyze/formatdoc.htm + // https://eeg.sourceforge.net/mri_orientation_notes.html + switch (orient) { + case '0': + // transverse unflipped + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + break; + case '1': + // coronal unflipped + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); + break; + case '2': + // sagittal unflipped + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + break; + case '3': + // transverse flipped + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.ANTERIOR_TO_POSTERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + break; + case '4': + // coronal flipped + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); + break; + case '5': + // sagittal flipped + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + break; + default: + LOGGER.warn("Could not determine orientation: {}", orient); + } + addGlobalMeta("Database name", imageName); addGlobalMeta("Number of dimensions", ndims); addGlobalMeta("Data type", dataType); diff --git a/components/formats-gpl/src/loci/formats/in/MINCReader.java b/components/formats-gpl/src/loci/formats/in/MINCReader.java index 10b04bc37e4..a31825d71f2 100644 --- a/components/formats-gpl/src/loci/formats/in/MINCReader.java +++ b/components/formats-gpl/src/loci/formats/in/MINCReader.java @@ -34,12 +34,16 @@ import loci.common.services.DependencyException; import loci.common.services.ServiceException; import loci.common.services.ServiceFactory; +import loci.formats.AnatomicalOrientation; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; import loci.formats.MissingLibraryException; +import loci.formats.Orientation; +import loci.formats.OrientationType; import loci.formats.meta.MetadataStore; import loci.formats.services.NetCDFService; @@ -48,7 +52,7 @@ /** * MINCReader is the file format reader for MINC MRI files. */ -public class MINCReader extends FormatReader { +public class MINCReader extends FormatReader implements IAxisOrientationReader { // -- Fields -- @@ -64,6 +68,20 @@ public MINCReader() { domains = new String[] {FormatTools.MEDICAL_DOMAIN}; } + // -- IAxisOrientationReader API methods -- + + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + // see https://en.wikibooks.org/wiki/MINC/SoftwareDevelopment/MINC2.0_File_Format_Reference#MINC_2.0_coordinate_system + return new Orientation[] { + new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.LEFT_TO_RIGHT), + new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR), + new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR), + null, null + }; + } + // -- IFormatReader API methods -- /** diff --git a/components/formats-gpl/src/loci/formats/in/NiftiReader.java b/components/formats-gpl/src/loci/formats/in/NiftiReader.java index 7373933c23a..03510f33123 100644 --- a/components/formats-gpl/src/loci/formats/in/NiftiReader.java +++ b/components/formats-gpl/src/loci/formats/in/NiftiReader.java @@ -31,11 +31,15 @@ import loci.common.DataTools; import loci.common.Location; import loci.common.RandomAccessInputStream; +import loci.formats.AnatomicalOrientation; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; +import loci.formats.Orientation; +import loci.formats.OrientationType; import loci.formats.meta.MetadataStore; import ome.units.quantity.Length; @@ -48,7 +52,7 @@ * * @author Melissa Linkert melissa at glencoesoftware.com */ -public class NiftiReader extends FormatReader { +public class NiftiReader extends FormatReader implements IAxisOrientationReader { // -- Constants -- @@ -92,6 +96,22 @@ public NiftiReader() { " .img file and a similarly-named .hdr file"; } + // -- IAxisOrientationReader API methods -- + + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + // see https://nifti.nimh.nih.gov/nifti-1/documentation/faq.html#Q14 + // and https://ngff.openmicroscopy.org/rfc/4/#background + return new Orientation[] { + new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.LEFT_TO_RIGHT), + new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR), + null, + new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR), + null + }; + } + // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isSingleFile(String) */ From b52374268ce4cf36ba31057173e9cb1f82baf899 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Wed, 15 Jul 2026 11:34:07 -0500 Subject: [PATCH 07/22] Report axis orientations for Bruker data --- .../src/loci/formats/in/BrukerReader.java | 164 +++++++++++++----- 1 file changed, 120 insertions(+), 44 deletions(-) diff --git a/components/formats-gpl/src/loci/formats/in/BrukerReader.java b/components/formats-gpl/src/loci/formats/in/BrukerReader.java index d9d6ba5ec89..b97b053b334 100644 --- a/components/formats-gpl/src/loci/formats/in/BrukerReader.java +++ b/components/formats-gpl/src/loci/formats/in/BrukerReader.java @@ -36,11 +36,15 @@ import loci.common.DateTools; import loci.common.Location; import loci.common.RandomAccessInputStream; +import loci.formats.AnatomicalOrientation; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; +import loci.formats.Orientation; +import loci.formats.OrientationType; import loci.formats.meta.MetadataStore; /** @@ -48,7 +52,7 @@ * * @author Melissa Linkert melissa at glencoesoftware.com */ -public class BrukerReader extends FormatReader { +public class BrukerReader extends FormatReader implements IAxisOrientationReader { // -- Constants -- @@ -74,6 +78,10 @@ public class BrukerReader extends FormatReader { private String[] users = null; private String[] timestamps = null; + private Orientation xAxis = null; + private Orientation yAxis = null; + private Orientation zAxis = null; + // -- Constructor -- /** Constructs a new Bruker reader. */ @@ -86,6 +94,18 @@ public BrukerReader() { "metadata files and a 'pdata' directory"; } + // -- IAxisOrientationReader API methods -- + + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + Orientation[] axes = new Orientation[5]; + axes[getDimensionOrder().indexOf("X")] = xAxis; + axes[getDimensionOrder().indexOf("Y")] = yAxis; + axes[getDimensionOrder().indexOf("Z")] = zAxis; + return axes; + } + // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#getRequiredDirectories(String[]) */ @@ -154,7 +174,9 @@ public String[] getSeriesUsedFiles(boolean noPixels) { files.add(new Location(getCurrentFile()).getAbsolutePath()); for (String f : allFiles) { - if (f.startsWith(dir) && (!f.endsWith("2dseq") || !noPixels)) { + if ((f.startsWith(dir) && (!f.endsWith("2dseq") || !noPixels)) || + f.endsWith("subject")) + { if (!files.contains(f)) { files.add(f); } @@ -234,6 +256,7 @@ public int compare(String s1, String s2) { ArrayList acqpFiles = new ArrayList(); ArrayList recoFiles = new ArrayList(); ArrayList procFiles = new ArrayList(); + String subjectFile = null; for (String f : acquisitionDirs) { Location dir = new Location(parent, f); @@ -280,15 +303,24 @@ else if (m.equals("d3proc")) { procFiles.remove(procFiles.size() - 1); } } + else if (f.equals("subject")) { + subjectFile = dir.getAbsolutePath(); + allFiles.add(subjectFile); + } } + core.clear(); + if (subjectFile != null) { + String subjectData = DataTools.readFile(subjectFile); + String[] lines = subjectData.split("\n"); + parseLines(lines); + } imageNames = new String[pixelsFiles.size()]; timestamps = new String[pixelsFiles.size()]; institutions = new String[pixelsFiles.size()]; users = new String[pixelsFiles.size()]; - core.clear(); for (int series=0; series= 0; + isFloat = !value.endsWith("_INT"); + } + else if (key.equals("##$SUBJECT_type")) { + subjectType = value; + } + else if (key.equals("##$SUBJECT_entry")) { + subjectEntry = value.substring(value.indexOf("_") + 1); + } + else if (key.equals("##$SUBJECT_pose")) { + subjectPose = value.substring(value.indexOf("_") + 1); + } + + if (ms != null) { + if (key.equals("##$BYTORDA")) { ms.littleEndian = value.toLowerCase().equals("little"); } - else if (key.equals("##$ACQ_size")) { - sizes = value.split(" "); - } - else if (key.equals("##$ACQ_obj_order")) { - ordering = value.split(" "); - } - else if (key.equals("##$ACQ_time")) { - timestamps[series] = value; - } - else if (key.equals("##$ACQ_institution")) { - institutions[series] = value; - } - else if (key.equals("##$ACQ_operator")) { - users[series] = value; - } - else if (key.equals("##$ACQ_scan_name")) { - imageNames[series] = value; - } - else if (key.equals("##$ACQ_ns_list_size")) { - ns = Integer.parseInt(value); - } - else if (key.equals("##$RECO_size")) { - sizes = value.split(" "); - } - else if (key.equals("##$RECO_wordtype")) { - bits = Integer.parseInt(value.substring(1, value.indexOf("BIT"))); - signed = value.indexOf("_SGN_") >= 0; - isFloat = !value.endsWith("_INT"); - } else if (key.equals("##$IM_SIX")) { ms.sizeX = Integer.parseInt(value); } @@ -468,6 +519,31 @@ else if (key.equals("##$IM_SIZ")) { else if (key.equals("##$IM_SIT")) { ms.sizeT = Integer.parseInt(value); } + } + } + } + + // see https://github.com/BrkRaw/brkraw/blob/0.5.7/src/brkraw/resolver/affine.py#L241 + // only supports subject type/entry/pose for which we have data right now + if (subjectType != null && subjectEntry != null && subjectPose != null) { + boolean headFirst = subjectEntry.toLowerCase().startsWith("head"); + boolean supine = subjectPose.equalsIgnoreCase("supine"); + boolean prone = subjectPose.equalsIgnoreCase("prone"); + boolean left = subjectPose.equalsIgnoreCase("left"); + boolean right = subjectPose.equalsIgnoreCase("right"); + + if (subjectType.equalsIgnoreCase("human")) { + if (headFirst && supine) { + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + } + else { + LOGGER.warn("Unsupported subject orientation: {}, {}", subjectEntry, subjectPose); + } + } + else { + LOGGER.warn("Unsupported subject type: {}", subjectType); } } } From bf827fb1d26c840d03f71e394a8cf0a1c5279a0d Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Wed, 15 Jul 2026 12:00:42 -0500 Subject: [PATCH 08/22] Report axis orientations for Varian data --- .../src/loci/formats/in/VarianFDFReader.java | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java b/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java index b8df93fb0dd..a4c90d67230 100644 --- a/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java +++ b/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java @@ -32,11 +32,15 @@ import loci.common.Location; import loci.common.RandomAccessInputStream; +import loci.formats.AnatomicalOrientation; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; +import loci.formats.Orientation; +import loci.formats.OrientationType; import loci.formats.meta.MetadataStore; import ome.units.UNITS; @@ -45,7 +49,7 @@ /** * VarianFDFReader is the file format reader for Varian FDF files. */ -public class VarianFDFReader extends FormatReader { +public class VarianFDFReader extends FormatReader implements IAxisOrientationReader { // -- Fields -- @@ -59,6 +63,10 @@ public class VarianFDFReader extends FormatReader { private Length originZ; private String[] units; + private Orientation xAxis; + private Orientation yAxis; + private Orientation zAxis; + // -- Constructor -- /** Constructs a new Varian FDF reader. */ @@ -67,6 +75,18 @@ public VarianFDFReader() { domains = new String[] {FormatTools.MEDICAL_DOMAIN}; } + // -- IAxisOrientationReader API methods -- + + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + Orientation[] axes = new Orientation[5]; + axes[getDimensionOrder().indexOf("X")] = xAxis; + axes[getDimensionOrder().indexOf("Y")] = yAxis; + axes[getDimensionOrder().indexOf("Z")] = zAxis; + return axes; + } + // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ @@ -138,6 +158,9 @@ public void close(boolean fileOnly) throws IOException { originY = null; originZ = null; units = null; + xAxis = null; + yAxis = null; + zAxis = null; } } @@ -215,6 +238,8 @@ private void parseFDF(String file) throws FormatException, IOException { String data = in.readString(Character.toString((char) 0x0c)); String[] lines = data.split("\n"); + String subjectEntry = null; + String subjectPose = null; for (String line : lines) { line = line.trim(); @@ -299,10 +324,37 @@ else if (var.equals("bigendian")) { m.littleEndian = value.equals("0"); in.order(isLittleEndian()); } + else if (var.equals("*position1")) { + subjectEntry = value.replaceAll("\"", ""); + } + else if (var.equals("*position2")) { + subjectPose = value.replaceAll("\"", ""); + } addGlobalMeta(var, value); } + if (subjectEntry != null && subjectPose != null) { + boolean headFirst = subjectEntry.toLowerCase().startsWith("head"); + boolean supine = subjectPose.equalsIgnoreCase("supine"); + boolean prone = subjectPose.equalsIgnoreCase("prone"); + + // only supports position values for which we have data + if (headFirst && supine) { + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + } + else if (!headFirst && prone) { + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.ANTERIOR_TO_POSTERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); + } + else { + LOGGER.warn("Unsupported subject position: '{}', '{}'", subjectEntry, subjectPose); + } + } + if (multifile && files.isEmpty()) { Location thisFile = new Location(file).getAbsoluteFile(); Location parent = thisFile.getParentFile(); From ae73818eba2db99ca1f005fb2af8eaf8502ffada Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Wed, 15 Jul 2026 13:29:35 -0500 Subject: [PATCH 09/22] Initial anatomical orientation work for DICOM --- .../loci/formats/dicom/DicomAttribute.java | 1 + .../src/loci/formats/in/DicomReader.java | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java b/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java index e270a604597..2614b326229 100644 --- a/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java +++ b/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java @@ -225,6 +225,7 @@ public enum DicomAttribute { PREGNANCY_STATUS(0x001021c0), LAST_MENSTRUAL_DATE(0x001021d0), PATIENT_RELIGIOUS_PREFERENCE(0x001021f0), + ANATOMICAL_ORIENTATION_TYPE(0x00102210), PATIENT_COMMENTS(0x00104000), CLINICAL_TRIAL_SPONSOR_NAME(0x00120010), CLINICAL_TRIAL_PROTOCOL_ID(0x00120020), diff --git a/components/formats-bsd/src/loci/formats/in/DicomReader.java b/components/formats-bsd/src/loci/formats/in/DicomReader.java index d6f3db5dbe4..0bf18032270 100644 --- a/components/formats-bsd/src/loci/formats/in/DicomReader.java +++ b/components/formats-bsd/src/loci/formats/in/DicomReader.java @@ -51,11 +51,15 @@ import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.common.Region; +import loci.formats.AnatomicalOrientation; import loci.formats.CoreMetadata; import loci.formats.FilePattern; import loci.formats.FormatException; import loci.formats.FormatTools; +import loci.formats.IAxisOrientationReader; import loci.formats.MetadataTools; +import loci.formats.Orientation; +import loci.formats.OrientationType; import loci.formats.SubResolutionFormatReader; import loci.formats.UnsupportedCompressionException; import loci.formats.codec.Codec; @@ -81,7 +85,7 @@ /** * DicomReader is the file format reader for DICOM files. */ -public class DicomReader extends SubResolutionFormatReader { +public class DicomReader extends SubResolutionFormatReader implements IAxisOrientationReader { // -- Constants -- @@ -138,6 +142,11 @@ public class DicomReader extends SubResolutionFormatReader { private transient RandomAccessInputStream currentTileStream = null; private Set privateContentHighWords = new HashSet(); + private transient String orientation = null; + private Orientation xAxis; + private Orientation yAxis; + private Orientation zAxis; + // -- Constructor -- /** Constructs a new DICOM reader. */ @@ -152,6 +161,18 @@ public DicomReader() { hasCompanionFiles = true; } + // -- IAxisOrientationReader API methods -- + + @Override + public Orientation[] getAxisOrientations() { + FormatTools.assertId(currentId, true, 1); + Orientation[] axes = new Orientation[5]; + axes[getDimensionOrder().indexOf("X")] = xAxis; + axes[getDimensionOrder().indexOf("Y")] = yAxis; + axes[getDimensionOrder().indexOf("Z")] = zAxis; + return axes; + } + // -- ICompressedTileReader API methods -- @Override @@ -470,6 +491,10 @@ public void close(boolean fileOnly) throws IOException { concatenationNumber = null; edf = false; tags = null; + orientation = null; + xAxis = null; + yAxis = null; + zAxis = null; currentTileFile = null; if (currentTileStream != null) { currentTileStream.close(); @@ -1067,6 +1092,7 @@ else if (info.concatenationIndex == 0) { } singleSeriesWatch.stop("updated metadata from file infos"); } + determineAnatomicalOrientation(); watch.start(); @@ -1243,6 +1269,9 @@ else if (infoString.startsWith("MONOCHROME")) { pixelSizeZ = infoNumber.doubleValue(); } break; + case ANATOMICAL_ORIENTATION_TYPE: + orientation = infoString; + break; case IMAGE_POSITION_PATIENT: String[] positions = infoString.replace('\\', '_').split("_"); if (positions.length > 0) { @@ -2103,6 +2132,33 @@ private void updateCoreMetadata(CoreMetadata ms) { } } + /** + * See https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1 + */ + private void determineAnatomicalOrientation() { + if (orientation == null) { + orientation = "BIPED"; + } + if (orientation.equals("BIPED")) { + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.ANTERIOR_TO_POSTERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + } + else if (orientation.equals("QUADRUPED")) { + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + // note this doesn't take into account the body part, so may be slightly incorrect for head/limbs + // presumably this requires mapping the "Body Part Examined" value from the tables defined in + // https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_L.html#table_L-2 + // https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_L.html#table_L-3 + // to the categories in https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1 + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.VENTRAL_TO_DORSAL); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.CAUDAL_TO_CRANIAL); + } + else { + LOGGER.warn("Unsupported anatomical orientation: {}", orientation); + } + } + public String getImageType() { return imageType; } From bea5fd1d931e11051cef09d035f96ec9cd50a3af Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Wed, 15 Jul 2026 20:04:26 -0500 Subject: [PATCH 10/22] Fix a few anatomical orientation issues after further testing --- .../src/loci/formats/in/FakeReader.java | 3 +++ .../src/loci/formats/in/AnalyzeReader.java | 27 ++++++++++--------- .../src/loci/formats/in/BrukerReader.java | 6 ++--- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/components/formats-bsd/src/loci/formats/in/FakeReader.java b/components/formats-bsd/src/loci/formats/in/FakeReader.java index 0e8e493e393..c34344e2703 100644 --- a/components/formats-bsd/src/loci/formats/in/FakeReader.java +++ b/components/formats-bsd/src/loci/formats/in/FakeReader.java @@ -328,6 +328,9 @@ public FakeReader() { @Override public Orientation[] getAxisOrientations() { FormatTools.assertId(currentId, true, 1); + if (orientations.size() == 0) { + return new Orientation[5]; + } return orientations.toArray(new Orientation[orientations.size()]); } diff --git a/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java b/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java index d43f8b48504..42c1053b01b 100644 --- a/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java +++ b/components/formats-gpl/src/loci/formats/in/AnalyzeReader.java @@ -287,39 +287,40 @@ protected void initFile(String id) throws FormatException, IOException { // further reference: // http://www.grahamwideman.com/gw/brain/analyze/formatdoc.htm // https://eeg.sourceforge.net/mri_orientation_notes.html + String orientationDescription = String.valueOf(orient); switch (orient) { - case '0': - // transverse unflipped + case 0: + orientationDescription = "transverse unflipped"; xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); break; - case '1': - // coronal unflipped + case 1: + orientationDescription = "coronal unflipped"; xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); break; - case '2': - // sagittal unflipped + case 2: + orientationDescription = "sagittal unflipped"; xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); break; - case '3': - // transverse flipped + case 3: + orientationDescription = "transverse flipped"; xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.ANTERIOR_TO_POSTERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); break; - case '4': - // coronal flipped + case 4: + orientationDescription = "coronal flipped"; xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); break; - case '5': - // sagittal flipped + case 5: + orientationDescription = "sagittal flipped"; xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); @@ -345,7 +346,7 @@ protected void initFile(String id) throws FormatException, IOException { addGlobalMeta("Pixel minimum", pixelMin); addGlobalMeta("Description", description); addGlobalMeta("Auxiliary file", auxFile); - addGlobalMeta("Orientation", orient); + addGlobalMeta("Orientation", orientationDescription); addGlobalMeta("Originator", originator); addGlobalMeta("Generated", generated); addGlobalMeta("Scan Number", scannum); diff --git a/components/formats-gpl/src/loci/formats/in/BrukerReader.java b/components/formats-gpl/src/loci/formats/in/BrukerReader.java index b97b053b334..309e74e1f36 100644 --- a/components/formats-gpl/src/loci/formats/in/BrukerReader.java +++ b/components/formats-gpl/src/loci/formats/in/BrukerReader.java @@ -497,10 +497,10 @@ else if (key.equals("##$SUBJECT_type")) { subjectType = value; } else if (key.equals("##$SUBJECT_entry")) { - subjectEntry = value.substring(value.indexOf("_") + 1); + subjectEntry = value.substring(value.lastIndexOf("_") + 1); } - else if (key.equals("##$SUBJECT_pose")) { - subjectPose = value.substring(value.indexOf("_") + 1); + else if (key.equals("##$SUBJECT_position")) { + subjectPose = value.substring(value.lastIndexOf("_") + 1); } if (ms != null) { From 46fe0ff9fa94f1e016f314ab5f31531cf3d88d6e Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 28 Sep 2026 16:15:22 -0500 Subject: [PATCH 11/22] MINC: fix physical size reading when units are missing --- .../formats-gpl/src/loci/formats/in/MINCReader.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/formats-gpl/src/loci/formats/in/MINCReader.java b/components/formats-gpl/src/loci/formats/in/MINCReader.java index a31825d71f2..4ee505cc445 100644 --- a/components/formats-gpl/src/loci/formats/in/MINCReader.java +++ b/components/formats-gpl/src/loci/formats/in/MINCReader.java @@ -363,14 +363,20 @@ else if (pixels instanceof double[][][]) { } private Length getStepSize(Hashtable attrs) { + if (!attrs.containsKey("step")) { + return null; + } Double stepSize = Double.parseDouble(attrs.get("step").toString()); - String units = attrs.get("units").toString(); + String units = attrs.containsKey("units") ? attrs.get("units").toString() : null; return FormatTools.getPhysicalSize(stepSize, units); } private Length getStart(Hashtable attrs) { + if (!attrs.containsKey("start")) { + return null; + } Double start = Double.parseDouble(attrs.get("start").toString()); - String units = attrs.get("units").toString(); + String units = attrs.containsKey("units") ? attrs.get("units").toString() : null; return FormatTools.getStagePosition(start, units); } From ae1121acfa4cd4244a337e62202244a114eb1e33 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 28 Sep 2026 16:31:59 -0500 Subject: [PATCH 12/22] FDF: fix handling of files that were once part of a multi-file dataset --- .../src/loci/formats/in/VarianFDFReader.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java b/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java index a4c90d67230..13206b00ae2 100644 --- a/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java +++ b/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java @@ -235,6 +235,8 @@ private void parseFDF(String file) throws FormatException, IOException { CoreMetadata m = core.get(0); boolean storedFloats = false; boolean multifile = false; + boolean slices = false; + boolean echoes = false; String data = in.readString(Character.toString((char) 0x0c)); String[] lines = data.split("\n"); @@ -280,10 +282,12 @@ else if (var.equals("matrix[]")) { } } else if (var.equals("slices")) { + slices = true; m.sizeZ = Integer.parseInt(value); multifile = true; } else if (var.equals("echoes")) { + echoes = true; m.sizeT = Integer.parseInt(value); multifile = true; } @@ -367,6 +371,17 @@ else if (!headFirst && prone) { } } } + // metadata may reflect a multi-file dataset with each slice (Z) + // or echo (T) stored in a separate file, but the dataset may have + // been split up + if (multifile && files.size() == 1) { + if (slices) { + m.sizeZ = 1; + } + if (echoes) { + m.sizeT = 1; + } + } } /** Split a String that represents an array into individual elements. */ From 014f168a8bf8c1f19c3d8533f54b3613a8697aa6 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 28 Sep 2026 16:47:13 -0500 Subject: [PATCH 13/22] FDF: switch X axis from "right-to-left" to "left-to-right" --- .../formats-gpl/src/loci/formats/in/VarianFDFReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java b/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java index 13206b00ae2..adf1a4d0834 100644 --- a/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java +++ b/components/formats-gpl/src/loci/formats/in/VarianFDFReader.java @@ -345,12 +345,12 @@ else if (var.equals("*position2")) { // only supports position values for which we have data if (headFirst && supine) { - xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.LEFT_TO_RIGHT); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); } else if (!headFirst && prone) { - xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.LEFT_TO_RIGHT); yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.ANTERIOR_TO_POSTERIOR); zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); } From 252ce5614df01a86fd7435cca78320bb04f08f3a Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 28 Sep 2026 17:13:45 -0500 Subject: [PATCH 14/22] Bruker: fix head-first supine orientation description --- components/formats-gpl/src/loci/formats/in/BrukerReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/formats-gpl/src/loci/formats/in/BrukerReader.java b/components/formats-gpl/src/loci/formats/in/BrukerReader.java index 309e74e1f36..5068e16d901 100644 --- a/components/formats-gpl/src/loci/formats/in/BrukerReader.java +++ b/components/formats-gpl/src/loci/formats/in/BrukerReader.java @@ -535,8 +535,8 @@ else if (key.equals("##$IM_SIT")) { if (subjectType.equalsIgnoreCase("human")) { if (headFirst && supine) { xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); - yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); - zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.SUPERIOR_TO_INFERIOR); + zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR); } else { LOGGER.warn("Unsupported subject orientation: {}, {}", subjectEntry, subjectPose); From f414a881af0cc947999cc548fb5b5636d9613738 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Mon, 28 Sep 2026 17:31:42 -0500 Subject: [PATCH 15/22] MINC: fix axis flipping --- .../src/loci/formats/in/MINCReader.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/components/formats-gpl/src/loci/formats/in/MINCReader.java b/components/formats-gpl/src/loci/formats/in/MINCReader.java index 4ee505cc445..7abfd059bc7 100644 --- a/components/formats-gpl/src/loci/formats/in/MINCReader.java +++ b/components/formats-gpl/src/loci/formats/in/MINCReader.java @@ -59,6 +59,9 @@ public class MINCReader extends FormatReader implements IAxisOrientationReader { private NetCDFService netcdf; private byte[][][] pixelData; private boolean isMINC2 = false; + private boolean xFlipped = false; + private boolean yFlipped = false; + private boolean zFlipped = false; // -- Constructor -- @@ -75,9 +78,12 @@ public Orientation[] getAxisOrientations() { FormatTools.assertId(currentId, true, 1); // see https://en.wikibooks.org/wiki/MINC/SoftwareDevelopment/MINC2.0_File_Format_Reference#MINC_2.0_coordinate_system return new Orientation[] { - new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.LEFT_TO_RIGHT), - new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.POSTERIOR_TO_ANTERIOR), - new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR), + new Orientation(OrientationType.ANATOMICAL, + xFlipped ? AnatomicalOrientation.RIGHT_TO_LEFT : AnatomicalOrientation.LEFT_TO_RIGHT), + new Orientation(OrientationType.ANATOMICAL, + yFlipped ? AnatomicalOrientation.ANTERIOR_TO_POSTERIOR : AnatomicalOrientation.POSTERIOR_TO_ANTERIOR), + new Orientation(OrientationType.ANATOMICAL, + zFlipped ? AnatomicalOrientation.SUPERIOR_TO_INFERIOR : AnatomicalOrientation.INFERIOR_TO_SUPERIOR), null, null }; } @@ -275,16 +281,19 @@ else if (pixels instanceof double[][][]) { m.sizeX = Integer.parseInt(attrs.get("length").toString()); physicalX = getStepSize(attrs); xPosition = getStart(attrs); + xFlipped = getStepValue(attrs) < 0; attrs = netcdf.getVariableAttributes("/minc-2.0/dimensions/yspace"); m.sizeY = Integer.parseInt(attrs.get("length").toString()); physicalY = getStepSize(attrs); yPosition = getStart(attrs); + yFlipped = getStepValue(attrs) < 0; attrs = netcdf.getVariableAttributes("/minc-2.0/dimensions/zspace"); m.sizeZ = Integer.parseInt(attrs.get("length").toString()); physicalZ = getStepSize(attrs); zPosition = getStart(attrs); + zFlipped = getStepValue(attrs) < 0; } else { m.sizeX = netcdf.getDimension("/xspace"); @@ -362,11 +371,15 @@ else if (pixels instanceof double[][][]) { } } - private Length getStepSize(Hashtable attrs) { + private Double getStepValue(Hashtable attrs) { if (!attrs.containsKey("step")) { return null; } - Double stepSize = Double.parseDouble(attrs.get("step").toString()); + return Double.parseDouble(attrs.get("step").toString()); + } + + private Length getStepSize(Hashtable attrs) { + Double stepSize = getStepValue(attrs); String units = attrs.containsKey("units") ? attrs.get("units").toString() : null; return FormatTools.getPhysicalSize(stepSize, units); } From a93e1b183a7c0c34d45cece0986298fb5f3f422f Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Tue, 29 Sep 2026 12:54:14 -0500 Subject: [PATCH 16/22] Make it easy to flip (multiply by -1) an Orientation --- .../loci/formats/AnatomicalOrientation.java | 56 +++++++++++++++++++ .../src/loci/formats/Orientation.java | 4 ++ .../src/loci/formats/OrientationTerm.java | 3 + 3 files changed, 63 insertions(+) diff --git a/components/formats-api/src/loci/formats/AnatomicalOrientation.java b/components/formats-api/src/loci/formats/AnatomicalOrientation.java index 21c08fa1869..0720b4f5668 100644 --- a/components/formats-api/src/loci/formats/AnatomicalOrientation.java +++ b/components/formats-api/src/loci/formats/AnatomicalOrientation.java @@ -72,4 +72,60 @@ private AnatomicalOrientation(String term) { public String getDefinedTerm() { return definedTerm; } + + @Override + public AnatomicalOrientation flip() { + switch (this) { + case LEFT_TO_RIGHT: + return RIGHT_TO_LEFT; + case RIGHT_TO_LEFT: + return LEFT_TO_RIGHT; + case ANTERIOR_TO_POSTERIOR: + return POSTERIOR_TO_ANTERIOR; + case POSTERIOR_TO_ANTERIOR: + return ANTERIOR_TO_POSTERIOR; + case INFERIOR_TO_SUPERIOR: + return SUPERIOR_TO_INFERIOR; + case SUPERIOR_TO_INFERIOR: + return INFERIOR_TO_SUPERIOR; + case DORSAL_TO_VENTRAL: + return VENTRAL_TO_DORSAL; + case VENTRAL_TO_DORSAL: + return DORSAL_TO_VENTRAL; + case DORSAL_TO_PALMAR: + return PALMAR_TO_DORSAL; + case PALMAR_TO_DORSAL: + return DORSAL_TO_PALMAR; + case DORSAL_TO_PLANTAR: + return PLANTAR_TO_DORSAL; + case PLANTAR_TO_DORSAL: + return DORSAL_TO_PLANTAR; + case ROSTRAL_TO_CAUDAL: + return CAUDAL_TO_ROSTRAL; + case CAUDAL_TO_ROSTRAL: + return ROSTRAL_TO_CAUDAL; + case CRANIAL_TO_CAUDAL: + return CAUDAL_TO_CRANIAL; + case CAUDAL_TO_CRANIAL: + return CRANIAL_TO_CAUDAL; + case PROXIMAL_TO_DISTAL: + return DISTAL_TO_PROXIMAL; + case DISTAL_TO_PROXIMAL: + return PROXIMAL_TO_DISTAL; + case SUPERFICIAL_TO_DEEP: + return DEEP_TO_SUPERFICIAL; + case DEEP_TO_SUPERFICIAL: + return SUPERFICIAL_TO_DEEP; + case APICAL_TO_BASAL: + return BASAL_TO_APICAL; + case BASAL_TO_APICAL: + return APICAL_TO_BASAL; + case APEX_TO_BASE: + return BASE_TO_APEX; + case BASE_TO_APEX: + return APEX_TO_BASE; + default: + throw new IllegalArgumentException(getDefinedTerm()); + } + } } diff --git a/components/formats-api/src/loci/formats/Orientation.java b/components/formats-api/src/loci/formats/Orientation.java index 4334b6bd4ad..d1a53b95040 100644 --- a/components/formats-api/src/loci/formats/Orientation.java +++ b/components/formats-api/src/loci/formats/Orientation.java @@ -62,4 +62,8 @@ public OrientationTerm getOrientationTerm() { return value; } + public Orientation flip() { + return new Orientation(type, value.flip()); + } + } diff --git a/components/formats-api/src/loci/formats/OrientationTerm.java b/components/formats-api/src/loci/formats/OrientationTerm.java index 54c7e660c1d..3ea2b36313d 100644 --- a/components/formats-api/src/loci/formats/OrientationTerm.java +++ b/components/formats-api/src/loci/formats/OrientationTerm.java @@ -39,4 +39,7 @@ public interface OrientationTerm { /** @return the defined vocabulary term for the orientation */ String getDefinedTerm(); + + /** @return the orientation that matches multiplying this orientation's axis by -1 */ + OrientationTerm flip(); } From 24fbf7114a8dcfe4748d1b0fc5e64d7b49929a5f Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Tue, 29 Sep 2026 12:54:49 -0500 Subject: [PATCH 17/22] DICOM: apply patient orientation matrix --- .../src/loci/formats/in/DicomReader.java | 67 ++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/components/formats-bsd/src/loci/formats/in/DicomReader.java b/components/formats-bsd/src/loci/formats/in/DicomReader.java index 8f597cd9984..27621be17e0 100644 --- a/components/formats-bsd/src/loci/formats/in/DicomReader.java +++ b/components/formats-bsd/src/loci/formats/in/DicomReader.java @@ -43,9 +43,7 @@ import java.util.Map; import java.util.Set; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMap.Builder; - +import loci.common.Constants; import loci.common.DataTools; import loci.common.DateTools; import loci.common.Location; @@ -143,6 +141,7 @@ public class DicomReader extends SubResolutionFormatReader implements IAxisOrien private Set privateContentHighWords = new HashSet(); private transient String orientation = null; + private transient double[][] patientOrientation = null; private Orientation xAxis; private Orientation yAxis; private Orientation zAxis; @@ -491,6 +490,7 @@ public void close(boolean fileOnly) throws IOException { edf = false; tags = null; orientation = null; + patientOrientation = null; xAxis = null; yAxis = null; zAxis = null; @@ -1276,6 +1276,13 @@ else if (infoString.startsWith("MONOCHROME")) { case ANATOMICAL_ORIENTATION_TYPE: orientation = infoString; break; + case IMAGE_ORIENTATION_PATIENT: + String[] matrix = infoString.replace('\\', '_').split("_"); + patientOrientation = new double[2][3]; + for (int i=0; i 0) { @@ -2151,24 +2158,66 @@ private void determineAnatomicalOrientation() { if (orientation == null) { orientation = "BIPED"; } + Orientation[] baseAxes = new Orientation[3]; if (orientation.equals("BIPED")) { - xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); - yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.ANTERIOR_TO_POSTERIOR); - zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); + baseAxes[0] = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + baseAxes[1] = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.ANTERIOR_TO_POSTERIOR); + baseAxes[2] = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.INFERIOR_TO_SUPERIOR); } else if (orientation.equals("QUADRUPED")) { - xAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); + baseAxes[0] = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.RIGHT_TO_LEFT); // note this doesn't take into account the body part, so may be slightly incorrect for head/limbs // presumably this requires mapping the "Body Part Examined" value from the tables defined in // https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_L.html#table_L-2 // https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_L.html#table_L-3 // to the categories in https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1 - yAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.VENTRAL_TO_DORSAL); - zAxis = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.CAUDAL_TO_CRANIAL); + baseAxes[1] = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.VENTRAL_TO_DORSAL); + baseAxes[2] = new Orientation(OrientationType.ANATOMICAL, AnatomicalOrientation.CAUDAL_TO_CRANIAL); } else { LOGGER.warn("Unsupported anatomical orientation: {}", orientation); } + + // apply patient orientation matrix + // see equation C.7.6.2.1-1 in https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1 + + double[] xOrientation = patientOrientation[0]; + if (useAxis(0, 0)) { + xAxis = xOrientation[0] > 0 ? baseAxes[0] : baseAxes[0].flip(); + baseAxes[0] = null; + } + if (useAxis(0, 1)) { + xAxis = xOrientation[1] > 0 ? baseAxes[1] : baseAxes[1].flip(); + baseAxes[1] = null; + } + if (useAxis(0, 2)) { + xAxis = xOrientation[2] > 0 ? baseAxes[2] : baseAxes[2].flip(); + baseAxes[2] = null; + } + + double[] yOrientation = patientOrientation[1]; + if (useAxis(1, 0)) { + yAxis = yOrientation[0] > 0 ? baseAxes[0] : baseAxes[0].flip(); + baseAxes[0] = null; + } + else if (useAxis(1, 1)) { + yAxis = yOrientation[1] > 0 ? baseAxes[1] : baseAxes[1].flip(); + baseAxes[1] = null; + } + else if (useAxis(1, 2)) { + yAxis = yOrientation[2] > 0 ? baseAxes[2] : baseAxes[2].flip(); + baseAxes[2] = null; + } + for (int i=0; i Constants.EPSILON; } public String getImageType() { From 67f03be3543416d3e8c4bb5d23f37df331acbd3c Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Tue, 29 Sep 2026 13:19:50 -0500 Subject: [PATCH 18/22] DICOM: handle missing patient orientation --- .../src/loci/formats/in/DicomReader.java | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/components/formats-bsd/src/loci/formats/in/DicomReader.java b/components/formats-bsd/src/loci/formats/in/DicomReader.java index 27621be17e0..d638c93fea6 100644 --- a/components/formats-bsd/src/loci/formats/in/DicomReader.java +++ b/components/formats-bsd/src/loci/formats/in/DicomReader.java @@ -2181,39 +2181,46 @@ else if (orientation.equals("QUADRUPED")) { // apply patient orientation matrix // see equation C.7.6.2.1-1 in https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1 - double[] xOrientation = patientOrientation[0]; - if (useAxis(0, 0)) { - xAxis = xOrientation[0] > 0 ? baseAxes[0] : baseAxes[0].flip(); - baseAxes[0] = null; - } - if (useAxis(0, 1)) { - xAxis = xOrientation[1] > 0 ? baseAxes[1] : baseAxes[1].flip(); - baseAxes[1] = null; - } - if (useAxis(0, 2)) { - xAxis = xOrientation[2] > 0 ? baseAxes[2] : baseAxes[2].flip(); - baseAxes[2] = null; - } + if (patientOrientation != null) { + double[] xOrientation = patientOrientation[0]; + if (useAxis(0, 0)) { + xAxis = xOrientation[0] > 0 ? baseAxes[0] : baseAxes[0].flip(); + baseAxes[0] = null; + } + if (useAxis(0, 1)) { + xAxis = xOrientation[1] > 0 ? baseAxes[1] : baseAxes[1].flip(); + baseAxes[1] = null; + } + if (useAxis(0, 2)) { + xAxis = xOrientation[2] > 0 ? baseAxes[2] : baseAxes[2].flip(); + baseAxes[2] = null; + } - double[] yOrientation = patientOrientation[1]; - if (useAxis(1, 0)) { - yAxis = yOrientation[0] > 0 ? baseAxes[0] : baseAxes[0].flip(); - baseAxes[0] = null; - } - else if (useAxis(1, 1)) { - yAxis = yOrientation[1] > 0 ? baseAxes[1] : baseAxes[1].flip(); - baseAxes[1] = null; - } - else if (useAxis(1, 2)) { - yAxis = yOrientation[2] > 0 ? baseAxes[2] : baseAxes[2].flip(); - baseAxes[2] = null; - } - for (int i=0; i 0 ? baseAxes[0] : baseAxes[0].flip(); + baseAxes[0] = null; + } + else if (useAxis(1, 1)) { + yAxis = yOrientation[1] > 0 ? baseAxes[1] : baseAxes[1].flip(); + baseAxes[1] = null; + } + else if (useAxis(1, 2)) { + yAxis = yOrientation[2] > 0 ? baseAxes[2] : baseAxes[2].flip(); + baseAxes[2] = null; + } + for (int i=0; i Date: Tue, 29 Sep 2026 20:28:47 -0500 Subject: [PATCH 19/22] Nifti: adjust axis orientation based on transform matrix --- .../src/loci/formats/in/NiftiReader.java | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/components/formats-gpl/src/loci/formats/in/NiftiReader.java b/components/formats-gpl/src/loci/formats/in/NiftiReader.java index 03510f33123..9b99921d705 100644 --- a/components/formats-gpl/src/loci/formats/in/NiftiReader.java +++ b/components/formats-gpl/src/loci/formats/in/NiftiReader.java @@ -83,6 +83,10 @@ public class NiftiReader extends FormatReader implements IAxisOrientationReader private Unit spatialUnit = UNITS.MICROMETER; private Unit