6

Is there a way in scipy (or other similar library) to get the convolution of an image with a given kernel only at some desired points?

I'm looking for something like:

ndimage.convolve(image, kernel, mask=mask)

Where mask contains True (or 1) whenever the kernel needs to be applied, False (or 0) otherwise.

EDIT: Example python code that does what I'm trying to do (but not faster than a whole image convolution using scipy):

def kernel_responses(im, kernel, mask=None, flatten=True):
    if mask is None:
        mask = np.ones(im.shape[:2], dtype=np.bool)

    ks = kernel.shape[0]//2

    data = np.pad(im, ks, mode='reflect')
    y, x = np.where(mask)

    responses = np.empty(y.shape[0], float)

    for k, (i, j) in enumerate(zip(y, x)):
        responses[k] = (data[i:i+ks*2+1, j:j+ks*2+1] * kernel).sum()

    if flatten:
        return responses

    result = np.zeros(im.shape[:2], dtype=float)
    result[y, x] = responses
    return result

The above code does the job with a wrap boundary conditions, but the inner loop is in python, and thus, slow. I was wondering if there is something faster already implemented in scipy/opencv/skimage.

Imanol Luengo
  • 15,366
  • 2
  • 49
  • 67
  • @tom10 Is not about ignoring points in the evaluation. The idea is to ignore the evaluation at some points. I don't want to convolve the whole image, I just want the kernel response at given (say 20) points. I want to speed up the traditional "convolve image and get points" by just "calculating kernel response at given points". – Imanol Luengo Apr 21 '15 at 20:20

3 Answers3

5

I know that I'm responding to my own answer, I hope the code bellow carries further improvements, or it might be useful for other users.

The code bellow is a cython/python implementation:

PYTHON:

def py_convolve(im, kernel, points):
    ks = kernel.shape[0]//2
    data = np.pad(im, ks, mode='constant', constant_values=0)
    return cy_convolve(data, kernel, points)

CYTHON:

import numpy as np
cimport cython

@cython.boundscheck(False)
def cy_convolve(unsigned char[:, ::1] im, double[:, ::1] kernel, Py_ssize_t[:, ::1] points):
    cdef Py_ssize_t i, j, y, x, n, ks = kernel.shape[0]
    cdef Py_ssize_t npoints = points.shape[0]
    cdef double[::1] responses = np.zeros(npoints, dtype='f8')

    for n in range(npoints):
        y = points[n, 0]
        x = points[n, 1]
        for i in range(ks):
            for j in range(ks):
                responses[n] += im[y+i, x+j] * kernel[i, j]

     return np.asarray(responses)

Comparision with other methods

The following tables shows evaluation of 4 methods:

  1. My python method in the question
  2. The method from @Vighnesh Birodkar
  3. Complete image convolution with scipy
  4. My python/cython implementation in this post

Each rows, in order, correspond to those methods for 3 different images (coins, camera and lena from skimage.data respectively) and each of the columns corresponds to a different ammount of points to calculate the kernel responses (is in percentages as meaning "calculate response in x% of the points of the image").

For calculating the kernel response in less than 50% of the points, my implementation is faster than convolving the whole image, but is not faster otherwise..

EDIT: kernel windows for the tests are 5x5 uniform windows (np.ones((5,5))).

['303x384']    1%     2%     5%    10%     20%     50%
1            4.97   9.58  24.32  48.28  100.39  245.77
2            7.60  15.09  37.42  75.17  150.09  375.60
3            3.05   2.99   3.04   2.88    2.96    2.98
4            0.17   0.22   0.38   0.60    1.10    2.49

['512x512']     1%     2%     5%     10%     20%     50%
1            10.68  21.87  55.47  109.16  223.58  543.73
2            17.90  34.59  86.02  171.20  345.46  858.24
3             6.52   6.53   6.74    6.63    6.43    6.60
4             0.31   0.43   0.78    1.34    2.73    6.82

['512x512']     1%     2%     5%     10%     20%     50%
1            13.21  21.45  54.98  110.80  217.11  554.96
2            19.55  34.78  87.09  172.33  344.58  893.02
3             6.87   6.82   7.00    6.60    6.64    7.71
4             0.35   0.47   0.87    1.57    2.47    6.07

NOTE: times are in ms.

Imanol Luengo
  • 15,366
  • 2
  • 49
  • 67
  • for your profile table you should add the kernel shape (since different kernel sizes will obviously change your results). Also, one typo suggestion, escribiste percentajes en espanol: "(is in percentajes as meaning..." – Scott Apr 22 '15 at 19:09
3

I don't know of any function that does exactly what you're asking. If instead of providing a mask of points to be convolved you provided a list of points ex. [(7, 7), (100, 100)] then it might be as simple as getting the appropriate image patch (say the same size as your provided kernel), convolve the image patch and kernel, and insert back into the original image.

Here's a coded example, hopefully it's close enough for you to modify lightly:

[EDIT: I noticed a couple errors I had in my padding and patch arithmetic. Previously, you could not convolve with a point right on the boarder (say (0, 0)), I doubled the padding, fixed some arithmetic, and now all is well.]

import cv2
import numpy as np
from scipy import ndimage
from matplotlib import pyplot as plt

def image_convolve_mask(image, list_points, kernel):
# list_points ex. [(7, 7), (100, 100)]
# assuming kernels of dims 2n+1 x 2n+1
rows, cols = image.shape
k_rows, k_cols = kernel.shape
r_pad = int(k_rows/2)
c_pad = int(k_cols/2)
# zero-pad the image in case desired point is close to border
padded_image = np.zeros((rows + 2*k_rows, cols + 2*k_cols))
# set the original image in the center
padded_image[k_rows: rows + k_rows, k_cols: cols + k_cols] = image
# should you prefer to use np.pad:
# padded_image = np.pad(image, (k_rows, k_cols), 'constant', constant_values=(0, 0))

for p in list_points:
    # extract pertinent patch from image
    # arbitrarily choosing the patch as same size as the kernel; change as needed
    patch = padded_image[p[0] + k_rows - r_pad: p[0] + 2*k_rows - r_pad, p[1] + k_cols - c_pad: p[1] + 2*k_cols - c_pad]

    # here use whatever function for convolution; I prefer cv2filter2D()
    # commented out is another option
    # conv = ndimage.convolve(patch, kernel, mode='constant', cval=0.0)
    conv = cv2.filter2D(patch, -1, kernel)
    # set the convolved patch back in to the image
    padded_image[p[0] + k_rows - r_pad: p[0] + 2*k_rows - r_pad, p[1] + k_cols - c_pad: p[1] + 2*k_cols - c_pad] = conv

return padded_image[k_rows: rows + k_rows, k_cols: cols + k_cols]

Now to try it out on an image:

penguins = cv2.imread('penguins.png', 0)
kernel = np.ones((5,5),np.float32)/25
# kernel = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], np.float32)
conv_image = image_convolve_mask(penguins, [(7, 7), (36, 192), (48, 207)], kernel)
plt.imshow(conv_image, cmap = 'gray', interpolation = 'bicubic')
plt.xticks([]), plt.yticks([])
plt.show()

I applied a 5x5 box smoother and can't really see any change around pixel (7, 7), but I chose the other two points to be the tips of the two left-most penguin's beaks. So you can see the smoothed patches. enter image description here enter image description here

Here is a lena512 image with 21 convolution points (time:0.006177 sec). enter image description here

[EDIT 2: An example of using a mask to generate a list of row, col tuples to feed in to the function.]

mask = np.eye(512)
k = np.ones((25, 25), np.float32)/625
list_mask = zip(np.where(mask==1)[0], np.where(mask==1)[1])
tic = time.time()
conv_image = image_convolve_mask(lena, list_mask, k)
print 'time: ', time.time()-tic # 0.08136 sec

enter image description here

Scott
  • 6,089
  • 4
  • 34
  • 51
  • Thanks! It's close enough to what I'm looking for (+1 for that), but I still need to check it's performance. A python loop might be slower than just convolving the whole image with scipy or opencv. – Imanol Luengo Apr 22 '15 at 08:39
  • You're welcome. If you really wanted to use an array mask obviously you could take the mask, pull out (row, col), and convert it to a list of tuples. As far as performance, it would all depend on how many points you have. I ran this at work with 20 points on a 512x512 image and it ran in 0.00286 sec. Separately, if your points end up being a contiguous patch(es) (say a large rectangle), you can modify the code above to just convolve the rectangle(s). – Scott Apr 22 '15 at 16:57
  • Thank you very much again. However the method is slower than convolving the whole image with scipy (which is in fortran/c++ I think). I've added an answer with some profiling. – Imanol Luengo Apr 22 '15 at 18:48
  • Since there is no other implementation, and yours helped me make the cython version I just gave you the answer :P Thank you for the help! – Imanol Luengo Apr 22 '15 at 19:14
  • I profiled this and it is slow. Not something you could use for video processing. So for lena512, 10% points, and 5x5 kernel on my hardware it's ~750ms. Thanks for the plus and check. I had fun playing with this. – Scott Apr 22 '15 at 19:22
  • 1
    Note also the ``np.pad`` function – Stefan van der Walt Apr 22 '15 at 21:17
  • @StefanvanderWalt Sometimes you just have to see if you can work out the arithmetic on your own. I'll insert a commented line using np.pad. – Scott Apr 22 '15 at 22:12
0

You can use the following code snippet. If you mask is sufficiently dense, it might not be that inefficient.

def mask_conv(img, kernel, mask):
    out = filters.convolve(img, kernel)
    return np.where(mask, out, img)

Some sample code

from skimage import data, draw, io, color
from scipy.ndimage import filters
import numpy as np

def mask_conv(img, kernel, mask):
    out = filters.convolve(img, kernel)
    return np.where(mask, out, img)

img = data.camera()
mask = np.zeros_like(img, dtype=np.bool)

kernel = np.ones((9,9))/100
circle = draw.circle(300, 350, 100)
mask[circle] = True

out = mask_conv(img, kernel, mask)

io.imshow(out)
io.show()

Convolution with mask