package ij.plugin.frame;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import ij.*;
import ij.plugin.*;
import ij.process.*;
import ij.gui.*;
import ij.measure.*;
import ij.plugin.frame.Recorder;
import ij.plugin.filter.*;
import ij.plugin.ChannelSplitter;
import ij.plugin.Thresholder;

/** Adjusts the lower and upper threshold levels of the active image. This
	class is multi-threaded to provide a more responsive user interface. */
public class ThresholdAdjuster extends PlugInDialog implements PlugIn, Measurements, Runnable,
	ActionListener, AdjustmentListener, ItemListener, FocusListener, KeyListener, MouseWheelListener, ImageListener {

	public static final String LOC_KEY = "threshold.loc";
	public static final String MODE_KEY = "threshold.mode";
	public static final String DARK_BACKGROUND = "threshold.dark";
	static final int RED=0, BLACK_AND_WHITE=1, OVER_UNDER=2;
	static final String[] modes = {"Red","B&W", "Over/Under"};
	static final double defaultMinThreshold = 85; 
	static final double defaultMaxThreshold = 170;
	static final int DEFAULT = 0;
	static boolean fill1 = true;
	static boolean fill2 = true;
	static boolean useBW = true;
	static boolean backgroundToNaN = true;
	static ThresholdAdjuster instance;
	static int mode = RED;	
	static String[] methodNames = AutoThresholder.getMethods();
	static String method = methodNames[DEFAULT];
	static AutoThresholder thresholder = new AutoThresholder();
	ThresholdPlot plot = new ThresholdPlot();
	Thread thread;
	
	int minValue = -1;
	int maxValue = -1;
	int sliderRange = 256;
	boolean doAutoAdjust,doReset,doApplyLut,doStateChange,doSet;
	
	Panel panel;
	Button autoB, resetB, applyB;
	int previousImageID;
	int previousImageType;
	int previousRoiHashCode;
	double previousMin, previousMax;
	int previousSlice;
	boolean imageWasUpdated;
	ImageJ ij;
	double minThreshold, maxThreshold;  // 0-255
	Scrollbar minSlider, maxSlider;
	TextField minLabel, maxLabel;           // for current threshold
	Label percentiles;
	boolean done;
	int lutColor;
	Choice methodChoice, modeChoice;
	Checkbox darkBackground, stackHistogram;
	boolean firstActivation = true;


	public ThresholdAdjuster() {
		super("Threshold");
		ImagePlus cimp = WindowManager.getCurrentImage();
		if (cimp!=null && cimp.getBitDepth()==24) {
			IJ.error("Threshold Adjuster",
				"Image>Adjust>Threshold only works with grayscale images.\n \n"
				+"You can:\n"
				+"   Convert to grayscale: Image>Type>8-bit\n"
				+"   Convert to RGB stack: Image>Type>RGB Stack\n"
				+"   Convert to HSB stack: Image>Type>HSB Stack\n"
				+"   Convert to 3 grayscale images: Image>Color>Split Channels\n"
				+"   Do color thresholding: Image>Adjust>Color Threshold\n");
			return;
		}
		if (instance!=null) {
			instance.firstActivation = true;
			instance.toFront();
			instance.setup(cimp, true);
			return;
		}
		
		WindowManager.addWindow(this);
		instance = this;
		mode = (int)Prefs.get(MODE_KEY, RED);
		if (modeOVER_UNDER) mode = RED;
		setLutColor(mode);
		IJ.register(PasteController.class);

		ij = IJ.getInstance();
		Font font = new Font("SansSerif", Font.PLAIN, 10);
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		setLayout(gridbag);
		
		// plot
		int y = 0;
		c.gridx = 0;
		c.gridy = y++;
		c.gridwidth = 2;
		c.fill = GridBagConstraints.BOTH;
		c.anchor = GridBagConstraints.CENTER;
		c.insets = new Insets(10, 10, 0, 10); //top left bottom right
		add(plot, c);
		plot.addKeyListener(ij);

		// percentiles
		c.gridx = 0;
		c.gridy = y++;
		c.insets = new Insets(1, 10, 0, 10);
		percentiles = new Label("");
		percentiles.setFont(font);
		add(percentiles, c);

		// minThreshold slider
		minSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange/3, 1, 0, sliderRange);
		c.gridx = 0;
		c.gridy = y++;
		c.gridwidth = 1;
		c.weightx = IJ.isMacintosh()?90:100;
		c.fill = GridBagConstraints.HORIZONTAL;
		c.insets = new Insets(1, 10, 0, 0);
		add(minSlider, c);
		minSlider.addAdjustmentListener(this);
		minSlider.addMouseWheelListener(this);
//		minSlider.addKeyListener(ij);
		minSlider.setUnitIncrement(1);
		minSlider.setFocusable(false);
		
		// minThreshold slider label
		c.gridx = 1;
		c.gridwidth = 1;
		c.weightx = IJ.isMacintosh()?10:0;
		c.insets = new Insets(5, 0, 0, 10);
		String text = "000000";
		int columns = 4;
		minLabel = new TextField(text,columns);
		minLabel.setFont(font);
		add(minLabel, c);
		minLabel.addFocusListener(this);
		minLabel.addMouseWheelListener(this);
		minLabel.addKeyListener(this);
		
		// maxThreshold slider
		maxSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange*2/3, 1, 0, sliderRange);
		c.gridx = 0;
		c.gridy = y++;
		c.gridwidth = 1;
		c.weightx = 100;
		c.insets = new Insets(2, 10, 0, 0);
		add(maxSlider, c);
		maxSlider.addAdjustmentListener(this);
		maxSlider.addMouseWheelListener(this);
//		maxSlider.addKeyListener(ij);
		maxSlider.setUnitIncrement(1);
		maxSlider.setFocusable(false);
		
		// maxThreshold slider label
		c.gridx = 1;
		c.gridwidth = 1;
		c.weightx = 0;
		c.insets = new Insets(2, 0, 0, 10);
		maxLabel = new TextField(text,columns);
		maxLabel.setFont(font);
		add(maxLabel, c);
		maxLabel.addFocusListener(this);
		maxLabel.addMouseWheelListener(this);
		maxLabel.addKeyListener(this);
				
		// choices
		panel = new Panel();
		methodChoice = new Choice();
		for (int i=0; i1;
    }

	void autoSetLevels(ImageProcessor ip, ImageStatistics stats) {
		if (stats==null || stats.histogram==null) {
			minThreshold = defaultMinThreshold;
			maxThreshold = defaultMaxThreshold;
			return;
		}
		//int threshold = ip.getAutoThreshold(stats.histogram);
		boolean darkb = darkBackground!=null && darkBackground.getState();
		boolean invertedLut = ip.isInvertedLut();
		int modifiedModeCount = stats.histogram[stats.mode];
		if (!method.equals(methodNames[DEFAULT]))
			stats.histogram[stats.mode] = plot.originalModeCount;
		int threshold = thresholder.getThreshold(method, stats.histogram);
		stats.histogram[stats.mode] = modifiedModeCount;
		if (darkb) {
			if (invertedLut)
				{minThreshold=0; maxThreshold=threshold;}
			else
				{minThreshold=threshold+1; maxThreshold=255;}
		} else {
			if (invertedLut)
				{minThreshold=threshold+1; maxThreshold=255;}
			else
				{minThreshold=0; maxThreshold=threshold;}
		}
		if (minThreshold>255)
			minThreshold = 255;
		if (Recorder.record) {
			boolean stack = stackHistogram!=null && stackHistogram.getState();
			String options = method+(darkb?" dark":"")+(stack?" stack":"");
			if (Recorder.scriptMode())
				Recorder.recordCall("IJ.setAutoThreshold(imp, \""+options+"\");");
			else
				Recorder.record("setAutoThreshold", options);
		}
	}
	
	/** Scales threshold levels in the range 0-255 to the actual levels. */
	void scaleUpAndSet(ImageProcessor ip, double lower, double upper) {
		ip.scaleAndSetThreshold(lower, upper, lutColor);
	}

	/** Scales a threshold level to the range 0-255. */
	double scaleDown(ImageProcessor ip, double threshold) {
		if (ip instanceof ByteProcessor)
			return threshold;
		double min = ip.getMin();
		double max = ip.getMax();
		if (max>min)
			return ((threshold-min)/(max-min))*255.0;
		else
			return ImageProcessor.NO_THRESHOLD;
	}
	
	/** Scales a threshold level in the range 0-255 to the actual level. */
	double scaleUp(ImageProcessor ip, double threshold) {
		double min = ip.getMin();
		double max = ip.getMax();
		if (max>min)
			return min + (threshold/255.0)*(max-min);
		else
			return ImageProcessor.NO_THRESHOLD;
	}

	void updatePlot(ImageProcessor ip) {
		int min = (int)Math.round(minThreshold);
		if (min<0) min=0;
 		if (min>255) min=255;
 		if (ip.getMinThreshold()==ImageProcessor.NO_THRESHOLD)
			min = -1; 
 		int max = (int)Math.round(maxThreshold);
 		if (max<0) max=0;
 		if (max>255) max=255;
		plot.lowerThreshold = min;
		plot.upperThreshold = max;
		plot.mode = mode;
		plot.repaint();
	}

	void updatePercentiles(ImagePlus imp, ImageProcessor ip) {
		if (percentiles==null)
			return;
		ImageStatistics stats = plot.stats;
		int minThresholdInt = (int)Math.round(minThreshold);
		if (minThresholdInt<0) minThresholdInt=0;
		if (minThresholdInt>255) minThresholdInt=255;
		int maxThresholdInt = (int)Math.round(maxThreshold);
		if (maxThresholdInt<0) maxThresholdInt=0;
		if (maxThresholdInt>255) maxThresholdInt=255;
		if (stats!=null && stats.histogram!=null && stats.histogram.length==256
		&& ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD) {
			int[] histogram = stats.histogram;
			int below = 0, inside = 0, above = 0;
			int minValue=0, maxValue=255;
			if (imp.getBitDepth()==16 && !entireStack(imp)) {
				ip.setRoi(imp.getRoi());
				histogram = ip.getHistogram();
				minThresholdInt = (int)Math.round(ip.getMinThreshold());
				if (minThresholdInt<0) minThresholdInt=0;
				maxThresholdInt = (int)Math.round(ip.getMaxThreshold());
				if (maxThresholdInt>65535) maxThresholdInt=65535;
				minValue=(int)ip.getMin(); maxValue=(int)ip.getMax();
			}
			for (int i=minValue; i"+maxThresholdInt+":"+above+" sum="+total);
			int digits = imp.getCalibration()!=null || (ip instanceof FloatProcessor) ?
			Math.max(Analyzer.getPrecision(), 2) : 0;
			if (mode==OVER_UNDER)
				percentiles.setText("below: "+IJ.d2s(100.*below/total)+" %,  above: "+IJ.d2s(100.*above/total)+" %");
			else
				percentiles.setText(IJ.d2s(100.*inside/total)+" %");
		} else
			percentiles.setText("");
	}

	void updateLabels(ImagePlus imp, ImageProcessor ip) {
		if (minLabel==null || maxLabel==null)
			return;
		double min = ip.getMinThreshold();
		double max = ip.getMaxThreshold();
		if (min==ImageProcessor.NO_THRESHOLD) {
			minLabel.setText("");
			maxLabel.setText("");
		} else {
			Calibration cal = imp.getCalibration();
			if (cal.calibrated()) {
				min = cal.getCValue((int)min);
				max = cal.getCValue((int)max);
			}
			if (((int)min==min && (int)max==max) || (ip instanceof ShortProcessor) || max>99999.0) {
				minLabel.setText(ResultsTable.d2s(min,0));
				maxLabel.setText(ResultsTable.d2s(max,0));
			} else {
				minLabel.setText(""+(min<-3.4e38?"-3.4e38":ResultsTable.d2s(min,2)));
				maxLabel.setText(""+ResultsTable.d2s(max,max==Double.MAX_VALUE?0:2));
			}
		}
	}

	void updateScrollBars() {
		minSlider.setValue((int)minThreshold);
		maxSlider.setValue((int)maxThreshold);
	}
	
	/** Restore image outside non-rectangular roi. */
  	void doMasking(ImagePlus imp, ImageProcessor ip) {
		ImageProcessor mask = imp.getMask();
		if (mask!=null)
			ip.reset(mask);
	}

	void adjustMinThreshold(ImagePlus imp, ImageProcessor ip, double value) {
		if (IJ.altKeyDown() || IJ.shiftKeyDown() ) {
			double width = maxThreshold-minThreshold;
			if (width<1.0) width = 1.0;
			minThreshold = value;
			maxThreshold = minThreshold+width;
			if ((minThreshold+width)>255) {
				minThreshold = 255-width;
				maxThreshold = minThreshold+width;
				minSlider.setValue((int)minThreshold);
			}
			maxSlider.setValue((int)maxThreshold);
			scaleUpAndSet(ip, minThreshold, maxThreshold);
			return;
		}
		minThreshold = value;
		if (maxThresholdmaxThreshold) {
			minThreshold = maxThreshold;
			minSlider.setValue((int)minThreshold);
		}
		if (minThreshold < 0) {     //remove NO_THRESHOLD
			minThreshold = 0;
			minSlider.setValue((int)minThreshold);
		}
		scaleUpAndSet(ip, minThreshold, maxThreshold);
		IJ.setKeyUp(KeyEvent.VK_ALT);
		IJ.setKeyUp(KeyEvent.VK_SHIFT);
	}

	void reset(ImagePlus imp, ImageProcessor ip) {
		ip.resetThreshold();
		ImageStatistics stats = plot.setHistogram(imp, entireStack(imp));
		if (!(ip instanceof ByteProcessor)) {
			if (entireStack(imp))
				ip.setMinAndMax(stats.min, stats.max);
			else
				ip.resetMinAndMax();
		}
		updateScrollBars();
		if (Recorder.record) {
			if (Recorder.scriptMode())
				Recorder.recordCall("IJ.resetThreshold(imp);");
			else
				Recorder.record("resetThreshold");
		}
	}

	void doSet(ImagePlus imp, ImageProcessor ip) {
/*
		double level1 = ip.getMinThreshold();
		double level2 = ip.getMaxThreshold();
		if (level1==ImageProcessor.NO_THRESHOLD) {
			level1 = scaleUp(ip, defaultMinThreshold);
			level2 = scaleUp(ip, defaultMaxThreshold);
		}
*/
		Calibration cal = imp.getCalibration();
		int digits = (ip instanceof FloatProcessor)||cal.calibrated()?2:0;
/*
		level1 = cal.getCValue(level1);
		level2 = cal.getCValue(level2);
		GenericDialog gd = new GenericDialog("Set Threshold Levels");
		gd.addNumericField("Lower Threshold Level: ", level1, digits);
		gd.addNumericField("Upper Threshold Level: ", level2, digits);
		gd.showDialog();
		if (gd.wasCanceled())
			return;
		level1 = gd.getNextNumber();
		level2 = gd.getNextNumber();
*/
		double level1 = Double.parseDouble(minLabel.getText());
		double level2 = Double.parseDouble(maxLabel.getText());
		level1 = cal.getRawValue(level1);
		level2 = cal.getRawValue(level2);
		if (level2maxValue) level2 = maxValue;
		}
		IJ.wait(500);
		ip.setThreshold(level1, level2, lutColor);
		ip.setSnapshotPixels(null); // disable undo
		previousImageID = 0;
		setup(imp, false);
		if (Recorder.record) {
			if (imp.getBitDepth()==32) {
				if (Recorder.scriptMode())
					Recorder.recordCall("IJ.setThreshold(imp, "+IJ.d2s(ip.getMinThreshold(),4)+", "+IJ.d2s(ip.getMaxThreshold(),4)+");");
				else
					Recorder.record("setThreshold", ip.getMinThreshold(), ip.getMaxThreshold());
			} else {
				int min = (int)ip.getMinThreshold();
				int max = (int)ip.getMaxThreshold();
				if (cal.isSigned16Bit()) {
					min = (int)cal.getCValue(level1);
					max = (int)cal.getCValue(level2);
					if (Recorder.scriptMode())
						Recorder.recordCall("IJ.setThreshold(imp, "+min+", "+max+");");
					else
						Recorder.record("setThreshold", min, max);
				}
				if (Recorder.scriptMode())
					Recorder.recordCall("IJ.setRawThreshold(imp, "+min+", "+max+", null);");
				else {
					if (cal.calibrated())
						Recorder.record("setThreshold", min, max, "raw");
					else
						Recorder.record("setThreshold", min, max);
				}
			}
		}
	}

	void changeState(ImagePlus imp, ImageProcessor ip) {
		scaleUpAndSet(ip, minThreshold, maxThreshold);
		updateScrollBars();
	}

	void autoThreshold(ImagePlus imp, ImageProcessor ip) {
		ip.resetThreshold();
		previousImageID = 0;
		setup(imp, true);
 	}
 	
 	void apply(ImagePlus imp) {
 		try {
 			if (imp.getBitDepth()==32) {
				GenericDialog gd = new GenericDialog("NaN Backround");
				gd.addCheckbox("Set background pixels to NaN", backgroundToNaN);
				gd.showDialog();
				if (gd.wasCanceled()) {
 					runThresholdCommand();
					return;
				}
				backgroundToNaN = gd.getNextBoolean();
				if (backgroundToNaN) {
					Recorder.recordInMacros = true;
 					IJ.run("NaN Background");
					Recorder.recordInMacros = false;
 				} else
 					runThresholdCommand();
 			} else
 				runThresholdCommand();
 		} catch (Exception e)
 			{/* do nothing */}
 		//close();
 	}
 	
 	void runThresholdCommand() {
		Thresholder.setMethod(method);
		Thresholder.setBackground(darkBackground.getState()?"Dark":"Light");
		if (Recorder.record) {
			Recorder.setCommand("Convert to Mask");
			(new Thresholder()).run("mask");
			Recorder.saveCommand();
		} else
			(new Thresholder()).run("mask");
	}
	
	static final int RESET=0, AUTO=1, HIST=2, APPLY=3, STATE_CHANGE=4, MIN_THRESHOLD=5, MAX_THRESHOLD=6, SET=7;

	// Separate thread that does the potentially time-consuming processing 
	public void run() {
		while (!done) {
			synchronized(this) {
				try {wait();}
				catch(InterruptedException e) {}
			}
			doUpdate();
		}
	}

	void doUpdate() {
		ImagePlus imp;
		ImageProcessor ip;
		int action;
		int min = minValue;
		int max = maxValue;
		if (doReset) action = RESET;
		else if (doAutoAdjust) action = AUTO;
		else if (doApplyLut) action = APPLY;
		else if (doStateChange) action = STATE_CHANGE;
		else if (doSet) action = SET;
		else if (minValue>=0) action = MIN_THRESHOLD;
		else if (maxValue>=0) action = MAX_THRESHOLD;
		else return;
		minValue = -1;
		maxValue = -1;
		doReset = false;
		doAutoAdjust = false;
		doApplyLut = false;
		doStateChange = false;
		doSet = false;
		imp = WindowManager.getCurrentImage();
		if (imp==null) {
			IJ.beep();
			IJ.showStatus("No image");
			return;
		}
		ip = setup(imp, false);
		if (ip==null) {
			imp.unlock();
			IJ.beep();
			if (imp.isComposite())
				IJ.showStatus("\"Composite\" mode images cannot be thresholded");
			else
				IJ.showStatus("RGB images cannot be thresholded");
			return;
		}
		switch (action) {
			case RESET: reset(imp, ip); break;
			case AUTO: autoThreshold(imp, ip); break;
			case APPLY: apply(imp); break;
			case STATE_CHANGE: changeState(imp, ip); break;
			case SET: doSet(imp, ip); break;
			case MIN_THRESHOLD: adjustMinThreshold(imp, ip, min); break;
			case MAX_THRESHOLD: adjustMaxThreshold(imp, ip, max); break;
		}
		updatePlot(ip);
		updateLabels(imp, ip);
		updatePercentiles(imp, ip);
		ip.setLutAnimation(true);
		imp.updateAndDraw();
	}

    /** Overrides close() in PlugInFrame. */
    public void close() {
    	super.close();
		instance = null;
		done = true;
		Prefs.saveLocation(LOC_KEY, getLocation());
		Prefs.set(MODE_KEY, mode);
		Prefs.set(DARK_BACKGROUND, darkBackground.getState());
		synchronized(this) {
			notify();
		}
	}

    public void windowActivated(WindowEvent e) {
    	super.windowActivated(e);
    	plot.requestFocus();
		ImagePlus imp = WindowManager.getCurrentImage();
		if (!firstActivation && imp!=null)
			setup(imp, false);
	}

	// Returns a hashcode for the specified ROI that typically changes 
	// if it is moved,  even though is still the same object.
	int roiHashCode(Roi roi) {
		return roi!=null?roi.getHashCode():0;
	}

    /** Notifies the ThresholdAdjuster that the image has changed.
     *  If the image has no threshold, it does not autothreshold the image. */
    public static void update() {
		if (instance!=null) {
			ThresholdAdjuster ta = ((ThresholdAdjuster)instance);
			ImagePlus imp = WindowManager.getCurrentImage();
			if (imp!=null && ta.previousImageID==imp.getID()) {
				if ((imp.getCurrentSlice()!=ta.previousSlice) && ta.entireStack(imp))
					return;
				ta.previousImageID = 0;
				ta.setup(imp, false);
			}
		}
    }

	/** Returns the current thresholding method ("Default", "Huang", etc). */
	public static String getMethod() {
		return method;
	}
	
	/** Sets the thresholding method ("Default", "Huang", etc). */
	public static void setMethod(String thresholdingMethod) {
		boolean valid = false;
		for (int i=0; i maxCount2) && (i != stats.mode))
				maxCount2 = histogram[i];
		hmax = stats.maxCount;
		if ((hmax>(maxCount2 * 2)) && (maxCount2 != 0))
			hmax = (int)(maxCount2 * 1.5);
		os = null;

		if (!(cm instanceof IndexColorModel))
			return null;
		IndexColorModel icm = (IndexColorModel)cm;
		int mapSize = icm.getMapSize();
		if (mapSize!=256)
			return null;
		byte[] r = new byte[256];
		byte[] g = new byte[256];
		byte[] b = new byte[256];
		icm.getReds(r); 
		icm.getGreens(g); 
		icm.getBlues(b);
		hColors = new Color[256];
		final int brightnessLimit = 1800; // 0 ... 2550 scale; brightness is reduced above
		for (int i=0; i<256; i++) {     //avoid colors that are too bright (invisible)
			int sum = 4*(r[i]&255) + 5*(g[i]&255) + (b[i]&255);
			if (sum > brightnessLimit) {
				r[i] = (byte)(((r[i]&255)*brightnessLimit*2)/(sum+brightnessLimit));
				g[i] = (byte)(((g[i]&255)*brightnessLimit*2)/(sum+brightnessLimit));
				b[i] = (byte)(((b[i]&255)*brightnessLimit*2)/(sum+brightnessLimit));
			}
			hColors[i] = new Color(r[i]&255, g[i]&255, b[i]&255);
		}
		imageID2 = imp.getID();
		entireStack2 = entireStack;
		return stats;
	}
	
	public void update(Graphics g) {
		paint(g);
	}

	public void paint(Graphics g) {
		if (g==null) return;
		if (histogram!=null) {
			if (os==null && hmax>0) {
				os = createImage(WIDTH,HEIGHT);
				osg = os.getGraphics();
				osg.setColor(Color.white);
				osg.fillRect(0, 0, WIDTH, HEIGHT);
				osg.setColor(Color.gray);
				for (int i = 0; i < WIDTH; i++) {
					if (hColors!=null) osg.setColor(hColors[i]);
					int histValue = histogram[i] 

Ads help maintain this website.