/************************************************************************/
/*                                                                      */
/*                 Copyright 2009 by Ullrich Koethe                     */
/*                                                                      */
/*    This file is part of the VIGRA computer vision library.           */
/*    The VIGRA Website is                                              */
/*        http://hci.iwr.uni-heidelberg.de/vigra/                       */
/*    Please direct questions, bug reports, and contributions to        */
/*        ullrich.koethe@iwr.uni-heidelberg.de    or                    */
/*        vigra@informatik.uni-hamburg.de                               */
/*                                                                      */
/*    Permission is hereby granted, free of charge, to any person       */
/*    obtaining a copy of this software and associated documentation    */
/*    files (the "Software"), to deal in the Software without           */
/*    restriction, including without limitation the rights to use,      */
/*    copy, modify, merge, publish, distribute, sublicense, and/or      */
/*    sell copies of the Software, and to permit persons to whom the    */
/*    Software is furnished to do so, subject to the following          */
/*    conditions:                                                       */
/*                                                                      */
/*    The above copyright notice and this permission notice shall be    */
/*    included in all copies or substantial portions of the             */
/*    Software.                                                         */
/*                                                                      */
/*    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND    */
/*    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES   */
/*    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND          */
/*    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT       */
/*    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,      */
/*    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING      */
/*    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR     */
/*    OTHER DEALINGS IN THE SOFTWARE.                                   */
/*                                                                      */
/************************************************************************/

#define PY_ARRAY_UNIQUE_SYMBOL vigranumpyfilters_PyArray_API
#define NO_IMPORT_ARRAY

#include <Python.h>
#include <vigra/config.hxx>
#include <boost/python.hpp>
#include <vigra/numpy_array.hxx>
#include <vigra/numpy_array_converters.hxx>
#include <vigra/functorexpression.hxx>
#include <vigra/convolution.hxx>
#include <vigra/multi_convolution.hxx>
#include <vigra/recursiveconvolution.hxx>
#include <vigra/nonlineardiffusion.hxx>
#include "vigranumpykernel.hxx"
#include "vigranumpyscaleparam.hxx"

namespace python = boost::python;

namespace vigra
{

template < class VoxelType, unsigned int ndim >
NumpyAnyArray
pythonConvolveOneDimension(NumpyArray<ndim, Multiband<VoxelType> > array,
                           unsigned int dim,
                           Kernel const & kernel,
                           NumpyArray<ndim, Multiband<VoxelType> > res=python::object())
{
    vigra_precondition(dim < ndim-1,
           "convolveOneDimension(): dim out of range.");

    res.reshapeIfEmpty(array.taggedShape(),
            "convolveOneDimension(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<array.shape(ndim-1);++k)
        {
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> barray = array.bindOuter(k);
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> bres = res.bindOuter(k);
            convolveMultiArrayOneDimension(srcMultiArrayRange(barray),
                                           destMultiArray(bres), dim, kernel);
        }
    }
    return res;
}

VIGRA_PYTHON_MULTITYPE_FUNCTOR_NDIM(pyConvolveOneDimension, pythonConvolveOneDimension)

template < class VoxelType, unsigned int ndim >
NumpyAnyArray
pythonSeparableConvolve_1Kernel(NumpyArray<ndim, Multiband<VoxelType> > array,
                                Kernel const & kernel,
                                NumpyArray<ndim, Multiband<VoxelType> > res=python::object())
{
    res.reshapeIfEmpty(array.taggedShape(),
            "convolve(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<array.shape(ndim-1);++k)
        {
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> barray = array.bindOuter(k);
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> bres = res.bindOuter(k);
            separableConvolveMultiArray(srcMultiArrayRange(barray), destMultiArray(bres), kernel);
        }
    }
    return res;
}

VIGRA_PYTHON_MULTITYPE_FUNCTOR_NDIM(pySeparableConvolve_1Kernel, pythonSeparableConvolve_1Kernel)

template < class VoxelType, unsigned int ndim >
NumpyAnyArray
pythonSeparableConvolve_NKernels(NumpyArray<ndim, Multiband<VoxelType> > array,
                                 python::tuple pykernels,
                                 NumpyArray<ndim, Multiband<VoxelType> > res=python::object())
{
    if(python::len(pykernels) == 1)
    {
        return pythonSeparableConvolve_1Kernel(array,
                    python::extract<Kernel1D<KernelValueType> const &>(pykernels[0]), res);
    }

    vigra_precondition(python::len(pykernels) == ndim-1,
       "convolve(): Number of kernels must be 1 or equal to the number of spatial dimensions.");

    ArrayVector<Kernel1D<KernelValueType> > kernels;
    for(unsigned int k=0; k < ndim-1; ++k)
        kernels.push_back(python::extract<Kernel1D<KernelValueType> const &>(pykernels[k]));

    kernels = array.permuteLikewise(kernels);

    res.reshapeIfEmpty(array.taggedShape(),
            "convolve(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0; k < array.shape(ndim-1); ++k)
        {
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> barray = array.bindOuter(k);
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> bres = res.bindOuter(k);
            separableConvolveMultiArray(srcMultiArrayRange(barray),
                                        destMultiArray(bres), kernels.begin());
        }
    }
    return res;
}

VIGRA_PYTHON_MULTITYPE_FUNCTOR_NDIM(pySeparableConvolve_NKernels, pythonSeparableConvolve_NKernels)

template <class PixelType>
NumpyAnyArray
pythonConvolveImage(NumpyArray<3, Multiband<PixelType> > image,
                    TwoDKernel const & kernel,
                    NumpyArray<3, Multiband<PixelType> > res = python::object())
{
    res.reshapeIfEmpty(image.taggedShape(),
            "convolve(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<image.shape(2);++k)
        {
            MultiArrayView<2, PixelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<2, PixelType, StridedArrayTag> bres = res.bindOuter(k);
            convolveImage(srcImageRange(bimage), destImage(bres),
                          kernel2d(kernel));
        }
    }
    return res;
}

VIGRA_PYTHON_MULTITYPE_FUNCTOR(pyConvolveImage, pythonConvolveImage)

template <class PixelType>
NumpyAnyArray
pythonNormalizedConvolveImage(NumpyArray<3, Multiband<PixelType> > image,
                              NumpyArray<3, Multiband<PixelType> > mask,
                              TwoDKernel const & kernel,
                              NumpyArray<3, Multiband<PixelType> > res = python::object())
{
    vigra_precondition(mask.shape(2)==1 || mask.shape(2)==image.shape(2),
               "normalizedConvolveImage(): mask image must either have 1 channel or as many as the input image");
    vigra_precondition(mask.shape(0)==image.shape(0) && mask.shape(1)==image.shape(1),
               "normalizedConvolveImage(): mask dimensions must be same as image dimensions");

    res.reshapeIfEmpty(image.taggedShape(),
           "normalizedConvolveImage(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<image.shape(2);++k)
        {
            MultiArrayView<2, PixelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<2, PixelType, StridedArrayTag> bmask = mask.bindOuter(mask.shape(2)==1?0:k);
            MultiArrayView<2, PixelType, StridedArrayTag> bres = res.bindOuter(k);
            normalizedConvolveImage(srcImageRange(bimage), srcImage(bmask), destImage(bres),
                                    kernel2d(kernel));
        }
    }
    return res;
}

template < class VoxelType, unsigned int ndim >
NumpyAnyArray
pythonGaussianSmoothing(NumpyArray<ndim, Multiband<VoxelType> > array,
                        python::object sigma,
                        NumpyArray<ndim, Multiband<VoxelType> > res=python::object(),
                        python::object sigma_d = python::object(0.0),
                        python::object step_size = python::object(1.0),
                        double window_size = 0.0,
                        python::object roi = python::object())
{
    static const unsigned int N = ndim - 1;

    pythonScaleParam<N> params(sigma, sigma_d, step_size, "gaussianSmoothing");

    params.permuteLikewise(array);

    ConvolutionOptions<N> opt(params().filterWindowSize(window_size));

    if(roi != python::object())
    {
        typedef typename MultiArrayShape<N>::type Shape;
        Shape start = array.permuteLikewise(python::extract<Shape>(roi[0])());
        Shape stop  = array.permuteLikewise(python::extract<Shape>(roi[1])());
        opt.subarray(start, stop);
        res.reshapeIfEmpty(array.taggedShape().resize(stop-start),
                "gaussianSmoothing(): Output array has wrong shape.");
    }
    else
    {
        res.reshapeIfEmpty(array.taggedShape(),
                "gaussianSmoothing(): Output array has wrong shape.");
    }

    {
        PyAllowThreads _pythread;
        for(int k=0; k<array.shape(ndim-1); ++k)
        {
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> barray = array.bindOuter(k);
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> bres = res.bindOuter(k);
            gaussianSmoothMultiArray(srcMultiArrayRange(barray), destMultiArray(bres), opt);
        }
    }

    return res;
}

VIGRA_PYTHON_MULTITYPE_FUNCTOR_NDIM(pyGaussianSmoothing, pythonGaussianSmoothing)

template < class VoxelType>
NumpyAnyArray
pythonRecursiveGaussian(NumpyArray<3, Multiband<VoxelType> > image,
                        python::tuple sigmas,
                        NumpyArray<3, Multiband<VoxelType> > res=python::object())
{
    typedef typename NumericTraits<VoxelType>::RealPromote TmpType;

    const unsigned int ndim = 3;
    unsigned int sigmaCount = python::len(sigmas);
    vigra_precondition(sigmaCount == 1 || sigmaCount == ndim-1,
       "recursiveGaussianSmoothing(): Number of kernels must be 1 or equal to the number of spatial dimensions.");

    ArrayVector<double> scales;
    for(unsigned int k=0; k < sigmaCount; ++k)
    {
        scales.push_back(python::extract<double>(sigmas[k]));
    }
    for(unsigned int k=sigmaCount; k < ndim-1; ++k)
    {
        scales.push_back(scales.back());
    }
    scales = image.permuteLikewise(scales);

    res.reshapeIfEmpty(image.taggedShape(),
            "recursiveGaussianSmoothing(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        MultiArray<ndim-1, TmpType> tmp(image.bindOuter(0).shape());

        for(int k=0;k<image.shape(ndim-1);++k)
        {
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<ndim-1, VoxelType, StridedArrayTag> bres = res.bindOuter(k);
            recursiveGaussianFilterX(srcImageRange(bimage), destImage(tmp), scales[0]);
            recursiveGaussianFilterY(srcImageRange(tmp), destImage(bres), scales[1]);
        }
    }
    return res;

}

template < class VoxelType >
NumpyAnyArray
pythonRecursiveGaussianIsotropic(NumpyArray<3, Multiband<VoxelType> > image,
                                 double sigma,
                                 NumpyArray<3, Multiband<VoxelType> > res=python::object())
{
    return pythonRecursiveGaussian(image, python::make_tuple(sigma), res);
}

template <class PixelType>
NumpyAnyArray
pythonSimpleSharpening2D(NumpyArray<3, Multiband<PixelType> > image,
                         double sharpeningFactor,
                         NumpyArray<3, Multiband<PixelType> > res=python::object() )
{
    vigra_precondition(sharpeningFactor >= 0 ,
       "simpleSharpening2D(): sharpeningFactor must be >= 0.");

    res.reshapeIfEmpty(image.taggedShape(),
          "simpleSharpening2D(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<image.shape(2);++k)
        {
            MultiArrayView<2, PixelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<2, PixelType, StridedArrayTag> bres = res.bindOuter(k);
            simpleSharpening(srcImageRange(bimage), destImage(bres),
                             sharpeningFactor);
        }
    }
    return res;
}

template <class PixelType>
NumpyAnyArray
pythonGaussianSharpening2D(NumpyArray<3, Multiband<PixelType> > image,
                           double sharpeningFactor, double scale,
                           NumpyArray<3, Multiband<PixelType> > res=python::object() )
{
    vigra_precondition(sharpeningFactor >= 0 ,
       "gaussianSharpening2D(): sharpeningFactor must be >= 0.");
    vigra_precondition(sharpeningFactor >= 0 ,
       "gaussianSharpening2D(): scale must be >= 0.");

    res.reshapeIfEmpty(image.taggedShape(),
             "gaussianSharpening2D(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<image.shape(2);++k)
        {
            MultiArrayView<2, PixelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<2, PixelType, StridedArrayTag> bres = res.bindOuter(k);
            gaussianSharpening(srcImageRange(bimage), destImage(bres),
                               sharpeningFactor, scale);
        }
    }
    return res;
}

template <class PixelType, unsigned int N>
NumpyAnyArray
pythonLaplacianOfGaussian(NumpyArray<N, Multiband<PixelType> > array,
                          python::object scale,
                          NumpyArray<N, Multiband<PixelType> > res=python::object(),
                          python::object sigma_d = python::object(0.0),
                          python::object step_size = python::object(1.0),
                          double window_size = 0.0,
                          python::object roi = python::object())
{
    pythonScaleParam<N - 1> params(scale, sigma_d, step_size, "laplacianOfGaussian");
    params.permuteLikewise(array);

    std::string description("channel-wise Laplacian of Gaussian, scale=");
    description += asString(scale);

    ConvolutionOptions<N-1> opt(params().filterWindowSize(window_size));

    if(roi != python::object())
    {
        typedef typename MultiArrayShape<N-1>::type Shape;
        Shape start = array.permuteLikewise(python::extract<Shape>(roi[0])());
        Shape stop  = array.permuteLikewise(python::extract<Shape>(roi[1])());
        opt.subarray(start, stop);
        res.reshapeIfEmpty(array.taggedShape().resize(stop-start).setChannelDescription(description),
                "laplacianOfGaussian(): Output array has wrong shape.");
    }
    else
    {
        res.reshapeIfEmpty(array.taggedShape().setChannelDescription(description),
                "laplacianOfGaussian(): Output array has wrong shape.");
    }

    {
        PyAllowThreads _pythread;
        for(int k=0; k<array.shape(N-1); ++k)
        {
            MultiArrayView<N-1, PixelType, StridedArrayTag> barray = array.bindOuter(k);
            MultiArrayView<N-1, PixelType, StridedArrayTag> bres = res.bindOuter(k);
            laplacianOfGaussianMultiArray(srcMultiArrayRange(barray), destMultiArray(bres), opt);
        }
    }

    return res;
}

VIGRA_PYTHON_MULTITYPE_FUNCTOR_NDIM(pyLaplacianOfGaussian, pythonLaplacianOfGaussian)

template <class PixelType, unsigned int N>
NumpyAnyArray
pythonGaussianDivergence(NumpyArray<N, TinyVector<PixelType, N> > array,
                         python::object scale,
                         NumpyArray<N, Singleband<PixelType> > res=python::object(),
                         python::object sigma_d = python::object(0.0),
                         python::object step_size = python::object(1.0),
                         double window_size = 0.0,
                         python::object roi = python::object())
{
    pythonScaleParam<N> params(scale, sigma_d, step_size, "gaussianDivergence");
    params.permuteLikewise(array);

    std::string description("divergence of a vector field using Gaussian derivatives, scale=");
    description += asString(scale);

    ConvolutionOptions<N> opt(params().filterWindowSize(window_size));

    if(roi != python::object())
    {
        typedef typename MultiArrayShape<N>::type Shape;
        Shape start = array.permuteLikewise(python::extract<Shape>(roi[0])());
        Shape stop  = array.permuteLikewise(python::extract<Shape>(roi[1])());
        opt.subarray(start, stop);
        res.reshapeIfEmpty(array.taggedShape().resize(stop-start).setChannelDescription(description),
                "gaussianDivergence(): Output array has wrong shape.");
    }
    else
    {
        res.reshapeIfEmpty(array.taggedShape().setChannelDescription(description),
                "gaussianDivergence(): Output array has wrong shape.");
    }

    {
        PyAllowThreads _pythread;
        gaussianDivergenceMultiArray(array, res, opt);
    }

    return res;
}

VIGRA_PYTHON_MULTITYPE_FUNCTOR_NDIM(pyGaussianDivergence, pythonGaussianDivergence)

template <class PixelType>
NumpyAnyArray
pythonRecursiveFilter1(NumpyArray<3, Multiband<PixelType> > image,
                       double b, BorderTreatmentMode borderTreatment,
                       NumpyArray<3, Multiband<PixelType> > res = python::object())
{
    res.reshapeIfEmpty(image.taggedShape(),
            "recursiveFilter2D(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<image.shape(2);++k)
        {
            MultiArrayView<2, PixelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<2, PixelType, StridedArrayTag> bres = res.bindOuter(k);
            recursiveFilterX(srcImageRange(bimage), destImage(bres), b, borderTreatment);
            recursiveFilterY(srcImageRange(bres), destImage(bres), b, borderTreatment);
        }
    }
    return res;
}

template <class PixelType>
NumpyAnyArray
pythonRecursiveFilter2(NumpyArray<3, Multiband<PixelType> > image,
                       double b1, double b2,
                       NumpyArray<3, Multiband<PixelType> > res = python::object())
{
    res.reshapeIfEmpty(image.taggedShape(),
            "recursiveFilter2D(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        for(int k=0;k<image.shape(2);++k)
        {
            MultiArrayView<2, PixelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<2, PixelType, StridedArrayTag> bres = res.bindOuter(k);
            recursiveFilterX(srcImageRange(bimage), destImage(bres), b1, b2);
            recursiveFilterY(srcImageRange(bres), destImage(bres), b1, b2);
        }
    }
    return res;
}


template <class PixelType>
NumpyAnyArray
pythonRecursiveSmooth(NumpyArray<3, Multiband<PixelType> > image,
                      double scale, BorderTreatmentMode borderTreatment,
                      NumpyArray<3, Multiband<PixelType> > res = python::object())
{
    return pythonRecursiveFilter1(image, std::exp(-1.0/scale), borderTreatment, res);
}

template <class PixelType>
NumpyAnyArray
pythonRecursiveGradient(NumpyArray<2, Singleband<PixelType> > image,
                        double scale,
                        NumpyArray<2, TinyVector<PixelType, 2> > res = python::object())
{
    std::string description("recursive gradient, scale=");
    description += asString(scale);

    res.reshapeIfEmpty(image.taggedShape().setChannelDescription(description),
            "recursiveGradient2D(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        VectorComponentValueAccessor<TinyVector<PixelType, 2> > band(0);
        recursiveFirstDerivativeX(srcImageRange(image), destImage(res, band), scale);
        recursiveSmoothY(srcImageRange(res, band), destImage(res, band), scale);

        band.setIndex(1);
        recursiveSmoothX(srcImageRange(image), destImage(res, band), scale);
        recursiveFirstDerivativeY(srcImageRange(res, band), destImage(res, band), scale);
    }

    return res;
}

template <class PixelType>
NumpyAnyArray
pythonRecursiveLaplacian(NumpyArray<3, Multiband<PixelType> > image,
                         double scale,
                         NumpyArray<3, Multiband<PixelType> > res = python::object())
{
    using namespace vigra::functor;

    std::string description("channel-wise recursive Laplacian, scale=");
    description += asString(scale);

    res.reshapeIfEmpty(image.taggedShape().setChannelDescription(description),
            "recursiveLaplacian2D(): Output array has wrong shape.");

    {
        PyAllowThreads _pythread;
        MultiArrayShape<2>::type tmpShape(image.shape().begin());
        MultiArray<2, PixelType > tmp(tmpShape);
        for(int k=0;k<image.shape(2);++k)
        {
            MultiArrayView<2, PixelType, StridedArrayTag> bimage = image.bindOuter(k);
            MultiArrayView<2, PixelType, StridedArrayTag> bres = res.bindOuter(k);

            recursiveSecondDerivativeX(srcImageRange(bimage), destImage(bres), scale);
            recursiveSmoothY(srcImageRange(bres), destImage(bres), scale);

            recursiveSmoothX(srcImageRange(bimage), destImage(tmp), scale);
            recursiveSecondDerivativeY(srcImageRange(tmp), destImage(tmp), scale);

            combineTwoImages(srcImageRange(bres), srcImage(tmp), destImage(bres), Arg1()+Arg2());
        }
    }
    return res;
}

void defineConvolutionFunctions()
{
    using namespace python;

    docstring_options doc_options(true, true, false);

    multidef("convolveOneDimension",
        pyConvolveOneDimension<2, 5, float, double>().installFallback(),
        (arg("array"),
         arg("dim"),
         arg("kernel"),
         arg("out")=python::object()),
        "\n"
        "Convolve a single dimension of a scalar or multiband array with up to five dimensions.\n"
        "'dim' denotes the dimension to be convolved.\n"
        "'kernel' must be an instance of Kernel1D.\n"
        "\n"
        "For details see convolveMultiArrayOneDimension_ in the vigra C++ documentation.\n");

    multidef("convolve",
        pySeparableConvolve_1Kernel<2, 5, float, double>().installFallback().noPythonSignature(),
        (arg("array"),
         arg("kernel"),
         arg("out")=python::object()),
         "");

    multidef("convolve",
        pySeparableConvolve_NKernels<2, 5, float, double>().noPythonSignature(),
        (arg("array"),
         arg("kernels"),
         arg("out")=python::object()),
         "");

    multidef("convolve",
        pyConvolveImage<float, double>().noPythonSignature(),
        (arg("image"),
         arg("kernel"),
         arg("out")=python::object()),
        "convolve( (ndarray)array, kernel [, (ndarray)out=None]) -> ndarray\n"
        "\n"
        "Convolve an array (up to 5D) with the given 'kernel' (or kernels).\n"
        "If the input has multiple channels, the filter is applied to each channel\n"
        "independently. The function can be used in 3 different ways:\n"
        "\n"
        "* When 'kernel' is a single object of type :class:`Kernel1D`, this kernel\n"
        "  is applied along all spatial dimensions of the data (separable filtering).\n"
        "* When 'kernel' is a tuple of :class:`Kernel1D` objects, a different kernel\n"
        "  is used for each spatial dimension (separable filtering). The number of\n"
        "  kernels must equal the number of dimensions.\n"
        "* When 'kernel' is an instance of :class:`Kernel2D`, a 2-dimensional convolution\n"
        "  is performed (non-separable filtering). This is only applicable to 2D arrays.\n"
        "\n"
        "For details see separableConvolveMultiArray_ and "
        "convolveImage_ in the vigra C++ documentation.\n");

    def("normalizedConvolveImage", registerConverters(&pythonNormalizedConvolveImage<float>),
        (arg("image"), arg("mask"), arg("kernel"), arg("out") = python::object()),
        "Perform normalized convolution of an image. If the image has multiple channels, "
        "every channel is convolved independently. The 'mask' tells the algorithm "
        "whether input pixels are valid (non-zero mask value) or not. Invalid pixels "
        "are ignored in the convolution. The mask must have one channel (which is then "
        "used for all channels input channels) or as many channels as the input image.\n\n"
        "For details, see normalizedConvolveImage_ in the C++ documentation.\n");

    multidef("gaussianSmoothing",
        pyGaussianSmoothing<2, 5, npy_uint8, float, double>().installFallback(),
        (arg("array"),
         arg("sigma"),
         arg("out")=python::object(),
         arg("sigma_d")=0.0,
         arg("step_size")=1.0,
         arg("window_size")=0.0,
         arg("roi")=python::object()),
        "\n"
        "Perform Gaussian smoothing of an array with up to five dimensions.\n\n"
        "If the array has multiple channels, each channel is smoothed independently.\n"
        "\n"
        "If 'sigma' is a single value, an isotropic filter at this scale is\n"
        "applied (i.e., each dimension is filtered in the same way).\n"
        "If 'sigma' is a tuple or list of values, the amount of smoothing\n"
        "will be different for each spatial dimension.\n"
        "The optional 'sigma_d' (single, tuple, or list) denotes the PSF\n"
        "standard deviation per axis, the optional 'step_size' (single, tuple,\n"
        "or list) the distance between two adjacent pixels for each dimension.\n"
        "The length of the tuples or lists must be equal to the number of spatial\n"
        "dimensions.\n"
        "\n"
        "'window_size' specifies the ratio between the effective filter scale and\n"
        "the size of the filter window. Use a value around 2.0 to speed-up\n"
        "the computation by increasing the error resulting from cutting off the Gaussian.\n"
        "For the default 0.0, the window size is automatically determined.\n"
        "\n"
        "If 'roi' is not None, it must specify the desired region-of-interest as\n"
        "a pair '(first_point, beyond_last_point)' (e.g. 'roi=((10,20), (200,250))').\n"
        "As usual, the second point is the first point outside the ROI, and the ROI\n"
        "must not be outside the input array dimensions.\n"
        "The coordinates refer only to non-channel axes - if your array has an explicit\n"
        "channel axis, the ROI dimension must be one less than the array dimension.\n"
        "If you pass in an explicit 'out' array and specify an ROI, the 'out' array\n"
        "must have the shape of the ROI.\n\n"
        "For details see gaussianSmoothing_ and ConvolutionOptions_ in the vigra C++ documentation.\n");

    def("recursiveGaussianSmoothing2D",
        registerConverters(&pythonRecursiveGaussian<float>),
        (arg("image"), arg("sigma"), arg("out")=python::object()),
        "Compute a fast approximate Gaussian smoothing of a 2D scalar or multiband image.\n\n"
        "This function uses the third-order recursive filter approximation to the "
        "Gaussian filter proposed by Young and van Vliet. "
        "Each channel of the array is smoothed independently. "
        "If 'sigma' is a single value, an isotropic Gaussian filter at this scale is "
        "applied (i.e. each dimension is smoothed in the same way). "
        "If 'sigma' is a tuple of values, the amount of smoothing will be different "
        "for each spatial dimension. The length of the tuple must be equal to the "
        "number of spatial dimensions.\n\n"
        "For details see recursiveGaussianFilterLine_ in the vigra C++ documentation.\n");

    def("recursiveGaussianSmoothing2D",
        registerConverters(&pythonRecursiveGaussianIsotropic<float>),
        (arg("image"), arg("sigma"), arg("out")=python::object()),
        "Compute isotropic fast approximate Gaussian smoothing.\n");

    def("simpleSharpening2D",
        registerConverters(&pythonSimpleSharpening2D<float>),
        (arg("image"), arg("sharpeningFactor")=1.0, arg("out") = python::object()),
        "Perform simple sharpening function.\n"
        "\n"
        "For details see simpleSharpening_ in the vigra C++ documentation.\n");

    def("gaussianSharpening2D",
        registerConverters(&pythonGaussianSharpening2D<float>),
        (arg("image"), arg("sharpeningFactor")=1.0, arg("scale")=1.0, arg("out") = python::object()),
          "Perform sharpening function with gaussian filter."
          "\n\n"
          "For details see gaussianSharpening_ in the vigra C++ documentation.\n");

    multidef("laplacianOfGaussian",
        pyLaplacianOfGaussian<3, 4, float, double>().installFallback(),
        (arg("array"),
         arg("scale") = 1.0,
         arg("out") = python::object(),
         arg("sigma_d") = 0.0,
         arg("step_size") = 1.0,
         arg("window_size")=0.0,
         arg("roi")=python::object()),
        "\n"
        "Filter a 2D or 3D scalar array with the Laplacian of Gaussian operator\n"
        "at the given scale. Multiple channels are filtered independently.\n"
        "\n"
        "If 'sigma' is a single value, an isotropic filter at this scale is\n"
        "applied (i.e., each dimension is filtered in the same way).\n"
        "If 'sigma' is a tuple or list of values, the amount of smoothing\n"
        "will be different for each spatial dimension.\n"
        "The optional 'sigma_d' (single, tuple, or list) denotes the PSF\n"
        "standard deviation per axis, the optional 'step_size' (single, tuple,\n"
        "or list) the distance between two adjacent pixels for each dimension.\n"
        "The length of the tuples or lists must be equal to the number of spatial\n"
        "dimensions.\n"
        "\n"
        "'window_size' and 'roi' have the same meaning as in :func:`gaussianSmoothing`.\n\n"
        "For details see laplacianOfGaussianMultiArray_ and ConvolutionOptions_ in the vigra C++ documentation.\n");

    multidef("gaussianDivergence",
        pyGaussianDivergence<2, 3, float, double>().installFallback(),
        (arg("array"),
         arg("scale") = 1.0,
         arg("out") = python::object(),
         arg("sigma_d") = 0.0,
         arg("step_size") = 1.0,
         arg("window_size")=0.0,
         arg("roi")=python::object()),
        "\n"
        "Compute the divergence of a 2D or 3D vector field with a first\n"
        "derivative of Gaussian at the given scale. The array must have\n"
        "as many channels as spatial dimensions.\n"
        "\n"
        "If 'sigma' is a single value, an isotropic filter at this scale is\n"
        "applied (i.e., each dimension is filtered in the same way).\n"
        "If 'sigma' is a tuple or list of values, the amount of smoothing\n"
        "will be different for each spatial dimension.\n"
        "The optional 'sigma_d' (single, tuple, or list) denotes the PSF\n"
        "standard deviation per axis, the optional 'step_size' (single, tuple,\n"
        "or list) the distance between two adjacent pixels for each dimension.\n"
        "The length of the tuples or lists must be equal to the number of spatial\n"
        "dimensions.\n"
        "\n"
        "'window_size' and 'roi' have the same meaning as in :func:`gaussianSmoothing`.\n\n"
        "For details see gaussianDivergenceMultiArray_ and ConvolutionOptions_ in the vigra C++ documentation.\n");

    def("recursiveFilter2D", registerConverters(&pythonRecursiveFilter1<float>),
              (arg("image"), arg("b"), arg("borderTreament") = BORDER_TREATMENT_REFLECT, arg("out") = python::object()),
              "Perform 2D convolution with a first-order recursive filter with "
              "parameter 'b' and given 'borderTreatment'. 'b' must be between -1 and 1.\n"
              "\n"
              "For details see recursiveFilterX_ and recursiveFilterY_ (which "
              "this function calls in succession) in the vigra C++ documentation.\n");

    def("recursiveFilter2D", registerConverters(&pythonRecursiveFilter2<float>),
              (arg("image"), arg("b1"), arg("b2"), arg("out") = python::object()),
              "Perform 2D convolution with a second-order recursive filter with "
              "parameters 'b1' and 'b2'. Border treatment is always BORDER_TREATMENT_REFLECT.\n"
              "\n"
              "For details see recursiveFilterX_ and recursiveFilterY_ (which "
              "this function calls in succession) in the vigra C++ documentation.\n");

    def("recursiveSmooth2D", registerConverters(&pythonRecursiveSmooth<float>),
              (arg("image"), arg("scale"), arg("borderTreament") = BORDER_TREATMENT_REFLECT, arg("out") = python::object()),
              "Calls recursiveFilter2D() with b = exp(-1/scale), which "
              "corresponds to smoothing with an exponential filter exp(-abs(x)/scale).\n"
              "\n"
              "For details see recursiveSmoothLine_ in the vigra C++ documentation.\n");

    def("recursiveGradient2D", registerConverters(&pythonRecursiveSmooth<float>),
              (arg("image"), arg("scale"), arg("out") = python::object()),
              "Compute the gradient of a scalar image using a recursive (exponential) filter "
              "at the given 'scale'. The output image (if given) must have two channels.\n"
              "\n"
              "For details see recursiveSmoothLine_ and recursiveFirstDerivativeLine_ (which "
              "this function calls internally) in the vigra C++ documentation.\n");

    def("recursiveLaplacian2D", registerConverters(&pythonRecursiveLaplacian<float>),
              (arg("image"), arg("scale"), arg("out") = python::object()),
              "Compute the gradient of a 2D scalar or multiband image using a recursive (exponential) filter "
              "at the given 'scale'. The output image (if given) must have as many channels as the input.\n"
              "\n"
              "For details see recursiveSmoothLine_ and recursiveSecondDerivativeLine_ (which "
              "this function calls internally) in the vigra C++ documentation.\n");

}

} // namespace vigra
