Julio da Silva
Julio da Silva

Reputation: 21

How to shift image array with supixel precison in python?

I am trying to shift a 2D array representing an image with subpixel precision using 2D FFTs and the Fourier transform shift theorem. It works well when the shift value is in an integer (pixel precision), however I get a lot of artifacts when the shift value is not an integer,ie., a fraction of a pixel. The code is below:

import numpy as np
from scipy.fftpack import fftfreq
def shift_fft(input_array,shift):
    shift_rows,shift_cols = shift
    nr,nc = input_array.shape
    Nr, Nc = fftfreq(nr), fftfreq(nc)
    Nc,Nr = np.meshgrid(Nc,Nr)
    fft_inputarray = np.fft.fft2(input_array)
    fourier_shift = np.exp(1j*2*np.pi*((shift_rows*Nr)+(shift_cols*Nc)))
    output_array = np.fft.ifft2(fft_inputarray*fourier_shift)
    return np.real(output_array)

Thus, shift_fft(input_array,[2,0]) will work, but shift_fft(input_array,[2.4,0]) will not work without artifacts. What I am doing wrong? For example, considering the image of Lena with 128x128 pixels. If I want to shift by 10.4 pixel in each direction, I get some wobbling modulation of the image. The images are the following:

Before:

Lena, before shift

After:

Lena, after shift

Upvotes: 2

Views: 5872

Answers (2)

Adrien Mau
Adrien Mau

Reputation: 324

For some reason I found scipy ndimage shift to be very slow, especially for n dimensional images. For shifting along a specific axis, for integer and non-integer shifts, I created a simple function :

def shift_img_along_axis( img, axis=0, shift = 1 , constant_values=0):
    """ shift array along a specific axis. New value is taken as weighted by the two distances to the assocaited original pixels.
    NOTE: at the border of image, when not enough original pixel is accessible, data will be meaned with regard to additional constant_values. 
    constant_values: value to set to pixels with no association in original image img 
    RETURNS : shifted image. 
    A.Mau. """
    intshift = int(shift)
    remain0 = abs( shift - int(shift) )
    remain1 = 1-remain0 #if shift is uint : remain1=1 and remain0 =0
    npad = int( np.ceil( abs( shift ) ) )  #ceil relative to 0. ( 0.5=> 1 and -0.5=> -1 )

    pad_arg = [(0,0)]*img.ndim
    pad_arg[axis] = (npad,npad)
    bigger_image = np.pad( img, pad_arg, 'constant', constant_values=constant_values) 
    
    part1 = remain1*bigger_image.take( np.arange(npad+intshift, npad+intshift+img.shape[axis]) ,axis)
    if remain0==0:
        shifted = part1
    else:
        if shift>0:
            part0 = remain0*bigger_image.take( np.arange(npad+intshift+1, npad+intshift+1+img.shape[axis]) ,axis) #
        else:
            part0 = remain0*bigger_image.take( np.arange(npad+intshift-1, npad+intshift-1+img.shape[axis]) ,axis) #

        shifted = part0 + part1
    return shifted

A quick example :

np.random.seed(1)
img = np.random.uniform(0,10,(3,4)).astype('int')
print( img ) 
shift = 1.5
shifted = shift_img_along_axis( img, axis=1, shift=shift )
print( shifted )

Image print :

[[4 7 0 3]
 [1 0 1 3]
 [3 5 4 6]]

Shifted image:

[[3.5 1.5 1.5 0. ]
 [0.5 2.  1.5 0. ]
 [4.5 5.  3.  0. ]]

With our shift of 1.5 the first value in shifted image is the mean of 7 and 0, and so on... If a value is missing in the original image an additionnal value of 0 will be taken. If you want to get a similar result as np.roll (image goes back to the other side...) you would have to modify it a bit !

Upvotes: 0

Ehsan Kia
Ehsan Kia

Reputation: 1544

You can try using scipy.ndimage.shift. It shifts pixels similar to numpy.roll, but also allows fractional shift values with interpolations.

For a colored image, make sure to provide a shift of 0 for the 3rd axis (channels).

import scipy.ndimage

scipy.ndimage.shift(input_array, (2.4, 0))

By default it'll set the background to black, but you can adjust the mode to have it wrap around or have a custom color.

Upvotes: 3

Related Questions