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.ImagePlus; 013import ij.plugin.filter.RankFilters; 014import ij.process.ByteProcessor; 015import ij.process.FloatProcessor; 016import imagingbook.common.ij.DialogUtils; 017import imagingbook.common.util.ParameterBundle; 018 019/** 020 * <p> 021 * This is an implementation of the adaptive thresholder proposed in [1]. See also Sec. 9.2 (Eq. 9.74) of [2]. 022 * </p> 023 * <p> 024 * [1] Adaptive thresholder as proposed in J. Sauvola and M. Pietikäinen, "Adaptive document image binarization", 025 * Pattern Recognition 33(2), 1135-1143 (2000). <br> [2] W. Burger, M.J. Burge, <em>Digital Image Processing – An 026 * Algorithmic Introduction</em> 3rd ed, Springer (2022). 027 * </p> 028 * 029 * @author WB 030 * @version 2022/08/01 031 */ 032public class SauvolaThresholder implements AdaptiveThresholder { 033 034 /** 035 * Parameters for class {@link SauvolaThresholder}. 036 */ 037 public static class Parameters implements ParameterBundle<SauvolaThresholder> { 038 @DialogUtils.DialogLabel("Radius") 039 public int radius = 15; 040 @DialogUtils.DialogLabel("kappa") 041 public double kappa = 0.5; 042 @DialogUtils.DialogLabel("sigma_max") 043 public double sigmaMax = 128; 044 @DialogUtils.DialogLabel("Background mode") 045 public BackgroundMode bgMode = BackgroundMode.DARK; 046 } 047 048 private final Parameters params; 049 050 public SauvolaThresholder() { 051 this(new Parameters()); 052 } 053 054 public SauvolaThresholder(Parameters params) { 055 this.params = params; 056 } 057 058 @Override 059 public FloatProcessor getThreshold(ByteProcessor I) { 060 FloatProcessor Imean = I.convertToFloatProcessor(); 061 FloatProcessor Isigma = (FloatProcessor) Imean.duplicate(); 062 063 RankFilters rf = new RankFilters(); 064 rf.rank(Imean, params.radius, RankFilters.MEAN); 065 // new ImagePlus("Imean", Imean). show(); 066 067 rf.rank(Isigma, params.radius, RankFilters.VARIANCE); 068 Isigma.sqrt(); 069 // new ImagePlus("Isigma", Isigma). show(); 070 071 final int W = I.getWidth(); 072 final int H = I.getHeight(); 073 final float kappa = (float) params.kappa; 074 final float sigmaMax = (float) params.sigmaMax; 075 final boolean darkBg = (params.bgMode == BackgroundMode.DARK); 076 077 FloatProcessor DIFF = new FloatProcessor(W, H); 078 FloatProcessor Q = new FloatProcessor(W, H); 079 for (int v = 0; v < H; v++) { 080 for (int u = 0; u < W; u++) { 081 final float sigmaR = Isigma.getf(u, v); 082 final float meanR = Imean.getf(u, v); 083 final float diff = kappa * (sigmaR / sigmaMax - 1); 084 DIFF.setf(u, v, diff); 085 float q = (darkBg) ? meanR * (1 - diff) : meanR * (1 + diff); 086 // if (q < 0) q = 0; // necessary? 087 // if (q > 255) q = 255; 088 Q.setf(u, v, q); 089 } 090 } 091 092 // new ImagePlus("DIFF", DIFF). show(); 093 return Q; 094 } 095 096}