package gdsc.threshold;
import gdsc.UsageTracker;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.plugin.filter.*;
import ij.plugin.*;
// AutoLocalThreshold segmentation
// Following the guidelines at http://pacific.mpi-cbg.de/wiki/index.php/PlugIn_Design_Guidelines
// ImageJ plugin by G. Landini at bham. ac. uk
// 1.0 15/Apr/2009
// 1.1 01/Jun/2009
// 1.2 25/May/2010
public class Auto_Local_Threshold implements PlugIn {
private static final String TITLE = "Auto Local Threshold";
/** Ask for parameters and then execute.*/
public void run(String arg) {
UsageTracker.recordPlugin(this.getClass(), arg);
// 1 - Obtain the currently active image:
ImagePlus imp = IJ.getImage();
if (null == imp){
IJ.showMessage("There must be at least one image open");
return;
}
if (imp.getBitDepth()!=8) {
IJ.showMessage("Error", "Only 8-bit images are supported");
return;
}
// 2 - Ask for parameters:
GenericDialog gd = new GenericDialog(TITLE);
String [] methods={"Try all", "Bernsen", "Mean", "Median", "MidGrey", "Niblack", "Sauvola"};
gd.addMessage("Auto Local Threshold v1.2");
gd.addChoice("Method", methods, methods[0]);
gd.addNumericField ("Radius", 15, 0);
gd.addMessage ("Special parameters (if different from default)");
gd.addNumericField ("Parameter_1", 0, 0);
gd.addNumericField ("Parameter_2", 0, 0);
gd.addCheckbox("White objects on black background",true);
if (imp.getStackSize()>1) {
gd.addCheckbox("Stack",false);
}
gd.addMessage("Thresholded result is always shown in white [255].");
gd.showDialog();
if (gd.wasCanceled()) return;
// 3 - Retrieve parameters from the dialog
String myMethod= gd.getNextChoice ();
int radius = (int) gd.getNextNumber();
double par1 = (double) gd.getNextNumber();
double par2 = (double) gd.getNextNumber();
boolean doIwhite = gd.getNextBoolean ();
boolean doIstack=false;
int stackSize = imp.getStackSize();
if (stackSize>1)
doIstack = gd.getNextBoolean ();
// 4 - Execute!
//long start = System.currentTimeMillis();
if(myMethod.equals("Try all")){
ImageProcessor ip = imp.getProcessor();
int xe = ip.getWidth();
int ye = ip.getHeight();
int ml = methods.length;
ImagePlus imp2, imp3;
ImageStack tstack=null, stackNew;
if (stackSize>1 && doIstack){
if (stackSize>25) {
YesNoCancelDialog d = new YesNoCancelDialog(IJ.getInstance(),"Auto Local Threshold", "You might run out of memory.\n \nDisplay "+stackSize+" slices?\n \n \'No\' will process without display and\noutput results to the log window.");
if (!d.yesPressed()){
// doIlog=true; //will show in the log window
}
if (d.cancelPressed())
return;
}
for (int j=1; j<=stackSize; j++){
imp.setSlice(j);
ip = imp.getProcessor();
tstack= new ImageStack(xe,ye);
for (int k=1; k<ml;k++)
tstack.addSlice(methods[k], ip.duplicate());
imp2 = new ImagePlus("Auto Threshold", tstack);
imp2.updateAndDraw();
for (int k=1; k<ml;k++){
imp2.setSlice(k);
exec(imp2, methods[k], radius, par1, par2, doIwhite );
}
//if (doItAnyway){
CanvasResizer cr= new CanvasResizer();
stackNew = cr.expandStack(tstack, (xe+2), (ye+18), 1, 1);
imp3 = new ImagePlus("Auto Threshold", stackNew);
imp3.updateAndDraw();
MontageMaker mm= new MontageMaker();
mm.makeMontage( imp3, 3, 2, 1.0, 1, (ml-1), 1, 0, true); // 5 columns and 3 rows
}
imp.setSlice(1);
//if (doItAnyway)
IJ.run("Images to Stack", "method=[Copy (center)] title=Montage");
return;
}
else { //single image try all
tstack= new ImageStack(xe,ye);
for (int k=1; k<ml;k++)
tstack.addSlice(methods[k], ip.duplicate());
imp2 = new ImagePlus("Auto Threshold", tstack);
imp2.updateAndDraw();
for (int k=1; k<ml;k++){
imp2.setSlice(k);
//IJ.log("analyzing slice with "+methods[k]);
exec(imp2, methods[k], radius, par1, par2, doIwhite );
}
//imp2.setSlice(1);
CanvasResizer cr= new CanvasResizer();
stackNew = cr.expandStack(tstack, (xe+2), (ye+18), 1, 1);
imp3 = new ImagePlus("Auto Threshold", stackNew);
imp3.updateAndDraw();
MontageMaker mm= new MontageMaker();
mm.makeMontage( imp3, 3, 2, 1.0, 1, (ml-1), 1, 0, true);
return;
}
}
else { // selected a method
if (stackSize>1 && doIstack ) { //whole stack
// if (doIstackHistogram) {// one global histogram
// Object[] result = exec(imp, myMethod, noWhite, noBlack, doIwhite, doIset, doIlog, doIstackHistogram );
// }
// else{ // slice by slice
for (int k=1; k<=stackSize; k++){
imp.setSlice(k);
exec(imp, myMethod, radius, par1, par2, doIwhite );
}
// }
imp.setSlice(1);
}
else { //just one slice
exec(imp, myMethod, radius, par1, par2, doIwhite );
}
// 5 - If all went well, show the image:
// not needed here as the source image is binarised
}
}
//IJ.showStatus(IJ.d2s((System.currentTimeMillis()-start)/1000.0, 2)+" seconds");
/** Execute the plugin functionality: duplicate and scale the given image.
* @return an Object[] array with the name and the scaled ImagePlus.
* Does NOT show the new, image; just returns it. */
public Object[] exec(ImagePlus imp, String myMethod, int radius, double par1, double par2, boolean doIwhite ) {
// 0 - Check validity of parameters
if (null == imp) return null;
ImageProcessor ip = imp.getProcessor();
ip.getHistogram();
IJ.showStatus("Thresholding...");
//1 Do it
if (imp.getStackSize()==1){
ip.snapshot();
Undo.setup(Undo.FILTER, imp);
}
// Apply the selected algorithm
if(myMethod.equals("Bernsen")){
Bernsen(imp, radius, par1, par2, doIwhite);
}
else if(myMethod.equals("Mean")){
Mean(imp, radius, par1, par2, doIwhite);
}
else if(myMethod.equals("Median")){
Median(imp, radius, par1, par2, doIwhite);
}
else if(myMethod.equals("MidGrey")){
MidGrey(imp, radius, par1, par2, doIwhite);
}
else if(myMethod.equals("Niblack")){
Niblack (imp, radius, par1, par2, doIwhite);
}
else if(myMethod.equals("Sauvola")){
Sauvola(imp, radius, par1, par2, doIwhite);
}
//IJ.showProgress((double)(255-i)/255);
imp.updateAndDraw();
imp.getProcessor().setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
// 2 - Return the threshold and the image
return new Object[] {imp};
}
void Bernsen(ImagePlus imp, int radius, double par1, double par2, boolean doIwhite ) {
// Bernsen recommends WIN_SIZE = 31 and CONTRAST_THRESHOLD = 15.
// 1) Bernsen J. (1986) "Dynamic Thresholding of Grey-Level Images"
// Proc. of the 8th Int. Conf. on Pattern Recognition, pp. 1251-1255
// 2) Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding
// Techniques and Quantitative Performance Evaluation" Journal of
// Electronic Imaging, 13(1): 146-165
// http://citeseer.ist.psu.edu/sezgin04survey.html
// Ported to ImageJ plugin from E Celebi's fourier_0.8 routines
// This version uses a circular local window, instead of a rectagular one
ImagePlus Maximp, Minimp;
ImageProcessor ip=imp.getProcessor(), ipMax, ipMin;
int contrast_threshold=15;
int local_contrast;
int mid_gray;
byte object;
byte backg;
int temp;
if (par1!=0) {
IJ.log("Bernsen: changed contrast_threshold from :"+ contrast_threshold + " to:" + par1);
contrast_threshold= (int) par1;
}
if (doIwhite){
object = (byte) 0xff;
backg = (byte) 0;
}
else {
object = (byte) 0;
backg = (byte) 0xff;
}
Maximp=duplicateImage(ip);
ipMax=Maximp.getProcessor();
RankFilters rf=new RankFilters();
rf.rank(ipMax, radius, RankFilters.MAX);// Maximum
//Maximp.show();
Minimp=duplicateImage(ip);
ipMin=Minimp.getProcessor();
rf.rank(ipMin, radius, RankFilters.MIN); //Minimum
//Minimp.show();
byte[] pixels = (byte [])ip.getPixels();
byte[] max = (byte [])ipMax.getPixels();
byte[] min = (byte [])ipMin.getPixels();
for (int i=0; i<pixels.length; i++) {
local_contrast = (int)((max[i]&0xff) -(min[i]&0xff));
mid_gray =(int) ((min[i]&0xff) + (max[i]&0xff) )/ 2;
temp=(int) (pixels[i] & 0x0000ff);
if ( local_contrast < contrast_threshold )
pixels[i] = ( mid_gray >= 128 ) ? object : backg; //Low contrast region
else
pixels[i] = (temp >= mid_gray ) ? object : backg;
}
//imp.updateAndDraw();
return;
}
void Mean(ImagePlus imp, int radius, double par1, double par2, boolean doIwhite ) {
// See: Image Processing Learning Resourches HIPR2
// http://homepages.inf.ed.ac.uk/rbf/HIPR2/adpthrsh.htm
ImagePlus Meanimp;
ImageProcessor ip=imp.getProcessor(), ipMean;
int c_value = 0;
byte object;
byte backg;
if (par1!=0) {
IJ.log("Mean: changed c_value from :"+ c_value + " to:" + par1);
c_value= (int)par1;
}
if (doIwhite){
object = (byte) 0xff;
backg = (byte) 0;
}
else {
object = (byte) 0;
backg = (byte) 0xff;
}
Meanimp=duplicateImage(ip);
ImageConverter ic = new ImageConverter(Meanimp);
ic.convertToGray32();
ipMean=Meanimp.getProcessor();
RankFilters rf=new RankFilters();
rf.rank(ipMean, radius, RankFilters.MEAN);// Mean
//Meanimp.show();
byte[] pixels = (byte []) ip.getPixels();
float[] mean = (float []) ipMean.getPixels();
for (int i=0; i<pixels.length; i++)
pixels[i] = ( (int)(pixels[i] &0xff) > (int)( mean[i] - c_value)) ? object : backg;
//imp.updateAndDraw();
return;
}
void Median(ImagePlus imp, int radius, double par1, double par2, boolean doIwhite ) {
// See: Image Processing Learning Resourches HIPR2
// http://homepages.inf.ed.ac.uk/rbf/HIPR2/adpthrsh.htm
ImagePlus Medianimp;
ImageProcessor ip=imp.getProcessor(), ipMedian;
int c_value = 0;
byte object;
byte backg;
if (par1!=0) {
IJ.log("Median: changed c_value from :"+ c_value + " to:" + par1);
c_value= (int) par1;
}
if (doIwhite){
object = (byte) 0xff;
backg = (byte) 0;
}
else {
object = (byte) 0;
backg = (byte) 0xff;
}
Medianimp=duplicateImage(ip);
ipMedian=Medianimp.getProcessor();
RankFilters rf=new RankFilters();
rf.rank(ipMedian, radius, RankFilters.MEDIAN);
//Medianimp.show();
byte[] pixels = (byte []) ip.getPixels();
byte[] median = (byte []) ipMedian.getPixels();
for (int i=0; i<pixels.length; i++)
pixels[i] = ( (int)(pixels[i] &0xff) > (int)( (median[i] &0xff) - c_value)) ? object : backg;
//imp.updateAndDraw();
return;
}
void MidGrey(ImagePlus imp, int radius, double par1, double par2, boolean doIwhite ) {
// See: Image Processing Learning Resourches HIPR2
// http://homepages.inf.ed.ac.uk/rbf/HIPR2/adpthrsh.htm
ImagePlus Maximp, Minimp;
ImageProcessor ip=imp.getProcessor(), ipMax, ipMin;
int c_value =0;
byte object;
byte backg;
if (par1!=0) {
IJ.log("MidGrey: changed c_value from :"+ c_value + " to:" + par1);
c_value= (int) par1;
}
if (doIwhite){
object = (byte) 0xff;
backg = (byte) 0;
}
else {
object = (byte) 0;
backg = (byte) 0xff;
}
Maximp=duplicateImage(ip);
ipMax=Maximp.getProcessor();
RankFilters rf=new RankFilters();
rf.rank(ipMax, radius, RankFilters.MAX);// Maximum
//Maximp.show();
Minimp=duplicateImage(ip);
ipMin=Minimp.getProcessor();
rf.rank(ipMin, radius, RankFilters.MIN); //Minimum
//Minimp.show();
byte[] pixels = (byte [])ip.getPixels();
byte[] max = (byte [])ipMax.getPixels();
byte[] min = (byte [])ipMin.getPixels();
for (int i=0; i<pixels.length; i++) {
pixels[i] = ( (int)(pixels[i] &0xff) > (int)(((max[i]&0xff) +(min[i]&0xff))/2)+c_value ) ? object : backg;
}
//imp.updateAndDraw();
return;
}
void Niblack(ImagePlus imp, int radius, double par1, double par2, boolean doIwhite ) {
// Niblack recommends K_VALUE = -0.2 for images with black foreground
// objects, and K_VALUE = +0.2 for images with white foreground objects.
// Niblack W. (1986) "An introduction to Digital Image Processing" Prentice-Hall.
// Ported to ImageJ plugin from E Celebi's fourier_0.8 routines
// This version uses a circular local window, instead of a rectagular one
ImagePlus Meanimp, Varimp;
ImageProcessor ip=imp.getProcessor(), ipMean, ipVar;
double k_value;
byte object;
byte backg ;
if (doIwhite){
k_value=0.2;
object = (byte) 0xff;
backg = (byte) 0;
}
else {
k_value= -0.2;
object = (byte) 0;
backg = (byte) 0xff;
}
if (par1!=0) {
IJ.log("Niblack: changed k_value from :"+ k_value + " to:" + par1);
k_value= par1;
}
Meanimp=duplicateImage(ip);
ImageConverter ic = new ImageConverter(Meanimp);
ic.convertToGray32();
ipMean=Meanimp.getProcessor();
RankFilters rf=new RankFilters();
rf.rank(ipMean, radius, RankFilters.MEAN);// Mean
//Meanimp.show();
Varimp=duplicateImage(ip);
ic = new ImageConverter(Varimp);
ic.convertToGray32();
ipVar=Varimp.getProcessor();
rf.rank(ipVar, radius, RankFilters.VARIANCE); //Variance
//Varimp.show();
byte[] pixels = (byte []) ip.getPixels();
float[] mean = (float []) ipMean.getPixels();
float[] var = (float []) ipVar.getPixels();
for (int i=0; i<pixels.length; i++)
pixels[i] = ( (int)(pixels[i] &0xff) > (int)( mean[i] + k_value * Math.sqrt ( var[i] ))) ? object : backg;
//imp.updateAndDraw();
return;
}
void Sauvola(ImagePlus imp, int radius, double par1, double par2, boolean doIwhite) {
// Sauvola recommends K_VALUE = 0.5 and R_VALUE = 128.
// This is a modification of Niblack's thresholding method.
// Sauvola J. and Pietaksinen M. (2000) "Adaptive Document Image Binarization"
// Pattern Recognition, 33(2): 225-236
// http://www.ee.oulu.fi/mvg/publications/show_pdf.php?ID=24
// Ported to ImageJ plugin from E Celebi's fourier_0.8 routines
// This version uses a circular local window, instead of a rectagular one
ImagePlus Meanimp, Varimp;
ImageProcessor ip=imp.getProcessor(), ipMean, ipVar;
double k_value = 0.5;
double r_value = 128;
byte object;
byte backg;
if (par1!=0) {
IJ.log("Sauvola: changed k_value from :"+ k_value + " to:" + par1);
k_value= par1;
}
if (par2!=0) {
IJ.log("Sauvola: changed r_value from :"+r_value + " to:" + par2);
r_value= par2;
}
if (doIwhite){
object = (byte) 0xff;
backg = (byte) 0;
}
else {
object = (byte) 0;
backg = (byte) 0xff;
}
Meanimp=duplicateImage(ip);
ImageConverter ic = new ImageConverter(Meanimp);
ic.convertToGray32();
ipMean=Meanimp.getProcessor();
RankFilters rf=new RankFilters();
rf.rank(ipMean, radius, RankFilters.MEAN);// Mean
//Meanimp.show();
Varimp=duplicateImage(ip);
ic = new ImageConverter(Varimp);
ic.convertToGray32();
ipVar=Varimp.getProcessor();
rf.rank(ipVar, radius, RankFilters.VARIANCE); //Variance
//Varimp.show();
byte[] pixels = (byte []) ip.getPixels();
float[] mean = (float []) ipMean.getPixels();
float[] var = (float []) ipVar.getPixels();
for (int i=0; i<pixels.length; i++)
pixels[i] = ( (int)(pixels[i] &0xff) > (int)( mean[i] * (1.0+ k_value *(( Math.sqrt ( var[i] )/r_value)-1.0)))) ? object : backg;
//imp.updateAndDraw();
return;
}
private ImagePlus duplicateImage(ImageProcessor iProcessor){
int w=iProcessor.getWidth();
int h=iProcessor.getHeight();
ImagePlus iPlus=NewImage.createByteImage("Image", w, h, 1, NewImage.FILL_BLACK);
ImageProcessor imageProcessor=iPlus.getProcessor();
imageProcessor.copyBits(iProcessor, 0,0, Blitter.COPY);
return iPlus;
}
}