#!/usr/bin/python2 """Demonstration of image convolution in Python Yes, scipy.ndimage does a better job at it. This is just a demonstration of proper optimization of numpy routines """ import numpy def apply_filter_mask(image, mask): """Applies some kind of convolution filter Arguments: image -- Array-like object (preferably numpy.array). First and second dimension: Height and width. Third dimension: Color channels mask -- Array-like object of the filter coefficients. Two-dimensional. For example ((1, 2, 1), (2, 4, 2), (1, 2, 1)) for a Gauss filter Return value: A new image object of the same shape and type as image. Borders are initialized to 0 """ # First, ensure we don't get any surprises with the argument types image, mask = (numpy.asarray(arr) for arr in (image, mask)) # Alright. Let's assume we have the mask mentioned in the description. # Over the course of this function we will need every pixel in image # multiplied by 1, 2, and 4. So let's do this only once in the beginning. # # Also, because we are multiplying a huge array with a scalar, it is worth # checking whether we actually need to do a multiplication. For 0 we skip # processing altogether. # This can be extended to deal with cases where a bit shift is sufficient # but you get the idea. parts = {factor: image if factor == 1 else image * factor for factor in set(mask.flat) if factor} # We don't need to care about the number of color channels. # Numpy's broadcast and vector operation rules will do it automatically height, width = image.shape[0:2] # For the iteration through the mask, we need the factor and the relative # offsets compared to the center pixel in the mask maskheight, maskwidth = mask.shape verticalborder, horizontalborder = (length / 2 for length in (maskheight, maskwidth)) # Now build the output array. As an optimization, we could copy the first # parts entry, saving the zero initialization and an addition but let's # keep it simple for now result = numpy.zeros_like(image) # Here we just leave the borders at 0 but in general, when we need special # treatment for border pixels, it is faster to do it separately outside # the loop for the center frame center_frame = result[verticalborder:-verticalborder, horizontalborder:-horizontalborder] # Let's skip being fancy and use a simple nested for loop for the rest. # It's only a few iterations, anyway for verticaloff, maskline in enumerate(mask): vertical_end = height - maskheight + verticaloff + 1 for horizontaloff, factor in enumerate(maskline): if not factor: continue part = parts[factor] horizontal_end = width - maskwidth + horizontaloff + 1 center_frame += part[verticaloff:vertical_end, horizontaloff:horizontal_end] # Alright. Nearly done. Now we just need to normalize the result. This is # an integer division and therefore extremely slow, especially on ARM CPUs. # So, let's go the extra mile and replace it with a bit shift if we can normfactor = mask.sum() if normfactor == 1: # maybe we are dealing with a normalized mask of floats? return result # In Python3 we could use math.log2. Oh well, let's do it the ugly way try: binnorm = bin(normfactor) except TypeError: # again, probably a mask of floats center_frame /= normfactor else: if binnorm.count('1') > 1: # no power of two center_frame /= normfactor else: normshift = binnorm.count('0') - 1 center_frame >>= normshift return result def main(): image = numpy.random.random_integers(0, 10, 25).reshape(5, 5) mask = numpy.array(((1, 2, 1), (2, 4, 2), (1, 2, 1))) filtered = apply_filter_mask(image, mask) print "%s\n*\n%s\n=\n%s\n" % (image, mask, filtered) if __name__ == '__main__': main()