diff --git a/.gitignore b/.gitignore index 025e963ead2aa54de670d6e933032e42ea68e61d..21f1a7f532cfb4cb24ebce26322eeb1b9b3adcb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ -/target +target/ +workspace/ +.settings/ +setting.xml .project .classpath *.prefs *.jardesc -*.jar -.settings/ \ No newline at end of file +*.jar \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7529cf8de4fec4071bac702e2f73b6096875f4fd..b4a935b0c1b590678282c2183e68a7210caf0240 100644 --- a/pom.xml +++ b/pom.xml @@ -8,12 +8,12 @@ <parent> <groupId>org.bioimageanalysis.icy</groupId> <artifactId>parent-pom-plugin</artifactId> - <version>1.0.1</version> + <version>1.0.4</version> </parent> <!-- Project Information --> <artifactId>intensity-projection</artifactId> - <version>1.7.0-SNAPSHOT</version> + <version>1.8.0-SNAPSHOT</version> <packaging>jar</packaging> @@ -77,7 +77,6 @@ <dependency> <groupId>org.bioimageanalysis.icy</groupId> <artifactId>protocols</artifactId> - <version>${protocols.version}</version> </dependency> </dependencies> diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java new file mode 100644 index 0000000000000000000000000000000000000000..9531fe89028bdb900801e3bb2e913dc8655a0039 --- /dev/null +++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java @@ -0,0 +1,24 @@ +package org.bioimageanalysis.icy.image.projection; + +/** + * Indicates the direction on which the projection should be performed. + * + * @author Daniel Felipe Gonzalez Obando + */ +public enum ProjectionAxis +{ + X("x"), Y("y"), Z("z"), T("t"), C("c"); + + private String name; + + ProjectionAxis(String name) + { + this.name = name; + } + + @Override + public String toString() + { + return name; + } +} diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java new file mode 100644 index 0000000000000000000000000000000000000000..3f277106e31393a16fa48d180c4baed9a9e28b30 --- /dev/null +++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java @@ -0,0 +1,788 @@ +package org.bioimageanalysis.icy.image.projection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Future; + +import org.bioimageanalysis.icy.image.projection.util.MessageProgressListener; + +import icy.image.IcyBufferedImage; +import icy.image.IcyBufferedImageCursor; +import icy.math.ArrayMath; +import icy.roi.ROI; +import icy.sequence.Sequence; +import icy.sequence.VolumetricImage; +import icy.type.DataType; +import icy.util.OMEUtil; + +/** + * Calculates the projection of a sequence along the specified angle and using the specified operation. The projection can be applied to a given set of ROIs, + * only pixels contained in the ROIs will be taken into account to compute the projection. + * + * @author Daniel Felipe Gonzalez Obando + */ +public class ProjectionCalculator implements Callable<Sequence> +{ + + /** + * Builder class for {@link ProjectionCalculator}s. This allows users to specify the sequence, ROIs, axis and operation before creating the calculator. + * + * @author Daniel Felipe Gonzalez Obando + */ + public static class Builder + { + private Sequence s; + private List<ROI> rois; + private ProjectionAxis axis; + private ProjectionOperationType op; + + /** + * Creates a builder instance with the target sequence. + * By default, the ROI list is set to empty, the projection axis is set to {@link ProjectionAxis#Z} and the operation is set to + * {@link ProjectionOperationType#MAX}. + * + * @param s + * Target sequence for the projection. + */ + public Builder(Sequence s) + { + if (s == null || s.isEmpty()) + throw new IllegalArgumentException("Input sequence is null or empty."); + + this.s = s; + this.rois = new ArrayList<ROI>(); + this.axis = ProjectionAxis.Z; + this.op = ProjectionOperationType.MAX; + } + + /** + * Adds a ROI to the projection constraints. + * + * @param roi + * The ROI. + * @return This builder instance once the ROI is added. + */ + public Builder addRoi(ROI roi) + { + rois.add(roi); + return this; + } + + /** + * Adds a collection of ROIs to the projection constraints. + * + * @param rois + * The collection of ROIs to add. + * @return This builder instance once the ROIs have been added. + */ + public Builder addRois(Collection<? extends ROI> rois) + { + this.rois.addAll(rois); + return this; + } + + /** + * Sets the projection axis. + * If the argument is null the axis is set to {@link ProjectionAxis#Z}. + * + * @param axis + * Axis. + * @return This builder instance after setting the axis. + */ + public Builder axis(ProjectionAxis axis) + { + if (axis == null) + this.axis = ProjectionAxis.Z; + else + this.axis = axis; + + return this; + } + + /** + * Sets the operation used during the projection. + * If the argument is null, then the operation is set to {@link ProjectionOperationType#MAX} + * + * @param op + * Operation to apply. + * @return This builder instance after setting the operation. + */ + public Builder operation(ProjectionOperationType op) + { + if (op == null) + this.op = ProjectionOperationType.MAX; + else + this.op = op; + + return this; + } + + /** + * Builds the calculator using the parameters from this instance. + * + * @return The projection calculator ready to be called. + */ + public ProjectionCalculator build() + { + ProjectionCalculator calculator = new ProjectionCalculator(); + calculator.seq = s; + calculator.rois = rois; + calculator.axis = axis; + calculator.op = op; + return calculator; + + } + } + + private Sequence seq; + private List<ROI> rois; + private ProjectionAxis axis; + private ProjectionOperationType op; + private List<MessageProgressListener> progressListeners; + private boolean computed; + + private ProjectionCalculator() + { + progressListeners = new ArrayList<>(); + computed = false; + } + + /** + * Adds a listener for progress events. + * + * @param listener + * Progress event listener + */ + public void addProgressListener(MessageProgressListener listener) + { + progressListeners.add(listener); + } + + /** + * Removes a listener for progress events. + * + * @param listener + * Progress event listener + */ + public void removeProgressListener(MessageProgressListener listener) + { + progressListeners.remove(listener); + } + + /** + * Starts the projection if the calculator has not already computed the result. + */ + @Override + public Sequence call() throws Exception + { + try + { + if (!computed) + { + createResultSequence(); + result.beginUpdate(); + computeProjection(); + result.endUpdate(); + computed = true; + } + return getResultSequence(); + } + finally + { + notifyProgress(1, "Projection finished"); + } + } + + private Sequence result; + + private void createResultSequence() throws InterruptedException + { + result = new Sequence(OMEUtil.createOMEXMLMetadata(seq.getOMEXMLMetadata()), + op + " " + axis + "-projection of " + seq.getName()); + + final int width = axis == ProjectionAxis.X ? 1 : seq.getSizeX(); + final int height = axis == ProjectionAxis.Y ? 1 : seq.getSizeY(); + final int depth = axis == ProjectionAxis.Z ? 1 : seq.getSizeZ(); + final int frames = axis == ProjectionAxis.T ? 1 : seq.getSizeT(); + final int channels = axis == ProjectionAxis.C ? 1 : seq.getSizeC(); + final DataType dataType = seq.getDataType_(); + + for (int t = 0; t < frames; t++) + { + VolumetricImage vol = new VolumetricImage(); + for (int z = 0; z < depth; z++) + { + if (Thread.interrupted()) + throw new InterruptedException(); + vol.setImage(z, new IcyBufferedImage(width, height, channels, dataType)); + } + result.addVolumetricImage(t, vol); + notifyProgress(Math.max(0.01, t * (0.1 / frames)), "Creating result sequence t=" + t); + } + + if (axis != ProjectionAxis.C) + { + for (int c = 0; c < seq.getSizeC(); c++) + result.getColorModel().setColorMap(c, seq.getColorMap(c), true); + } + notifyProgress(0.01, "Result sequence instatiated"); + } + + private void computeProjection() throws InterruptedException, ExecutionException + { + + switch (axis) + { + case X: + startProjectionOnPlane(); + break; + case Y: + startProjectionOnPlane(); + break; + case Z: + startZProjection(); + break; + case T: + startTProjection(); + break; + case C: + startProjectionOnPlane(); + break; + } + notifyProgress(1, axis + "-projection done"); + } + + private void startProjectionOnPlane() throws InterruptedException, ExecutionException + { + final int frames = seq.getSizeT(); + final int depth = seq.getSizeZ(); + final int channels = seq.getSizeC(); + final int height = seq.getSizeY(); + final int width = seq.getSizeX(); + + final IcyBufferedImage[] inputPlanes = new IcyBufferedImage[frames * depth]; + final IcyBufferedImage[] outputPlanes = new IcyBufferedImage[frames * depth]; + int t, z, tOff; + for (t = 0; t < frames; t++) + { + tOff = t * depth; + for (z = 0; z < depth; z++) + { + inputPlanes[tOff + z] = seq.getImage(t, z); + outputPlanes[tOff + z] = result.getImage(t, z); + } + } + + ForkJoinPool generalTaskPool = (ForkJoinPool) Executors + .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1)); + + List<Future<IcyBufferedImage>> futures = new ArrayList<>(frames * depth); + for (t = 0; t < frames; t++) + { + tOff = t * depth; + for (z = 0; z < depth; z++) + { + Callable<IcyBufferedImage> task; + switch (axis) + { + case X: + task = getImageXProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels, + height, width); + break; + case Y: + task = getImageYProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels, + height, width); + break; + case C: + task = getImageCProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels, + height, width); + break; + default: + throw new IllegalArgumentException("Wrong axis parameter"); + } + futures.add(generalTaskPool.submit(task)); + } + } + + generalTaskPool.shutdown(); + + int pos; + for (pos = 0, t = 0; t < frames; t++) + { + for (z = 0; z < depth; z++) + { + try + { + futures.get(pos++).get(); + } + catch (InterruptedException | ExecutionException e) + { + generalTaskPool.shutdownNow(); + throw e; + } + } + notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t); + } + } + + private Callable<IcyBufferedImage> getImageXProjectionTask(IcyBufferedImage inputPlane, + IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height, + final int width) + { + return () -> { + int channel, line, column, elementsCount; + double[] elements = new double[width]; + + IcyBufferedImageCursor inputCursor = new IcyBufferedImageCursor(inputPlane); + IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(outputPlane); + for (channel = 0; channel < channels; channel++) + { + for (line = 0; line < height; line++) + { + elementsCount = 0; + for (column = 0; column < width; column++) + { + if (rois.isEmpty()) + { + elements[elementsCount++] = inputCursor.get(column, line, channel); + } + else + { + for (ROI roi : rois) + { + if (roi.contains(column, line, z, t, channel)) + { + elements[elementsCount++] = inputCursor.get(column, line, channel); + break; + } + } + } + } + if (elementsCount == 0) + { + outputCursor.setSafe(0, line, channel, 0d); + } + else + { + outputCursor.setSafe(0, line, channel, computeResVal(Arrays.copyOf(elements, elementsCount))); + } + + } + } + outputCursor.commitChanges(); + return outputPlane; + }; + } + + private Callable<IcyBufferedImage> getImageYProjectionTask(IcyBufferedImage inputPlane, + IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height, + final int width) + { + return () -> { + int channel, line, column, elementsCount; + double[] elements = new double[height]; + + IcyBufferedImageCursor inputCursor = new IcyBufferedImageCursor(inputPlane); + IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(outputPlane); + for (channel = 0; channel < channels; channel++) + { + for (column = 0; column < width; column++) + { + elementsCount = 0; + + for (line = 0; line < height; line++) + { + if (rois.isEmpty()) + { + elements[elementsCount++] = inputCursor.get(column, line, channel); + } + else + { + for (ROI roi : rois) + { + if (roi.contains(column, line, z, t, channel)) + { + elements[elementsCount++] = inputCursor.get(column, line, channel); + break; + } + } + } + } + if (elementsCount == 0) + { + outputCursor.setSafe(column, 0, channel, 0d); + } + else + { + outputCursor.setSafe(column, 0, channel, computeResVal(Arrays.copyOf(elements, elementsCount))); + } + + } + } + outputCursor.commitChanges(); + return outputPlane; + }; + } + + private Callable<IcyBufferedImage> getImageCProjectionTask(IcyBufferedImage inputPlane, + IcyBufferedImage outputPlane, int t, int z, int channels, int height, int width) + { + return () -> { + int channel, line, column, elementsCount; + double[] elements = new double[height]; + + IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[channels]; + IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(outputPlane); + for (channel = 0; channel < channels; channel++) + { + inputCursors[channel] = new IcyBufferedImageCursor(inputPlane); + } + + for (column = 0; column < width; column++) + { + for (line = 0; line < height; line++) + { + elementsCount = 0; + + for (channel = 0; channel < channels; channel++) + { + if (rois.isEmpty()) + { + elements[elementsCount++] = inputCursors[channel].get(column, line, channel); + } + else + { + for (ROI roi : rois) + { + if (roi.contains(column, line, z, t, channel)) + { + elements[elementsCount++] = inputCursors[channel].get(column, line, channel); + break; + } + } + } + } + + if (elementsCount == 0) + { + outputCursor.setSafe(column, line, 0, 0d); + } + else + { + outputCursor.setSafe(column, line, 0, computeResVal(Arrays.copyOf(elements, elementsCount))); + } + } + + } + + outputCursor.commitChanges(); + + return outputPlane; + }; + + } + + private void startZProjection() throws InterruptedException, ExecutionException + { + final int width = seq.getSizeX(); + final int height = seq.getSizeY(); + final int depth = seq.getSizeZ(); + final int frames = seq.getSizeT(); + final int channels = seq.getSizeC(); + + ForkJoinPool generalTaskPool = (ForkJoinPool) Executors + .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2)); + ForkJoinPool volumeTaskPool = (ForkJoinPool) Executors + .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1)); + int t, c; + VolumetricImage inputVolume, resultVolume; + List<Future<VolumetricImage>> futures = new ArrayList<>(frames * depth); + for (t = 0; t < frames; t++) + { + inputVolume = seq.getVolumetricImage(t); + resultVolume = result.getVolumetricImage(t); + for (c = 0; c < channels; c++) + { + futures.add(generalTaskPool.submit(getImageZProjectionTask(t, c, inputVolume, resultVolume, channels, + height, width, depth, volumeTaskPool))); + + } + } + + generalTaskPool.shutdown(); + + try + { + int pos; + for (pos = 0, t = 0; t < frames; t++) + { + for (c = 0; c < channels; c++) + { + futures.get(pos++).get(); + } + notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t); + } + } + catch (InterruptedException | ExecutionException e) + { + generalTaskPool.shutdownNow(); + throw e; + } + finally + { + volumeTaskPool.shutdown(); + } + + } + + private Callable<VolumetricImage> getImageZProjectionTask(int t, int c, VolumetricImage inputVolume, + VolumetricImage resultVolume, int channels, int height, int width, int depth, ForkJoinPool volumeTaskPool) + { + return () -> { + int slice, line, column; + + IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[depth]; + IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(resultVolume.getImage(0)); + for (slice = 0; slice < depth; slice++) + { + inputCursors[slice] = new IcyBufferedImageCursor(inputVolume.getImage(slice)); + } + + ArrayList<Future<double[]>> futureLines = new ArrayList<>(width); + for (line = 0; line < height; line++) + { + final int y = line; + futureLines.add(volumeTaskPool.submit(() -> { + int x, elemCount, z; + double[] lineElements = new double[width], elements = new double[depth]; + + for (x = 0; x < width; x++) + { + elemCount = 0; + for (z = 0; z < depth; z++) + { + if (rois.isEmpty()) + { + elements[elemCount++] = inputCursors[z].get(x, y, c); + } + else + { + for (ROI roi : rois) + { + if (roi.contains(x, y, z, t, c)) + { + elements[elemCount++] = inputCursors[z].get(x, y, c); + break; + } + } + } + } + + if (elemCount == 0) + { + lineElements[x] = 0d; + } + else + { + lineElements[x] = computeResVal(Arrays.copyOf(elements, elemCount)); + } + } + + return lineElements; + })); + } + + try + { + for (line = 0; line < height; line++) + { + double[] lineElements = futureLines.get(line).get(); + for (column = 0; column < width; column++) + { + outputCursor.setSafe(column, line, c, lineElements[column]); + } + } + } + finally + { + outputCursor.commitChanges(); + } + + return resultVolume; + }; + + } + + private void startTProjection() throws InterruptedException, ExecutionException + { + final int width = seq.getSizeX(); + final int height = seq.getSizeY(); + final int depth = seq.getSizeZ(); + final int frames = seq.getSizeT(); + final int channels = seq.getSizeC(); + + ForkJoinPool generalTaskPool = (ForkJoinPool) Executors + .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2)); + ForkJoinPool temporalTaskPool = (ForkJoinPool) Executors + .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1)); + int z, c; + Sequence inputS, resultS; + List<Future<Sequence>> futures = new ArrayList<>(depth); + for (z = 0; z < depth; z++) + { + for (c = 0; c < channels; c++) + { + inputS = seq; + resultS = result; + { + futures.add(generalTaskPool.submit(getImageTProjectionTask(z, c, inputS, resultS, channels, height, + width, depth, frames, temporalTaskPool))); + } + } + } + + generalTaskPool.shutdown(); + + try + { + int pos; + for (pos = 0, z = 0; z < depth; z++) + { + for (c = 0; c < channels; c++) + { + futures.get(pos++).get(); + } + notifyProgress(0.01 + 0.99 * ((double) z / depth), axis + "-projection: Processed z=" + z); + } + } + catch (InterruptedException | ExecutionException e) + { + generalTaskPool.shutdownNow(); + throw e; + } + finally + { + temporalTaskPool.shutdown(); + } + } + + private Callable<Sequence> getImageTProjectionTask(int z, int c, Sequence inputS, Sequence resultS, int channels, + int height, int width, int depth, int frames, ForkJoinPool temporalTaskPool) + { + return () -> { + int frame, line, column; + IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[frames]; + IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(resultS.getImage(0, z)); + for (frame = 0; frame < frames; frame++) + { + inputCursors[frame] = new IcyBufferedImageCursor(inputS.getImage(frame, z)); + } + + List<Future<double[]>> futureLines = new ArrayList<>(height); + for (line = 0; line < height; line++) + { + final int y = line; + futureLines.add(temporalTaskPool.submit(() -> { + int x, elemCount, t; + double[] lineElements = new double[width], elements = new double[frames]; + + for (x = 0; x < width; x++) + { + elemCount = 0; + for (t = 0; t < frames; t++) + { + if (rois.isEmpty()) + { + elements[elemCount++] = inputCursors[t].get(x, y, c); + } + else + { + for (ROI roi : rois) + { + if (roi.contains(x, y, z, t, c)) + { + elements[elemCount++] = inputCursors[t].get(x, y, c); + break; + } + } + } + } + + if (elemCount == 0) + { + lineElements[x] = 0d; + } + else + { + lineElements[x] = computeResVal(Arrays.copyOf(elements, elemCount)); + } + } + return lineElements; + })); + } + + try + { + for (line = 0; line < height; line++) + { + double[] lineElements = futureLines.get(line).get(); + for (column = 0; column < width; column++) + { + outputCursor.setSafe(column, line, c, lineElements[column]); + } + } + } + finally + { + outputCursor.commitChanges(); + } + + return resultS; + }; + } + + private double computeResVal(double[] elements) + { + switch (op) + { + case MAX: + return ArrayMath.max(elements); + case MEAN: + return ArrayMath.mean(elements); + case MED: + if (elements.length == 1) + return elements[0]; + else + return ArrayMath.median(elements, false); + case MIN: + return ArrayMath.min(elements); + case SATSUM: + return ArrayMath.sum(elements); + case STD: + return ArrayMath.std(elements, true); + } + return 0; + } + + private void notifyProgress(double progress, String message) + { + progressListeners.forEach(l -> l.onProgress(progress, message)); + } + + private Sequence getResultSequence() + { + return result; + } + + public void reset() + { + result = null; + computed = false; + } +} diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java new file mode 100644 index 0000000000000000000000000000000000000000..4c693918e3ba0d22acffa6ab0e9e6b1a4b550793 --- /dev/null +++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java @@ -0,0 +1,27 @@ +/** + * + */ +package org.bioimageanalysis.icy.image.projection; + +/** + * Represents the operation to be applied when performing an intensity projection on a sequence. + * + * @author Daniel Felipe Gonzalez Obando + */ +public enum ProjectionOperationType +{ + MAX("Maximum"), MEAN("Average"), MED("Median"), MIN("Minimum"), STD("Standard Deviation"), SATSUM("Saturated Sum"); + + private final String description; + + ProjectionOperationType(String description) + { + this.description = description; + } + + @Override + public String toString() + { + return description; + } +} diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java new file mode 100644 index 0000000000000000000000000000000000000000..c3de81e3f42542a963d549157ec78f6b75163640 --- /dev/null +++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java @@ -0,0 +1,22 @@ +/** + * + */ +package org.bioimageanalysis.icy.image.projection.util; + +/** + * A Progress event listener that handles events associating a value and a message to them. + * + * @author Daniel Felipe Gonzalez Obando + */ +public interface MessageProgressListener +{ + /** + * A method handling progress events with a value and a message. + * + * @param progress + * The progress value from 0.0 to 1.0. {@link Double#NaN} value should be taken as no change and -1.0 as indeterminate progress. + * @param message + * The event message. A null value should be taken as no change on the message. + */ + void onProgress(double progress, String message); +} diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java new file mode 100644 index 0000000000000000000000000000000000000000..5c10cd6f2b5592a2f998ce089ca9dcf089a453c8 --- /dev/null +++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java @@ -0,0 +1,150 @@ +package org.bioimageanalysis.icy.image.projection.util; + +import java.util.concurrent.atomic.AtomicBoolean; + +import icy.sequence.Sequence; + +/** + * This class allows to optimally access randomly around a {@link Sequence}. Instances of this class can perform reading and writing operations on + * non-contiguous positions of the sequence without incurring in important performance issues. When a set of modifications to pixel data is performed a call to + * {@link #commitChanges()} must be made in order to make this changes permanent of the image and let other resources using the image be aware of to these + * changes. + * + * @author Daniel Felipe Gonzalez Obando + */ +public class SequenceCursor +{ + private Sequence seq; + private VolumetricImageCursor[] volumeCursors; + private AtomicBoolean sequenceChanged; + + /** + * Creates a cursor for the given sequence {@code seq}. + */ + public SequenceCursor(Sequence seq) + { + this.seq = seq; + this.volumeCursors = new VolumetricImageCursor[seq.getSizeT()]; + this.sequenceChanged = new AtomicBoolean(); + this.currentT = -1; + } + + /** + * Retrieves the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}) at time {@code t}. + * + * @param x + * Position in the X-axis. + * @param y + * Position in the Y-axis. + * @param z + * Position in the Z-axis. + * @param t + * Time point index. + * @param c + * Channel index. + * @return Intensity value at specified position. + * @throws IndexOutOfBoundsException + * If the position is not in the image. + * @throws RuntimeException + * If the data type is not a valid format. + */ + public double get(int x, int y, int z, int t, int c) throws IndexOutOfBoundsException, RuntimeException + { + return getVolumeCursor(t).get(x, y, z, c); + } + + /** + * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}) at time {@code t}. + * + * @param x + * Position in the X-axis. + * @param y + * Position in the Y-axis. + * @param z + * Position in the Z-axis. + * @param t + * Time point index. + * @param c + * Channel index. + * @param val + * Intensity value to set. + * @throws IndexOutOfBoundsException + * If the position is not in the image. + * @throws RuntimeException + * If the data type is not a valid format. + */ + public synchronized void set(int x, int y, int z, int t, int c, double val) + throws IndexOutOfBoundsException, RuntimeException + { + getVolumeCursor(t).set(x, y, z, c, val); + sequenceChanged.set(true); + } + + /** + * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}) at time {@code t}. This method limits the + * value of the intensity according to the image data type value range. + * + * @param x + * Position in the X-axis. + * @param y + * Position in the Y-axis. + * @param z + * Position in the Z-axis. + * @param t + * Time point index. + * @param c + * Channel index. + * @param val + * Intensity value to set. + * @throws IndexOutOfBoundsException + * If the position is not in the image. + * @throws RuntimeException + * If the data type is not a valid format. + */ + public synchronized void setSafe(int x, int y, int z, int t, int c, double val) + throws IndexOutOfBoundsException, RuntimeException + { + getVolumeCursor(t).setSafe(x, y, z, c, val); + sequenceChanged.set(true); + } + + private int currentT; + private VolumetricImageCursor currentCursor; + + private synchronized VolumetricImageCursor getVolumeCursor(int t) throws IndexOutOfBoundsException + { + if (currentT != t) + { + if (volumeCursors[t] == null) + { + volumeCursors[t] = new VolumetricImageCursor(seq, t); + } + currentCursor = volumeCursors[t]; + currentT = t; + } + return currentCursor; + } + + /** + * This method should be called after a set of intensity changes have been made to the target sequence. This methods allows other resources using the target + * sequence to be informed about the changes made to it. + */ + public synchronized void commitChanges() + { + if (sequenceChanged.get()) + { + for (int i = 0; i < volumeCursors.length; i++) + { + if (volumeCursors[i] != null) + volumeCursors[i].commitChanges(); + } + sequenceChanged.set(false); + } + } + + @Override + public String toString() + { + return "last T=" + currentT + " " + currentCursor != null ? currentCursor.toString() : ""; + } +} diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java new file mode 100644 index 0000000000000000000000000000000000000000..54d36d39bdb3567224bb1666ae9fe934f12583f6 --- /dev/null +++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java @@ -0,0 +1,165 @@ +package org.bioimageanalysis.icy.image.projection.util; + +import java.util.concurrent.atomic.AtomicBoolean; + +import icy.image.IcyBufferedImageCursor; +import icy.sequence.Sequence; +import icy.sequence.VolumetricImage; + +/** + * This class allows to optimally access randomly around an {@link VolumetricImage}. Instances of this class can perform reading and writing operations on + * non-contiguous positions of the volume without incurring in important performance issues. When a set of modifications to pixel data is performed a call to + * {@link #commitChanges()} must be made in order to make this changes permanent of the image and let other resources using the image be aware of to these + * changes. + * + * @author Daniel Felipe Gonzalez Obando + */ +public class VolumetricImageCursor +{ + private VolumetricImage vol; + + private AtomicBoolean volumeChanged; + + private IcyBufferedImageCursor[] planeCursors; + + /** + * Creates a cursor on the given volume {@code vol}. + * + * @param vol + * Target volume. + */ + public VolumetricImageCursor(VolumetricImage vol) + { + this.vol = vol; + planeCursors = new IcyBufferedImageCursor[vol.getSize()]; + volumeChanged = new AtomicBoolean(false); + currentZ = -1; + } + + /** + * Creates a cursor on the volume at time position {@code t} in the given sequence {@code seq}. + * + * @param seq + * Target sequence. + * @param t + * Time position where the volume is located in {@code seq}. + */ + public VolumetricImageCursor(Sequence seq, int t) + { + this(seq.getVolumetricImage(t)); + } + + /** + * Retrieves the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}). + * + * @param x + * Position in the X-axis. + * @param y + * Position in the Y-axis. + * @param z + * Position in the Z-axis. + * @param c + * Channel index. + * @return Intensity value at specified position. + * @throws IndexOutOfBoundsException + * If the position is not in the image. + * @throws RuntimeException + * If the data type is not a valid format. + */ + public double get(int x, int y, int z, int c) throws IndexOutOfBoundsException, RuntimeException + { + return getPlaneCursor(z).get(x, y, c); + + } + + /** + * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}). + * + * @param x + * Position in the X-axis. + * @param y + * Position in the Y-axis. + * @param z + * Position in the Z-axis. + * @param c + * Channel index. + * @param val + * Intensity value to set. + * @throws IndexOutOfBoundsException + * If the position is not in the image. + * @throws RuntimeException + * If the data type is not a valid format. + */ + public synchronized void set(int x, int y, int z, int c, double val) + throws IndexOutOfBoundsException, RuntimeException + { + getPlaneCursor(z).set(x, y, c, val); + volumeChanged.set(true); + } + + /** + * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}). This method limits the + * value of the intensity according to the image data type value range. + * + * @param x + * Position in the X-axis. + * @param y + * Position in the Y-axis. + * @param z + * Position in the Z-axis. + * @param c + * Channel index. + * @param val + * Intensity value to set. + * @throws IndexOutOfBoundsException + * If the position is not in the image. + * @throws RuntimeException + * If the data type is not a valid format. + */ + public synchronized void setSafe(int x, int y, int z, int c, double val) + throws IndexOutOfBoundsException, RuntimeException + { + getPlaneCursor(z).setSafe(x, y, c, val); + volumeChanged.set(true); + } + + private IcyBufferedImageCursor currentCursor; + private int currentZ; + + private synchronized IcyBufferedImageCursor getPlaneCursor(int z) throws IndexOutOfBoundsException + { + if (currentZ != z) + { + if (planeCursors[z] == null) + { + planeCursors[z] = new IcyBufferedImageCursor(vol.getImage(z)); + } + currentCursor = planeCursors[z]; + currentZ = z; + } + return currentCursor; + } + + /** + * This method should be called after a set of intensity changes have been made to the target volume. This methods allows other resources using the target + * volume to be informed about the changes made to it. + */ + public synchronized void commitChanges() + { + if (volumeChanged.get()) + { + for (int i = 0; i < planeCursors.length; i++) + { + if (planeCursors[i] != null) + planeCursors[i].commitChanges(); + } + volumeChanged.set(false); + } + } + + @Override + public String toString() + { + return "last Z=" + currentZ + " " + currentCursor != null ? currentCursor.toString() : ""; + } +} diff --git a/src/main/java/plugins/adufour/projection/Projection.java b/src/main/java/plugins/adufour/projection/Projection.java index 78e98bad685966c5bdd81c731cf6458120eade59..bea801a1f090324a5f73aabf7d7a44252dbc7516 100644 --- a/src/main/java/plugins/adufour/projection/Projection.java +++ b/src/main/java/plugins/adufour/projection/Projection.java @@ -1,24 +1,18 @@ package plugins.adufour.projection; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; -import icy.image.IcyBufferedImage; +import org.bioimageanalysis.icy.image.projection.ProjectionAxis; +import org.bioimageanalysis.icy.image.projection.ProjectionCalculator; +import org.bioimageanalysis.icy.image.projection.ProjectionOperationType; + import icy.main.Icy; -import icy.math.ArrayMath; import icy.plugin.PluginLauncher; import icy.plugin.PluginLoader; import icy.roi.ROI; import icy.sequence.Sequence; -import icy.system.SystemUtil; -import icy.system.thread.Processor; -import icy.type.DataType; -import icy.type.collection.array.Array1DUtil; -import icy.util.OMEUtil; +import icy.system.IcyHandledException; import plugins.adufour.blocks.lang.Block; import plugins.adufour.blocks.util.VarList; import plugins.adufour.ezplug.EzPlug; @@ -32,7 +26,7 @@ public class Projection extends EzPlug implements Block, EzStoppable { public enum ProjectionDirection { - Z, T + Z, T, C, Y, X } public enum ProjectionType @@ -54,58 +48,175 @@ public class Projection extends EzPlug implements Block, EzStoppable } } - private final EzVarSequence input = new EzVarSequence("Input"); + private EzVarSequence input; - private final EzVarEnum<ProjectionDirection> projectionDir = new EzVarEnum<Projection.ProjectionDirection>( - "Project along", ProjectionDirection.values(), ProjectionDirection.Z); + private EzVarEnum<ProjectionDirection> projectionDir; - private final EzVarEnum<ProjectionType> projectionType = new EzVarEnum<Projection.ProjectionType>("Projection type", - ProjectionType.values(), ProjectionType.MAX); + private EzVarEnum<ProjectionType> projectionType; - private final EzVarBoolean restrictToROI = new EzVarBoolean("Restrict to ROI", false); + private EzVarBoolean restrictToROI; - private final VarSequence output = new VarSequence("projected sequence", null); + private VarSequence output; @Override protected void initialize() { + initCommonVars(); + restrictToROI.setToolTipText( + "Check this option to project only the intensity data contained within the sequence ROI"); + addEzComponent(input); addEzComponent(projectionDir); addEzComponent(projectionType); - - restrictToROI.setToolTipText( - "Check this option to project only the intensity data contained within the sequence ROI"); addEzComponent(restrictToROI); + } + + @Override + public void declareInput(VarList inputMap) + { + initCommonVars(); + inputMap.add("input", input.getVariable()); + inputMap.add("projection direction", projectionDir.getVariable()); + inputMap.add("projection type", projectionType.getVariable()); + inputMap.add("restrict to ROI", restrictToROI.getVariable()); + } + private void initCommonVars() + { + input = new EzVarSequence("Input"); + projectionDir = new EzVarEnum<Projection.ProjectionDirection>("Project along", ProjectionDirection.values(), + ProjectionDirection.Z); + projectionType = new EzVarEnum<Projection.ProjectionType>("Projection type", ProjectionType.values(), + ProjectionType.MAX); + restrictToROI = new EzVarBoolean("Restrict to ROI", false); setTimeDisplay(true); } + @Override + public void declareOutput(VarList outputMap) + { + output = new VarSequence("projected sequence", null); + outputMap.add("projection output", output); + } + @Override protected void execute() { - switch (projectionDir.getValue()) + if (!isHeadLess()) { - case T: - output.setValue(tProjection(input.getValue(true), projectionType.getValue(), true, - restrictToROI.getValue().booleanValue())); - break; + getUI().setProgressBarVisible(true); + getUI().setProgressBarValue(Double.NaN); + } + + readInput(); + try + { + computeCalculator(); + } + catch (Exception e) + { + e.printStackTrace(); + throw new IcyHandledException("Error while projecting: " + e.getMessage(), e); + } + setOutput(); + internalClean(); + } + + private Sequence inputSequence; + private ProjectionAxis axis; + private ProjectionOperationType op; + private List<ROI> rois; + + private void readInput() + { + inputSequence = input.getValue(true); + axis = getAxis(projectionDir.getValue(true)); + op = getOperation(projectionType.getValue(true)); + rois = restrictToROI.getValue(true) ? inputSequence.getROIs() : Collections.emptyList(); + } + + private static ProjectionAxis getAxis(ProjectionDirection direction) + { + switch (direction) + { + case X: + return ProjectionAxis.X; + case Y: + return ProjectionAxis.Y; + case C: + return ProjectionAxis.C; case Z: - output.setValue(zProjection(input.getValue(true), projectionType.getValue(), true, - restrictToROI.getValue().booleanValue())); - break; + return ProjectionAxis.Z; + case T: + return ProjectionAxis.T; + + default: + throw new IllegalArgumentException("" + direction); + } + } + + private static ProjectionOperationType getOperation(ProjectionType type) + { + switch (type) + { + case MAX: + return ProjectionOperationType.MAX; + case MEAN: + return ProjectionOperationType.MEAN; + case MED: + return ProjectionOperationType.MED; + case MIN: + return ProjectionOperationType.MIN; + case SATSUM: + return ProjectionOperationType.SATSUM; + case STD: + return ProjectionOperationType.STD; + default: - throw new UnsupportedOperationException( - "Projection along " + projectionDir.getValue() + " not supported"); + throw new IllegalArgumentException("" + type); } + } + + private Sequence resultSequence; + + private void computeCalculator() throws Exception + { + ProjectionCalculator calculator = new ProjectionCalculator.Builder(inputSequence).axis(axis).operation(op) + .addRois(rois).build(); + if (!isHeadLess()) + { + calculator.addProgressListener(this::onProgress); + } + resultSequence = calculator.call(); + } + + private void onProgress(double progress, String message) + { + getUI().setProgressBarValue(progress); + getUI().setProgressBarMessage(message); + } + + private void setOutput() + { + if (isHeadLess()) + output.setValue(resultSequence); + else + addSequence(resultSequence); + } - if (getUI() != null) - addSequence(output.getValue()); + private void internalClean() + { + getUI().setProgressBarVisible(false); + inputSequence = null; + axis = null; + op = null; + rois = null; + resultSequence = null; } @Override public void clean() { - // } /** @@ -117,8 +228,11 @@ public class Projection extends EzPlug implements Block, EzStoppable * @param projection * the type of projection to perform (see {@link ProjectionType} enumeration) * @return the projected sequence + * @throws Exception + * If the projection cannot be correctly done. + * @see ProjectionCalculator */ - public static Sequence zProjection(final Sequence in, final ProjectionType projection) + public static Sequence zProjection(final Sequence in, final ProjectionType projection) throws Exception { return zProjection(in, projection, true, false); } @@ -134,8 +248,12 @@ public class Projection extends EzPlug implements Block, EzStoppable * @param multiThread * deprecated (there is not reason to not use it) * @return the projected sequence + * @throws Exception + * If the projection cannot be correctly done. + * @see ProjectionCalculator */ public static Sequence zProjection(final Sequence in, final ProjectionType projection, boolean multiThread) + throws Exception { return zProjection(in, projection, multiThread, false); } @@ -154,332 +272,19 @@ public class Projection extends EzPlug implements Block, EzStoppable * <code>true</code> projects only data located within the sequence ROI, * <code>false</code> projects the entire data set * @return the projected sequence + * @throws Exception + * If the projection cannot be correctly done. + * @see ProjectionCalculator */ public static Sequence zProjection(final Sequence in, final ProjectionType projection, boolean multiThread, - boolean restrictToROI) + boolean restrictToROI) throws Exception { - final Sequence out = new Sequence(OMEUtil.createOMEXMLMetadata(in.getOMEXMLMetadata()), - projection.name() + " projection of " + in.getName()); - - final int width = in.getSizeX(); - final int height = in.getSizeY(); - final int depth = in.getSizeZ(); - final int frames = in.getSizeT(); - final int channels = in.getSizeC(); - final DataType dataType = in.getDataType_(); - - final Collection<ROI> rois = in.getROISet(); - final boolean processROI = restrictToROI && (rois.size() > 0); - - final Processor processor = new Processor(Math.max(1024, height), SystemUtil.getNumberOfCPUs()); - final List<Future<?>> futures = new ArrayList<Future<?>>(); - - for (int frame = 0; frame < frames; frame++) - { - final int t = frame; - - // set new image in result sequence - final IcyBufferedImage resultImg = new IcyBufferedImage(width, height, channels, dataType); - // to optimize image access in main loop ! - final IcyBufferedImage[] images = in.getImages(t).toArray(new IcyBufferedImage[0]); - - for (int channel = 0; channel < channels; channel++) - { - final int c = channel; - // fast access to result pixel data for this channel - final Object resultData = resultImg.getDataXY(c); - - try - { - for (int line = 0; line < height; line++) - { - final int y = line; - - futures.add(processor.submit(new Runnable() - { - @Override - public void run() - { - double[] pixelStack = new double[depth]; - int offset = y * width; - - for (int x = 0; x < width; x++, offset++) - { - int nbPixel = 0; - - for (int z = 0; z < depth; z++) - { - boolean processPixel; - - if (processROI) - { - processPixel = false; - - for (ROI roi : rois) - { - if (roi.contains(x, y, z, t, c)) - { - processPixel = true; - break; - } - } - } - else - processPixel = true; - - if (processPixel) - pixelStack[nbPixel++] = images[z].getData(x, y, c); - } - - // no pixel processed here ? --> next - if (nbPixel == 0) - continue; - - final double[] pixels; - - // adjust pixel array size if needed - if (pixelStack.length > nbPixel) - pixels = Arrays.copyOf(pixelStack, nbPixel); - else - pixels = pixelStack; - - double result = 0d; - - switch (projection) - { - case MAX: - result = ArrayMath.max(pixels); - break; - case MEAN: - result = ArrayMath.mean(pixels); - break; - case MED: - result = ArrayMath.median(pixels, false); - break; - case MIN: - result = ArrayMath.min(pixels); - break; - case STD: - result = ArrayMath.std(pixels, true); - break; - case SATSUM: - result = ArrayMath.sum(pixels); - break; - default: - throw new UnsupportedOperationException( - projection + " intensity projection not implemented"); - } - - // set result in data array - Array1DUtil.setValue(resultData, offset, dataType, result); - } - } - })); - } - - // wait for completion for tasks - for (Future<?> future : futures) - future.get(); - } - catch (RejectedExecutionException e) - { - // mean that we were interrupted - processor.shutdownNow(); - } - catch (InterruptedException e) - { - // ignore - processor.shutdownNow(); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - - // data changed and cache update - resultImg.setDataXY(c, resultData); - } - - // set image in sequence - out.setImage(t, 0, resultImg); - } - - processor.shutdown(); - - // Copy color map information - for (int c = 0; c < in.getSizeC(); c++) - out.getColorModel().setColorMap(c, in.getColorMap(c), true); - - return out; + List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList(); + ProjectionCalculator calculator = new ProjectionCalculator.Builder(in).axis(ProjectionAxis.Z) + .operation(getOperation(projection)).addRois(rois).build(); + return calculator.call(); } - // /** - // * Performs a Z projection of the input sequence using the specified algorithm. If the sequence - // * is already 2D, then a copy of the sequence is returned - // * - // * @param in - // * the sequence to project - // * @param projection - // * the type of projection to perform (see {@link ProjectionType} enumeration) - // * @param multiThread - // * true if the process should be multi-threaded - // * @param restrictToROI - // * <code>true</code> projects only data located within the sequence ROI, - // * <code>false</code> projects the entire data set - // * @return the projected sequence - // */ - // public static Sequence zProjection_(final Sequence in, final ProjectionType projection, boolean multiThread, - // boolean restrictToROI) - // { - // final int depth = in.getSizeZ(); - // if (depth == 1 && !restrictToROI) - // return SequenceUtil.getCopy(in); - // - // final Sequence out = new Sequence(projection.name() + " projection of " + in.getName()); - // out.copyMetaDataFrom(in, false); - // - // final int width = in.getSizeX(); - // final int height = in.getSizeY(); - // final int frames = in.getSizeT(); - // final int channels = in.getSizeC(); - // final DataType dataType = in.getDataType_(); - // - // final Collection<ROI> rois = in.getROISet(); - // final boolean processROI = restrictToROI && rois.size() > 0; - // - // int cpus = SystemUtil.getNumberOfCPUs(); - // int chunkSize = width * height / cpus; - // final int[] minOffsets = new int[cpus]; - // final int[] maxOffsets = new int[cpus]; - // for (int cpu = 0; cpu < cpus; cpu++) - // { - // minOffsets[cpu] = chunkSize * cpu; - // maxOffsets[cpu] = chunkSize * (cpu + 1); - // } - // // NB: the last chunk must include the remaining pixels (in case rounding off occurs) - // maxOffsets[cpus - 1] = width * height; - // - // ExecutorService service = multiThread ? Executors.newFixedThreadPool(cpus) - // : Executors.newSingleThreadExecutor(); - // ArrayList<Future<?>> futures = new ArrayList<Future<?>>(channels * frames * cpus); - // - // for (int frame = 0; frame < frames; frame++) - // { - // final int t = frame; - // - // if (Thread.currentThread().isInterrupted()) - // break; - // - // out.setImage(t, 0, new IcyBufferedImage(width, height, channels, dataType)); - // - // for (int channel = 0; channel < channels; channel++) - // { - // final int c = channel; - // final Object[] in_Z_XY = (Object[]) in.getDataXYZ(t, c); - // final Object out_Z_XY = out.getDataXY(t, 0, c); - // - // for (int cpu = 0; cpu < cpus; cpu++) - // { - // final int minOffset = minOffsets[cpu]; - // final int maxOffset = maxOffsets[cpu]; - // - // futures.add(service.submit(new Runnable() - // { - // @Override - // public void run() - // { - // double[] buffer = new double[depth]; - // double[] dataToProject = null; - // - // for (int offset = minOffset; offset < maxOffset; offset++) - // { - // if (processROI) - // { - // int x = offset % width; - // int y = offset / width; - // - // int nbValues = 0; - // - // for (int z = 0; z < depth; z++) - // for (ROI roi : rois) - // if (roi.contains(x, y, z, t, c)) - // { - // buffer[nbValues++] = Array1DUtil.getValue(in_Z_XY[z], offset, dataType); - // break; - // } - // - // if (nbValues == 0) - // continue; - // - // dataToProject = (nbValues == buffer.length) ? buffer - // : Arrays.copyOf(buffer, nbValues); - // } - // else - // { - // for (int z = 0; z < depth; z++) - // buffer[z] = Array1DUtil.getValue(in_Z_XY[z], offset, dataType); - // dataToProject = buffer; - // } - // - // switch (projection) - // { - // case MAX: - // Array1DUtil.setValue(out_Z_XY, offset, dataType, ArrayMath.max(dataToProject)); - // break; - // case MEAN: - // Array1DUtil.setValue(out_Z_XY, offset, dataType, ArrayMath.mean(dataToProject)); - // break; - // case MED: - // Array1DUtil.setValue(out_Z_XY, offset, dataType, - // ArrayMath.median(dataToProject, false)); - // break; - // case MIN: - // Array1DUtil.setValue(out_Z_XY, offset, dataType, ArrayMath.min(dataToProject)); - // break; - // case STD: - // Array1DUtil.setValue(out_Z_XY, offset, dataType, - // ArrayMath.std(dataToProject, true)); - // break; - // case SATSUM: - // Array1DUtil.setValue(out_Z_XY, offset, dataType, - // Math.min(ArrayMath.sum(dataToProject), dataType.getMaxValue())); - // break; - // default: - // throw new UnsupportedOperationException( - // projection + " intensity projection not implemented"); - // } - // } // offset - // } - // })); - // } - // } - // } - // - // try - // { - // for (Future<?> future : futures) - // future.get(); - // } - // catch (InterruptedException iE) - // { - // Thread.currentThread().interrupt(); - // } - // catch (ExecutionException eE) - // { - // throw new RuntimeException(eE); - // } - // - // service.shutdown(); - // - // // Copy color map information - // for (int c = 0; c < in.getSizeC(); c++) - // out.getColorModel().setColorMap(c, in.getColorMap(c), true); - // - // out.dataChanged(); - // - // return out; - // } - /** * Performs a T projection of the input sequence using the specified algorithm. If the sequence * has only one time point, then a copy of the sequence is returned @@ -491,8 +296,12 @@ public class Projection extends EzPlug implements Block, EzStoppable * @param multiThread * true if the process should be multi-threaded * @return the projected sequence + * @throws Exception + * If the projection cannot be correctly done. + * @see ProjectionCalculator */ public static Sequence tProjection(final Sequence in, final ProjectionType projection, boolean multiThread) + throws Exception { return tProjection(in, projection, multiThread, false); } @@ -511,344 +320,24 @@ public class Projection extends EzPlug implements Block, EzStoppable * <code>true</code> projects only data located within the sequence ROI, * <code>false</code> projects the entire data set * @return the projected sequence + * @throws Exception + * If the projection cannot be correctly done. + * @see ProjectionCalculator */ public static Sequence tProjection(final Sequence in, final ProjectionType projection, boolean multiThread, - boolean restrictToROI) + boolean restrictToROI) throws Exception { - final Sequence out = new Sequence(OMEUtil.createOMEXMLMetadata(in.getOMEXMLMetadata()), - projection.name() + " projection of " + in.getName()); - - final int width = in.getSizeX(); - final int height = in.getSizeY(); - final int depth = in.getSizeZ(); - final int frames = in.getSizeT(); - final int channels = in.getSizeC(); - final DataType dataType = in.getDataType_(); - - final Collection<ROI> rois = in.getROISet(); - final boolean processROI = restrictToROI && (rois.size() > 0); - - final Processor processor = new Processor(Math.max(1024, height), SystemUtil.getNumberOfCPUs()); - final List<Future<?>> futures = new ArrayList<Future<?>>(); - - // to optimize image access in main loop ! - final IcyBufferedImage[] images = new IcyBufferedImage[depth * frames]; - for (int z = 0; z < depth; z++) - for (int t = 0; t < frames; t++) - images[(z * frames) + t] = in.getImage(t, z); - - for (int slice = 0; slice < depth; slice++) - { - final int z = slice; - final int imgOff = z * frames; - - // set new image in result sequence - final IcyBufferedImage resultImg = new IcyBufferedImage(width, height, channels, dataType); - - for (int channel = 0; channel < channels; channel++) - { - final int c = channel; - // fast access to result pixel data for this channel - final Object resultData = resultImg.getDataXY(c); - - try - { - for (int line = 0; line < height; line++) - { - final int y = line; - - futures.add(processor.submit(new Runnable() - { - @Override - public void run() - { - double[] pixelFrames = new double[frames]; - int offset = y * width; - - for (int x = 0; x < width; x++, offset++) - { - int nbPixel = 0; - - for (int t = 0; t < frames; t++) - { - boolean processPixel; - - if (processROI) - { - processPixel = false; - - for (ROI roi : rois) - { - if (roi.contains(x, y, z, t, c)) - { - processPixel = true; - break; - } - } - } - else - processPixel = true; - - if (processPixel) - pixelFrames[nbPixel++] = images[imgOff + t].getData(x, y, c); - } - - // no pixel processed here ? --> next - if (nbPixel == 0) - continue; - - final double[] pixels; - - // adjust pixel array size if needed - if (pixelFrames.length > nbPixel) - pixels = Arrays.copyOf(pixelFrames, nbPixel); - else - pixels = pixelFrames; - - double result = 0d; - - switch (projection) - { - case MAX: - result = ArrayMath.max(pixels); - break; - case MEAN: - result = ArrayMath.mean(pixels); - break; - case MED: - result = ArrayMath.median(pixels, false); - break; - case MIN: - result = ArrayMath.min(pixels); - break; - case STD: - result = ArrayMath.std(pixels, true); - break; - case SATSUM: - result = ArrayMath.sum(pixels); - break; - default: - throw new UnsupportedOperationException( - projection + " intensity projection not implemented"); - } - - // set result in data array - Array1DUtil.setValue(resultData, offset, dataType, result); - } - } - })); - } - - // wait for completion for tasks - for (Future<?> future : futures) - future.get(); - } - catch (RejectedExecutionException e) - { - // mean that we were interrupted - processor.shutdownNow(); - } - catch (InterruptedException e) - { - // ignore - processor.shutdownNow(); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - - // data changed and cache update - resultImg.setDataXY(c, resultData); - } - - // set image in sequence - out.setImage(0, z, resultImg); - } - - processor.shutdown(); - - // Copy color map information - for (int c = 0; c < in.getSizeC(); c++) - out.getColorModel().setColorMap(c, in.getColorMap(c), true); - - return out; - - /* - * - * - * final Sequence out = new Sequence(OMEUtil.createOMEXMLMetadata(in.getOMEXMLMetadata()), - * projection.name() + " projection of " + in.getName()); - * - * final int width = in.getSizeX(); - * final int height = in.getSizeY(); - * final int depth = in.getSizeZ(); - * final int frames = in.getSizeT(); - * final int channels = in.getSizeC(); - * final DataType dataType = in.getDataType_(); - * - * final Collection<ROI> rois = in.getROISet(); - * final boolean processROI = restrictToROI && (rois.size() > 0); - * final int cpus = SystemUtil.getNumberOfCPUs(); - * - * ExecutorService service = multiThread ? Executors.newFixedThreadPool(cpus) - * : Executors.newSingleThreadExecutor(); - * ArrayList<Future<?>> futures = new ArrayList<Future<?>>(); - * - * for (int slice = 0; slice < depth; slice++) - * { - * if (Thread.currentThread().isInterrupted()) - * { - * // stop all task now - * service.shutdownNow(); - * break; - * } - * - * final int z = slice; - * - * // set new image in result sequence - * out.setImage(0, z, new IcyBufferedImage(width, height, channels, dataType)); - * - * for (int channel = 0; channel < channels; channel++) - * { - * final int c = channel; - * - * futures.add(service.submit(new Runnable() - * { - * - * @Override - * public void run() - * { - * final IcyBufferedImage resultImg = out.getImage(0, z); - * final Object resultData = resultImg.getDataXY(c); - * int offset = 0; - * - * for (int y = 0; y < height; y++) - * { - * for (int x = 0; x < width; x++, offset++) - * { - * double[] framePixels = new double[frames]; - * int nbPixel = 0; - * - * for (int t = 0; t < frames; t++) - * { - * boolean processPixel; - * - * if (processROI) - * { - * processPixel = false; - * - * for (ROI roi : rois) - * { - * if (roi.contains(x, y, z, t, c)) - * { - * processPixel = true; - * break; - * } - * } - * } - * else - * processPixel = true; - * - * if (processPixel) - * framePixels[nbPixel++] = in.getData(t, z, c, y, x); - * } - * - * if (nbPixel == 0) - * continue; - * - * // adjust pixel array size if needed - * framePixels = Arrays.copyOf(framePixels, nbPixel); - * - * double result = 0d; - * - * switch (projection) - * { - * case MAX: - * result = ArrayMath.max(framePixels); - * break; - * case MEAN: - * result = ArrayMath.mean(framePixels); - * break; - * case MED: - * result = ArrayMath.median(framePixels, false); - * break; - * case MIN: - * result = ArrayMath.min(framePixels); - * break; - * case STD: - * result = ArrayMath.std(framePixels, true); - * break; - * case SATSUM: - * result = ArrayMath.sum(framePixels); - * break; - * default: - * throw new UnsupportedOperationException( - * projection + " intensity projection not implemented"); - * } - * - * // set result in data array - * Array1DUtil.setValue(resultData, offset, dataType, result); - * } - * - * // task interrupted ? - * if (Thread.currentThread().isInterrupted()) - * { - * // propagate partial changes and stop here - * resultImg.setDataXY(c, resultData); - * return; - * } - * } - * - * // data changed and cache update - * resultImg.setDataXY(c, resultData); - * } - * })); - * } - * } - * - * try - * { - * for (Future<?> future : futures) - * future.get(); - * } - * catch (InterruptedException e) - * { - * service.shutdownNow(); - * } - * catch (Exception e) - * { - * throw new RuntimeException(e); - * } - * - * service.shutdown(); - * - * // Copy color map information - * for (int c = 0; c < in.getSizeC(); c++) - * out.getColorModel().setColorMap(c, in.getColorMap(c), true); - * - * return out; - */ - } - - @Override - public void declareInput(VarList inputMap) - { - inputMap.add("input", input.getVariable()); - inputMap.add("projection direction", projectionDir.getVariable()); - inputMap.add("projection type", projectionType.getVariable()); - inputMap.add("restrict to ROI", restrictToROI.getVariable()); - } - - @Override - public void declareOutput(VarList outputMap) - { - outputMap.add("projection output", output); + List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList(); + ProjectionCalculator calculator = new ProjectionCalculator.Builder(in).axis(ProjectionAxis.T) + .operation(getOperation(projection)).addRois(rois).build(); + return calculator.call(); } /** + * Main method used for testing. + * * @param args - * input args + * Input arguments. */ public static void main(String[] args) {