supplier, int nTries, long waitTimeMillis) throws InterruptedException {
19 |
20 | return tryWaitRepeat(supplier, nTries, waitTimeMillis, 2);
21 | }
22 |
23 | /**
24 | * Attempts to execute a provided {@link Supplier} multiple times, with an increasing wait period
25 | * between each attempt. If the supplier returns a non-null result, it is wrapped in an
26 | * {@code Optional} and returned. If all attempts fail or return null, an empty {@link Optional} is returned.
27 | *
28 | * The wait time between attempts increases after each failure, multiplied by a specified factor.
29 | *
30 | * @param the type of result provided by the supplier
31 | * @param supplier the {@link Supplier} function that provides the result to be evaluated. The
32 | * function may throw a {@link RuntimeException} if it fails, which will be caught and retried.
33 | * @param nTries the maximum number of attempts to invoke the supplier
34 | * @param initialWaitTimeMillis the initial wait time in milliseconds before retrying after the first failure
35 | * @param waitTimeMultiplier the multiplier to apply to the wait time after each failure, increasing
36 | * the wait time for subsequent retries
37 | * @return an {@link Optional} containing the result from the supplier if a non-null result is returned
38 | * before the maximum number of tries, or an empty {@code Optional} if all attempts fail or
39 | * return null
40 | * @throws InterruptedException thrown if interrupted while waiting
41 | */
42 | public static Optional tryWaitRepeat(
43 | final Supplier supplier,
44 | final int nTries,
45 | final long initialWaitTimeMillis,
46 | final int waitTimeMultiplier) throws InterruptedException {
47 |
48 | int i = 0;
49 | long waitTime = initialWaitTimeMillis;
50 | while (i < nTries) {
51 |
52 | if (i == nTries)
53 | break;
54 |
55 | try {
56 | T result = supplier.get();
57 | if (result != null)
58 | return Optional.of(result);
59 | } catch (RuntimeException e) {}
60 |
61 | Thread.sleep(waitTime);
62 | waitTime *= waitTimeMultiplier;
63 | i++;
64 | }
65 |
66 | return Optional.empty();
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/CustomMetadataExamples.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5;
27 |
28 | import org.janelia.saalfeldlab.n5.metadata.imagej.ImagePlusMetadataTemplate;
29 | import org.janelia.saalfeldlab.n5.metadata.imagej.MetadataTemplateMapper;
30 |
31 | import ij.IJ;
32 | import ij.ImagePlus;
33 |
34 | public class CustomMetadataExamples
35 | {
36 |
37 | public static void main( final String[] args ) throws Exception
38 | {
39 | final ImagePlus mitosisImage = IJ.openImage( "/home/john/tmp/mitosis.tif" );
40 | System.out.println( mitosisImage );
41 |
42 | System.out.println( "resolution only mapper: " );
43 | runWith( mitosisImage, MetadataTemplateMapper.RESOLUTION_ONLY_MAPPER );
44 |
45 | System.out.println( "COSEM mapper: " );
46 | runWith( mitosisImage, MetadataTemplateMapper.COSEM_MAPPER );
47 |
48 | }
49 |
50 | public static void runWith( final ImagePlus imp, final String translationSpec ) throws Exception
51 | {
52 | System.out.println( " " );
53 | System.out.println( translationSpec );
54 | final ImagePlusMetadataTemplate metaTemplate = ImagePlusMetadataTemplate.readMetadataStatic( imp );
55 | final MetadataTemplateMapper mapper = new MetadataTemplateMapper( translationSpec );
56 | System.out.println( " " );
57 |
58 | }
59 |
60 | public static final String perAxisMapper = "{\n" +
61 | "\t\"x\": [.xResolution, .yResolution, .zResolution],\n" +
62 | "\t\"translate\": [.xOrigin, .yOrigin, .zOrigin],\n" +
63 | "\t\"axes\": [.axis0, .axis1, .axis2, .axis3, .axis4],\n" +
64 | "\t\"units\": [.xUnit, .yUnit, .zUnit]\n" +
65 | "\t}\n" +
66 | "}";
67 |
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/converters/UshortConverterTests.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.converters;
2 |
3 | import org.janelia.saalfeldlab.n5.converters.UnsignedShortLinearConverter;
4 | import org.junit.Assert;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 |
8 | import net.imglib2.img.array.ArrayImg;
9 | import net.imglib2.img.array.ArrayImgs;
10 | import net.imglib2.img.array.ArrayRandomAccess;
11 | import net.imglib2.img.basictypeaccess.array.LongArray;
12 | import net.imglib2.type.numeric.integer.LongType;
13 | import net.imglib2.type.numeric.integer.UnsignedShortType;
14 |
15 | public class UshortConverterTests
16 | {
17 |
18 | private final long maxUshort = UnsignedShortLinearConverter.MAXUSHORT;
19 |
20 | private ArrayImg< LongType, LongArray > img;
21 |
22 | private ArrayImg< LongType, LongArray > imgBig;
23 |
24 | @Before
25 | public void setUp()
26 | {
27 | img = ArrayImgs.longs( new long[]{ maxUshort, maxUshort + 1000, maxUshort + 2000 }, 3 );
28 | imgBig = ArrayImgs.longs( new long[]{ maxUshort, 2 * maxUshort, 3 * maxUshort }, 3 );
29 | }
30 |
31 | @Test
32 | public void testLUTConvert()
33 | {
34 | LongType in = new LongType();
35 | UnsignedShortType v = new UnsignedShortType();
36 | UnsignedShortLUTConverter< LongType > conv = new UnsignedShortLUTConverter<>( img );
37 |
38 | in.set( maxUshort );
39 | conv.accept( in, v );
40 | Assert.assertEquals( "lut, min to zero", 0, v.getInteger() );
41 |
42 | in.set( maxUshort + 1000 );
43 | conv.accept( in, v );
44 | Assert.assertEquals( "lut, mid to one", 1, v.getInteger() );
45 |
46 | in.set( maxUshort + 2000 );
47 | conv.accept( in, v );
48 | Assert.assertEquals( "lut, max to two", 2, v.getInteger() );
49 |
50 | UnsignedShortLUTConverter< LongType > convBig = new UnsignedShortLUTConverter<>( imgBig );
51 |
52 | in.set( maxUshort );
53 | convBig.accept( in, v );
54 | Assert.assertEquals( "big lut, min to zero", 0, v.getInteger() );
55 |
56 | in.set( 2 * maxUshort );
57 | convBig.accept( in, v );
58 | Assert.assertEquals( "big lut, mid to one", 1, v.getInteger() );
59 |
60 | in.set( 3 * maxUshort );
61 | convBig.accept( in, v );
62 | Assert.assertEquals( "big lut, max to two", 2, v.getInteger() );
63 | }
64 |
65 | @Test
66 | public void testLinearConvert()
67 | {
68 | UnsignedShortLinearConverter< LongType > conv = new UnsignedShortLinearConverter<>( img );
69 |
70 | LongType in = new LongType();
71 | UnsignedShortType v = new UnsignedShortType();
72 | in.set( maxUshort );
73 | conv.accept( in, v );
74 | Assert.assertEquals( "small range, min to zero", 0, v.getInteger() );
75 |
76 | in.set( maxUshort + 2000 );
77 | conv.accept( in, v );
78 | Assert.assertEquals( "small range, max to 2k", 2000, v.getInteger() );
79 |
80 |
81 | UnsignedShortLinearConverter< LongType > convBig = new UnsignedShortLinearConverter<>( imgBig );
82 |
83 | in.set( maxUshort );
84 | convBig.accept( in, v );
85 | Assert.assertEquals( "big range, min to zero", 0, v.getInteger() );
86 |
87 | in.set( 3 * maxUshort );
88 | convBig.accept( in, v );
89 | Assert.assertEquals( "big range, max to shortMax", maxUshort, v.getInteger() );
90 |
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/ImageplusMetadata.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.metadata.imagej;
27 |
28 | import ij.ImagePlus;
29 | import net.imglib2.img.Img;
30 | import net.imglib2.img.display.imagej.ImageJFunctions;
31 | import net.imglib2.type.NativeType;
32 | import net.imglib2.util.Util;
33 |
34 | import java.io.IOException;
35 | import java.util.Arrays;
36 |
37 | import org.janelia.saalfeldlab.n5.DataType;
38 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
39 | import org.janelia.saalfeldlab.n5.RawCompression;
40 | import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
41 | import org.janelia.saalfeldlab.n5.universe.metadata.N5DatasetMetadata;
42 |
43 | /**
44 | * A interface for reading and writing metadata to an {@link ImagePlus}.
45 | *
46 | * @param
47 | * the metadata type
48 | * @author John Bogovic
49 | */
50 | public interface ImageplusMetadata {
51 |
52 | /**
53 | * Modify the metadata of the {@link ImagePlus} according to the given
54 | * metadata.
55 | *
56 | * @param t
57 | * metadata
58 | * @param ip
59 | * ImagePlus
60 | * @throws IOException
61 | * the io exception
62 | */
63 | public void writeMetadata(T t, ImagePlus ip) throws IOException;
64 |
65 | /**
66 | * Create and return a new metadata object from the given {@link ImagePlus}.
67 | *
68 | * @param ip the ImagePlus
69 | * @return the metadata extracted from the ImagePlus
70 | * @throws IOException the io exception
71 | */
72 | public T readMetadata(ImagePlus ip) throws IOException;
73 |
74 | public static > DatasetAttributes datasetAttributes( final ImagePlus imp )
75 | {
76 | @SuppressWarnings("unchecked")
77 | final Img img = (Img)ImageJFunctions.wrap(imp);
78 | final DataType dtype = N5Utils.dataType(Util.getTypeFromInterval(img));
79 | final long[] dims = img.dimensionsAsLongArray();
80 |
81 | return new DatasetAttributes(
82 | dims,
83 | Arrays.stream(dims).mapToInt(x -> (int)x).toArray(),
84 | dtype,
85 | new RawCompression());
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/exps/GithubActionsIssuesTests.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.exps;
2 |
3 | import static org.junit.Assert.assertNotNull;
4 |
5 | import java.io.File;
6 | import java.io.IOException;
7 | import java.nio.file.Files;
8 | import java.util.concurrent.ExecutorService;
9 | import java.util.concurrent.Executors;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | import org.janelia.saalfeldlab.n5.DataType;
13 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
14 | import org.janelia.saalfeldlab.n5.N5FSReader;
15 | import org.janelia.saalfeldlab.n5.N5FSWriter;
16 | import org.janelia.saalfeldlab.n5.RawCompression;
17 | import org.junit.AfterClass;
18 | import org.junit.BeforeClass;
19 | import org.junit.Ignore;
20 | import org.junit.Test;
21 |
22 | public class GithubActionsIssuesTests {
23 |
24 | private static File baseDir;
25 |
26 | @BeforeClass
27 | public static void before() {
28 |
29 | try {
30 | baseDir = Files.createTempDirectory("n5-ij-tests-").toFile();
31 | baseDir.deleteOnExit();
32 | } catch (IOException e) {
33 | e.printStackTrace();
34 | }
35 |
36 | }
37 |
38 | @AfterClass
39 | public static void after() {
40 |
41 | baseDir.delete();
42 | }
43 |
44 | @Test
45 | public void n5WriteReadWithExecutor() {
46 |
47 | System.out.println("n5WriteReadWithExecutor");
48 |
49 | final boolean cacheAttributes = false;
50 | try (final N5FSWriter n5w = new N5FSWriter(baseDir.getAbsolutePath(), cacheAttributes)) {
51 |
52 | for (int i = 0; i < 50; i++) {
53 |
54 | final String dset = String.format("%04d", i);
55 | ExecutorService exec = Executors.newFixedThreadPool(1);
56 | exec.submit(() -> {
57 | try (final N5FSReader n5r = new N5FSReader(baseDir.getAbsolutePath())) {
58 | final DatasetAttributes attrs = n5r.getDatasetAttributes(dset);
59 | assertNotNull("null attrs for dataset: " + dset, attrs);
60 | n5r.close();
61 | }
62 | });
63 | exec.shutdown();
64 | try {
65 | exec.awaitTermination(1000, TimeUnit.MILLISECONDS);
66 | } catch (InterruptedException e) { }
67 |
68 | n5w.remove(dset);
69 | }
70 | n5w.remove();
71 | n5w.close();
72 | }
73 | }
74 |
75 | @Test
76 | public void n5WriteReadSameInstance() {
77 |
78 | System.out.println("n5WriteReadSameInstance");
79 |
80 | final boolean cacheAttributes = false;
81 | try (final N5FSWriter n5w = new N5FSWriter(baseDir.getAbsolutePath(), cacheAttributes)) {
82 |
83 | for (int i = 0; i < 50; i++) {
84 |
85 | final String dset = String.format("%04d", i);
86 | n5w.createDataset(dset,
87 | new DatasetAttributes(new long[]{6, 5, 4}, new int[]{6, 5, 4}, DataType.FLOAT32, new RawCompression()));
88 |
89 | final DatasetAttributes attrs = n5w.getDatasetAttributes(dset);
90 | assertNotNull("null attrs for dataset: " + dset , attrs);
91 |
92 | n5w.remove(dset);
93 | }
94 | n5w.remove();
95 | n5w.close();
96 | }
97 | }
98 |
99 | @Test
100 | public void n5WriteRead() {
101 |
102 | System.out.println("n5WriteRead");
103 |
104 | // seems to work
105 | try (final N5FSWriter n5w = new N5FSWriter(baseDir.getAbsolutePath())) {
106 |
107 | for (int i = 0; i < 50; i++) {
108 |
109 | final String dset = String.format("%04d", i);
110 | n5w.createDataset(dset,
111 | new DatasetAttributes(new long[]{6, 5, 4}, new int[]{6, 5, 4}, DataType.FLOAT32, new RawCompression()));
112 |
113 | try (final N5FSReader n5r = new N5FSReader(baseDir.getAbsolutePath())) {
114 |
115 | final DatasetAttributes attrs = n5r.getDatasetAttributes(dset);
116 | assertNotNull("null attrs for dataset: " + dset , attrs);
117 | n5r.close();
118 | }
119 | n5w.remove(dset);
120 | }
121 | n5w.remove();
122 | n5w.close();
123 | }
124 | }
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/TranslationResultPanel.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.ui;
2 |
3 | import java.awt.GridBagConstraints;
4 | import java.awt.GridBagLayout;
5 | import java.awt.Insets;
6 | import java.util.HashMap;
7 |
8 | import javax.swing.JLabel;
9 | import javax.swing.JPanel;
10 | import javax.swing.JScrollPane;
11 | import javax.swing.JTextArea;
12 |
13 | import com.fasterxml.jackson.core.JsonProcessingException;
14 | import com.fasterxml.jackson.core.util.DefaultIndenter;
15 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
16 | import com.fasterxml.jackson.databind.JsonMappingException;
17 | import com.fasterxml.jackson.databind.ObjectMapper;
18 | import com.fasterxml.jackson.databind.SerializationFeature;
19 | import com.google.gson.Gson;
20 |
21 | public class TranslationResultPanel {
22 |
23 | private float fontScale = 1.0f;
24 |
25 | private JTextArea original;
26 |
27 | private JTextArea translated;
28 |
29 | private static final int OUTER_PAD = 8;
30 | private static final int BUTTON_PAD = 3;
31 | private static final int MID_PAD = 5;
32 |
33 | private final ObjectMapper objMapper;
34 |
35 | private final DefaultPrettyPrinter prettyPrinter;
36 |
37 | public TranslationResultPanel()
38 | {
39 | prettyPrinter = new DefaultPrettyPrinter();
40 | DefaultPrettyPrinter.Indenter i = new DefaultIndenter(" ", "\n");
41 | prettyPrinter.indentArraysWith(i);
42 | prettyPrinter.indentObjectsWith(i);
43 |
44 | objMapper = new ObjectMapper();
45 | objMapper.enable(SerializationFeature.INDENT_OUTPUT);
46 | objMapper.setDefaultPrettyPrinter(prettyPrinter);
47 | }
48 |
49 | public JTextArea getOriginal() {
50 |
51 | return original;
52 | }
53 |
54 | public JTextArea getTranslated() {
55 |
56 | return translated;
57 | }
58 |
59 | private String prettyJson( final Gson gson, final Object obj )
60 | {
61 | String jsonTxt = gson.toJson( obj );
62 | String jsonPretty = jsonTxt;
63 | try {
64 | HashMap,?> tmpObj = objMapper.readValue( jsonTxt, HashMap.class );
65 | jsonPretty = objMapper.writer(prettyPrinter).writeValueAsString(tmpObj);
66 | } catch (JsonMappingException e) { } catch (JsonProcessingException e) { }
67 | return jsonPretty;
68 | }
69 |
70 | public void set( final Gson gson, final Object orig, final Object xlated ) {
71 | original.setText( prettyJson( gson, orig ));
72 | translated.setText( prettyJson( gson, xlated ));
73 | }
74 |
75 | public JPanel buildPanel() {
76 |
77 | original = new JTextArea();
78 | original.setFont(original.getFont().deriveFont((float) fontScale * 18f));
79 | original.setText("");
80 |
81 | translated = new JTextArea();
82 | translated.setFont(translated.getFont().deriveFont((float) fontScale * 18f));
83 | translated.setText("");
84 |
85 | final JPanel panel = new JPanel();
86 | panel.setLayout(new GridBagLayout());
87 |
88 | final GridBagConstraints gbc = new GridBagConstraints();
89 | gbc.gridx = 0;
90 | gbc.gridy = 0;
91 | gbc.gridwidth = 1;
92 | gbc.gridheight = 1;
93 | gbc.weightx = 0.5;
94 | gbc.weighty = 0.0;
95 | gbc.fill = GridBagConstraints.HORIZONTAL;
96 | gbc.insets = new Insets(OUTER_PAD, OUTER_PAD, MID_PAD, BUTTON_PAD);
97 |
98 | gbc.anchor = GridBagConstraints.LINE_START;
99 | panel.add( new JLabel("Original"), gbc );
100 |
101 | gbc.gridy = 1;
102 | gbc.weighty = 1.0;
103 | gbc.gridheight = 2;
104 | gbc.fill = GridBagConstraints.BOTH;
105 | panel.add(new JScrollPane(original), gbc);
106 |
107 | gbc.gridx = 1;
108 | gbc.gridheight = 1;
109 | gbc.gridy = 0;
110 | gbc.weighty = 0.0;
111 | gbc.anchor = GridBagConstraints.LINE_START;
112 | panel.add( new JLabel("Translated"), gbc );
113 |
114 | gbc.gridx = 1;
115 | gbc.gridy = 1;
116 | gbc.weighty = 1.0;
117 | panel.add(new JScrollPane(translated), gbc);
118 |
119 | return panel;
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/N5SpatialKeySpecDialog.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.ui;
27 |
28 | import java.util.Optional;
29 |
30 | import javax.swing.BoxLayout;
31 | import javax.swing.JLabel;
32 | import javax.swing.JPanel;
33 | import javax.swing.JTextField;
34 |
35 | import org.janelia.saalfeldlab.n5.universe.metadata.N5GenericSingleScaleMetadataParser;
36 |
37 | public class N5SpatialKeySpecDialog {
38 |
39 | private JTextField resolutionField;
40 | private JTextField offsetField;
41 | private JTextField unitField;
42 | private JTextField downsamplingFactorsField;
43 | private JTextField minIntensityField;
44 | private JTextField maxIntensityField;
45 |
46 | public N5SpatialKeySpecDialog() {}
47 |
48 | public N5GenericSingleScaleMetadataParser getParser() {
49 |
50 | return new N5GenericSingleScaleMetadataParser(
51 | minIntensityField.getText(), maxIntensityField.getText(),
52 | resolutionField.getText(), offsetField.getText(), unitField.getText(),
53 | downsamplingFactorsField.getText());
54 | }
55 |
56 | /**
57 | * Returns an optional containing the parser if any fields are non empty.
58 | *
59 | * @return the parser optional
60 | */
61 | public Optional getParserOptional() {
62 |
63 | if (anyNonEmptyFields())
64 | return Optional.of(getParser());
65 | else
66 | return Optional.empty();
67 | }
68 |
69 | public boolean anyNonEmptyFields() {
70 |
71 | return (resolutionField != null && !resolutionField.getText().isEmpty() ) ||
72 | (offsetField != null && !offsetField.getText().isEmpty()) ||
73 | (downsamplingFactorsField != null && !downsamplingFactorsField.getText().isEmpty()) ||
74 | (minIntensityField != null && !minIntensityField.getText().isEmpty()) ||
75 | (maxIntensityField != null && !maxIntensityField.getText().isEmpty());
76 | }
77 |
78 | public JPanel buildPanel()
79 | {
80 | final JPanel panel = new JPanel();
81 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
82 |
83 | panel.add( new JLabel( "Resolution key"));
84 | resolutionField = new JTextField();
85 | panel.add( resolutionField );
86 |
87 | panel.add( new JLabel( "Offset key"));
88 | offsetField = new JTextField();
89 | panel.add( offsetField );
90 |
91 | panel.add( new JLabel( "Unit key"));
92 | unitField = new JTextField();
93 | panel.add( unitField );
94 |
95 | panel.add( new JLabel( "Downsampling factors key"));
96 | downsamplingFactorsField = new JTextField();
97 | panel.add( downsamplingFactorsField );
98 |
99 | panel.add( new JLabel( "min intensity key"));
100 | minIntensityField = new JTextField();
101 | panel.add( minIntensityField );
102 |
103 | panel.add( new JLabel( "max intensity key"));
104 | maxIntensityField = new JTextField();
105 | panel.add( maxIntensityField );
106 |
107 | return panel;
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/N5MetadataTranslationPanel.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.ui;
27 |
28 | import java.awt.BorderLayout;
29 | import java.util.Optional;
30 | import java.util.function.Predicate;
31 |
32 | import javax.swing.BoxLayout;
33 | import javax.swing.JLabel;
34 | import javax.swing.JPanel;
35 | import javax.swing.JScrollPane;
36 | import javax.swing.JTextArea;
37 |
38 | import org.janelia.saalfeldlab.n5.N5Reader;
39 | import org.janelia.saalfeldlab.n5.universe.metadata.canonical.CanonicalMetadata;
40 | import org.janelia.saalfeldlab.n5.universe.translation.TranslatedN5Reader;
41 |
42 | import com.google.gson.Gson;
43 |
44 | public class N5MetadataTranslationPanel {
45 |
46 | private static final String DEFAULT_TEXT = "include \"n5\";";
47 |
48 | private float fontScale = 1.0f;
49 |
50 | private JTextArea textArea;
51 |
52 | private Predicate filter;
53 |
54 | public N5MetadataTranslationPanel() {
55 | }
56 |
57 | public N5MetadataTranslationPanel(final float fontScale) {
58 | this.fontScale = fontScale;
59 | }
60 |
61 | public void setFilter(Predicate filter) {
62 | this.filter = filter;
63 | }
64 |
65 | public TranslatedN5Reader getTranslatedN5( final N5Reader n5, final Gson gson ) {
66 |
67 | final TranslatedN5Reader translatedN5 = new TranslatedN5Reader(n5, gson, textArea.getText(), ".");
68 | if( translatedN5.getTranslation().getTranslationFunction().isValid())
69 | return translatedN5;
70 | else
71 | return null;
72 | }
73 |
74 | /**
75 | * Returns an optional containing the parser if any fields are non empty.
76 | *
77 | * @param n5 the {@link N5Reader}
78 | * @param gson the {@link Gson}
79 | * @return the parser optional
80 | */
81 | public Optional getTranslatedN5Optional( final N5Reader n5, final Gson gson ) {
82 |
83 | if (isTranslationProvided())
84 | return Optional.ofNullable(getTranslatedN5( n5, gson ));
85 | else
86 | return Optional.empty();
87 | }
88 |
89 | public boolean isTranslationProvided() {
90 |
91 | if( textArea == null )
92 | return false;
93 |
94 | final String txt = textArea.getText();
95 | final boolean textSet = !( txt.isEmpty() || txt.equals(DEFAULT_TEXT));
96 | return textSet;
97 | // if( textSet ) {
98 | // return TranslatedTreeMetadataParser.testTranslation( txt );
99 | // }
100 | // else
101 | // return false;
102 | }
103 |
104 | public JPanel buildPanel()
105 | {
106 | final JPanel panel = new JPanel();
107 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
108 |
109 | panel.add( new JLabel("Translation specification"));
110 |
111 | textArea = new JTextArea();
112 | textArea.setFont(textArea.getFont().deriveFont((float) fontScale * 18f));
113 | textArea.setText(DEFAULT_TEXT);
114 |
115 | final JScrollPane textView = new JScrollPane( textArea );
116 | panel.add( textView, BorderLayout.CENTER );
117 |
118 | return panel;
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/CosemToImagePlus.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.metadata.imagej;
2 |
3 | import java.io.IOException;
4 | import java.util.Arrays;
5 |
6 | import org.apache.commons.lang.ArrayUtils;
7 | import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata;
8 | import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata.CosemTransform;
9 |
10 | import ij.ImagePlus;
11 | import ij.measure.Calibration;
12 |
13 | public class CosemToImagePlus extends SpatialMetadataToImagePlus {
14 |
15 | private boolean includeChannelAxis = false;
16 |
17 | public void includeChannelAxis(boolean includeChannelAxis) {
18 |
19 | this.includeChannelAxis = includeChannelAxis;
20 | }
21 |
22 | @Override
23 | public void writeMetadata(final N5CosemMetadata t, final ImagePlus ip) throws IOException {
24 |
25 | ip.setTitle(t.getPath());
26 | final Calibration cal = ip.getCalibration();
27 |
28 | // dims are aligned with t.getAxes()
29 | final long[] dims = ArrayUtils.clone(t.getAttributes().getDimensions());
30 | // now they're aligned with t.getCosmTransform.axes
31 | ArrayUtils.reverse(dims);
32 |
33 | // spatial indices are aligned with t.getCosemTransform().axes);
34 | final int[] spatialIndexes = spatialIndexes(t.getCosemTransform().axes);
35 |
36 | final CosemTransform transform = t.getCosemTransform();
37 | cal.pixelWidth = spatialIndexes[0] > -1 ? transform.scale[spatialIndexes[0]] : 1;
38 | cal.pixelHeight = spatialIndexes[1] > -1 ? transform.scale[spatialIndexes[1]] : 1;
39 | cal.pixelDepth = spatialIndexes[2] > -1 ? transform.scale[spatialIndexes[2]] : 1;
40 |
41 | cal.xOrigin = spatialIndexes[0] > -1 ? transform.translate[spatialIndexes[0]] : 0;
42 | cal.yOrigin = spatialIndexes[1] > -1 ? transform.translate[spatialIndexes[1]] : 0;
43 | cal.zOrigin = spatialIndexes[2] > -1 ? transform.translate[spatialIndexes[2]] : 0;
44 |
45 | cal.setUnit(t.unit());
46 |
47 | final int ci = channelIndex(t.getAxisLabels());
48 | final int nc = ci == -1 ? 1 : (int)dims[ci];
49 |
50 | final int ti = timeIndex(transform.axes);
51 | final int nt = ti == -1 ? 1 : (int)dims[ti];
52 | if (ti != -1)
53 | cal.frameInterval = transform.scale[ti];
54 |
55 | final int nz = spatialIndexes.length > 2 && spatialIndexes[2] != -1 ? (int)dims[spatialIndexes[2]] : 1;
56 |
57 | // ip.setDimensions(nc, nz, nt);
58 | }
59 |
60 | @Override
61 | public N5CosemMetadata readMetadata(final ImagePlus imp) throws IOException {
62 |
63 | int nd = 2;
64 | if (includeChannelAxis && imp.getNChannels() > 1) {
65 | nd++;
66 | }
67 | if (imp.getNSlices() > 1) {
68 | nd++;
69 | }
70 | if (imp.getNFrames() > 1) {
71 | nd++;
72 | }
73 |
74 | final String[] axes = new String[nd];
75 | final double[] scale = new double[nd];
76 | Arrays.fill(scale, 1);
77 |
78 | final double[] translation = new double[nd];
79 |
80 | int k = nd - 1;
81 | scale[k] = imp.getCalibration().pixelWidth;
82 | translation[k] = imp.getCalibration().xOrigin;
83 | axes[k--] = "x";
84 |
85 | scale[k] = imp.getCalibration().pixelHeight;
86 | translation[k] = imp.getCalibration().yOrigin;
87 | axes[k--] = "y";
88 |
89 | if (includeChannelAxis && imp.getNChannels() > 1) {
90 | axes[k--] = "c";
91 | }
92 | if (imp.getNSlices() > 1) {
93 | scale[k] = imp.getCalibration().pixelDepth;
94 | translation[k] = imp.getCalibration().zOrigin;
95 | axes[k--] = "z";
96 | }
97 | if (imp.getNFrames() > 1) {
98 | axes[k--] = "t";
99 | }
100 |
101 | // unit
102 | final String[] units = new String[nd];
103 | Arrays.fill(units, imp.getCalibration().getUnit());
104 |
105 | return new N5CosemMetadata("",
106 | new CosemTransform(axes, scale, translation, units),
107 | ImageplusMetadata.datasetAttributes(imp));
108 | }
109 |
110 | private int[] spatialIndexes(final String[] axes) {
111 |
112 | final int[] spaceIndexes = new int[3];
113 | Arrays.fill(spaceIndexes, -1);
114 |
115 | // COSEM scales and translations are in c-order
116 | // but detect the axis types to be extra safe
117 | for (int i = 0; i < axes.length; i++) {
118 | if (axes[i].equals("x"))
119 | spaceIndexes[0] = i;
120 | else if (axes[i].equals("y"))
121 | spaceIndexes[1] = i;
122 | else if (axes[i].equals("z"))
123 | spaceIndexes[2] = i;
124 | }
125 | return spaceIndexes;
126 | }
127 |
128 | private int timeIndex(final String[] axes) {
129 |
130 | for (int i = 0; i < axes.length; i++) {
131 | if (axes[i].equals("t"))
132 | return i;
133 | }
134 | return -1;
135 | }
136 |
137 | private int channelIndex(final String[] axes) {
138 |
139 | for (int i = 0; i < axes.length; i++) {
140 | if (axes[i].equals("c"))
141 | return i;
142 | }
143 | return -1;
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/metadata/NgffTests.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.metadata;
2 |
3 | import static org.junit.Assert.assertArrayEquals;
4 | import static org.junit.Assert.assertEquals;
5 | import static org.junit.Assert.assertTrue;
6 | import static org.junit.Assert.fail;
7 |
8 | import java.io.File;
9 | import java.io.IOException;
10 | import java.nio.file.Files;
11 | import java.util.Arrays;
12 |
13 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
14 | import org.janelia.saalfeldlab.n5.N5Exception;
15 | import org.janelia.saalfeldlab.n5.N5FSReader;
16 | import org.janelia.saalfeldlab.n5.N5Reader;
17 | import org.janelia.saalfeldlab.n5.ij.N5Importer;
18 | import org.janelia.saalfeldlab.n5.ij.N5ScalePyramidExporter;
19 | import org.janelia.saalfeldlab.n5.universe.N5Factory;
20 | import org.janelia.saalfeldlab.n5.universe.metadata.NgffMultiScaleGroupAttributes;
21 | import org.janelia.saalfeldlab.n5.universe.metadata.NgffMultiScaleGroupAttributes.MultiscaleDataset;
22 | import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis;
23 | import org.junit.Assert;
24 | import org.junit.BeforeClass;
25 | import org.junit.Test;
26 |
27 |
28 | import ij.ImagePlus;
29 | import ij.gui.NewImage;
30 |
31 | public class NgffTests {
32 |
33 | private final String n5Root = "src/test/resources/ngff.n5";
34 |
35 | private static File baseDir;
36 |
37 | @BeforeClass
38 | public static void setup() {
39 |
40 | try {
41 | baseDir = Files.createTempDirectory("ngff-tests-").toFile();
42 | baseDir.deleteOnExit();
43 |
44 | } catch (IOException e) {
45 | e.printStackTrace();
46 | }
47 | }
48 |
49 | @Test
50 | public void testNgffGroupAttributeParsing() {
51 |
52 | final double eps = 1e-9;
53 | try( final N5FSReader n5 = new N5FSReader(n5Root) ) {
54 |
55 | NgffMultiScaleGroupAttributes[] multiscales = n5.getAttribute("ngff_grpAttributes", "multiscales",
56 | NgffMultiScaleGroupAttributes[].class);
57 | Assert.assertEquals("one set of multiscales", 1, multiscales.length);
58 |
59 | MultiscaleDataset[] datasets = multiscales[0].datasets;
60 | Assert.assertEquals("num levels", 6, datasets.length);
61 |
62 | double scale = 4;
63 | for (int i = 0; i < datasets.length; i++) {
64 |
65 | String pathName = String.format("s%d", i);
66 | Assert.assertEquals("path name " + i, pathName, datasets[i].path);
67 | Assert.assertEquals("scale " + i, scale, datasets[i].transform.scale[2], eps);
68 |
69 | scale *= 2;
70 | }
71 |
72 | } catch (N5Exception e) {
73 | fail("Ngff parsing failed");
74 | e.printStackTrace();
75 | }
76 | }
77 |
78 | @Test
79 | public void testNgffExportAxisOrder() {
80 |
81 | testNgfffAxisOrder("xyczt", new int[] { 10, 8, 6, 4, 2 });
82 |
83 | testNgfffAxisOrder("xyzt", new int[] { 10, 8, 1, 4, 2 });
84 | testNgfffAxisOrder("xyct", new int[] { 10, 8, 6, 1, 2 });
85 | testNgfffAxisOrder("xycz", new int[] { 10, 8, 6, 4, 1 });
86 |
87 | testNgfffAxisOrder("xyc", new int[] { 10, 8, 6, 1, 1 });
88 | testNgfffAxisOrder("xyz", new int[] { 10, 8, 1, 4, 1 });
89 | testNgfffAxisOrder("xyt", new int[] { 10, 8, 1, 1, 2 });
90 | }
91 |
92 | public void testNgfffAxisOrder(final String dataset, int[] size) {
93 |
94 | final int nx = size[0];
95 | final int ny = size[1];
96 | final int nc = size[2];
97 | final int nz = size[3];
98 | final int nt = size[4];
99 |
100 | final String metadataType = N5Importer.MetadataOmeZarrKey;
101 | final String compressionType = N5ScalePyramidExporter.RAW_COMPRESSION;
102 |
103 | final ImagePlus imp = NewImage.createImage("test", nx, ny, nz * nc * nt, 8, NewImage.FILL_BLACK);
104 | imp.setDimensions(nc, nz, nt);
105 |
106 | final N5ScalePyramidExporter writer = new N5ScalePyramidExporter();
107 | writer.setOptions(imp, baseDir.getAbsolutePath(), dataset, N5ScalePyramidExporter.ZARR_FORMAT, "64", false,
108 | N5ScalePyramidExporter.DOWN_SAMPLE, metadataType, compressionType);
109 | writer.run();
110 |
111 | final long[] expectedDims = Arrays.stream(new long[] { nx, ny, nz, nc, nt }).filter(x -> x > 1).toArray();
112 |
113 | try (final N5Reader n5 = new N5Factory().openReader(baseDir.getAbsolutePath())) {
114 |
115 | assertTrue(n5.exists(dataset));
116 | assertTrue(n5.datasetExists(dataset + "/s0"));
117 |
118 | final DatasetAttributes dsetAttrs = n5.getDatasetAttributes(dataset + "/s0");
119 | assertArrayEquals("dimensions", expectedDims, dsetAttrs.getDimensions());
120 |
121 | int i = 0;
122 | final Axis[] axes = n5.getAttribute(dataset, "multiscales[0]/axes", Axis[].class);
123 |
124 | if (nt > 1)
125 | assertEquals("t", axes[i++].getName());
126 |
127 | if (nc > 1)
128 | assertEquals("c", axes[i++].getName());
129 |
130 | if (nz > 1)
131 | assertEquals("z", axes[i++].getName());
132 |
133 | assertEquals("y", axes[i++].getName());
134 | assertEquals("x", axes[i++].getName());
135 | }
136 |
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/ui/TestUriValidation.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.ui;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import static org.junit.Assert.assertThrows;
5 | import static org.junit.Assert.assertTrue;
6 |
7 | import java.net.URI;
8 | import java.nio.file.Path;
9 | import java.nio.file.Paths;
10 | import java.text.ParseException;
11 |
12 | import org.janelia.saalfeldlab.n5.ui.DatasetSelectorDialog.UriValidator;
13 | import org.junit.Test;
14 |
15 | public class TestUriValidation {
16 |
17 | @Test
18 | public void testUriValidator() throws ParseException {
19 |
20 | final UriValidator urival = new UriValidator();
21 |
22 | final String os = System.getProperty("os.name").toLowerCase();
23 |
24 | final String[] names = new String[]{"b", "c"};
25 | final Path p = Paths.get("a", names);
26 |
27 | if (os.contains("windows")) {
28 | // windows only
29 |
30 | assertThrows(ParseException.class, () -> {
31 | urival.stringToValue(p.toString() + "?d/e");
32 | });
33 | assertThrows(ParseException.class, () -> {
34 | urival.stringToValue(p.toString() + "?d/e#/f/g");
35 | });
36 | } else {
37 | // not windows
38 | assertIsPathGet("relative path", p.normalize().toString(), urival);
39 |
40 | // test some weird strings that can technically be interpreted as
41 | // paths or uris
42 | assertIsPathGet("weird", "\\\\", urival);
43 | assertIsPathGet("weird 2", "::", urival);
44 | }
45 |
46 | /**
47 | * Test getting a path from the filesystem pth = Paths.get(tmpfile)
48 | * assert that's the same as pth.toString + ? dataset is parsable as an
49 | * n5uri
50 | */
51 |
52 | // both windows and not
53 | assertIsPathGet("cwd", ".", urival);
54 | assertIsPathGet("parent dir", "..", urival);
55 | assertIsPathGet("absolute path", p.toAbsolutePath().normalize().toString(), urival);
56 | assertIsPathGet("relative path", p.normalize().toString(), urival);
57 |
58 | assertIsUriCreate("path", "file:///a/b/c", urival);
59 | assertIsUriCreate("path", "file:///a/b/c?d/e", urival);
60 | assertIsUriCreate("path", "file:///a/b/c?d/e#f/g", urival);
61 |
62 | assertIsUriCreate("s3 path", "s3:///a/b/c", urival);
63 | assertIsUriCreate("s3 path, query", "s3:///a/b/c?d/e", urival);
64 | assertIsUriCreate("s3 path, query, frament", "s3:///a/b/c?d/e#f/g", urival);
65 |
66 | assertIsUriCreate("https s3 path", "https://s3.us-east-1.amazonaws.com/a/b/c", urival);
67 | assertIsUriCreate("https s3 path, query", "https://s3.us-east-1.amazonaws.com/a/b/c?d/e", urival);
68 | assertIsUriCreate("https s3 path, query, frament", "https://s3.us-east-1.amazonaws.com/a/b/c?d/e#f/g", urival);
69 |
70 | assertIsUriCreate("gcs path", "gs://storage.googleapis.com/a/b/c", urival);
71 | assertIsUriCreate("gcs path, query", "gs://storage.googleapis.com/a/b/c?d/e", urival);
72 | assertIsUriCreate("gcs path, query, frament", "gs://storage.googleapis.com/a/b/c?d/e#f/g", urival);
73 |
74 | assertIsUriCreate("https gcs path", "https://storage.googleapis.com/a/b/c", urival);
75 | assertIsUriCreate("https gcs path, query", "https://storage.googleapis.com/a/b/c?d/e", urival);
76 | assertIsUriCreate("https gcs path, query, frament", "https://storage.googleapis.com/a/b/c?d/e#f/g", urival);
77 |
78 | assertIsPathGetType("zarr: path", "zarr:", "/a/b/c", urival);
79 | assertIsUriCreateType("zarr:file: path", "zarr:", "file:///a/b/c", urival);
80 | assertIsUriCreateType("zarr:file: path, query", "zarr:", "file:///a/b/c?d/e", urival);
81 | assertIsUriCreateType("zarr:file: path, query fragment", "zarr:", "file:///a/b/c?d/e#f/g", urival);
82 |
83 | assertIsPathGetType("zarr: path", "zarr://", "/a/b/c", urival);
84 | assertIsUriCreateType("zarr:file: path", "zarr://", "file:///a/b/c", urival);
85 | assertIsUriCreateType("zarr:file: path, query", "zarr://", "file:///a/b/c?d/e", urival);
86 | assertIsUriCreateType("zarr:file: path, query fragment", "zarr://", "file:///a/b/c?d/e#f/g", urival);
87 | }
88 |
89 | private void assertIsUriCreate(String message, String s, UriValidator urival) throws ParseException {
90 |
91 | assertEquals(message, URI.create(s).normalize(), urival.stringToValue(s));
92 | }
93 |
94 | private void assertIsUriCreateType(String message, String typeScheme, String s, UriValidator urival) throws ParseException {
95 |
96 | final URI val = (URI)urival.stringToValue(typeScheme + s);
97 | assertTrue(message + " starts with typescheme", val.toString().startsWith(typeScheme));
98 | final URI uriNoType = URI.create(val.toString().replaceFirst("^" + typeScheme + "\\/*", ""));
99 | assertEquals(message, URI.create(s).normalize(), uriNoType);
100 | }
101 |
102 | private void assertIsPathGet(String message, String s, UriValidator urival) throws ParseException {
103 |
104 | assertEquals(message, Paths.get(s).toUri().normalize(), urival.stringToValue(s));
105 | }
106 |
107 | private void assertIsPathGetType(String message, String typeScheme, String s, UriValidator urival) throws ParseException {
108 |
109 | final URI val = (URI)urival.stringToValue(typeScheme + s);
110 | assertTrue(message + " starts with typescheme", val.toString().startsWith(typeScheme));
111 | final URI uriNoType = URI.create(val.toString().replaceFirst("^" + typeScheme + "\\/*", ""));
112 | assertEquals(message, Paths.get(s).toUri().normalize(), uriNoType);
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/N5MetadataSpecDialog.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.ui;
27 |
28 | import java.awt.BorderLayout;
29 | import java.awt.Dimension;
30 | import java.awt.event.ActionEvent;
31 | import java.awt.event.ActionListener;
32 | import java.awt.event.WindowEvent;
33 | import java.awt.event.WindowListener;
34 | import java.io.IOException;
35 | import java.nio.file.Files;
36 | import java.nio.file.Paths;
37 |
38 | import javax.swing.JButton;
39 | import javax.swing.JFrame;
40 | import javax.swing.JPanel;
41 | import javax.swing.JScrollPane;
42 | import javax.swing.JTextArea;
43 |
44 | import org.janelia.saalfeldlab.n5.metadata.imagej.MetadataTemplateMapper;
45 |
46 | import ij.Prefs;
47 |
48 | public class N5MetadataSpecDialog
49 | {
50 | private String metadataSpecText;
51 |
52 | private String mapperString;
53 |
54 | private MetadataTemplateMapper mapper;
55 |
56 | private WindowListener listener;
57 |
58 | public N5MetadataSpecDialog( ){ }
59 |
60 | public N5MetadataSpecDialog( final WindowListener listener )
61 | {
62 | this.listener = listener;
63 | }
64 |
65 | public String getMapperString()
66 | {
67 | return mapperString;
68 | }
69 |
70 | public MetadataTemplateMapper getMapper()
71 | {
72 | return mapper;
73 | }
74 |
75 | public JFrame show( final String init )
76 | {
77 | final JFrame frame = new JFrame( "Metadata translation" );
78 |
79 | final double guiScale = Prefs.getGuiScale();
80 | final int frameSizeX = (int)(guiScale * 600);
81 | final int frameSizeY = (int)(guiScale * 400);
82 |
83 | frame.setPreferredSize( new Dimension( frameSizeX, frameSizeY ) );
84 | frame.setMinimumSize( frame.getPreferredSize() );
85 |
86 | final JPanel panel = new JPanel( new BorderLayout() );
87 |
88 | if( listener != null )
89 | frame.addWindowListener( listener );
90 |
91 | final JTextArea textArea = new JTextArea();
92 | textArea.setText( init );
93 | textArea.setFont( textArea.getFont().deriveFont( (float)guiScale * 18f) );
94 |
95 | final JScrollPane textView = new JScrollPane( textArea );
96 | panel.add( textView, BorderLayout.CENTER );
97 |
98 | final JButton okButton = new JButton("OK");
99 | okButton.addActionListener( new ActionListener()
100 | {
101 | @Override
102 | public void actionPerformed( final ActionEvent event )
103 | {
104 | metadataSpecText = textArea.getText();
105 | mapper = new MetadataTemplateMapper( metadataSpecText );
106 |
107 | frame.setVisible( false );
108 | frame.dispatchEvent( new WindowEvent( frame, WindowEvent.WINDOW_CLOSING ));
109 | }
110 | });
111 |
112 | final JButton cancelButton = new JButton("Cancel");
113 | cancelButton.addActionListener( new ActionListener()
114 | {
115 | @Override
116 | public void actionPerformed( final ActionEvent event )
117 | {
118 | frame.setVisible( false );
119 | frame.dispatchEvent( new WindowEvent( frame, WindowEvent.WINDOW_CLOSING ));
120 | }
121 | });
122 |
123 | final JPanel buttonPanel = new JPanel();
124 | buttonPanel.add( okButton, BorderLayout.WEST );
125 | buttonPanel.add( cancelButton , BorderLayout.EAST );
126 | panel.add( buttonPanel, BorderLayout.SOUTH );
127 |
128 | frame.add( panel );
129 | frame.pack();
130 | frame.setVisible( true );
131 | return frame;
132 | }
133 |
134 | // public static void main( final String[] args ) throws IOException
135 | // {
136 | // System.out.println( MetadataTemplateMapper.COSEM_MAPPER);
137 | //
138 | // N5MetadataSpecDialog dialog = new N5MetadataSpecDialog( );
139 | //
140 | // String templateJsonF = "/home/john/dev/json/jq_Examples/template.json";
141 | // String content = new String(Files.readAllBytes(Paths.get( templateJsonF )));
142 | //
143 | // dialog.show( MetadataTemplateMapper.RESOLUTION_ONLY_MAPPER );
144 | // String result;
145 | // try
146 | // {
147 | // mapperString = mapper.compute( metadataSpecText, content );
148 | // System.out.println( "result: " );
149 | // System.out.println( result );
150 | // }
151 | // catch ( IOException e )
152 | // {
153 | // e.printStackTrace();
154 | // }
155 | // }
156 | }
157 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/N5SwingTreeNode.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.ui;
2 |
3 | import java.util.Arrays;
4 | import java.util.Collections;
5 | import java.util.Enumeration;
6 | import java.util.HashMap;
7 | import java.util.Optional;
8 | import java.util.stream.Stream;
9 |
10 | import javax.swing.tree.DefaultTreeModel;
11 | import javax.swing.tree.MutableTreeNode;
12 | import javax.swing.tree.TreeNode;
13 |
14 | import org.janelia.saalfeldlab.n5.universe.N5TreeNode;
15 |
16 | public class N5SwingTreeNode extends N5TreeNode implements MutableTreeNode {
17 |
18 | private N5SwingTreeNode parent;
19 |
20 | private DefaultTreeModel treeModel;
21 |
22 | public N5SwingTreeNode( final String path ) {
23 | super( path );
24 | }
25 |
26 | public N5SwingTreeNode( final String path, final DefaultTreeModel model ) {
27 | super( path );
28 | this.treeModel = model;
29 | }
30 |
31 | public N5SwingTreeNode( final String path, final N5SwingTreeNode parent ) {
32 | super( path );
33 | this.parent = parent;
34 | }
35 |
36 | public N5SwingTreeNode( final String path, final N5SwingTreeNode parent, final DefaultTreeModel model ) {
37 | super( path );
38 | this.parent = parent;
39 | this.treeModel = model;
40 | }
41 |
42 | @SuppressWarnings("rawtypes")
43 | @Override
44 | public Enumeration children() {
45 | return Collections.enumeration(childrenList());
46 | }
47 |
48 | public void add(final N5SwingTreeNode child) {
49 |
50 | childrenList().add(child);
51 | }
52 |
53 | @Override
54 | public N5SwingTreeNode addPath(final String path) {
55 |
56 | String normPath = removeLeadingSlash(path);
57 | if (!getPath().isEmpty() && !normPath.startsWith(getPath()))
58 | return null;
59 |
60 | if (getPath().equals(normPath))
61 | return this;
62 |
63 | final String relativePath = removeLeadingSlash(normPath.replaceAll("^" + getPath(), ""));
64 |
65 | final int sepIdx = relativePath.indexOf("/");
66 | final String childName;
67 | if (sepIdx < 0)
68 | childName = relativePath;
69 | else
70 | childName = relativePath.substring(0, sepIdx);
71 |
72 | // get the appropriate child along the path if it exists, otherwise add
73 | // it
74 | N5TreeNode child = null;
75 | Stream cs = childrenList().stream().filter(n -> n.getNodeName().equals(childName));;
76 | Optional copt = cs.findFirst();
77 | if (copt.isPresent())
78 | child = copt.get();
79 | else {
80 | child = new N5SwingTreeNode(
81 | getPath().isEmpty() ? childName : getPath() + "/" + childName,
82 | this, treeModel);
83 |
84 | add(child);
85 |
86 | if (treeModel != null)
87 | treeModel.nodesWereInserted(this, new int[]{childrenList().size() - 1});
88 | }
89 | return (N5SwingTreeNode)child.addPath(normPath);
90 | }
91 |
92 | @Override
93 | public boolean getAllowsChildren() {
94 | return true;
95 | }
96 |
97 | @Override
98 | public N5SwingTreeNode getChildAt(int i) {
99 | return (N5SwingTreeNode) childrenList().get(i);
100 | }
101 |
102 | @Override
103 | public int getChildCount() {
104 | return childrenList().size();
105 | }
106 |
107 | @SuppressWarnings("unlikely-arg-type")
108 | @Override
109 | public int getIndex(TreeNode n) {
110 | return childrenList().indexOf(n);
111 | }
112 |
113 | @Override
114 | public TreeNode getParent() {
115 | return parent;
116 | }
117 |
118 | @Override
119 | public boolean isLeaf() {
120 | return getChildCount() < 1;
121 | }
122 |
123 | public static void fromFlatList(final N5SwingTreeNode root, final String[] pathList, final String groupSeparator) {
124 |
125 | final HashMap pathToNode = new HashMap<>();
126 |
127 | final String normalizedBase = normalDatasetName(root.getPath(), groupSeparator);
128 | pathToNode.put(normalizedBase, root);
129 |
130 | // sort the paths by length such that parent nodes always have smaller
131 | // indexes than their children
132 | Arrays.sort(pathList);
133 |
134 | final String prefix = normalizedBase == groupSeparator ? "" : normalizedBase;
135 | for (final String datasetPath : pathList) {
136 |
137 | final String fullPath = prefix + groupSeparator + datasetPath;
138 | final String parentPath = fullPath.substring(0, fullPath.lastIndexOf(groupSeparator));
139 |
140 | N5SwingTreeNode parent = pathToNode.get(parentPath);
141 | if (parent == null) {
142 | // possible for the parent to not appear in the list
143 | // if deepList is called with a filter
144 | parent = new N5SwingTreeNode(parentPath);
145 | pathToNode.put(parentPath, parent);
146 | }
147 | final N5SwingTreeNode node = new N5SwingTreeNode(fullPath, parent );
148 | pathToNode.put(fullPath, node);
149 |
150 | parent.add(node);
151 | }
152 | }
153 |
154 | private static String normalDatasetName(final String fullPath, final String groupSeparator) {
155 |
156 | return fullPath.replaceAll("(^" + groupSeparator + "*)|(" + groupSeparator + "*$)", "");
157 | }
158 |
159 | @Override
160 | public void insert(MutableTreeNode child, int index) {
161 | if( child instanceof N5SwingTreeNode )
162 | childrenList().add(index, (N5SwingTreeNode)child);
163 | }
164 |
165 | @Override
166 | public void remove(int index) {
167 | childrenList().remove(index);
168 | }
169 |
170 | @SuppressWarnings("unlikely-arg-type")
171 | @Override
172 | public void remove(MutableTreeNode node) {
173 | childrenList().remove(node);
174 | }
175 |
176 | @Override
177 | public void removeFromParent() {
178 | parent.childrenList().remove(this);
179 | }
180 |
181 | @Override
182 | public void setParent(MutableTreeNode newParent) {
183 | if( newParent instanceof N5SwingTreeNode )
184 | this.parent = (N5SwingTreeNode)newParent;
185 | }
186 |
187 | @Override
188 | public void setUserObject(Object object) {
189 | // does nothing
190 | }
191 |
192 | }
193 |
--------------------------------------------------------------------------------
/doc/ngffDeveloperExamples.md:
--------------------------------------------------------------------------------
1 | # NGFF developer examples
2 |
3 | Examples of reading and writing [ngff](https://github.com/ome/ngff) data using the N5 API.
4 |
5 | ## v0.4
6 |
7 | The example below deal with [ngff version 0.4](https://ngff.openmicroscopy.org/0.4/)
8 |
9 | ### write: N5ScalePyramidExporter
10 |
11 | ```java
12 | // parameters
13 | final String n5Root = "/home/john/tmp/ngff-test.zarr";
14 | final String metaType = N5Importer.MetadataOmeZarrKey;
15 | final String downsamplingMethod = N5ScalePyramidExporter.DOWN_AVG;
16 | final String dataset = "";
17 | final String blockSizeArgument = "64";
18 | final boolean multiscale = true;
19 | final String compressionType = "gzip";
20 | final long[] imageDimensions = new long[] { 64, 32, 16 };
21 | final double[] imageResolutions = new double[] { 2.0, 3.0, 4.0 };
22 |
23 | // make a sample image
24 | ImagePlus imp = makeDemoImagePlus(imageDimensions, imageResolutions);
25 |
26 | // export the image
27 | final N5ScalePyramidExporter exp = new N5ScalePyramidExporter(
28 | imp, n5Root, dataset, blockSizeArgument, multiscale, downsamplingMethod, metaType, compressionType );
29 | exp.run();
30 | ```
31 |
32 |
33 |
34 |
35 | where `makeDemoImagePlus`
36 |
37 | ```java
38 | public static ImagePlus makeDemoImagePlus( long[] dimensions, double... resolution )
39 | {
40 | final IntImagePlus img = ImagePlusImgs.ints(dimensions);
41 | final PlanarCursor c = img.cursor();
42 | int i = 0;
43 | while( c.hasNext() )
44 | c.next().set(i++);
45 |
46 | final ImagePlus imp = img.getImagePlus();
47 | for( i = 0; i < resolution.length; i++ )
48 | if( i == 0 )
49 | imp.getCalibration().pixelWidth = resolution[i];
50 | else if( i == 1 )
51 | imp.getCalibration().pixelHeight = resolution[i];
52 | else if( i == 2 )
53 | imp.getCalibration().pixelDepth = resolution[i];
54 |
55 | return imp;
56 | }
57 | ```
58 |
59 |
60 |
61 |
62 | ### write: low-level, single-scale
63 |
64 | ```java
65 | // parameters
66 | final String n5Root = "/home/john/tmp/ngff-test.zarr";
67 | final String baseDataset = "";
68 | final long[] imageDimensions = new long[] { 64, 32, 16 };
69 | final int[] blockSize = new int[] { 64, 32, 16 };
70 |
71 | // make a demo array
72 | final ArrayImg img = makeDemoImage( imageDimensions );
73 |
74 | // make demo metadata
75 | final OmeNgffMetadata meta = OmeNgffMetadata.buildForWriting( 3,
76 | "name",
77 | AxisUtils.defaultAxes("x", "y", "z"), // a helper method to create axes
78 | new String[] {"s0"}, // location of the array in the hierarchy
79 | new double[][]{{ 2.0, 3.0, 4.0 }}, // resolution
80 | null); // translation / offset (if null, interpreted as zero)
81 |
82 |
83 | // make the n5 writer
84 | final N5Writer n5 = new N5Factory().openWriter(n5Root);
85 |
86 | // write the array
87 | N5Utils.save(img, n5, baseDataset + "/s0", blockSize, new GzipCompression());
88 |
89 | // write the metadata
90 | try {
91 | new OmeNgffMetadataParser().writeMetadata(meta, n5, baseDataset);
92 | } catch (Exception e) { }
93 | ```
94 |
95 |
96 |
97 | where `makeDemoImage`
98 |
99 | ```java
100 | public static ArrayImg makeDemoImage( long[] dimensions )
101 | {
102 | int N = 1;
103 | for (int i = 0; i < dimensions.length; i++)
104 | N *= dimensions[i];
105 |
106 | return ArrayImgs.ints(
107 | IntStream.range(0, N).toArray(),
108 | dimensions);
109 | }
110 | ```
111 |
112 |
113 |
114 | ### write: low-level, multi-scale
115 |
116 | ```java
117 | // parameters
118 | final String n5Root = "/home/john/tmp/ngff-test.zarr";
119 | final String baseDataset = "";
120 | final long[] imageDimensions = new long[] { 64, 32, 16 };
121 | final int[] blockSize = new int[] { 64, 32, 16 };
122 |
123 | // make a demo array and scale levels
124 | List> scaleLevels = makeDemoImageMultiscales( 3, imageDimensions, new long[]{2, 2, 2} );
125 |
126 | // make demo metadata
127 |
128 | // make the resolutions, 3 scale levels, base resolution[2,3,4], downsampled by [2,2,2]
129 | final double[][] resolutions = MetadataUtils.scalesAndTranslations(new double[]{2.0, 3.0, 4.0}, new double[]{2.0, 2.0, 2.0}, 3);
130 | // this will be:
131 | /*
132 | * [[2, 3, 4]
133 | * [4, 6, 8]
134 | * [8, 12, 16]]
135 | */
136 | final OmeNgffMetadata meta = OmeNgffMetadata.buildForWriting( 3,
137 | "name",
138 | AxisUtils.defaultAxes("x", "y", "z"), // a helper method to create axes
139 | new String[] {"s0", "s1", "s2"}, // location of the scale arrays in the hierarchy
140 | resolutions, // resolutions
141 | null); // translation / offset (if null, interpreted as zero)
142 |
143 | // make the n5 writer
144 | final N5Writer n5 = new N5Factory().openWriter(n5Root);
145 |
146 | // write the array
147 | int s = 0;
148 | for( RandomAccessibleInterval img : scaleLevels )
149 | N5Utils.save(img, n5, String.format("%s/s%d", baseDataset, s++), blockSize, new GzipCompression());
150 |
151 | // write the metadata
152 | try {
153 | new OmeNgffMetadataParser().writeMetadata(meta, n5, baseDataset);
154 | } catch (Exception e) { }
155 | ```
156 |
157 |
158 |
159 | where `makeDemoImageMultiscales`
160 |
161 | ```java
162 | public static List> makeDemoImageMultiscales( int numScales, long[] baseDimensions, long[] factors )
163 | {
164 | int N = 1;
165 | for (int i = 0; i < baseDimensions.length; i++)
166 | N *= baseDimensions[i];
167 |
168 | ArrayList> scaleList = new ArrayList<>();
169 | scaleList.add( ArrayImgs.ints( IntStream.range(0, N).toArray(), baseDimensions));
170 | for( int i = 1; i < numScales; i++ )
171 | scaleList.add(Views.subsample( scaleList.get(0), factors ));
172 |
173 | return scaleList;
174 | }
175 | ```
176 |
177 |
178 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/N5ImagePlusMetadata.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.metadata.imagej;
27 |
28 | import net.imglib2.realtransform.AffineGet;
29 | import net.imglib2.realtransform.ScaleAndTranslation;
30 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
31 | import org.janelia.saalfeldlab.n5.universe.metadata.AbstractN5DatasetMetadata;
32 | import org.janelia.saalfeldlab.n5.universe.metadata.SpatialMetadata;
33 | import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis;
34 | import org.janelia.saalfeldlab.n5.universe.metadata.axes.AxisMetadata;
35 |
36 | import java.util.Map;
37 | import java.util.Objects;
38 |
39 | public class N5ImagePlusMetadata extends AbstractN5DatasetMetadata implements SpatialMetadata, AxisMetadata {
40 |
41 | public final String name;
42 |
43 | public final double fps;
44 | public final double frameInterval;
45 |
46 | public final double pixelWidth;
47 | public final double pixelHeight;
48 | public final double pixelDepth;
49 | public final double xOrigin;
50 | public final double yOrigin;
51 | public final double zOrigin;
52 |
53 | public final int numChannels;
54 | public final int numSlices;
55 | public final int numFrames;
56 |
57 | public final int type;
58 | public final String unit;
59 |
60 | public final Map properties;
61 |
62 | private transient Axis[] axes;
63 |
64 | public N5ImagePlusMetadata(final String path, final DatasetAttributes attributes, final String name,
65 | final double fps, final double frameInterval, final String unit, final Double pixelWidth,
66 | final Double pixelHeight, final Double pixelDepth, final Double xOrigin, final Double yOrigin,
67 | final Double zOrigin, final Integer numChannels, final Integer numSlices, final Integer numFrames,
68 | final Integer type, final Map properties) {
69 |
70 | super(path, attributes);
71 |
72 | this.name = name;
73 | this.fps = Objects.requireNonNull(fps, "fps must be non null");
74 | this.frameInterval = Objects.requireNonNull(frameInterval, "frameInterval must be non null");
75 |
76 | this.unit = Objects.requireNonNull(unit, "unit must be non null");
77 | this.pixelWidth = Objects.requireNonNull(pixelWidth, "pixelWidth must be non null");
78 | this.pixelHeight = Objects.requireNonNull(pixelHeight, "pixelHeight must be non null");
79 | this.pixelDepth = Objects.requireNonNull(pixelDepth, "pixelDepth must be non null");
80 |
81 | this.xOrigin = Objects.requireNonNull(xOrigin, "xOrigin must be non null");
82 | this.yOrigin = Objects.requireNonNull(yOrigin, "yOrigin must be non null");
83 | this.zOrigin = Objects.requireNonNull(zOrigin, "zOrigin must be non null");
84 |
85 | this.numChannels = Objects.requireNonNull(numChannels, "numChannels must be non null");
86 | this.numSlices = Objects.requireNonNull(numSlices, "numSlices must be non null");
87 | this.numFrames = Objects.requireNonNull(numFrames, "numFrames must be non null");
88 |
89 | // type is not required and so may be null
90 | if (type == null)
91 | this.type = -1;
92 | else
93 | this.type = type;
94 |
95 | this.properties = properties;
96 |
97 | axes = buildAxes();
98 | }
99 |
100 | private Axis[] buildAxes() {
101 |
102 | int nd = 2;
103 | if( numChannels > 1 )
104 | nd++;
105 |
106 | if( numSlices > 1 )
107 | nd++;
108 |
109 | if( numFrames > 1 )
110 | nd++;
111 |
112 | axes = new Axis[nd];
113 | axes[0] = new Axis(Axis.SPACE, "x", unit);
114 | axes[1] = new Axis(Axis.SPACE, "y", unit);
115 |
116 | int i = 2;
117 | if( numChannels > 1 )
118 | axes[i++] = new Axis(Axis.CHANNEL, "c", "");
119 |
120 | if( numSlices > 1 )
121 | axes[i++] = new Axis(Axis.SPACE, "z", unit);
122 |
123 | if( numFrames > 1 )
124 | axes[i++] = new Axis(Axis.TIME, "t", "sec");
125 |
126 | return axes;
127 | }
128 |
129 | public int getType() {
130 |
131 | return type;
132 | }
133 |
134 | @Override
135 | public AffineGet spatialTransform() {
136 |
137 | final int nd = numSlices > 1 ? 3 : 2;
138 | final double[] spacing = new double[nd];
139 | final double[] offset = new double[nd];
140 |
141 | spacing[0] = pixelWidth;
142 | spacing[1] = pixelHeight;
143 | if (numSlices > 1)
144 | spacing[2] = pixelDepth;
145 |
146 | offset[0] = xOrigin;
147 | offset[1] = yOrigin;
148 | if (numSlices > 1)
149 | offset[2] = zOrigin;
150 |
151 | return new ScaleAndTranslation(spacing, offset);
152 | }
153 |
154 | @Override
155 | public String unit() {
156 |
157 | return unit;
158 | }
159 |
160 | @Override
161 | public Axis[] getAxes() {
162 |
163 | return axes;
164 | }
165 |
166 | }
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/ij/MacroTests.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.ij;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import static org.junit.Assert.assertTrue;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.net.URI;
9 | import java.net.URISyntaxException;
10 | import java.nio.file.Files;
11 | import java.nio.file.Paths;
12 | import java.util.List;
13 |
14 | import org.janelia.saalfeldlab.n5.N5Writer;
15 | import org.janelia.saalfeldlab.n5.TestExportImports;
16 | import org.janelia.saalfeldlab.n5.universe.N5Factory;
17 | import org.junit.After;
18 | import org.junit.Before;
19 | import org.junit.Test;
20 |
21 | import ij.IJ;
22 | import ij.ImagePlus;
23 | import ij.gui.NewImage;
24 | import net.imglib2.img.display.imagej.ImageJFunctions;
25 | import net.imglib2.img.display.imagej.ImageJVirtualStack;
26 | import net.imglib2.type.numeric.integer.UnsignedShortType;
27 | import net.imglib2.util.Intervals;
28 | import net.imglib2.view.IntervalView;
29 | import net.imglib2.view.Views;
30 |
31 | public class MacroTests {
32 |
33 | private URI containerDir;
34 |
35 | private URI n5rootF;
36 |
37 | private String dataset;
38 |
39 | private ImagePlus imp;
40 |
41 | @Before
42 | public void before() {
43 | System.setProperty("java.awt.headless", "true");
44 |
45 | try {
46 | containerDir = new File(tempN5PathName()).getCanonicalFile().toURI();
47 | } catch (final IOException e) {}
48 |
49 | n5rootF = Paths.get("src", "test", "resources", "test.n5").toUri();
50 | dataset = "dataset";
51 |
52 | imp = NewImage.createImage("test", 8, 7, 9, 16, NewImage.FILL_NOISE);
53 | final String format = N5ScalePyramidExporter.N5_FORMAT;
54 |
55 | final N5ScalePyramidExporter writer = new N5ScalePyramidExporter();
56 | writer.setOptions( imp, containerDir.toString(), dataset, format, "16,16,16", false,
57 | N5ScalePyramidExporter.NONE, N5ScalePyramidExporter.DOWN_SAMPLE, N5ScalePyramidExporter.RAW_COMPRESSION);
58 | writer.run(); // run() closes the n5 writer
59 | }
60 |
61 | @After
62 | public void after() {
63 |
64 | final N5Writer n5 = new N5Factory().openWriter(containerDir.toString());
65 | n5.remove();
66 | }
67 |
68 | private static String tempN5PathName() {
69 |
70 | try {
71 | final File tmpFile = Files.createTempDirectory("n5-ij-macro-test-").toFile();
72 | tmpFile.deleteOnExit();
73 | return tmpFile.getCanonicalPath();
74 | } catch (final Exception e) {
75 | throw new RuntimeException(e);
76 | }
77 | }
78 |
79 | protected String tempN5Location() throws URISyntaxException {
80 |
81 | final String basePath = new File(tempN5PathName()).toURI().normalize().getPath();
82 | return new URI("file", null, basePath, null).toString();
83 | }
84 |
85 | @Test
86 | public void testMacroContentPath() throws IOException {
87 | testMacroContentHelper("url=%s/%s");
88 | }
89 |
90 | @Test
91 | public void testMacroContentUri() throws IOException {
92 | System.out.println("testMacroContent URI style skip windows");
93 | final String os = System.getProperty("os.name").toLowerCase();
94 | if( !os.startsWith("windows"))
95 | testMacroContentHelper("url=%s?%s");
96 | }
97 |
98 | public void testMacroContentHelper( String urlFormat ) throws IOException {
99 |
100 | final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer",
101 | String.format( urlFormat + " hide", containerDir.toString(), dataset ));
102 |
103 | final List res = plugin.getResult();
104 | final ImagePlus imgImported = res.get(0);
105 | assertTrue( "equal content", TestExportImports.equal(imp, imgImported));
106 |
107 | final N5Importer pluginCrop = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer",
108 | String.format( urlFormat + " hide min=0,1,2 max=5,5,5",
109 | containerDir.toString(), "dataset" ));
110 | final List resCrop = pluginCrop.getResult();
111 | final ImagePlus imgImportedCrop = resCrop.get(0);
112 |
113 | final IntervalView imgCrop = Views.zeroMin( Views.interval(
114 | ImageJFunctions.wrapShort(imp),
115 | Intervals.createMinMax( 0, 1, 2, 5, 5, 5 )));
116 |
117 | final ImagePlus impCrop = ImageJFunctions.wrap(imgCrop, "imgCrop");
118 | assertEquals("cont crop w", impCrop.getWidth(), imgImportedCrop.getWidth());
119 | assertEquals("cont crop h", impCrop.getHeight(), imgImportedCrop.getHeight());
120 | assertEquals("cont crop d", impCrop.getNSlices(), imgImportedCrop.getNSlices());
121 | assertEquals("cont crop c", impCrop.getNChannels(), imgImportedCrop.getNChannels());
122 | assertTrue("equal content crop", TestExportImports.equal(impCrop, imgImportedCrop));
123 | }
124 |
125 | @Test
126 | public void testMacro() {
127 |
128 | final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer",
129 | "url=" + n5rootF.resolve("cosem").toString() + " hide");
130 |
131 | final List res = plugin.getResult();
132 | assertEquals("crop num", 1, res.size());
133 | final ImagePlus img = res.get(0);
134 |
135 | assertEquals("crop w", 256, img.getWidth());
136 | assertEquals("crop h", 256, img.getHeight());
137 | assertEquals("crop d", 129, img.getNSlices());
138 | }
139 |
140 | @Test
141 | public void testMacroVirtual() {
142 |
143 | final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer",
144 | "url=" + n5rootF.resolve("cosem").toString() + " hide virtual");
145 |
146 | final List res = plugin.getResult();
147 | assertEquals("crop num", 1, res.size());
148 | final ImagePlus img = res.get(0);
149 | assertTrue( "is virtual", (img.getStack() instanceof ImageJVirtualStack) );
150 | }
151 |
152 | @Test
153 | public void testMacroCrop() {
154 |
155 | final String minString = "100,100,50";
156 | final String maxString = "250,250,120";
157 | final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer",
158 | String.format("url=%s hide min=%s max=%s",
159 | n5rootF.resolve("cosem").toString(),
160 | minString, maxString));
161 |
162 | final List res = plugin.getResult();
163 | assertEquals(" crop num", 1, res.size());
164 |
165 | final ImagePlus img = res.get(0);
166 | assertEquals("crop w", 151, img.getWidth());
167 | assertEquals("crop h", 151, img.getHeight());
168 | assertEquals("crop d", 71, img.getNSlices() );
169 | }
170 |
171 | }
172 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/RunImportExportTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5;
27 |
28 | import java.util.HashMap;
29 | import java.util.List;
30 |
31 | import org.janelia.saalfeldlab.n5.ij.N5Importer;
32 | import org.janelia.saalfeldlab.n5.ij.N5ScalePyramidExporter;
33 |
34 | import ij.IJ;
35 | import ij.ImagePlus;
36 | import net.imglib2.Cursor;
37 | import net.imglib2.RandomAccess;
38 | import net.imglib2.img.Img;
39 | import net.imglib2.img.display.imagej.ImageJFunctions;
40 | import net.imglib2.type.NativeType;
41 | import net.imglib2.type.numeric.RealType;
42 |
43 | public class RunImportExportTest
44 | {
45 | final ImagePlus imp;
46 |
47 | final String outputPath;
48 |
49 | final String dataset;
50 |
51 | private String metadataType;
52 |
53 | private String compressionType;
54 |
55 | private String blockSizeString;
56 |
57 | /*
58 | * "Outputs"
59 | */
60 | private boolean singletonListOut;
61 |
62 | private boolean resEqual;
63 |
64 | private boolean unitsEqual;
65 |
66 | private boolean imagesEqual;
67 |
68 | public RunImportExportTest(
69 | final ImagePlus imp,
70 | final String outputPath,
71 | final String dataset,
72 | final String metadataType,
73 | final String compressionType,
74 | final String blockSizeString )
75 | {
76 | this.imp = imp;
77 | this.outputPath = outputPath;
78 | this.dataset = dataset;
79 |
80 | this.metadataType = metadataType;
81 | this.compressionType = compressionType;
82 | this.blockSizeString = blockSizeString;
83 | }
84 |
85 | public void run()
86 | {
87 | final N5ScalePyramidExporter writer = new N5ScalePyramidExporter();
88 | writer.setOptions( imp, outputPath, dataset, N5ScalePyramidExporter.AUTO_FORMAT, blockSizeString, false,
89 | metadataType, N5ScalePyramidExporter.DOWN_SAMPLE, compressionType);
90 | writer.run();
91 |
92 | final String n5PathAndDataset = outputPath + dataset;
93 | final N5Importer reader = new N5Importer();
94 | final List< ImagePlus > impList = reader.process( n5PathAndDataset, false );
95 |
96 | singletonListOut = impList.size() == 1;
97 |
98 | final ImagePlus impRead = impList.get( 0 );
99 |
100 | resEqual = impRead.getCalibration().pixelWidth == imp.getCalibration().pixelWidth &&
101 | impRead.getCalibration().pixelHeight == imp.getCalibration().pixelHeight &&
102 | impRead.getCalibration().pixelDepth == imp.getCalibration().pixelDepth;
103 |
104 | unitsEqual = impRead.getCalibration().getUnit().equals( imp.getCalibration().getUnit() );
105 |
106 | imagesEqual = equal( imp, impRead );
107 |
108 | impRead.close();
109 | }
110 |
111 | public static void main( final String[] args )
112 | {
113 | final ImagePlus imp = IJ.openImage( "/home/john/tmp/blobs.tif" );
114 |
115 | final String[] metadataTypes = new String[]{
116 | N5Importer.MetadataImageJKey,
117 | N5Importer.MetadataN5CosemKey,
118 | N5Importer.MetadataN5ViewerKey
119 | };
120 |
121 | //String[] containerTypes = new String[] { "FILESYSTEM", "ZARR" };
122 | final String[] containerTypes = new String[] { "HDF5" };
123 |
124 | final HashMap typeToExtension = new HashMap<>();
125 | typeToExtension.put( "FILESYSTEM", "n5" );
126 | typeToExtension.put( "ZARR", "zarr" );
127 | typeToExtension.put( "HDF5", "h5" );
128 |
129 | for( final String containerType : containerTypes )
130 | {
131 | for( final String metatype : metadataTypes )
132 | {
133 | final String n5RootPath = "/home/john/tmp/test." + typeToExtension.get( containerType );
134 | final RunImportExportTest testRunner = new RunImportExportTest(
135 | imp, n5RootPath, "/blobs",
136 | metatype, "gzip", "32,32" );
137 |
138 | testRunner.run();
139 | System.out.println( "metadata type: " + metatype );
140 | final boolean allPassed = testRunner.singletonListOut &&
141 | testRunner.resEqual &&
142 | testRunner.unitsEqual &&
143 | testRunner.imagesEqual ;
144 | System.out.println( " " + allPassed );
145 | if( ! allPassed )
146 | {
147 | System.out.println( " " +
148 | testRunner.singletonListOut + " " +
149 | testRunner.resEqual + " " +
150 | testRunner.unitsEqual + " " +
151 | testRunner.imagesEqual );
152 | }
153 | System.out.println( " " );
154 | }
155 | }
156 | }
157 |
158 | public static < T extends RealType< T > & NativeType< T > > boolean equal( final ImagePlus a, final ImagePlus b )
159 | {
160 | try {
161 | final Img imgA = ImageJFunctions.wrapRealNative( a );
162 | final Img imgB = ImageJFunctions.wrapRealNative( a );
163 |
164 | final Cursor< T > c = imgA.cursor();
165 | final RandomAccess< T > r = imgB.randomAccess();
166 |
167 | while( c.hasNext() )
168 | {
169 | c.fwd();
170 | r.setPosition( c );
171 | if( c.get().getRealDouble() != r.get().getRealDouble() )
172 | return false;
173 |
174 | }
175 |
176 | return true;
177 |
178 | }catch( final Exception e )
179 | {
180 | return false;
181 | }
182 | }
183 |
184 | }
185 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/ImagePlusMetadataTemplate.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.metadata.imagej;
27 |
28 | import java.io.IOException;
29 | import java.util.HashMap;
30 | import java.util.Map;
31 | import java.util.Properties;
32 |
33 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
34 | import org.janelia.saalfeldlab.n5.universe.metadata.AbstractN5DatasetMetadata;
35 | import org.janelia.saalfeldlab.n5.universe.metadata.AbstractN5Metadata;
36 | import org.janelia.saalfeldlab.n5.universe.metadata.SpatialMetadata;
37 |
38 | import ij.ImagePlus;
39 | import net.imglib2.realtransform.AffineGet;
40 | import net.imglib2.realtransform.ScaleAndTranslation;
41 |
42 | public class ImagePlusMetadataTemplate extends AbstractN5DatasetMetadata
43 | implements ImageplusMetadata< ImagePlusMetadataTemplate >, SpatialMetadata
44 | {
45 | public int numDims;
46 | public int numSpatialDims;
47 |
48 | public int numChannels;
49 | public int numFrames;
50 | public int numSlices;
51 |
52 | public String name;
53 |
54 | public double xResolution;
55 | public double yResolution;
56 | public double zResolution;
57 | public double tResolution;
58 |
59 | public double xOrigin;
60 | public double yOrigin;
61 | public double zOrigin;
62 | public double tOrigin;
63 |
64 | public String xUnit;
65 | public String yUnit;
66 | public String zUnit;
67 | public String tUnit;
68 |
69 | public String globalUnit;
70 |
71 | public Map otherMetadata;
72 |
73 | public ImagePlusMetadataTemplate( ) {
74 | super("", null);
75 | }
76 |
77 | public ImagePlusMetadataTemplate( final String path, final ImagePlus imp ) {
78 | this( path, imp, null );
79 | }
80 |
81 | public ImagePlusMetadataTemplate( final String path, final DatasetAttributes attributes ) {
82 | this( path, null, attributes );
83 | }
84 |
85 | public ImagePlusMetadataTemplate( final String path, final ImagePlus imp, final DatasetAttributes attributes ) {
86 |
87 | super( path, attributes );
88 |
89 | numChannels = imp.getNChannels();
90 | numFrames = imp.getNFrames();
91 | numSlices = imp.getNSlices();
92 |
93 | numDims = imp.getNDimensions();
94 | numSpatialDims = numSlices > 1 ? 3 : 2;
95 |
96 | name = imp.getTitle();
97 |
98 | xResolution = imp.getCalibration().pixelWidth;
99 | yResolution = imp.getCalibration().pixelHeight;
100 | zResolution = imp.getCalibration().pixelDepth;
101 | tResolution = imp.getCalibration().frameInterval;
102 |
103 | xOrigin = imp.getCalibration().xOrigin;
104 | yOrigin = imp.getCalibration().yOrigin;
105 | zOrigin = imp.getCalibration().zOrigin;
106 | tOrigin = 0.0;
107 |
108 | xUnit = imp.getCalibration().getXUnit();
109 | yUnit = imp.getCalibration().getYUnit();
110 | zUnit = imp.getCalibration().getZUnit();
111 | tUnit = imp.getCalibration().getTimeUnit();
112 |
113 | globalUnit = imp.getCalibration().getUnit();
114 |
115 | otherMetadata = new HashMap<>();
116 | final Properties props = imp.getProperties();
117 | if ( props != null )
118 | for ( final Object k : props.keySet() )
119 | otherMetadata.put( k.toString(), props.get( k ).toString() );
120 | }
121 |
122 | @Override
123 | public void writeMetadata( final ImagePlusMetadataTemplate t, final ImagePlus ip )
124 | {
125 | ip.setTitle( t.name );
126 | ip.setDimensions(numChannels, numSlices, numFrames);
127 |
128 | ip.getCalibration().pixelWidth = t.xResolution;
129 | ip.getCalibration().pixelDepth = t.yResolution;
130 | ip.getCalibration().pixelHeight = t.zResolution;
131 |
132 | ip.getCalibration().xOrigin = t.xOrigin;
133 | ip.getCalibration().yOrigin = t.yOrigin;
134 | ip.getCalibration().zOrigin = t.zOrigin;
135 |
136 | ip.getCalibration().setXUnit( t.xUnit );
137 | ip.getCalibration().setYUnit( t.yUnit );
138 | ip.getCalibration().setZUnit( t.zUnit );
139 | ip.getCalibration().setUnit( t.globalUnit );
140 |
141 | ip.getCalibration().setTimeUnit( t.tUnit );
142 |
143 | final Properties props = ip.getProperties();
144 | if( t.otherMetadata != null )
145 | for( final String k : t.otherMetadata.keySet() )
146 | props.put( k, t.otherMetadata.get( k ));
147 |
148 | }
149 |
150 | @Override
151 | public ImagePlusMetadataTemplate readMetadata(ImagePlus ip) throws IOException {
152 |
153 | return new ImagePlusMetadataTemplate( "", ip, null );
154 | }
155 |
156 | public static ImagePlusMetadataTemplate readMetadataStatic(ImagePlus ip) throws IOException {
157 |
158 | return new ImagePlusMetadataTemplate( "", ip, null );
159 | }
160 |
161 | @Override
162 | public AffineGet spatialTransform() {
163 |
164 | if( numSpatialDims == 3 )
165 | return new ScaleAndTranslation(
166 | new double[] {xResolution, yResolution, zResolution},
167 | new double[] {xOrigin, yOrigin, zOrigin });
168 | else
169 | return new ScaleAndTranslation(
170 | new double[] {xResolution, yResolution },
171 | new double[] {xOrigin, yOrigin });
172 | }
173 |
174 | @Override
175 | public String unit() {
176 |
177 | return globalUnit;
178 | }
179 |
180 | }
181 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/NodePopupMenu.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.ui;
2 |
3 | import java.awt.BorderLayout;
4 | import java.awt.Component;
5 | import java.awt.Dimension;
6 | import java.awt.Point;
7 | import java.awt.event.MouseAdapter;
8 | import java.awt.event.MouseEvent;
9 | import java.lang.reflect.Type;
10 | import java.util.HashMap;
11 |
12 | import javax.swing.BoxLayout;
13 | import javax.swing.JFrame;
14 | import javax.swing.JLabel;
15 | import javax.swing.JMenuItem;
16 | import javax.swing.JPanel;
17 | import javax.swing.JPopupMenu;
18 | import javax.swing.JScrollPane;
19 | import javax.swing.JTextArea;
20 | import javax.swing.JTree;
21 | import javax.swing.SwingUtilities;
22 | import javax.swing.tree.TreePath;
23 |
24 | import org.janelia.saalfeldlab.n5.universe.N5TreeNode;
25 | import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata;
26 |
27 | import com.fasterxml.jackson.core.JsonProcessingException;
28 | import com.fasterxml.jackson.core.util.DefaultIndenter;
29 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
30 | import com.fasterxml.jackson.databind.JsonMappingException;
31 | import com.fasterxml.jackson.databind.ObjectMapper;
32 | import com.fasterxml.jackson.databind.SerializationFeature;
33 | import com.google.gson.Gson;
34 | import com.google.gson.GsonBuilder;
35 | import com.google.gson.JsonDeserializationContext;
36 | import com.google.gson.JsonDeserializer;
37 | import com.google.gson.JsonElement;
38 | import com.google.gson.JsonObject;
39 | import com.google.gson.JsonParseException;
40 | import com.google.gson.JsonSerializationContext;
41 | import com.google.gson.JsonSerializer;
42 |
43 | import net.imglib2.realtransform.AffineTransform3D;
44 |
45 | public class NodePopupMenu extends JPopupMenu {
46 |
47 | private static final long serialVersionUID = 1431304870893536697L;
48 |
49 | protected final DatasetSelectorDialog n5SelectionDialog;
50 |
51 | protected final JMenuItem showMetadata;
52 |
53 | protected final PopupListener popupListener;
54 |
55 | private Point clickPt;
56 |
57 | private JTree tree;
58 |
59 | private JFrame metadataFrame;
60 |
61 | private JTextArea metadataTextArea;
62 |
63 | private final Gson gson;
64 |
65 | private final ObjectMapper objMapper;
66 |
67 | private final DefaultPrettyPrinter prettyPrinter;
68 |
69 | public NodePopupMenu(final DatasetSelectorDialog n5SelectionDialog) {
70 |
71 | this.n5SelectionDialog = n5SelectionDialog;
72 | this.tree = n5SelectionDialog.getJTree();
73 |
74 | popupListener = new PopupListener();
75 |
76 | GsonBuilder gsonBuilder = new GsonBuilder();
77 | gsonBuilder.registerTypeAdapter(AffineTransform3D.class, new NodePopupMenu.AffineTransform3DGsonAdapter());
78 | gson = gsonBuilder.create();
79 | objMapper = new ObjectMapper();
80 | objMapper.enable(SerializationFeature.INDENT_OUTPUT);
81 |
82 | prettyPrinter = new DefaultPrettyPrinter();
83 | DefaultPrettyPrinter.Indenter i = new DefaultIndenter(" ", "\n");
84 | prettyPrinter.indentArraysWith(i);
85 | prettyPrinter.indentObjectsWith(i);
86 | objMapper.setDefaultPrettyPrinter(prettyPrinter);
87 |
88 | showMetadata = new JMenuItem("Show Metadata");
89 | showMetadata.addActionListener(e -> showDialog());
90 | add(showMetadata);
91 |
92 | buildMetadataFrame();
93 | }
94 |
95 | public PopupListener getPopupListener() {
96 | return popupListener;
97 | }
98 |
99 | public void setupListeners() {
100 | n5SelectionDialog.getJTree().addMouseListener(popupListener);
101 | }
102 |
103 | public void showDialog() {
104 | if (popupListener.selPath != null) {
105 | Object o = popupListener.selPath.getLastPathComponent();
106 | if (o instanceof N5TreeNode) {
107 | final N5TreeNode node = (N5TreeNode) o;
108 | setText(node);
109 | } else if (o instanceof N5TreeNodeWrapper) {
110 | final N5TreeNodeWrapper wrapper = (N5TreeNodeWrapper) o;
111 | setText(wrapper.getNode());
112 | } else
113 | System.out.println(o.getClass());
114 | }
115 | metadataFrame.setVisible(true);
116 | }
117 |
118 | public void setText(final N5TreeNode node) {
119 | N5Metadata meta = node.getMetadata();
120 | String jsonTxt = gson.toJson(node.getMetadata());
121 | String jsonPretty = jsonTxt;
122 | try {
123 | HashMap tmpObj = objMapper.readValue( jsonTxt, HashMap.class );
124 | jsonPretty = objMapper.writer(prettyPrinter).writeValueAsString(tmpObj);
125 | } catch (JsonMappingException e) { } catch (JsonProcessingException e) { }
126 |
127 | metadataTextArea.setText(jsonPretty);
128 | }
129 |
130 | public class PopupListener extends MouseAdapter {
131 |
132 | TreePath selPath;
133 |
134 | public void mousePressed(MouseEvent e) {
135 |
136 | if( SwingUtilities.isRightMouseButton(e)) {
137 | clickPt = e.getPoint();
138 | Component c = e.getComponent();
139 |
140 | selPath = tree.getPathForLocation(e.getX(), e.getY());
141 | NodePopupMenu.this.show(e.getComponent(), e.getX(), e.getY());
142 | }
143 | }
144 | }
145 |
146 | public JFrame buildMetadataFrame()
147 | {
148 | metadataFrame = new JFrame("Metadata");
149 | metadataFrame.setPreferredSize(new Dimension( 400, 400 ));
150 | metadataFrame.setMinimumSize(new Dimension( 200, 200 ));
151 |
152 | final JPanel panel = new JPanel();
153 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
154 | panel.add( new JLabel("Metadata"));
155 |
156 | metadataTextArea = new JTextArea();
157 | metadataTextArea.setEditable( false );
158 |
159 | final JScrollPane textView = new JScrollPane( metadataTextArea );
160 | panel.add( textView, BorderLayout.CENTER );
161 |
162 | metadataFrame.add(panel);
163 | return metadataFrame;
164 | }
165 |
166 | public static class AffineTransform3DGsonAdapter implements JsonDeserializer, JsonSerializer {
167 |
168 | @Override
169 | public JsonElement serialize(AffineTransform3D src, Type typeOfSrc, JsonSerializationContext context) {
170 | JsonObject obj = new JsonObject();
171 | obj.add("matrix", context.serialize(src.getRowPackedCopy()));
172 | return obj;
173 | }
174 |
175 | @Override
176 | public AffineTransform3D deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
177 | throws JsonParseException {
178 |
179 | final AffineTransform3D affine = new AffineTransform3D();
180 | final double[] mtx = context.deserialize( json.getAsJsonObject().get("a"), double[].class );
181 | affine.set(mtx);
182 | return affine;
183 | }
184 | }
185 |
186 | }
187 |
--------------------------------------------------------------------------------
/src/test/resources/ngff.n5/ngff_grpAttributes/attributes.json:
--------------------------------------------------------------------------------
1 | {
2 | "axes": [
3 | "x",
4 | "y",
5 | "z"
6 | ],
7 | "multiscales": [
8 | {
9 | "datasets": [
10 | {
11 | "path": "s0",
12 | "transform": {
13 | "axes": [
14 | "z",
15 | "y",
16 | "x"
17 | ],
18 | "scale": [
19 | 5.24,
20 | 4.0,
21 | 4.0
22 | ],
23 | "translate": [
24 | 0.0,
25 | 0.0,
26 | 0.0
27 | ],
28 | "units": [
29 | "nm",
30 | "nm",
31 | "nm"
32 | ]
33 | }
34 | },
35 | {
36 | "path": "s1",
37 | "transform": {
38 | "axes": [
39 | "z",
40 | "y",
41 | "x"
42 | ],
43 | "scale": [
44 | 10.48,
45 | 8.0,
46 | 8.0
47 | ],
48 | "translate": [
49 | 2.62,
50 | 2.0,
51 | 2.0
52 | ],
53 | "units": [
54 | "nm",
55 | "nm",
56 | "nm"
57 | ]
58 | }
59 | },
60 | {
61 | "path": "s2",
62 | "transform": {
63 | "axes": [
64 | "z",
65 | "y",
66 | "x"
67 | ],
68 | "scale": [
69 | 20.96,
70 | 16.0,
71 | 16.0
72 | ],
73 | "translate": [
74 | 7.86,
75 | 6.0,
76 | 6.0
77 | ],
78 | "units": [
79 | "nm",
80 | "nm",
81 | "nm"
82 | ]
83 | }
84 | },
85 | {
86 | "path": "s3",
87 | "transform": {
88 | "axes": [
89 | "z",
90 | "y",
91 | "x"
92 | ],
93 | "scale": [
94 | 41.92,
95 | 32.0,
96 | 32.0
97 | ],
98 | "translate": [
99 | 18.34,
100 | 14.0,
101 | 14.0
102 | ],
103 | "units": [
104 | "nm",
105 | "nm",
106 | "nm"
107 | ]
108 | }
109 | },
110 | {
111 | "path": "s4",
112 | "transform": {
113 | "axes": [
114 | "z",
115 | "y",
116 | "x"
117 | ],
118 | "scale": [
119 | 83.84,
120 | 64.0,
121 | 64.0
122 | ],
123 | "translate": [
124 | 39.300000000000004,
125 | 30.0,
126 | 30.0
127 | ],
128 | "units": [
129 | "nm",
130 | "nm",
131 | "nm"
132 | ]
133 | }
134 | },
135 | {
136 | "path": "s5",
137 | "transform": {
138 | "axes": [
139 | "z",
140 | "y",
141 | "x"
142 | ],
143 | "scale": [
144 | 167.68,
145 | 128.0,
146 | 128.0
147 | ],
148 | "translate": [
149 | 81.22,
150 | 62.0,
151 | 62.0
152 | ],
153 | "units": [
154 | "nm",
155 | "nm",
156 | "nm"
157 | ]
158 | }
159 | }
160 | ]
161 | }
162 | ],
163 | "n5": "2.0.0",
164 | "name": "em/fibsem-uint8",
165 | "pixelResolution": {
166 | "dimensions": [
167 | 4.0,
168 | 4.0,
169 | 5.24
170 | ],
171 | "unit": "nm"
172 | },
173 | "scales": [
174 | [
175 | 1,
176 | 1,
177 | 1
178 | ],
179 | [
180 | 2,
181 | 2,
182 | 2
183 | ],
184 | [
185 | 4,
186 | 4,
187 | 4
188 | ],
189 | [
190 | 8,
191 | 8,
192 | 8
193 | ],
194 | [
195 | 16,
196 | 16,
197 | 16
198 | ],
199 | [
200 | 32,
201 | 32,
202 | 32
203 | ]
204 | ],
205 | "units": [
206 | "nm",
207 | "nm",
208 | "nm"
209 | ]
210 | }
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/N5DatasetTreeCellRenderer.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.ui;
2 |
3 | import java.awt.Component;
4 | import java.text.CharacterIterator;
5 | import java.text.StringCharacterIterator;
6 | import java.util.Arrays;
7 |
8 | import javax.swing.JTree;
9 | import javax.swing.tree.DefaultTreeCellRenderer;
10 |
11 | import org.apache.commons.lang.ArrayUtils;
12 | import org.janelia.saalfeldlab.n5.DataType;
13 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
14 | import org.janelia.saalfeldlab.n5.metadata.imagej.N5ImagePlusMetadata;
15 | import org.janelia.saalfeldlab.n5.universe.N5TreeNode;
16 | import org.janelia.saalfeldlab.n5.universe.metadata.N5DatasetMetadata;
17 | import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata;
18 | import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMultiScaleMetadata;
19 |
20 | import ij.ImagePlus;
21 |
22 | public class N5DatasetTreeCellRenderer extends DefaultTreeCellRenderer
23 | {
24 | private static final long serialVersionUID = -4245251506197982653L;
25 |
26 | protected static final String thinSpace = " ";
27 |
28 | protected static final String times = "×";
29 |
30 | protected static final String warningFormat = "%s";
31 |
32 | protected static final String nameFormat = "%s";
33 |
34 | protected static final String dimDelimeter = thinSpace + times + thinSpace;
35 |
36 | protected final boolean showConversionWarning;
37 |
38 | protected String rootName;
39 |
40 | public N5DatasetTreeCellRenderer( final boolean showConversionWarning )
41 | {
42 | this.showConversionWarning = showConversionWarning;
43 | }
44 |
45 | public void setRootName( final String rootName ) {
46 | this.rootName = rootName;
47 | }
48 |
49 | @Override
50 | public Component getTreeCellRendererComponent( final JTree tree, final Object value,
51 | final boolean sel, final boolean exp, final boolean leaf, final int row, final boolean hasFocus )
52 | {
53 |
54 | super.getTreeCellRendererComponent( tree, value, sel, exp, leaf, row, hasFocus );
55 |
56 | N5SwingTreeNode node;
57 | if ( value instanceof N5SwingTreeNode )
58 | {
59 | node = ( ( N5SwingTreeNode ) value );
60 | if ( node.getMetadata() != null && node.getMetadata() instanceof N5DatasetMetadata )
61 | {
62 | final String convSuffix = conversionSuffix( node );
63 | final String conversionString;
64 | if ( showConversionWarning && !convSuffix.isEmpty() )
65 | conversionString = " " + String.format( warningFormat, conversionSuffix( node ) );
66 | else
67 | conversionString = "";
68 |
69 | final String memStr = memString( node );
70 | final String memSizeString = memStr.isEmpty() ? "" : " (" + memStr + ")";
71 | final String name = node.getParent() == null ? rootName : node.getNodeName();
72 |
73 | setText( String.join( "", new String[]{
74 | "",
75 | String.format( nameFormat, name ),
76 | " (",
77 | getParameterString( node ),
78 | conversionString,
79 | ")",
80 | memSizeString,
81 | ""
82 | }));
83 | }
84 | else
85 | {
86 | setText(node.getParent() == null ? rootName : node.getNodeName());
87 | }
88 | }
89 | return this;
90 | }
91 |
92 | public static String conversionSuffix( final N5TreeNode node ) {
93 |
94 | DataType type;
95 | final N5Metadata meta = node.getMetadata();
96 | if ( meta != null && meta instanceof N5DatasetMetadata )
97 | type = ((N5DatasetMetadata)node.getMetadata()).getAttributes().getDataType();
98 | else
99 | return "";
100 |
101 | if ( node.getMetadata() instanceof N5ImagePlusMetadata ) {
102 | final N5ImagePlusMetadata ijMeta = (N5ImagePlusMetadata)node.getMetadata();
103 | if( ijMeta.getType() == ImagePlus.COLOR_RGB && type == DataType.UINT32 )
104 | return "(RGB)";
105 | }
106 |
107 | if (type == DataType.FLOAT64) {
108 | return "→ 32-bit";
109 | } else if (type == DataType.INT8) {
110 | return "→ 8-bit";
111 | } else if (type == DataType.INT32 || type == DataType.INT64 ||
112 | type == DataType.UINT32 || type == DataType.UINT64 ||
113 | type == DataType.INT16 )
114 | {
115 | return "→ 16-bit";
116 | }
117 | return "";
118 | }
119 |
120 | public String getParameterString(final N5TreeNode node) {
121 |
122 | final N5Metadata meta = node.getMetadata();
123 | if (meta == null || !(meta instanceof N5DatasetMetadata))
124 | return "";
125 |
126 | final DatasetAttributes attributes = ((N5DatasetMetadata)node.getMetadata()).getAttributes();
127 | final String[] dimStrArr = Arrays.stream(attributes.getDimensions()).mapToObj(d -> Long.toString(d)).toArray(n -> new String[n]);
128 |
129 | if (OmeNgffMultiScaleMetadata.fOrder(attributes))
130 | ArrayUtils.reverse(dimStrArr);
131 |
132 | return String.join(dimDelimeter, dimStrArr) + ", " + attributes.getDataType();
133 | }
134 |
135 | protected String memString( final N5TreeNode node )
136 | {
137 | final N5Metadata meta = node.getMetadata();
138 | if ( meta == null || !(meta instanceof N5DatasetMetadata ) )
139 | return "";
140 |
141 | final DatasetAttributes attributes = ((N5DatasetMetadata)node.getMetadata()).getAttributes();
142 | final long nBytes = estimateBytes(attributes);
143 | if( nBytes < 0)
144 | return "";
145 | else
146 | return humanReadableByteCountSI(nBytes);
147 | }
148 |
149 | /*
150 | * https://programming.guide/java/formatting-byte-size-to-human-readable-format.html
151 | */
152 | protected static String humanReadableByteCountSI(long bytes) {
153 | if (-1000 < bytes && bytes < 1000) {
154 | return bytes + " B";
155 | }
156 | final CharacterIterator ci = new StringCharacterIterator("kMGTPE");
157 | while (bytes <= -999_950 || bytes >= 999_950) {
158 | bytes /= 1000;
159 | ci.next();
160 | }
161 | return String.format("%.1f %cB", bytes / 1000.0, ci.current());
162 | }
163 |
164 | private long estimateBytes( final DatasetAttributes attrs )
165 | {
166 | final long N = Arrays.stream( attrs.getDimensions() ).reduce( 1, (i,v) -> i*v );
167 | final String typeString = attrs.getDataType().toString();
168 | long nBytes = -1;
169 | if( typeString.endsWith( "8" ))
170 | nBytes = N;
171 | else if( typeString.endsWith( "16" ))
172 | nBytes = N*2;
173 | else if( typeString.endsWith( "32" ))
174 | nBytes = N*4;
175 | else if( typeString.endsWith( "64" ))
176 | nBytes = N*8;
177 |
178 | return nBytes;
179 | }
180 |
181 | }
182 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/MetadataTemplateMapper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.metadata.imagej;
27 |
28 | import com.fasterxml.jackson.databind.JsonNode;
29 | import com.fasterxml.jackson.databind.ObjectMapper;
30 | import com.fasterxml.jackson.databind.node.TextNode;
31 | import com.google.gson.Gson;
32 | import com.google.gson.GsonBuilder;
33 | import com.google.gson.JsonElement;
34 | import com.google.gson.JsonParser;
35 | import net.thisptr.jackson.jq.BuiltinFunctionLoader;
36 | import net.thisptr.jackson.jq.Expression;
37 | import net.thisptr.jackson.jq.Function;
38 | import net.thisptr.jackson.jq.JsonQuery;
39 | import net.thisptr.jackson.jq.PathOutput;
40 | import net.thisptr.jackson.jq.Scope;
41 | import net.thisptr.jackson.jq.Version;
42 | import net.thisptr.jackson.jq.Versions;
43 | import net.thisptr.jackson.jq.exception.JsonQueryException;
44 | import net.thisptr.jackson.jq.internal.misc.Strings;
45 | import net.thisptr.jackson.jq.path.Path;
46 |
47 | import java.io.IOException;
48 | import java.util.ArrayList;
49 | import java.util.List;
50 | import java.util.Map;
51 |
52 | import org.janelia.saalfeldlab.n5.N5Writer;
53 | import org.janelia.saalfeldlab.n5.universe.metadata.N5MetadataWriter;
54 |
55 | public class MetadataTemplateMapper implements N5MetadataWriter {
56 |
57 | public ImagePlusMetadataTemplate template;
58 |
59 | private final Scope scope;
60 |
61 | private final String query;
62 |
63 | private Gson gson;
64 |
65 | private static final ObjectMapper MAPPER = new ObjectMapper();
66 |
67 | private ObjectMapper objMapper;
68 |
69 | public MetadataTemplateMapper(final Scope scope, final String query) {
70 |
71 | this.scope = scope;
72 | this.query = query;
73 | gson = new GsonBuilder().create();
74 | objMapper = MAPPER;
75 | }
76 |
77 | public MetadataTemplateMapper( final String query )
78 | {
79 | this( buildRootScope(), query );
80 | }
81 |
82 | public void setObjectMapper( final ObjectMapper objMapper )
83 | {
84 | this.objMapper = objMapper;
85 | }
86 |
87 | public JsonQuery getQuery() throws JsonQueryException
88 | {
89 | return JsonQuery.compile( query, Versions.JQ_1_6 );
90 | }
91 |
92 | public Scope getScope()
93 | {
94 | return scope;
95 | }
96 |
97 | public String mapToJson( final ImagePlusMetadataTemplate metadata ) throws IOException
98 | {
99 | return map( gson.toJson( metadata ));
100 | }
101 |
102 | public List map( final JsonNode in ) throws JsonQueryException
103 | {
104 | final List< JsonNode > out = new ArrayList<>();
105 | getQuery().apply( scope, in, out::add );
106 | return out;
107 | }
108 |
109 | public String map( final String input ) throws IOException
110 | {
111 | final List out = map( objMapper.readTree( input ));
112 | final StringBuffer stringOutput = new StringBuffer();
113 | for ( final JsonNode node : out )
114 | stringOutput.append( node.toString() + "\n" );
115 |
116 | return stringOutput.toString();
117 | }
118 |
119 | public Object computeToMap( final String json ) throws IOException
120 | {
121 | return gson.fromJson( map( json ), Object.class );
122 | }
123 |
124 | public JsonElement computeToJson( final String input ) throws IOException
125 | {
126 | return new JsonParser().parse( map( input ));
127 | }
128 |
129 | public static Scope buildRootScope()
130 | {
131 | // First of all, you have to prepare a Scope which s a container of built-in/user-defined functions and variables.
132 | final Scope rootScope = Scope.newEmptyScope();
133 |
134 | // Use BuiltinFunctionLoader to load built-in functions from the classpath.
135 | BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, rootScope);
136 |
137 | // You can also define a custom function. E.g.
138 | rootScope.addFunction("repeat", 1, new Function() {
139 | @Override
140 | public void apply(final Scope scope, final List args, final JsonNode in, final Path path, final PathOutput output, final Version version) throws JsonQueryException {
141 | args.get(0).apply(scope, in, (time) -> {
142 | output.emit(new TextNode(Strings.repeat(in.asText(), time.asInt())), null);
143 | });
144 | }
145 | });
146 | return rootScope;
147 | }
148 |
149 | public static final String RESOLUTION_ONLY_MAPPER = "{\n\"resolution\" : [.xResolution, .yResolution, .zResolution ]\n}";
150 |
151 | public static final String COSEM_MAPPER = "{\n\t\"transform\":\n" +
152 | "\t{\n" +
153 | "\t\"scale\": [.zResolution, .yResolution, .xResolution],\n" +
154 | "\t\"translate\": [.zOrigin, .yOrigin, .xOrigin],\n" +
155 | "\t\"axes\": [\"z\", \"y\", \"x\"],\n" +
156 | "\t\"units\": [.globalUnit, .globalUnit, .globalUnit]\n" +
157 | "\t}\n" +
158 | "}";
159 |
160 | @Override
161 | public void writeMetadata(final ImagePlusMetadataTemplate t, final N5Writer n5, final String group) throws Exception {
162 |
163 | final Map map = (Map)computeToMap(gson.toJson(t));
164 | for (String key : map.keySet())
165 | n5.setAttribute(group, key, map.get(key));
166 | }
167 |
168 | public String toJsonString( final ImagePlusMetadataTemplate t ) throws Exception
169 | {
170 | return computeToJson( gson.toJson( t )).toString();
171 | }
172 |
173 | }
174 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/TestRegionExport.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5;
2 |
3 | import static org.junit.Assert.assertArrayEquals;
4 | import static org.junit.Assert.assertEquals;
5 |
6 | import java.io.File;
7 | import java.nio.file.Files;
8 | import java.util.Arrays;
9 | import java.util.stream.Collectors;
10 |
11 | import org.janelia.saalfeldlab.n5.ij.N5ScalePyramidExporter;
12 | import org.janelia.saalfeldlab.n5.ij.N5SubsetExporter;
13 | import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
14 | import org.junit.Test;
15 |
16 | import ij.ImagePlus;
17 | import ij.gui.NewImage;
18 | import net.imglib2.FinalInterval;
19 | import net.imglib2.RandomAccessibleInterval;
20 | import net.imglib2.cache.img.CachedCellImg;
21 | import net.imglib2.img.array.ArrayImg;
22 | import net.imglib2.img.array.ArrayImgs;
23 | import net.imglib2.img.basictypeaccess.array.ByteArray;
24 | import net.imglib2.loops.LoopBuilder;
25 | import net.imglib2.type.numeric.integer.UnsignedByteType;
26 | import net.imglib2.util.Intervals;
27 | import net.imglib2.view.Views;
28 |
29 | public class TestRegionExport {
30 |
31 | private static String tempN5PathName() {
32 |
33 | try {
34 | final File tmpFile = Files.createTempDirectory("n5-region-test-").toFile();
35 | tmpFile.deleteOnExit();
36 | return tmpFile.getCanonicalPath();
37 | } catch (final Exception e) {
38 | throw new RuntimeException(e);
39 | }
40 | }
41 |
42 | @Test
43 | public void testCreate() {
44 |
45 | long[] trueDims = new long[]{8, 6, 2};
46 | final ImagePlus imp = NewImage.createImage("test",
47 | (int)trueDims[0], (int)trueDims[1], (int)trueDims[2],
48 | 16, NewImage.FILL_NOISE);
49 |
50 | String baseDir = tempN5PathName();
51 | System.out.println(baseDir);
52 |
53 | final String rootPath = baseDir + "/test_create.n5";
54 | final String blockSizeString = "32";
55 | final String compressionString = N5ScalePyramidExporter.RAW_COMPRESSION;
56 |
57 | final String dsetZeroOffset = "/zeroOffset";
58 | final String zeroOffsetString = "0,0,0";
59 |
60 | // should create a dataset
61 | // a zero offset should write an array of the same size as the input
62 | final N5SubsetExporter writerZero = new N5SubsetExporter();
63 | writerZero.setOptions(imp, rootPath, dsetZeroOffset, zeroOffsetString, blockSizeString, compressionString);
64 | writerZero.run();
65 |
66 | final N5Writer n5 = new N5FSWriter(rootPath);
67 | final long[] dims = n5.getDatasetAttributes(dsetZeroOffset).getDimensions();
68 | assertArrayEquals("zero-offset", trueDims, dims);
69 |
70 | // should create a dataset
71 | // a non-zero offset should write an array of size larger than the input
72 | final String dsetOffset = "/offset";
73 | final int[] offset = new int[]{10, 20, 30};
74 | final String offsetString = Arrays.stream(offset).mapToObj(Integer::toString).collect(Collectors.joining(","));
75 |
76 | final N5SubsetExporter writerOffset = new N5SubsetExporter();
77 | writerOffset.setOptions(imp, rootPath, dsetOffset, offsetString, blockSizeString, compressionString);
78 | writerOffset.run();
79 |
80 | final long[] trueOffsetDims = new long[3];
81 | for (int i = 0; i < 3; i++)
82 | trueOffsetDims[i] = trueDims[i] + offset[i];
83 |
84 | final long[] dimsOffset = n5.getDatasetAttributes(dsetOffset).getDimensions();
85 | assertArrayEquals("offset", trueOffsetDims, dimsOffset);
86 |
87 | n5.remove();
88 | n5.close();
89 | }
90 |
91 | @Test
92 | public void testOverwrite() {
93 |
94 | final long[] origDims = new long[]{16, 16, 16};
95 | final ImagePlus impBase = NewImage.createImage("test",
96 | (int)origDims[0], (int)origDims[1], (int)origDims[2],
97 | 8, NewImage.FILL_BLACK);
98 |
99 | final long[] patchDims = new long[]{3, 3, 3};
100 | final ImagePlus impFill = NewImage.createImage("test",
101 | (int)patchDims[0], (int)patchDims[1], (int)patchDims[2],
102 | 8, NewImage.FILL_WHITE);
103 |
104 | String baseDir = tempN5PathName();
105 | System.out.println(baseDir);
106 |
107 | final String rootPath = baseDir + "/test_patch.n5";
108 | final String blockSizeString = "32";
109 | final String compressionString = N5ScalePyramidExporter.RAW_COMPRESSION;
110 |
111 | final String dset = "/patch";
112 | final String zeroOffsetString = "0,0,0";
113 |
114 | // should create a dataset
115 | // a zero offset should write an array of the same size as the input
116 | final N5SubsetExporter writerZero = new N5SubsetExporter();
117 | writerZero.setOptions(impBase, rootPath, dset, zeroOffsetString, blockSizeString, compressionString);
118 | writerZero.run();
119 |
120 | final N5Reader n5 = new N5FSReader(rootPath);
121 | final CachedCellImg origImg = N5Utils.open(n5, dset);
122 | final byte[] dataBefore = copyToArray(origImg);
123 |
124 | final byte[] zeros = new byte[(int)Intervals.numElements(origImg)];
125 | assertArrayEquals("orig data", zeros, dataBefore);
126 |
127 |
128 | // should create a dataset
129 | // a non-zero offset should write an array of size larger than the input
130 | final long[] offset = new long[]{1,2,3};
131 | final String offsetString = Arrays.stream(offset).mapToObj(Long::toString).collect(Collectors.joining(","));
132 |
133 | final N5SubsetExporter writerOffset = new N5SubsetExporter();
134 | writerOffset.setOptions(impFill, rootPath, dset, offsetString, blockSizeString, compressionString);
135 | writerOffset.run();
136 |
137 | final long[] dimsOffset = n5.getDatasetAttributes(dset).getDimensions();
138 | assertArrayEquals("dims unchanged", origDims, dimsOffset);
139 |
140 | final CachedCellImg patchedImg = N5Utils.open(n5, dset);
141 | final byte[] dataPatched = copyToArray(patchedImg);
142 |
143 | // '-1' when represented as a signed byte
144 | final byte UBYTEMAX = new UnsignedByteType(255).getByte();
145 |
146 | // check that every value is either 0 or 255
147 | int numZero = 0;
148 | int num255 = 0;
149 | for( int i = 0; i < dataPatched.length; i++ )
150 | if( dataPatched[i] == 0)
151 | numZero++;
152 | else if( dataPatched[i] == UBYTEMAX)
153 | num255++;
154 |
155 | assertEquals("all values must be 0 or 255", dataPatched.length, numZero + num255);
156 |
157 | // check that every value in the patch is 255
158 | final long[] min = offset;
159 | final long[] max = new long[ min.length ];
160 | for( int i = 0; i < min.length; i++ )
161 | max[i] = min[i] + patchDims[i] - 1;
162 |
163 | final FinalInterval patchInterval = new FinalInterval(min, max);
164 | final byte[] dataInPatch = copyToArray(Views.interval(patchedImg, patchInterval));
165 | final byte[] data255 = new byte[dataInPatch.length];
166 | Arrays.fill(data255, UBYTEMAX);
167 | assertArrayEquals("patched data", data255, dataInPatch);
168 |
169 | n5.close();
170 | }
171 |
172 | private static final byte[] copyToArray( final RandomAccessibleInterval img ) {
173 |
174 | final byte[] data = new byte[(int)Intervals.numElements(img)];
175 | ArrayImg imgCopy = ArrayImgs.unsignedBytes(data, img.dimensionsAsLongArray());
176 | LoopBuilder.setImages(img, imgCopy).forEachPixel((x, y) -> {
177 | y.set(x.get());
178 | });
179 | return data;
180 | }
181 |
182 | }
183 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/ImagePlusLegacyMetadataParser.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.metadata.imagej;
2 |
3 | import java.io.IOException;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 | import java.util.Optional;
7 | import java.util.Properties;
8 |
9 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
10 | import org.janelia.saalfeldlab.n5.N5Exception;
11 | import org.janelia.saalfeldlab.n5.N5Reader;
12 | import org.janelia.saalfeldlab.n5.universe.N5TreeNode;
13 | import org.janelia.saalfeldlab.n5.N5Writer;
14 | import org.janelia.saalfeldlab.n5.universe.metadata.N5MetadataParser;
15 | import org.janelia.saalfeldlab.n5.universe.metadata.N5MetadataWriter;
16 |
17 | import ij.ImagePlus;
18 | import ij.measure.Calibration;
19 |
20 | public class ImagePlusLegacyMetadataParser implements N5MetadataParser,
21 | N5MetadataWriter, ImageplusMetadata
22 | {
23 |
24 | public static final String titleKey = "title";
25 | public static final String fpsKey = "fps";
26 | public static final String frameIntervalKey = "frameInterval";
27 | public static final String pixelWidthKey = "pixelWidth";
28 | public static final String pixelHeightKey = "pixelHeight";
29 | public static final String pixelDepthKey = "pixelDepth";
30 | public static final String pixelUnitKey = "pixelUnit";
31 | public static final String xOriginKey = "xOrigin";
32 | public static final String yOriginKey = "yOrigin";
33 | public static final String zOriginKey = "zOrigin";
34 |
35 | public static final String numChannelsKey = "numChannels";
36 | public static final String numSlicesKey = "numSlices";
37 | public static final String numFramesKey = "numFrames";
38 |
39 | public static final String typeKey = "ImagePlusType";
40 |
41 | public static final String imagePropertiesKey = "imageProperties";
42 |
43 | public static final String downsamplingFactorsKey = "downsamplingFactors";
44 |
45 | @Override
46 | public void writeMetadata(final N5ImagePlusMetadata t, final N5Writer n5, final String dataset) throws Exception {
47 |
48 | if (!n5.datasetExists(dataset))
49 | throw new Exception("Can't write into " + dataset + ". Must be a dataset.");
50 |
51 | final HashMap attrs = new HashMap<>();
52 | attrs.put(titleKey, t.name);
53 |
54 | attrs.put(fpsKey, t.fps);
55 | attrs.put(frameIntervalKey, t.frameInterval);
56 | attrs.put(pixelWidthKey, t.pixelWidth);
57 | attrs.put(pixelHeightKey, t.pixelHeight);
58 | attrs.put(pixelDepthKey, t.pixelDepth);
59 | attrs.put(pixelUnitKey, t.unit);
60 |
61 | attrs.put(xOriginKey, t.xOrigin);
62 | attrs.put(yOriginKey, t.yOrigin);
63 | attrs.put(zOriginKey, t.zOrigin);
64 |
65 | attrs.put(numChannelsKey, t.numChannels);
66 | attrs.put(numSlicesKey, t.numSlices);
67 | attrs.put(numFramesKey, t.numFrames);
68 |
69 | attrs.put(typeKey, t.type);
70 |
71 | attrs.put( imagePropertiesKey, t.properties );
72 |
73 | // if (t.properties != null) {
74 | // for (final Object k : t.properties.keySet()) {
75 | // try {
76 | // attrs.put(k.toString(), t.properties.get(k).toString());
77 | // } catch (final Exception e) {
78 | // }
79 | // }
80 | // }
81 |
82 | n5.setAttributes(dataset, attrs);
83 | }
84 |
85 | @Override
86 | public void writeMetadata(final N5ImagePlusMetadata t, final ImagePlus ip) throws IOException {
87 | ip.setTitle(t.name);
88 |
89 | final Calibration cal = ip.getCalibration();
90 | cal.fps = t.fps;
91 | cal.frameInterval = t.frameInterval;
92 | cal.pixelWidth = t.pixelWidth;
93 | cal.pixelHeight = t.pixelHeight;
94 | cal.pixelDepth = t.pixelDepth;
95 | cal.setUnit(t.unit);
96 |
97 | cal.xOrigin = t.xOrigin;
98 | cal.yOrigin = t.yOrigin;
99 | cal.zOrigin = t.zOrigin;
100 | ip.setCalibration( cal );
101 |
102 | ip.setDimensions(t.numChannels, t.numSlices, t.numFrames);
103 |
104 | final Properties props = ip.getProperties();
105 | if (t.properties != null) {
106 | for (final String k : t.properties.keySet()) {
107 | try {
108 | props.put(k, t.properties.get(k));
109 | } catch (final Exception e) {
110 | }
111 | }
112 | }
113 | }
114 |
115 | @Override
116 | public N5ImagePlusMetadata readMetadata(final ImagePlus ip) throws IOException {
117 |
118 | String name;
119 | if (ip.getTitle() == null)
120 | name = "ImagePlus";
121 | else
122 | name = ip.getTitle();
123 |
124 | final HashMap properties = new HashMap<>();
125 | final Properties props = ip.getProperties();
126 | if (props != null) {
127 | for (final Object k : props.keySet()) {
128 | try {
129 | properties.put(k.toString(), props.get(k));
130 | } catch (final Exception e) {
131 | }
132 | }
133 | }
134 |
135 | final Calibration cal = ip.getCalibration();
136 | final N5ImagePlusMetadata t = new N5ImagePlusMetadata("", null, name, cal.fps, cal.frameInterval, cal.getUnit(),
137 | cal.pixelWidth, cal.pixelHeight, cal.pixelDepth, cal.xOrigin, cal.yOrigin, cal.zOrigin, ip.getNChannels(),
138 | ip.getNSlices(), ip.getNFrames(), ip.getType(), properties );
139 |
140 | return t;
141 | }
142 |
143 | @Override
144 | public Optional parseMetadata(N5Reader n5, N5TreeNode node) {
145 |
146 | try {
147 | final String dataset = node.getPath();
148 | final DatasetAttributes attributes = n5.getDatasetAttributes(dataset);
149 | if (attributes == null)
150 | return Optional.empty();
151 |
152 | final String name = n5.getAttribute(dataset, titleKey, String.class);
153 |
154 | final Double pixelWidth = n5.getAttribute(dataset, pixelWidthKey, Double.class);
155 | final Double pixelHeight = n5.getAttribute(dataset, pixelHeightKey, Double.class);
156 | final Double pixelDepth = n5.getAttribute(dataset, pixelDepthKey, Double.class);
157 | final String unit = n5.getAttribute(dataset, pixelUnitKey, String.class);
158 |
159 | final Double xOrigin = n5.getAttribute(dataset, xOriginKey, Double.class);
160 | final Double yOrigin = n5.getAttribute(dataset, yOriginKey, Double.class);
161 | final Double zOrigin = n5.getAttribute(dataset, zOriginKey, Double.class);
162 |
163 | final Integer numChannels = n5.getAttribute(dataset, numChannelsKey, Integer.class);
164 | final Integer numSlices = n5.getAttribute(dataset, numSlicesKey, Integer.class);
165 | final Integer numFrames = n5.getAttribute(dataset, numFramesKey, Integer.class);
166 |
167 | final Double fps = n5.getAttribute(dataset, fpsKey, Double.class);
168 | final Double frameInterval = n5.getAttribute(dataset, fpsKey, Double.class);
169 |
170 | final Integer type = n5.getAttribute(dataset, typeKey, Integer.class);
171 |
172 | Map properties = n5.getAttribute(dataset, imagePropertiesKey, HashMap.class);
173 | if( properties == null )
174 | properties = new HashMap<>();
175 |
176 | final N5ImagePlusMetadata meta = new N5ImagePlusMetadata(dataset, attributes, name, fps, frameInterval,
177 | unit, pixelWidth, pixelHeight, pixelDepth, xOrigin, yOrigin, zOrigin, numChannels, numSlices,
178 | numFrames, type, properties);
179 |
180 | return Optional.of(meta);
181 |
182 | } catch (final N5Exception e) { }
183 |
184 | return Optional.empty();
185 | }
186 |
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/CanonicalMetadataToImagePlus.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.metadata.imagej;
2 |
3 | import ij.ImagePlus;
4 | import ij.measure.Calibration;
5 | import ij.process.LUT;
6 | import net.imglib2.realtransform.AffineGet;
7 | import net.imglib2.realtransform.AffineTransform;
8 | import net.imglib2.type.numeric.ARGBType;
9 |
10 | import org.janelia.saalfeldlab.n5.DataType;
11 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
12 | import org.janelia.saalfeldlab.n5.GzipCompression;
13 | import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis;
14 | import org.janelia.saalfeldlab.n5.universe.metadata.canonical.CanonicalDatasetMetadata;
15 | import org.janelia.saalfeldlab.n5.universe.metadata.canonical.CanonicalSpatialDatasetMetadata;
16 | import org.janelia.saalfeldlab.n5.universe.metadata.canonical.SpatialMetadataCanonical;
17 | import org.janelia.saalfeldlab.n5.universe.metadata.transforms.AffineSpatialTransform;
18 |
19 | import java.awt.Color;
20 | import java.io.IOException;
21 | import java.util.Arrays;
22 |
23 | public class CanonicalMetadataToImagePlus implements ImageplusMetadata {
24 |
25 | private final boolean setDims;
26 |
27 | public CanonicalMetadataToImagePlus( boolean setDims ) {
28 | this.setDims = setDims;
29 | }
30 |
31 | public CanonicalMetadataToImagePlus() {
32 | this(false);
33 | }
34 |
35 | @Override
36 | public void writeMetadata(final CanonicalDatasetMetadata t, final ImagePlus ip) throws IOException {
37 | if( t instanceof CanonicalSpatialDatasetMetadata )
38 | writeSpatialMetadata( (CanonicalSpatialDatasetMetadata) t, ip );
39 |
40 | ip.setDisplayRange( t.minIntensity(), t.maxIntensity());
41 |
42 | if( t.getColorMetadata() != null ) {
43 | int c = t.getColor().get();
44 | ip.setLut( LUT.createLutFromColor(
45 | new Color(
46 | ARGBType.red( c ),
47 | ARGBType.green( c ),
48 | ARGBType.blue( c ),
49 | ARGBType.alpha(c))));
50 | }
51 |
52 | }
53 |
54 | public void writeSpatialMetadata(final CanonicalSpatialDatasetMetadata t, final ImagePlus ip) throws IOException {
55 |
56 | final int nd = t.getAttributes().getNumDimensions();
57 | final AffineGet xfm = t.getSpatialTransform().spatialTransform();
58 | final String unit = t.getSpatialTransform().unit();
59 |
60 | ip.setTitle(t.getPath());
61 | final Calibration cal = ip.getCalibration();
62 | cal.pixelWidth = xfm.get(0, 0);
63 | cal.xOrigin = xfm.get(0, nd);
64 | cal.pixelHeight = xfm.get(1, 1);
65 | cal.yOrigin = xfm.get(1, nd);
66 |
67 | cal.setUnit(unit);
68 |
69 | final Axis[] axes = t.getSpatialTransform().getAxes();
70 | final long[] dims = t.getAttributes().getDimensions();
71 |
72 | // TODO setDimensions is not needed because
73 | // a permutation is applied elsewhere
74 | if( axes != null ) {
75 | ip.getCalibration().setXUnit( axes[0].getUnit() );
76 | ip.getCalibration().setYUnit( axes[1].getUnit() );
77 |
78 | int i = 2;
79 | int nz = 0;
80 | int nc = 0;
81 | int nt = 0;
82 | while( i < axes.length ) {
83 | // anything that is not space or time goes into channels
84 | // could be wrong if multiple dimensions are neither space nor time
85 | // if so, flatten all those dimensions into channels
86 | // (I think adding to dimensions accomplishes this - JB)
87 | // TODO reconsider if these defaults are what we want
88 | if( axes[i].getType().equals("space")) {
89 | nz += (int)dims[i];
90 | cal.pixelDepth = xfm.get(i, i);
91 | cal.zOrigin = xfm.get(i, i+1);
92 | }
93 | else if( axes[i].getType().equals("time")) {
94 | nt += (int)dims[i];
95 | cal.frameInterval = xfm.get(i, i);
96 | }
97 | else
98 | nc += (int)dims[i];
99 |
100 | i++;
101 | }
102 | nc = nc == 0 ? 1 : nc;
103 | nz = nz == 0 ? 1 : nz;
104 | nt = nt == 0 ? 1 : nt;
105 |
106 | if( setDims )
107 | ip.setDimensions(nc, nz, nt);
108 | }
109 | else if( setDims ){
110 | // if axes are not specified, assume xyz for 3d
111 | // and assume xyzc for 4d
112 | if (nd == 3) {
113 | ip.setDimensions(1, (int) dims[2], 1);
114 | cal.pixelDepth = xfm.get(2, 2);
115 | cal.zOrigin = xfm.get(2, nd);
116 | }
117 | else if (nd == 4) {
118 | ip.setDimensions((int) dims[3], (int) dims[2], 1);
119 | cal.pixelDepth = xfm.get(3, 3);
120 | cal.zOrigin = xfm.get(3, nd);
121 | }
122 | }
123 |
124 | }
125 |
126 | public Axis[] axesFromImageplus(final ImagePlus imp) {
127 |
128 | int nd = imp.getNDimensions();
129 | Axis[] axes = new Axis[ nd ];
130 |
131 | axes[ 0 ] = new Axis( "X", "space", imp.getCalibration().getXUnit());
132 | axes[ 1 ] = new Axis( "Y", "space", imp.getCalibration().getYUnit());
133 |
134 | int i = 2;
135 | if( imp.getNChannels() > 1 )
136 | axes[ i++ ] = new Axis( "channels", "C", "null" );
137 |
138 | if( imp.getNSlices() > 1 )
139 | axes[ i++ ] = new Axis( "space", "Z", imp.getCalibration().getZUnit());
140 |
141 | if( imp.getNFrames() > 1 )
142 | axes[ i++ ] = new Axis( "time", "T", imp.getCalibration().getTimeUnit() );
143 |
144 | return axes;
145 | }
146 |
147 | public AffineTransform getAffine( final ImagePlus imp ) {
148 | int nd = imp.getNDimensions();
149 | AffineTransform affine = new AffineTransform( nd );
150 |
151 | affine.set( imp.getCalibration().pixelWidth, 0, 0);
152 | affine.set( imp.getCalibration().xOrigin, 0, nd);
153 |
154 | affine.set( imp.getCalibration().pixelHeight, 1, 1);
155 | affine.set( imp.getCalibration().yOrigin, 1, nd);
156 |
157 | int i = 2;
158 | if( imp.getNChannels() > 1 ) {
159 | // channels dont have spacing information
160 | i++;
161 | }
162 |
163 | if( imp.getNSlices() > 1 ) {
164 | affine.set( imp.getCalibration().pixelDepth, i, i);
165 | affine.set( imp.getCalibration().zOrigin, i, nd);
166 | i++;
167 | }
168 |
169 | if( imp.getNFrames() > 1 ) {
170 | affine.set( imp.getCalibration().frameInterval, i, i);
171 | i++;
172 | }
173 |
174 | return affine;
175 | }
176 |
177 | @Override
178 | public CanonicalSpatialDatasetMetadata readMetadata(final ImagePlus imp) throws IOException {
179 | int nd = imp.getNDimensions();
180 |
181 | double[] params = getAffine( imp ).getRowPackedCopy();
182 | // double[] params;
183 | // if (nd == 2)
184 | // params = new double[] {
185 | // imp.getCalibration().pixelWidth, 0, imp.getCalibration().xOrigin,
186 | // 0, imp.getCalibration().pixelHeight, imp.getCalibration().yOrigin };
187 | // else if (nd == 3)
188 | // params = new double[]{
189 | // imp.getCalibration().pixelWidth, 0, 0, imp.getCalibration().xOrigin,
190 | // 0, imp.getCalibration().pixelHeight, 0, imp.getCalibration().yOrigin,
191 | // 0, 0, imp.getCalibration().pixelDepth, imp.getCalibration().zOrigin };
192 | // else
193 | // return null;
194 |
195 | // TODO what to about attrs?
196 | final int[] impDims = Arrays.stream(imp.getDimensions()).filter( x -> x > 1 ).toArray();
197 | final long[] dims = Arrays.stream(impDims).mapToLong( x -> x ).toArray();
198 | final DatasetAttributes attributes = new DatasetAttributes( dims, impDims, DataType.FLOAT32, new GzipCompression());
199 |
200 | final SpatialMetadataCanonical spatialMeta = new SpatialMetadataCanonical("",
201 | new AffineSpatialTransform(params), imp.getCalibration().getUnit(),
202 | axesFromImageplus(imp));
203 |
204 | return new CanonicalSpatialDatasetMetadata("", spatialMeta, attributes);
205 | }
206 |
207 | }
208 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/ImprovedFormattedTextField.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.ui;
2 |
3 | import javax.swing.JFormattedTextField;
4 | import javax.swing.JTextField;
5 | import javax.swing.KeyStroke;
6 | import javax.swing.SwingUtilities;
7 | import javax.swing.event.DocumentEvent;
8 | import javax.swing.event.DocumentListener;
9 |
10 | import java.awt.Color;
11 | import java.awt.event.FocusAdapter;
12 | import java.awt.event.FocusEvent;
13 | import java.awt.event.KeyEvent;
14 | import java.net.URI;
15 | import java.net.URISyntaxException;
16 | import java.text.ParseException;
17 |
18 | /**
19 | *
20 | * Extension of {@code JFormattedTextField} which solves some of the usability
21 | * issues
22 | *
23 | *
24 | *
25 | * from
26 | * https://stackoverflow.com/questions/1313390/is-there-any-way-to-accept-only-numeric-values-in-a-jtextfield?answertab=scoredesc#tab-top
27 | *
28 | */
29 | public class ImprovedFormattedTextField extends JFormattedTextField {
30 |
31 | private static final long serialVersionUID = 6986337989217402465L;
32 |
33 | private static final Color ERROR_BACKGROUND_COLOR = new Color(255, 215, 215);
34 |
35 | private static final Color ERROR_FOREGROUND_COLOR = null;
36 |
37 | private Color fBackground, fForeground;
38 |
39 | private Runnable updateCallback;
40 |
41 | private boolean runCallback;
42 |
43 | public ImprovedFormattedTextField(AbstractFormatter formatter) {
44 |
45 | super(formatter);
46 | setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT);
47 | updateBackgroundOnEachUpdate();
48 |
49 | // improve the caret behavior
50 | // see also
51 | // http://tips4java.wordpress.com/2010/02/21/formatted-text-field-tips/
52 | addFocusListener(new MousePositionCorrectorListener());
53 | addFocusListener(new ContainerTextUpdateOnFocus(this));
54 | runCallback = true;
55 | }
56 |
57 | /**
58 | * Create a new {@code ImprovedFormattedTextField} instance which will use
59 | * {@code aFormat} for the validation of the user input. The field will be
60 | * initialized with {@code aValue}.
61 | *
62 | * @param formatter
63 | * The formatter. May not be {@code null}
64 | * @param aValue
65 | * The initial value
66 | */
67 | public ImprovedFormattedTextField(AbstractFormatter formatter, Object aValue) {
68 |
69 | this(formatter);
70 | String strValue = aValue instanceof String ? (String)aValue : "";
71 | try {
72 | setValue(new URI(strValue));
73 | } catch (URISyntaxException e) {
74 | e.printStackTrace();
75 | }
76 | }
77 |
78 | public void setCallback(final Runnable updateCallback) {
79 |
80 | this.updateCallback = updateCallback;
81 | }
82 |
83 | private void updateBackgroundOnEachUpdate() {
84 |
85 | getDocument().addDocumentListener(new DocumentListener() {
86 |
87 | @Override
88 | public void insertUpdate(DocumentEvent e) {
89 |
90 | update();
91 | }
92 |
93 | @Override
94 | public void removeUpdate(DocumentEvent e) {
95 |
96 | update();
97 | }
98 |
99 | @Override
100 | public void changedUpdate(DocumentEvent e) {
101 |
102 | update();
103 | }
104 |
105 | public void update() {
106 |
107 | updateBackground();
108 | if (runCallback && updateCallback != null)
109 | updateCallback.run();
110 | }
111 | });
112 | }
113 |
114 | /**
115 | * Update the background color depending on the valid state of the current
116 | * input. This provides visual feedback to the user
117 | */
118 | private void updateBackground() {
119 |
120 | final boolean valid = validContent();
121 | if (ERROR_BACKGROUND_COLOR != null) {
122 | setBackground(valid ? fBackground : ERROR_BACKGROUND_COLOR);
123 | }
124 | if (ERROR_FOREGROUND_COLOR != null) {
125 | setForeground(valid ? fForeground : ERROR_FOREGROUND_COLOR);
126 | }
127 | }
128 |
129 | @Override
130 | public void updateUI() {
131 |
132 | super.updateUI();
133 | fBackground = getBackground();
134 | fForeground = getForeground();
135 | }
136 |
137 | private boolean validContent() {
138 |
139 | final AbstractFormatter formatter = getFormatter();
140 | if (formatter != null) {
141 | try {
142 | formatter.stringToValue(getText());
143 | return true;
144 | } catch (final ParseException e) {
145 | return false;
146 | }
147 | }
148 | return true;
149 | }
150 |
151 |
152 | public void setValue(Object value, boolean callback, boolean validate) {
153 |
154 | boolean validValue = true;
155 | // before setting the value, parse it by using the format
156 | try {
157 | final AbstractFormatter formatter = getFormatter();
158 | if (formatter != null && validate ) {
159 | formatter.stringToValue(getText());
160 | }
161 | } catch (final ParseException e) {
162 | validValue = false;
163 | updateBackground();
164 | }
165 | // only set the value when valid
166 | if (validValue) {
167 | final int old_caret_position = getCaretPosition();
168 |
169 | final boolean before = runCallback;
170 | runCallback = callback;
171 | super.setValue(value);
172 | runCallback = before;
173 |
174 | setCaretPosition(Math.min(old_caret_position, getText().length()));
175 | }
176 | }
177 |
178 | public void setValue(Object value, boolean callback) {
179 |
180 | setValue(value, callback, true);
181 | }
182 |
183 | public void setValueNoCallback(Object value) {
184 |
185 | setValue(value, false);
186 | }
187 |
188 | @Override
189 | public void setValue(Object value) {
190 |
191 | setValue(value, true);
192 | }
193 |
194 | @Override
195 | protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
196 |
197 | // do not let the formatted text field consume the enters. This allows
198 | // to trigger an OK button by
199 | // pressing enter from within the formatted text field
200 | if (validContent()) {
201 | return super.processKeyBinding(ks, e, condition, pressed) && ks != KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
202 | } else {
203 | return super.processKeyBinding(ks, e, condition, pressed);
204 | }
205 | }
206 |
207 | public void validateAndUpdate() {
208 |
209 | final AbstractFormatter formatter = getFormatter();
210 | if (formatter != null) {
211 | try {
212 | Object result = formatter.stringToValue((String)getText());
213 | setValue((URI)result, false, false); // no callback, no validation
214 | } catch (ParseException ignore) {}
215 | }
216 | }
217 |
218 | private static class MousePositionCorrectorListener extends FocusAdapter {
219 |
220 | @Override
221 | public void focusGained(FocusEvent e) {
222 |
223 | /*
224 | * After a formatted text field gains focus, it replaces its text
225 | * with its current value, formatted appropriately of course. It
226 | * does this after any focus listeners are notified. We want to make
227 | * sure that the caret is placed in the correct position rather than
228 | * the dumb default that is before the 1st character !
229 | */
230 | final JTextField field = (JTextField)e.getSource();
231 | final int dot = field.getCaret().getDot();
232 | final int mark = field.getCaret().getMark();
233 | if (field.isEnabled() && field.isEditable()) {
234 | SwingUtilities.invokeLater(new Runnable() {
235 |
236 | @Override
237 | public void run() {
238 |
239 | // Only set the caret if the textfield hasn't got a
240 | // selection on it
241 | if (dot == mark) {
242 | field.getCaret().setDot(dot);
243 | }
244 | }
245 | });
246 | }
247 | }
248 | }
249 |
250 | private static class ContainerTextUpdateOnFocus extends FocusAdapter {
251 |
252 | private final ImprovedFormattedTextField field;
253 |
254 | public ContainerTextUpdateOnFocus(final ImprovedFormattedTextField field) {
255 |
256 | this.field = field;
257 | }
258 |
259 | @Override
260 | public void focusLost(FocusEvent e) {
261 | field.validateAndUpdate();
262 | }
263 | }
264 |
265 | }
266 |
--------------------------------------------------------------------------------
/src/test/java/org/janelia/saalfeldlab/n5/metadata/ome/ngff/v04/AxisPermutationTest.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.metadata.ome.ngff.v04;
2 |
3 | import static org.junit.Assert.assertEquals;
4 |
5 | import java.io.File;
6 | import java.io.IOException;
7 | import java.net.URI;
8 | import java.nio.file.Files;
9 | import java.util.Arrays;
10 | import java.util.function.BiConsumer;
11 |
12 | import org.apache.commons.lang3.ArrayUtils;
13 | import org.janelia.saalfeldlab.n5.DataType;
14 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
15 | import org.janelia.saalfeldlab.n5.N5Writer;
16 | import org.janelia.saalfeldlab.n5.RawCompression;
17 | import org.janelia.saalfeldlab.n5.ij.N5Importer;
18 | import org.janelia.saalfeldlab.n5.universe.N5Factory;
19 | import org.janelia.saalfeldlab.n5.universe.StorageFormat;
20 | import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata.CosemTransform;
21 | import org.janelia.saalfeldlab.n5.universe.metadata.NgffTests;
22 | import org.janelia.saalfeldlab.n5.universe.metadata.axes.AxisUtils;
23 | import org.junit.After;
24 | import org.junit.Before;
25 | import org.junit.Test;
26 |
27 | import ij.ImagePlus;
28 |
29 | public class AxisPermutationTest {
30 |
31 | private static final boolean C_ORDER = true;
32 | private static final boolean F_ORDER = false;
33 |
34 | private static final double EPS = 1e-6;
35 |
36 | public static final String[] AXIS_PERMUTATIONS = new String[]{
37 | "xyz", "zyx", "yzx",
38 | "xyc", "xcy", "cyx",
39 | "xyt", "xty", "tyx",
40 | "xyzt", "xtyz", "tyzx", "zxty",
41 | "xyczt", "xyzct", "xytcz", "tzcyx", "ctzxy"
42 | };
43 |
44 | private URI containerUri;
45 |
46 | @Before
47 | public void before() {
48 |
49 | System.setProperty("java.awt.headless", "true");
50 |
51 | try {
52 | containerUri = new File(tempN5PathName()).getCanonicalFile().toURI();
53 | } catch (final IOException e) {}
54 | }
55 |
56 | @After
57 | public void after() {
58 |
59 | final N5Writer n5 = new N5Factory().openWriter(containerUri.toString());
60 | n5.remove();
61 | }
62 |
63 | private static String tempN5PathName() {
64 |
65 | try {
66 | final File tmpFile = Files.createTempDirectory("n5-ij-ngff-test-").toFile();
67 | tmpFile.deleteOnExit();
68 | return tmpFile.getCanonicalPath();
69 | } catch (final Exception e) {
70 | throw new RuntimeException(e);
71 | }
72 | }
73 |
74 |
75 | /**
76 | * The default behavior of n5-imglib2 and n5-zarr is such that the dimensions of an imglib2 img
77 | * are the same as the zarr arrays shape if the zarr is c-order, but will be reversed if
78 | * c-order.
79 | *
80 | * Assuming that most people using zarr interpret their axes as ZYX regardless of f- or
81 | * c-ordering, we'd like that the N5Importer outputs an ImagePlus that "reverses" the shape of
82 | * for both c- and f-order zarr arrays.
83 | */
84 | @Test
85 | public void testZarrDimensionReversal() {
86 |
87 | final N5Writer zarr = new N5Factory().openWriter(StorageFormat.ZARR, containerUri.toString());
88 |
89 | final long[] dims = new long[]{6, 5, 4}; // x y z
90 | final int[] blkSize = new int[]{6, 5, 4};
91 | final RawCompression compression = new RawCompression();
92 | final DataType type = DataType.UINT8;
93 |
94 | String dset = "COrder";
95 | // writes a c-order zarr array with shape [4,5,6] "ZYX"
96 | NgffTests.createDataset(zarr, C_ORDER, dset, dims, blkSize, type, compression);
97 | final String uriC = containerUri.toString() + "?" + dset;
98 | final ImagePlus impC = N5Importer.open(uriC, false);
99 | assertEquals("nx C", 6, impC.getWidth());
100 | assertEquals("ny C", 5, impC.getHeight());
101 | assertEquals("nz C", 4, impC.getNChannels());
102 |
103 | dset = "FOrder";
104 | // writes a f-order zarr array with shape [4,5,6] "ZYX"
105 | NgffTests.createDataset(zarr, F_ORDER, dset, dims, blkSize, type, compression);
106 |
107 | final String uriF = containerUri.toString() + "?" + dset;
108 | final ImagePlus impF = N5Importer.open(uriF, false);
109 | assertEquals("nx F", 6, impF.getWidth());
110 | assertEquals("ny F", 5, impF.getHeight());
111 | assertEquals("nz F", 4, impF.getNChannels());
112 | }
113 |
114 | @Test
115 | public void testNgffPermutations() {
116 |
117 | final N5Writer zarr = new N5Factory().openWriter(StorageFormat.ZARR, containerUri.toString());
118 | // don't check every axis permutation, but some relevant ones, and some strange ones
119 | // check both c- and f-order storage
120 | for (final String axes : AXIS_PERMUTATIONS) {
121 | final String dsetC = axes + "_c";
122 | writeAndTest((z, n) -> {
123 | writePermutedAxesNgff(zarr, dsetC);
124 | }, zarr, dsetC);
125 |
126 | final String dsetF = axes + "_f";
127 | writeAndTest((z, n) -> {
128 | writePermutedAxesNgff(zarr, dsetF);
129 | }, zarr, dsetF);
130 | }
131 | }
132 |
133 | @Test
134 | public void testCosemAxisPermutations() {
135 |
136 | final N5Writer zarr = new N5Factory().openWriter(StorageFormat.ZARR, containerUri.toString());
137 | // don't check every axis permutation, but some relevant ones, and some strange ones
138 | // check both c- and f-order storage
139 | for (final String axes : AXIS_PERMUTATIONS) {
140 |
141 | final String dsetC = axes + "_c";
142 | writeAndTest((z, n) -> {
143 | writePermutedAxesCosem(zarr, dsetC + "/s0");
144 | }, zarr, dsetC);
145 |
146 | final String dsetF = axes + "_f";
147 | writeAndTest((z, n) -> {
148 | writePermutedAxesCosem(zarr, dsetF + "/s0");
149 | }, zarr, dsetF);
150 | }
151 | }
152 |
153 | protected void writeAndTest(final BiConsumer writeTestData, final N5Writer zarr, final String dset) {
154 |
155 | // write
156 | writeTestData.accept(zarr, dset);
157 |
158 | // read
159 | final ImagePlus imp = N5Importer.open(String.format("%s?%s", containerUri.toString(), dset + "/s0"), false);
160 |
161 | // test
162 | assertEquals("size x", NgffTests.NX, imp.getWidth());
163 | assertEquals("size y", NgffTests.NY, imp.getHeight());
164 |
165 | assertEquals("res x", NgffTests.RX, imp.getCalibration().pixelWidth, EPS);
166 | assertEquals("res y", NgffTests.RY, imp.getCalibration().pixelHeight, EPS);
167 |
168 | final char[] axes = dset.split("_")[0].toCharArray();
169 | if (ArrayUtils.contains(axes, NgffTests.Z)) {
170 | assertEquals("n slices", NgffTests.NZ, imp.getNSlices());
171 | assertEquals("res z", NgffTests.RZ, imp.getCalibration().pixelDepth, EPS);
172 | }
173 |
174 | if (ArrayUtils.contains(axes, NgffTests.C)) {
175 | assertEquals("n channels", NgffTests.NC, imp.getNChannels());
176 | }
177 |
178 | if (ArrayUtils.contains(axes, NgffTests.T)) {
179 | assertEquals("n timepoints", NgffTests.NT, imp.getNFrames());
180 | assertEquals("res t", NgffTests.RT, imp.getCalibration().frameInterval, EPS);
181 | }
182 |
183 | zarr.remove(dset);
184 | }
185 |
186 | public static void writePermutedAxesNgff(final N5Writer zarr, final String dsetPath) {
187 |
188 | NgffTests.writePermutedAxes(zarr, dsetPath,
189 | NgffTests.isCOrderFromName(dsetPath),
190 | NgffTests.permutationFromName(dsetPath));
191 | }
192 |
193 | public static void writePermutedAxesCosem(final N5Writer zarr, final String dsetPath) {
194 |
195 | writePermutedAxesCosem(zarr, dsetPath,
196 | NgffTests.isCOrderFromName(dsetPath),
197 | NgffTests.permutationFromName(dsetPath));
198 | }
199 |
200 | public static void writePermutedAxesCosem(final N5Writer zarr, final String dsetPath, final boolean cOrder, final int[] permutation) {
201 |
202 | final long[] dims = AxisUtils.permute(NgffTests.DEFAULT_DIMENSIONS, permutation);
203 | final int[] blkSize = Arrays.stream(dims).mapToInt(x -> (int)x).toArray();
204 |
205 | NgffTests.createDataset(zarr, cOrder, dsetPath, dims, blkSize, DataType.UINT8, new RawCompression());
206 |
207 | final DatasetAttributes dsetAttrs = zarr.getDatasetAttributes(dsetPath);
208 |
209 | final CosemTransform cosemTform = NgffTests.buildPermutedAxesCosemMetadata(permutation, true, dsetAttrs);
210 | zarr.setAttribute(dsetPath, "transform", cosemTform);
211 | }
212 |
213 | }
214 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/metadata/imagej/NgffToImagePlus.java:
--------------------------------------------------------------------------------
1 | package org.janelia.saalfeldlab.n5.metadata.imagej;
2 |
3 | import java.io.IOException;
4 | import java.util.Arrays;
5 |
6 | import org.janelia.saalfeldlab.n5.DatasetAttributes;
7 | import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis;
8 | import org.janelia.saalfeldlab.n5.universe.metadata.axes.Unit;
9 | import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.NgffSingleScaleAxesMetadata;
10 | import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMultiScaleMetadata;
11 | import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMultiScaleMetadata.OmeNgffDataset;
12 |
13 | import ij.ImagePlus;
14 |
15 | public class NgffToImagePlus extends SpatialMetadataToImagePlus {
16 |
17 | @Override
18 | public void writeMetadata(final NgffSingleScaleAxesMetadata t, final ImagePlus ip) throws IOException {
19 |
20 | ip.setTitle(t.getPath());
21 | int numChannels = 0;
22 | int numTimes = 0;
23 | int numZ = 0;
24 | int numSpace = 0;
25 |
26 | int xIdx = -1, yIdx = -1, cIdx = -1, zIdx = -1, tIdx = -1;
27 | for (int i = 0; i < t.getAxes().length; i++) {
28 |
29 | final Axis axis = t.getAxis(i);
30 | if (axis.getType().equals(Axis.TIME)) {
31 | numTimes = (int) t.getAttributes().getDimensions()[i];
32 | tIdx = i;
33 |
34 | final String timeUnit = axis.getUnit();
35 | if( timeUnit != null && !timeUnit.isEmpty())
36 | ip.getCalibration().setTimeUnit(axis.getUnit());
37 | }
38 |
39 | if (axis.getType().equals(Axis.CHANNEL)) {
40 | numChannels = (int) t.getAttributes().getDimensions()[i];
41 | cIdx = i;
42 | }
43 |
44 | if( axis.getType().equals(Axis.SPACE))
45 | {
46 | numSpace++;
47 |
48 | final String spaceUnit = axis.getUnit();
49 | if( spaceUnit != null && !spaceUnit.isEmpty())
50 | ip.getCalibration().setUnit(spaceUnit);
51 |
52 | if( numSpace == 1 )
53 | xIdx = i;
54 | else if ( numSpace == 2 )
55 | yIdx = i;
56 | else if ( numSpace == 3 )
57 | zIdx = i;
58 |
59 | if( numSpace > 2 )
60 | numZ = (int)t.getAttributes().getDimensions()[i];
61 | }
62 | }
63 |
64 |
65 | // permuting data if axes are in non-standard order
66 | // must happen before calling this method
67 |
68 | // setDimensions can't handle zeros, so set these to one if they're zero
69 | numChannels = numChannels == 0 ? 1 : numChannels;
70 | numZ = numZ == 0 ? 1 : numZ;
71 | numTimes = numTimes == 0 ? 1 : numTimes;
72 | if( xIdx >= 0 ) {
73 | ip.getCalibration().pixelWidth = t.getScale()[xIdx];
74 | ip.getCalibration().xOrigin = t.getTranslation()[xIdx];
75 | }
76 |
77 | if( yIdx >= 0 ) {
78 | ip.getCalibration().pixelHeight = t.getScale()[yIdx];
79 | ip.getCalibration().yOrigin = t.getTranslation()[yIdx];
80 | }
81 |
82 | if( zIdx >= 0 ) {
83 | ip.getCalibration().pixelDepth = t.getScale()[zIdx];
84 | ip.getCalibration().zOrigin = t.getTranslation()[zIdx];
85 | }
86 |
87 | if( tIdx > 0 )
88 | ip.getCalibration().frameInterval = t.getScale()[tIdx];
89 |
90 | }
91 |
92 | @Override
93 | public NgffSingleScaleAxesMetadata readMetadata(final ImagePlus ip) throws IOException {
94 |
95 | final int nc = ip.getNChannels();
96 | final int nz = ip.getNSlices();
97 | final int nt = ip.getNFrames();
98 |
99 | int N = 2;
100 | if (nz > 1)
101 | N++;
102 |
103 | if (nc > 1)
104 | N++;
105 |
106 | if (nt > 1)
107 | N++;
108 |
109 | final Axis[] axes = new Axis[N];
110 | final double[] scale = new double[N];
111 | final double[] offset = new double[N];
112 |
113 | final String spaceUnit = parseUnitWithWarning(ip.getCalibration().getUnit());
114 | axes[0] = new Axis(Axis.SPACE, "x", spaceUnit);
115 | scale[0] = ip.getCalibration().pixelWidth;
116 | offset[0] = ip.getCalibration().xOrigin;
117 |
118 | axes[1] = new Axis(Axis.SPACE, "y", spaceUnit);
119 | scale[1] = ip.getCalibration().pixelHeight;
120 | offset[1] = ip.getCalibration().yOrigin;
121 |
122 | int k = 2;
123 | // channels
124 | if (nc > 1) {
125 | axes[k] = new Axis(Axis.CHANNEL, "c", null);
126 | scale[k] = 1;
127 | offset[k] = 0;
128 | k++;
129 | }
130 |
131 | // space z
132 | if (nz > 1) {
133 | axes[k] = new Axis(Axis.SPACE, "z", spaceUnit);
134 | scale[k] = ip.getCalibration().pixelDepth;
135 | offset[k] = ip.getCalibration().zOrigin;
136 | k++;
137 | }
138 |
139 | // time
140 | if (nt > 1) {
141 | final String timeUnit = parseUnitWithWarning(ip.getCalibration().getTimeUnit());
142 | axes[k] = new Axis(Axis.TIME, "t", timeUnit);
143 | scale[k] = ip.getCalibration().frameInterval;
144 | if( scale[k] == 0.0 )
145 | scale[k] = 1.0;
146 |
147 | offset[k] = 0;
148 | k++;
149 | }
150 |
151 | final boolean noOffset = Arrays.stream(offset).allMatch( x -> x == 0.0 );
152 | if( noOffset )
153 | return new NgffSingleScaleAxesMetadata("", scale, null, axes, ImageplusMetadata.datasetAttributes(ip));
154 | else
155 | return new NgffSingleScaleAxesMetadata("", scale, offset, axes, ImageplusMetadata.datasetAttributes(ip));
156 | }
157 |
158 | private String parseUnitWithWarning(final String unitString) {
159 | final Unit unit = Unit.fromString(unitString);
160 | final String normalUnit;
161 | if( unit == null ) {
162 | System.err.println("WARNING: could not infer unit from (" + unitString +
163 | "). Will use it as the unit directly, but may be invalid.");
164 | normalUnit = unitString;
165 | }
166 | else {
167 | normalUnit = unit.toString();
168 | }
169 | return normalUnit;
170 | }
171 |
172 | public static OmeNgffMultiScaleMetadata buildMetadata(final ImagePlus image, final String path, final DatasetAttributes[] dsetAttrs,
173 | final OmeNgffDataset[] datasets) {
174 |
175 | final int nc = image.getNChannels();
176 | final int nz = image.getNSlices();
177 | final int nt = image.getNFrames();
178 | final String unit = image.getCalibration().getUnit();
179 |
180 | int N = 2;
181 | if (nc > 1) {
182 | N++;
183 | }
184 | if (nz > 1) {
185 | N++;
186 | }
187 | if (nt > 1) {
188 | N++;
189 | }
190 | final Axis[] axes = new Axis[N];
191 | final double[] pixelSpacing = new double[N];
192 |
193 | axes[0] = new Axis(Axis.SPACE, "x", unit);
194 | pixelSpacing[0] = image.getCalibration().pixelWidth;
195 |
196 | axes[1] = new Axis(Axis.SPACE, "y", unit);
197 | pixelSpacing[1] = image.getCalibration().pixelHeight;
198 |
199 | int d = 2;
200 | if (nc > 1) {
201 | axes[d] = new Axis(Axis.CHANNEL, "c", "");
202 | pixelSpacing[d] = 1.0;
203 | d++;
204 | }
205 |
206 | if (nz > 1) {
207 | axes[d] = new Axis(Axis.SPACE, "z", unit);
208 | pixelSpacing[d] = image.getCalibration().pixelDepth;
209 | d++;
210 | }
211 |
212 | if (nt > 1) {
213 | axes[d] = new Axis(Axis.TIME, "t", image.getCalibration().getTimeUnit());
214 | pixelSpacing[d] = image.getCalibration().frameInterval;
215 | d++;
216 | }
217 |
218 | // need to reverse the axes if the arrays are in C order
219 | final Axis[] axesToWrite;
220 | if( dsetAttrs != null )
221 | axesToWrite = OmeNgffMultiScaleMetadata.reverseIfCorder( dsetAttrs[0], axes );
222 | else
223 | axesToWrite = axes;
224 |
225 | final String name = image.getTitle();
226 | final String type = "sampling";
227 | final String version = "0.4";
228 |
229 | return new OmeNgffMultiScaleMetadata(
230 | N, path, name, type, version, axesToWrite,
231 | datasets, dsetAttrs,
232 | null, null); // no global coordinate transforms of downsampling metadata
233 | }
234 |
235 | public static OmeNgffMultiScaleMetadata buildMetadata(final NgffSingleScaleAxesMetadata meta, final String name, final String path, final DatasetAttributes[] dsetAttrs,
236 | final OmeNgffDataset[] datasets) {
237 |
238 | final int N = meta.getScale().length;
239 |
240 | // need to reverse the axes if the arrays are in C order
241 | final String type = "sampling";
242 | final String version = "0.4";
243 |
244 | return new OmeNgffMultiScaleMetadata(
245 | N, path, name, type, version, meta.getAxes(),
246 | datasets, dsetAttrs,
247 | null, null); // no global coordinate transforms of downsampling metadata
248 | }
249 |
250 | }
251 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | org.scijava
7 | pom-scijava
8 | 40.0.0
9 |
10 |
11 |
12 | org.janelia.saalfeldlab
13 | n5-ij
14 | 4.4.2-SNAPSHOT
15 |
16 | N5 ImageJ Bindings
17 | ImageJ convenience layer for N5
18 | https://github.com/saalfeldlab/n5-ij
19 | 2017
20 |
21 | Saalfeld Lab
22 | http://saalfeldlab.janelia.org/
23 |
24 |
25 |
26 | Simplified BSD License
27 | repo
28 |
29 |
30 |
31 |
32 |
33 | axtimwalde
34 | Stephan Saalfeld
35 | http://imagej.net/User:Saalfeld
36 |
37 | founder
38 | lead
39 | developer
40 | debugger
41 | reviewer
42 | support
43 | maintainer
44 |
45 |
46 |
47 |
48 |
49 | Igor Pisarev
50 | https://imagej.net/User:Pisarevi
51 | igorpisarev
52 |
53 |
54 |
55 |
56 |
57 | Image.sc Forum
58 | https://forum.image.sc/tag/n5
59 |
60 |
61 |
62 |
63 | scm:git:git://github.com/saalfeldlab/n5-ij
64 | scm:git:git@github.com:saalfeldlab/n5-ij
65 | HEAD
66 | https://github.com/saalfeldlab/n5-ij
67 |
68 |
69 | GitHub
70 | https://github.com/saalfeldlab/n5-ij/issues
71 |
72 |
73 | GitHub Actions
74 | https://github.com/saalfeldlab/n5-ij/actions
75 |
76 |
77 |
78 | org.janelia.saalfeldlab.n5.ij
79 |
80 | bsd_2
81 | N5 ImageJ
82 | Saalfeld Lab
83 | Stephan Saalfeld
84 |
85 |
86 | sign,deploy-to-scijava
87 |
88 | 1.0.0-preview.20191208
89 | 1.4.1
90 |
91 | 3.4.1
92 | 4.2.2
93 | 5.0.0
94 | 2.1.0
95 | 1.5.0
96 |
97 |
98 |
99 |
100 |
101 | org.janelia.saalfeldlab
102 | n5
103 |
104 |
105 | org.janelia.saalfeldlab
106 | n5-blosc
107 |
108 |
109 | org.janelia.saalfeldlab
110 | n5-imglib2
111 |
112 |
113 | org.janelia.saalfeldlab
114 | n5-hdf5
115 | test
116 |
117 |
118 | org.janelia.saalfeldlab
119 | n5-universe
120 |
121 |
122 | org.janelia.saalfeldlab
123 | n5-zarr
124 |
125 |
126 | org.janelia
127 | n5-zstandard
128 | ${n5-zstandard.version}
129 |
130 |
131 |
132 |
133 | net.imglib2
134 | imglib2
135 |
136 |
137 | net.imglib2
138 | imglib2-algorithm
139 |
140 |
141 | net.imglib2
142 | imglib2-cache
143 |
144 |
145 | net.imglib2
146 | imglib2-ij
147 |
148 |
149 | net.imglib2
150 | imglib2-label-multisets
151 |
152 |
153 | net.imglib2
154 | imglib2-realtransform
155 |
156 |
157 |
158 |
159 | net.imagej
160 | ij
161 |
162 |
163 | net.imagej
164 | imagej-common
165 |
166 |
167 |
168 |
169 | org.scijava
170 | scijava-common
171 |
172 |
173 |
174 |
175 | com.fasterxml.jackson.core
176 | jackson-databind
177 |
178 |
179 | com.fasterxml.jackson.core
180 | jackson-core
181 |
182 |
188 |
194 |
195 | com.google.code.gson
196 | gson
197 |
198 |
199 | com.formdev
200 | flatlaf
201 |
202 |
208 |
209 | commons-lang
210 | commons-lang
211 |
212 |
213 | info.picocli
214 | picocli
215 |
216 |
217 | org.apache.commons
218 | commons-lang3
219 | test
220 |
221 |
222 | net.thisptr
223 | jackson-jq
224 | ${jackson-jq.version}
225 |
226 |
227 | se.sawano.java
228 | alphanumeric-comparator
229 | ${alphanumeric-comparator.version}
230 |
231 |
232 |
233 |
234 |
235 | junit
236 | junit
237 | test
238 |
239 |
240 | org.codehaus.plexus
241 | plexus-utils
242 | test
243 |
244 |
245 | org.janelia.saalfeldlab
246 | n5-universe
247 | ${n5-universe.version}
248 | tests
249 | test
250 |
251 |
252 |
253 |
254 |
255 | scijava.public
256 | https://maven.scijava.org/content/groups/public
257 |
258 |
259 |
260 |
261 |
262 |
263 | org.apache.maven.plugins
264 | maven-surefire-plugin
265 |
266 | 1
267 | true
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
--------------------------------------------------------------------------------
/src/main/java/org/janelia/saalfeldlab/n5/ui/N5DatasetSelectorDialog.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018--2020, Saalfeld lab
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions are met:
7 | *
8 | * 1. Redistributions of source code must retain the above copyright notice,
9 | * this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright notice,
11 | * this list of conditions and the following disclaimer in the documentation
12 | * and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | * POSSIBILITY OF SUCH DAMAGE.
25 | */
26 | package org.janelia.saalfeldlab.n5.ui;
27 |
28 | import java.awt.BorderLayout;
29 | import java.awt.event.ActionEvent;
30 | import java.awt.event.ActionListener;
31 | import java.awt.event.WindowEvent;
32 | import java.io.File;
33 | import java.io.IOException;
34 | import java.util.ArrayList;
35 | import java.util.List;
36 | import java.util.function.BiPredicate;
37 |
38 | import javax.swing.JButton;
39 | import javax.swing.JFrame;
40 | import javax.swing.JPanel;
41 | import javax.swing.JScrollPane;
42 | import javax.swing.JTable;
43 | import javax.swing.JTree;
44 | import javax.swing.tree.DefaultMutableTreeNode;
45 | import javax.swing.tree.TreePath;
46 |
47 | import org.janelia.saalfeldlab.n5.N5Reader;
48 |
49 | @Deprecated
50 | public class N5DatasetSelectorDialog
51 | {
52 | public final N5Reader n5;
53 |
54 | public final String root;
55 |
56 | public BiPredicate< N5Reader, String > isMultiscale;
57 |
58 | public List< String > selectedDatasets;
59 |
60 | public List listenerList;
61 |
62 | public static final String sep = File.separator;
63 |
64 | public N5DatasetSelectorDialog( final N5Reader n5, final String root )
65 | {
66 | this.n5 = n5;
67 | this.root = root;
68 | listenerList = new ArrayList<>();
69 | }
70 |
71 | public N5DatasetSelectorDialog( final N5Reader n5 )
72 | {
73 | this( n5, "" );
74 | }
75 |
76 | public JFrame showAsTree()
77 | {
78 |
79 | DefaultMutableTreeNode root;
80 | try
81 | {
82 | root = datasetTree();
83 | }
84 | catch ( final IOException e )
85 | {
86 | e.printStackTrace();
87 | return null;
88 | }
89 |
90 | final JFrame frame = new JFrame( "Choose N5 datasets" );
91 | final JPanel panel = new JPanel( new BorderLayout() );
92 |
93 | final JTree tree = new JTree( root );
94 | final JScrollPane treeView = new JScrollPane( tree );
95 | panel.add( treeView, BorderLayout.CENTER );
96 |
97 | final JButton okButton = new JButton("OK");
98 | okButton.addActionListener( new ActionListener()
99 | {
100 | @Override
101 | public void actionPerformed( final ActionEvent event )
102 | {
103 | selectedDatasets = new ArrayList<>();
104 | final TreePath[] selectedPaths = tree.getSelectionPaths();
105 | for( final TreePath path : selectedPaths )
106 | {
107 | final StringBuffer pathString = new StringBuffer();
108 | for( final Object o : path.getPath())
109 | {
110 | pathString.append( "/");
111 | pathString.append( o.toString() );
112 | }
113 | selectedDatasets.add( pathString.toString() );
114 | }
115 |
116 | frame.setVisible( false );
117 | frame.dispatchEvent( new WindowEvent( frame, WindowEvent.WINDOW_CLOSING ));
118 | }
119 | });
120 |
121 | final JButton cancelButton = new JButton("Cancel");
122 | cancelButton.addActionListener( new ActionListener()
123 | {
124 | @Override
125 | public void actionPerformed( final ActionEvent event )
126 | {
127 | frame.setVisible( false );
128 | frame.dispatchEvent( new WindowEvent( frame, WindowEvent.WINDOW_CLOSING ));
129 | }
130 | });
131 |
132 | final JPanel buttonPanel = new JPanel();
133 | buttonPanel.add( okButton, BorderLayout.WEST );
134 | buttonPanel.add( cancelButton , BorderLayout.EAST );
135 | panel.add( buttonPanel, BorderLayout.SOUTH );
136 |
137 | frame.add( panel );
138 | frame.pack();
139 | frame.setVisible( true );
140 | return frame;
141 | }
142 |
143 | public List getSelectedDatasets()
144 | {
145 | return selectedDatasets;
146 | }
147 |
148 | public DefaultMutableTreeNode datasetTree() throws IOException
149 | {
150 | final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode( root );
151 | datasetTreeRecursive( rootNode, root, n5.list( root ) );
152 | return rootNode;
153 | }
154 |
155 | private void datasetTreeRecursive( final DefaultMutableTreeNode baseNode, final String path, final String... bases ) throws IOException
156 | {
157 | for( final String s : bases )
158 | {
159 | String fullPath;
160 | if( path.equals( sep ))
161 | fullPath = path + s;
162 | else
163 | fullPath = path + sep + s;
164 |
165 | if( n5.exists( fullPath ))
166 | {
167 | if( n5.datasetExists( fullPath ))
168 | {
169 | final DefaultMutableTreeNode childNode = new DefaultMutableTreeNode( s );
170 | baseNode.add( childNode );
171 | }
172 | else
173 | {
174 | String suffix = "";
175 | if( isMultiscale != null && isMultiscale.test( n5, fullPath ))
176 | {
177 | suffix = " (multiscale)";
178 | }
179 | final DefaultMutableTreeNode childNode = new DefaultMutableTreeNode( s + suffix );
180 |
181 | baseNode.add( childNode );
182 | final String[] children = n5.list( fullPath );
183 | datasetTreeRecursive( childNode, fullPath, children );
184 | }
185 | }
186 | }
187 | }
188 |
189 | public JFrame show()
190 | {
191 |
192 | List list;
193 | try
194 | {
195 | list = datasetList();
196 | }
197 | catch ( final IOException e )
198 | {
199 | e.printStackTrace();
200 | return null;
201 | }
202 |
203 | final JFrame frame = new JFrame( "Choose N5 datasets" );
204 | final JPanel panel = new JPanel( new BorderLayout() );
205 |
206 | final String[] columnNames = new String[]{ "datasets" };
207 | final String[][] data = new String[ list.size() ][];
208 | for( int i = 0; i < list.size(); i++ )
209 | {
210 | data[ i ] = new String[]{ list.get( i ) };
211 | }
212 |
213 | final JTable table = new JTable( data, columnNames );
214 | final JScrollPane treeView = new JScrollPane( table );
215 | panel.add( treeView, BorderLayout.CENTER );
216 |
217 | final JButton okButton = new JButton("OK");
218 | okButton.addActionListener( new ActionListener()
219 | {
220 | @Override
221 | public void actionPerformed( final ActionEvent event )
222 | {
223 | final int[] selected = table.getSelectedRows();
224 | if( selected.length < 1 )
225 | return;
226 | else
227 | {
228 | selectedDatasets = new ArrayList<>();
229 | for( final int i : selected )
230 | {
231 | selectedDatasets.add( data[ i ][ 0 ] );
232 | }
233 | }
234 |
235 | frame.setVisible( false );
236 | frame.dispatchEvent( new WindowEvent( frame, WindowEvent.WINDOW_CLOSING ));
237 | }
238 | });
239 |
240 | final JButton cancelButton = new JButton("Cancel");
241 | cancelButton.addActionListener( new ActionListener()
242 | {
243 | @Override
244 | public void actionPerformed( final ActionEvent event )
245 | {
246 | frame.setVisible( false );
247 | frame.dispatchEvent( new WindowEvent( frame, WindowEvent.WINDOW_CLOSING ));
248 | }
249 | });
250 |
251 | final JPanel buttonPanel = new JPanel();
252 | buttonPanel.add( okButton, BorderLayout.WEST );
253 | buttonPanel.add( cancelButton , BorderLayout.EAST );
254 | panel.add( buttonPanel, BorderLayout.SOUTH );
255 |
256 | frame.add( panel );
257 | frame.pack();
258 | frame.setVisible( true );
259 | return frame;
260 | }
261 |
262 | public List datasetList() throws IOException
263 | {
264 | final ArrayList list = new ArrayList<>();
265 | datasetListRecursive( list, root, n5.list( root ) );
266 | return list;
267 | }
268 |
269 | private void datasetListRecursive( final List< String > list, final String path, final String... bases ) throws IOException
270 | {
271 | for( final String s : bases )
272 | {
273 | String fullPath;
274 | if( path.equals( sep ))
275 | fullPath = path + s;
276 | else
277 | fullPath = path + sep + s;
278 |
279 | if( n5.exists( fullPath ))
280 | {
281 | if( n5.datasetExists( fullPath ))
282 | list.add( fullPath );
283 | else
284 | {
285 | if( isMultiscale != null && isMultiscale.test( n5, fullPath ))
286 | list.add( fullPath + " (multiscale)" );
287 |
288 | final String[] children = n5.list( fullPath );
289 | datasetListRecursive( list, fullPath, children );
290 | }
291 | }
292 | }
293 | }
294 |
295 | }
296 |
--------------------------------------------------------------------------------