package ij.plugin.filter; import ij.*; import ij.process.*; import ij.gui.*; import ij.plugin.filter.PlugInFilter.*; import ij.plugin.filter.*; import ij.measure.Calibration; import ij.macro.Interpreter; import java.awt.*; import java.util.*; public class PlugInFilterRunner implements Runnable, DialogListener { private String command; // the command, can be but need not be the name of the PlugInFilter private Object theFilter; // the instance of the PlugInFilter private ImagePlus imp; private int flags; // the flags returned by the PlugInFilter private boolean snapshotDone; // whether the ImageProcessor has a snapshot already private boolean previewCheckboxOn; // the state of the preview checkbox (true = on) private boolean bgPreviewOn; // tells the background thread that preview is allowed private boolean bgKeepPreview; // tells the background thread to keep the result of preview private Thread previewThread; // the thread of the preview private GenericDialog gd; // the dialog (if it registers here by setDialog) private Checkbox previewCheckbox; // reference to the preview Checkbox of the dialog private long previewTime; // time (ms) needed for preview processing private boolean ipChanged; // whether the image data have been changed private int processedAsPreview; // the slice processed during preview (if non-zero) private Hashtable slicesForThread; // gives first&last slice that a given thread should process private Hashtable roisForThread; // gives ROI that a given thread should process Hashtable sliceForThread = new Hashtable(); // here the stack slice currently processed is stored. private int nPasses; // the number of calls to the run(ip) method of the filter private int pass; // passes done so far private boolean doStack; /** The constructor runs a PlugInFilter or ExtendedPlugInFilter by calling its * setup, run, etc. methods. For details, see the documentation of interfaces * PlugInFilter and ExtendedPlugInFilter. * @param theFilter The PlugInFilter to be run * @param command The command that has caused running the PlugInFilter * @param arg The argument specified for this PlugInFilter in IJ_Props.txt or in the * plugins.config file of a .jar archive conatining a collection of plugins. <code>arg</code> * may be a string of length zero. */ public PlugInFilterRunner(Object theFilter, String command, String arg) { this.theFilter = theFilter; this.command = command; imp = WindowManager.getCurrentImage(); flags = ((PlugInFilter)theFilter).setup(arg, imp); // S E T U P if ((flags&PlugInFilter.DONE)!=0) return; if (!checkImagePlus(imp, flags, command)) return; // check whether the PlugInFilter can handle this image type if ((flags&PlugInFilter.NO_IMAGE_REQUIRED)!=0) imp = null; // if the plugin does not want an image, it should not get one Roi roi = null; if (imp != null) { roi = imp.getRoi(); if (roi!=null) roi.endPaste(); // prepare the image: finish previous paste operation (if any) if (!imp.lock()) return; // exit if image is in use nPasses = ((flags&PlugInFilter.CONVERT_TO_FLOAT)!=0) ? imp.getProcessor().getNChannels():1; } if (theFilter instanceof ExtendedPlugInFilter) { // calling showDialog required? flags = ((ExtendedPlugInFilter)theFilter).showDialog(imp, command, this); // D I A L O G (may include preview) if (snapshotDone) Undo.setup(Undo.FILTER, imp); // ip has a snapshot that may be used for Undo boolean keepPreviewFlag = (flags&ExtendedPlugInFilter.KEEP_PREVIEW)!=0; if (keepPreviewFlag && imp!=null && previewThread!=null && ipChanged && previewCheckbox!=null && previewCheckboxOn) { bgKeepPreview = true; waitForPreviewDone(); processedAsPreview = imp.getCurrentSlice(); } else { killPreview(); previewTime = 0; } } // if ExtendedPlugInFilter if ((flags&PlugInFilter.DONE)!=0) { if (imp != null) imp.unlock(); return; } else if (imp==null) { ((PlugInFilter)theFilter).run(null); // not DONE, but NO_IMAGE_REQUIRED return; } /* preparing for the run(ip) method of the PlugInFilter... */ int slices = imp.getStackSize(); //IJ.log("processedAsPreview="+processedAsPreview+"; slices="+slices+"; doesStacks="+((flags&PlugInFilter.DOES_STACKS)!=0)); if ((flags&PlugInFilter.PARALLELIZE_IMAGES)!=0) flags &= ~PlugInFilter.PARALLELIZE_STACKS; doStack = slices>1 && (flags&PlugInFilter.DOES_STACKS)!=0; imp.startTiming(); if (doStack || processedAsPreview==0) { // if processing during preview was not enough IJ.showStatus(command + (doStack ? " (Stack)..." : "...")); ImageProcessor ip = imp.getProcessor(); pass = 0; if (!doStack) { // single image FloatProcessor fp = null; prepareProcessor(ip, imp); announceSliceNumber(imp.getCurrentSlice()); if (theFilter instanceof ExtendedPlugInFilter) ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); if ((flags&PlugInFilter.NO_CHANGES)==0) { // for filters modifying the image boolean disableUndo = Prefs.disableUndo || (flags&PlugInFilter.NO_UNDO)!=0; if (!disableUndo) { ip.snapshot(); snapshotDone = true; } } processOneImage(ip, fp, snapshotDone); // may also set snapShotDone if ((flags&PlugInFilter.NO_CHANGES)==0) { // (filters doing no modifications don't change undo status) if (snapshotDone) Undo.setup(Undo.FILTER, imp); else Undo.reset(); } if ((flags&PlugInFilter.NO_CHANGES)==0&&(flags&PlugInFilter.KEEP_THRESHOLD)==0) ip.resetBinaryThreshold(); } else { // stack Undo.reset(); // no undo for processing a complete stack IJ.resetEscape(); int slicesToDo = processedAsPreview!=0 ? slices-1 : slices; nPasses *= slicesToDo; if (theFilter instanceof ExtendedPlugInFilter) ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); int threads = 1; if ((flags&PlugInFilter.PARALLELIZE_STACKS)!=0) { threads = Prefs.getThreads(); // multithread support for multiprocessor machines if (threads>slicesToDo) threads = slicesToDo; if (threads>1) slicesForThread = new Hashtable(threads-1); } int startSlice = 1; for (int i=1; i<threads; i++) { // setup the background threads int endSlice = (slicesToDo*i)/threads; if (processedAsPreview!=0 && processedAsPreview<=endSlice) endSlice++; Thread bgThread = new Thread(this, command+" "+startSlice+"-"+endSlice); slicesForThread.put(bgThread, new int[] {startSlice, endSlice}); bgThread.start(); //IJ.log("Stack: Thread for slices "+startSlice+"-"+endSlice+" started"); startSlice = endSlice+1; } //IJ.log("Stack: Slices "+startSlice+"-"+slices+" by main thread"); processStack(startSlice, slices); // the current thread does the rest if (slicesForThread != null) { while (slicesForThread.size()>0) { // for all other threads: Thread theThread = (Thread)slicesForThread.keys().nextElement(); try { theThread.join(); // wait until thread has finished } catch (InterruptedException e) {} slicesForThread.remove(theThread); // and remove it from the list. } } } } // end processing: if ((flags&PlugInFilter.FINAL_PROCESSING)!=0 && !IJ.escapePressed()) ((PlugInFilter)theFilter).setup("final", imp); if (IJ.escapePressed()) { IJ.showStatus(command + " INTERRUPTED"); IJ.showProgress(1,1); } else IJ.showTime(imp, imp.getStartTime()-previewTime, command + ": ", doStack?slices:1); IJ.showProgress(1.0); if (ipChanged) { imp.changes = true; imp.updateAndDraw(); } ImageWindow win = imp.getWindow(); if (win!=null) { win.running = false; win.running2 = false; } imp.unlock(); } /** Process a stack or part of it. The slice given by class variable * processedAsPreview remains unchanged. * @param firstSlice Slice number of the first slice to be processed * @param endSlice Slice number of the last slice to be processed */ private void processStack(int firstSlice, int endSlice) { ImageStack stack = imp.getStack(); ImageProcessor ip = stack.getProcessor(firstSlice); prepareProcessor(ip, imp); ip.setLineWidth(Line.getWidth()); //in contrast to imp.getProcessor, stack.getProcessor does not do this FloatProcessor fp = null; int slices = imp.getNSlices(); for (int i=firstSlice; i<=endSlice; i++) { if (i != processedAsPreview) { announceSliceNumber(i); ip.setPixels(stack.getPixels(i)); ip.setSliceNumber(i); processOneImage(ip, fp, false); if (IJ.escapePressed()) {IJ.beep(); break;} } } } /** prepare an ImageProcessor by setting roi and CalibrationTable. */ private void prepareProcessor(ImageProcessor ip, ImagePlus imp) { ImageProcessor mask = imp.getMask(); Roi roi = imp.getRoi(); if (roi!=null && roi.isArea()) ip.setRoi(roi); else ip.setRoi((Roi)null); if (imp.getStackSize()>1) { ImageProcessor ip2 = imp.getProcessor(); double min1 = ip2.getMinThreshold(); double max1 = ip2.getMaxThreshold(); double min2 = ip.getMinThreshold(); double max2 = ip.getMaxThreshold(); if (min1!=ImageProcessor.NO_THRESHOLD && (min1!=min2||max1!=max2)) ip.setThreshold(min1, max1, ImageProcessor.NO_LUT_UPDATE); } //float[] cTable = imp.getCalibration().getCTable(); //ip.setCalibrationTable(cTable); } /** * Process a single image with the PlugInFilter. * @param ip The image data that should be processed * @param fp A Floatprocessor as a target for conversion to Float. May be null. * @param snapshotDone Whether a snapshot of ip has been be taken previously. if * snapshotDone is true, the snapshot needed for the SUPPORTS_MASKING or SNAPSHOT * flags of the filter is not created any more. * Class variables used: flags (input), snapshotDone (set if a snapshot of ip * is taken), ipChanged (set if ip was probably changed). */ private void processOneImage(ImageProcessor ip, FloatProcessor fp, boolean snapshotDone) { if ((flags&PlugInFilter.PARALLELIZE_IMAGES)!=0) { processImageUsingThreads(ip, fp, snapshotDone); return; } Thread thread = Thread.currentThread(); boolean convertToFloat = (flags&PlugInFilter.CONVERT_TO_FLOAT)!=0 && !(ip instanceof FloatProcessor); boolean doMasking = (flags&PlugInFilter.SUPPORTS_MASKING)!=0 && ip.getMask() != null; if (!snapshotDone && (doMasking || ((flags&PlugInFilter.SNAPSHOT)!=0) && !convertToFloat)) { ip.snapshot(); this.snapshotDone = true; } if (convertToFloat) { for (int i=0; i<ip.getNChannels(); i++) { fp = ip.toFloat(i, fp); fp.setSliceNumber(ip.getSliceNumber()); if (thread.isInterrupted()) return; // interrupt processing for preview? if ((flags&PlugInFilter.SNAPSHOT)!=0) fp.snapshot(); if (doStack) IJ.showProgress(pass/(double)nPasses); ((PlugInFilter)theFilter).run(fp); if (thread.isInterrupted()) return; //IJ.log("slice="+getSliceNumber()+" pass="+pass+"/"+nPasses); pass++; if ((flags&PlugInFilter.NO_CHANGES)==0) { ipChanged = true; ip.setPixels(i, fp); } } } else { if ((flags&PlugInFilter.NO_CHANGES)==0) ipChanged = true; if (doStack) IJ.showProgress(pass/(double)nPasses); ((PlugInFilter)theFilter).run(ip); pass++; } if (thread.isInterrupted()) return; if (doMasking) ip.reset(ip.getMask()); //restore image outside irregular roi } private void processImageUsingThreads(ImageProcessor ip, FloatProcessor fp, boolean snapshotDone) { Thread thread = Thread.currentThread(); boolean convertToFloat = (flags&PlugInFilter.CONVERT_TO_FLOAT)!=0 && !(ip instanceof FloatProcessor); boolean doMasking = (flags&PlugInFilter.SUPPORTS_MASKING)!=0 && ip.getMask() != null; if (!snapshotDone && (doMasking || ((flags&PlugInFilter.SNAPSHOT)!=0) && !convertToFloat)) { ip.snapshot(); this.snapshotDone = true; } if (convertToFloat) { for (int i=0; i<ip.getNChannels(); i++) { fp = ip.toFloat(i, fp); fp.setSliceNumber(ip.getSliceNumber()); if (thread.isInterrupted()) return; // interrupt processing for preview? if ((flags&PlugInFilter.SNAPSHOT)!=0) fp.snapshot(); if (doStack) IJ.showProgress(pass/(double)nPasses); processChannelUsingThreads(fp); if (thread.isInterrupted()) return; //IJ.log("slice="+getSliceNumber()+" pass="+pass+"/"+nPasses); if ((flags&PlugInFilter.NO_CHANGES)==0) { ipChanged = true; ip.setPixels(i, fp); } } } else { if ((flags&PlugInFilter.NO_CHANGES)==0) ipChanged = true; if (doStack) IJ.showProgress(pass/(double)nPasses); processChannelUsingThreads(ip); } if (thread.isInterrupted()) return; if (doMasking) ip.reset(ip.getMask()); //restore image outside irregular roi } private void processChannelUsingThreads(ImageProcessor ip) { ImageProcessor mask = ip.getMask(); Rectangle roi = ip.getRoi(); int threads = Prefs.getThreads(); if (threads>roi.height) threads = roi.height; if (threads>1) roisForThread = new Hashtable(threads-1); int y1 = roi.y; for (int i=1; i<threads; i++) { int y2 = roi.y+(roi.height*i)/threads-1; Thread bgThread = new Thread(this, command+" "+y1+"-"+y2); Rectangle roi2 = new Rectangle(roi.x, y1, roi.width, y2-y1+1); roisForThread.put(bgThread, duplicateProcessor(ip, roi2)); bgThread.start(); //IJ.log("Thread for ROI "+y1+"-"+y2+" started"); y1 = y2+1; } //IJ.log("Rest "+y1+"-"+(roi.height-1)+" by main thread ("+Thread.currentThread()+")"); ip.setRoi(new Rectangle(roi.x, y1, roi.width, roi.y+roi.height-y1)); ((PlugInFilter)theFilter).run(ip); // the current thread does the rest pass++; if (roisForThread != null) { while (roisForThread.size()>0) { // for all other threads: Thread theThread = (Thread)roisForThread.keys().nextElement(); try { theThread.join(); // wait until thread has finished } catch (InterruptedException e) {} roisForThread.remove(theThread); // and remove it from the list. } } roisForThread = null; ip.setMask(mask); // restore ROI ip.setRoi(roi); } ImageProcessor duplicateProcessor(ImageProcessor ip, Rectangle roi) { ImageProcessor ip2 = (ImageProcessor)ip.clone(); ip2.setRoi(roi); return ip2; } /** test whether an ImagePlus can be processed based on the flags specified * and display an error message if not. */ private boolean checkImagePlus(ImagePlus imp, int flags, String cmd) { boolean imageRequired = (flags&PlugInFilter.NO_IMAGE_REQUIRED)==0; if (imageRequired && imp==null) {IJ.noImage(); return false;} if (imageRequired) { if (imp.getProcessor()==null) {wrongType(flags, cmd); return false;} int type = imp.getType(); switch (type) { case ImagePlus.GRAY8: if ((flags&PlugInFilter.DOES_8G)==0) {wrongType(flags, cmd); return false;} break; case ImagePlus.COLOR_256: if ((flags&PlugInFilter.DOES_8C)==0) {wrongType(flags, cmd); return false;} break; case ImagePlus.GRAY16: if ((flags&PlugInFilter.DOES_16)==0) {wrongType(flags, cmd); return false;} break; case ImagePlus.GRAY32: if ((flags&PlugInFilter.DOES_32)==0) {wrongType(flags, cmd); return false;} break; case ImagePlus.COLOR_RGB: if ((flags&PlugInFilter.DOES_RGB)==0) {wrongType(flags, cmd); return false;} break; } if ((flags&PlugInFilter.ROI_REQUIRED)!=0 && imp.getRoi()==null) {IJ.error(cmd, "This command requires a selection"); return false;} if ((flags&PlugInFilter.STACK_REQUIRED)!=0 && imp.getStackSize()==1) {IJ.error(cmd, "This command requires a stack"); return false;} } // if imageRequired return true; } /** Display an error message, telling the allowed image types */ static void wrongType(int flags, String cmd) { String s = "\""+cmd+"\" requires an image of type:\n \n"; if ((flags&PlugInFilter.DOES_8G)!=0) s += " 8-bit grayscale\n"; if ((flags&PlugInFilter.DOES_8C)!=0) s += " 8-bit color\n"; if ((flags&PlugInFilter.DOES_16)!=0) s += " 16-bit grayscale\n"; if ((flags&PlugInFilter.DOES_32)!=0) s += " 32-bit (float) grayscale\n"; if ((flags&PlugInFilter.DOES_RGB)!=0) s += " RGB color\n"; IJ.error(s); } /** Make the slice number accessible to the PlugInFilter by putting it * into the appropriate hashtable. */ private void announceSliceNumber(int slice) { synchronized(sliceForThread){ Integer number = new Integer(slice); sliceForThread.put(Thread.currentThread(), number); } } /** Return the slice number currently processed by the calling thread. * @return The slice number. Returns -1 on error (when not processing). */ public int getSliceNumber() { synchronized(sliceForThread){ Integer number = (Integer)sliceForThread.get(Thread.currentThread()); return (number == null) ? -1 : number.intValue(); } } /** The dispatcher for the background threads */ public void run() { Thread thread = Thread.currentThread(); try { if (thread==previewThread) runPreview(); else if (roisForThread!=null && roisForThread.containsKey(thread)) { ImageProcessor ip = (ImageProcessor)roisForThread.get(thread); ((PlugInFilter)theFilter).run(ip); ip.setPixels(null); ip.setSnapshotPixels(null); } else if (slicesForThread!=null && slicesForThread.containsKey(thread)) { int[] range = (int[])slicesForThread.get(thread); processStack(range[0], range[1]); } else IJ.error("PlugInFilterRunner internal error:\nunsolicited background thread"); } catch (Exception err) { if (thread==previewThread) { gd.previewRunning(false); IJ.wait(100); // needed on Macs previewCheckbox.setState(false); bgPreviewOn = false; previewThread = null; } String msg = ""+err; if (msg.indexOf(Macro.MACRO_CANCELED)==-1) { IJ.beep(); IJ.log("ERROR: "+msg+"\nin "+thread.getName()+ "\nat "+(err.getStackTrace()[0])+"\nfrom "+(err.getStackTrace()[1])); } } } /** The background thread for preview */ private void runPreview() { if (IJ.debugMode) IJ.log("preview thread started; imp="+imp.getTitle()); Thread thread = Thread.currentThread(); ImageProcessor ip = imp.getProcessor(); Roi originalRoi = imp.getRoi(); FloatProcessor fp = null; prepareProcessor(ip, imp); announceSliceNumber(imp.getCurrentSlice()); if (!snapshotDone && (flags&PlugInFilter.NO_CHANGES)==0) { ip.snapshot(); snapshotDone = true; } boolean previewDataOk = false; while(bgPreviewOn) { if (previewCheckboxOn) gd.previewRunning(true); // visual feedback interruptable: { if (imp.getRoi() != originalRoi) { imp.setRoi(originalRoi); // restore roi; the PlugInFilter may have affected it if (originalRoi!=null && originalRoi.isArea()) ip.setRoi(originalRoi); else ip.setRoi((Roi)null); } if (ipChanged) // restore image data if necessary ip.reset(); ipChanged = false; previewDataOk = false; long startTime = System.currentTimeMillis(); pass = 0; if (theFilter instanceof ExtendedPlugInFilter) ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); //this should reset pass in the filter if (thread.isInterrupted()) break interruptable; processOneImage(ip, fp, true); // P R O C E S S (sets ipChanged) IJ.showProgress(1.0); if (thread.isInterrupted()) break interruptable; previewDataOk = true; previewTime = System.currentTimeMillis() - startTime; imp.updateAndDraw(); if (IJ.debugMode) IJ.log("preview processing done"); } gd.previewRunning(false); // optical feedback IJ.showStatus(""); //delete last status messages from processing synchronized(this) { if (!bgPreviewOn) break; //thread should stop and possibly keep the data try { wait(); //wait for interrupted (don't keep preview) or notify (keep preview) } catch (InterruptedException e) { previewDataOk = false; } } // synchronized } // while bgPreviewOn if (thread.isInterrupted()) previewDataOk = false; //interrupted always means "don't keep preview" if (!previewDataOk || !bgKeepPreview) { //no need to keep the result imp.setRoi(originalRoi); //restore roi if (ipChanged) { //revert the image data ip.reset(); ipChanged = false; } } imp.updateAndDraw(); //display current state of image (reset or result of preview) sliceForThread.remove(thread); //no need to announce the slice number any more } /** stop the background process responsible for preview as fast as possible and wait until the preview thread has finished */ private void killPreview() { if (previewThread == null) return; //IJ.log("killPreview"); synchronized (this) { previewThread.interrupt(); //ask for premature finishing (interrupt first -> no keepPreview) bgPreviewOn = false; //tell a possible background thread to terminate when it has finished if (roisForThread!=null) { for (Enumeration en=roisForThread.keys(); en.hasMoreElements();) { Thread thread = (Thread)en.nextElement(); thread.interrupt(); } } } waitForPreviewDone(); } /** stop the background process responsible for preview and wait until the preview thread has finished */ private void waitForPreviewDone() { if (previewThread.isAlive()) try { //a NullPointerException is possible if the thread finishes in the meanwhile previewThread.setPriority(Thread.currentThread().getPriority()); } catch (Exception e) {} synchronized (this) { bgPreviewOn = false; //tell a possible background thread to terminate notify(); //(but finish processing unless interrupted) } try {previewThread.join();} //wait until the background thread is done catch (InterruptedException e){} previewThread = null; } /* set the GenericDialog gd where the preview comes from (if gd is * suitable for listening, i.e., if it has a preview checkbox). */ public void setDialog(GenericDialog gd) { if (gd != null && imp != null) { previewCheckbox = gd.getPreviewCheckbox(); if (previewCheckbox != null) { gd.addDialogListener(this); this.gd = gd; } } //IJ.log("setDialog done"); } /** The listener to any change in the dialog. It is used for preview. * It is invoked every time the user changes something in the dialog * (except OK and cancel buttons), provided that all previous * listeners (parameter checking) have returned true. * * @param e The event that has happened in the dialog. This method may * be also called with e=null, e.g. to start preview already * when the dialog appears. * @return Always true. (The return value determines whether the * dialog will enable the OK button) */ public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { if (previewCheckbox == null || imp == null) return true; previewCheckboxOn = previewCheckbox.getState(); if (previewCheckboxOn && previewThread == null) { bgPreviewOn = true; //need to start a background thread for preview previewThread = new Thread(this, command+" Preview"); int priority = Thread.currentThread().getPriority() - 2; if (priority < Thread.MIN_PRIORITY) priority = Thread.MIN_PRIORITY; previewThread.setPriority(priority); //preview on lower priority than dialog previewThread.start(); if (IJ.debugMode) IJ.log(command+" Preview thread was started"); return true; } if (previewThread != null) { //thread runs already if (!previewCheckboxOn) { //preview toggled off killPreview(); return true; } else previewThread.interrupt(); //all other changes: restart calculating preview (with new parameters) } return true; } }