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.threshold.adaptive;
011
012import ij.plugin.filter.RankFilters;
013import ij.process.ByteProcessor;
014import ij.process.FloatProcessor;
015import imagingbook.common.filter.generic.GenericFilter;
016import imagingbook.common.filter.linear.GaussianFilterSeparable;
017import imagingbook.common.ij.DialogUtils.DialogLabel;
018import imagingbook.common.util.ParameterBundle;
019
020/**
021 * <p>
022 * This is an implementation of the adaptive thresholder proposed by Niblack in [1]. See Sec. 9.2.2 of [2] for a
023 * detailed description. It comes in three different version, depending on the type of local support region:
024 * </p>
025 * <ul>
026 * <li>{@link Box}: uses a rectangular (box-shaped) support region;</li>
027 * <li>{@link Disk}: uses a circular (disk-shaped) support region (see [2], Alg.
028 * 9.8);</li>
029 * <li>{@link Gauss}: uses a 2D isotropic Gaussian support region (see [2], Alg.
030 * 9.9 and Prog. 9.2).</li>
031 * </ul>
032 * Note that {@link NiblackThresholder} itself is abstract and thus cannot be
033 * instantiated.
034 * <p>
035 * [1] W. Niblack. "An Introduction to Digital Image Processing". Prentice-Hall
036 * (1986). <br>
037 * [2] W. Burger, M.J. Burge, <em>Digital Image Processing &ndash; An
038 * Algorithmic Introduction</em>, 3rd ed, Springer (2022).
039 * </p>
040 *
041 * @author WB
042 * @version 2022/08/01
043 */
044public abstract class NiblackThresholder implements AdaptiveThresholder {
045        
046        public enum RegionType { Box, Disk, Gauss }
047        
048        /**
049         * Parameters for class {@link NiblackThresholder}.
050         */
051        public static class Parameters implements ParameterBundle<NiblackThresholder> {
052                @DialogLabel("Radius")
053                public int radius = 15;
054                @DialogLabel("kappa (κ)")
055                public double kappa = 0.30;
056                @DialogLabel("Min. offset (d)")
057                public double dMin = 5;
058                @DialogLabel("Background mode")
059                public BackgroundMode bgMode = BackgroundMode.DARK;
060        }
061        
062        private final Parameters params;
063        FloatProcessor Imean;
064        FloatProcessor Isigma;
065
066        private NiblackThresholder() {
067                this(new Parameters());
068        }
069
070        private NiblackThresholder(Parameters params) {
071                this.params = params;
072        }
073        
074        // method to be implemented by real sub-classes:
075        abstract void makeMeanAndVariance(ByteProcessor I, int radius);
076        
077        @Override
078        public FloatProcessor getThreshold(ByteProcessor I) {
079                final int W = I.getWidth();
080                final int H = I.getHeight();
081                makeMeanAndVariance(I, params.radius);
082                FloatProcessor Q = new FloatProcessor(W, H);
083                final float kappa = (float) params.kappa;
084                final float dMin = (float) params.dMin;
085                final boolean darkBg = (params.bgMode == BackgroundMode.DARK);
086                
087                for (int v = 0; v < H; v++) {
088                        for (int u = 0; u < W; u++) {
089                                float sigma = Isigma.getf(u, v);
090                                float mu = Imean.getf(u, v);
091                                float diff = kappa * sigma + dMin;
092                                float q = (darkBg) ? mu + diff : mu - diff;
093                                // if (q < 0)    q = 0;
094                                // if (q > 255) q = 255;
095                                Q.setf(u, v, q);
096                        }
097                }
098                return Q;
099        }
100        
101        // -----------------------------------------------------------------------
102        
103        /**
104         * Static convenience method for creating a {@link NiblackThresholder} with
105         * a specific support region type.
106         * Note that {@link NiblackThresholder} itself is abstract and cannot be instantiated
107         * (see concrete sub-types {@link Box}, {@link Disk}, {@link Gauss}).
108         * 
109         * @param regType support region type
110         * @param params other parameters
111         * @return an instance of {@link NiblackThresholder}
112         */
113        public static NiblackThresholder create(RegionType regType, Parameters params) {
114                switch (regType) {
115                case Box :              return new NiblackThresholder.Box(params);
116                case Disk :     return new NiblackThresholder.Disk(params);
117                case Gauss : return new NiblackThresholder.Gauss(params);
118                default :               return null;
119                }
120        }
121        
122        // -----------------------------------------------------------------------
123        
124        /**
125         * Implementation of Niblack's adaptive thresholder using a
126         * rectangular (box-shaped) support region 
127         * (concrete implementation of abstract class {@link NiblackThresholder}).
128         */
129        public static class Box extends NiblackThresholder {
130
131                /**
132                 * Constructor using default parameters.
133                 */
134                public Box() {
135                        super();
136                }
137                
138                /**
139                 * Constructor with specific parameters.
140                 * @param params parameters
141                 */
142                public Box(Parameters params) {
143                        super(params);
144                }
145
146                @Override
147                void makeMeanAndVariance(ByteProcessor I, int radius) {
148                        final int W = I.getWidth();
149                        final int H = I.getHeight();
150                        this.Imean =  new FloatProcessor(W, H);
151                        this.Isigma =  new FloatProcessor(W, H);
152                        final int n = (radius + 1 + radius) * (radius + 1 + radius);
153
154                        for (int v = 0; v < H; v++) {
155                                for (int u = 0; u < W; u++) {
156                                        long A = 0;     // sum of image values in support region
157                                        long B = 0;     // sum of squared image values in support region
158                                        for (int j = -radius; j <= radius; j++) {
159                                                for (int i = -radius; i <= radius; i++) {
160                                                        int p = getPaddedPixel(I, u + i, v + j); // this is slow!
161                                                        A = A + p;
162                                                        B = B + p * p;
163                                                }
164                                        }
165                                        Imean.setf(u, v, (float) A / n);
166                                        Isigma.setf(u, v, (float) Math.sqrt((B - (double) (A * A) / n) / n));
167                                }
168                        }
169                }
170                
171                // TODO: change to use an ImageAccessor!
172                private int getPaddedPixel(ByteProcessor bp, int u, int v) {
173                        final int w = bp.getWidth();
174                        final int h = bp.getHeight();
175                        if (u < 0)
176                                u = 0;
177                        else if (u >= w)
178                                u = w - 1;
179                        if (v < 0)
180                                v = 0;
181                        else if (v >= h)
182                                v = h - 1;
183                        return bp.get(u, v);
184                }
185                
186        }
187        
188        // -----------------------------------------------------------------------
189        
190        /**
191         * Implementation of Niblack's adaptive thresholder using a
192         * circular (disk-shaped) support region 
193         * (concrete implementation of abstract class {@link NiblackThresholder}).
194         */
195        public static class Disk extends NiblackThresholder {
196                
197                /**
198                 * Constructor using default parameters.
199                 */
200                public Disk() {
201                        super();
202                }
203                
204                /**
205                 * Constructor with specific parameters.
206                 * @param params parameters
207                 */
208                public Disk(Parameters params) {
209                        super(params);
210                }
211
212                @Override
213                void makeMeanAndVariance(ByteProcessor I, int radius) {
214                        FloatProcessor mean = (FloatProcessor) I.convertToFloat();
215                        FloatProcessor var =  (FloatProcessor) mean.duplicate();
216                        
217                        RankFilters rf = new RankFilters();
218                        rf.rank(mean, radius, RankFilters.MEAN);
219                        this.Imean = mean;
220                        
221                        rf.rank(var, radius, RankFilters.VARIANCE);
222                        var.sqrt();
223                        this.Isigma = var;
224                }
225        }
226        
227        // -----------------------------------------------------------------------
228        
229        /**
230         * Implementation of Niblack's adaptive thresholder using a
231         * 2D Gaussian support region 
232         * (concrete implementation of abstract class {@link NiblackThresholder}).
233         */
234        public static class Gauss extends NiblackThresholder {
235                
236                /**
237                 * Constructor using default parameters.
238                 */
239                public Gauss() {
240                        super();
241                }
242                
243                /**
244                 * Constructor with specific parameters.
245                 * @param params parameters
246                 */
247                public Gauss(Parameters params) {
248                        super(params);
249                }
250                
251                @Override
252                void makeMeanAndVariance(ByteProcessor I, int r) {
253                        // //uses ImageJ's GaussianBlur
254                        // local variance over square of size (size + 1 + size)^2
255                        final int W = I.getWidth();
256                        final int H = I.getHeight();
257                        
258                        this.Imean = new FloatProcessor(W,H);
259                        this.Isigma = new FloatProcessor(W,H);
260
261                        FloatProcessor A = I.convertToFloatProcessor();
262                        FloatProcessor B = I.convertToFloatProcessor();
263                        B.sqr();
264                        
265                        double sigma = r * 0.6; // sigma of Gaussian filter should be approx. 0.6 of the disk's radius
266                        
267//                      GaussianBlur gb = new GaussianBlur();
268//                      gb.blurFloat(A, sigma, sigma, 0.002);
269//                      gb.blurFloat(B, sigma, sigma, 0.002);
270                        
271                        GenericFilter gaussian = new GaussianFilterSeparable(sigma);
272                        gaussian.applyTo(A);
273                        gaussian.applyTo(B);
274
275                        for (int v = 0; v < H; v++) {
276                                for (int u = 0; u < W; u++) {
277                                        float a = A.getf(u, v);
278                                        float b = B.getf(u, v);
279                                        float sigmaG = (float) Math.sqrt(b - a * a);
280                                        this.Imean.setf(u, v, a);
281                                        this.Isigma.setf(u, v, sigmaG);
282                                }
283                        }
284                }
285        }
286        
287}