diff mupdf-source/thirdparty/leptonica/src/bilateral.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/thirdparty/leptonica/src/bilateral.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,812 @@
+/*====================================================================*
+ -  Copyright (C) 2001 Leptonica.  All rights reserved.
+ -
+ -  Redistribution and use in source and binary forms, with or without
+ -  modification, are permitted provided that the following conditions
+ -  are met:
+ -  1. Redistributions of source code must retain the above copyright
+ -     notice, this list of conditions and the following disclaimer.
+ -  2. Redistributions in binary form must reproduce the above
+ -     copyright notice, this list of conditions and the following
+ -     disclaimer in the documentation and/or other materials
+ -     provided with the distribution.
+ -
+ -  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ -  ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ -  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ -  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL ANY
+ -  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ -  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ -  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ -  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ -  OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ -  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ -  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *====================================================================*/
+
+/*!
+ * \file bilateral.c
+ * <pre>
+ *
+ *     Top level approximate separable grayscale or color bilateral filtering
+ *          PIX                 *pixBilateral()
+ *          PIX                 *pixBilateralGray()
+ *
+ *     Implementation of approximate separable bilateral filter
+ *          static L_BILATERAL  *bilateralCreate()
+ *          static void         *bilateralDestroy()
+ *          static PIX          *bilateralApply()
+ *
+ *     Slow, exact implementation of grayscale or color bilateral filtering
+ *          PIX                 *pixBilateralExact()
+ *          PIX                 *pixBilateralGrayExact()
+ *          PIX                 *pixBlockBilateralExact()
+ *
+ *     Kernel helper function
+ *          L_KERNEL            *makeRangeKernel()
+ *
+ *  This includes both a slow, exact implementation of the bilateral
+ *  filter algorithm (given by Sylvain Paris and Frédo Durand),
+ *  and a fast, approximate and separable implementation (following
+ *  Yang, Tan and Ahuja).  See bilateral.h for algorithmic details.
+ *
+ *  The bilateral filter has the nice property of applying a gaussian
+ *  filter to smooth parts of the image that don't vary too quickly,
+ *  while at the same time preserving edges.  The filter is nonlinear
+ *  and cannot be decomposed into two separable filters; however,
+ *  there exists an approximate method that is separable.  To further
+ *  speed up the separable implementation, you can generate the
+ *  intermediate data at reduced resolution.
+ *
+ *  The full kernel is composed of two parts: a spatial gaussian filter
+ *  and a nonlinear "range" filter that depends on the intensity difference
+ *  between the reference pixel at the spatial kernel origin and any other
+ *  pixel within the kernel support.
+ *
+ *  In our implementations, the range filter is a parameterized,
+ *  one-sided, 256-element, monotonically decreasing gaussian function
+ *  of the absolute value of the difference between pixel values; namely,
+ *  abs(I2 - I1).  In general, any decreasing function can be used,
+ *  and more generally,  any two-dimensional kernel can be used if
+ *  you wish to relax the 'abs' condition.  (In that case, the range
+ *  filter can be 256 x 256).
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <math.h>
+#include "allheaders.h"
+#include "bilateral.h"
+
+static L_BILATERAL *bilateralCreate(PIX *pixs, l_float32 spatial_stdev,
+                                    l_float32 range_stdev, l_int32 ncomps,
+                                    l_int32 reduction);
+static PIX *bilateralApply(L_BILATERAL *bil);
+static void bilateralDestroy(L_BILATERAL **pbil);
+
+
+#ifndef  NO_CONSOLE_IO
+#define  DEBUG_BILATERAL    0
+#endif  /* ~NO_CONSOLE_IO */
+
+/*--------------------------------------------------------------------------*
+ *  Top level approximate separable grayscale or color bilateral filtering  *
+ *--------------------------------------------------------------------------*/
+/*!
+ * \brief   pixBilateral()
+ *
+ * \param[in]    pixs            8 bpp gray or 32 bpp rgb, no colormap
+ * \param[in]    spatial_stdev   of gaussian kernel; in pixels, > 0.5
+ * \param[in]    range_stdev     of gaussian range kernel; > 5.0; typ. 50.0
+ * \param[in]    ncomps          number of intermediate sums J(k,x);
+ *                               in [4 ... 30]
+ * \param[in]    reduction       1, 2 or 4
+ * \return  pixd   bilateral filtered image, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This performs a relatively fast, separable bilateral
+ *          filtering operation.  The time is proportional to ncomps
+ *          and varies inversely approximately as the cube of the
+ *          reduction factor.  See bilateral.h for algorithm details.
+ *      (2) We impose minimum values for range_stdev and ncomps to
+ *          avoid nasty artifacts when either are too small.  We also
+ *          impose a constraint on their product:
+ *               ncomps * range_stdev >= 100.
+ *          So for values of range_stdev >= 25, ncomps can be as small as 4.
+ *          Here is a qualitative, intuitive explanation for this constraint.
+ *          Call the difference in k values between the J(k) == 'delta', where
+ *              'delta' ~ 200 / ncomps
+ *          Then this constraint is roughly equivalent to the condition:
+ *              'delta' < 2 * range_stdev
+ *          Note that at an intensity difference of (2 * range_stdev), the
+ *          range part of the kernel reduces the effect by the factor 0.14.
+ *          This constraint requires that we have a sufficient number of
+ *          PCBs (i.e, a small enough 'delta'), so that for any value of
+ *          image intensity I, there exists a k (and a PCB, J(k), such that
+ *              |I - k| < range_stdev
+ *          Any fewer PCBs and we don't have enough to support this condition.
+ *      (3) The upper limit of 30 on ncomps is imposed because the
+ *          gain in accuracy is not worth the extra computation.
+ *      (4) The size of the gaussian kernel is twice the spatial_stdev
+ *          on each side of the origin.  The minimum value of
+ *          spatial_stdev, 0.5, is required to have a finite sized
+ *          spatial kernel.  In practice, a much larger value is used.
+ *      (5) Computation of the intermediate images goes inversely
+ *          as the cube of the reduction factor.  If you can use a
+ *          reduction of 2 or 4, it is well-advised.
+ *      (6) The range kernel is defined over the absolute value of pixel
+ *          grayscale differences, and hence must have size 256 x 1.
+ *          Values in the array represent the multiplying weight
+ *          depending on the absolute gray value difference between
+ *          the source pixel and the neighboring pixel, and should
+ *          be monotonically decreasing.
+ *      (7) Interesting observation.  Run this on prog/fish24.jpg, with
+ *          range_stdev = 60, ncomps = 6, and spatial_dev = {10, 30, 50}.
+ *          As spatial_dev gets larger, we get the counter-intuitive
+ *          result that the body of the red fish becomes less blurry.
+ *      (8) The image must be sufficiently big to get reasonable results.
+ *          This requires the dimensions to be at least twice the filter size.
+ *          Otherwise, return a copy of the input with warning.
+ * </pre>
+ */
+PIX *
+pixBilateral(PIX       *pixs,
+             l_float32  spatial_stdev,
+             l_float32  range_stdev,
+             l_int32    ncomps,
+             l_int32    reduction)
+{
+l_int32       w, h, d, filtersize;
+l_float32     sstdev;  /* scaled spatial stdev */
+PIX          *pixt, *pixr, *pixg, *pixb, *pixd;
+
+    if (!pixs || pixGetColormap(pixs))
+        return (PIX *)ERROR_PTR("pixs not defined or cmapped", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 8 && d != 32)
+        return (PIX *)ERROR_PTR("pixs not 8 or 32 bpp", __func__, NULL);
+    if (reduction != 1 && reduction != 2 && reduction != 4)
+        return (PIX *)ERROR_PTR("reduction invalid", __func__, NULL);
+    filtersize = (l_int32)(2.0 * spatial_stdev + 1.0 + 0.5);
+    if (w < 2 * filtersize || h < 2 * filtersize) {
+        L_WARNING("w = %d, h = %d; w or h < 2 * filtersize = %d; "
+                  "returning copy\n", __func__, w, h, 2 * filtersize);
+        return pixCopy(NULL, pixs);
+    }
+    sstdev = spatial_stdev / (l_float32)reduction;  /* reduced spat. stdev */
+    if (sstdev < 0.5)
+        return (PIX *)ERROR_PTR("sstdev < 0.5", __func__, NULL);
+    if (range_stdev <= 5.0)
+        return (PIX *)ERROR_PTR("range_stdev <= 5.0", __func__, NULL);
+    if (ncomps < 4 || ncomps > 30)
+        return (PIX *)ERROR_PTR("ncomps not in [4 ... 30]", __func__, NULL);
+    if (ncomps * range_stdev < 100.0)
+        return (PIX *)ERROR_PTR("ncomps * range_stdev < 100.0", __func__, NULL);
+
+    if (d == 8)
+        return pixBilateralGray(pixs, spatial_stdev, range_stdev,
+                                ncomps, reduction);
+
+    pixt = pixGetRGBComponent(pixs, COLOR_RED);
+    pixr = pixBilateralGray(pixt, spatial_stdev, range_stdev, ncomps,
+                            reduction);
+    pixDestroy(&pixt);
+    pixt = pixGetRGBComponent(pixs, COLOR_GREEN);
+    pixg = pixBilateralGray(pixt, spatial_stdev, range_stdev, ncomps,
+                            reduction);
+    pixDestroy(&pixt);
+    pixt = pixGetRGBComponent(pixs, COLOR_BLUE);
+    pixb = pixBilateralGray(pixt, spatial_stdev, range_stdev, ncomps,
+                            reduction);
+    pixDestroy(&pixt);
+    pixd = pixCreateRGBImage(pixr, pixg, pixb);
+    pixDestroy(&pixr);
+    pixDestroy(&pixg);
+    pixDestroy(&pixb);
+    return pixd;
+}
+
+
+/*!
+ * \brief   pixBilateralGray()
+ *
+ * \param[in]    pixs             8 bpp gray
+ * \param[in]    spatial_stdev    of gaussian kernel; in pixels, > 0.5
+ * \param[in]    range_stdev      of gaussian range kernel; > 5.0; typ. 50.0
+ * \param[in]    ncomps           number of intermediate sums J(k,x);
+ *                                in [4 ... 30]
+ * \param[in]    reduction        1, 2 or 4
+ * \return  pixd   8 bpp bilateral filtered image, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) See pixBilateral() for constraints on the input parameters.
+ *      (2) See pixBilateral() for algorithm details.
+ * </pre>
+ */
+PIX *
+pixBilateralGray(PIX       *pixs,
+                 l_float32  spatial_stdev,
+                 l_float32  range_stdev,
+                 l_int32    ncomps,
+                 l_int32    reduction)
+{
+l_float32     sstdev;  /* scaled spatial stdev */
+PIX          *pixd;
+L_BILATERAL  *bil;
+
+    if (!pixs || pixGetColormap(pixs))
+        return (PIX *)ERROR_PTR("pixs not defined or cmapped", __func__, NULL);
+    if (pixGetDepth(pixs) != 8)
+        return (PIX *)ERROR_PTR("pixs not 8 bpp gray", __func__, NULL);
+    if (reduction != 1 && reduction != 2 && reduction != 4)
+        return (PIX *)ERROR_PTR("reduction invalid", __func__, NULL);
+    sstdev = spatial_stdev / (l_float32)reduction;  /* reduced spat. stdev */
+    if (sstdev < 0.5)
+        return (PIX *)ERROR_PTR("sstdev < 0.5", __func__, NULL);
+    if (range_stdev <= 5.0)
+        return (PIX *)ERROR_PTR("range_stdev <= 5.0", __func__, NULL);
+    if (ncomps < 4 || ncomps > 30)
+        return (PIX *)ERROR_PTR("ncomps not in [4 ... 30]", __func__, NULL);
+    if (ncomps * range_stdev < 100.0)
+        return (PIX *)ERROR_PTR("ncomps * range_stdev < 100.0", __func__, NULL);
+
+    bil = bilateralCreate(pixs, spatial_stdev, range_stdev, ncomps, reduction);
+    if (!bil) return (PIX *)ERROR_PTR("bil not made", __func__, NULL);
+    pixd = bilateralApply(bil);
+    bilateralDestroy(&bil);
+    return pixd;
+}
+
+
+/*----------------------------------------------------------------------*
+ *       Implementation of approximate separable bilateral filter       *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   bilateralCreate()
+ *
+ * \param[in]    pixs            8 bpp gray, no colormap
+ * \param[in]    spatial_stdev   of gaussian kernel; in pixels, > 0.5
+ * \param[in]    range_stdev     of gaussian range kernel; > 5.0; typ. 50.0
+ * \param[in]    ncomps          number of intermediate sums J(k,x);
+ *                               in [4 ... 30]
+ * \param[in]    reduction       1, 2 or 4
+ * \return  bil, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This initializes a bilateral filtering operation, generating all
+ *          the data required.  It takes most of the time in the bilateral
+ *          filtering operation.
+ *      (2) See bilateral.h for details of the algorithm.
+ *      (3) See pixBilateral() for constraints on input parameters, which
+ *          are not checked here.
+ * </pre>
+ */
+static L_BILATERAL *
+bilateralCreate(PIX       *pixs,
+                l_float32  spatial_stdev,
+                l_float32  range_stdev,
+                l_int32    ncomps,
+                l_int32    reduction)
+{
+l_int32       w, ws, wd, h, hs, hd, i, j, k, index;
+l_int32       border, minval, maxval, spatial_size;
+l_int32       halfwidth, wpls, wplt, wpld, kval, nval, dval;
+l_float32     sstdev, fval1, fval2, denom, sum, norm, kern;
+l_int32      *nc, *kindex;
+l_float32    *kfract, *range, *spatial;
+l_uint32     *datas, *datat, *datad, *lines, *linet, *lined;
+L_BILATERAL  *bil;
+PIX          *pix1, *pix2, *pixt, *pixsc, *pixd;
+PIXA         *pixac;
+
+    if (reduction == 1) {
+        pix1 = pixClone(pixs);
+    } else if (reduction == 2) {
+        pix1 = pixScaleAreaMap2(pixs);
+    } else {  /* reduction == 4) */
+        pix2 = pixScaleAreaMap2(pixs);
+        pix1 = pixScaleAreaMap2(pix2);
+        pixDestroy(&pix2);
+    }
+    if (!pix1)
+        return (L_BILATERAL *)ERROR_PTR("pix1 not made", __func__, NULL);
+
+    sstdev = spatial_stdev / (l_float32)reduction;  /* reduced spat. stdev */
+    border = (l_int32)(2 * sstdev + 1);
+    pixsc = pixAddMirroredBorder(pix1, border, border, border, border);
+    pixGetExtremeValue(pix1, 1, L_SELECT_MIN, NULL, NULL, NULL, &minval);
+    pixGetExtremeValue(pix1, 1, L_SELECT_MAX, NULL, NULL, NULL, &maxval);
+    pixDestroy(&pix1);
+    if (!pixsc)
+        return (L_BILATERAL *)ERROR_PTR("pixsc not made", __func__, NULL);
+
+    bil = (L_BILATERAL *)LEPT_CALLOC(1, sizeof(L_BILATERAL));
+    bil->spatial_stdev = sstdev;
+    bil->range_stdev = range_stdev;
+    bil->reduction = reduction;
+    bil->ncomps = ncomps;
+    bil->minval = minval;
+    bil->maxval = maxval;
+    bil->pixsc = pixsc;
+    bil->pixs = pixClone(pixs);
+
+    /* -------------------------------------------------------------------- *
+     * Generate arrays for interpolation of J(k,x):
+     *  (1.0 - kfract[.]) * J(kindex[.], x) + kfract[.] * J(kindex[.] + 1, x),
+     * where I(x) is the index into kfract[] and kindex[],
+     * and x is an index into the 2D image array.
+     * -------------------------------------------------------------------- */
+        /* nc is the set of k values to be used in J(k,x) */
+    nc = (l_int32 *)LEPT_CALLOC(ncomps, sizeof(l_int32));
+    for (i = 0; i < ncomps; i++)
+        nc[i] = minval + i * (maxval - minval) / (ncomps - 1);
+    bil->nc = nc;
+
+        /* kindex maps from intensity I(x) to the lower k index for J(k,x) */
+    kindex = (l_int32 *)LEPT_CALLOC(256, sizeof(l_int32));
+    for (i = minval, k = 0; i <= maxval && k < ncomps - 1; k++) {
+        fval2 = nc[k + 1];
+        while (i < fval2) {
+            kindex[i] = k;
+            i++;
+        }
+    }
+    kindex[maxval] = ncomps - 2;
+    bil->kindex = kindex;
+
+        /* kfract maps from intensity I(x) to the fraction of J(k+1,x) used */
+    kfract = (l_float32 *)LEPT_CALLOC(256, sizeof(l_float32));  /* from lower */
+    for (i = minval, k = 0; i <= maxval && k < ncomps - 1; k++) {
+        fval1 = nc[k];
+        fval2 = nc[k + 1];
+        while (i < fval2) {
+            kfract[i] = (l_float32)(i - fval1) / (l_float32)(fval2 - fval1);
+            i++;
+        }
+    }
+    kfract[maxval] = 1.0;
+    bil->kfract = kfract;
+
+#if  DEBUG_BILATERAL
+    for (i = minval; i <= maxval; i++)
+      lept_stderr("kindex[%d] = %d; kfract[%d] = %5.3f\n",
+                  i, kindex[i], i, kfract[i]);
+    for (i = 0; i < ncomps; i++)
+      lept_stderr("nc[%d] = %d\n", i, nc[i]);
+#endif  /* DEBUG_BILATERAL */
+
+    /* -------------------------------------------------------------------- *
+     *             Generate 1-D kernel arrays (spatial and range)           *
+     * -------------------------------------------------------------------- */
+    spatial_size = 2 * sstdev + 1;  /* same as the added border */
+    spatial = (l_float32 *)LEPT_CALLOC(spatial_size, sizeof(l_float32));
+    denom = 2. * sstdev * sstdev;
+    for (i = 0; i < spatial_size; i++)
+        spatial[i] = expf(-(l_float32)(i * i) / denom);
+    bil->spatial = spatial;
+
+    range = (l_float32 *)LEPT_CALLOC(256, sizeof(l_float32));
+    denom = 2. * range_stdev * range_stdev;
+    for (i = 0; i < 256; i++)
+        range[i] = expf(-(l_float32)(i * i) / denom);
+    bil->range = range;
+
+    /* -------------------------------------------------------------------- *
+     *            Generate principal bilateral component images             *
+     * -------------------------------------------------------------------- */
+    pixac = pixaCreate(ncomps);
+    pixGetDimensions(pixsc, &ws, &hs, NULL);
+    datas = pixGetData(pixsc);
+    wpls = pixGetWpl(pixsc);
+    pixGetDimensions(pixs, &w, &h, NULL);
+    wd = (w + reduction - 1) / reduction;
+    hd = (h + reduction - 1) / reduction;
+    halfwidth = (l_int32)(2.0 * sstdev);
+    for (index = 0; index < ncomps; index++) {
+        pixt = pixCopy(NULL, pixsc);
+        datat = pixGetData(pixt);
+        wplt = pixGetWpl(pixt);
+        kval = nc[index];
+            /* Separable convolutions: horizontal first */
+        for (i = 0; i < hd; i++) {
+            lines = datas + (border + i) * wpls;
+            linet = datat + (border + i) * wplt;
+            for (j = 0; j < wd; j++) {
+                sum = 0.0;
+                norm = 0.0;
+                for (k = -halfwidth; k <= halfwidth; k++) {
+                    nval = GET_DATA_BYTE(lines, border + j + k);
+                    kern = spatial[L_ABS(k)] * range[L_ABS(kval - nval)];
+                    sum += kern * nval;
+                    norm += kern;
+                }
+                if (norm > 0.0) {
+                    dval = (l_int32)((sum / norm) + 0.5);
+                    SET_DATA_BYTE(linet, border + j, dval);
+                }
+            }
+        }
+            /* Vertical convolution */
+        pixd = pixCreate(wd, hd, 8);
+        datad = pixGetData(pixd);
+        wpld = pixGetWpl(pixd);
+        for (i = 0; i < hd; i++) {
+            linet = datat + (border + i) * wplt;
+            lined = datad + i * wpld;
+            for (j = 0; j < wd; j++) {
+                sum = 0.0;
+                norm = 0.0;
+                for (k = -halfwidth; k <= halfwidth; k++) {
+                    nval = GET_DATA_BYTE(linet + k * wplt, border + j);
+                    kern = spatial[L_ABS(k)] * range[L_ABS(kval - nval)];
+                    sum += kern * nval;
+                    norm += kern;
+                }
+                if (norm > 0.0)
+                    dval = (l_int32)((sum / norm) + 0.5);
+                else
+                    dval = GET_DATA_BYTE(linet, border + j);
+                SET_DATA_BYTE(lined, j, dval);
+            }
+        }
+        pixDestroy(&pixt);
+        pixaAddPix(pixac, pixd, L_INSERT);
+    }
+    bil->pixac = pixac;
+    bil->lineset = (l_uint32 ***)pixaGetLinePtrs(pixac, NULL);
+    return bil;
+}
+
+
+/*!
+ * \brief   bilateralApply()
+ *
+ * \param[in]    bil
+ * \return  pixd
+ */
+static PIX *
+bilateralApply(L_BILATERAL  *bil)
+{
+l_int32      i, j, k, ired, jred, w, h, wpls, wpld, ncomps, reduction;
+l_int32      vals, vald, lowval, hival;
+l_int32     *kindex;
+l_float32    fract;
+l_float32   *kfract;
+l_uint32    *lines, *lined, *datas, *datad;
+l_uint32  ***lineset = NULL;  /* for set of PBC */
+PIX         *pixs, *pixd;
+PIXA        *pixac;
+
+    if (!bil)
+        return (PIX *)ERROR_PTR("bil not defined", __func__, NULL);
+    pixs = bil->pixs;
+    ncomps = bil->ncomps;
+    kindex = bil->kindex;
+    kfract = bil->kfract;
+    reduction = bil->reduction;
+    pixac = bil->pixac;
+    lineset = bil->lineset;
+    if (pixaGetCount(pixac) != ncomps)
+        return (PIX *)ERROR_PTR("PBC images do not exist", __func__, NULL);
+
+    if ((pixd = pixCreateTemplate(pixs)) == NULL)
+        return (PIX *)ERROR_PTR("pixd not made", __func__, NULL);
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    datad = pixGetData(pixd);
+    wpld = pixGetWpl(pixd);
+    pixGetDimensions(pixs, &w, &h, NULL);
+    for (i = 0; i < h; i++) {
+        lines = datas + i * wpls;
+        lined = datad + i * wpld;
+        ired = i / reduction;
+        for (j = 0; j < w; j++) {
+            jred = j / reduction;
+            vals = GET_DATA_BYTE(lines, j);
+            k = kindex[vals];
+            lowval = GET_DATA_BYTE(lineset[k][ired], jred);
+            hival = GET_DATA_BYTE(lineset[k + 1][ired], jred);
+            fract = kfract[vals];
+            vald = (l_int32)((1.0 - fract) * lowval + fract * hival + 0.5);
+            SET_DATA_BYTE(lined, j, vald);
+        }
+    }
+
+    return pixd;
+}
+
+
+/*!
+ * \brief   bilateralDestroy()
+ *
+ * \param[in,out]   pbil    will be set to null before returning
+ */
+static void
+bilateralDestroy(L_BILATERAL  **pbil)
+{
+l_int32       i;
+L_BILATERAL  *bil;
+
+    if (pbil == NULL) {
+        L_WARNING("ptr address is null!\n", __func__);
+        return;
+    }
+
+    if ((bil = *pbil) == NULL)
+        return;
+
+    pixDestroy(&bil->pixs);
+    pixDestroy(&bil->pixsc);
+    pixaDestroy(&bil->pixac);
+    LEPT_FREE(bil->spatial);
+    LEPT_FREE(bil->range);
+    LEPT_FREE(bil->nc);
+    LEPT_FREE(bil->kindex);
+    LEPT_FREE(bil->kfract);
+    for (i = 0; i < bil->ncomps; i++)
+        LEPT_FREE(bil->lineset[i]);
+    LEPT_FREE(bil->lineset);
+    LEPT_FREE(bil);
+    *pbil = NULL;
+}
+
+
+/*----------------------------------------------------------------------*
+ *    Exact implementation of grayscale or color bilateral filtering    *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   pixBilateralExact()
+ *
+ * \param[in]    pixs          8 bpp gray or 32 bpp rgb
+ * \param[in]    spatial_kel   gaussian kernel
+ * \param[in]    range_kel     [optional] 256 x 1, monotonically decreasing
+ * \return  pixd   8 bpp bilateral filtered image
+ *
+ * <pre>
+ * Notes:
+ *      (1) The spatial_kel is a conventional smoothing kernel, typically a
+ *          2-d Gaussian kernel or other block kernel.  It can be either
+ *          normalized or not, but must be everywhere positive.
+ *      (2) The range_kel is defined over the absolute value of pixel
+ *          grayscale differences, and hence must have size 256 x 1.
+ *          Values in the array represent the multiplying weight for each
+ *          gray value difference between the target pixel and center of the
+ *          kernel, and should be monotonically decreasing.
+ *      (3) If range_kel == NULL, a constant weight is applied regardless
+ *          of the range value difference.  This degenerates to a regular
+ *          pixConvolve() with a normalized kernel.
+ * </pre>
+ */
+PIX *
+pixBilateralExact(PIX       *pixs,
+                  L_KERNEL  *spatial_kel,
+                  L_KERNEL  *range_kel)
+{
+l_int32  d;
+PIX     *pixt, *pixr, *pixg, *pixb, *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetColormap(pixs) != NULL)
+        return (PIX *)ERROR_PTR("pixs is cmapped", __func__, NULL);
+    d = pixGetDepth(pixs);
+    if (d != 8 && d != 32)
+        return (PIX *)ERROR_PTR("pixs not 8 or 32 bpp", __func__, NULL);
+    if (!spatial_kel)
+        return (PIX *)ERROR_PTR("spatial_ke not defined", __func__, NULL);
+
+    if (d == 8) {
+        return pixBilateralGrayExact(pixs, spatial_kel, range_kel);
+    } else {  /* d == 32 */
+        pixt = pixGetRGBComponent(pixs, COLOR_RED);
+        pixr = pixBilateralGrayExact(pixt, spatial_kel, range_kel);
+        pixDestroy(&pixt);
+        pixt = pixGetRGBComponent(pixs, COLOR_GREEN);
+        pixg = pixBilateralGrayExact(pixt, spatial_kel, range_kel);
+        pixDestroy(&pixt);
+        pixt = pixGetRGBComponent(pixs, COLOR_BLUE);
+        pixb = pixBilateralGrayExact(pixt, spatial_kel, range_kel);
+        pixDestroy(&pixt);
+        pixd = pixCreateRGBImage(pixr, pixg, pixb);
+
+        pixDestroy(&pixr);
+        pixDestroy(&pixg);
+        pixDestroy(&pixb);
+        return pixd;
+    }
+}
+
+
+/*!
+ * \brief   pixBilateralGrayExact()
+ *
+ * \param[in]    pixs          8 bpp gray
+ * \param[in]    spatial_kel   gaussian kernel
+ * \param[in]    range_kel     [optional] 256 x 1, monotonically decreasing
+ * \return  pixd   8 bpp bilateral filtered image
+ *
+ * <pre>
+ * Notes:
+ *      (1) See pixBilateralExact().
+ * </pre>
+ */
+PIX *
+pixBilateralGrayExact(PIX       *pixs,
+                      L_KERNEL  *spatial_kel,
+                      L_KERNEL  *range_kel)
+{
+l_int32    i, j, id, jd, k, m, w, h, d, sx, sy, cx, cy, wplt, wpld;
+l_int32    val, center_val;
+l_uint32  *datat, *datad, *linet, *lined;
+l_float32  sum, weight_sum, weight;
+L_KERNEL  *keli;
+PIX       *pixt, *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetDepth(pixs) != 8)
+        return (PIX *)ERROR_PTR("pixs must be gray", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (!spatial_kel)
+        return (PIX *)ERROR_PTR("spatial kel not defined", __func__, NULL);
+    kernelGetParameters(spatial_kel, &sy, &sx, NULL, NULL);
+    if (w < 2 * sx + 1 || h < 2 * sy + 1) {
+        L_WARNING("w = %d < 2 * sx + 1 = %d, or h = %d < 2 * sy + 1 = %d; "
+                  "returning copy\n", __func__, w, 2 * sx + 1, h, 2 * sy + 1);
+        return pixCopy(NULL, pixs);
+    }
+    if (!range_kel)
+        return pixConvolve(pixs, spatial_kel, 8, 1);
+    if (range_kel->sx != 256 || range_kel->sy != 1)
+        return (PIX *)ERROR_PTR("range kel not {256 x 1", __func__, NULL);
+
+    keli = kernelInvert(spatial_kel);
+    kernelGetParameters(keli, &sy, &sx, &cy, &cx);
+    if ((pixt = pixAddMirroredBorder(pixs, cx, sx - cx, cy, sy - cy)) == NULL) {
+        kernelDestroy(&keli);
+        return (PIX *)ERROR_PTR("pixt not made", __func__, NULL);
+    }
+
+    pixd = pixCreate(w, h, 8);
+    datat = pixGetData(pixt);
+    datad = pixGetData(pixd);
+    wplt = pixGetWpl(pixt);
+    wpld = pixGetWpl(pixd);
+    for (i = 0, id = 0; id < h; i++, id++) {
+        lined = datad + id * wpld;
+        for (j = 0, jd = 0; jd < w; j++, jd++) {
+            center_val = GET_DATA_BYTE(datat + (i + cy) * wplt, j + cx);
+            weight_sum = 0.0;
+            sum = 0.0;
+            for (k = 0; k < sy; k++) {
+                linet = datat + (i + k) * wplt;
+                for (m = 0; m < sx; m++) {
+                    val = GET_DATA_BYTE(linet, j + m);
+                    weight = keli->data[k][m] *
+                        range_kel->data[0][L_ABS(center_val - val)];
+                    weight_sum += weight;
+                    sum += val * weight;
+                }
+            }
+            SET_DATA_BYTE(lined, jd, (l_int32)(sum / weight_sum + 0.5));
+        }
+    }
+
+    kernelDestroy(&keli);
+    pixDestroy(&pixt);
+    return pixd;
+}
+
+
+/*!
+ * \brief   pixBlockBilateralExact()
+ *
+ * \param[in]    pixs             8 bpp gray or 32 bpp rgb
+ * \param[in]    spatial_stdev    must be > 0.0
+ * \param[in]    range_stdev      must be > 0.0
+ * \return  pixd   8 bpp or 32 bpp bilateral filtered image
+ *
+ * <pre>
+ * Notes:
+ *      (1) See pixBilateralExact().  This provides an interface using
+ *          the standard deviations of the spatial and range filters.
+ *      (2) The convolution window halfwidth is 2 * spatial_stdev,
+ *          and the square filter size is 4 * spatial_stdev + 1.
+ *          The kernel captures 95% of total energy.  This is compensated
+ *          by normalization.
+ *      (3) The range_stdev is analogous to spatial_halfwidth in the
+ *          grayscale domain [0...255], and determines how much damping of the
+ *          smoothing operation is applied across edges.  The larger this
+ *          value is, the smaller the damping.  The smaller the value, the
+ *          more edge details are preserved.  These approximations are useful
+ *          for deciding the appropriate cutoff.
+ *              kernel[1 * stdev] ~= 0.6  * kernel[0]
+ *              kernel[2 * stdev] ~= 0.14 * kernel[0]
+ *              kernel[3 * stdev] ~= 0.01 * kernel[0]
+ *          If range_stdev is infinite there is no damping, and this
+ *          becomes a conventional gaussian smoothing.
+ *          This value does not affect the run time.
+ *      (4) If range_stdev is negative or zero, the range kernel is
+ *          ignored and this degenerates to a straight gaussian convolution.
+ *      (5) This is very slow for large spatial filters.  The time
+ *          on a 3GHz pentium is roughly
+ *             T = 1.2 * 10^-8 * (A * sh^2)  sec
+ *          where A = # of pixels, sh = spatial halfwidth of filter.
+ * </pre>
+ */
+PIX*
+pixBlockBilateralExact(PIX       *pixs,
+                       l_float32  spatial_stdev,
+                       l_float32  range_stdev)
+{
+l_int32    d, halfwidth;
+L_KERNEL  *spatial_kel, *range_kel;
+PIX       *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    d = pixGetDepth(pixs);
+    if (d != 8 && d != 32)
+        return (PIX *)ERROR_PTR("pixs not 8 or 32 bpp", __func__, NULL);
+    if (pixGetColormap(pixs) != NULL)
+        return (PIX *)ERROR_PTR("pixs is cmapped", __func__, NULL);
+    if (spatial_stdev <= 0.0)
+        return (PIX *)ERROR_PTR("invalid spatial stdev", __func__, NULL);
+    if (range_stdev <= 0.0)
+        return (PIX *)ERROR_PTR("invalid range stdev", __func__, NULL);
+
+    halfwidth = 2 * spatial_stdev;
+    spatial_kel = makeGaussianKernel(halfwidth, halfwidth, spatial_stdev, 1.0);
+    range_kel = makeRangeKernel(range_stdev);
+    pixd = pixBilateralExact(pixs, spatial_kel, range_kel);
+    kernelDestroy(&spatial_kel);
+    kernelDestroy(&range_kel);
+    return pixd;
+}
+
+
+/*----------------------------------------------------------------------*
+ *                         Kernel helper function                       *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   makeRangeKernel()
+ *
+ * \param[in]    range_stdev   must be > 0.0
+ * \return  kel, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Creates a one-sided Gaussian kernel with the given
+ *          standard deviation.  At grayscale difference of one stdev,
+ *          the kernel falls to 0.6, and to 0.01 at three stdev.
+ *      (2) A typical input number might be 20.  Then pixels whose
+ *          value differs by 60 from the center pixel have their
+ *          weight in the convolution reduced by a factor of about 0.01.
+ * </pre>
+ */
+L_KERNEL *
+makeRangeKernel(l_float32  range_stdev)
+{
+l_int32    x;
+l_float32  val, denom;
+L_KERNEL  *kel;
+
+    if (range_stdev <= 0.0)
+        return (L_KERNEL *)ERROR_PTR("invalid stdev <= 0", __func__, NULL);
+
+    denom = 2. * range_stdev * range_stdev;
+    if ((kel = kernelCreate(1, 256)) == NULL)
+        return (L_KERNEL *)ERROR_PTR("kel not made", __func__, NULL);
+    kernelSetOrigin(kel, 0, 0);
+    for (x = 0; x < 256; x++) {
+        val = expf(-(l_float32)(x * x) / denom);
+        kernelSetElement(kel, 0, x, val);
+    }
+    return kel;
+}