Compare commits

...

73 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
Damien Goutte-Gattat aeb781d0e5 [maven-release-plugin] prepare release incenp-plugins-0.8.4 2 years ago
Damien Goutte-Gattat 7dc83415e8 Bump version number. 2 years ago
Damien Goutte-Gattat 1d6d127f91 [maven-release-plugin] prepare release incenp-plugins-0.8.3 2 years ago
Damien Goutte-Gattat 62c8252c98 Fix some Javadoc issues. 2 years ago
Damien Goutte-Gattat a6f68a8dcb [maven-release-plugin] rollback the release of incenp-plugins-0.8.3 2 years ago
Damien Goutte-Gattat 456e698aee [maven-release-plugin] prepare for next development iteration 2 years ago
Damien Goutte-Gattat 8f31c13049 [maven-release-plugin] prepare release incenp-plugins-0.8.3 2 years ago
Damien Goutte-Gattat fbf0136e42 Update NEWS file. 2 years ago
Damien Goutte-Gattat e6a1049b61 Update ChannelMasker documentation. 2 years ago
Damien Goutte-Gattat ce36579fe3 Delay translation of channel codes. 2 years ago
Damien Goutte-Gattat 9071b78fc5 Add the getResultsTable helper method. 2 years ago
Damien Goutte-Gattat ebe1bd0818 Add the MASKTHR operation to the ChannelMasker. 2 years ago
Damien Goutte-Gattat 27eaf468d4 Allow to invert the meaning of masks. 2 years ago
Damien Goutte-Gattat fbd703281a Make applying a binary mask slightly more efficient. 2 years ago
Damien Goutte-Gattat 528da5a2bd [maven-release-plugin] prepare for next development iteration 2 years ago
  1. 56
      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. 430
      src/main/java/org/incenp/imagej/ChannelMasker.java
  7. 279
      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

56
NEWS

@ -1,3 +1,59 @@
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
-------------------------------
* Speed up some masking operations.
* Allow to invert the meaning of black/white pixels in binary masks.
* Add the MASKTHR operation to the ChannelMasker.
* A single ChannelMasker can now be applied to images with different
channel orders.
Changes in incenp-plugins-0.8.2
-------------------------------

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.2</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.2</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;
}
}

430
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,25 +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 int maskingOptions;
private String channelOrder;
private ChannelMasker next;
/**
* Creates a new ChannelMasker object with the given list of operations. This
@ -64,6 +61,60 @@ public class ChannelMasker {
*/
public ChannelMasker(ChannelOperation[] operations) {
this.operations = operations;
this.maskingOptions = Masking.CLOSE_OPEN_MASK;
this.channelOrder = null;
this.next = null;
}
/**
* Sets the default channel order specification. A channel order specification
* is a string of one-letter channel codes; those channel codes may then be used
* when calling the {@link #createMasker} method to refer to channels without
* knowing the position of the channels in the images the masker will be applied
* to.
* <p>
* For example, a masker object created with the "A:MASK(Huang),B:COPY()"
* command and applied to an image with channel order "ABC" will perform the
* MASK operation to the first channel and the COPY operation to the second
* channel; the same masker applied to an image with channel order "CADB" will
* perform the MASK operation on the second channel and the COPY operation to
* the fourth channel.
*
* @param order the default channel order to use when applying the masker
*/
public void setChannelOrder(String order) {
channelOrder = order;
}
/**
* Gets the default channel order specification.
*
* @return the default channel order used by this masker
*
* @see #setChannelOrder(String)
*/
public String getChannelOrder() {
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;
}
/**
@ -73,12 +124,18 @@ public class ChannelMasker {
*
* @param image the source image to apply the operations to
* @param name the name to give to the new image
* @param order the order of channels in the source image (may be null, in which
* case the default channel order will be used)
* @return the resulting image
*/
public ImagePlus apply(ImagePlus image, String name) {
public ImagePlus apply(ImagePlus image, String name, String order) {
int nslices = image.getNSlices();
int nframes = image.getNFrames();
if ( order == null || order == "" ) {
order = channelOrder;
}
ImagePlus result = IJ.createHyperStack(name, image.getWidth(), image.getHeight(), operations.length, nslices,
nframes, 8);
result.setCalibration(image.getCalibration());
@ -87,7 +144,7 @@ public class ChannelMasker {
for ( int j = 0; j < nslices; j++ ) {
for ( int k = 0; k < operations.length; k++ ) {
ChannelOperation op = operations[k];
image.setPosition(op.source, j + 1, i + 1);
image.setPosition(getChannel(op.source, order), j + 1, i + 1);
ImageProcessor ip = null;
ImageProcessor src = null;
@ -97,8 +154,8 @@ public class ChannelMasker {
break;
case MASK:
Method method = methods[op.argument[0]];
ip = Helper.createMask(image.getProcessor(), method);
ip = Masking.createMask(image.getProcessor(), methods[op.argument[0]], op.argument[1],
maskingOptions);
break;
case INVERT:
@ -107,41 +164,13 @@ public class ChannelMasker {
break;
case APPLY:
BinaryOperator operator = operators[op.argument[op.argument.length - 1]];
src = image.getProcessor().duplicate();
image.setC(op.argument[0]);
ip = Helper.applyMask(src, image.getProcessor());
break;
case COMBINE:
ip = image.getProcessor().duplicate();
image.setC(op.argument[0]);
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;
}
@ -152,23 +181,49 @@ public class ChannelMasker {
}
}
if ( next != null ) {
result = next.apply(result, name, null);
}
return result;
}
private static int channelFromString(String channel, String order) {
int ch;
if ( order != null && (ch = order.indexOf(channel)) != -1 ) {
ch += 1;
} else {
try {
ch = Integer.parseInt(channel);
} catch ( NumberFormatException nfe ) {
throw new IllegalArgumentException("Invalid command");
}
}
/**
* Apply this ChannelMasker object to an image. This method will create a new
* image by performing all the operations defined in the objects operations
* list. If the ChannelMasker uses channel codes instead of channel indexes,
* they will be translated using the default channel order specification.
*
* @param image the source image to apply the operations to
* @param name the name to give to the new image
* @return the resulting image
*/
public ImagePlus apply(ImagePlus image, String name) {
return apply(image, name, null);
}
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;
}
/**
@ -177,16 +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";
* <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
@ -199,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
@ -211,68 +269,55 @@ public class ChannelMasker {
* "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 a list of letters giving the order of channels in the source
* image (may be null)
* @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) {
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 = channelFromString(m.group(1), order);
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];
operation.argument[0] = Method.valueOf(m.group(3)).ordinal();
break;
case APPLY:
operation.argument = new int[1];
operation.argument[0] = channelFromString(m.group(3), order);
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] = channelFromString(argm.group(1), order);
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);
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, 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
* {@link #setChannelOrder(String)} (may be null)
* @return a ChannelMasker object
*/
public static ChannelMasker createMasker(String command, String order) {
return createMasker(command, order, Masking.CLOSE_OPEN_MASK);
}
/**
* Creates a ChannelMasker object from a text description of the operations.
* This method is similar to {@link #createMasker(String, String)} but does not
* allow to use letters to identify channels.
* allow to set a default channel order.
*
* @param command a text description of the operations to perform
* @return a ChannelMasker object
*/
public static ChannelMasker createMasker(String command) {
return createMasker(command, null);
return createMasker(command, null, Masking.CLOSE_OPEN_MASK);
}
/**
@ -288,8 +333,8 @@ public class ChannelMasker {
* @return the resulting image
*/
public static ImagePlus applyMasker(ImagePlus image, String command, String name, String order) {
ChannelMasker masker = createMasker(command, order);
return masker.apply(image, name);
ChannelMasker masker = createMasker(command);
return masker.apply(image, name, order);
}
/**
@ -297,22 +342,18 @@ public class ChannelMasker {
*/
enum OperationType {
COPY,
MASK,
APPLY,
INVERT,
COMBINE;
MASK,
APPLY;
static OperationType fromString(String s) {
if ( s.equalsIgnoreCase("mask") )
return MASK;
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;
}
}
@ -324,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;
}
}
}

279
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
@ -18,12 +18,16 @@
package org.incenp.imagej;
import java.awt.Window;
import ij.IJ;
import ij.ImagePlus;
import ij.WindowManager;
import ij.measure.Calibration;
import ij.process.AutoThresholder;
import ij.process.Blitter;
import ij.process.ByteProcessor;
import ij.measure.ResultsTable;
import ij.process.ImageProcessor;
import ij.process.ImageStatistics;
import ij.text.TextWindow;
/**
* A class of static helper methods.
@ -31,174 +35,62 @@ import ij.process.ImageProcessor;
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.
* Counts all black pixels in a given image.
*
* @param processor the image to create a mask from
* @param method the thresholding algorithm to use
* @return the resulting binary image
* @param processor the image to count black pixels from
* @return the total count of black pixels
*/
public static ImageProcessor createMask(ImageProcessor processor, AutoThresholder.Method method) {
if ( processor.getBitDepth() != 8 ) {
throw new IllegalArgumentException("8-bit image required");
}
int thresholder = new AutoThresholder().getThreshold(method, processor.getHistogram());
processor.setThreshold(thresholder, 255, ImageProcessor.NO_LUT_UPDATE);
ByteProcessor mask = (ByteProcessor) processor.createMask();
mask.invert();
/* Closing. */
mask.dilate(1, 255);
mask.erode(1, 255);
/* Opening. */
mask.erode(1, 255);
mask.dilate(1, 255);
public static int countBlackPixels(ImageProcessor processor) {
int nonBlackPixels = countPixels(processor, 1);
return mask;
return (processor.getWidth() * processor.getHeight()) - nonBlackPixels;
}
/**
* 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.
* Counts all white pixels in a given image.
*
* @param image the image to create a mask from
* @param method the thresholding algorithm to use
* @return the resulting binary image
* @param processor the image to count white pixels from
* @return the total count of white pixels
*/
public static ImageProcessor createMask(ImagePlus image, AutoThresholder.Method method) {
return Helper.createMask(image.getProcessor(), method);
public static int countWhitePixels(ImageProcessor processor) {
return countPixels(processor, (1 << processor.getBitDepth()) - 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.
* Counts all non-black pixels in a given image.
*
* @param source the image to apply the mask to
* @param mask the binary mask
* @return the masked image
* @param processor the image to count pixels from
* @return the total count of non-black pixels
*/
public static ImageProcessor applyMask(ImageProcessor source, ImageProcessor mask) {
if ( mask.getBitDepth() != 8 ) {
throw new IllegalArgumentException("8-bit image required for mask");
}
ImageProcessor result = source.duplicate();
ImageProcessor tmp = mask.duplicate();
tmp.invert();
tmp.subtract(254);
result.copyBits(tmp, 0, 0, Blitter.MULTIPLY);
return result;
public static int countPixels(ImageProcessor processor) {
return countPixels(processor, 1);
}
/**
* Combines two binary masks. Given two binary masks, this method generates a
* third image that is the result of the specified binary operation.
*
* @param mask1 the first mask to combine
* @param mask2 the second mask to combine
* @param operator the binary operator
* @return the combined mask
*/
public static ImageProcessor combineMasks(ImageProcessor mask1, ImageProcessor mask2, MaskOperator operator) {
if ( mask1.getBitDepth() != 8 || mask2.getBitDepth() != 8 ) {
throw new IllegalArgumentException("8-bit image required for mask");
}
ImageProcessor result = mask1.duplicate();
switch ( operator ) {
case AND:
case NAND:
result.copyBits(mask2, 0, 0, Blitter.AND);
if ( operator == MaskOperator.NAND ) {
result.invert();
}
break;
case OR: