diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..025e963ead2aa54de670d6e933032e42ea68e61d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +.project +.classpath +*.prefs +*.jardesc +*.jar +.settings/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9595cda03eef3585d8cfcc24e8b9cebabf60e260 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- Inherited Icy Parent POM --> + <parent> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>parent-pom-plugin</artifactId> + <version>1.0.1</version> + </parent> + + <!-- Project Information --> + <artifactId>intensity-projection</artifactId> + <version>1.6.2</version> + + <packaging>jar</packaging> + + <name>Intensity Projection plugin</name> + <description>Intensity projection along depth or time with multiple algorithms: mean, max, median, variance, standard deviation, saturated sum. Projection can be restricted to ROI.</description> + <url>http://icy.bioimageanalysis.org/plugin/intensity-projection</url> + <inceptionYear>2020</inceptionYear> + + <organization> + <name>Institut Pasteur - BIA</name> + <url>http://icy.bioimageanalysis.org</url> + </organization> + + <licenses> + <license> + <name>GNU GPLv3</name> + <url>https://www.gnu.org/licenses/gpl-3.0.en.html</url> + <distribution>repo</distribution> + </license> + </licenses> + + <developers> + <developer> + <id>adufour</id> + <name>Alexandre Dufour</name> + <roles> + <role>developer</role> + <role>debugger</role> + <role>tester</role> + <role>maintainer</role> + <role>support</role> + </roles> + </developer> + </developers> + + <!-- Project properties --> + <properties> + + </properties> + + <!-- Project build configuration --> + <build> + + </build> + + <!-- List of project's dependencies --> + <dependencies> + <!-- The core of Icy --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>icy-kernel</artifactId> + </dependency> + + <!-- The EzPlug library, simplifies writing UI for Icy plugins. --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>ezplug</artifactId> + </dependency> + + <!-- The EzPlug library, simplifies writing UI for Icy plugins. --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>protocols</artifactId> + <version>${protocols.version}</version> + </dependency> + </dependencies> + + <!-- Icy Maven repository (to find parent POM) --> + <repositories> + <repository> + <id>icy</id> + <name>Icy's Nexus</name> + <url>https://icy-nexus.pasteur.fr/repository/Icy/</url> + </repository> + </repositories> +</project> diff --git a/src/main/java/plugins/adufour/projection/Projection.java b/src/main/java/plugins/adufour/projection/Projection.java new file mode 100644 index 0000000000000000000000000000000000000000..6e85f6af86e56e5c12124dc545874309a42f6030 --- /dev/null +++ b/src/main/java/plugins/adufour/projection/Projection.java @@ -0,0 +1,688 @@ +package plugins.adufour.projection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import icy.image.IcyBufferedImage; +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.sequence.SequenceUtil; +import icy.system.SystemUtil; +import icy.type.DataType; +import icy.type.collection.array.Array1DUtil; +import plugins.adufour.blocks.lang.Block; +import plugins.adufour.blocks.util.VarList; +import plugins.adufour.ezplug.EzPlug; +import plugins.adufour.ezplug.EzStoppable; +import plugins.adufour.ezplug.EzVarBoolean; +import plugins.adufour.ezplug.EzVarEnum; +import plugins.adufour.ezplug.EzVarSequence; +import plugins.adufour.vars.lang.VarSequence; + +public class Projection extends EzPlug implements Block, EzStoppable +{ + public enum ProjectionDirection + { + Z, T + } + + public enum ProjectionType + { + MAX("Maximum"), MEAN("Average"), MED("Median"), MIN("Minimum"), STD("Standard Deviation"), + SATSUM("Saturated Sum"); + + private final String description; + + ProjectionType(String description) + { + this.description = description; + } + + @Override + public String toString() + { + return description; + } + } + + private final EzVarSequence input = new EzVarSequence("Input"); + + private final EzVarEnum<ProjectionDirection> projectionDir = new EzVarEnum<Projection.ProjectionDirection>( + "Project along", ProjectionDirection.values(), ProjectionDirection.Z); + + private final EzVarEnum<ProjectionType> projectionType = new EzVarEnum<Projection.ProjectionType>("Projection type", + ProjectionType.values(), ProjectionType.MAX); + + private final EzVarBoolean restrictToROI = new EzVarBoolean("Restrict to ROI", false); + + private final VarSequence output = new VarSequence("projected sequence", null); + + @Override + protected void initialize() + { + addEzComponent(input); + addEzComponent(projectionDir); + addEzComponent(projectionType); + + restrictToROI.setToolTipText( + "Check this option to project only the intensity data contained within the sequence ROI"); + addEzComponent(restrictToROI); + + setTimeDisplay(true); + } + + @Override + protected void execute() + { + switch (projectionDir.getValue()) + { + case T: + output.setValue( + tProjection(input.getValue(true), projectionType.getValue(), true, restrictToROI.getValue())); + break; + case Z: + output.setValue( + zProjection(input.getValue(true), projectionType.getValue(), true, restrictToROI.getValue())); + break; + default: + throw new UnsupportedOperationException( + "Projection along " + projectionDir.getValue() + " not supported"); + } + + if (getUI() != null) + addSequence(output.getValue()); + } + + @Override + public void clean() + { + + } + + /** + * 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 + * @return the projected sequence + */ + public static Sequence zProjection(final Sequence in, final ProjectionType projection, boolean multiThread) + { + return zProjection(in, projection, multiThread, false); + } + + /** + * 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); + final int cpus = SystemUtil.getNumberOfCPUs(); + + ExecutorService service = multiThread ? Executors.newFixedThreadPool(cpus) + : Executors.newSingleThreadExecutor(); + ArrayList<Future<?>> futures = new ArrayList<Future<?>>(); + + for (int frame = 0; frame < frames; frame++) + { + if (Thread.currentThread().isInterrupted()) + { + // stop all task now + service.shutdownNow(); + break; + } + + final int t = frame; + + // set new image in result sequence + out.setImage(t, 0, new IcyBufferedImage(width, height, channels, dataType)); + + for (int channel = 0; channel < channels; channel++) + { + final int c = channel; + // to optimize image access in main loop ! + final IcyBufferedImage[] imagesZ = in.getImages(t).toArray(new IcyBufferedImage[0]); + + futures.add(service.submit(new Runnable() + { + @Override + public void run() + { + final IcyBufferedImage resultImg = out.getImage(t, 0); + 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[] stackPixels = new double[depth]; + 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) + stackPixels[nbPixel++] = imagesZ[z].getData(x, y, c); + } + + if (nbPixel == 0) + continue; + + // adjust pixel array size if needed + stackPixels = Arrays.copyOf(stackPixels, nbPixel); + + double result = 0d; + + switch (projection) + { + case MAX: + result = ArrayMath.max(stackPixels); + break; + case MEAN: + result = ArrayMath.mean(stackPixels); + break; + case MED: + result = ArrayMath.median(stackPixels, false); + break; + case MIN: + result = ArrayMath.min(stackPixels); + break; + case STD: + result = ArrayMath.std(stackPixels, true); + break; + case SATSUM: + result = ArrayMath.sum(stackPixels); + 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) + { + // ignore + 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; + } + + /** + * 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 + * + * @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 + * @return the projected sequence + */ + public static Sequence tProjection(final Sequence in, final ProjectionType projection, boolean multiThread) + { + return tProjection(in, projection, multiThread, false); + } + + /** + * 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 + * + * @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 tProjection(final Sequence in, final ProjectionType projection, boolean multiThread, + boolean restrictToROI) + { + final int frames = in.getSizeT(); + if (frames == 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 depth = in.getSizeZ(); + 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); + } + + /** + * @param args + * input args + */ + public static void main(String[] args) + { + // start icy + Icy.main(args); + + // then start plugin + PluginLauncher.start(PluginLoader.getPlugin(Projection.class.getName())); + } +}