[Numpy-discussion] Bilateral filter

Nadav Horesh nadavh@visionsense....
Tue Aug 5 10:38:41 CDT 2008


Attached here my cython implementation of the bilateral filter, which is
my first cython program. I would ask for the following:


     1. Is there any way to speed up the code just by "cosmetic"
        modifications?
     2. In particular I use the unportable gcc function __builtin_abs:
        Is there any way to access this in a portable way?
     3. I would like to donate this code to scipy or any other suitable
        project. What can/should I do to realise it?


Remarks:

The code contains 3 end-user routines:

     1. A pure python implementation: Easy to read and modify --- it can
        be cut out into a python source code.
     2. A straight forward cython implementation: About 4 times as fast
        as the python implementation.
     3. An optimised cython implementation earning another factor of 2
        in speed (depends on the parameters used).

I see this code as a "research grade" that would evolve in the near
future, as I am working on a related project, and hopefully following
your comments.

  Nadav
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://projects.scipy.org/pipermail/numpy-discussion/attachments/20080805/29c0641a/attachment-0001.html 
-------------- next part --------------
'''
  A cython implementation of the plain (and slow) algorithm for bilateral
  filtering.

  Copyright 2008, Nadav Horesh
  nadavh at visionsense.com 
'''

from numpy.numarray.nd_image import generic_filter
import numpy as np


cdef extern from "arrayobject.h":
    ctypedef int intp
    ctypedef extern class numpy.ndarray [object PyArrayObject]:
        cdef char *data
        cdef int nd
        cdef intp *dimensions
        cdef intp *strides
        cdef int flags

cdef extern from "math.h":
    double exp(double x)

## gcc specific (?)
cdef extern:
    int __builtin_abs(int x)

cdef int GAUSS_SAMP = 32
cdef int GAUSS_IDX_MAX = GAUSS_SAMP - 1

class _Bilat_fcn(object):
    '''
    The class provides the bilaterl filter function to be called by
    generic_filter.
    initialization parameters:
      spat_sig:    The sigma of the spatial Gaussian filter
      inten_sig:   The sigma of the gray-levels Gaussian filter
      filter_size: (int) The size of the spatial convolution kernel. If
                   not set, it is set to ~ 4*spat_sig.
    '''
    def __init__(self, spat_sig, inten_sig, filter_size=None):
        if filter_size is not None and filter_size >= 2:
            self.xy_size = int(filter_size)
        else:
            self.xy_size = int(round(spat_sig*4))
            # Make filter size odd
            self.xy_size += 1-self.xy_size%2
        x = np.arange(self.xy_size, dtype=float)
        x = (x-x.mean())**2
        #xy_ker: Spatial convolution kernel
        self.xy_ker = np.exp(-np.add.outer(x,x)/(2*spat_sig**2)).ravel()
        self.xy_ker /= self.xy_ker.sum()
        self.inten_sig = 2 * inten_sig**2
        self.index = self.xy_size**2 // 2

        ## An initialization for LUT instead of a Gaussian function call
        ## (for the fc_filter method)

        x = np.linspace(0,3.0, GAUSS_SAMP)
        self.gauss_lut = np.exp(-x**2/2)
        self.gauss_lut[-1] = 0.0  # Nullify all distant deltas        
        self.x_quant = 3*inten_sig / GAUSS_IDX_MAX


    ##
    ## Filtering functions
    ##

    def __call__ (self, data):
        'An unoptimized (pure python) implementation'
        weight = np.exp(-(data-data[self.index])**2/self.inten_sig) * self.xy_ker
        return np.dot(data, weight) / weight.sum()

    def cfilter(self, ndarray data):
        'An optimized implementation'
        cdef ndarray kernel = self.xy_ker
        cdef double sigma   = self.inten_sig
        cdef double weight_i, weight, result, centre, dat_i
        cdef double *pdata=<double *>data.data, *pker=<double *>kernel.data
        cdef int i, dim = data.dimensions[0]
        centre = pdata[self.index]

        weight = 0.0
        result = 0.0

        for i from  0 <= i < dim:
            dat_i = pdata[i]
            weight_i = exp(-(dat_i-centre)**2 / sigma) * pker[i]
            weight += weight_i;
            result += dat_i * weight_i
        return result / weight


    def fc_filter(self, ndarray data):
        'Use further optimisation by replacing exp functions calls by a LUT'
        cdef ndarray kernel = self.xy_ker
        cdef ndarray gauss_lut_arr = self.gauss_lut

        cdef double sigma   = self.inten_sig
        cdef double weight_i, weight, result, centre, dat_i
        cdef double *pdata=<double *>data.data, *pker=<double *>kernel.data
        cdef int i, dim = data.dimensions[0]
        cdef int exp_i    # Entry index for the LUT
        cdef double x_quant = self.x_quant
        cdef double *gauss_lut = <double *>gauss_lut_arr.data

        centre = pdata[self.index]

        weight = 0.0
        result = 0.0

        for i from  0 <= i < dim:
            dat_i = pdata[i]
            exp_i = __builtin_abs(<int>((dat_i-centre) / x_quant))
            if exp_i > GAUSS_IDX_MAX:
                exp_i = GAUSS_IDX_MAX
            weight_i = gauss_lut[exp_i] * pker[i]
            weight += weight_i;
            result += dat_i * weight_i
        return result / weight


#
# Filtering functions to be called from outsize
#


def bilateral(mat, xy_sig, z_sig, filter_size=None, mode='reflect'):
    '''
    Bilateral filter a 2D array.
      mat:         A 2D array
      xy_sig:      The sigma of the spatial Gaussian filter.
      z_sig:       The sigma of the gray levels Gaussian filter.
      filter_size: Sise of the spatial filter kernel: None of values < 2 --- auto select.
      mode:        See numpy.numarray.nd_image.generic_filter documentation

    output: A 2D array of the same dimensions as mat and a float64 dtype

    Remarks:
      1. The spatial filter kernel size is ~4*xy_sig, unless specified otherwise
      2. The algorithm is slow but has a minimal memory footprint
    '''
    
    filter_fcn = _Bilat_fcn(xy_sig, z_sig, filter_size)
    size = filter_fcn.xy_size
    return generic_filter(mat, filter_fcn.cfilter, size =(size,size), mode=mode)

def bilateral_slow(mat, xy_sig, z_sig, filter_size=None, mode='reflect'):
    'A pure python implementation of the bilateral filter, for details see bilateral doc.'
    filter_fcn = _Bilat_fcn(xy_sig, z_sig, filter_size)
    size = filter_fcn.xy_size
    return generic_filter(mat, filter_fcn, size =(size,size), mode=mode)

def bilateral_fast(mat, xy_sig, z_sig, filter_size=None, mode='reflect'):
    'A fast implementation of bilateral filter, for details see bilateral doc.'
    filter_fcn = _Bilat_fcn(xy_sig, z_sig, filter_size)
    size = filter_fcn.xy_size
    return generic_filter(mat, filter_fcn.fc_filter, size =(size,size), mode=mode)


More information about the Numpy-discussion mailing list