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