package ij.plugin.filter; import ij.*; import ij.process.*; import ij.gui.*; import ij.io.*; import ij.plugin.TextReader; import ij.plugin.frame.Recorder; import ij.util.Tools; import java.awt.*; import java.util.*; import java.awt.event.*; import java.io.*; /** This plugin convolves images using user user defined kernels. */ public class Convolver implements ExtendedPlugInFilter, DialogListener, ActionListener { private ImagePlus imp; private int kw, kh; private boolean canceled; private float[] kernel; private boolean isLineRoi; private Button open, save; private GenericDialog gd; private boolean normalize = true; private int nSlices; private int flags = DOES_ALL|CONVERT_TO_FLOAT|SUPPORTS_MASKING|KEEP_PREVIEW|FINAL_PROCESSING|SNAPSHOT; private int nPasses = 1; private boolean kernelError; private PlugInFilterRunner pfr; private Thread mainThread; private int pass; static String kernelText = "-1 -1 -1 -1 -1\n-1 -1 -1 -1 -1\n-1 -1 24 -1 -1\n-1 -1 -1 -1 -1\n-1 -1 -1 -1 -1\n"; static boolean normalizeFlag = true; public int setup(String arg, ImagePlus imp) { this.imp = imp; mainThread = Thread.currentThread(); if (imp==null) {IJ.noImage(); return DONE;} if (arg.equals("final")&&imp.getRoi()==null) { imp.getProcessor().resetMinAndMax(); imp.updateAndDraw(); return DONE; } IJ.resetEscape(); Roi roi = imp.getRoi(); isLineRoi= roi!=null && roi.isLine(); nSlices = imp.getStackSize(); if (imp.getStackSize()==1) flags |= PARALLELIZE_IMAGES; else flags |= PARALLELIZE_STACKS; imp.startTiming(); return flags; } public void run(ImageProcessor ip) { if (canceled) return; if (isLineRoi) ip.resetRoi(); if (!kernelError) convolve(ip, kernel, kw, kh); } public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { gd = new GenericDialog("Convolver...", IJ.getInstance()); gd.addTextAreas(kernelText, null, 10, 30); gd.addPanel(makeButtonPanel(gd)); gd.addCheckbox("Normalize Kernel", normalizeFlag); gd.addPreviewCheckbox(pfr); gd.addDialogListener(this); gd.showDialog(); if (gd.wasCanceled()) return DONE; this.pfr = pfr; return IJ.setupDialog(imp, flags); } public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { kernelText = gd.getNextText(); normalizeFlag = gd.getNextBoolean(); normalize = normalizeFlag; kernelError = !decodeKernel(kernelText); if (!kernelError) { IJ.showStatus("Convolve: "+kw+"x"+kh+" kernel"); return true; } else return !gd.getPreviewCheckbox().getState(); } boolean decodeKernel(String text) { if (Macro.getOptions()!=null && !hasNewLine(text)) return decodeSquareKernel(text); String[] rows = Tools.split(text, "\n"); kh = rows.length; if (kh==0) return false; String[] values = Tools.split(rows[0]); kw = values.length; kernel = new float[kw*kh]; boolean done = gd.wasOKed(); int i = 0; for (int y=0; y<kh; y++) { values = Tools.split(rows[y]); if (values.length!=kw) { String err = "Row "+(y+1)+" is not the same length as the first row"; if (done) IJ.error("Convolver", err); else IJ.showStatus(err); return false; } for (int x=0; x<kw; x++) kernel[i++] = (float)Tools.parseDouble(values[x], 0.0); } if ((kw&1)!=1 || (kh&1)!=1) { String err = "Kernel must have odd width and height. This one is "+kw+"x"+kh+"."; if (done) IJ.error("Convolver", err); else IJ.showStatus(err); return false; } return true; } boolean hasNewLine(String text) { for (int i=0; i<text.length(); i++) { if (text.charAt(i)=='\n') return true; } return false; } boolean decodeSquareKernel(String text) { String[] values = Tools.split(text); int n = values.length; kw = (int)Math.sqrt(n); kh = kw; n = kw*kh; kernel = new float[n]; for (int i=0; i<n; i++) kernel[i] = (float)Tools.parseDouble(values[i]); if (kw>=3 && (kw&1)==1) { StringBuffer sb = new StringBuffer(); int i = 0; for (int y=0; y<kh; y++) { for (int x=0; x<kw; x++) { sb.append(""+kernel[i++]); if (x<kw-1) sb.append(" "); } sb.append("\n"); } kernelText = new String(sb); gd.getTextArea1().setText(new String(sb)); return true; } else { IJ.error("Kernel must be square with odd width. This one is "+kw+"x"+kh+"."); return false; } } /** Creates a panel containing "Save...", "Save..." and "Preview" buttons. */ Panel makeButtonPanel(GenericDialog gd) { Panel buttons = new Panel(); buttons.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0)); open = new Button("Open..."); open.addActionListener(this); buttons.add(open); save = new Button("Save..."); save.addActionListener(this); buttons.add(save); return buttons; } /** Convolves <code>ip</code> with a kernel of width <code>kw</code> and height <code>kh</code>. Returns false if the user cancels the operation. */ public boolean convolve(ImageProcessor ip, float[] kernel, int kw, int kh) { if (canceled || kw*kh!=kernel.length) return false; if ((kw&1)!=1 || (kh&1)!=1) throw new IllegalArgumentException("Kernel width or height not odd ("+kw+"x"+kh+")"); boolean notFloat = !(ip instanceof FloatProcessor); ImageProcessor ip2 = ip; if (notFloat) { if (ip2 instanceof ColorProcessor) throw new IllegalArgumentException("RGB images not supported"); ip2 = ip2.convertToFloat(); } if (kw==1 || kh==1) convolveFloat1D(ip2, kernel, kw, kh); else convolveFloat(ip2, kernel, kw, kh); if (notFloat) { if (ip instanceof ByteProcessor) ip2 = ip2.convertToByte(false); else ip2 = ip2.convertToShort(false); ip.setPixels(ip2.getPixels()); } return !canceled; } public void setNormalize(boolean normalizeKernel) { normalize = normalizeKernel; } /** Convolves the float image <code>ip</code> with a kernel of width <code>kw</code> and height <code>kh</code>. Returns false if the user cancels the operation by pressing 'Esc'. */ public boolean convolveFloat(ImageProcessor ip, float[] kernel, int kw, int kh) { if (!(ip instanceof FloatProcessor)) throw new IllegalArgumentException("FloatProcessor required"); if (canceled) return false; int width = ip.getWidth(); int height = ip.getHeight(); Rectangle r = ip.getRoi(); //boolean nonRectRoi = ip.getMask()!=null; //if (nonRectRoi) // ip.snapshot(); int x1 = r.x; int y1 = r.y; int x2 = x1 + r.width; int y2 = y1 + r.height; int uc = kw/2; int vc = kh/2; float[] pixels = (float[])ip.getPixels(); float[] pixels2 = (float[])ip.getSnapshotPixels(); if (pixels2==null) pixels2 = (float[])ip.getPixelsCopy(); double scale = getScale(kernel); Thread thread = Thread.currentThread(); boolean isMainThread = thread==mainThread || thread.getName().indexOf("Preview")!=-1; if (isMainThread) pass++; double sum; int offset, i; boolean edgePixel; int xedge = width-uc; int yedge = height-vc; long lastTime = System.currentTimeMillis(); for(int y=y1; y<y2; y++) { long time = System.currentTimeMillis(); if (time-lastTime>100) { lastTime = time; if (thread.isInterrupted()) return false; if (isMainThread) { if (IJ.escapePressed()) { canceled = true; ip.reset(); ImageProcessor originalIp = imp.getProcessor(); if (originalIp.getNChannels() > 1) originalIp.reset(); return false; } showProgress((y-y1)/(double)(y2-y1)); } } for(int x=x1; x<x2; x++) { if (canceled) return false; sum = 0.0; i = 0; edgePixel = y<vc || y>=yedge || x<uc || x>=xedge; for(int v=-vc; v <= vc; v++) { offset = x+(y+v)*width; for(int u = -uc; u <= uc; u++) { if (edgePixel) { if (i>=kernel.length) // work around for JIT compiler bug on Linux IJ.log("kernel index error: "+i); sum += getPixel(x+u, y+v, pixels2, width, height)*kernel[i++]; } else sum += pixels2[offset+u]*kernel[i++]; } } pixels[x+y*width] = (float)(sum*scale); } } //if (nonRectRoi) // ip.reset(ip.getMask()); return true; } /** Convolves the image <code>ip</code> with a kernel of width <code>kw</code> and height <code>kh</code>. */ void convolveFloat1D(ImageProcessor ip, float[] kernel, int kw, int kh) { if (!(ip instanceof FloatProcessor)) throw new IllegalArgumentException("FloatProcessor required"); int width = ip.getWidth(); int height = ip.getHeight(); Rectangle r = ip.getRoi(); int x1 = r.x; int y1 = r.y; int x2 = x1 + r.width; int y2 = y1 + r.height; int uc = kw/2; int vc = kh/2; float[] pixels = (float[])ip.getPixels(); float[] pixels2 = (float[])ip.getSnapshotPixels(); if (pixels2==null) pixels2 = (float[])ip.getPixelsCopy(); double scale = getScale(kernel); boolean vertical = kw==1; double sum; int offset, i; boolean edgePixel; int xedge = width-uc; int yedge = height-vc; for(int y=y1; y<y2; y++) { for(int x=x1; x<x2; x++) { sum = 0.0; i = 0; if (vertical) { edgePixel = y<vc || y>=yedge; offset = x+(y-vc)*width; for(int v=-vc; v<=vc; v++) { if (edgePixel) sum += getPixel(x+uc, y+v, pixels2, width, height)*kernel[i++]; else sum += pixels2[offset+uc]*kernel[i++]; offset += width; } } else { edgePixel = x<uc || x>=xedge; offset = x+(y-vc)*width; for(int u = -uc; u<=uc; u++) { if (edgePixel) sum += getPixel(x+u, y+vc, pixels2, width, height)*kernel[i++]; else sum += pixels2[offset+u]*kernel[i++]; } } pixels[x+y*width] = (float)(sum*scale); } } } double getScale(float[] kernel) { double scale = 1.0; if (normalize) { double sum = 0.0; for (int i=0; i<kernel.length; i++) sum += kernel[i]; if (sum!=0.0) scale = (float)(1.0/sum); } return scale; } private float getPixel(int x, int y, float[] pixels, int width, int height) { if (x<=0) x = 0; if (x>=width) x = width-1; if (y<=0) y = 0; if (y>=height) y = height-1; return pixels[x+y*width]; } void save() { TextArea ta1 = gd.getTextArea1(); ta1.selectAll(); String text = ta1.getText(); ta1.select(0, 0); if (text==null || text.length()==0) return; text += "\n"; SaveDialog sd = new SaveDialog("Save as Text...", "kernel", ".txt"); String name = sd.getFileName(); if (name == null) return; String directory = sd.getDirectory(); PrintWriter pw = null; try { FileOutputStream fos = new FileOutputStream(directory+name); BufferedOutputStream bos = new BufferedOutputStream(fos); pw = new PrintWriter(bos); } catch (IOException e) { IJ.error("" + e); return; } IJ.wait(250); // give system time to redraw ImageJ window pw.print(text); pw.close(); } void open() { OpenDialog od = new OpenDialog("Open Kernel...", ""); String directory = od.getDirectory(); String name = od.getFileName(); if (name==null) return; String path = directory + name; TextReader tr = new TextReader(); ImageProcessor ip = tr.open(path); if (ip==null) return; int width = ip.getWidth(); int height = ip.getHeight(); if ((width&1)!=1 || (height&1)!=1) { IJ.error("Convolver", "Kernel must be have odd width and height"); return; } StringBuffer sb = new StringBuffer(); boolean integers = true; for (int y=0; y<height; y++) { for (int x=0; x<width; x++) { double v = ip.getPixelValue(x, y); if ((int)v!=v) integers = false; } } for (int y=0; y<height; y++) { for (int x=0; x<width; x++) { if (x!=0) sb.append(" "); double v = ip.getPixelValue(x, y); if (integers) sb.append(IJ.d2s(ip.getPixelValue(x, y),0)); else sb.append(""+ip.getPixelValue(x, y)); } if (y!=height-1) sb.append("\n"); } gd.getTextArea1().setText(new String(sb)); } public void setNPasses(int nPasses) { this.nPasses = nPasses; pass = 0; } private void showProgress(double percent) { percent = (double)(pass-1)/nPasses + percent/nPasses; IJ.showProgress(percent); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); Recorder.disablePathRecording(); if (source==save) save(); else if (source==open) open(); } }