io.github.mianalysis.mia.module.images.process.threshold.GlobalAutoThreshold Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mia-modules Show documentation
Show all versions of mia-modules Show documentation
ModularImageAnalysis (MIA) is an ImageJ plugin which provides a modular framework for assembling image and object analysis workflows. Detected objects can be transformed, filtered, measured and related. Analysis workflows are batch-enabled by default, allowing easy processing of high-content datasets.
package io.github.mianalysis.mia.module.images.process.threshold;
import org.scijava.Priority;
import org.scijava.plugin.Plugin;
import ij.ImagePlus;
import ij.gui.Roi;
import ij.gui.ShapeRoi;
import ij.plugin.Duplicator;
import ij.process.AutoThresholder;
import ij.process.ImageProcessor;
import io.github.mianalysis.mia.module.Categories;
import io.github.mianalysis.mia.module.Category;
import io.github.mianalysis.mia.module.Module;
import io.github.mianalysis.mia.module.Modules;
import io.github.mianalysis.mia.module.images.process.ImageTypeConverter;
import io.github.mianalysis.mia.module.images.process.InvertIntensity;
import io.github.mianalysis.mia.object.Measurement;
import io.github.mianalysis.mia.object.Obj;
import io.github.mianalysis.mia.object.Objs;
import io.github.mianalysis.mia.object.Workspace;
import io.github.mianalysis.mia.object.image.Image;
import io.github.mianalysis.mia.object.image.ImageFactory;
import io.github.mianalysis.mia.object.parameters.BooleanP;
import io.github.mianalysis.mia.object.parameters.ChoiceP;
import io.github.mianalysis.mia.object.parameters.InputImageP;
import io.github.mianalysis.mia.object.parameters.InputObjectsP;
import io.github.mianalysis.mia.object.parameters.OutputImageP;
import io.github.mianalysis.mia.object.parameters.Parameters;
import io.github.mianalysis.mia.object.parameters.SeparatorP;
import io.github.mianalysis.mia.object.parameters.choiceinterfaces.BinaryLogicInterface;
import io.github.mianalysis.mia.object.parameters.text.DoubleP;
import io.github.mianalysis.mia.object.refs.ImageMeasurementRef;
import io.github.mianalysis.mia.object.refs.collections.ImageMeasurementRefs;
import io.github.mianalysis.mia.object.refs.collections.MetadataRefs;
import io.github.mianalysis.mia.object.refs.collections.ObjMeasurementRefs;
import io.github.mianalysis.mia.object.refs.collections.ObjMetadataRefs;
import io.github.mianalysis.mia.object.refs.collections.ParentChildRefs;
import io.github.mianalysis.mia.object.refs.collections.PartnerRefs;
import io.github.mianalysis.mia.object.system.Status;
/**
* Created by sc13967 on 06/06/2017.
*/
/**
* Binarise an image in the workspace such that the output only has pixel values
* of 0 and 255. Uses the built-in ImageJ global
* auto-thresholding
* algorithms.
*
* Note: Currently only works on 8-bit images. Images with other bit depths will
* be automatically converted to 8-bit based on the "Fill target range
* (normalise)" scaling method from the "Image type converter" module.
*/
@Plugin(type = Module.class, priority = Priority.LOW, visible = true)
public class GlobalAutoThreshold extends Module {
/**
*
*/
public static final String INPUT_SEPARATOR = "Image input/output";
/**
* Image to apply threshold to.
*/
public static final String INPUT_IMAGE = "Input image";
/**
* Controls if the threshold is applied to the input image or only calculated
* and stored as a measurement:
*
* - "Calculate and apply" Calculate the threshold and apply it to the input
* image. Whether the binarised image updates the input image or is saved as a
* separate image to the workspace is controlled by the "Apply to input image"
* parameter. In this mode the calculated threshold is still stored as a
* measurement of the input image.
* - "Calculate only" Calculate the threshold, but do not apply it to the
* input image. The calculated threshold is only stored as a measurement of the
* input image.
*
*/
public static final String OUTPUT_MODE = "Output mode";
/**
* Select if the threshold should be applied directly to the input image, or if
* it should be applied to a duplicate, then stored as a different image in the
* workspace.
*/
public static final String APPLY_TO_INPUT = "Apply to input image";
/**
* Name of the output image created during the thresholding process. This image
* will be added to the workspace.
*/
public static final String OUTPUT_IMAGE = "Output image";
/**
*
*/
public static final String THRESHOLD_SEPARATOR = "Threshold controls";
/**
* Global thresholding algorithm to use. Choices are: Huang, Intermodes,
* IsoData, Li, MaxEntropy, Mean, MinError, Minimum, Moments, Otsu, Percentile,
* RenyiEntropy, Shanbhag, Triangle, Yen.
*/
public static final String ALGORITHM = "Algorithm";
/**
* Prior to application of automatically-calculated thresholds the threshold
* value is multiplied by this value. This allows the threshold to be
* systematically increased or decreased. For example, a "Threshold multiplier"
* of 0.9 applied to an automatically-calculated threshold of 200 will see the
* image thresholded at the level 180.
*/
public static final String THRESHOLD_MULTIPLIER = "Threshold multiplier";
/**
* Limit the lowest threshold that can be applied to the image. This is used to
* prevent unintentional segmentation of an image containing only background
* (i.e. no features present).
*/
public static final String USE_LOWER_THRESHOLD_LIMIT = "Use lower threshold limit";
/**
* Lowest absolute threshold value that can be applied.
*/
public static final String LOWER_THRESHOLD_LIMIT = "Lower threshold limit";
/**
* Controls whether objects are considered to be white (255 intensity) on a
* black (0 intensity) background, or black on a white background.
*/
public static final String BINARY_LOGIC = "Binary logic";
/**
*
*/
public static final String MEASURE_ON_OBJECTS = "Measure on objects";
/**
*
*/
public static final String INPUT_OBJECTS = "Input objects";
public GlobalAutoThreshold(Modules modules) {
super("Global auto-threshold", modules);
}
public interface OutputModes {
String CALCULATE_AND_APPLY = "Calculate and apply";
String CALCULATE_ONLY = "Calculate only";
String[] ALL = new String[] { CALCULATE_AND_APPLY, CALCULATE_ONLY };
}
public interface BinaryLogic extends BinaryLogicInterface {
}
public interface Algorithms {
String HUANG = "Huang";
String INTERMODES = "Intermodes";
String ISO_DATA = "IsoData";
String LI = "Li";
String MAX_ENTROPY = "MaxEntropy";
String MEAN = "Mean";
String MIN_ERROR = "MinError";
String MINIMUM = "Minimum";
String MOMENTS = "Moments";
String OTSU = "Otsu";
String PERCENTILE = "Percentile";
String RENYI_ENTROPY = "RenyiEntropy";
String SHANBHAG = "Shanbhag";
String TRIANGLE = "Triangle";
String YEN = "Yen";
String[] ALL = new String[] { HUANG, INTERMODES, ISO_DATA, LI, MAX_ENTROPY, MEAN, MIN_ERROR, MINIMUM, MOMENTS,
OTSU, PERCENTILE, RENYI_ENTROPY, SHANBHAG, TRIANGLE, YEN };
}
public interface Measurements {
String GLOBAL_VALUE = "GLOBAL";
}
public static String getFullName(String measurement, String method) {
return "THRESHOLD // " + measurement + " " + method;
}
public int calculateThreshold(ImagePlus inputImagePlus, String algorithm, double thrMult, boolean useLowerLim,
double lowerLim, Objs inputObjects) {
// Image must be 8-bit
if (inputImagePlus.getBitDepth() != 8) {
inputImagePlus = inputImagePlus.duplicate();
ImageTypeConverter.process(inputImagePlus, 8, ImageTypeConverter.ScalingModes.FILL);
}
// Compiling stack histogram. This is stored as long to prevent the Integer
// overflowing.
long[] histogram = null;
int count = 0;
int total = inputImagePlus.getNChannels() * inputImagePlus.getNSlices() * inputImagePlus.getNFrames();
for (int z = 1; z <= inputImagePlus.getNSlices(); z++) {
for (int c = 1; c <= inputImagePlus.getNChannels(); c++) {
for (int t = 1; t <= inputImagePlus.getNFrames(); t++) {
inputImagePlus.setPosition(c, z, t);
ImageProcessor ipr = inputImagePlus.getProcessor();
if (inputObjects != null)
ipr.setRoi(getRoi(inputObjects, t, z));
int[] tempHist = ipr.getHistogram();
if (inputObjects != null)
inputImagePlus.killRoi();
if (histogram == null)
histogram = new long[tempHist.length];
for (int i = 0; i < histogram.length; i++)
histogram[i] = histogram[i] + tempHist[i];
writeProgressStatus(++count, total, "images");
}
}
}
if (histogram == null)
return 0;
// Calculating the maximum value in any bin
long maxVal = Long.MIN_VALUE;
for (long val : histogram)
maxVal = Math.max(maxVal, val);
if (maxVal <= Integer.MAX_VALUE)
maxVal = Integer.MAX_VALUE;
// Normalising histogram, so it will fit in an int[]
int[] normHist = new int[histogram.length];
for (int i = 0; i < histogram.length; i++)
normHist[i] = (int) Math.round(Integer.MAX_VALUE * ((double) histogram[i]) / ((double) maxVal));
// Applying the threshold
int threshold = new AutoThresholder().getThreshold(algorithm, normHist);
// Applying limits, where applicable
if (useLowerLim && threshold < lowerLim)
threshold = (int) Math.round(lowerLim);
// Applying threshold scaling
threshold = (int) Math.round(threshold * thrMult);
return threshold;
}
public static Roi getRoi(Objs inputObjects, int t, int z) {
ShapeRoi roi = null;
for (Obj inputObject : inputObjects.values()) {
if (inputObject.getT() != t - 1)
continue;
double[][] extents = inputObject.getExtents(true, false);
if ((z - 1) < extents[2][0] || (z - 1) > extents[2][1])
continue;
Roi currRoi = inputObject.getRoi(z - 1);
if (currRoi == null)
continue;
if (roi == null)
roi = new ShapeRoi(currRoi);
else
roi.xor(new ShapeRoi(currRoi));
}
return roi;
}
public void addMeasurements(Image image, double threshold, String algorithm) {
String measurementName = getFullName(Measurements.GLOBAL_VALUE, algorithm);
image.addMeasurement(new Measurement(measurementName, threshold));
}
@Override
public Category getCategory() {
return Categories.IMAGES_PROCESS_THRESHOLD;
}
@Override
public String getVersionNumber() {
return "1.0.0";
}
@Override
public String getDescription() {
return "Binarise an image in the workspace such that the output only has pixel values of 0 and 255. Uses the "
+ "built-in ImageJ global auto-thresholding algorithms."
+ "
"
+ "
Note: Currently only works on 8-bit images. Images with other bit depths will be automatically "
+ "converted to 8-bit based on the \"" + ImageTypeConverter.ScalingModes.FILL
+ "\" scaling method from the " + "\"" + new ImageTypeConverter(null).getName() + "\" module.";
}
@Override
public Status process(Workspace workspace) {
// Getting input image
String inputImageName = parameters.getValue(INPUT_IMAGE, workspace);
Image inputImage = workspace.getImages().get(inputImageName);
ImagePlus inputImagePlus = inputImage.getImagePlus();
// Getting parameters
String outputMode = parameters.getValue(OUTPUT_MODE, workspace);
boolean applyToInput = parameters.getValue(APPLY_TO_INPUT, workspace);
String algorithm = parameters.getValue(ALGORITHM, workspace);
double thrMult = parameters.getValue(THRESHOLD_MULTIPLIER, workspace);
String binaryLogic = parameters.getValue(BINARY_LOGIC, workspace);
boolean useLowerLim = parameters.getValue(USE_LOWER_THRESHOLD_LIMIT, workspace);
double lowerLim = parameters.getValue(LOWER_THRESHOLD_LIMIT, workspace);
boolean measureOnObjects = parameters.getValue(MEASURE_ON_OBJECTS, workspace);
String inputObjectsName = parameters.getValue(INPUT_OBJECTS, workspace);
Objs inputObjects = measureOnObjects ? workspace.getObjects(inputObjectsName) : null;
int threshold = 0;
// Calculating the threshold based on the selected algorithm
writeStatus("Applying " + algorithm + " threshold (multiplier = " + thrMult + " x)");
threshold = calculateThreshold(inputImagePlus, algorithm, thrMult, useLowerLim, lowerLim, inputObjects);
if (outputMode.equals(OutputModes.CALCULATE_AND_APPLY)) {
// If applying to a new image, the input image is duplicated
if (!applyToInput)
inputImagePlus = new Duplicator().run(inputImagePlus);
// Image must be 8-bit
if (inputImagePlus.getBitDepth() != 8)
ImageTypeConverter.process(inputImagePlus, 8, ImageTypeConverter.ScalingModes.FILL);
// Applying threshold
ManualThreshold.applyThreshold(inputImagePlus, threshold);
if (binaryLogic.equals(BinaryLogic.WHITE_BACKGROUND))
InvertIntensity.process(inputImagePlus);
// If the image is being saved as a new image, adding it to the workspace
if (applyToInput) {
inputImage.setImagePlus(inputImagePlus);
addMeasurements(inputImage, threshold, algorithm);
if (showOutput)
inputImage.show();
if (showOutput)
inputImage.showMeasurements(this);
} else {
String outputImageName = parameters.getValue(OUTPUT_IMAGE, workspace);
Image outputImage = ImageFactory.createImage(outputImageName, inputImagePlus);
workspace.addImage(outputImage);
addMeasurements(outputImage, threshold, algorithm);
if (showOutput)
outputImage.show();
if (showOutput)
outputImage.showMeasurements(this);
}
} else {
addMeasurements(inputImage, threshold, algorithm);
if (showOutput)
inputImage.showMeasurements(this);
}
return Status.PASS;
}
@Override
protected void initialiseParameters() {
parameters.add(new SeparatorP(INPUT_SEPARATOR, this));
parameters.add(new InputImageP(INPUT_IMAGE, this));
parameters.add(new ChoiceP(OUTPUT_MODE, this, OutputModes.CALCULATE_AND_APPLY, OutputModes.ALL));
parameters.add(new BooleanP(APPLY_TO_INPUT, this, true));
parameters.add(new OutputImageP(OUTPUT_IMAGE, this));
parameters.add(new SeparatorP(THRESHOLD_SEPARATOR, this));
parameters.add(new ChoiceP(ALGORITHM, this, Algorithms.HUANG, Algorithms.ALL));
parameters.add(new DoubleP(THRESHOLD_MULTIPLIER, this, 1.0));
parameters.add(new BooleanP(USE_LOWER_THRESHOLD_LIMIT, this, false));
parameters.add(new DoubleP(LOWER_THRESHOLD_LIMIT, this, 0.0));
parameters.add(new ChoiceP(BINARY_LOGIC, this, BinaryLogic.BLACK_BACKGROUND, BinaryLogic.ALL));
parameters.add(new BooleanP(MEASURE_ON_OBJECTS, this, false));
parameters.add(new InputObjectsP(INPUT_OBJECTS, this));
addParameterDescriptions();
}
@Override
public Parameters updateAndGetParameters() {
Workspace workspace = null;
Parameters returnedParameters = new Parameters();
returnedParameters.add(parameters.getParameter(INPUT_SEPARATOR));
returnedParameters.add(parameters.getParameter(INPUT_IMAGE));
returnedParameters.add(parameters.getParameter(OUTPUT_MODE));
if (parameters.getValue(OUTPUT_MODE, workspace).equals(OutputModes.CALCULATE_AND_APPLY)) {
returnedParameters.add(parameters.getParameter(APPLY_TO_INPUT));
if (!(boolean) parameters.getValue(APPLY_TO_INPUT, workspace)) {
returnedParameters.add(parameters.getParameter(OUTPUT_IMAGE));
}
}
returnedParameters.add(parameters.getParameter(THRESHOLD_SEPARATOR));
returnedParameters.add(parameters.getParameter(THRESHOLD_MULTIPLIER));
returnedParameters.add(parameters.getParameter(ALGORITHM));
returnedParameters.add(parameters.getParameter(USE_LOWER_THRESHOLD_LIMIT));
if ((boolean) parameters.getValue(USE_LOWER_THRESHOLD_LIMIT, workspace))
returnedParameters.add(parameters.getParameter(LOWER_THRESHOLD_LIMIT));
returnedParameters.add(parameters.getParameter(BINARY_LOGIC));
returnedParameters.add(parameters.getParameter(MEASURE_ON_OBJECTS));
if ((boolean) parameters.getValue(MEASURE_ON_OBJECTS, workspace))
returnedParameters.add(parameters.getParameter(INPUT_OBJECTS));
return returnedParameters;
}
@Override
public ImageMeasurementRefs updateAndGetImageMeasurementRefs() {
Workspace workspace = null;
ImageMeasurementRefs returnedRefs = new ImageMeasurementRefs();
String imageName = "";
switch ((String) parameters.getValue(OUTPUT_MODE, workspace)) {
case OutputModes.CALCULATE_AND_APPLY:
if ((boolean) parameters.getValue(APPLY_TO_INPUT, workspace)) {
imageName = parameters.getValue(INPUT_IMAGE, workspace);
} else {
imageName = parameters.getValue(OUTPUT_IMAGE, workspace);
}
break;
case OutputModes.CALCULATE_ONLY:
imageName = parameters.getValue(INPUT_IMAGE, workspace);
break;
}
String method = parameters.getValue(ALGORITHM, workspace);
String measurementName = getFullName(Measurements.GLOBAL_VALUE, method);
ImageMeasurementRef reference = imageMeasurementRefs.getOrPut(measurementName);
reference.setImageName(imageName);
reference.setDescription(
"Threshold value applied to the image during binarisation. Specified in intensity units.");
returnedRefs.add(reference);
return returnedRefs;
}
@Override
public ObjMeasurementRefs updateAndGetObjectMeasurementRefs() {
return null;
}
@Override
public ObjMetadataRefs updateAndGetObjectMetadataRefs() {
return null;
}
@Override
public MetadataRefs updateAndGetMetadataReferences() {
return null;
}
@Override
public ParentChildRefs updateAndGetParentChildRefs() {
return null;
}
@Override
public PartnerRefs updateAndGetPartnerRefs() {
return null;
}
@Override
public boolean verify() {
return true;
}
void addParameterDescriptions() {
parameters.get(INPUT_IMAGE).setDescription("Image to apply threshold to.");
parameters.get(OUTPUT_MODE).setDescription(
"Controls if the threshold is applied to the input image or only calculated and stored as a measurement:
"
+ "- \"" + OutputModes.CALCULATE_AND_APPLY
+ "\" Calculate the threshold and apply it to the input image. Whether the binarised image updates the input image or is saved as a separate image to the workspace is controlled by the \""
+ APPLY_TO_INPUT
+ "\" parameter. In this mode the calculated threshold is still stored as a measurement of the input image.
"
+ "- \"" + OutputModes.CALCULATE_ONLY
+ "\" Calculate the threshold, but do not apply it to the input image. The calculated threshold is only stored as a measurement of the input image.
");
parameters.get(APPLY_TO_INPUT).setDescription(
"Select if the threshold should be applied directly to the input image, or if it should be applied to a duplicate, then stored as a different image in the workspace.");
parameters.get(OUTPUT_IMAGE).setDescription(
"Name of the output image created during the thresholding process. This image will be added to the workspace.");
parameters.get(ALGORITHM).setDescription(
"Global thresholding algorithm to use. Choices are: " + String.join(", ", Algorithms.ALL) + ".");
parameters.get(THRESHOLD_MULTIPLIER).setDescription(
"Prior to application of automatically-calculated thresholds the threshold value is multiplied by this value. This allows the threshold to be systematically increased or decreased. For example, a \""
+ THRESHOLD_MULTIPLIER
+ "\" of 0.9 applied to an automatically-calculated threshold of 200 will see the image thresholded at the level 180.");
parameters.get(USE_LOWER_THRESHOLD_LIMIT).setDescription(
"Limit the lowest threshold that can be applied to the image. This is used to prevent unintentional segmentation of an image containing only background (i.e. no features present).");
parameters.get(LOWER_THRESHOLD_LIMIT).setDescription("Lowest absolute threshold value that can be applied.");
parameters.get(BINARY_LOGIC).setDescription(BinaryLogicInterface.getDescription());
}
}