Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shadows Op #561

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.scijava</groupId>
<artifactId>pom-scijava</artifactId>
<version>20.0.0</version>
<version>22.4.0</version>
<relativePath />
</parent>

Expand Down Expand Up @@ -293,5 +293,10 @@
<artifactId>imglib2-ij</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scijava</groupId>
<artifactId>scijava-io-http</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
22 changes: 18 additions & 4 deletions src/main/java/net/imagej/ops/filter/FilterNamespace.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -136,7 +136,7 @@ public <I extends RealType<I>, O extends RealType<O>> O addPoissonNoise(

/**
* Executes a bilateral filter on the given arguments.
*
*
* @param in
* @param out
* @param sigmaR
Expand Down Expand Up @@ -757,7 +757,7 @@ RandomAccessibleInterval<V> dog(final RandomAccessibleInterval<T> 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
Expand Down Expand Up @@ -1271,6 +1271,20 @@ public <T extends RealType<T>> IterableInterval<T> sigma(
return result;
}

// -- Shadows

/** Executes the "shadows" filter operation on the given arguments. */
@OpMethod(op = net.imagej.ops.filter.shadow.DefaultShadows.class)
public <T extends RealType<T>> RandomAccessibleInterval<T> shadows(
final RandomAccessibleInterval<T> in, final double angle)
{
@SuppressWarnings("unchecked")
final RandomAccessibleInterval<T> result =
(RandomAccessibleInterval<T>) ops().run(Ops.Filter.Shadows.class, in,
angle);
return result;
}

// -- Sobel

@OpMethod(op = net.imagej.ops.filter.sobel.SobelRAI.class)
Expand Down
153 changes: 153 additions & 0 deletions src/main/java/net/imagej/ops/filter/shadow/DefaultShadows.java
Original file line number Diff line number Diff line change
@@ -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<T extends RealType<T>> extends
AbstractUnaryFunctionOp<RandomAccessibleInterval<T>, RandomAccessibleInterval<T>>
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<T> calculate(
final RandomAccessibleInterval<T> input)
{
final RandomAccessibleInterval<T> 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<T> input,
final RandomAccessibleInterval<T> 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<T> 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<T> refactoredInput = Views.extendMirrorSingle(
slicedInput);
final RectangleNeighborhoodFactory<T> factory = RectangleNeighborhood
.factory();
final FinalInterval neighborhoodSpan = new FinalInterval(new long[] { -1,
-1 }, new long[] { 1, 1 });

final NeighborhoodsAccessible<T> neighborhoods =
new NeighborhoodsAccessible<>(refactoredInput, neighborhoodSpan, factory);

// create cursors and random accesses for loop.
final Cursor<T> cursor = Views.iterable(input).localizingCursor();
final RandomAccess<T> outputRA = output.randomAccess();
for (int i = 0; i < planePos.numDimensions(); i++) {
outputRA.setPosition(planePos.getLongPosition(i), i + 2);
}
final RandomAccess<Neighborhood<T>> neighborhoodsRA = neighborhoods
.randomAccess();

int algorithmIndex = 0;
double sum;
final double[] n = new double[9];
while (cursor.hasNext()) {
cursor.fwd();
neighborhoodsRA.setPosition(cursor);
final Neighborhood<T> current = neighborhoodsRA.get();
final Cursor<T> 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);
}
}

}
1 change: 1 addition & 0 deletions src/main/templates/net/imagej/ops/Ops.list
Original file line number Diff line number Diff line change
Expand Up @@ -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"]],
Expand Down
121 changes: 121 additions & 0 deletions src/test/java/net/imagej/ops/filter/shadows/DefaultShadowsTest.java
Original file line number Diff line number Diff line change
@@ -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<FloatType> actualOutput = (Img<FloatType>) ops.run(
"filter.shadows", input, north);
final Img<FloatType> expectedOutput = (Img<FloatType>) io.open(
"src/test/resources/net/imagej/ops/filter/shadows/ExpectedShadowsNorth.tif");
assertIterationsEqual(actualOutput, expectedOutput);
}

}
Binary file not shown.