├── test-data ├── lena.png ├── img_orig.png ├── img_moved.png ├── lena_orig.png ├── img_test_orig.png ├── lena_rot_12.png ├── lena_rot_120.png ├── lena_rot_350.png ├── lena_rot_45.png ├── img_test_moved.png ├── test_scale_orig.png ├── test_scale_larger.png ├── test_scale_rotate.png ├── lena_scale(90)_rot30.png └── test_scale_rotate_10.png ├── README.md ├── .gitignore └── script.py /test-data/lena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/lena.png -------------------------------------------------------------------------------- /test-data/img_orig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/img_orig.png -------------------------------------------------------------------------------- /test-data/img_moved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/img_moved.png -------------------------------------------------------------------------------- /test-data/lena_orig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/lena_orig.png -------------------------------------------------------------------------------- /test-data/img_test_orig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/img_test_orig.png -------------------------------------------------------------------------------- /test-data/lena_rot_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/lena_rot_12.png -------------------------------------------------------------------------------- /test-data/lena_rot_120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/lena_rot_120.png -------------------------------------------------------------------------------- /test-data/lena_rot_350.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/lena_rot_350.png -------------------------------------------------------------------------------- /test-data/lena_rot_45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/lena_rot_45.png -------------------------------------------------------------------------------- /test-data/img_test_moved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/img_test_moved.png -------------------------------------------------------------------------------- /test-data/test_scale_orig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/test_scale_orig.png -------------------------------------------------------------------------------- /test-data/test_scale_larger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/test_scale_larger.png -------------------------------------------------------------------------------- /test-data/test_scale_rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/test_scale_rotate.png -------------------------------------------------------------------------------- /test-data/lena_scale(90)_rot30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/lena_scale(90)_rot30.png -------------------------------------------------------------------------------- /test-data/test_scale_rotate_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polakluk/fourier-mellin/HEAD/test-data/test_scale_rotate_10.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Registration using Log-polar transformation, Phase correlation (Fourier-Mellin) 2 | 3 | Implemented in Python 2.7.11. using OpenCV, Pandas and NumPy 4 | 5 | ## Used Materials 6 | 7 | This project is based on paper "An Application of Fourier-Mellin Transform in Image Registration" written by Xiaoxin Guo, Zhiwen Xu, Yinan Lu, Yunjie Pang. 8 | 9 | ## How to run code 10 | 11 | In command-line, use command: 12 | ``` 13 | python script.py {original_image} {image_we_want_to_detect} 14 | ``` 15 | Example 16 | ``` 17 | python .\script.py .\test-data\lena_orig.png '.\test-data\lena_scale(90)_rot30.png' 18 | ``` 19 | 20 | ## Noise 21 | 22 | The script allows you to add two kinds of noise - Gaussian Noise and Salt & Pepper. The flag for turning one of the noises is at line 16 23 | ``` 24 | noiseMode = "none" # "gaussian", "s&p", "none" 25 | ``` 26 | 27 | At line 17, you can specify parameters for each type of noise: 28 | ``` 29 | noiseIntensity = {'sigma' : 2, 'mean' : 0, 'whiteThreshold' : 0.01, 'blackThreshold' : 0.99} 30 | ``` 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import math 3 | import cv2 4 | import numpy as np 5 | from matplotlib import pyplot as plt 6 | import scipy.ndimage.interpolation as ndii 7 | import pprint 8 | import time 9 | 10 | # global constants 11 | RE_IDX = 0 12 | IM_IDX = 1 13 | ROWS_AXIS = 0 14 | COLS_AXIS = 1 15 | polarMode = "spline" 16 | noiseMode = "none" # "gaussian", "s&p", "none" 17 | noiseIntensity = {'sigma' : 2, 'mean' : 0, 'whiteThreshold' : 0.01, 'blackThreshold' : 0.99} 18 | resultsComparation = False 19 | 20 | # this function will calculates parameters for log polar transformation 21 | # (center of transformation, angle step and log base) 22 | def computeLogPolarParameters(img): 23 | # Step 1 - Get center of the transformation 24 | centerTrans = [math.floor((img.shape[ROWS_AXIS] + 1) / 2), math.floor((img.shape[COLS_AXIS] + 1 ) / 2)] 25 | # Step 2 - Estimate dimensions of final image after discrete log-polar transformation 26 | # num of columns = log(radius) 27 | # num of rows = angle in radius (0, 2pi) 28 | maxDiff = np.maximum(centerTrans, np.asarray(img.shape) - centerTrans) 29 | maxDistance = ((maxDiff[0] ** 2 + maxDiff[1] ** 2 ) ** 0.5) 30 | dimsLogPolar = [0,0] 31 | dimsLogPolar[COLS_AXIS] = img.shape[COLS_AXIS] 32 | dimsLogPolar[ROWS_AXIS] = img.shape[ROWS_AXIS] 33 | # Step 2.1 - Estimate log base 34 | logBase = math.exp(math.log(maxDistance) / dimsLogPolar[COLS_AXIS]) 35 | # Step 3 - Calculate step for angle in log polar coordinates 36 | angleStep = ( 1.0 * math.pi ) / dimsLogPolar[ROWS_AXIS] 37 | return (centerTrans, angleStep, logBase) 38 | 39 | # converts image to its log polar representation 40 | # returns the log polar representation and log base 41 | def convertToLogPolar(img, centerTrans, angleStep, logBase, mode = "nearest"): 42 | if mode == "nearest": 43 | # Step 1 - Initialize transformed image 44 | transformedImage = np.zeros(img.shape, dtype = img.dtype) 45 | # Step 2 - Apply reverse log polar transformation 46 | for radius in range(img.shape[COLS_AXIS]): # start with radius, because calculating exponential power is time consuming 47 | actRadius = logBase ** radius 48 | for angle in range(img.shape[ROWS_AXIS]): 49 | anglePi = angle * angleStep 50 | # calculate euclidian coordinates (source: https://en.wikipedia.org/wiki/Log-polar_coordinates) 51 | row = int(centerTrans[ROWS_AXIS] + actRadius * math.sin(anglePi)) 52 | col = int(centerTrans[COLS_AXIS] + actRadius * math.cos(anglePi)) 53 | # copy pixel from the location to log polar image 54 | if 0 <= row < img.shape[ROWS_AXIS] and 0 <= col < img.shape[COLS_AXIS]: 55 | transformedImage[angle, radius] = img[row, col] 56 | 57 | return transformedImage 58 | else: 59 | print("Base: " + str(logBase)) 60 | # create matrix with angles 61 | anglesMap = np.zeros(img.shape, dtype=np.float64) 62 | # each column has 0 in its first row and -pi in its last row 63 | anglesVector = -np.linspace(0, np.pi, img.shape[0], endpoint=False) 64 | # initialize it by columns using the same vector 65 | anglesMap.T[:] = anglesVector 66 | # create matrix with radii 67 | radiusMap = np.zeros(img.shape, dtype=np.float64) 68 | # each line contains a vector with numbers from in (0, cols) to power logBase 69 | radiusVector = np.power(logBase, np.arange(img.shape[1], dtype=np.float64)) - 1.0 70 | # initialize it by rows using the same vector 71 | radiusMap[:] = radiusVector 72 | # calculate x coordinates (source: https://en.wikipedia.org/wiki/Log-polar_coordinates) 73 | x = radiusMap * np.sin(anglesMap) + centerTrans[1] 74 | # calculate y coordinates (source: https://en.wikipedia.org/wiki/Log-polar_coordinates) 75 | y = radiusMap * np.cos(anglesMap) + centerTrans[0] 76 | # initialize final image 77 | outputImg = np.zeros(img.shape) 78 | # use spline interpolation to map pixels from original image to calculated coordinates 79 | ndii.map_coordinates(img, [x, y], output=outputImg) 80 | return outputImg 81 | 82 | 83 | # computes phase correlation and returns position of pixel with highest value (row, column) 84 | def phaseCorrelation(img_orig, img_transformed): 85 | # Step 3.1 - Initialize complex conjugates for original image and magnitudes 86 | orig_conj = np.copy(img_orig) 87 | orig_conj[:,:,IM_IDX] = -orig_conj[:,:,IM_IDX] 88 | orig_mags = cv2.magnitude(img_orig[:,:,RE_IDX],img_orig[:,:,IM_IDX]) 89 | img_trans_mags = cv2.magnitude(img_transformed[:,:,RE_IDX],img_transformed[:,:,IM_IDX]) 90 | # Step 3.2 - Do deconvolution 91 | # multiplication compex numbers ===> (x + yi) * (u + vi) = (xu - yv) + (xv + yu)i 92 | # deconvolution ( H* x G ) / |H x G| 93 | realPart = (orig_conj[:,:,RE_IDX] * img_transformed[:,:,RE_IDX] - orig_conj[:,:,IM_IDX] * img_transformed[:,:,IM_IDX]) / (orig_mags * img_trans_mags) 94 | imaginaryPart = (orig_conj[:,:,RE_IDX] * img_transformed[:,:,IM_IDX] + orig_conj[:,:,IM_IDX] * img_transformed[:,:,RE_IDX]) / ( orig_mags * img_trans_mags) 95 | result = np.dstack((realPart, imaginaryPart)) 96 | result_idft = cv2.idft(result) 97 | # Step 3.3 - Find Max value (angle and scaling factor) 98 | result_mags = cv2.magnitude(result_idft[:,:,RE_IDX],result_idft[:,:,IM_IDX]) 99 | return np.unravel_index( np.argmax(result_mags), result_mags.shape) 100 | 101 | 102 | # adds artifical noise to images 103 | def addNoiseToImage(img, noise, noiseIntensity): 104 | if noise == 's&p': 105 | print("Adding S&P Noise: wT = " + str(noiseIntensity['whiteThreshold']) + "; bT = " + str(noiseIntensity['blackThreshold'])) 106 | # generate probabilities from uniform distribution 107 | distData = np.random.uniform(0.0, 1.0, img.shape).reshape(img.shape) 108 | # set ones below whiteThreshold to white 109 | img[distData < noiseIntensity['whiteThreshold']] = 255 110 | # set ones above blackThreshold to black 111 | img[distData > noiseIntensity['blackThreshold']] = 0 112 | return img 113 | else: 114 | if noise == 'gaussian': 115 | print("Adding Gaussian noise: sigma = " + str(noiseIntensity['sigma']) + '; mean = ' + str(noiseIntensity['mean'])) 116 | return noiseIntensity['sigma'] * np.random.randn(img.shape[0], img.shape[1]) + img + noiseIntensity['mean'] 117 | else: 118 | return img 119 | 120 | # reads image, runs FFT and returns FFT image + its magnitude spectrum 121 | def readImage(img): 122 | imgData = cv2.imread(img,0) # 0 means Grayscale 123 | imgData = addNoiseToImage(imgData, noiseMode, noiseIntensity) 124 | imgFft, imgFftShifted = calculateFft(imgData) # FFT of the image 125 | imgMags = cv2.magnitude(imgFftShifted[:,:,RE_IDX],imgFftShifted[:,:,IM_IDX]) 126 | return (imgData, imgFftShifted, imgMags) 127 | 128 | 129 | # applies highpass filter and returns the image 130 | # H(col, row) = (1.0 - X(col, row)) * (2.0 - X(col, row)), row and col have to be transformed to range <-pi/2, pi/2> 131 | # X(valX, valY) = cos(pi * valX) * cos(pi * valY), both valX and valY in range <-pi/2, pi/2> 132 | def prepareHighPassFilter(img): 133 | pi2 = math.pi / 2.0 134 | # transform number of rows to <-pi/2,pi/2> range and calculate cos for each element 135 | rows = np.cos(np.linspace(-pi2, pi2, img.shape[0])) 136 | # transform number of cols to <-pi/2,pi/2> range and calculate cos for each element 137 | cols = np.cos(np.linspace(-pi2, pi2, img.shape[1])) 138 | # creates matrix the whole image 139 | x = np.outer( rows, cols) 140 | return (1.0 - x) * (2.0 - x) 141 | 142 | 143 | # Central point for running FFT 144 | def calculateFft(img): 145 | imgTmp = np.float32(img) 146 | # FFT of the image 147 | imgFft = cv2.dft(imgTmp,flags = cv2.DFT_COMPLEX_OUTPUT) 148 | # the FFT shift is needed in order to center the results 149 | imgFftShifted = np.fft.fftshift(imgFft) 150 | return (imgFft, imgFftShifted) 151 | 152 | # main script 153 | def main(argv): 154 | timeStart = time.time() 155 | # Step 1 - Apply FFT on both images and get their magnitude spectrums 156 | # image (we are looking for), lets call it original 157 | imgOriginal, imgOriginalFft, imgOriginalMags = readImage(argv[1]) 158 | # image (we are searching in), lets call it transformed 159 | imgTransformed, imgTransformedFft, imgTransformedMags = readImage(argv[2]) 160 | 161 | # Step 2 - Apply highpass filter on their magnitude spectrums 162 | highPassFilter = prepareHighPassFilter(imgOriginalMags) 163 | imgOriginalMagsFilter = imgOriginalMags * highPassFilter 164 | imgTransformedMagsFilter = imgTransformedMags * highPassFilter 165 | 166 | # Step 3 - Convert magnitudes both images to log-polar coordinates 167 | # Step 3.1 - Precompute parameters (both images have the same dimensions) 168 | centerTrans, angleStep, logBase = computeLogPolarParameters(imgOriginalMagsFilter) 169 | imgOriginalLogPolar = convertToLogPolar(imgOriginalMagsFilter, centerTrans, angleStep, logBase, polarMode) 170 | imgTransformedLogPolar = convertToLogPolar(imgTransformedMagsFilter, centerTrans, angleStep, logBase, polarMode) 171 | 172 | # Step 3.1 - Apply FFT on magnitude spectrums in log polar coordinates (in this case, not using FFT shift as it leads to computing [180-angle] results) 173 | imgOriginalLogPolarComplex = cv2.dft(np.float32(imgOriginalLogPolar),flags = cv2.DFT_COMPLEX_OUTPUT) 174 | imgTransformedLogPolarComplex = cv2.dft(np.float32(imgTransformedLogPolar),flags = cv2.DFT_COMPLEX_OUTPUT) 175 | 176 | # Step 4 - Apply phase corelation on both images (FFT applied on log polar images) to retrieve rotation (angle) and scale factor 177 | angle, scale = phaseCorrelation(imgOriginalLogPolarComplex, imgTransformedLogPolarComplex) 178 | # Step 4.1 Convert to degrees based on formula in paper (26) and adjust it to (-pi/2, pi/2) range 179 | angleDeg = -(float(angle) * 180.0 ) / imgOriginalLogPolarComplex.shape[0] 180 | if angleDeg < - 45: 181 | angleDeg += 180 182 | else: 183 | if angleDeg > 90.0: 184 | angleDeg -= 180 185 | 186 | # Step 4.2 Calculate scale factor based on formula in paper (25) 187 | scaleFactor = logBase ** scale 188 | 189 | # Step 5 - Apply rotation and scaling on transformed image 190 | transformMatrix = cv2.getRotationMatrix2D((centerTrans[0], centerTrans[1]), angleDeg, scaleFactor) 191 | imgTransformedNew = cv2.warpAffine(imgTransformed, transformMatrix, (imgTransformed.shape[1], imgTransformed.shape[0]) ) 192 | 193 | # Step 6 - Apply phase corelation on both images to retrieve translation 194 | # Step 6.1 Apply FFT to newly created transformed image 195 | imgTransformedNewFft, imgTransformedNewftShifted = calculateFft(imgTransformedNew) 196 | # Step 6.2 - Use phase corelation to get translation coordinates 197 | y, x = phaseCorrelation(imgTransformedNewftShifted, imgOriginalFft) 198 | # Step 6.3 Apply translation on the final image 199 | if x > imgOriginal.shape[0] // 2: 200 | x -= imgOriginal.shape[0] 201 | if y > imgOriginal.shape[1] // 2: 202 | y -= imgOriginal.shape[1] 203 | 204 | translationMatrix = np.float32([[1,0,-x],[0,1,-y]]) 205 | imgFinal = cv2.warpAffine(imgTransformedNew, translationMatrix, (imgTransformed.shape[1], imgTransformed.shape[0])) 206 | timeEnd = time.time() 207 | 208 | # Step 7 - Return final results (rotation, scale factor, translation) 209 | print("Angle = " + str(angleDeg) + " Deg") 210 | print("Scale = " + str(scaleFactor)) 211 | print("Translation") 212 | print("X = " + str(-x)) 213 | print("Y = " + str(-y)) 214 | print("Time = " + str(timeEnd - timeStart)) 215 | 216 | if resultsComparation: 217 | plt.subplot(221),plt.imshow(imgOriginal, cmap = 'gray') 218 | plt.subplot(222),plt.imshow(imgTransformed, cmap = 'gray') 219 | plt.subplot(223),plt.imshow(imgOriginal - imgFinal, cmap = 'bwr') 220 | plt.subplot(224),plt.imshow(imgFinal, cmap = 'gray') 221 | plt.show() 222 | else: 223 | plt.subplot(521),plt.imshow(imgOriginal, cmap = 'gray') 224 | plt.subplot(522),plt.imshow(imgTransformed, cmap = 'gray') 225 | plt.subplot(523),plt.imshow(imgOriginalMagsFilter, cmap = 'gray') 226 | plt.subplot(524),plt.imshow(imgTransformedMagsFilter, cmap = 'gray') 227 | plt.subplot(525),plt.imshow(imgOriginalLogPolar, cmap = 'gray') 228 | plt.subplot(526),plt.imshow(imgTransformedLogPolar, cmap = 'gray') 229 | plt.subplot(527),plt.imshow(imgTransformedNew, cmap = 'gray') 230 | plt.subplot(528),plt.imshow(imgOriginal - imgFinal, cmap = 'bwr') 231 | plt.subplot(529),plt.imshow(imgFinal, cmap = 'gray') 232 | plt.show() 233 | 234 | 235 | # script.py {image, we are looking for} {image, we are searching in} 236 | if __name__ == '__main__': 237 | sys.exit(main(sys.argv)) 238 | --------------------------------------------------------------------------------