Compare commits

...

58 Commits

Author SHA1 Message Date
Damien Goutte-Gattat ce283aba22 [maven-release-plugin] prepare for next development iteration 2 months ago
Damien Goutte-Gattat 6c35f22a01 [maven-release-plugin] prepare release incenp-imagej-plugins-0.9.6 2 months ago
Damien Goutte-Gattat b35fd4d14b Match header names in a case-insensitive manner by default. 2 months ago
Damien Goutte-Gattat 90f35b8bac Allow several names when looking for a specific cell. 2 months ago
Damien Goutte-Gattat 8a7339a2e8 [maven-release-plugin] prepare for next development iteration 2 months ago
Damien Goutte-Gattat b4a710ebbc [maven-release-plugin] prepare release incenp-imagej-plugins-0.9.5 2 months ago
Damien Goutte-Gattat 48fe51eb13 Update documentation. 2 months ago
Damien Goutte-Gattat f8c4267e18 Add the SciJava repository. 2 months ago
Damien Goutte-Gattat 1792a18875 Always call readCSV() if needed. 2 months ago
Damien Goutte-Gattat 7527b2e42f Bump dependencies. 2 months ago
Damien Goutte-Gattat ca7fa551bc Update documentation. 9 months ago
Damien Goutte-Gattat da77c7abc1 [maven-release-plugin] prepare for next development iteration 9 months ago
Damien Goutte-Gattat 3408d2b40a [maven-release-plugin] prepare release incenp-imagej-plugins-0.9.4 9 months ago
Damien Goutte-Gattat a6231d61e4 Prepare release to Maven Central. 9 months ago
Damien Goutte-Gattat c4ef652228 Add some more Maven plugins. 9 months ago
Damien Goutte-Gattat d1373bdfc0 Rename to incenp-imagej-plugins. 9 months ago
Damien Goutte-Gattat 3b2365967b [maven-release-plugin] prepare for next development iteration 10 months ago
Damien Goutte-Gattat e910fc440f [maven-release-plugin] prepare release incenp-plugins-0.9.3 10 months ago
Damien Goutte-Gattat 54f8063b3d Update documentation and copyright notices. 10 months ago
Damien Goutte-Gattat 454c1ff3f2 Fix the augmentHyperstack method. 10 months ago
Damien Goutte-Gattat aa1e4d776d Allow multiple mask applications. 10 months ago
Damien Goutte-Gattat 65a277a6ff Add the augmentHyperstack method. 10 months ago
Damien Goutte-Gattat 85fb7e7d65 Do not update display when creating an augmented hyperstack. 10 months ago
Damien Goutte-Gattat ada2ffacd4 Fix the extractVolumes helper method. 10 months ago
Damien Goutte-Gattat 3c71d8179e Bump ImageJ dependency. 11 months ago
Damien Goutte-Gattat cf9861b4e3 Accept alternative spelling for the Renyi_Entropy algorithm. 1 year ago
Damien Goutte-Gattat bf655ea73d [maven-release-plugin] prepare for next development iteration 1 year ago
Damien Goutte-Gattat 0b48f79d0e [maven-release-plugin] prepare release incenp-plugins-0.9.2 1 year ago
Damien Goutte-Gattat 727adc335a Update NEWS file. 1 year ago
Damien Goutte-Gattat e5490b6263 Fix typo in library documentation. 1 year ago
Damien Goutte-Gattat 92b240f10f Add the createAugmentedHyperstack helper method. 1 year ago
Damien Goutte-Gattat ba40f5f040 Do not set the "Stitch Tiles" option. 2 years ago
Damien Goutte-Gattat 9778b3db74 Use explicit importer options when processing batch file. 2 years ago
Damien Goutte-Gattat 810d58f029 [maven-release-plugin] prepare for next development iteration 2 years ago
Damien Goutte-Gattat 6f630e502e [maven-release-plugin] prepare release incenp-plugins-0.9.1 2 years ago
Damien Goutte-Gattat 6cff47c4f2 Prepare next release. 2 years ago
Damien Goutte-Gattat f84ca5c2e3 Document the helper library. 2 years ago
Damien Goutte-Gattat 007ea2d464 Add convenient constructors to the BatchReader class. 2 years ago
Damien Goutte-Gattat f034a3d519 BatchReader: Ignore empty lines. 2 years ago
Damien Goutte-Gattat f723ef8ba5 UnifiedMerge: Set source processor to grayscale if needed. 2 years ago
Damien Goutte-Gattat baba6b86d9 Update documentation. 2 years ago
Damien Goutte-Gattat 08fa631899 [maven-release-plugin] prepare for next development iteration 2 years ago
Damien Goutte-Gattat 77d1d81443 [maven-release-plugin] prepare release incenp-plugins-0.9.0 2 years ago
Damien Goutte-Gattat 21b6485cf3 Update NEWS file. 2 years ago
Damien Goutte-Gattat 076f1021a6 Avoid needless duplication of ImageProcessor. 2 years ago
Damien Goutte-Gattat c876b59c75 Fix bogus reference in a Javadoc comment. 2 years ago
Damien Goutte-Gattat 6b8abccbd6 Remove deprecated methods. 2 years ago
Damien Goutte-Gattat 597c8cefa2 Bump version to 0.9.0-SNAPSHOT. 2 years ago
Damien Goutte-Gattat 2f44ac0983 Count white pixels when extracting volumes. 2 years ago
Damien Goutte-Gattat 2f15101b2e Rework the ChannelMasker class. 2 years ago
Damien Goutte-Gattat 7e7a83c61b Add convenience methods to parse enum values. 2 years ago
Damien Goutte-Gattat 671f840383 Add a new unified interface to create masks. 2 years ago
Damien Goutte-Gattat 877afcb7ff Make local thresholding available to ChannelMasker. 2 years ago
Damien Goutte-Gattat 6dbed10b4e Add a helper method interfacing the Auto Local Threshold plugin. 2 years ago
Damien Goutte-Gattat f2171e88ec Do not invert image after nuclei segmentation. 2 years ago
Damien Goutte-Gattat 827e1a7ed2 Let ImageJ count black pixels. 2 years ago
Damien Goutte-Gattat 3a5e7af317 Support chaining ChannelMasker objects. 2 years ago
Damien Goutte-Gattat e7a6d206cd [maven-release-plugin] prepare for next development iteration 2 years ago
  1. 46
      NEWS
  2. 31
      README.md
  3. 98
      pom.xml
  4. 140
      src/main/java/org/incenp/imagej/BatchReader.java
  5. 41
      src/main/java/org/incenp/imagej/BinaryOperator.java
  6. 455
      src/main/java/org/incenp/imagej/ChannelMasker.java
  7. 313
      src/main/java/org/incenp/imagej/Helper.java
  8. 223
      src/main/java/org/incenp/imagej/Masking.java
  9. 3
      src/main/java/org/incenp/imagej/NucleiSegmenter.java
  10. 291
      src/main/java/org/incenp/imagej/ThresholdingMethod.java
  11. 2
      src/main/java/org/incenp/imagej/plugins/About.java
  12. 2
      src/main/java/org/incenp/imagej/plugins/ExtendedPanel.java
  13. 2
      src/main/java/org/incenp/imagej/plugins/FrameIntervalInfo.java
  14. 6
      src/main/java/org/incenp/imagej/plugins/Info.java
  15. 2
      src/main/java/org/incenp/imagej/plugins/Launcher.java
  16. 2
      src/main/java/org/incenp/imagej/plugins/MergeDialog.java
  17. 2
      src/main/java/org/incenp/imagej/plugins/PanelDialog.java
  18. 7
      src/main/java/org/incenp/imagej/plugins/UnifiedMerge.java
  19. 20
      src/site/apt/index.apt
  20. 310
      src/site/apt/library.apt
  21. 5
      src/site/site.xml

46
NEWS

@ -1,3 +1,49 @@
Changes in incenp-imagej-plugins-0.9.5
--------------------------------------
* Remove the need to explicitly call BatchReader.readCSV().
* Explicitly refer to the SciJava Maven repository in POM.
* Require more recent ImageJ (needed for MacOS development).
Changes in incenp-imagej-plugins-0.9.4
--------------------------------------
* Rename the artifact to incenp-imagej-plugins.
* Release to the Maven Central repository.
Changes in incenp-plugins-0.9.3
-------------------------------
* Allow application of multiple masks in a single operation.
* Accept alternative spelling for the Renyi_Entropy algorithm.
* Small bugfixes in methods from the Helper class.
Changes in incenp-plugins-0.9.2
-------------------------------
* Use explicit importer options when processing batch file.
Changes in incenp-plugins-0.9.1
-------------------------------
* UnifiedMerge: set source image to grayscale if needed.
* BatchReader: Ignore empty lines.
Changes in incenp-plugins-0.9.0
-------------------------------
* Support chaining ChannelMasker objects.
* Breaking: Do not invert image after nuclei segmentation.
* Breaking: New interface for masking operations.
* Support local thresholding algorithms.
* Breaking: Use white pixels when extracting volumes.
Changes in incenp-plugins-0.8.4
-------------------------------

31
README.md

@ -1,7 +1,7 @@
Incenp-Plugins - Incenp.org ImageJ Plugins
==========================================
Incenp.org ImageJ Plugins
=========================
Incenp-Plugins is a set of plugins for the [ImageJ](https://imagej.net)
Incenp-ImageJ-Plugins is a set of plugins for the [ImageJ](https://imagej.net)
image analysis software.
Available Plugins
@ -18,24 +18,39 @@ This plugin extracts an user-specified selection of frames from a
hyperstack and generates a new image containing the selected frames
disposed in a array.
Helper Library
--------------
Bundled with the plugins is a small library of helper classes for use in
other plugins or scripts. Notable classes include `BatchReader`, to
facilitate batch processing of images listed in a CSV input file, and
`ChannelMasker`, to facilitate the creation and manipulation of
binary masks. Refer to the package´s
[Javadoc](https://incenp.org/dvlpt/imagej-plugins/apidocs/index.html)
for more details.
Installation
------------
Build the plugins with [Maven](https://maven.apache.org/) by running
Build the plugins with [Maven](https://maven.apache.org/) by running,
from the source directory
```
$ mvn package
```
in the source directory. Then copy the generated JAR file (found in the
`target/` folder) to the `plugins` folder of your ImageJ installation.
Then copy the generated JAR file (found in the `target/` folder) to the
`plugins` folder of your ImageJ installation.
(Pre-compiled JAR files are also available for each release on the
[releases page](https://git.incenp.org/damien/imagej-plugins/releases)
for the project.)
Once installed (and after restarting ImageJ if it was already running),
the plugins will be available in the `Plugins > Incenp.org` menu.
Copying
-------
Incenp-Plugins is distributed under the terms of the GNU General Public
License, version 3 or higher. The full license is included in the
Incenp-ImageJ-Plugins is distributed under the terms of the GNU General
Public License, version 3 or higher. The full license is included in the
[LICENSE file](LICENSE) of the source distribution.
Homepage and repository

98
pom.xml

@ -1,14 +1,9 @@
<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>
<parent>
<groupId>org.incenp</groupId>
<artifactId>incenp-base</artifactId>
<version>1.1.0</version>
</parent>
<groupId>org.incenp.imagej</groupId>
<artifactId>incenp-plugins</artifactId>
<version>0.8.4</version>
<groupId>org.incenp</groupId>
<artifactId>incenp-imagej-plugins</artifactId>
<version>0.9.7-SNAPSHOT</version>
<name>Incenp.org ImageJ Plugins</name>
<description>Some plugins for ImageJ</description>
<url>https://incenp.org/dvlpt/imagej-plugins/</url>
@ -34,34 +29,48 @@
<connection>scm:git:https://git.incenp.org/damien/imagej-plugins.git</connection>
<developerConnection>scm:git:ssh://git@git.incenp.org/damien/imagej-plugins.git</developerConnection>
<url>https://git.incenp.org/damien/imagej-plugins</url>
<tag>incenp-plugins-0.8.4</tag>
<tag>HEAD</tag>
</scm>
<issueManagement>
<system>Gitea</system>
<url>https://git.incenp.org/damien/imagej-plugins/issues</url>
</issueManagement>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>scijava.public</id>
<url>https://maven.scijava.org/content/groups/public</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.imagej</groupId>
<artifactId>imagej</artifactId>
<version>2.0.0-rc-71</version>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>net.imagej</groupId>
<artifactId>imagej-legacy</artifactId>
<version>0.35.0</version>
<version>0.37.4</version>
</dependency>
<dependency>
<!-- For ImageJ2 plugin API -->
<groupId>org.scijava</groupId>
<artifactId>scijava-common</artifactId>
<version>2.77.0</version>
<version>2.85.0</version>
</dependency>
<dependency>
<!-- For util.opencsv -->
@ -75,6 +84,12 @@
<artifactId>bio-formats_plugins</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<!-- For fiji.threshold.Auto_Local_Threshold -->
<groupId>sc.fiji</groupId>
<artifactId>Auto_Local_Threshold</artifactId>
<version>1.10.1</version>
</dependency>
</dependencies>
<build>
@ -82,7 +97,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
@ -108,12 +123,63 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.8.2</version>
<version>3.9.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
<version>3.1.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.8</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</build>
@ -123,7 +189,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.1.1</version>
<version>3.2.0</version>
</plugin>
</plugins>
</reporting>

140
src/main/java/org/incenp/imagej/BatchReader.java

@ -1,5 +1,5 @@
/*
* Incenp.org Plugins
* Incenp.org ImageJ Plugins
* Copyright © 2019, 2020 Damien Goutte-Gattat
*
* This program is free software; you can redistribute it and/or modify
@ -28,6 +28,7 @@ import ij.ImagePlus;
import ij.measure.ResultsTable;
import loci.formats.FormatException;
import loci.plugins.BF;
import loci.plugins.in.ImporterOptions;
import util.opencsv.CSVReader;
/**
@ -63,6 +64,7 @@ public class BatchReader {
private String filename;
private String[] headers;
private boolean withHeaders;
private boolean caseSensitiveHeaders;
private ArrayList<String[]> rows;
private ImagePlus[] currentImages;
private Hashtable<String, Integer> headersIndex;
@ -77,6 +79,7 @@ public class BatchReader {
public BatchReader(String filename, boolean noHeaders) {
this.filename = filename;
this.withHeaders = !noHeaders;
caseSensitiveHeaders = false;
headers = null;
headersIndex = null;
cursor = subcursor = -1;
@ -85,7 +88,7 @@ public class BatchReader {
}
/**
* Creates a new reader
* Creates a new reader.
*
* @param filename pathname to the file to read
*/
@ -93,6 +96,39 @@ public class BatchReader {
this(filename, false);
}
/**
* Creates a new reader.
*
* @param file the file object to read from
*/
public BatchReader(File file) {
this(file.getAbsolutePath(), false);
}
/**
* Creates a new reader.
*
* @param file the file object to read from
* @param noHeaders if true, the first non-comment line will not be treated as a
* header line
*/
public BatchReader(File file, boolean noHeaders) {
this(file.getAbsolutePath(), noHeaders);
}
/**
* Sets the case-sensitivity option. If set to true, the
* {@link #getCell(String)} method and its overloaded variants will match header
* names in a case-sensitive way. The default is false.
*
* @param caseSensitive true to match header names case-sensitively.
*/
public void setCaseSensitive(boolean caseSensitive) {
caseSensitiveHeaders = caseSensitive;
if ( headersIndex != null )
headersIndex = null;
}
/**
* Moves to the next available image. Once this method has been called and if it
* returned true, the image can be obtained by calling the {@link #getImage()}
@ -120,7 +156,10 @@ public class BatchReader {
return false;
try {
currentImages = BF.openImagePlus(rows.get(cursor)[0]);
ImporterOptions options = new ImporterOptions();
options.setId(rows.get(cursor)[0]);
options.setOpenAllSeries(true);
currentImages = BF.openImagePlus(options);
subcursor = 0;
} catch ( FormatException | IOException e ) {
return false;
@ -190,73 +229,66 @@ public class BatchReader {
}
/**
* Gets the contents of a specific cell within the current row.
* Gets the contents of a specific cell within the current row. The cell is
* searched for using several equally accepted names.
*
* @param name the column name, as indicated in the header row
* @param names an array of accepted column names
* @param fallback the default value to return if the cell is not found
* @return the contents of the specified cell
*/
public String getCell(String name) {
public String getCell(String[] names, String fallback) {
if ( headersIndex == null ) {
if ( (headersIndex = getHeadersIndex()) == null )
return null;
}
Integer indexObject = headersIndex.get(name);
if ( indexObject == null )
return "";
return getCell(indexObject.intValue());
}
private Hashtable<String, Integer> getHeadersIndex() {
if ( headersIndex == null && headers != null ) {
headersIndex = new Hashtable<String, Integer>();
for ( int i = 0; i < headers.length; i++ ) {
headersIndex.put(headers[i], i);
}
Integer index = null;
for ( int i = 0; i < names.length && index == null; i++ ) {
if ( !caseSensitiveHeaders )
index = headersIndex.get(names[i].toLowerCase());
else
index = headersIndex.get(names[i]);
}
return headersIndex;
if ( index == null )
return fallback;
return getCell(index.intValue());
}
/**
* Gets the contents of any extra columns beyond the filename in the current
* row.
* Gets the contents of a specific cell within the current row. The cell is
* searched for using several equally accepted names.
*
* @return the fields of the current row, minus the image filename
*
* @deprecated Use {@link #getRow()} instead.
* @param names an array of accepted column names
* @return the contents of the specified cell
*/
@Deprecated
public String[] getFields() {
if ( cursor >= rows.size() )
return null;
String[] row = rows.get(cursor);
String[] fields = new String[row.length - 1];
System.arraycopy(row, 1, fields, 0, row.length - 1);
return fields;
public String getCell(String[] names) {
return getCell(names, "");
}
/**
* Gets the contents of the specified column of the current row.
*
* @param index a 1-based column index
* @return the contents of the specified column for the current row
* Gets the contents of a specific cell within the current row.
*
* @deprecated Use {@link #getCell(int)} instead.
* @param name the column name, as indicated in the header row
* @return the contents of the specified cell
*/
@Deprecated
public String getField(int index) {
if ( cursor >= rows.size() )
return null;
public String getCell(String name) {
return getCell(new String[] { name }, "");
}
String[] row = rows.get(cursor);
if ( index >= row.length - 1 )
return null;
private Hashtable<String, Integer> getHeadersIndex() {
if ( headersIndex == null && headers != null ) {
headersIndex = new Hashtable<String, Integer>();
for ( int i = 0; i < headers.length; i++ ) {
if ( !caseSensitiveHeaders )
headersIndex.put(headers[i].toLowerCase(), i);
else
headersIndex.put(headers[i], i);
}
}
return row[index + 1];
return headersIndex;
}
/**
@ -297,7 +329,11 @@ public class BatchReader {
*
* @return the CSV file headers
*/
public String[] getHeaders() {
public String[] getHeaders() throws IOException {
if ( rows == null ) {
this.readCSV();
}
if ( headers == null ) {
headers = new String[1];
headers[0] = "Image";
@ -342,9 +378,7 @@ public class BatchReader {
/**
* Reads the entire CSV file. This method is automatically called by the
* {@link #next()} method if needed. However it is necessary to explicitly call
* it if the CSV headers need to be accessed (through the {@link #getHeaders()}
* method) prior to any call to next().
* {@link #next()} or {@link #getHeaders()} methods if needed.
*
* @throws IOException if any I/O error occurs while attempting to read the file
*/
@ -355,7 +389,7 @@ public class BatchReader {
CSVReader reader = new CSVReader(new FileReader(filename));
String[] line;
while ( (line = reader.readNext()) != null ) {
if ( line[0].charAt(0) == '#' )
if ( line[0].length() == 0 || line[0].charAt(0) == '#' )
continue;
if ( withHeaders && rows.size() == 0 && headers == null ) {

41
src/main/java/org/incenp/imagej/BinaryOperator.java

@ -0,0 +1,41 @@
/*
* Incenp.org ImageJ Plugins
* Copyright © 2020 Damien Goutte-Gattat
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the Gnu General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.incenp.imagej;
/**
* Binary operations that can be used when combining two images.
*/
public enum BinaryOperator {
AND,
NAND,
OR,
NOR,
XOR,
XNOR;
public static BinaryOperator fromString(String s) {
for ( BinaryOperator operator : BinaryOperator.values() ) {
if ( operator.name().equalsIgnoreCase(s) ) {
return operator;
}
}
return null;
}
}

455
src/main/java/org/incenp/imagej/ChannelMasker.java

@ -1,6 +1,6 @@
/*
* Incenp.org Plugins
* Copyright © 2019, 2020 Damien Goutte-Gattat
* Incenp.org ImageJ Plugins
* Copyright © 2019, 2020, 2021 Damien Goutte-Gattat
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -24,8 +24,6 @@ import java.util.regex.Pattern;
import ij.IJ;
import ij.ImagePlus;
import ij.process.AutoThresholder.Method;
import ij.process.Blitter;
import ij.process.ImageProcessor;
/**
@ -34,27 +32,24 @@ import ij.process.ImageProcessor;
* from a given source image. The generated image will contain as many channels
* as there are operations in the list.
* <p>
* Five different types of operations are supported:
* Four different types of operations are supported:
* <ul>
* <li>copying a channel verbatim from the source image;
* <li>creating a binary mask from one the channel of the source image;
* <li>applying a binary mask to one of the channel of the source image; the
* binary mask itself is also taken from a channel of the source image;
* <li>inverting a binary mask;
* <li>combining two binary masks with AND, OR, and XOR operators or their
* complement counterparts (NAND, NOR, and XNOR).
* <li>inverting a binary mask.
* </ul>
*/
public class ChannelMasker {
private static final Method[] methods = Method.values();
private static Pattern commandPattern = Pattern.compile("([0-9A-Z]):([A-Z]+)\\(([0-9A-Za-z;]*)\\)");
private static Pattern combinePattern = Pattern.compile("([0-9A-Z]+);(AND|NAND|OR|NOR|XOR|XNOR)");
private static final ThresholdingMethod[] methods = ThresholdingMethod.values();
private static final BinaryOperator[] operators = BinaryOperator.values();
private ChannelOperation[] operations;
private boolean excludeBlack;
private int maskingOptions;
private String channelOrder;
private ChannelMasker next;
/**
* Creates a new ChannelMasker object with the given list of operations. This
@ -66,41 +61,9 @@ public class ChannelMasker {
*/
public ChannelMasker(ChannelOperation[] operations) {
this.operations = operations;
this.excludeBlack = false;
this.channelOrder = null;
}
/**
* Creates a new ChannelMasker object. Same as
* {@link #ChannelMasker(ChannelOperation[])} but allows to specify how masks are
* to be interpreted.
*
* @param operations the list of operations to apply
* @param excludeBlack if true, mask operations use black pixels to exclude, not
* include, pixels in the resulting image
*/
public ChannelMasker(ChannelOperation[] operations, boolean excludeBlack) {
this.operations = operations;
this.excludeBlack = excludeBlack;
this.maskingOptions = Masking.CLOSE_OPEN_MASK;
this.channelOrder = null;
}
/**
* Creates a new ChannelMasker object. Same as
* {@link #ChannelMasker(ChannelOperation[], boolean)} but allows to specify the
* default channel order to use when applying the masker.
*
* @param operations the list of operations to apply
* @param excludeBlack if true, mask operations use black pixels to exclude, not
* include, pixels in the resulting imagee
* @param channelOrder a string indicating the order of channels in source
* images when applying the masker, as described in
* {@link #setChannelOrder(String)}
*/
public ChannelMasker(ChannelOperation[] operations, boolean excludeBlack, String channelOrder) {
this.operations = operations;
this.excludeBlack = excludeBlack;
this.channelOrder = channelOrder;
this.next = null;
}
/**
@ -134,6 +97,26 @@ public class ChannelMasker {
return channelOrder;
}
/**
* Sets the masking options used by this object. See the constant fields in
* {@link Masking} for available options. By default ChannelMasker objects uses
* the CLOSE_OPEN_MASK option.
*
* @param options the new masking options
*/
public void setMaskingOptions(int options) {
maskingOptions = options;
}
/**
* Gets the masking options currently used by this object.
*
* @return the current masking options
*/
public int getMaskingOptions() {
return maskingOptions;
}
/**
* Apply this ChannelMasker object to an image. This method will create a new
* image by performing all the operations defined in the objects operations
@ -171,12 +154,8 @@ public class ChannelMasker {
break;
case MASK:
Method method = methods[op.argument[0]];
ip = Helper.createMask(image.getProcessor(), method, excludeBlack);
break;
case MASKTHR:
ip = Helper.createMask(image.getProcessor(), op.argument[0], excludeBlack);
ip = Masking.createMask(image.getProcessor(), methods[op.argument[0]], op.argument[1],
maskingOptions);
break;
case INVERT:
@ -185,41 +164,13 @@ public class ChannelMasker {
break;
case APPLY:
BinaryOperator operator = operators[op.argument[op.argument.length - 1]];
src = image.getProcessor().duplicate();
image.setC(getChannel(op.argument[0], order));
ip = Helper.applyMask(src, image.getProcessor(), excludeBlack);
break;
case COMBINE:
ip = image.getProcessor().duplicate();
image.setC(getChannel(op.argument[0], order));
switch ( Helper.MaskOperator.values()[op.argument[1]] ) {
case AND:
ip.copyBits(image.getProcessor(), 0, 0, Blitter.AND);
break;
case NAND:
ip.copyBits(image.getProcessor(), 0, 0, Blitter.AND);
ip.invert();
break;
case OR:
ip.copyBits(image.getProcessor(), 0, 0, Blitter.OR);
break;
case NOR:
ip.copyBits(image.getProcessor(), 0, 0, Blitter.OR);
ip.invert();
break;
case XOR:
ip.copyBits(image.getProcessor(), 0, 0, Blitter.XOR);
break;
case XNOR:
ip.copyBits(image.getProcessor(), 0, 0, Blitter.XOR);
ip.invert();
break;
for ( int m = 0; m < op.argument.length - 1; m++ ) {
ip = src;
image.setC(getChannel(op.argument[m], order));
Masking.applyMask(ip, image.getProcessor(), operator,
maskingOptions | Masking.NO_DUPLICATE);
}
break;
}
@ -230,6 +181,10 @@ public class ChannelMasker {
}
}
if ( next != null ) {
result = next.apply(result, name, null);
}
return result;
}
@ -247,16 +202,28 @@ public class ChannelMasker {
return apply(image, name, null);
}
private int getChannel(int source, String order) {
int ch;
if ( order != null && (ch = order.indexOf(source)) != -1 ) {
ch += 1;
} else {
ch = source - '0';
}
return ch;
/**
* Sets a ChannelMasker to apply after the current one. When the {@link #apply}
* method is called, the specified ChannelMasker will be applied to the
* resulting image, and the output of that application is what will be returned.
* <p>
* The method returns the object it is called on, allowing to create a chain as
* follows:
*
* <pre>
* CM1.chain(CM2.chain(CM3))
* </pre>
* <p>
* When <code>CM1.apply(...)</code> is called, it will return the output of CM3
* applied to the output of CM2 applied to the output of CM1 applied to the
* original image.
*
* @param next the ChannelMasker object to apply after this one
* @return the current ChannelMasker object
*/
public ChannelMasker chain(ChannelMasker next) {
this.next = next;
return this;
}
/**
@ -265,18 +232,19 @@ public class ChannelMasker {
* comma-separated list of operation descriptors, each descriptor being one of
* the following:
* <ul>
* <li>"X:COPY()": copy verbatim channel number X of the source image;
* <li>"X:MASK(alg)": create a binary mask by applying the specified automatic
* thresholding algorithm to channel number X (see
* {@link ij.process.AutoThresholder.Method} for possible values of "alg");
* "alg" can also be an integer, in which case it is interpreted as the threshold
* to apply directly;
* <li>"X:APPLY(Y)": apply the binary mask found in channel number Y to channel
* number X;
* <li>"X:INVERT()": invert channel number X of the source image;
* <li>"X:COMBINE(Y;OP)": combine the mask found in channel number Y with the
* mask in channel number X, by applying the binary operator "OP" which can be
* AND, NAND, OR, NOR, XOR, or XNOR.
* <li>"X:COPY()": Copy verbatim channel number X of the source image.
* <li>"X:MASK(algo[,param])": Create a binary mask by applying the specified
* automatic thresholding algorithm to channel number X (see
* {@link ThresholdingMethod} for possible values of "algo"; values are matched
* in a case-insensitive way). The optional "param" value is either the actual
* threshold to use (when using the FIXED method) or the radius to consider when
* using one of the local thresholding methods (defaulting to 15 if no value is
* specified).
* <li>"X:APPLY(Y[,Z...],OP)": Apply the binary mask found in channel number Y,
* Z, etc. to channel number X, using the binary operator OP which can be one of
* AND, NAND, OR, NOR, XOR, or XNOR. If only one channel is specified, the
* operator may be omitted, in which case it defaults to AND.
* <li>"X:INVERT()": Invert channel number X of the source image.
* </ul>
* <p>
* For example, the command "2:MASK(MaxEntropy),1:COPY()" will create a
@ -289,7 +257,7 @@ public class ChannelMasker {
* The command "2:APPLY(1)" will create a ChannelMasker that, when applied to an
* image, will produce an output image containing a single channel, which will
* be result of applying the binary mask found in the first channel of the
* source image to the second channel of that same image.
* source image to the second channel of that same image, with the AND operator.
* <p>
* The order parameter, if non-null, is expected to be a list of letters.
* Channels in the command string may then be specified by a letter from that
@ -300,79 +268,36 @@ public class ChannelMasker {
* For example, if the order parameter is "GRB", then the command
* "R:MASK(MaxEntropy),G:COPY()" will be equivalent to the first example above.
*
* @param command a text description of the operations to perform
* @param order the default channel order specification, as described in
* {@link #setChannelOrder(String)} (may be null)
* @param excludeBlack if true, black pixels in masks are interpreted as
* excluding, instead of including, source pixels
* @param command a text description of the operations to perform
* @param order the default channel order specification, as described in
* {@link #setChannelOrder(String)} (may be null)
* @param options flags for masking operation (see constant fields in
* {@link Masking} for available flags)
* @return a ChannelMasker object
*/
public static ChannelMasker createMasker(String command, String order, boolean excludeBlack) {
public static ChannelMasker createMasker(String command, String order, int options) {
ArrayList<ChannelOperation> operations = new ArrayList<ChannelOperation>();
for ( String item : command.split(",") ) {
Matcher m = commandPattern.matcher(item.trim());
if ( !m.matches() )
throw new IllegalArgumentException("Invalid command");
ChannelOperation operation = new ChannelOperation();
operation.source = m.group(1).codePointAt(0);
operation.type = OperationType.fromString(m.group(2));
switch ( operation.type ) {
case COPY:
case INVERT:
operation.argument = null;
break;
case MASK:
operation.argument = new int[1];
try {
operation.argument[0] = Method.valueOf(m.group(3)).ordinal();
} catch ( IllegalArgumentException iae ) {
/*
* If the argument was not a valid thresholding algoritm, we accept MASK(x) as
* an alias for MASKTHR(x).
*/
operation.type = OperationType.MASKTHR;
operation.argument[0] = Integer.parseInt(m.group(3));
}
break;
case MASKTHR:
operation.argument = new int[1];
operation.argument[0] = Integer.parseInt(m.group(3));
break;
case APPLY:
operation.argument = new int[1];
operation.argument[0] = m.group(3).codePointAt(0);
break;
case COMBINE:
Matcher argm = combinePattern.matcher(m.group(3).trim());
if ( !argm.matches() )
throw new IllegalArgumentException("Invalid COMBINE command");
operation.argument = new int[2];
operation.argument[0] = argm.group(1).codePointAt(0);
operation.argument[1] = Helper.MaskOperator.fromString(argm.group(2)).ordinal();
break;
}
operations.add(operation);
ChannelOperation op;
Parser p = new Parser(command);
while ( (op = p.getNextOperation()) != null ) {
operations.add(op);
}
ChannelOperation[] opArray = new ChannelOperation[operations.size()];
operations.toArray(opArray);
return new ChannelMasker(opArray, excludeBlack, order);
ChannelMasker cm = new ChannelMasker(opArray);
cm.setChannelOrder(order);
cm.setMaskingOptions(options);
return cm;
}
/**
* Creates a ChannelMasker object from a text description of the operations.
* This method is similar to {@link #createMasker(String, String, boolean)} but
* always use black pixels in masks to include pixels from the source image.
* This method is similar to {@link #createMasker(String, String, int)} but uses
* the default option CLOSE_OPEN_MASK.
*
* @param command a text description of the operations to perform
* @param order the default channel order specification, as described in
@ -380,7 +305,7 @@ public class ChannelMasker {
* @return a ChannelMasker object
*/
public static ChannelMasker createMasker(String command, String order) {
return createMasker(command, order, false);
return createMasker(command, order, Masking.CLOSE_OPEN_MASK);
}
/**
@ -392,21 +317,7 @@ public class ChannelMasker {
* @return a ChannelMasker object
*/
public static ChannelMasker createMasker(String command) {
return createMasker(command, null, false);
}
/**
* Creates a ChannelMasker from a text description of the operations. This
* method is similar to {@link #createMasker(String, String, boolean)} but does
* not allow to set a default channel order.
*
* @param command a text description of the operations to perform
* @param excludeBlack if true, black pixels in masks are interpreted as
* excluding, instead of including, source pixels
* @return a ChannelMasker object
*/
public static ChannelMasker createMasker(String command, boolean excludeBlack) {
return createMasker(command, null, excludeBlack);
return createMasker(command, null, Masking.CLOSE_OPEN_MASK);
}
/**
@ -422,27 +333,7 @@ public class ChannelMasker {
* @return the resulting image
*/
public static ImagePlus applyMasker(ImagePlus image, String command, String name, String order) {
ChannelMasker masker = createMasker(command, null, false);
return masker.apply(image, name, order);
}
/**
* Apply a list of operations to an image. This is a convenience method to apply
* a list of operations (defined in a string as in the
* {@link #createMasker(String, String, boolean)} method directly without having
* to manipulate a ChannelMasker object.
*
* @param image the image to apply the operations to
* @param command the list of operations to apply
* @param name the name to give to the new image
* @param order the order of channels in the source image (may be null)
* @param excludeBlack if true, black pixels in masks are interpreted as
* excluding, instead of including, source pixels
* @return the resulting image
*/
public static ImagePlus applyMasker(ImagePlus image, String command, String name, String order,
boolean excludeBlack) {
ChannelMasker masker = createMasker(command, null, excludeBlack);
ChannelMasker masker = createMasker(command);
return masker.apply(image, name, order);
}
@ -451,25 +342,18 @@ public class ChannelMasker {
*/
enum OperationType {
COPY,
MASK,
MASKTHR,
APPLY,
INVERT,
COMBINE;
MASK,
APPLY;
static OperationType fromString(String s) {
if ( s.equalsIgnoreCase("mask") )
return MASK;
else if ( s.equalsIgnoreCase("maskthr") )
return MASKTHR;
else if ( s.equalsIgnoreCase("apply") )
return APPLY;
else if ( s.equalsIgnoreCase("invert") )
return INVERT;
else if ( s.equalsIgnoreCase("combine") )
return COMBINE;
else
return COPY;
for ( OperationType operation : OperationType.values() ) {
if ( operation.name().equalsIgnoreCase(s) ) {
return operation;
}
}
return null;
}
}
@ -481,4 +365,129 @@ public class ChannelMasker {
OperationType type;
int[] argument;
}
/*
* Translates a channel-specifying character into a channel index, according to
* the specified order.
*/
private int getChannel(int source, String order) {
int ch;
if ( order != null && (ch = order.indexOf(source)) != -1 ) {
ch += 1;
} else {
ch = source - '0';
}
return ch;
}
/*
* Helper class to parse a ChannelMasker text description as expected by the
* ChannelMasker.createMasker methods.
*/
private static class Parser {
private static Pattern commandPattern = Pattern.compile("[ \n\t\r]*([0-9A-Z]):([A-Z]+)\\(([0-9A-Za-z_, ]*)\\)");
String command;
boolean done;
public Parser(String command) {
this.command = command;
this.done = false;
}
public ChannelOperation getNextOperation() {
if ( done ) {
return null;
}
Matcher m = commandPattern.matcher(command);
if ( !m.find() ) {
throw new IllegalArgumentException(String.format("Invalid command: %s", command));
}
ChannelOperation operation = new ChannelOperation();
operation.source = m.group(1).codePointAt(0);
if ( (operation.type = OperationType.fromString(m.group(2))) == null ) {
throw new IllegalArgumentException(String.format("Invalid command: Unknown operation %s", m.group(2)));
}
String[] args = m.group(3).trim().split(" *, *");
switch ( operation.type ) {
case COPY:
case INVERT:
operation.argument = null;
break;
case MASK:
if ( args.length == 0 ) {
throw new IllegalArgumentException("Invalid MASK command: Parameter(s) expected");
}
operation.argument = new int[2];
ThresholdingMethod method = ThresholdingMethod.fromString(args[0]);
if ( method == null ) {
if ( args.length == 1 ) {
method = ThresholdingMethod.FIXED;
try {
operation.argument[1] = Integer.parseInt(args[0]);
} catch ( NumberFormatException nfe ) {
throw new IllegalArgumentException(
String.format("Invalid MASK command: Unknown method %s", args[0]));
}
} else {
throw new IllegalArgumentException(
String.format("Invalid MASK command: Unknown method %s", args[0]));
}
}
if ( args.length >= 2 ) {
try {
operation.argument[1] = Integer.parseInt(args[1]);
} catch ( NumberFormatException nfe ) {
throw new IllegalArgumentException(
String.format("Invalid MASK command: Invalid parameter %s", args[1]));
}
} else if ( method != ThresholdingMethod.FIXED ) {
operation.argument[1] = 15;
}
operation.argument[0] = method.ordinal();
break;
case APPLY:
if ( args.length == 0 ) {
throw new IllegalArgumentException("Invalid APPLY command: Parameter(s) expected");
}
operation.argument = new int[Math.max(2, args.length)];
operation.argument[0] = args[0].codePointAt(0);
if ( args.length >= 2 ) {
for ( int i = 1; i < args.length - 1; i++ ) {
operation.argument[i] = args[i].codePointAt(0);
}
BinaryOperator operator = BinaryOperator.fromString(args[args.length - 1]);
if ( operator == null ) {
throw new IllegalArgumentException(
String.format("Invalid APPLY command: Unknown operator %s", args[args.length - 1]));
}
operation.argument[args.length - 1] = operator.ordinal();
} else {
operation.argument[1] = BinaryOperator.AND.ordinal();
}
break;
}
command = command.substring(m.end());
if ( command.length() > 0 && command.charAt(0) == ',' ) {
command = command.substring(1);
} else {
done = true;
}
return operation;
}
}
}

313
src/main/java/org/incenp/imagej/Helper.java

@ -1,6 +1,6 @@
/*
* Incenp.org Plugins
* Copyright © 2019, 2020 Damien Goutte-Gattat
* Incenp.org ImageJ Plugins
* Copyright © 2019, 2020, 2021 Damien Goutte-Gattat
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -20,14 +20,13 @@ package org.incenp.imagej;
import java.awt.Window;
import ij.IJ;
import ij.ImagePlus;
import ij.WindowManager;
import ij.measure.Calibration;
import ij.measure.ResultsTable;
import ij.process.AutoThresholder;
import ij.process.Blitter;
import ij.process.ByteProcessor;
import ij.process.ImageProcessor;
import ij.process.ImageStatistics;
import ij.text.TextWindow;
/**
@ -36,233 +35,62 @@ import ij.text.TextWindow;
public class Helper {
/**
* Creates a binary mask using an automatic thresholding algorithm. Pixels above
* the automatic threshold will be black (0) in the generated mask.
*
* @param processor the image to create a mask from
* @param method the thresholding algorithm to use
* @return the resulting binary image
*/
public static ImageProcessor createMask(ImageProcessor processor, AutoThresholder.Method method) {
return createMask(processor, method, false);
}
/**
* Creates a binary mask using an automatic thresholding algorithm. This method
* is similar to {@link #createMask(ImageProcessor, AutoThresholder.Method)} but
* allows to invert the generated mask: if the excludeBlack parameter is true,
* pixels above the automated threshold will be white (0xFF) in the resulting
* mask.
*
* @param processor the image to create a mask from
* @param method the thresholding algorithm to use
* @param excludeBlack true to generate a mask with pixels above the threshold
* in white instead of black
* @return the resulting binary image
*/
public static ImageProcessor createMask(ImageProcessor processor, AutoThresholder.Method method,
boolean excludeBlack) {
int threshold = new AutoThresholder().getThreshold(method, processor.getHistogram());
return createMask(processor, threshold, excludeBlack);
}
/**
* Creates a binary mask using a fixed threshold. Apply a specified threshold to
* an image and create a binary mask from it.
*
* @param processor the image to create a mask from
* @param threshold the threshold value
* @param excludeBlack true to generate a mask with pixels above the pixels in
* white instead of black
* @return the resulting binary image
*/
public static ImageProcessor createMask(ImageProcessor processor, int threshold, boolean excludeBlack) {
if ( processor.getBitDepth() != 8 ) {
throw new IllegalArgumentException("8-bit image required");
}
processor.setThreshold(threshold, 255, ImageProcessor.NO_LUT_UPDATE);
ByteProcessor mask = (ByteProcessor) processor.createMask();
int background = 0;
if ( !excludeBlack ) {
mask.invert();
background = 255;
}
/* Closing. */
mask.dilate(1, background);
mask.erode(1, background);
/* Opening. */
mask.erode(1, background);
mask.dilate(1, background);
return mask;
}
/**
* Creates a binary mask using an automatic thresholding algorithm. This method
* is similar to
* {@link #createMask(ImageProcessor, ij.process.AutoThresholder.Method)} but
* works on a {@link ij.ImagePlus} object. If the image is a hyperstack, the
* mask will be created from the current slice.
*
* @param image the image to create a mask from
* @param method the thresholding algorithm to use
* @return the resulting binary image
* Counts all black pixels in a given image.
*
* @deprecated Use {@link #createMask(ImageProcessor, AutoThresholder.Method)}
* instead.
* @param processor the image to count black pixels from
* @return the total count of black pixels
*/
@Deprecated
public static ImageProcessor createMask(ImagePlus image, AutoThresholder.Method method) {
return Helper.createMask(image.getProcessor(), method, false);
}
public static int countBlackPixels(ImageProcessor processor) {
int nonBlackPixels = countPixels(processor, 1);
/**
* Applies a binary mask to an image. Given a source image and a binary mask,
* this method generates a third image in which pixels from the source image are
* copied only if the corresponding pixels from the mask are black (0); other
* pixels in the resulting image will be black.
*
* @param source the image to apply the mask to
* @param mask the binary mask
* @return the masked image
*/
public static ImageProcessor applyMask(ImageProcessor source, ImageProcessor mask) {
return applyMask(source, mask, false);
return (processor.getWidth() * processor.getHeight()) - nonBlackPixels;
}
/**
* Applies a binary mask to an image. This method is similar to
* {@link #applyMask(ImageProcessor, ImageProcessor)} but allows to invert how
* the mask is to be interpreted: if the excludeBlack parameter is true, pixels
* from the source image will be copied only if the corresponding pixels in the
* mask are white (0xFF).
* Counts all white pixels in a given image.
*
* @param source the image to apply the mask to
* @param mask the binary mask
* @param excludeBlack true to exclude black pixels instead of white pixels in
* the mask
* @return the masked image