// Java API classes import java.awt.Frame; import java.lang.Integer; import java.util.Random; // ImageJ classes import ij.ImagePlus; import ij.gui.GenericDialog; import ij.gui.MessageDialog; import ij.plugin.filter.PlugInFilter; import ij.process.ByteBlitter; import ij.process.ByteProcessor; import ij.process.ImageProcessor; /** * This is a contrast-enhancing ImageJ plugin for 8 bit * greyscale images. * *
It creates a new contrast-enhanced image from the processed image. * *
It asks for the geometric half length (halfLength) of the * contrast-enhancing square. * *
The contrast-enhancing algorithm was proposed by * Didier Pelat (Paris Observatory * http://www.obspm.fr/) * and implemented by Artenum * (http://www.artenum.com/). * *
Copyright (C) 2001 ARTENUM SARL. * *
This program is a 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 2 * of 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, write to the Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * @author Jean-Francois Leveque * (software@artenum.com) * @version 1.0.3 */ public class Contrast_Enhancer implements PlugInFilter { /** * This method gets a reference to the image to be contrast-enhanced * and returns the filter capabilities. *
* * @param arg This parameter is not used. * @param imp This is the image to be processed * * @return Returns DOES_8G + NO_CHANGES + NO_UNDO. */ public int setup(String arg, ImagePlus imp) { // Gets a reference to the original image to be processed. this.origImg = imp ; // Gets the original image height and Width. this.origHeight = this.origImg.getHeight(); this.origWidth = this.origImg.getWidth(); /* Flag word returned that specifies the filter capabilities: DOES_8G Filter handles 8-bit grayscale images. NO_CHANGES Filter makes no changes to the pixel data. It creates a new image. NO_UNDO Filter does not require undo. */ return DOES_8G + NO_CHANGES + NO_UNDO ; } /** * This method creates a new contrast-enhanced image from the processed image. * *
It returns when the contrast-enhanced image is created or when the * geometric half length (halfLength) asking dialog is canceled. * *
* * @param ip This is the ImageProcessor of the image to be * processed. It is cast to be used as a ByteProcessor. */ public void run(ImageProcessor ip) { /* Gets a ByteProcessor from the original image ImageProcessor to make a copy with extra stripes for future processing. */ this.origBp = (ByteProcessor) ip ; // Asks for the halfLength and exits processing if canceled. if ( !askHalfLength() ) { return; } // Makes the ByteProcessor copy with extra stripes for future processing. copyOrig(); // Creates the contrast-enhanced image. enhance(); return; } /** * This method asks the geometric half length (halfLength) of the * contrast-enhancing square. * *
* * @return Returns true is a valid half length is given and * false if the halfLength asking dialog is canceled. */ private boolean askHalfLength() { // Do we keep on asking the value? boolean askValue = true ; // value returned from the dialog int value; while (askValue) { // Gets the value from the dialog. GenericDialog gd = new GenericDialog("Parameters"); gd.addNumericField("Half Length",1,0); gd.showDialog(); if ( gd.wasCanceled() ) { gd.dispose(); return false; } if ( gd.invalidNumber() ) { MessageDialog md = new MessageDialog(new Frame(),"Invalid Number", "You must enter a valid number for half length."); md.dispose(); } else { value = (int) gd.getNextNumber(); /* Checks value correctness regarding MIN_HALF_LENGTH, MAX_HALF_LENGTH and the original image height and width and acts accordingly. */ if ( checkSize(value) ) { askValue = false ; this.halfLength = value ; this.sideLength = 2 * this.halfLength + 1 ; this.maxRank = this.sideLength * this.sideLength - 1 ; gd.dispose(); } } } return true; } /** * This method checks if the half length value is correct regarding * MIN_HALF_LENGTH, MAX_HALF_LENGTH and the original image height and width. * *
* * @param value This is the half length value to be checked. * * @return Returns true if it's ok. Pops a dialog and returns false * if it's not. */ private boolean checkSize(int value) { // length calculated from the half length value int length; // max half length value for image int maxHalfLength; if ( value < MIN_HALF_LENGTH ) { MessageDialog md = new MessageDialog(new Frame(),"Invalid Value", "The minimum value for half length is "+MIN_HALF_LENGTH+"."); md.dispose(); return false; } if ( value > MAX_HALF_LENGTH ) { MessageDialog md = new MessageDialog(new Frame(), "Invalid value", "The absolute maximum value for half length is "+MAX_HALF_LENGTH+ "because ranking could go past java.lang.Integer.MAX_VALUE if we "+ "use a higher value.\n"+"If you want to go beyond that, you will "+ "have to alter this plugin (and maybe ImageJ) to use Long "+ "instead of Integer."); md.dispose(); return false; } length = 2 * value + 1 ; if ( ( this.origHeight < length ) || ( this.origWidth < length ) ) { maxHalfLength = this.origHeight < this.origWidth ? (int) (this.origHeight - 1) / 2 : (int) (this.origWidth - 1) / 2 ; MessageDialog md = new MessageDialog(new Frame(), "Invalid value", "You image must have width and length greater or equal to "+ length+" if you want a half length of "+value+".\n"+ "The maximum half length value for your image is equal to "+ maxHalfLength+"."); md.dispose(); return false; } return true; } /** * This method makes a ByteProcessor copy of the original image ByteProcessor * with extra stripes for future processing. */ private void copyOrig() { int copyHeight; int copyWidth; // Creates ByteProcessor with necessary height and width. copyHeight = this.origHeight + 2 * this.halfLength ; copyWidth = this.origWidth + 2 * this.halfLength ; this.tmpCopyBp = new ByteProcessor(copyWidth, copyHeight); // Copies the original image to the upper left corner. this.tmpCopyBp.copyBits(this.origBp,0,0,ByteBlitter.COPY); // Copies the right stripe for future processing. this.tmpCopyBp.copyBits(this.origBp,origWidth,0,ByteBlitter.COPY); // Copies the lower stripe for future processing. this.tmpCopyBp.copyBits(this.origBp,0,origHeight,ByteBlitter.COPY); // Copies the lower right stripe for future processing. this.tmpCopyBp.copyBits(this.origBp,origWidth,origHeight,ByteBlitter.COPY); } /** * This method calls the contrast-enhancing routine on all regions * of the image using the necessary stripes. */ private void enhance() { // new image ByteProcessor ByteProcessor newBp; // original image x-coordinate int i; // original image y-coordinate int j; // original image x-coordinate translated into a copy stripe int i_plus; // original image y-coordinate translated into a copy stripe int j_plus; // Creates the new image ByteProcessor. newBp = new ByteProcessor(origWidth, origHeight); // Initializes the random number generator used in makePixelFromRoi. rand = new Random(); // Calls upper left corner processing translating it right and down. for ( i = 0 , i_plus = origWidth ; i < halfLength ; i++ , i_plus++ ) { for ( j = 0 , j_plus = origHeight ; j < halfLength ; j++ , j_plus++ ) { makePixelFromRoi(i,j,i_plus,j_plus,newBp); } } // Calls upper stripe processing translating it down. for ( i = halfLength ; i < origWidth ; i++ ) { for ( j = 0 , j_plus = origHeight ; j < halfLength ; j++ , j_plus++ ) { makePixelFromRoi(i,j,i,j_plus,newBp); } } // Calls left stripe processing translating it right. for ( i = 0 , i_plus = origWidth ; i < halfLength ; i++ , i_plus++ ) { for ( j = halfLength ; j < origHeight ; j++ ) { makePixelFromRoi(i,j,i_plus,j,newBp); } } // Calls processing on remaining image with no translation. for ( i = halfLength ; i < origWidth ; i++ ) { for ( j = halfLength ; j < origHeight ; j++ ) { makePixelFromRoi(i,j,i,j,newBp); } } // Creates the new image from the ByteProcessor processed. this.newImg = new ImagePlus(origImg.getTitle()+" (contrast-enhanced : "+ halfLength+" )", newBp); this.newImg.show(); } /** * This method contrast-enhances a pixel using an histogram from a sideLength * square Roi centered on it. * *
The pixel value is set to its rank in the Roi. * *
Because more than one pixel in the Roi may have the same rank, we * randomize the rank between rank and rank + number of pixels of the same * rank. * *
The Roi rank is converted into the 0-255 range of 8 bit grayscale images. * *
* * @param x This is the original and new image pixel x-coordinate. * @param y This is the original and new image pixel y-coordinate. * @param x_plus This is the x-coordinate of the Roi selection in the * original image copy. * @param y_plus This is the y-coordinate of the Roi selection in the * original image copy. * @param bp This is the new ByteProcessor where the pixel value is * set. */ private void makePixelFromRoi(int x, int y, int x_plus, int y_plus, ByteProcessor bp) { // Rank of the pixel in the Roi (1st rank=0) int rank=0; int i; int[] histogram; int pixelValue; // Sets the Roi around the pixel. this.tmpCopyBp.setRoi(x_plus - halfLength, y_plus - halfLength, sideLength, sideLength); // Gets the histogram from the set Roi. histogram = this.tmpCopyBp.getHistogram(); // Gets value from the pixel to be ranked. pixelValue = this.origBp.getPixel(x,y); // Gets the rank of all the pixels with the same value. for ( i = 0 ; i < pixelValue - 1 ; i++ ) { rank += histogram[i]; } // Puts the random shift. // Java Platform 1.2 API version // rank += rand.nextInt(histogram[pixelValue]); // Java Platform 1.1.8 API version for ImageJ users with no Java 2 JRE rank += (int) ( ( (double) rand.nextInt() - Integer.MIN_VALUE ) / MAX_U_INT_VALUE * ( histogram[pixelValue] ) ); // Converts rank back to the 8 bit grayscale images range. rank = (int) ( (double) rank / this.maxRank * this.MAX_8BIT_RANK ) ; // Sets the pixel rank as its value in the new ByteProcessor. bp.putPixel(x,y,rank); } /** * Geometric half length of the contrast-enhancing square. * *
It must be greater or equal to MIN_HALF_LENGTH. */ private int halfLength; /** * Minimum half length (halfLength) value (is equal to 1). */ private static final int MIN_HALF_LENGTH=1; /** * Maximum half length (halfLength) value (is equal to 23169). * *
It is such because ranking could go past java.lang.Integer.MAX_VALUE if * we use a higher value. * *
If you want to go beyond that, you will have to alter this plugin (and * maybe ImageJ) to use Long instead of Integer. */ private static final int MAX_HALF_LENGTH=23169; /** * Pixel side length of the contrast-enhancing square. * *
It is equal to 2*halfLength+1. */ private int sideLength; /** * Maximum possible rank for contrast-enhancing algorithm with half length * (halfLength) selected. * *
It is equal to sideLength*sideLength-1. */ private int maxRank; /** * Maximum possible rank for making a 8 bit contrast-enhanced image (is equal * to 255). */ private static final int MAX_8BIT_RANK=255; /** * Maximum possible value for an unsigner 32 bit integer (2^32-1). * Its value is calculated with : (long) Integer.MAX_VALUE - Integer.MIN_VALUE . */ private static final long MAX_U_INT_VALUE = (long) Integer.MAX_VALUE - Integer.MIN_VALUE ; /** * This new image is the constrast-enhanced original image */ ImagePlus newImg; /** * This is the original image to be constrast-enhanced. */ ImagePlus origImg; /** * This is the original image height. */ private int origHeight; /** * This is the original image width. */ private int origWidth; /** * This is the ByteProcessor from the original image. */ private ByteProcessor origBp; // ByteProcessor pour l'image originale plus ... /** * This is the temporary ByteProcessor needed for this implementation. */ private ByteProcessor tmpCopyBp; /** * This is the random number generator used to make the rank of the pixel in * the Roi unique in makePixelFromRoi. */ private Random rand; }