diff --git a/src/main/java/net/imagej/ops/OpEnvironment.java b/src/main/java/net/imagej/ops/OpEnvironment.java index 39090528a8..b2d603f6a2 100644 --- a/src/main/java/net/imagej/ops/OpEnvironment.java +++ b/src/main/java/net/imagej/ops/OpEnvironment.java @@ -67,10 +67,15 @@ import net.imagej.ops.thread.ThreadNamespace; import net.imagej.ops.threshold.ThresholdNamespace; import net.imagej.ops.transform.TransformNamespace; +import net.imglib2.Interval; import net.imglib2.IterableInterval; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.neighborhood.RectangleShape; import net.imglib2.algorithm.neighborhood.Shape; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.outofbounds.OutOfBoundsFactory; +import net.imglib2.type.NativeType; import net.imglib2.type.Type; import org.scijava.Contextual; @@ -723,6 +728,19 @@ default IterableInterval map( return result; } + /** Executes the "map" operation on the given arguments. */ + @OpMethod(op = net.imagej.ops.map.neighborhood.DefaultMapNeighborhood.class) + default IterableInterval map(final IterableInterval out, + final RandomAccessibleInterval in, final Shape shape, + final UnaryComputerOp, EO> op, + final OutOfBoundsFactory> oobFactory) + { + @SuppressWarnings("unchecked") + final IterableInterval result = (IterableInterval) run( + net.imagej.ops.Ops.Map.class, out, in, op, shape, oobFactory); + return result; + } + /** Executes the "map" operation on the given arguments. */ @OpMethod( op = net.imagej.ops.map.neighborhood.MapNeighborhoodWithCenter.class) @@ -737,6 +755,76 @@ default IterableInterval map( return result; } + /** Executes the "map" operation on the given arguments. */ + @OpMethod( + op = net.imagej.ops.map.neighborhood.MapNeighborhoodWithCenter.class) + default IterableInterval map(final IterableInterval out, + final RandomAccessibleInterval in, final Shape shape, + final CenterAwareComputerOp func, + final OutOfBoundsFactory> oobFactory) + { + @SuppressWarnings("unchecked") + final IterableInterval result = (IterableInterval) run( + net.imagej.ops.Ops.Map.class, out, in, func, shape, oobFactory); + return result; + } + + /** Executes the "map" operation on the given arguments. */ + @OpMethod( + op = net.imagej.ops.map.neighborhood.array.MapNeighborhoodWithCenterNativeType.class) + default , O extends NativeType> ArrayImg map( + final ArrayImg out, final ArrayImg in, final RectangleShape shape, + final CenterAwareComputerOp op) + { + @SuppressWarnings("unchecked") + final ArrayImg result = (ArrayImg) run( + net.imagej.ops.Ops.Map.class, out, in, shape, op); + return result; + } + + /** Executes the "map" operation on the given arguments. */ + @OpMethod(ops = { + net.imagej.ops.map.neighborhood.array.MapNeighborhoodNativeType.class, + net.imagej.ops.map.neighborhood.array.MapNeighborhoodNativeTypeExtended.class }) + default , O extends NativeType> ArrayImg map( + final ArrayImg out, final ArrayImg in, + final RectangleShape shape, final UnaryComputerOp, O> op) + { + @SuppressWarnings("unchecked") + final ArrayImg result = (ArrayImg) run( + net.imagej.ops.Ops.Map.class, out, in, shape, op); + return result; + } + + /** Executes the "map" operation on the given arguments. */ + @OpMethod( + op = net.imagej.ops.map.neighborhood.array.MapNeighborhoodNativeType.class) + default , O extends NativeType> ArrayImg map( + final ArrayImg out, final ArrayImg in, + final RectangleShape shape, final UnaryComputerOp, O> op, + final Interval interval) + { + @SuppressWarnings("unchecked") + final ArrayImg result = (ArrayImg) run( + net.imagej.ops.Ops.Map.class, out, in, shape, op, interval); + return result; + } + + /** Executes the "map" operation on the given arguments. */ + @OpMethod( + op = net.imagej.ops.map.neighborhood.array.MapNeighborhoodNativeTypeExtended.class) + default + , O extends NativeType> ArrayImg map( + final ArrayImg out, final ArrayImg in, final RectangleShape shape, + final UnaryComputerOp, O> op, + final OutOfBoundsFactory oobFactory) + { + @SuppressWarnings("unchecked") + final ArrayImg result = (ArrayImg) run( + net.imagej.ops.Ops.Map.class, out, in, op, shape, oobFactory); + return result; + } + /** Executes the "map" operation on the given arguments. */ @OpMethod(op = net.imagej.ops.map.MapIterableToIterable.class) default Iterable map(final Iterable out, diff --git a/src/main/java/net/imagej/ops/filter/AbstractCenterAwareNeighborhoodBasedFilter.java b/src/main/java/net/imagej/ops/filter/AbstractCenterAwareNeighborhoodBasedFilter.java index ed4f3ce923..3c07a0054e 100644 --- a/src/main/java/net/imagej/ops/filter/AbstractCenterAwareNeighborhoodBasedFilter.java +++ b/src/main/java/net/imagej/ops/filter/AbstractCenterAwareNeighborhoodBasedFilter.java @@ -30,11 +30,8 @@ package net.imagej.ops.filter; -import org.scijava.plugin.Parameter; - import net.imagej.ops.Ops.Map; import net.imagej.ops.map.neighborhood.CenterAwareComputerOp; -import net.imagej.ops.special.chain.RAIs; import net.imagej.ops.special.computer.AbstractUnaryComputerOp; import net.imagej.ops.special.computer.Computers; import net.imagej.ops.special.computer.UnaryComputerOp; @@ -44,6 +41,8 @@ import net.imglib2.outofbounds.OutOfBoundsBorderFactory; import net.imglib2.outofbounds.OutOfBoundsFactory; +import org.scijava.plugin.Parameter; + public abstract class AbstractCenterAwareNeighborhoodBasedFilter extends AbstractUnaryComputerOp, IterableInterval> { @@ -62,7 +61,8 @@ public abstract class AbstractCenterAwareNeighborhoodBasedFilter extends @Override public void initialize() { filterOp = unaryComputer(out().firstElement()); - map = Computers.unary(ops(), Map.class, out(), in(), shape, filterOp); + map = Computers.unary(ops(), Map.class, out(), in(), shape, filterOp, + outOfBoundsFactory); } @Override @@ -70,7 +70,7 @@ public void compute1(RandomAccessibleInterval input, IterableInterval output) { // map computer to neighborhoods - map.compute1(RAIs.extend(input, outOfBoundsFactory), output); + map.compute1(input, output); } /** diff --git a/src/main/java/net/imagej/ops/filter/AbstractNeighborhoodBasedFilter.java b/src/main/java/net/imagej/ops/filter/AbstractNeighborhoodBasedFilter.java index 9a64da21be..51b1b0c407 100644 --- a/src/main/java/net/imagej/ops/filter/AbstractNeighborhoodBasedFilter.java +++ b/src/main/java/net/imagej/ops/filter/AbstractNeighborhoodBasedFilter.java @@ -30,8 +30,6 @@ package net.imagej.ops.filter; -import org.scijava.plugin.Parameter; - import net.imagej.ops.Ops.Map; import net.imagej.ops.special.computer.AbstractUnaryComputerOp; import net.imagej.ops.special.computer.Computers; @@ -41,7 +39,8 @@ import net.imglib2.algorithm.neighborhood.Shape; import net.imglib2.outofbounds.OutOfBoundsBorderFactory; import net.imglib2.outofbounds.OutOfBoundsFactory; -import net.imglib2.view.Views; + +import org.scijava.plugin.Parameter; public abstract class AbstractNeighborhoodBasedFilter extends AbstractUnaryComputerOp, IterableInterval> @@ -61,15 +60,15 @@ public abstract class AbstractNeighborhoodBasedFilter extends @Override public void initialize() { filterOp = unaryComputer(out().firstElement()); - map = Computers.unary(ops(), Map.class, out(), in(), shape, filterOp); + map = Computers.unary(ops(), Map.class, out(), in(), shape, filterOp, + outOfBoundsFactory); } @Override public void compute1(RandomAccessibleInterval input, IterableInterval output) { - map.compute1(Views.interval(Views.extend(input, outOfBoundsFactory), input), - output); + map.compute1(input, output); } /** diff --git a/src/main/java/net/imagej/ops/map/neighborhood/AbstractMapNeighborhood.java b/src/main/java/net/imagej/ops/map/neighborhood/AbstractMapNeighborhood.java index 9edaf9f051..3a05aced55 100644 --- a/src/main/java/net/imagej/ops/map/neighborhood/AbstractMapNeighborhood.java +++ b/src/main/java/net/imagej/ops/map/neighborhood/AbstractMapNeighborhood.java @@ -46,9 +46,9 @@ * @param producer of outputs * @param type of {@link Op} which processes each element */ -public abstract class AbstractMapNeighborhood - extends AbstractBinaryComputerOp implements - MapNeighborhood +public abstract class AbstractMapNeighborhood + extends AbstractBinaryComputerOp implements + MapNeighborhood { @Parameter diff --git a/src/main/java/net/imagej/ops/map/neighborhood/DefaultMapNeighborhood.java b/src/main/java/net/imagej/ops/map/neighborhood/DefaultMapNeighborhood.java index aea4155707..956f278911 100644 --- a/src/main/java/net/imagej/ops/map/neighborhood/DefaultMapNeighborhood.java +++ b/src/main/java/net/imagej/ops/map/neighborhood/DefaultMapNeighborhood.java @@ -37,8 +37,11 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.algorithm.neighborhood.Neighborhood; import net.imglib2.algorithm.neighborhood.Shape; +import net.imglib2.outofbounds.OutOfBoundsFactory; +import net.imglib2.view.Views; import org.scijava.Priority; +import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; /** @@ -52,9 +55,12 @@ */ @Plugin(type = Ops.Map.class, priority = Priority.LOW_PRIORITY) public class DefaultMapNeighborhood extends - AbstractMapNeighborhood, IterableInterval, UnaryComputerOp, O>> + AbstractMapNeighborhood, IterableInterval, Shape, UnaryComputerOp, O>> { + @Parameter(required = false) + private OutOfBoundsFactory> oobFactory; + private UnaryComputerOp>, IterableInterval> map; @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -69,7 +75,9 @@ IterableInterval.class, in1() == null ? IterableInterval.class : in2() public void compute2(final RandomAccessibleInterval in1, final Shape in2, final IterableInterval out) { - map.compute1(in2.neighborhoodsSafe(in1), out); + final RandomAccessibleInterval extended = oobFactory == null ? in1 + : Views.interval(Views.extend(in1, oobFactory), in1); + map.compute1(in2.neighborhoodsSafe(extended), out); } } diff --git a/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhood.java b/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhood.java index 8aa96e8bc3..cf57cd5ff7 100644 --- a/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhood.java +++ b/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhood.java @@ -43,8 +43,8 @@ * @param element type of outputs * @param type of {@link Op} which processes each neighborhood */ -public interface MapNeighborhood extends - BinaryComputerOp, MapOp +public interface MapNeighborhood + extends BinaryComputerOp, MapOp { // NB: Marker interface. } diff --git a/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhoodWithCenter.java b/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhoodWithCenter.java index 34af8e5977..f5a8394175 100644 --- a/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhoodWithCenter.java +++ b/src/main/java/net/imagej/ops/map/neighborhood/MapNeighborhoodWithCenter.java @@ -39,8 +39,11 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.algorithm.neighborhood.Neighborhood; import net.imglib2.algorithm.neighborhood.Shape; +import net.imglib2.outofbounds.OutOfBoundsFactory; +import net.imglib2.view.Views; import org.scijava.Priority; +import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; /** @@ -55,11 +58,14 @@ * CenterAwareComputerOp) * @see CenterAwareComputerOp */ -@Plugin(type = Ops.Map.class, priority = Priority.LOW_PRIORITY + 1) +@Plugin(type = Ops.Map.class, priority = Priority.LOW_PRIORITY + 21) public class MapNeighborhoodWithCenter extends - AbstractMapNeighborhood, IterableInterval, CenterAwareComputerOp> + AbstractMapNeighborhood, IterableInterval, Shape, CenterAwareComputerOp> { + @Parameter(required = false) + private OutOfBoundsFactory> oobFactory; + private BinaryComputerOp>, RandomAccessibleInterval, IterableInterval> map; @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -73,7 +79,9 @@ public void initialize() { public void compute2(final RandomAccessibleInterval in1, final Shape in2, final IterableInterval out) { - map.compute2(in2.neighborhoodsSafe(in1), in1, out); + final RandomAccessibleInterval extended = oobFactory == null ? in1 + : Views.interval(Views.extend(in1, oobFactory), in1); + map.compute2(in2.neighborhoodsSafe(extended), in1, out); } } diff --git a/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodNativeType.java b/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodNativeType.java new file mode 100644 index 0000000000..9bccf72562 --- /dev/null +++ b/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodNativeType.java @@ -0,0 +1,133 @@ + +package net.imagej.ops.map.neighborhood.array; + +import net.imagej.ops.Contingent; +import net.imagej.ops.Op; +import net.imagej.ops.Ops; +import net.imagej.ops.map.neighborhood.AbstractMapNeighborhood; +import net.imagej.ops.special.computer.UnaryComputerOp; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.algorithm.neighborhood.RectangleShape; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.type.NativeType; + +import org.scijava.Priority; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +/** + * Optimized neighborhood map implementation for 1D/2D/3D {@link Img}. This + * implementation uses access to the underlying types, which bypasses + * OutOfBounds checks, though. This means that pixels which are out of bounds + * are not considered as belonging to the neighborhood of a pixel. This can + * change results of averages over a neighborhood in comparison to using an out + * of bounds strategy which "creates" pixels in the neighborhood where there are + * none after the bounds of the image. + * + * @author Jonathan Hale + * @param Input {@link NativeType} + * @param Ouput {@link NativeType} + * @see MapNeighborhoodWithCenterNativeType + */ +@Plugin(type = Op.class, name = Ops.Map.NAME, priority = Priority.LOW_PRIORITY + + 10) +public class MapNeighborhoodNativeType, O extends NativeType> + extends + AbstractMapNeighborhood, O, ArrayImg, ArrayImg, RectangleShape, UnaryComputerOp, O>> + implements Contingent +{ + + /** + * Interval for input and output images to constrain interation to. + */ + @Parameter(required = false) + private Interval interval; + + @Override + public void compute2(final ArrayImg input, final RectangleShape shape, + final ArrayImg output) + { + final I in = input.firstElement(); + final O out = output.firstElement(); + + final int width = (int) input.dimension(0); + final int height = (input.numDimensions() > 1) ? (int) input.dimension(1) + : 1; + final int depth = (input.numDimensions() > 2) ? (int) input.dimension(2) + : 1; + + final Interval i = (interval == null) ? input : interval; + + final int minX = (int) i.min(0); + final int minY = (i.numDimensions() > 1) ? (int) i.min(1) : 0; + final int minZ = (i.numDimensions() > 2) ? (int) i.min(2) : 0; + + final int maxX = (int) i.max(0); + final int maxY = (i.numDimensions() > 1) ? (int) i.max(1) : 0; + final int maxZ = (i.numDimensions() > 2) ? (int) i.max(2) : 0; + + final int skipX = width - (maxX - minX + 1); + final int skipY = width * (height - (maxY - minY + 1)); + + final UnaryComputerOp, O> op = getOp(); + + int index = minX + minY * width + minZ * height * width; + in.updateIndex(index); + out.updateIndex(index); + + for (int z = minZ; z <= maxZ; ++z) { + for (int y = minY; y <= maxY; ++y) { + for (int x = minX; x <= maxX; ++x) { + // save the current index, since it will be changed by the + // NeighborhoodIterable. Increment to save doing that later. + index = in.getIndex() + 1; + + final Iterable neighborhood = new NeighborhoodIterableNativeType<>( + in, x, y, z, width, height, depth, shape.getSpan()); + + op.compute1(neighborhood, out); + + in.updateIndex(index); + out.incIndex(); + } + in.incIndex(skipX); + out.incIndex(skipX); + } + in.incIndex(skipY); + out.incIndex(skipY); + } + } + + /** + * @return The interval which constraints iteration or null, if + * none is set. + */ + public Interval getInterval() { + return interval; + } + + /** + * Set the interval which constraints iteration. + */ + public void setInterval(Interval i) { + interval = i; + } + + /** + * Convenience method to set the interval which constraints iteration. + * + * @see #setInterval(Interval) + */ + public void setInterval(long[] min, long[] max) { + interval = new FinalInterval(min, max); + } + + @Override + public boolean conforms() { + return in().numDimensions() > 0 && in().numDimensions() <= 3 && !in2() + .isSkippingCenter(); + } + +} diff --git a/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodNativeTypeExtended.java b/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodNativeTypeExtended.java new file mode 100644 index 0000000000..0fa97fdb59 --- /dev/null +++ b/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodNativeTypeExtended.java @@ -0,0 +1,228 @@ + +package net.imagej.ops.map.neighborhood.array; + +import java.util.ArrayList; +import java.util.concurrent.Future; + +import net.imagej.ops.Contingent; +import net.imagej.ops.OpService; +import net.imagej.ops.Ops; +import net.imagej.ops.map.neighborhood.AbstractMapNeighborhood; +import net.imagej.ops.map.neighborhood.MapNeighborhood; +import net.imagej.ops.special.computer.UnaryComputerOp; +import net.imglib2.FinalInterval; +import net.imglib2.algorithm.neighborhood.RectangleShape; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.outofbounds.OutOfBoundsFactory; +import net.imglib2.type.NativeType; +import net.imglib2.view.Views; + +import org.scijava.Cancelable; +import org.scijava.Priority; +import org.scijava.log.LogService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.thread.ThreadService; + +/** + * Optimized implementation of MapNeighborhood which uses + * {@link MapNeighborhoodNativeType} for a center interval which does not + * require out of bounds checks and {@link MapNeighborhood} for external + * intervals. + * + * @author Jonathan Hale + * @param Input {@link NativeType} + * @param Ouput {@link NativeType} + * @see MapNeighborhoodWithCenterNativeType + * @see MapNeighborhood + */ +@Plugin(type = net.imagej.ops.Op.class, name = Ops.Map.NAME, + priority = Priority.LOW_PRIORITY + 11) +public class MapNeighborhoodNativeTypeExtended, O extends NativeType, Op extends UnaryComputerOp> + extends + AbstractMapNeighborhood, O, ArrayImg, ArrayImg, RectangleShape, UnaryComputerOp, O>> + implements Contingent, Cancelable +{ + + @Parameter + protected ThreadService threadService; + + @Parameter + protected OpService ops; + + @Parameter + protected LogService log; + + @Parameter(required = false) + private OutOfBoundsFactory> oobFactory; + + @Override + public void compute2(final ArrayImg input, final RectangleShape shape, + final ArrayImg output) + { + // all dimensions are equal as ensured in conforms() */ + final int span = shape.getSpan(); + + final int numDimensions = input.numDimensions(); + + // calculate intervals + FinalInterval[] intervals; + FinalInterval center; + final long dim0 = input.dimension(0); + final long max0 = input.max(0); + final long maxSafe0 = max0 - span; // maximal extension of horizontally + // centered intervals + final long spanPlus1 = span + 1; + /* Note about ordering of intervals: + * Since done parallel, the idea is to queue work by decreasing size. + * The center interval is usually the biggest, but can be best optimized. + * Following: front/back, top/bottom, left/right. (Depending on dimensions, + * some might not be available.) + * + * The intervals were chosen intentionally to optimize access to storage, + * which is expected to be in order of dimensions (0 first, then 1, ...). + * + * The order for queuing the tasks is: Unoptimized intervals in order of size, + * then optimized center interval + */ + /* build intervals */ + if (numDimensions == 1) { + intervals = new FinalInterval[] { + /* left */ + new FinalInterval(new long[] { 0 }, new long[] { span }), + /* right */ + new FinalInterval(new long[] { dim0 - span }, new long[] { max0 }) }; + /* center */ + center = new FinalInterval(new long[] { spanPlus1 }, new long[] { + maxSafe0 }); + } + else if (numDimensions == 2) { + final long dim1 = input.dimension(1); + final long max1 = input.max(1); + final long maxSafe1 = max1 - span; // maximal extension of vertically + // centered intervals + + intervals = new FinalInterval[] { + /* top */ + new FinalInterval(new long[] { 0, 0 }, new long[] { max0, span }), + /* bottom */ + new FinalInterval(new long[] { 0, dim1 - span }, new long[] { max0, + max1 }), + /* left */ + new FinalInterval(new long[] { 0, spanPlus1 }, new long[] { span, max1 - + span }), + /* right */ + new FinalInterval(new long[] { dim0 - span, spanPlus1 }, new long[] { + max0, maxSafe1 }) }; + /* center */ + center = new FinalInterval(new long[] { spanPlus1, spanPlus1 }, + new long[] { maxSafe0, maxSafe1 }); + } + else { + // numDimensions == 3, guaranteed by conforms() + + final long dim1 = input.dimension(1); + final long dim2 = input.dimension(2); + final long max1 = input.max(1); + final long max2 = input.max(2); + final long maxSafe1 = max1 - span; // maximal extension of vertically + // centered intervals + final long maxSafe2 = max2 - span; // maximal extension of depth + // centered intervals + intervals = new FinalInterval[] { + /* front */ + new FinalInterval(new long[] { 0, 0, 0 }, new long[] { max0, max1, + span }), + /* back */ + new FinalInterval(new long[] { 0, 0, dim2 - span }, new long[] { max0, + max1, max2 }), + /* top */ + new FinalInterval(new long[] { 0, 0, spanPlus1 }, new long[] { max0, + span, maxSafe2 }), + /* bottom */ + new FinalInterval(new long[] { 0, dim1 - span, spanPlus1 }, new long[] { + max0, max1, maxSafe2 }), + /* left */ + new FinalInterval(new long[] { 0, spanPlus1, spanPlus1 }, new long[] { + span, maxSafe1, maxSafe2 }), + /* right */ + new FinalInterval(new long[] { dim0 - span, spanPlus1, spanPlus1 }, + new long[] { max0, maxSafe1, maxSafe2 }) }; + /* center */ + center = new FinalInterval(new long[] { spanPlus1, spanPlus1, spanPlus1 }, + new long[] { maxSafe0, maxSafe1, maxSafe2 }); + } + + ArrayList> futures = new ArrayList<>(2 * numDimensions + 1); + + for (final FinalInterval interval : intervals) { + futures.add(threadService.run(new Runnable() { + + @Override + public void run() { + if (oobFactory == null) { + ops.run(MapNeighborhood.class, Views.interval(output, interval), + Views.interval(input, interval), shape, getOp()); + } + else { + ops.run(MapNeighborhood.class, Views.interval(output, interval), + Views.interval(input, interval), shape, getOp(), oobFactory); + } + } + })); + + } + + final FinalInterval finalCenter = center; + futures.add(threadService.run(new Runnable() { + + @Override + public void run() { + ops.run(MapNeighborhoodNativeType.class, output, input, shape, getOp(), + finalCenter); + } + })); + + // wait for tasks to complete + for (final Future future : futures) { + try { + if (isCanceled()) { + break; + } + future.get(); + } + catch (final Exception e) { + log.error(e); + cancel(e.getMessage()); + break; + } + } + } + + @Override + public boolean conforms() { + return in().numDimensions() > 0 && in().numDimensions() <= 3 && // + !in2().isSkippingCenter(); + } + + // --- Cancelable methods --- + + private String cancelReason = null; + private boolean canceled; + + @Override + public boolean isCanceled() { + return canceled; + } + + @Override + public void cancel(String reason) { + cancelReason = reason; + canceled = true; + } + + @Override + public String getCancelReason() { + return cancelReason; + } +} diff --git a/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodWithCenterNativeType.java b/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodWithCenterNativeType.java new file mode 100644 index 0000000000..ab8d6024b4 --- /dev/null +++ b/src/main/java/net/imagej/ops/map/neighborhood/array/MapNeighborhoodWithCenterNativeType.java @@ -0,0 +1,83 @@ + +package net.imagej.ops.map.neighborhood.array; + +import net.imagej.ops.Contingent; +import net.imagej.ops.Op; +import net.imagej.ops.Ops; +import net.imagej.ops.map.neighborhood.AbstractMapNeighborhood; +import net.imagej.ops.map.neighborhood.CenterAwareComputerOp; +import net.imglib2.algorithm.neighborhood.RectangleShape; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.type.NativeType; + +import org.scijava.Priority; +import org.scijava.plugin.Plugin; + +/** + * Optimized center aware neighborhood map implementation for 1D/2D/3D + * {@link Img}. This implementation uses access to the underlying types, which + * bypasses OutOfBounds checks, though. This means that pixels which are out of + * bounds are not considered as belonging to the neighborhood of a pixel. This + * can change results of averages over a neighborhood in comparison to using an + * out of bounds strategy which "creates" pixels in the neighborhood where there + * are none after the bounds of the image. + * + * @author Jonathan Hale + * @param Input {@link NativeType} + * @param Ouput {@link NativeType} + * @see MapNeighborhoodNativeType + */ +@Plugin(type = Op.class, name = Ops.Map.NAME, priority = Priority.LOW_PRIORITY + + 22) +public class MapNeighborhoodWithCenterNativeType, O extends NativeType> + extends + AbstractMapNeighborhood, ArrayImg, RectangleShape, CenterAwareComputerOp> + implements Contingent +{ + + @Override + public void compute2(final ArrayImg input, final RectangleShape shape, + final ArrayImg output) + { + final I in = input.firstElement(); + final O out = output.firstElement(); + + final int width = (int) input.dimension(0); + final int height = (int) input.dimension(1); + final int depth = Math.max(1, (int) input.dimension(2)); + + final CenterAwareComputerOp op = getOp(); + + int index; + + for (int z = 0; z < depth; ++z) { + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + // save the current index, since it will be changed by the + // NeighborhoodIterable. Increment to save doing that later. + index = in.getIndex() + 1; + + // copy for center pixel access, since it will get changed, again, by + // NeighborhoodIterable. + final I center = in.copy(); + + final Iterable neighborhood = new NeighborhoodIterableNativeType<>( + in, x, y, z, width, height, depth, shape.getSpan()); + + op.compute2(neighborhood, center, out); + + in.updateIndex(index); + out.incIndex(); + } + } + } + } + + @Override + public boolean conforms() { + return in().numDimensions() > 0 && in().numDimensions() <= 3 && !in2() + .isSkippingCenter(); + } + +} diff --git a/src/main/java/net/imagej/ops/map/neighborhood/array/NeighborhoodIterableNativeType.java b/src/main/java/net/imagej/ops/map/neighborhood/array/NeighborhoodIterableNativeType.java new file mode 100644 index 0000000000..6bbdf15fdd --- /dev/null +++ b/src/main/java/net/imagej/ops/map/neighborhood/array/NeighborhoodIterableNativeType.java @@ -0,0 +1,161 @@ + +package net.imagej.ops.map.neighborhood.array; + +import java.util.Iterator; + +import net.imglib2.type.NativeType; + +/** + * Optimized rectangle neighborhood {@link Iterable} for {@link NativeType} + * which ignores out of bounds pixels. + * + * @param Type of the contents of the Iterable. + * @author Jonathan Hale + */ +final class NeighborhoodIterableNativeType> implements + Iterable +{ + + private final I pointer; + private final int neighSize; + private final int hDiameter; + private final int vDiameter; + private final int startIndex; + private final int nextLineSkip; + private final int nextSliceSkip; + + /* whether this is a 3D neighborhood */ + private final boolean volume; + + /** + * Constructor + * + * @param pointer NativeType to use as "cursor" + * @param x Left bounds of the rectangle neighborhood + * @param y Top bounds of the rectangle neighborhood + * @param z Front bounds of the rectangle neighborhood + * @param w Width of the rectangle neighborhood + * @param h Height of the rectangle neighborhood + * @param d Depth of the rectangle neighborhood + * @param span Span of the neighborhood (to avoid redundant calculation) + */ + public NeighborhoodIterableNativeType(final I pointer, final int x, + final int y, final int z, final int w, final int h, final int d, + final int span) + { + // clamp extensions in every direction to ensure we won't go out of bounds + final int left = Math.min(x, span); + final int top = Math.min(y, span); + final int right = Math.min(w - 1 - x, span); + final int bottom = Math.min(h - 1 - y, span); + final int front = Math.min(z, span); + final int back = Math.min(d - 1 - z, span); + + this.hDiameter = left + right + 1; + this.vDiameter = top + bottom + 1; + final int dDiameter = back + front + 1; + this.pointer = pointer; + this.neighSize = hDiameter * vDiameter * dDiameter; + + pointer.decIndex(front * w * h + top * w + left + 1); + + this.startIndex = pointer.getIndex(); + + this.nextLineSkip = w - (hDiameter - 1); + this.nextSliceSkip = (h - vDiameter) * hDiameter; + + this.volume = vDiameter != 1; + } + + @Override + public final Iterator iterator() { + return (volume) ? new MapNeighborhoodIterator3D() + : new MapNeighborhoodIterator2D(); + } + + /** + * Iterator over a rectangular neighborhood. + * + * @author Jonathan Hale + */ + private final class MapNeighborhoodIterator2D implements Iterator { + + public MapNeighborhoodIterator2D() { + pointer.updateIndex(startIndex); + } + + int index = 0; + int x = -1; + + @Override + public final boolean hasNext() { + return index < neighSize; + } + + @Override + public final I next() { + ++index; + ++x; + + if (x == hDiameter) { + // end of line, skip pixels until next line + pointer.incIndex(nextLineSkip); + x = 0; + } + else { + pointer.incIndex(); + } + + return pointer; + } + } + + /** + * Iterator over a rectangular neighborhood. + * + * @author Jonathan Hale + */ + private final class MapNeighborhoodIterator3D implements Iterator { + + public MapNeighborhoodIterator3D() { + pointer.updateIndex(startIndex); + } + + int index = 0; + int x = -1; + int y = -1; + + @Override + public final boolean hasNext() { + return index < neighSize; + } + + @Override + public final I next() { + ++index; + ++x; + + if (x == hDiameter) { + // end of line, skip pixels until next line + pointer.incIndex(nextLineSkip); + x = 0; + ++y; + if (y == vDiameter) { + // end of slice, skip pixels until next slice + pointer.incIndex(nextSliceSkip); + y = 0; + } + } + else { + pointer.incIndex(); + } + + return pointer; + } + + @Override + public final void remove() { + // noop + } + } +} diff --git a/src/main/java/net/imagej/ops/morphology/dilate/DefaultDilate.java b/src/main/java/net/imagej/ops/morphology/dilate/DefaultDilate.java index b1d79c5d82..39c935755f 100644 --- a/src/main/java/net/imagej/ops/morphology/dilate/DefaultDilate.java +++ b/src/main/java/net/imagej/ops/morphology/dilate/DefaultDilate.java @@ -80,7 +80,7 @@ public class DefaultDilate> extends private OutOfBoundsFactory> f; private T minVal; - private MapNeighborhood, IterableInterval, UnaryComputerOp, T>> mapper; + private MapNeighborhood, IterableInterval, Shape, UnaryComputerOp, T>> mapper; private UnaryFunctionOp> imgCreator; @Override diff --git a/src/main/java/net/imagej/ops/morphology/erode/DefaultErode.java b/src/main/java/net/imagej/ops/morphology/erode/DefaultErode.java index 543e7af692..c7837a9bdb 100644 --- a/src/main/java/net/imagej/ops/morphology/erode/DefaultErode.java +++ b/src/main/java/net/imagej/ops/morphology/erode/DefaultErode.java @@ -80,7 +80,7 @@ public class DefaultErode> extends private OutOfBoundsFactory> f; private T maxVal; - private MapNeighborhood, IterableInterval, UnaryComputerOp, T>> mapper; + private MapNeighborhood, IterableInterval, Shape, UnaryComputerOp, T>> mapper; private UnaryFunctionOp> imgCreator; @Override diff --git a/src/test/java/net/imagej/ops/map/neighborhood/MapNeighborhoodTest.java b/src/test/java/net/imagej/ops/map/neighborhood/MapNeighborhoodTest.java index 577fa7328c..a70e3f0cdd 100644 --- a/src/test/java/net/imagej/ops/map/neighborhood/MapNeighborhoodTest.java +++ b/src/test/java/net/imagej/ops/map/neighborhood/MapNeighborhoodTest.java @@ -36,6 +36,9 @@ import net.imagej.ops.AbstractOpTest; import net.imagej.ops.Op; +import net.imagej.ops.map.neighborhood.array.MapNeighborhoodNativeType; +import net.imagej.ops.map.neighborhood.array.MapNeighborhoodNativeTypeExtended; +import net.imagej.ops.map.neighborhood.array.MapNeighborhoodWithCenterNativeType; import net.imagej.ops.special.computer.AbstractUnaryComputerOp; import net.imglib2.algorithm.neighborhood.RectangleShape; import net.imglib2.img.Img; @@ -113,6 +116,179 @@ public void testMapNeighborhoodsWithCenterAccess() { } } + /** + * Test if every neighborhood pixel of the 2D image was really accessed during + * the map operation. + * + * @see MapNeighborhoodNativeType + */ + @Test + public void testMapNeighborhoodsArrayImage2D() { + final Op functional = + ops.op(MapNeighborhoodNativeType.class, out, in, + new RectangleShape(1, false), new CountNeighbors()); + functional.run(); + + final byte[] expected = + new byte[] { 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, + 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4 }; + + int index = 0; + for (ByteType t : out) { + assertEquals("Index " + index + ": ", expected[index++], t.get()); + } + } + + /** + * Test if every neighborhood pixel of the 2D image was really accessed during + * the map operation. + * + * @see MapNeighborhoodNativeTypeExtended + */ + @Test + public void testMapNeighborhoodsArrayImageAlias2D() { + in = generateByteArrayTestImg(true, 7, 7); + out = generateByteArrayTestImg(false, 7, 7); + + final Op functional = + ops.op(MapNeighborhoodNativeTypeExtended.class, out, in, + new RectangleShape(1, false), new CountNeighbors()); + functional.run(); + + int index = 0; + for (ByteType t : out) { + assertEquals("Index " + index + ": ", 9, t.get()); + index++; + } + } + + /** + * Test if every neighborhood pixel of the 1D image was really accessed during + * the map operation. + * + * @see MapNeighborhoodNativeType + */ + @Test + public void testMapNeighborhoodsArrayImage1D() { + in = generateByteArrayTestImg(true, 7); + out = generateByteArrayTestImg(false, 7); + + final Op functional = + ops.op(MapNeighborhoodNativeType.class, out, in, + new RectangleShape(1, false), new CountNeighbors()); + functional.run(); + + final byte[] expected = new byte[] { 2, 3, 3, 3, 3, 3, 2 }; + + int index = 0; + for (ByteType t : out) { + assertEquals("Index " + index + ": ", expected[index++], t.get()); + } + } + + /** + * Test if every neighborhood pixel of the 1D image was really accessed during + * the map operation. + * + * @see MapNeighborhoodNativeTypeExtended + */ + @Test + public void testMapNeighborhoodsArrayImageAlias1D() { + in = generateByteArrayTestImg(true, 7); + out = generateByteArrayTestImg(false, 7); + + final Op functional = + ops.op(MapNeighborhoodNativeTypeExtended.class, out, in, + new RectangleShape(1, false), new CountNeighbors()); + functional.run(); + + int index = 0; + for (ByteType t : out) { + assertEquals("Index " + index + ": ", 3, t.get()); + index++; + } + } + + /** + * Test if every neighborhood pixel of the 3D image was really accessed during + * the map operation. + * + * @see MapNeighborhoodNativeType + */ + @Test + public void testMapNeighborhoodsArrayImage3D() { + in = generateByteArrayTestImg(true, 3, 3, 3); + out = generateByteArrayTestImg(false, 3, 3, 3); + + final Op functional = + ops.op(MapNeighborhoodNativeType.class, out, in, + new RectangleShape(1, false), new CountNeighbors()); + functional.run(); + + final byte[] expected = + new byte[] { 8, 12, 8, 12, 18, 12, 8, 12, 8, 12, 18, 12, 18, 27, 18, 12, + 18, 12, 8, 12, 8, 12, 18, 12, 8, 12, 8 }; + + int index = 0; + for (ByteType t : out) { + assertEquals("Index " + index + ": ", expected[index++], t.get()); + } + } + + /** + * Test if every neighborhood pixel of the 3D image was really accessed during + * the map operation. + * + * @see MapNeighborhoodNativeTypeExtended + */ + @Test + public void testMapNeighborhoodsArrayImageAlias3D() { + in = generateByteArrayTestImg(true, 7, 7, 7); + out = generateByteArrayTestImg(false, 7, 7, 7); + + final Op functional = + ops.op(MapNeighborhoodNativeTypeExtended.class, out, in, + new RectangleShape(1, false), new CountNeighbors()); + functional.run(); + + int index = 0; + for (ByteType t : out) { + assertEquals("Index " + index + ": ", 27, t.get()); + index++; + } + } + + /** + * Test if every neighborhood pixel of the 2D image was really accessed during + * the map operation. + * + * @see MapNeighborhoodWithCenterNativeType + */ + @Test + public void testMapNeighborhoodsWithCenterAccessArrayImage2D() { + final Op functional = + ops.op(MapNeighborhoodWithCenterNativeType.class, out, in, + new RectangleShape(1, false), new CountNeighborsWithCenter()); + functional.run(); + + final byte[] expected = + new byte[] { 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, + 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4 }; + + int index = 0; + for (ByteType t : out) { + assertEquals("Index " + index + ": ", expected[index++], t.get()); + } + } + + + /** * Function which increments the output value for every pixel in the * neighborhood.