I'm working on an image processing problem and wondering if DFT(rotation(image)) == rotation(DFT(image)) (1). My final goal is to apply rotations in the Fourier domain then do an inverse Fourier transform and get the rotated image.
This question is pretty close to what I'm looking for. That question links to ressources saying that (1) is true in theory (for infinite signals and infinite frequency bins), and the accepted answer shows that it is so mathematically. However, the answers also point out that it may be actually hard to reproduce in the "real world" with DFT algorithms, because of artefacts.
The images I'm showing here in the Fourier domain are zooms of the imaginary parts of the FTs, with normalized intensity to enhance visualization. However, the point stands for the real part, and also for magnitude and phase spectrums if you choose to visualize them like so.
As you can see, I am not getting results consistent with (1), as the two images of the last column are not equal (at all). And I feel like the problem is in the rotation of the Fourier image. The rotated Fourier image looks nothing like the Fourier of the rotated. I actually don't really get how the latter is computed: even with a 45 degrees rotation, the middle part stays about the same for instance (look at the 4 black neighbours of the central white pixels, they are the same in both images of column 2).
I've tried more severe padding of the original image, am careful with bit depths, have tried rotation magnitude and phase instead of real and imaginary parts, with no luck so far. Is there something I'm doing very wrong ? How can I achieve my goal to "correctly" apply rotation in the Fourier domain ?
import numpy as np import cv2 from scipy import ndimage # Helper function to save an image with a normalized range for visualization def normalize_and_cvt_to_uint8(im): min_ = np.min(im) max_ = np.max(im) im_norm = (im - min_) / (max_ - min_) * 255 return im_norm.astype(np.uint8) # Rotate a complex image def rot(im_tot, angle): # Get real and imaginary part im_real = im_tot[:,:,0] im_imag = im_tot[:,:,1] # Rotate each part individually im_real_rot = ndimage.rotate(im_real, angle, reshape = False) im_imag_rot = ndimage.rotate(im_imag, angle, reshape = False) # Recreate complex image im_rot = np.dstack((im_real_rot, im_imag_rot)) return im_rot # Helper function to pad an image to a given shape def pad_to_shape(im, goal_h, goal_w): [h, w] = im.shape[:2] delta_w = goal_w - w delta_h = goal_h - h top, bottom = delta_h // 2, delta_h - (delta_h // 2) left, right = delta_w // 2, delta_w - (delta_w // 2) return cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=0) path_im = "poivrons.png" im = cv2.imread(path_im, 0) im_shape = 801 im = pad_to_shape(im, im_shape, im_shape) cv2.imwrite("starting_im.png", im) # Compute DFT of original image dft1 = cv2.dft(np.float32(im), flags=cv2.DFT_COMPLEX_OUTPUT) dft1 = np.fft.fftshift(dft1) cv2.imwrite("dft1.png", normalize_and_cvt_to_uint8(dft1[:,:,1])) # Rotate original image angle = 45 im_rot = ndimage.rotate(im, angle, reshape=False) cv2.imwrite("rotated_img.png", im_rot) # Compute DFT of rotated image dft2 = cv2.dft(np.float32(im_rot), flags=cv2.DFT_COMPLEX_OUTPUT) dft2 = np.fft.fftshift(dft2) cv2.imwrite("dft2.png", normalize_and_cvt_to_uint8(dft2[:,:,1])) # Rotate DFT of original image dft3 = rot(dft1, angle) dft3 = np.fft.fftshift(dft3) # Compute IDFT of rotated DFT cv2.imwrite("dft3.png", normalize_and_cvt_to_uint8(dft3[:,:,1])) im_back = cv2.idft(np.fft.ifftshift(dft3), flags=cv2.DFT_SCALE | cv2.DFT_REAL_OUTPUT) cv2.imwrite("back_img.png", im_back)