diff --git a/pom.xml b/pom.xml index 994ccfcd24..c8b65480a5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 20.0.0 + 22.4.0 @@ -293,5 +293,10 @@ imglib2-ij test + + org.scijava + scijava-io-http + test + diff --git a/src/main/java/net/imagej/ops/filter/FilterNamespace.java b/src/main/java/net/imagej/ops/filter/FilterNamespace.java index 136eb763a1..527cec382b 100644 --- a/src/main/java/net/imagej/ops/filter/FilterNamespace.java +++ b/src/main/java/net/imagej/ops/filter/FilterNamespace.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -136,7 +136,7 @@ public , O extends RealType> O addPoissonNoise( /** * Executes a bilateral filter on the given arguments. - * + * * @param in * @param out * @param sigmaR @@ -757,7 +757,7 @@ RandomAccessibleInterval dog(final RandomAccessibleInterval in, /** * Executes the "Frangi Vesselness" filter operation on the given arguments. - * + * * @param in - input image * @param out - output image * @param spacing - n-dimensional array indicating the physical distance @@ -1271,6 +1271,20 @@ public > IterableInterval sigma( return result; } + // -- Shadows + + /** Executes the "shadows" filter operation on the given arguments. */ + @OpMethod(op = net.imagej.ops.filter.shadow.DefaultShadows.class) + public > RandomAccessibleInterval shadows( + final RandomAccessibleInterval in, final double angle) + { + @SuppressWarnings("unchecked") + final RandomAccessibleInterval result = + (RandomAccessibleInterval) ops().run(Ops.Filter.Shadows.class, in, + angle); + return result; + } + // -- Sobel @OpMethod(op = net.imagej.ops.filter.sobel.SobelRAI.class) diff --git a/src/main/java/net/imagej/ops/filter/shadow/DefaultShadows.java b/src/main/java/net/imagej/ops/filter/shadow/DefaultShadows.java new file mode 100644 index 0000000000..5decb25cce --- /dev/null +++ b/src/main/java/net/imagej/ops/filter/shadow/DefaultShadows.java @@ -0,0 +1,153 @@ + +package net.imagej.ops.filter.shadow; + +import net.imagej.Extents; +import net.imagej.Position; +import net.imagej.ops.Ops; +import net.imagej.ops.special.function.AbstractUnaryFunctionOp; +import net.imglib2.Cursor; +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.neighborhood.Neighborhood; +import net.imglib2.algorithm.neighborhood.RectangleNeighborhood; +import net.imglib2.algorithm.neighborhood.RectangleNeighborhoodFactory; +import net.imglib2.algorithm.neighborhood.RectangleShape.NeighborhoodsAccessible; +import net.imglib2.type.numeric.RealType; +import net.imglib2.util.Util; +import net.imglib2.view.Views; + +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +@Plugin(type = Ops.Filter.Shadows.class) +public class DefaultShadows> extends + AbstractUnaryFunctionOp, RandomAccessibleInterval> + implements Ops.Filter.Shadows +{ + + // cos(theta) and sin(theta) LUTs used for 3x3 neighborhood calculations where + // index 4 is the center of the neighborhood and theta increases around the + // edges of the neighborhood (note that theta does not increase as the index + // increase but instead increases as if this array was mapped to a 3x3 + // square, then theta increases counterclockwise). + static final double[] cos = { -0.7071067811865475, 0, 0.7071067811865476, + -1.0, 0, 1.0, -0.7071067811865477, 0, 0.7071067811865474 }; + static final double[] sin = { 0.7071067811865476, 1.0, 0.7071067811865475, 0, + 0.0, 0.0, -0.7071067811865475, -1.0, -0.7071067811865477 }; + static final double[] kernel = new double[9]; + double scale; + + @Parameter(min = "0", max = "2 * Math.PI") + private double theta; + + @Override + public RandomAccessibleInterval calculate( + final RandomAccessibleInterval input) + { + final RandomAccessibleInterval output = ops().copy().rai(input); + + final long[] planeDims = new long[input.numDimensions() - 2]; + for (int i = 0; i < planeDims.length; i++) + planeDims[i] = input.dimension(i + 2); + final Extents extents = new Extents(planeDims); + final Position planePos = extents.createPosition(); + if (planeDims.length == 0) { + computePlanar(planePos, input, output); + } + else { + while (planePos.hasNext()) { + planePos.fwd(); + computePlanar(planePos, input, output); + } + + } + + return output; + + } + + private void computePlanar(final Position planePos, + final RandomAccessibleInterval input, + final RandomAccessibleInterval output) + { + + // angle vector + final double cosTheta = Math.cos(theta); + final double sinTheta = Math.sin(theta); + + // kernel equal to unit vector of (x dot angle) + for (int i = 0; i < kernel.length; i++) { + kernel[i] = 2 * (cos[i] * cosTheta + sin[i] * sinTheta); + } + // N.B. the rules of the surrounding pixels do not apply to the center pixel + kernel[4] = 1; + + scale = 0; + for (final double d : kernel) + scale += d; + if (scale == 0) scale = 1; // TODO is this necessary? + + final T type = Util.getTypeFromInterval(input); + + final long[] imageDims = new long[input.numDimensions()]; + input.dimensions(imageDims); + + // create all objects needed for NeighborhoodsAccessible + RandomAccessibleInterval slicedInput = ops().copy().rai(input); + for (int i = planePos.numDimensions() - 1; i >= 0; i--) { + slicedInput = Views.hyperSlice(slicedInput, input.numDimensions() - 1 - i, + planePos.getLongPosition(i)); + } + + final RandomAccessible refactoredInput = Views.extendMirrorSingle( + slicedInput); + final RectangleNeighborhoodFactory factory = RectangleNeighborhood + .factory(); + final FinalInterval neighborhoodSpan = new FinalInterval(new long[] { -1, + -1 }, new long[] { 1, 1 }); + + final NeighborhoodsAccessible neighborhoods = + new NeighborhoodsAccessible<>(refactoredInput, neighborhoodSpan, factory); + + // create cursors and random accesses for loop. + final Cursor cursor = Views.iterable(input).localizingCursor(); + final RandomAccess outputRA = output.randomAccess(); + for (int i = 0; i < planePos.numDimensions(); i++) { + outputRA.setPosition(planePos.getLongPosition(i), i + 2); + } + final RandomAccess> neighborhoodsRA = neighborhoods + .randomAccess(); + + int algorithmIndex = 0; + double sum; + final double[] n = new double[9]; + while (cursor.hasNext()) { + cursor.fwd(); + neighborhoodsRA.setPosition(cursor); + final Neighborhood current = neighborhoodsRA.get(); + final Cursor neighborhoodCursor = current.cursor(); + + algorithmIndex = 0; + sum = 0; + while (algorithmIndex < n.length) { + neighborhoodCursor.fwd(); + n[algorithmIndex++] = neighborhoodCursor.get().getRealDouble(); + } + + for (int i = 0; i < kernel.length; i++) { + sum += kernel[i] * n[i]; + } + + double value = sum / scale; + + outputRA.setPosition(cursor.getLongPosition(0), 0); + outputRA.setPosition(cursor.getLongPosition(1), 1); + if (value > type.getMaxValue()) value = type.getMaxValue(); + if (value < type.getMinValue()) value = type.getMinValue(); + outputRA.get().setReal(value); + } + } + +} diff --git a/src/main/templates/net/imagej/ops/Ops.list b/src/main/templates/net/imagej/ops/Ops.list index e38e8c72f6..28e471a13e 100644 --- a/src/main/templates/net/imagej/ops/Ops.list +++ b/src/main/templates/net/imagej/ops/Ops.list @@ -122,6 +122,7 @@ namespaces = ``` [name: "paddingIntervalCentered", iface: "PaddingIntervalCentered"], [name: "paddingIntervalOrigin", iface: "PaddingIntervalOrigin"], [name: "tubeness", iface: "Tubeness"], + [name: "shadows", iface: "Shadows"], [name: "sigma", iface: "Sigma", aliases: ["sigmaFilter", "filterSigma"]], [name: "sobel", iface: "Sobel"], [name: "variance", iface: "Variance", aliases: ["varianceFilter", "filterVariance", "var", "varFilter", "filterVar"]], diff --git a/src/test/java/net/imagej/ops/filter/shadows/DefaultShadowsTest.java b/src/test/java/net/imagej/ops/filter/shadows/DefaultShadowsTest.java new file mode 100644 index 0000000000..9dff62b3d7 --- /dev/null +++ b/src/test/java/net/imagej/ops/filter/shadows/DefaultShadowsTest.java @@ -0,0 +1,121 @@ +/* + * #%L + * ImageJ software for multidimensional image processing and analysis. + * %% + * Copyright (C) 2014 - 2018 ImageJ developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package net.imagej.ops.filter.shadows; + +import io.scif.SCIFIOService; +import io.scif.services.DatasetIOService; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.ExecutionException; + +import net.imagej.Dataset; +import net.imagej.ops.AbstractOpTest; +import net.imagej.ops.OpService; +import net.imagej.ops.filter.shadow.DefaultShadows; +import net.imglib2.img.Img; +import net.imglib2.type.numeric.real.FloatType; + +import org.junit.Before; +import org.junit.Test; +import org.scijava.Context; +import org.scijava.app.StatusService; +import org.scijava.download.DiskLocationCache; +import org.scijava.download.Download; +import org.scijava.download.DownloadService; +import org.scijava.io.IOService; +import org.scijava.io.http.HTTPLocation; +import org.scijava.io.location.FileLocation; + +/** + * Tests for {@link DefaultShadows} + * + * @author Gabe Selzer + */ +public class DefaultShadowsTest extends AbstractOpTest { + + private Dataset input; + + @Override + protected Context createContext() { + return new Context(OpService.class, IOService.class, DownloadService.class, + SCIFIOService.class, StatusService.class); + } + + @Before + public void init() throws IOException, InterruptedException, + ExecutionException, URISyntaxException + { + // URL of test image + final URL imageSrc = new URL("http://imagej.net/images/abe.tif"); + + // FileLocation that we want to store this image in. + final File destFile = File.createTempFile(getClass().getName(), ".tif"); + final FileLocation inputDest = new FileLocation(destFile); + + downloadOnce(imageSrc.toURI(), inputDest); + + final String srcPath = inputDest.getFile().getAbsolutePath(); + final DatasetIOService dio = context.service(DatasetIOService.class); + input = dio.open(srcPath); + } + + // move to DownloadService + public void downloadOnce(final URI srcURI, final FileLocation inputDest) + throws URISyntaxException, InterruptedException, ExecutionException + { + final DownloadService ds = context.service(DownloadService.class); + final DiskLocationCache cache = new DiskLocationCache(); + cache.getBaseDirectory().mkdirs(); + + final HTTPLocation inputSrc = new HTTPLocation(srcURI); + + // download the data from the URI to inputDest. Once we are done the method + // calling downloadOnce can access the File from the FileLocation passed + // through. + final Download download = ds.download(inputSrc, inputDest, cache); + download.task().waitFor(); + } + + @Test + public void testRegression() throws IOException { + final IOService io = context.getService(IOService.class); + final double north = Math.PI / 2; + final Img actualOutput = (Img) ops.run( + "filter.shadows", input, north); + final Img expectedOutput = (Img) io.open( + "src/test/resources/net/imagej/ops/filter/shadows/ExpectedShadowsNorth.tif"); + assertIterationsEqual(actualOutput, expectedOutput); + } + +} diff --git a/src/test/resources/net/imagej/ops/filter/shadows/ExpectedShadowsNorth.tif b/src/test/resources/net/imagej/ops/filter/shadows/ExpectedShadowsNorth.tif new file mode 100644 index 0000000000..6b8aa226f5 Binary files /dev/null and b/src/test/resources/net/imagej/ops/filter/shadows/ExpectedShadowsNorth.tif differ