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 ******************************************************************************/
009package Ch05_Edges_Contours;
010
011import ij.ImagePlus;
012import ij.gui.GenericDialog;
013import ij.plugin.filter.PlugInFilter;
014import ij.process.ColorProcessor;
015import ij.process.FloatProcessor;
016import ij.process.ImageProcessor;
017import imagingbook.common.edges.GrayscaleEdgeDetector;
018import imagingbook.common.ij.DialogUtils;
019import imagingbook.core.jdoc.JavaDocHelp;
020import imagingbook.sampleimages.GeneralSampleImage;
021
022import static imagingbook.common.ij.IjUtils.noCurrentImage;
023import static imagingbook.common.math.Arithmetic.clipTo;
024
025/**
026 * <p>
027 * ImageJ plugin, implementing a "cartoon" or "edge burn-in" effect by controlled darkening of image edges. Pixels are
028 * darkened depending on the value of the normalized edge magnitude (as produced by an edge operator). At points of
029 * maximum edge magnitude the darkening effect are strongest, while pixels remain unmodified where the edge magnitude is
030 * zero. See Ch. 5 (Exercise 5.8) of [1] for additional details. Works for RGB color images only. The input image is
031 * modified.
032 * </p>
033 * <p>
034 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer
035 * (2022).
036 * </p>
037 *
038 * @author WB
039 * @version 2022/11/17
040 */
041public class Cartoon_Effect implements PlugInFilter, JavaDocHelp {
042
043    private static double A = 0.05;
044    private static double B = 0.3;
045    private static boolean ShowOriginalEdgeMagnitude = false;
046    private static boolean ShowSoftenedEdgeMagnitude = false;
047
048    /**
049     * Constructor, asks to open a predefined sample image if no other image is currently open.
050     */
051    public Cartoon_Effect() {
052        if (noCurrentImage()) {
053            DialogUtils.askForSampleImage(GeneralSampleImage.Clown);
054        }
055    }
056
057    @Override
058    public int setup(String arg, ImagePlus im) {
059        return DOES_RGB;    // TODO: make work for DOES_8G
060    }
061
062    @Override
063    public void run(ImageProcessor ip) {
064        if (!runDialog()) {
065            return;
066        }
067
068        A = clipTo(A, 0.0, 1.0);
069        B = clipTo(B, 0.0, 1.0);
070
071        int w = ip.getWidth();
072        int h = ip.getHeight();
073
074        GrayscaleEdgeDetector ed = new GrayscaleEdgeDetector(ip);
075        FloatProcessor mag = ed.getEdgeMagnitude();
076
077        if (ShowOriginalEdgeMagnitude) {
078            ImageProcessor mag2 = mag.duplicate();
079            mag2.invert();
080            mag2.resetMinAndMax();
081            new ImagePlus("E (inverted)", mag2).show();
082        }
083
084        double magMax = mag.getMax();
085
086        // soft-threshold edge magnitude
087        for (int u = 0; u < w; u++) {
088            for (int v = 0; v < h; v++) {
089                double s = mag.getf(u, v) / magMax;    // scale to 1.0
090                mag.setf(u, v, (float) f(s, A, B));
091            }
092        }
093
094        if (ShowSoftenedEdgeMagnitude) {
095            ImageProcessor mag3 = mag.duplicate();
096            mag3.resetMinAndMax();
097            new ImagePlus("f(E)", mag3).show();
098        }
099
100        // burn-in edges:
101        final int[] RGB = new int[3];
102        ColorProcessor cp = (ColorProcessor) ip;
103        for (int u = 0; u < w; u++) {
104            for (int v = 0; v < h; v++) {
105                cp.getPixel(u, v, RGB);
106                float s = mag.getf(u, v);
107                // darken pixel value by factor s
108                RGB[0] = Math.round(RGB[0] * s);
109                RGB[1] = Math.round(RGB[1] * s);
110                RGB[2] = Math.round(RGB[2] * s);
111                cp.putPixel(u, v, RGB);
112            }
113        }
114    }
115
116    // soft-threshold function
117    private double f(double m, double a, double b) {
118        if (m < a) {
119            return 1;
120        } else if (m <= b) {
121            return 0.5 * (1 + Math.cos(Math.PI * (m - a) / (b - a)));
122        } else {
123            return 0;
124        }
125    }
126
127    // ---------------------------------------------------------------------
128
129    private boolean runDialog() {    // TODO: add a preview button
130        GenericDialog gd = new GenericDialog(Cartoon_Effect.class.getSimpleName());
131        gd.addHelp(getJavaDocUrl());
132        gd.addMessage("Parameters: 0 \u2264 a \u2264 b \u2264 1");
133        gd.addNumericField("a", A, 2);
134        gd.addNumericField("b", B, 2);
135        gd.addCheckbox("Show original edge magnitude", ShowOriginalEdgeMagnitude);
136        gd.addCheckbox("Show softened edge magnitude", ShowSoftenedEdgeMagnitude);
137
138        gd.showDialog();
139        if (gd.wasCanceled()) {
140            return false;
141        }
142        A = gd.getNextNumber();
143        B = gd.getNextNumber();
144        ShowOriginalEdgeMagnitude = gd.getNextBoolean();
145        ShowSoftenedEdgeMagnitude = gd.getNextBoolean();
146        return true;
147    }
148
149}