├── UNLICENSE.txt ├── README.md ├── wahab ├── gabor └── utils.py /UNLICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fingerprint Image Enhancement 2 | Implementation of fingerprint image enhancement filters. This repo contains two different filtering tools (`gabor` and `wahab`) and a library file for common utility functions (`utils.py`). Below is a general description of all three files. For more information, please read the source code. 3 | 4 | ## [`wahab`](wahab) 5 | An executable script file that contains the code for applying the Wahab filter. Based on (and named after) [WCT98](#wct98). It consists mainly of a function called `wahabKernel()` that creates a directional kernel for a given orientation, and a function called `wahabFilter()` that divides the image into cells, and convolves each cell with a directional kernel corresponding to the average orientation of the cell. 6 | 7 | ## [`gabor`](gabor) 8 | An executable script file that contains the code for applying the Gabor filter. Based on [HWJ98](#hwj98). It contains the gaborKernel() function that creates a Gabor kernel for a given orientation and frequency. It contains two functions, `gaborFilter()` and `gaborFilterSubdivide()` processes the image by cell iteration or by area subdivision, respectively. They both 9 | divide the image into smaller chunks, and convolve each chunk with a Gabor kernel corresponding to the average orientation in the chunk. 10 | 11 | ## [`utils.py`](utils.py) 12 | A Python file that is not meant to be invoked directly, but imported into other scripts. It contains a number of commonly useful functions for fingerprint image enhancement. The most important functions are: 13 | 14 | ### `convolve()` 15 | A custom convolution function that allows us to convolve a whole image, or just a sub-area of an image. 16 | 17 | ### `findMask()` 18 | Marks areas as good or bad, depending on the standard deviation of values within the area. 19 | 20 | ### `estimateOrientations()` 21 | Creates an orientation field for an image, using a combination of the methods from [HWJ98](#hwj98) and [SMM94](#smm94). 22 | 23 | ### `estimateFrequencies()` 24 | Createsafrequencyfieldforanimage,usingthemethod from [HWJ98]. 25 | 26 | # References 27 | 28 | ## SMM94 29 | Sherlock, BG; Monro, DM; Millard, K: Fingerprint enhancement by directional Fourier filtering. IEE Proceedings-Vision, Image and Signal Processing, 141(2):87–94, 1994. 30 | 31 | ## HWJ98 32 | Hong, Lin; Wan, Yifei; Jain, Anil: Fingerprint image enhancement: Algorithm and performance evaluation. IEEE transactions on pattern analysis and machine intelligence, 20(8):777–789, 1998. 33 | 34 | ## WCT98 35 | Wahab, A; Chin, SH; Tan, EC: Novel approach to automated fingerprint recognition. IEE Proceedings-Vision, Image and Signal Processing, 145(3):160–166, 1998. 36 | -------------------------------------------------------------------------------- /wahab: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from optparse import OptionParser 6 | from skimage import draw 7 | import imageio 8 | 9 | import utils 10 | 11 | parser = OptionParser(usage="%prog [options] sourceimage [destinationimage]") 12 | 13 | parser.add_option("-i", dest="images", default=0, action="count", 14 | help="Show intermediate images.") 15 | 16 | parser.add_option("-d", "--dry-run", dest="dryrun", default=False, action="store_true", 17 | help="Do not save the result.") 18 | 19 | parser.add_option("-b", "--no-binarization", dest="binarize", default=True, action="store_false", 20 | help="Use this option to disable the final binarization step") 21 | 22 | (options, args) = parser.parse_args() 23 | 24 | if len(args) == 0 or len(args) > 2: 25 | parser.print_help() 26 | exit(1) 27 | 28 | sourceImage = args[0] 29 | if len(args) == 1: 30 | destinationImage = args[0] 31 | else: 32 | destinationImage = args[1] 33 | 34 | def wahabKernel(size, angle): 35 | y = int(np.sin(angle) * size) 36 | x = int(np.cos(angle) * size) 37 | 38 | kernel = np.zeros((np.abs(y) + 1, np.abs(x) + 1)) 39 | 40 | if y < 0: 41 | rr, cc = draw.line(0, 0, y, x) 42 | else: 43 | rr, cc = draw.line(-y, 0, 0, x) 44 | 45 | kernel[rr, cc] = 1.0 46 | return kernel 47 | 48 | 49 | def wahabFilter(image, orientations, w=8): 50 | result = np.empty(image.shape) 51 | 52 | height, width = image.shape 53 | for y in range(0, height - w, w): 54 | for x in range(0, width - w, w): 55 | orientation = orientations[y+w//2, x+w//2] 56 | kernel = wahabKernel(16, orientation) 57 | result[y:y+w, x:x+w] = utils.convolve(image, kernel, (y, x), (w, w)) 58 | result[y:y+w, x:x+w] /= np.sum(kernel) 59 | 60 | return result 61 | 62 | 63 | if __name__ == '__main__': 64 | np.set_printoptions( 65 | threshold=np.inf, 66 | precision=4, 67 | suppress=True) 68 | 69 | print("Reading image") 70 | image = imageio.v2.imread(sourceImage).astype("float64") 71 | if options.images > 0: 72 | utils.showImage(image, "original", vmax=255.0) 73 | 74 | print("Normalizing") 75 | image = utils.normalize(image) 76 | if options.images > 1: 77 | utils.showImage(image, "normalized") 78 | 79 | print("Finding mask") 80 | mask = utils.findMask(image) 81 | if options.images > 1: 82 | utils.showImage(mask, "mask") 83 | 84 | print("Applying local normalization") 85 | image = np.where(mask == 1.0, utils.localNormalize(image), image) 86 | if options.images > 1: 87 | utils.showImage(image, "locally normalized") 88 | 89 | print("Estimating orientations") 90 | orientations = np.where(mask == 1.0, utils.estimateOrientations(image, interpolate=False), -1.0) 91 | if options.images > 0: 92 | utils.showOrientations(image, orientations, "orientations", 8) 93 | 94 | print("Filtering") 95 | image = np.where(mask == 1.0, wahabFilter(image, orientations), 1.0) 96 | if options.images > 0: 97 | utils.showImage(image, "filtered") 98 | 99 | if options.binarize: 100 | print("Binarizing") 101 | image = np.where(mask == 1.0, utils.binarize(image), 1.0) 102 | if options.images > 0: 103 | utils.showImage(image, "binarized") 104 | 105 | if options.images > 0: 106 | plt.show() 107 | 108 | if not options.dryrun: 109 | imageio.v2.imwrite(destinationImage, image) 110 | -------------------------------------------------------------------------------- /gabor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from optparse import OptionParser 6 | import imageio 7 | 8 | import utils 9 | 10 | parser = OptionParser(usage="%prog [options] sourceimage [destinationimage]") 11 | 12 | parser.add_option("-i", dest="images", default=0, action="count", 13 | help="Show intermediate images.") 14 | 15 | parser.add_option("-s", "--subdivide", dest="subdivide", 16 | default=False, action="store_true", 17 | help="Iterate the image by subdividing areas.") 18 | 19 | parser.add_option("-d", "--dry-run", dest="dryrun", default=False, action="store_true", 20 | help="Do not save the result.") 21 | 22 | parser.add_option("-b", "--no-binarization", dest="binarize", default=True, action="store_false", 23 | help="Use this option to disable the final binarization step") 24 | 25 | options, args = parser.parse_args() 26 | 27 | if len(args) == 0 or len(args) > 2: 28 | parser.print_help() 29 | exit(1) 30 | 31 | sourceImage = args[0] 32 | if len(args) == 1: 33 | destinationImage = args[0] 34 | else: 35 | destinationImage = args[1] 36 | 37 | def gaborKernel(size, angle, frequency): 38 | """ 39 | Create a Gabor kernel given a size, angle and frequency. 40 | 41 | Code is taken from https://github.com/rtshadow/biometrics.git 42 | """ 43 | 44 | angle += np.pi * 0.5 45 | cos = np.cos(angle) 46 | sin = -np.sin(angle) 47 | 48 | yangle = lambda x, y: x * cos + y * sin 49 | xangle = lambda x, y: -x * sin + y * cos 50 | 51 | xsigma = ysigma = 4 52 | 53 | return utils.kernelFromFunction(size, lambda x, y: 54 | np.exp(-( 55 | (xangle(x, y) ** 2) / (xsigma ** 2) + 56 | (yangle(x, y) ** 2) / (ysigma ** 2)) / 2) * 57 | np.cos(2 * np.pi * frequency * xangle(x, y))) 58 | 59 | def gaborFilter(image, orientations, frequencies, w=32): 60 | result = np.empty(image.shape) 61 | 62 | height, width = image.shape 63 | for y in range(0, height - w, w): 64 | for x in range(0, width - w, w): 65 | orientation = orientations[y+w//2, x+w//2] 66 | frequency = utils.averageFrequency(frequencies[y:y+w, x:x+w]) 67 | 68 | if frequency < 0.0: 69 | result[y:y+w, x:x+w] = image[y:y+w, x:x+w] 70 | continue 71 | 72 | kernel = gaborKernel(16, orientation, frequency) 73 | result[y:y+w, x:x+w] = utils.convolve(image, kernel, (y, x), (w, w)) 74 | 75 | return utils.normalize(result) 76 | 77 | 78 | def gaborFilterSubdivide(image, orientations, frequencies, rect=None): 79 | if rect: 80 | y, x, h, w = rect 81 | else: 82 | y, x = 0, 0 83 | h, w = image.shape 84 | 85 | result = np.empty((h, w)) 86 | 87 | orientation, deviation = utils.averageOrientation( 88 | orientations[y:y+h, x:x+w], deviation=True) 89 | 90 | if (deviation < 0.2 and h < 50 and w < 50) or h < 6 or w < 6: 91 | #print(deviation) 92 | #print(rect) 93 | 94 | frequency = utils.averageFrequency(frequencies[y:y+h, x:x+w]) 95 | 96 | if frequency < 0.0: 97 | result = image[y:y+h, x:x+w] 98 | else: 99 | kernel = gaborKernel(16, orientation, frequency) 100 | result = utils.convolve(image, kernel, (y, x), (h, w)) 101 | 102 | else: 103 | if h > w: 104 | hh = h // 2 105 | 106 | result[0:hh, 0:w] = \ 107 | gaborFilterSubdivide(image, orientations, frequencies, (y, x, hh, w)) 108 | 109 | result[hh:h, 0:w] = \ 110 | gaborFilterSubdivide(image, orientations, frequencies, (y + hh, x, h - hh, w)) 111 | else: 112 | hw = w // 2 113 | 114 | result[0:h, 0:hw] = \ 115 | gaborFilterSubdivide(image, orientations, frequencies, (y, x, h, hw)) 116 | 117 | result[0:h, hw:w] = \ 118 | gaborFilterSubdivide(image, orientations, frequencies, (y, x + hw, h, w - hw)) 119 | 120 | 121 | 122 | if w > 20 and h > 20: 123 | result = utils.normalize(result) 124 | 125 | return result 126 | 127 | 128 | if __name__ == '__main__': 129 | np.set_printoptions( 130 | threshold=np.inf, 131 | precision=4, 132 | suppress=True) 133 | 134 | print("Reading image") 135 | image = imageio.v2.imread(sourceImage).astype("float64") 136 | if options.images > 0: 137 | utils.showImage(image, "original", vmax=255.0) 138 | 139 | print("Normalizing") 140 | image = utils.normalize(image) 141 | if options.images > 1: 142 | utils.showImage(image, "normalized") 143 | 144 | print("Finding mask") 145 | mask = utils.findMask(image) 146 | if options.images > 1: 147 | utils.showImage(mask, "mask") 148 | 149 | print("Applying local normalization") 150 | image = np.where(mask == 1.0, utils.localNormalize(image), image) 151 | if options.images > 1: 152 | utils.showImage(image, "locally normalized") 153 | 154 | print("Estimating orientations") 155 | orientations = np.where(mask == 1.0, utils.estimateOrientations(image), -1.0) 156 | if options.images > 0: 157 | utils.showOrientations(image, orientations, "orientations", 8) 158 | 159 | print("Estimating frequencies") 160 | frequencies = np.where(mask == 1.0, utils.estimateFrequencies(image, orientations), -1.0) 161 | if options.images > 1: 162 | utils.showImage(utils.normalize(frequencies), "frequencies") 163 | 164 | print("Filtering") 165 | if options.subdivide: 166 | image = utils.normalize(gaborFilterSubdivide(image, orientations, frequencies)) 167 | else: 168 | image = gaborFilter(image, orientations, frequencies) 169 | image = np.where(mask == 1.0, image, 1.0) 170 | if options.images > 0: 171 | utils.showImage(image, "gabor") 172 | 173 | if options.binarize: 174 | print("Binarizing") 175 | image = np.where(mask == 1.0, utils.binarize(image), 1.0) 176 | if options.images > 0: 177 | utils.showImage(image, "binarized") 178 | 179 | if options.images > 0: 180 | plt.show() 181 | 182 | if not options.dryrun: 183 | imageio.v2.imwrite(destinationImage, image) 184 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import scipy.ndimage as ndimage 4 | import scipy.signal as signal 5 | 6 | 7 | def showImage(image, label, vmin=0.0, vmax=1.0): 8 | plt.figure().suptitle(label) 9 | plt.imshow(image, cmap="gray", vmin=vmin, vmax=vmax) 10 | 11 | 12 | def showOrientations(image, orientations, label, w=32, vmin=0.0, vmax=1.0): 13 | showImage(image, label) 14 | height, width = image.shape 15 | for y in range(0, height, w): 16 | for x in range(0, width, w): 17 | if np.any(orientations[y : y + w, x : x + w] == -1.0): 18 | continue 19 | 20 | cy = (y + min(y + w, height)) // 2 21 | cx = (x + min(x + w, width)) // 2 22 | 23 | orientation = orientations[y + w // 2, x + w // 2] 24 | 25 | plt.plot( 26 | [ 27 | cx - w * 0.5 * np.cos(orientation), 28 | cx + w * 0.5 * np.cos(orientation), 29 | ], 30 | [ 31 | cy - w * 0.5 * np.sin(orientation), 32 | cy + w * 0.5 * np.sin(orientation), 33 | ], 34 | "r-", 35 | lw=1.0, 36 | ) 37 | 38 | 39 | def drawImage(source, destination, y, x): 40 | height, width = source.shape 41 | height = min(height, destination.shape[0] - y) 42 | width = min(width, destination.shape[1] - x) 43 | destination[y : y + height, x : x + width] = source[0:height, 0:width] 44 | 45 | 46 | def normalize(image): 47 | image = np.copy(image) 48 | image -= np.min(image) 49 | m = np.max(image) 50 | if m > 0.0: 51 | image *= 1.0 / m 52 | return image 53 | 54 | 55 | def localNormalize(image, w=32): 56 | image = np.copy(image) 57 | height, width = image.shape 58 | for y in range(0, height, w): 59 | for x in range(0, width, w): 60 | image[y : y + w, x : x + w] = normalize(image[y : y + w, x : x + w]) 61 | 62 | return image 63 | 64 | 65 | def binarize(image, w=32): 66 | """ 67 | Perform a local binarization of an image. For each cell of the given size 68 | w, the average value is calculated. Every pixel that is below this value, 69 | is set to 0, every pixel above, is set to 1. 70 | 71 | :param image: The image to be binarized. 72 | :param w: The size of the cell. 73 | :returns: The binarized image. 74 | """ 75 | 76 | image = np.copy(image) 77 | height, width = image.shape 78 | for y in range(0, height, w): 79 | for x in range(0, width, w): 80 | block = image[y : y + w, x : x + w] 81 | threshold = np.average(block) 82 | image[y : y + w, x : x + w] = np.where(block >= threshold, 1.0, 0.0) 83 | 84 | return image 85 | 86 | 87 | def kernelFromFunction(size, f): 88 | """ 89 | Creates a kernel of the given size, populated with values obtained by 90 | calling the given function. 91 | 92 | :param size: The desired size of the kernel. 93 | :param f: The function. 94 | :returns: The created kernel. 95 | """ 96 | 97 | kernel = np.empty((size, size)) 98 | for i in range(0, size): 99 | for j in range(0, size): 100 | kernel[i, j] = f(i - size / 2, j - size / 2) 101 | 102 | return kernel 103 | 104 | 105 | def sobelKernelX(): 106 | """ 107 | Creates a horizontal Sobel kernel. 108 | """ 109 | return np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) 110 | 111 | 112 | def sobelKernelY(): 113 | """ 114 | Creates a vertical Sobel kernel. 115 | """ 116 | return np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]) 117 | 118 | 119 | def convolve(image, kernel, origin=None, shape=None, pad=True): 120 | """ 121 | Apply a kernel to an image or to a part of an image. 122 | 123 | :param image: The source image. 124 | :param kernel: The kernel (an ndarray of black and white, or grayvalues). 125 | :param origin: The origin of the part of the image to be convolved. 126 | Defaults to (0, 0). 127 | :param shape: The shape of the part of the image that is to be convolved. 128 | Defaults to the shape of the image. 129 | :param pad: Whether the image should be padded before applying the 130 | kernel. Passing False here will cause indexing errors if 131 | the kernel is applied at the edge of the image. 132 | :returns: The resulting image. 133 | """ 134 | if not origin: 135 | origin = (0, 0) 136 | 137 | if not shape: 138 | shape = (image.shape[0] - origin[0], image.shape[1] - origin[1]) 139 | 140 | result = np.empty(shape) 141 | 142 | if callable(kernel): 143 | k = kernel(0, 0) 144 | else: 145 | k = kernel 146 | 147 | kernelOrigin = (-k.shape[0] // 2, -k.shape[1] // 2) 148 | kernelShape = k.shape 149 | 150 | topPadding = 0 151 | leftPadding = 0 152 | 153 | if pad: 154 | topPadding = max(0, -(origin[0] + kernelOrigin[0])) 155 | leftPadding = max(0, -(origin[1] + kernelOrigin[1])) 156 | bottomPadding = max( 157 | 0, 158 | (origin[0] + shape[0] + kernelOrigin[0] + kernelShape[0]) - image.shape[0], 159 | ) 160 | rightPadding = max( 161 | 0, 162 | (origin[1] + shape[1] + kernelOrigin[1] + kernelShape[1]) - image.shape[1], 163 | ) 164 | 165 | padding = (topPadding, bottomPadding), (leftPadding, rightPadding) 166 | 167 | if np.max(padding) > 0.0: 168 | image = np.pad(image, padding, mode="edge") 169 | 170 | for y in range(shape[0]): 171 | for x in range(shape[1]): 172 | iy = topPadding + origin[0] + y + kernelOrigin[0] 173 | ix = leftPadding + origin[1] + x + kernelOrigin[1] 174 | 175 | block = image[iy : iy + kernelShape[0], ix : ix + kernelShape[1]] 176 | if callable(kernel): 177 | result[y, x] = np.sum(block * kernel(y, x)) 178 | else: 179 | result[y, x] = np.sum(block * kernel) 180 | 181 | return result 182 | 183 | 184 | def findMask(image, threshold=0.1, w=32): 185 | """ 186 | Create a mask image consisting of only 0's and 1's. The areas containing 187 | 1's represent the areas that look interesting to us, meaning that they 188 | contain a good variety of color values. 189 | """ 190 | 191 | mask = np.empty(image.shape) 192 | height, width = image.shape 193 | for y in range(0, height, w): 194 | for x in range(0, width, w): 195 | block = image[y : y + w, x : x + w] 196 | standardDeviation = np.std(block) 197 | if standardDeviation < threshold: 198 | mask[y : y + w, x : x + w] = 0.0 199 | elif block.shape != (w, w): 200 | mask[y : y + w, x : x + w] = 0.0 201 | else: 202 | mask[y : y + w, x : x + w] = 1.0 203 | 204 | return mask 205 | 206 | 207 | def averageOrientation(orientations, weights=None, deviation=False): 208 | """ 209 | Calculate the average orientation in an orientation field. 210 | """ 211 | 212 | orientations = np.asarray(orientations).flatten() 213 | o = orientations[0] 214 | 215 | aligned = np.where( 216 | np.absolute(orientations - o) > np.pi * 0.5, 217 | np.where(orientations > o, orientations - np.pi, orientations + np.pi), 218 | orientations, 219 | ) 220 | if deviation: 221 | return np.average(aligned, weights=weights) % np.pi, np.std(aligned) 222 | else: 223 | return np.average(aligned, weights=weights) % np.pi 224 | 225 | 226 | def averageFrequency(frequencies): 227 | """ 228 | Calculate the average frequency in a frequency field. 229 | """ 230 | 231 | frequencies = frequencies[np.where(frequencies >= 0.0)] 232 | if frequencies.size == 0: 233 | return -1 234 | return np.average(frequencies) 235 | 236 | 237 | def rotateAndCrop(image, angle): 238 | """ 239 | Rotate an image and crop the result so that there are no black borders. 240 | 241 | This implementation is based on this stackoverflow answer: 242 | 243 | http://stackoverflow.com/a/16778797 244 | 245 | :param image: The image to rotate. 246 | :param angle: The angle in gradians. 247 | :returns: The rotated and cropped image. 248 | """ 249 | 250 | h, w = image.shape 251 | 252 | width_is_longer = w >= h 253 | side_long, side_short = (w, h) if width_is_longer else (h, w) 254 | 255 | # since the solutions for angle, -angle and 180-angle are all the same, 256 | # if suffices to look at the first quadrant and the absolute values of sin,cos: 257 | sin_a, cos_a = abs(np.sin(angle)), abs(np.cos(angle)) 258 | if side_short <= 2.0 * sin_a * cos_a * side_long: 259 | # half constrained case: two crop corners touch the longer side, 260 | # the other two corners are on the mid-line parallel to the longer line 261 | x = 0.5 * side_short 262 | wr, hr = (x / sin_a, x / cos_a) if width_is_longer else (x / cos_a, x / sin_a) 263 | else: 264 | # fully constrained case: crop touches all 4 sides 265 | cos_2a = cos_a * cos_a - sin_a * sin_a 266 | wr, hr = (w * cos_a - h * sin_a) / cos_2a, (h * cos_a - w * sin_a) / cos_2a 267 | 268 | image = ndimage.interpolation.rotate(image, np.degrees(angle), reshape=False) 269 | 270 | hr, wr = int(hr), int(wr) 271 | y, x = (h - hr) // 2, (w - wr) // 2 272 | 273 | return image[y : y + hr, x : x + wr] 274 | 275 | 276 | def estimateOrientations(image, w=16, interpolate=True): 277 | """ 278 | Estimate orientations of lines or ridges in an image. 279 | 280 | This is more or less an implementation of of the algorithm in Chapter 2.4 in 281 | the paper: 282 | 283 | Fingerprint image enhancement: Algorithm and performance evaluation 284 | Hong, L., Wan, Y. & Jain, A. (1998) 285 | 286 | In addition to calculating the orientation in each cell, we create a 287 | continuous orientation field, the same shape as the input image, by 288 | interpolating the orientation values between the cell centers, as 289 | suggested in this paper: 290 | 291 | Novel approach to automated fingerprint recognition 292 | Wahab, A., Chin, S. & Tan, E. (1998) 293 | 294 | :param image: The image to estimate orientations in. 295 | :param w: The block size. 296 | :returns: An ndarray the same shape as the image, filled with orientation 297 | angles in radians. 298 | """ 299 | 300 | height, width = image.shape 301 | 302 | # First we smooth the whole image with a Gaussian filter, to make the 303 | # individual pixel gradients less spurious. 304 | image = ndimage.filters.gaussian_filter(image, 2.0) 305 | 306 | # Compute the gradients G_x and G_y at each pixel 307 | G_x = convolve(image, sobelKernelX()) 308 | G_y = convolve(image, sobelKernelY()) 309 | 310 | # Estimate the local orientation of each block 311 | yblocks, xblocks = height // w, width // w 312 | O = np.empty((yblocks, xblocks)) 313 | for j in range(yblocks): 314 | for i in range(xblocks): 315 | V_y, V_x = 0, 0 316 | for v in range(w): 317 | for u in range(w): 318 | V_x += 2 * G_x[j * w + v, i * w + u] * G_y[j * w + v, i * w + u] 319 | V_y += ( 320 | G_x[j * w + v, i * w + u] ** 2 - G_y[j * w + v, i * w + u] ** 2 321 | ) 322 | 323 | O[j, i] = np.arctan2(V_x, V_y) * 0.5 324 | 325 | # Rotate the orientations so that they point along the ridges, and wrap 326 | # them into only half of the circle (all should be less than 180 degrees). 327 | O = (O + np.pi * 0.5) % np.pi 328 | 329 | # Smooth the orientation field 330 | orientations = np.full(image.shape, -1.0) 331 | O_p = np.empty(O.shape) 332 | O = np.pad(O, 2, mode="edge") 333 | for y in range(yblocks): 334 | for x in range(xblocks): 335 | surrounding = O[y : y + 5, x : x + 5] 336 | orientation, deviation = averageOrientation(surrounding, deviation=True) 337 | if deviation > 0.5: 338 | orientation = O[y + 2, x + 2] 339 | O_p[y, x] = orientation 340 | O = O_p 341 | 342 | # Make an orientation field the same shape as the input image, and fill it 343 | # with values interpolated from the preliminary orientation field. 344 | # 345 | # BUG: This is currently quite slow. It should be possible to implement 346 | # this more efficiently. 347 | orientations = np.full(image.shape, -1.0) 348 | if interpolate: 349 | hw = w // 2 350 | for y in range(yblocks - 1): 351 | for x in range(xblocks - 1): 352 | for iy in range(w): 353 | for ix in range(w): 354 | orientations[ 355 | y * w + hw + iy, x * w + hw + ix 356 | ] = averageOrientation( 357 | [O[y, x], O[y + 1, x], O[y, x + 1], O[y + 1, x + 1]], 358 | [iy + ix, w - iy + ix, iy + w - ix, w - iy + w - ix], 359 | ) 360 | else: 361 | for y in range(yblocks): 362 | for x in range(xblocks): 363 | orientations[y * w : (y + 1) * w, x * w : (x + 1) * w] = O[y, x] 364 | 365 | return orientations 366 | 367 | 368 | def estimateFrequencies(image, orientations, w=32): 369 | """ 370 | Estimate ridge or line frequencies in an image, given an orientation field. 371 | 372 | This is more or less an implementation of of the algorithm in Chapter 2.5 in 373 | the paper: 374 | 375 | Fingerprint image enhancement: Algorithm and performance evaluation 376 | Hong, L., Wan, Y. & Jain, A. (1998) 377 | 378 | :param image: The image to estimate orientations in. 379 | :param orientations: An orientation field such as one returned from the 380 | estimateOrientations() function. 381 | :param w: The block size. 382 | :returns: An ndarray the same shape as the image, filled with frequencies. 383 | """ 384 | rotations = np.zeros(image.shape) 385 | 386 | height, width = image.shape 387 | yblocks, xblocks = height // w, width // w 388 | F = np.empty((yblocks, xblocks)) 389 | for y in range(yblocks): 390 | for x in range(xblocks): 391 | orientation = orientations[y * w + w // 2, x * w + w // 2] 392 | 393 | block = image[y * w : (y + 1) * w, x * w : (x + 1) * w] 394 | block = rotateAndCrop(block, np.pi * 0.5 + orientation) 395 | if block.size == 0: 396 | F[y, x] = -1 397 | continue 398 | 399 | drawImage(block, rotations, y * w, x * w) 400 | 401 | columns = np.sum(block, (0,)) 402 | columns = normalize(columns) 403 | peaks = signal.find_peaks_cwt(columns, np.array([3])) 404 | if len(peaks) < 2: 405 | F[y, x] = -1 406 | else: 407 | f = (peaks[-1] - peaks[0]) / (len(peaks) - 1) 408 | if f < 5 or f > 15: 409 | F[y, x] = -1 410 | else: 411 | F[y, x] = 1 / f 412 | 413 | # showImage(rotations, "rotations") 414 | 415 | frequencies = np.full(image.shape, -1.0) 416 | F = np.pad(F, 1, mode="edge") 417 | for y in range(yblocks): 418 | for x in range(xblocks): 419 | surrounding = F[y : y + 3, x : x + 3] 420 | surrounding = surrounding[np.where(surrounding >= 0.0)] 421 | if surrounding.size == 0: 422 | frequencies[y * w : (y + 1) * w, x * w : (x + 1) * w] = -1 423 | else: 424 | frequencies[y * w : (y + 1) * w, x * w : (x + 1) * w] = np.median( 425 | surrounding 426 | ) 427 | 428 | return frequencies 429 | --------------------------------------------------------------------------------