001/*******************************************************************************
002 * This software is provided as a supplement to the authors' textbooks on digital
003 * image processing published by Springer-Verlag in various languages and editions.
004 * Permission to use and distribute this software is granted under the BSD 2-Clause
005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause).
006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved.
007 * Visit https://imagingbook.com for additional details.
008 ******************************************************************************/
009
010package imagingbook.common.ij;
011
012import ij.IJ;
013import ij.ImagePlus;
014import ij.WindowManager;
015import ij.gui.GenericDialog;
016import ij.io.Opener;
017import ij.plugin.PlugIn;
018import ij.plugin.filter.Convolver;
019import ij.plugin.filter.PlugInFilter;
020import ij.plugin.filter.PlugInFilterRunner;
021import ij.process.ByteProcessor;
022import ij.process.ColorProcessor;
023import ij.process.FloatProcessor;
024import ij.process.ImageProcessor;
025import ij.process.ShortProcessor;
026import imagingbook.common.color.RgbUtils;
027import imagingbook.common.geometry.basic.Pnt2d;
028import imagingbook.common.geometry.basic.Pnt2d.PntInt;
029import imagingbook.common.math.Matrix;
030import imagingbook.common.util.bits.BitMap;
031
032import java.awt.Rectangle;
033import java.io.File;
034import java.lang.reflect.InvocationTargetException;
035import java.net.URI;
036import java.nio.file.Paths;
037import java.util.ArrayList;
038import java.util.Arrays;
039import java.util.Comparator;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Objects;
043
044/**
045 * This class defines static utility methods adding to ImageJs functionality.
046 *
047 * @author WB
048 */
049public abstract class IjUtils {
050        
051        private IjUtils() {}
052
053        /**
054         * Returns a (possibly empty) array of ImagePlus objects that are sorted by their titles if the 'sortByTitle' flag
055         * is set.
056         *
057         * @param sortByTitle flag, result is sorted if true.
058         * @return an array of currently open images.
059         */
060        public static ImagePlus[] getOpenImages(boolean sortByTitle) {
061                return getOpenImages(sortByTitle, null);
062        }
063
064        /**
065         * Returns an array of strings containing the short titles of the images supplied.
066         *
067         * @param images array of images.
068         * @return array of names.
069         */
070        public static String[] getImageShortTitles(ImagePlus[] images) {
071                String[] imageNames = new String[images.length];
072                for (int i = 0; i < images.length; i++) {
073                        imageNames[i] = images[i].getShortTitle();
074                }
075                return imageNames;
076        }
077
078        /**
079         * Opens a dialog to let the user select one of the currently open images.
080         *
081         * @param title string to show in the dialog
082         * @return a {@link ImagePlus} object, use the getProcessor method to obtain the associated {@link ImageProcessor}
083         */
084        public static ImagePlus selectOpenImage(String title) {
085                ImagePlus[] openImages = getOpenImages(true, null);
086                String[] imageNames = getImageShortTitles(openImages);
087                if (title == null) {
088                        title = "image:";
089                }
090                GenericDialog gd = new GenericDialog("Select image");
091                gd.addChoice(title, imageNames, imageNames[0]);
092                gd.showDialog(); 
093                if (gd.wasCanceled()) 
094                        return null;
095                else {
096                        return openImages[gd.getNextChoiceIndex()];
097                }
098        }
099
100
101        /**
102         * Returns a (possibly empty) array of {@link ImagePlus} objects that are sorted by their titles if the sortByTitle
103         * flag is set. The image "exclude" (typically the current image) is not included in the returned array (pass null
104         * to exclude no image).
105         *
106         * @param sortByTitle set {@code true} to return images sorted by title
107         * @param exclude reference to an image to be excluded (may be {@code null})
108         * @return a (possibly empty) array of {@link ImagePlus} objects
109         */
110        public static ImagePlus[] getOpenImages(boolean sortByTitle, ImagePlus exclude) {
111                List<ImagePlus> imgList = new LinkedList<ImagePlus>();
112                int[] wList = WindowManager.getIDList();
113        if (wList != null) {
114                for (int i : wList) {
115                    ImagePlus imp = WindowManager.getImage(i);
116                    if (imp != null && imp != exclude) {
117                        imgList.add(imp);
118                    }
119                }
120        }
121        ImagePlus[] impArr = imgList.toArray(new ImagePlus[0]);
122        if (sortByTitle) {
123                Comparator<ImagePlus> cmp = new Comparator<ImagePlus>() {
124                        @Override
125                                public int compare(ImagePlus impA, ImagePlus impB) {
126                                return impA.getTitle().compareTo(impB.getTitle());
127                        }
128                };
129                Arrays.sort(impArr, cmp);
130        }
131                return impArr;
132        }
133        
134        @SuppressWarnings("unused")
135        private static String encodeURL(String url) {
136                //url = url.replaceAll(" ","%20");      // this doesn't work with spaces
137                url = url.replace('\\','/');
138                return url;
139        }
140        
141        //----------------------------------------------------------------------
142
143        /**
144         * Creates an ImageJ {@link ImagePlus} image for the matrix {@code M[r][c]} (2D array), where {@code r} is treated
145         * as the row (vertical) coordinate and {@code c} is treated as the column (horizontal) coordinate. Use
146         * {@code show()} to display the resulting image.
147         *
148         * @param title image title
149         * @param M 2D array
150         * @return a new {@link ImagePlus} image
151         */
152        public static ImagePlus createImage(String title, float[][] M) {
153                FloatProcessor fp = new FloatProcessor(M[0].length, M.length);
154                for (int u = 0; u < M[0].length; u++) {
155                        for (int v = 0; v < M.length; v++) {
156                                fp.setf(u, v, M[v][u]);
157                        }
158                }
159                return new ImagePlus(title, fp);
160        }
161
162
163        /**
164         * Creates an ImageJ {@link ImagePlus} image for the matrix {@code M[r][c]} (2D array), where {@code r} is treated
165         * as the row (vertical) coordinate and {@code c} is treated as the column (horizontal) coordinate. Use
166         * {@code show()} to display the resulting image.
167         *
168         * @param title the image title
169         * @param M a 2D array holding the image data
170         * @return a new {@link ImagePlus} instance
171         */
172        public static ImagePlus createImage(String title, double[][] M) {
173                FloatProcessor fp = new FloatProcessor(M[0].length, M.length);
174                for (int u = 0; u < M[0].length; u++) {
175                        for (int v = 0; v < M.length; v++) {
176                                fp.setf(u, v, (float) M[v][u]);
177                        }
178                }
179                return new ImagePlus(title, fp);
180        }
181
182        /**
183         * Sets the weighing factors for the color components used in RGB-to-grayscale conversion for the specified image
184         * {@code ip}. Note that this method can be applied to any {@link ImageProcessor} instance but has no effect unless
185         * {@code ip} is of type {@link ColorProcessor}. Applies standard (ITU-709) weights.
186         *
187         * @param ip the affected image
188         */
189        public static void setRgbConversionWeights(ImageProcessor ip) {
190                setRgbConversionWeights(ip, 0.299, 0.587, 0.114);
191        }
192
193        /**
194         * Sets the weighing factors for the color components used in RGB-to-grayscale conversion for the specified image
195         * {@code ip}. Note that this method can be applied to any {@link ImageProcessor} instance but has no effect unless
196         * {@code ip} is of type {@link ColorProcessor}.
197         *
198         * @param ip the affected image
199         * @param wr red component weight
200         * @param wg green component weight
201         * @param wb blue component weight
202         */
203        public static void setRgbConversionWeights(ImageProcessor ip, double wr, double wg, double wb) {
204                if (ip instanceof ColorProcessor) {
205                        ((ColorProcessor) ip).setRGBWeights(wr, wg, wb);
206                }
207        }
208        
209        // -------------------------------------------------------------------
210
211        /**
212         * Extracts (crops) a rectangular region from the given image and returns it as a new image (of the same sub-type of
213         * {@link ImageProcessor}). If the specified rectangle extends outside the source image, only the overlapping region
214         * is cropped. Thus the returned image may have smaller size than the specified rectangle. An exception is thrown if
215         * the specified width or height is less than 1. {@code null} is returned if the rectangle does not overlap the
216         * image at all.
217         *
218         * @param <T> the generic image type
219         * @param ip the image to be cropped
220         * @param x the left corner coordinate of the cropping rectangle
221         * @param y the top corner coordinate of the cropping rectangle
222         * @param width the width of the cropping rectangle
223         * @param height the height of the cropping rectangle
224         * @return the cropped image
225         */
226        @SuppressWarnings("unchecked")
227        public static <T extends ImageProcessor> T crop(T ip, int x, int y, int width, int height) {
228//              if (x < 0 || x >= ip.getWidth() || y < 0 || y >= ip.getHeight()) {
229//                      throw new IllegalArgumentException("(x,y) must be inside the image");
230//              }
231                if (width < 1 || height < 1) {
232                        throw new IllegalArgumentException("crop width/height must be at least 1");
233                }
234//              if (x + width > ip.getWidth() || y + height > ip.getHeight()) {
235//                      throw new IllegalArgumentException("crop rectangle must not extend outside image");
236//              }
237                T ipc = null;
238                synchronized (ip) {
239                        Rectangle roiOrig = ip.getRoi();
240                        ip.setRoi(x, y, width, height);
241                        Rectangle roiTmp = ip.getRoi();
242                        if (roiTmp.width > 0 && roiTmp.height > 0) {
243                                ipc = (T) ip.crop();
244                        }
245                        else {
246                                throw new IllegalArgumentException("empty crop rectangle");
247                        }
248                        ip.setRoi(roiOrig);
249                }
250                return ipc;
251        }
252        
253        // -------------------------------------------------------------------
254
255        /**
256         * Returns a copy of the pixel data as a 2D double array with dimensions [x = 0,..,width-1][y = 0,..,height-1].
257         *
258         * @param fp the image
259         * @return the resulting array
260         */
261        public static double[][] toDoubleArray(FloatProcessor fp) {
262                final int width = fp.getWidth();
263                final int height = fp.getHeight();
264                float[] fPixels = (float[]) fp.getPixels();
265                double[][] dPixels = new double[width][height];
266                int i = 0;
267                for (int v = 0; v < height; v++) {
268                        for (int u = 0; u < width; u++) {
269                                dPixels[u][v] = fPixels[i];
270                                i++;
271                        }
272                }
273                return dPixels;
274        }
275
276        /**
277         * Creates a new {@link FloatProcessor} instance of size width x height from the given {@code double[][]} with
278         * dimensions [x = 0,..,width-1][y = 0,..,height-1].
279         *
280         * @param A a 2D {@code double} array
281         * @return a new {@link FloatProcessor} instance
282         */
283        public static FloatProcessor toFloatProcessor(double[][] A) {
284                final int width = A.length;
285                final int height = A[0].length;
286                float[] fPixels = new float[width * height];
287                int i = 0;
288                for (int v = 0; v < height; v++) {
289                        for (int u = 0; u < width; u++) {
290                                fPixels[i] = (float) A[u][v];
291                                i++;
292                        }
293                }
294                return new FloatProcessor(width, height, fPixels);
295        }
296
297        /**
298         * Creates a new {@link FloatProcessor} instance of size width x height from the given {@code float[][]} with
299         * dimensions [x = 0,..,width-1][y = 0,..,height-1].
300         *
301         * @param A a 2D {@code float} array
302         * @return a new {@link FloatProcessor} instance
303         */
304        public static FloatProcessor toFloatProcessor(float[][] A) {
305//              final int width = A.length;
306//              final int height = A[0].length;
307//              float[] fPixels = new float[width * height];
308//              int i = 0;
309//              for (int v = 0; v < height; v++) {
310//                      for (int u = 0; u < width; u++) {
311//                              fPixels[i] = A[u][v];
312//                              i++;
313//                      }
314//              }
315//              return new FloatProcessor(width, height, fPixels);
316                return new FloatProcessor(A);
317        }
318
319        /**
320         * Converts a {@link FloatProcessor} to a {@code float[][]}.
321         *
322         * @param fp a {@link FloatProcessor}
323         * @return the resulting {@code float[][]}
324         */
325        public static float[][] toFloatArray(FloatProcessor fp) {
326                return fp.getFloatArray();
327        }
328
329        /**
330         * Converts the given RGB {@link ColorProcessor} to a scalar-valued {@link ByteProcessor}, using clearly specified
331         * RGB component weights. The processor's individual RGB component weights are used if they have been set (not
332         * null), otherwise ITU709 weights (see {@link RgbUtils#ITU709RgbWeights}) are applied. This is to avoid problems
333         * with standard conversion methods in ImageJ, which depend on a variety of factors (including current user
334         * settings). See also {@link ColorProcessor#getRGBWeights()}, {@link ColorProcessor#setRGBWeights(double[])},
335         * {@link ImageProcessor#convertToByteProcessor()}.
336         *
337         * @param cp a {@link ColorProcessor}
338         * @return the resulting {@link ByteProcessor}
339         * @see #toByteProcessor(ColorProcessor, double[])
340         */
341        public static ByteProcessor toByteProcessor(ColorProcessor cp) {
342                if (cp.getRGBWeights() == null) {       // no weights are set
343                        return toByteProcessor(cp, null);
344                }
345                else {  // use the ColorProcessor's own weights
346                        return cp.convertToByteProcessor();
347                }
348        }
349
350        /**
351         * Converts the given RGB {@link ColorProcessor} to a scalar-valued {@link ByteProcessor}, applying the specified
352         * set of RGB component weights. The processor's individual weights (if set) are ignored. This is to avoid problems
353         * with standard conversion methods in ImageJ, which depend on a variety of factors (including current user
354         * settings). See also {@link ColorProcessor#getRGBWeights()}, {@link ColorProcessor#setRGBWeights(double[])},
355         * {@link ImageProcessor#convertToByteProcessor()}.
356         *
357         * @param cp a {@link ColorProcessor}
358         * @param rgbWeights a 3-vector of RGB component weights (must sum to 1)
359         * @return the resulting {@link ByteProcessor}
360         * @see RgbUtils#ITU601RgbWeights
361         * @see RgbUtils#ITU709RgbWeights
362         */
363        public static ByteProcessor toByteProcessor(ColorProcessor cp, double[] rgbWeights) {
364                if (rgbWeights == null) {
365                        rgbWeights = RgbUtils.getDefaultWeights();
366                }
367                if (rgbWeights.length != 3) {
368                        throw new IllegalArgumentException("rgbWeights must be of length 3");
369                }
370                double[] oldweights = cp.getRGBWeights();
371                cp.setRGBWeights(rgbWeights);
372                ByteProcessor bp = cp.convertToByteProcessor();
373                cp.setRGBWeights(oldweights);
374                return bp;
375        }
376
377        /**
378         * Converts the given RGB {@link ColorProcessor} to a scalar-valued {@link FloatProcessor}, using clearly specified
379         * RGB component weights. The processor's individual RGB component weights are used if set, otherweise default
380         * weights are used (see {@link RgbUtils#getDefaultWeights()}). This should avoid problems with standard conversion
381         * methods in ImageJ, which depend on a variety of factors (including current user settings). See also
382         * {@link ColorProcessor#getRGBWeights()}, {@link ColorProcessor#setRGBWeights(double[])},
383         * {@link ImageProcessor#convertToFloatProcessor()}.
384         *
385         * @param cp a {@link ColorProcessor}
386         * @return the resulting {@link FloatProcessor}
387         * @see #toFloatProcessor(ColorProcessor, double[])
388         */
389        public static FloatProcessor toFloatProcessor(ColorProcessor cp) {
390                if (cp.getRGBWeights() == null) {       // no weights are set
391                        return toFloatProcessor(cp, null);
392                }
393                else {  // use the FloatProcessor's individual weights
394                        return cp.convertToFloatProcessor();
395                }
396        }
397
398        /**
399         * Converts the given RGB {@link ColorProcessor} to a scalar-valued {@link FloatProcessor}, applying the specified
400         * set of RGB component weights. If {@code null} is passed for the weights, default weights are used (see
401         * {@link RgbUtils#getDefaultWeights()}). The processor's individual weights (if set at all) are ignored. This
402         * should avoid problems with standard conversion methods in ImageJ, which depend on a variety of factors (including
403         * current user settings). See also {@link ColorProcessor#getRGBWeights()},
404         * {@link ColorProcessor#setRGBWeights(double[])}, {@link ImageProcessor#convertToFloatProcessor()}.
405         *
406         * @param cp a {@link ColorProcessor}
407         * @param rgbWeights a 3-vector of RGB component weights (must sum to 1)
408         * @return the resulting {@link FloatProcessor}
409         * @see RgbUtils#ITU601RgbWeights
410         * @see RgbUtils#ITU709RgbWeights
411         */
412        public static FloatProcessor toFloatProcessor(ColorProcessor cp, double[] rgbWeights) {
413                if (rgbWeights == null) {
414                        rgbWeights = RgbUtils.getDefaultWeights();
415                }
416                if (rgbWeights.length != 3) {
417                        throw new IllegalArgumentException("rgbWeights must be of length 3");
418                }
419                double[] oldweights = cp.getRGBWeights();
420                cp.setRGBWeights(rgbWeights);
421                FloatProcessor fp = cp.convertToFloatProcessor();
422                cp.setRGBWeights(oldweights);
423                return fp;
424        }
425
426        /**
427         * Creates and returns a new {@link ByteProcessor} from the specified 2D {@code byte} array, assumed to be arranged
428         * in the form {@code A[x][y]}, i.e., the first coordinate is horizontal, the second vertical. Thus {@code A.length}
429         * is the width and {@code A[0].length} the height of the resulting image.
430         *
431         * @param A a 2D {@code byte} array
432         * @return a new {@link ByteProcessor} of size {@code A.length} x {@code A[0].length}
433         */
434        public static ByteProcessor toByteProcessor(byte[][] A) {
435                final int w = A.length;
436                final int h = A[0].length;
437                ByteProcessor bp = new ByteProcessor(w, h);
438                for (int v = 0; v < h; v++) {
439                        for (int u = 0; u < w; u++) {
440                                bp.putPixel(u, v, 0xFF & A[u][v]);
441                        }
442                }
443                return bp;
444        }
445
446        /**
447         * Creates and returns a new {@code byte[][]} from the specified {@link ByteProcessor}. The resulting array is
448         * arranged in the form {@code A[x][y]}, i.e., the first coordinate is horizontal, the second vertical. Thus
449         * {@code A.length} is the width and {@code A[0].length} the height of the image.
450         *
451         * @param bp a {@link ByteProcessor}
452         * @return a 2D {@code byte} array
453         */
454        public static byte[][] toByteArray(ByteProcessor bp) {
455                final int w = bp.getWidth();
456                final int h = bp.getHeight();
457                byte[][] A = new byte[w][h];
458                for (int v = 0; v < h; v++) {
459                        for (int u = 0; u < w; u++) {
460                                A[u][v] =  (byte) (0xFF & bp.get(u, v));
461                        }
462                }
463                return A;
464        }
465
466        /**
467         * Creates and returns a new {@link ByteProcessor} from the specified 2D {@code int} array, assumed to be arranged
468         * in the form {@code A[x][y]}, i.e., the first coordinate is horizontal, the second vertical. Thus {@code A.length}
469         * is the width and {@code A[0].length} the height of the resulting image. Pixel values are clamped to [0, 255].
470         *
471         * @param A a 2D {@code int} array
472         * @return a new {@link ByteProcessor} of size {@code A.length} x {@code A[0].length}
473         */
474        public static ByteProcessor toByteProcessor(int[][] A) {
475                final int w = A.length;
476                final int h = A[0].length;
477                ByteProcessor bp = new ByteProcessor(w, h);
478                for (int v = 0; v < h; v++) {
479                        for (int u = 0; u < w; u++) {
480                                int val = A[u][v];
481                                if (val < 0)
482                                        val = 0;
483                                else if (val > 255) 
484                                        val = 255;
485                                bp.putPixel(u, v, val);
486                        }
487                }
488                return bp;
489        }
490
491        /**
492         * Creates and returns a new {@code int[][]} from the specified {@link ByteProcessor}. The resulting array is
493         * arranged in the form {@code A[x][y]}, i.e., the first coordinate is horizontal, the second vertical. Thus
494         * {@code A.length} is the width and {@code A[0].length} the height of the image.
495         *
496         * @param bp a {@link ByteProcessor}
497         * @return a 2D {@code int} array
498         */
499        public static int[][] toIntArray(ByteProcessor bp) {
500                return bp.getIntArray();
501        }
502
503        /**
504         * Opens the image from the specified {@link URI} and returns it as a {@link ImagePlus} instance.
505         *
506         * @param uri the URI leading to the image (including extension)
507         * @return a new {@link ImagePlus} instance or {@code null} if unable to open
508         */
509        public static ImagePlus openImage(URI uri) {
510                Objects.requireNonNull(uri);
511                return new Opener().openImage(uri.toString());
512        }
513
514        /**
515         * Opens the image from the specified filename and returns it as a {@link ImagePlus} instance.
516         *
517         * @param filename the path and filename to be opened
518         * @return a new {@link ImagePlus} instance or {@code null} if unable to open
519         */
520        public static ImagePlus openImage(String filename) {
521                return openImage(new File(filename).toURI());
522        }
523        
524        
525        // Methods for checking/comparing images (primarily used for testing)  ---------------------
526
527        /**
528         * Checks if two images are of the same type.
529         *
530         * @param ip1 the first image
531         * @param ip2 the second image
532         * @return true if both images have the same type
533         */
534        public static boolean sameType(ImageProcessor ip1, ImageProcessor ip2) {
535                return ip1.getClass().equals(ip2.getClass());
536        }
537        
538        /**
539         * Checks if two images have the same size.
540         * 
541         * @param ip1 the first image
542         * @param ip2 the second image
543         * @return true if both images have the same size
544         */
545        public static boolean sameSize(ImageProcessor ip1, ImageProcessor ip2) {
546                return ip1.getWidth() == ip2.getWidth() && ip1.getHeight() == ip2.getHeight();
547        }
548
549        /**
550         * Checks if the given image is possibly a binary image. This requires that the image contains at most
551         * <strong>two</strong> different pixel values, one of (the 'background' value) <strong>which must be zero</strong>.
552         * Also returns true if the image is filled with zeros or a single nonzero value. All pixels are checked. This
553         * should work for all image types. More efficient implementations are certainly possible.
554         *
555         * @param ip the image ({@link ImageProcessor}) to be checked
556         * @return true if the image is possibly binary
557         */
558        public static boolean isBinary(ImageProcessor ip) {
559                final int width = ip.getWidth();
560                final int height = ip.getHeight();
561                int fgVal = 0;
562
563                outer:
564                for (int v = 0; v < height; v++) {
565                        for (int u = 0; u < width; u++) {
566                                int val = 0x007FFFFF & ip.get(u, v); // = mantissa in case of float
567                                if (val != 0) {
568                                        if (fgVal == 0) {       // first non-zero (foreground) value
569                                                fgVal = val;
570                                        }
571                                        else if (val != fgVal) {        // found another non-zero value
572                                                return false;
573                                        }
574                                }
575                        }
576                }
577                
578                return true;
579        }
580
581        /**
582         * Checks if the given image is "flat", i.e., all pixels have the same value. This should work for all image types.
583         *
584         * @param ip the image ({@link ImageProcessor}) to be checked
585         * @return true if the image is flat
586         */
587        public static boolean isFlat(ImageProcessor ip) {
588                final int width = ip.getWidth();
589                final int height = ip.getHeight();
590                boolean flat = true;
591                int fgVal = ip.get(0, 0);
592                
593                outer:
594                for (int v = 0; v < height; v++) {
595                        for (int u = 0; u < width; u++) {
596                                if (ip.get(u, v) != fgVal) {
597                                        return false;
598                                }
599                        }
600                }
601                
602                return true;
603        }
604
605        /**
606         * Collects all image coordinates with non-zero pixel values into an array of 2D points ({@link Pnt2d}).
607         *
608         * @param ip an image (of any type)
609         * @return an array of 2D points
610         */
611        public static Pnt2d[] collectNonzeroPoints(ImageProcessor ip) {
612                List<Pnt2d> points = new ArrayList<>();
613                int M = ip.getWidth();
614                int N = ip.getHeight();
615                for (int v = 0; v < N; v++) {
616                        for (int u = 0; u < M; u++) {
617                                int val = 0x007FFFFF & ip.get(u, v); // = mantissa in case of float
618                                if (val != 0) {
619                                        points.add(PntInt.from(u, v));
620                                }
621                        }
622                }
623                return points.toArray(new Pnt2d[0]);
624        }
625        
626        // -----------------------------------------------------------------
627        
628        public static final double DefaultMatchTolerance = 1E-6;
629
630        /**
631         * Checks if two images have the same type, size and content (using {@link #DefaultMatchTolerance} for float
632         * images).
633         *
634         * @param ip1 the first image
635         * @param ip2 the second image
636         * @return true if both images have the same type and content
637         */
638        public static boolean match(ImageProcessor ip1, ImageProcessor ip2) {
639                // TODO: check redundancy with ImageTestUtils.match() - same names but slightly differently implemented!
640                return match(ip1, ip2, DefaultMatchTolerance);
641        }
642
643        /**
644         * Checks if two images have the same type, size and values (using the specified tolerance for float images).
645         *
646         * @param ip1 the first image
647         * @param ip2 the second image
648         * @param tolerance the matching tolerance
649         * @return true if both images have the same type, size and content
650         */
651        public static boolean match(ImageProcessor ip1, ImageProcessor ip2, double tolerance) {
652                if (!sameType(ip1, ip2)) {
653                        return false;
654                }
655                if (!sameSize(ip1, ip2)) {
656                        return false;
657                }
658                
659                if (ip1 instanceof ByteProcessor) {
660                        return Arrays.equals((byte[]) ip1.getPixels(), (byte[]) ip2.getPixels());
661                }
662                else if (ip1 instanceof ShortProcessor) {
663                        return Arrays.equals((short[]) ip1.getPixels(), (short[]) ip2.getPixels());
664                }
665                else if (ip1 instanceof ColorProcessor) {
666                        return Arrays.equals((int[]) ip1.getPixels(), (int[]) ip2.getPixels());
667                }
668                
669                else if (ip1 instanceof FloatProcessor) {
670                        final float[] p1 = (float[]) ip1.getPixels();
671                        final float[] p2 = (float[]) ip2.getPixels();
672                        final float toleranceF = (float) tolerance;
673                        boolean same = true;
674                        for (int i = 0; i < p1.length; i++) {
675                                if (Math.abs(p1[i] - p2[i]) > toleranceF) {
676                                        same = false;
677                                        break;
678                                }
679                        }
680                        return same;
681                }
682
683                throw new IllegalArgumentException("unknown processor type " + ip1.getClass().getSimpleName());
684        }
685        
686        // BitMap from/to ByteProcessor conversion
687
688        /**
689         * Converts the specified {@link ByteProcessor} to a {@link BitMap} of the same size, with all zero values set to 0
690         * and non-zero values set to 1.
691         *
692         * @param bp a {@link ByteProcessor}
693         * @return the corresponding {@link BitMap}
694         * @see #convertToByteProcessor(BitMap)
695         */
696        public static BitMap convertToBitMap(ByteProcessor bp) {
697                return new BitMap(bp.getWidth(), bp.getHeight(), (byte[]) bp.getPixels());
698        }
699
700        /**
701         * <p>
702         * Converts the specified {@link BitMap} to a {@link ByteProcessor} of the same size, with all zero values set to 0
703         * and non-zero values set to 1. The resulting image should be multiplied by 255 to achieve full contrast, e.g.:
704         * </p>
705         * <pre>
706         * ByteProcessor bp1 = ... // some ByteProcessor
707         * BitMap bm = IjUtils.convertToBitMap(bp);
708         * ByteProcessor bp2 = IjUtils.convertToByteProcessor(bm);
709         * bp2.multiply(255);
710         * ...
711         * </pre>
712         *
713         * @param bitmap a {@link BitMap}
714         * @return the corresponding {@link ByteProcessor}
715         * @see #convertToBitMap(ByteProcessor)
716         */
717        public static ByteProcessor convertToByteProcessor(BitMap bitmap) {
718                byte[] pixels = bitmap.getBitVector().toByteArray();
719                return new ByteProcessor(bitmap.getWidth(), bitmap.getHeight(), pixels);
720        }
721
722        /**
723         * Draws the given set of points onto the specified image (by setting the corresponding pixels).
724         *
725         * @param ip the image to draw to
726         * @param points the 2D points
727         * @param value the pixel value to use
728         */
729        public static void drawPoints(ImageProcessor ip, Pnt2d[] points, int value) {
730                for (int i = 0; i < points.length; i++) {
731                        Pnt2d p = points[i];
732                        if (p != null) {
733                                int u = p.getXint();
734                                int v = p.getYint();
735                                ip.putPixel(u, v, value);
736                        }
737                }
738        }
739        
740        // -------------------------------------------------------------------------
741
742        /**
743         * Runs the given {@link PlugInFilter} instance with empty argument string.
744         *
745         * @param pluginfilter an instance of {@link PlugInFilter}
746         * @return true if no exception was thrown
747         */
748        public static boolean run(PlugInFilter pluginfilter) {
749                return run(pluginfilter, "");
750        }
751
752        /**
753         * Runs the given {@link PlugInFilter} instance.
754         *
755         * @param pluginfilter an instance of {@link PlugInFilter}
756         * @param arg argument passed to {@link PlugInFilter#setup(String, ImagePlus)}
757         * @return true if no exception was thrown
758         */
759        public static boolean run(PlugInFilter pluginfilter, String arg) {
760                try {
761                        new PlugInFilterRunner(pluginfilter, pluginfilter.getClass().getSimpleName(), arg);
762                } catch (Exception e) {
763                        return false;
764                }
765                return true;
766        }
767
768        /**
769         * Runs the given {@link PlugIn} instance with empty argument string.
770         *
771         * @param plugin an instance of {@link PlugIn}
772         * @return true if no exception was thrown
773         */
774        public static boolean run(PlugIn plugin) {
775                return run(plugin, "");
776        }
777
778        /**
779         * Runs the given {@link PlugIn} instance.
780         *
781         * @param plugin an instance of {@link PlugIn}
782         * @param arg argument passed to {@link PlugIn#run(String)}
783         * @return true if no exception was thrown
784         */
785        public static boolean run(PlugIn plugin, String arg) {
786                try {
787                        plugin.run(arg);
788                } catch (Exception e) {
789                        return false;
790                }
791                return true;
792        }
793        
794        // ------------------------------------------------------------------
795
796        /**
797         * Run a {@link PlugInFilter} from the associated class with empty argument string. If the plugin's constructor is
798         * available, use method {@link #run(PlugInFilter)} instead.
799         *
800         * @param clazz class of the pluginfilter
801         * @return true if no exception was thrown
802         */
803        public static boolean runPlugInFilter(Class<? extends PlugInFilter> clazz) {
804                return runPlugInFilter(clazz, "");
805        }
806
807        /**
808         * Run a {@link PlugInFilter} from the associated class. If the plugin's constructor is available, use method
809         * {@link #run(PlugInFilter, String)} instead.
810         *
811         * @param clazz class of the plugin
812         * @param arg argument string
813         * @return true if no exception was thrown
814         */
815        public static boolean runPlugInFilter(Class<? extends PlugInFilter> clazz, String arg) {
816                PlugInFilter thePlugIn = null;
817                try {
818                        thePlugIn = clazz.getDeclaredConstructor().newInstance();
819                } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
820                                | InvocationTargetException | NoSuchMethodException | SecurityException e) {
821                        throw new RuntimeException(e.getMessage());     // should never happen
822                }
823                return run(thePlugIn, arg);
824        }
825
826        /**
827         * Run a {@link PlugIn} from the associated class with empty argument string. If the plugin's constructor is
828         * available, use method {@link #run(PlugIn)} instead.
829         *
830         * @param clazz class of the plugin
831         * @return true if no exception was thrown
832         */
833        public static boolean runPlugIn(Class<? extends PlugIn> clazz) {
834                return runPlugIn(clazz, "");
835        }
836
837        /**
838         * Run a {@link PlugIn} from the associated class. If the plugin's constructor is available, use method
839         * {@link #run(PlugIn, String)} instead.
840         *
841         * @param clazz class of the plugin
842         * @param arg argument string
843         * @return true if no exception was thrown
844         */
845        public static boolean runPlugIn(Class<? extends PlugIn> clazz, String arg) {
846                PlugIn thePlugIn = null;
847                try {
848                        thePlugIn = clazz.getDeclaredConstructor().newInstance();
849                } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
850                                | InvocationTargetException | NoSuchMethodException | SecurityException e) {
851                        throw new RuntimeException(e.getMessage());     // should never happen
852                }
853                return run(thePlugIn, arg);
854        }
855
856        
857        //  static methods for filtering images using ImageJ's {@link Convolver} class. 
858
859        /**
860         * Applies a one-dimensional convolution kernel to the given image, which is modified. The 1D kernel is applied in
861         * horizontal direction only.# The supplied filter kernel is not normalized.
862         *
863         * @param ip the image to be filtered (modified)
864         * @param h the filter kernel
865         * @see Convolver
866         */
867        public static void convolveX (ImageProcessor ip, float[] h) { // TODO: unit test missing
868                Convolver conv = new Convolver();
869                conv.setNormalize(false);
870                conv.convolve(ip, h, h.length, 1);
871        }
872
873        /**
874         * Applies a one-dimensional convolution kernel to the given image, which is modified. The 1D kernel is applied in
875         * vertical direction only. The supplied filter kernel must be odd-sized. It is not normalized.
876         *
877         * @param ip the image to be filtered (modified)
878         * @param h the filter kernel
879         * @see Convolver
880         */
881        public static void convolveY (ImageProcessor ip, float[] h) {
882                Convolver conv = new Convolver();
883                conv.setNormalize(false);
884                conv.convolve(ip, h, 1, h.length);
885        }
886
887        /**
888         * Applies a one-dimensional convolution kernel to the given image, which is modified. The same 1D kernel is applied
889         * twice, once in horizontal and once in vertical direction. The supplied filter kernel must be odd-sized. It is not
890         * normalized.
891         *
892         * @param ip the image to be filtered (modified)
893         * @param h the filter kernel
894         * @see Convolver
895         */
896        public static void convolveXY (ImageProcessor ip, float[] h) {
897                Convolver conv = new Convolver();
898                conv.setNormalize(false);
899                conv.convolve(ip, h, h.length, 1);
900                conv.convolve(ip, h, 1, h.length);
901        }
902
903        /**
904         * Applies a two-dimensional convolution kernel to the given image, which is modified. The supplied kernel
905         * {@code float[x][y]} must be rectangular and odd-sized. It is not normalized.
906         *
907         * @param ip the image to be filtered (modified)
908         * @param H the filter kernel
909         */
910        public static void convolve(ImageProcessor ip, float[][] H) {
911                float[] h = Matrix.flatten(H);  // TODO: right order? transpose?
912                Convolver conv = new Convolver();
913                conv.setNormalize(false);
914                conv.convolve(ip, h, H[0].length, H.length);
915        }
916        
917        // ---------------------------------------------------------------
918
919        /**
920         * Saves the given {@link ImageProcessor} using the specified path. The image file type is inferred from the file
921         * extension. TIFF is used if no file extension is given. This method simply invokes
922         * {@link IJ#save(ImagePlus, String)}, creating a temporary and titleless {@link ImagePlus} instance. Existing files
923         * with the same path are overwritten.
924         *
925         * @param ip a {@link ImageProcessor}
926         * @param filepath the path where to save the image, e.g. {@code "C:/tmp/MyImage.png"}
927         * @return the absolute file path
928         */
929        public static String save(ImageProcessor ip, String filepath) {
930                Objects.requireNonNull(filepath);
931                // TODO: check if the file was actually written or not
932                File file = Paths.get(filepath).toFile();
933                String absPath = file.getAbsolutePath();
934                IJ.save(new ImagePlus("", ip), absPath);
935                return absPath;
936        }
937        
938        // ---------------------------------------------------------------
939        
940        /**
941         * Returns true if no image is currently open in ImageJ.
942         * 
943         * @return true if no image is open
944         */
945        public static boolean noCurrentImage() {
946                return (WindowManager.getCurrentImage() == null);
947        }
948
949        /**
950         * <p>
951         * Returns true if the current (active) image is compatible with the specified flags (as specified by
952         * {@link PlugInFilter}, typically used to compose the return value of
953         * {@link PlugInFilter#setup(String, ImagePlus)}). This method emulates the compatibility check performed by
954         * ImageJ's built-in {@link PlugInFilterRunner} before a {@link PlugInFilter} is executed. It may be used, e.g., in
955         * the (normally empty) constructor of a class implementing {@link PlugInFilter}.
956         * </p>
957         * <p>
958         * Example, checking if the current image is either 8-bit or 32-bit gray:
959         * </p>
960         * <pre>
961         * if (checkImageFlagsCurrent(PlugInFilter.DOES_8G + PlugInFilter.DOES_32)) {
962         *      // some action ...
963         * }
964         * </pre>
965         *
966         * @param flags int-encoded binary flags
967         * @return true if the current image is compatible
968         * @see PlugInFilter
969         * @see #checkImageFlags(ImagePlus, int)
970         */
971        public static boolean checkImageFlagsCurrent(int flags) {
972                return checkImageFlags(WindowManager.getCurrentImage(), flags);
973        }
974
975        public static boolean checkImageFlags(ImagePlus im, int flags) {
976                // if no image is required, no more checks are needed:
977                if ((flags & PlugInFilter.NO_IMAGE_REQUIRED) != 0) {
978                        return true;
979                }
980                // void if no active image or one without a processor:
981                if (im == null || im.getProcessor() == null) {
982                        return false;
983                }
984                // check if the image type is compatible:
985                if (!checkImageType(im, flags)) {
986                        return false;
987                }
988                // check if im is a stack, if required:
989                if (((flags & PlugInFilter.STACK_REQUIRED) != 0) && !im.hasImageStack()) {
990                        return false;
991                }
992                // all checks passed:
993                return true;
994        }
995        
996        private static boolean checkImageType(ImagePlus im, int flags) {
997                switch (im.getType()) {
998                case ImagePlus.GRAY8:
999                        return ((flags & PlugInFilter.DOES_8G) != 0);
1000                case ImagePlus.COLOR_256:
1001                        return ((flags & PlugInFilter.DOES_8C) != 0);
1002                case ImagePlus.GRAY16:
1003                        return ((flags & PlugInFilter.DOES_16) != 0);
1004                case ImagePlus.GRAY32:
1005                        return ((flags & PlugInFilter.DOES_32) != 0);           
1006                case ImagePlus.COLOR_RGB:
1007                        return ((flags & PlugInFilter.DOES_RGB) != 0);
1008                default:
1009                        return false;
1010                }
1011        }
1012
1013    /**
1014     * Determines how many different colors are contained in the specified 24 bit full-color RGB image.
1015     *
1016     * @param cp a RGB image
1017     * @return the number of distinct colors
1018     */
1019    public static int countColors(ColorProcessor cp) {
1020        // duplicate pixel array and sort
1021        int[] pixels = (int[]) cp.getPixelsCopy();
1022        Arrays.sort(pixels);
1023
1024        int k = 1;      // image contains at least one color
1025        for (int i = 0; i < pixels.length - 1; i++) {
1026            if (pixels[i] != pixels[i + 1])
1027                k = k + 1;
1028        }
1029        return k;
1030    }
1031
1032        /**
1033         * Checks if the specified class implements one of ImageJ's plugin interfaces.
1034         *
1035         * @param clazz any class
1036         * @return true iff class implements one of ImageJ's plugin interfaces
1037         */
1038        public static boolean isIjPlugin(Class<?> clazz) {
1039                return PlugIn.class.isAssignableFrom(clazz) || PlugInFilter.class.isAssignableFrom(clazz);
1040        }
1041}