├── .black_config ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── LICENSE ├── README.md ├── devtool.py ├── images ├── 1.jpg └── 2.jpg ├── setup.py └── src ├── example.py └── fingerprint_enhancer ├── __init__.py └── fingerprint_image_enhancer.py /.black_config: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 130 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .idea/ 3 | *.pyc 4 | .vscode/launch.json 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-added-large-files # Not to commit huge files 6 | - id: check-case-conflict # Check for conflicts in case sensitive files 7 | - id: check-executables-have-shebangs # executables must have shebang 8 | - id: check-yaml # check yaml files for parsable syntax 9 | - id: end-of-file-fixer # check for end of file fixer 10 | - id: trailing-whitespace # check for trailing whitespaces 11 | - id: check-merge-conflict # check for files containing merge conflicts 12 | - repo: https://github.com/pre-commit/mirrors-prettier 13 | rev: v3.0.0-alpha.9-for-vscode 14 | hooks: 15 | - id: prettier 16 | files: .(json|yaml|yml) 17 | 18 | - repo: https://github.com/psf/black 19 | rev: 23.3.0 20 | hooks: 21 | - id: black 22 | args: ["--config", ".black_config"] 23 | 24 | - repo: https://github.com/pycqa/pylint 25 | rev: v2.17.4 26 | hooks: 27 | - id: pylint 28 | args: [.pylintrc] 29 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | max-line-length = 130 3 | generated-members = cv2.* 4 | disable = W0621,W0221,R0903,R0801,E0401 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Utkarsh-Deshmukh 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fingerprint-Enhancement-Python 2 | 3 | Uses oriented gabor filter bank to enhance the fingerprint image. The orientation of the gabor filters is decided by the orientation of ridges in the input image. 4 | 5 | ## Installation and Running the tests 6 | 7 | ### method 1 - use the library 8 | ``` 9 | pip install fingerprint_enhancer 10 | ``` 11 | 12 | **Usage:** 13 | ``` 14 | import fingerprint_enhancer # Load the library 15 | import cv2 16 | 17 | img = cv2.imread('image_path', 0) # read input image 18 | out = fingerprint_enhancer.enhance_fingerprint(img) # enhance the fingerprint image 19 | cv2.imshow('enhanced_image', out); # display the result 20 | cv2.waitKey(0) # hold the display window 21 | ``` 22 | - Alternatively, the script "src/example.py" can be used to run the example for this library. 23 | 24 | ### method 2 - use the source codes 25 | 1) go into the src folder 26 | - if on "develop" branch, run the file "example.py" 27 | - if on "master" branch, run the file file "main_enhancement.py" 28 | 29 | 2) The sample images are stored in the "images" folder 30 | 31 | 3) The enhanced image will be stored in the "enhanced" folder 32 | 33 | ## Linter check: 34 | run the command `python devtool.py run` to run linter checks. 35 | 36 | ## important note: 37 | The Develop Branch is what is up to date. Other branches might not be up to date. 38 | 39 | 40 | ## Results 41 | ![temp](https://cloud.githubusercontent.com/assets/13918778/25770604/637b3f38-31ee-11e7-818f-1f8359c96e07.jpg) 42 | 43 | ## Theory 44 | - We use oriented gabor filters to enhance a fingerprint image. The orientation of the gabor filters are based on the orientation of the ridges. the shape of the gabor filter is based on the frequency and wavelength of the ridges. 45 | 46 | ## License 47 | - This project is licensed under the BSD 2 License - see the LICENSE.md file for details 48 | 49 | ## Acknowledgements 50 | - This program is based on the paper: Hong, L., Wan, Y., and Jain, A. K. 'Fingerprint image enhancement: Algorithm and performance evaluation'. IEEE Transactions on Pattern Analysis and Machine Intelligence 20, 8 (1998), pp 777-789. 51 | 52 | - The author would like to thank Dr. Peter Kovesi (This code is a python implementation of his work) 53 | -------------------------------------------------------------------------------- /devtool.py: -------------------------------------------------------------------------------- 1 | """Module to run pre commit checks for staged files.""" 2 | from subprocess import check_call 3 | import os 4 | 5 | import fire 6 | import git 7 | 8 | PRECOMMIT_CHECK_EXTENSIONS = [".py"] 9 | PRE_COMMIT_HOOKS = [ 10 | "check-case-conflict", 11 | "check-merge-conflict", 12 | "end-of-file-fixer", 13 | "trailing-whitespace", 14 | "prettier", 15 | "black", 16 | "pylint", 17 | ] 18 | 19 | 20 | def _run_pre_commit_check(hook: str, files: str) -> None: 21 | """Run pre commit check on selected files 22 | 23 | Args: 24 | hook (_type_): name of hook. 25 | files (_type_): selected files. 26 | """ 27 | check_call(["pre-commit", "run", hook, "-v", "--files", *files]) 28 | 29 | 30 | class Command: 31 | """Commands to be run by fire lib.""" 32 | 33 | def run(self): 34 | """run linter check by running all PRECOMMIT HOOKS.""" 35 | repo_path = "." 36 | 37 | repo = git.Repo(repo_path) 38 | 39 | # Get the list of changed or staged files 40 | changed_or_staged_files = [item.a_path for item in repo.index.diff(None)] 41 | changed_or_staged_files.extend([item.a_path for item in repo.index.diff("HEAD")]) 42 | 43 | res = [item for item in changed_or_staged_files if os.path.splitext(item)[1] in PRECOMMIT_CHECK_EXTENSIONS] 44 | 45 | for hook in PRE_COMMIT_HOOKS: 46 | # for cur_file in res: 47 | _run_pre_commit_check(hook, res) 48 | 49 | 50 | if __name__ == "__main__": 51 | fire.Fire(Command) 52 | print("All looks good. 👍 ") 53 | -------------------------------------------------------------------------------- /images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Utkarsh-Deshmukh/Fingerprint-Enhancement-Python/2f41325e5e5ee19a8344c6ee3c4f8688a09cdda9/images/1.jpg -------------------------------------------------------------------------------- /images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Utkarsh-Deshmukh/Fingerprint-Enhancement-Python/2f41325e5e5ee19a8344c6ee3c4f8688a09cdda9/images/2.jpg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """generate the fingerprint_enhancer python package.""" 2 | 3 | from setuptools import setup 4 | 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | name="fingerprint_enhancer", 10 | version="0.0.14", 11 | author="utkarsh-deshmukh", 12 | author_email="utkarsh.deshmukh@gmail.com", 13 | description="enhance fingerprint images", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/Utkarsh-Deshmukh/Fingerprint-Enhancement-Python", 17 | download_url="https://github.com/Utkarsh-Deshmukh/Fingerprint-Enhancement-Python/archive/develop.zip", 18 | install_requires=["numpy", "opencv-python", "scipy"], 19 | license="MIT", 20 | keywords="Fingerprint Image Enhancement", 21 | package_dir={"": "src"}, 22 | packages=["fingerprint_enhancer"], 23 | classifiers=[ 24 | "Programming Language :: Python :: 3", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /src/example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Apr 18 11:42:58 2016 4 | 5 | @author: utkarsh 6 | """ 7 | 8 | import sys 9 | import cv2 10 | from fingerprint_enhancer.fingerprint_image_enhancer import FingerprintImageEnhancer 11 | 12 | if __name__ == "__main__": 13 | image_enhancer = FingerprintImageEnhancer() # Create object called image_enhancer 14 | if len(sys.argv) < 2: # load input image 15 | print("loading sample image") 16 | IMG_NAME = "1.jpg" 17 | img = cv2.imread("images/" + IMG_NAME) 18 | elif len(sys.argv) >= 2: 19 | IMG_NAME = sys.argv[1] 20 | img = cv2.imread("../images/" + IMG_NAME) 21 | 22 | if len(img.shape) > 2: # convert image into gray if necessary 23 | img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 24 | 25 | image_enhancer.enhance(img, invert_output=True) # run image enhancer 26 | image_enhancer.save_enhanced_image("enhanced/" + IMG_NAME) # save output 27 | -------------------------------------------------------------------------------- /src/fingerprint_enhancer/__init__.py: -------------------------------------------------------------------------------- 1 | """fingerprint_enhancer module.""" 2 | from .fingerprint_image_enhancer import enhance_fingerprint 3 | -------------------------------------------------------------------------------- /src/fingerprint_enhancer/fingerprint_image_enhancer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Nov 4 19:46:32 2020 4 | 5 | @author: utkarsh 6 | """ 7 | 8 | import math 9 | import os 10 | 11 | import cv2 12 | import numpy as np 13 | import scipy 14 | from scipy import ndimage, signal 15 | 16 | 17 | # pylint: disable=too-many-instance-attributes, too-many-function-args, too-many-locals, too-many-arguments 18 | class FingerprintImageEnhancer: 19 | """Fingerprint Enhancer Object.""" 20 | 21 | def __init__( 22 | self, 23 | ridge_segment_blksze=16, 24 | ridge_segment_thresh=0.1, 25 | gradient_sigma=1, 26 | block_sigma=7, 27 | orient_smooth_sigma=7, 28 | ridge_freq_blksze=38, 29 | ridge_freq_windsze=5, 30 | min_wave_length=5, 31 | max_wave_length=15, 32 | relative_scale_factor_x=0.65, 33 | relative_scale_factor_y=0.65, 34 | angle_inc=3.0, 35 | ridge_filter_thresh=-3, 36 | ): 37 | """initialize the object 38 | 39 | Args: 40 | ridge_segment_blksze (int, optional): ridge_segment_blksze. Defaults to 16. 41 | ridge_segment_thresh (float, optional): ridge_segment_thresh. Defaults to 0.1. 42 | gradient_sigma (int, optional): gradient_sigma. Defaults to 1. 43 | block_sigma (int, optional): block_sigma. Defaults to 7. 44 | orient_smooth_sigma (int, optional): orient_smooth_sigma. Defaults to 7. 45 | ridge_freq_blksze (int, optional): block size for ridge_freq calculation. Defaults to 38. 46 | ridge_freq_windsze (int, optional): window size for ridge_freq calculation. Defaults to 5. 47 | min_wave_length (int, optional): min_wave_length. Defaults to 5. 48 | max_wave_length (int, optional): max_wave_length. Defaults to 15. 49 | relative_scale_factor_x (float, optional): relative_scale_factor_x. Defaults to 0.65. 50 | relative_scale_factor_y (float, optional): relative_scale_factor_x. Defaults to 0.65. 51 | angle_inc (float, optional): angle increment for gabor filtering. Defaults to 3.0. 52 | ridge_filter_thresh (int, optional): ridge filter threshold. Defaults to -3. 53 | """ 54 | self.ridge_segment_blksze = ridge_segment_blksze 55 | self.ridge_segment_thresh = ridge_segment_thresh 56 | self.gradient_sigma = gradient_sigma 57 | self.block_sigma = block_sigma 58 | self.orient_smooth_sigma = orient_smooth_sigma 59 | self.ridge_freq_blksze = ridge_freq_blksze 60 | self.ridge_freq_windsze = ridge_freq_windsze 61 | self.min_wave_length = min_wave_length 62 | self.max_wave_length = max_wave_length 63 | self.relative_scale_factor_x = relative_scale_factor_x 64 | self.relative_scale_factor_y = relative_scale_factor_y 65 | self.angle_inc = angle_inc 66 | self.ridge_filter_thresh = ridge_filter_thresh 67 | 68 | self._mask = [] 69 | self._normim = [] 70 | self._orientim = [] 71 | self._mean_freq = [] 72 | self._median_freq = [] 73 | self._freq = [] 74 | self._freqim = [] 75 | self._binim = [] 76 | 77 | def __normalise(self, img: np.ndarray) -> np.ndarray: 78 | """Normalize the image. 79 | 80 | Args: 81 | img (np.ndarray): input image. 82 | 83 | Raises: 84 | ValueError: raises an exception if image is faulty. 85 | 86 | Returns: 87 | np.ndarray: normalized image 88 | """ 89 | if np.std(img) == 0: 90 | raise ValueError("Image standard deviation is 0. Please review image again") 91 | normed = (img - np.mean(img)) / (np.std(img)) 92 | return normed 93 | 94 | def __ridge_segment(self, img: np.ndarray): 95 | # RIDGESEGMENT - Normalises fingerprint image and segments ridge region 96 | # 97 | # Function identifies ridge regions of a fingerprint image and returns a 98 | # mask identifying this region. It also normalises the intesity values of 99 | # the image so that the ridge regions have zero mean, unit standard 100 | # deviation. 101 | # 102 | # This function breaks the image up into blocks of size blksze x blksze and 103 | # evaluates the standard deviation in each region. If the standard 104 | # deviation is above the threshold it is deemed part of the fingerprint. 105 | # Note that the image is normalised to have zero mean, unit standard 106 | # deviation prior to performing this process so that the threshold you 107 | # specify is relative to a unit standard deviation. 108 | # 109 | # Usage: [normim, mask, maskind] = ridgesegment(im, blksze, thresh) 110 | # 111 | # Arguments: im - Fingerprint image to be segmented. 112 | # blksze - Block size over which the the standard 113 | # deviation is determined (try a value of 16). 114 | # thresh - Threshold of standard deviation to decide if a 115 | # block is a ridge region (Try a value 0.1 - 0.2) 116 | # 117 | # Ouput: normim - Image where the ridge regions are renormalised to 118 | # have zero mean, unit standard deviation. 119 | # mask - Mask indicating ridge-like regions of the image, 120 | # 0 for non ridge regions, 1 for ridge regions. 121 | # maskind - Vector of indices of locations within the mask. 122 | # 123 | # Suggested values for a 500dpi fingerprint image: 124 | # 125 | # [normim, mask, maskind] = ridgesegment(im, 16, 0.1) 126 | # 127 | # See also: RIDGEORIENT, RIDGEFREQ, RIDGEFILTER 128 | 129 | ### REFERENCES 130 | 131 | # Peter Kovesi 132 | # School of Computer Science & Software Engineering 133 | # The University of Western Australia 134 | # pk at csse uwa edu au 135 | # http://www.csse.uwa.edu.au/~pk 136 | rows, cols = img.shape 137 | normalized_im = self.__normalise(img) # normalise to get zero mean and unit standard deviation 138 | 139 | new_rows = int(self.ridge_segment_blksze * np.ceil((float(rows)) / (float(self.ridge_segment_blksze)))) 140 | new_cols = int(self.ridge_segment_blksze * np.ceil((float(cols)) / (float(self.ridge_segment_blksze)))) 141 | 142 | padded_img = np.zeros((new_rows, new_cols)) 143 | stddevim = np.zeros((new_rows, new_cols)) 144 | padded_img[0:rows][:, 0:cols] = normalized_im 145 | for i in range(0, new_rows, self.ridge_segment_blksze): 146 | for j in range(0, new_cols, self.ridge_segment_blksze): 147 | block = padded_img[i : i + self.ridge_segment_blksze][:, j : j + self.ridge_segment_blksze] 148 | 149 | stddevim[i : i + self.ridge_segment_blksze][:, j : j + self.ridge_segment_blksze] = np.std(block) * np.ones( 150 | block.shape 151 | ) 152 | 153 | stddevim = stddevim[0:rows][:, 0:cols] 154 | self._mask = stddevim > self.ridge_segment_thresh 155 | mean_val = np.mean(normalized_im[self._mask]) 156 | std_val = np.std(normalized_im[self._mask]) 157 | self._normim = (normalized_im - mean_val) / (std_val) 158 | 159 | def __ridge_orient(self) -> None: 160 | # RIDGEORIENT - Estimates the local orientation of ridges in a fingerprint 161 | # 162 | # Usage: [orientim, reliability, coherence] = ridgeorientation(im, gradientsigma,... 163 | # blocksigma, ... 164 | # orientsmoothsigma) 165 | # 166 | # Arguments: im - A normalised input image. 167 | # gradientsigma - Sigma of the derivative of Gaussian 168 | # used to compute image gradients. 169 | # blocksigma - Sigma of the Gaussian weighting used to 170 | # sum the gradient moments. 171 | # orientsmoothsigma - Sigma of the Gaussian used to smooth 172 | # the final orientation vector field. 173 | # Optional: if ommitted it defaults to 0 174 | # 175 | # Output: orientim - The orientation image in radians. 176 | # Orientation values are +ve clockwise 177 | # and give the direction *along* the 178 | # ridges. 179 | # reliability - Measure of the reliability of the 180 | # orientation measure. This is a value 181 | # between 0 and 1. I think a value above 182 | # about 0.5 can be considered 'reliable'. 183 | # reliability = 1 - Imin./(Imax+.001); 184 | # coherence - A measure of the degree to which the local 185 | # area is oriented. 186 | # coherence = ((Imax-Imin)./(Imax+Imin)).^2; 187 | # 188 | # With a fingerprint image at a 'standard' resolution of 500dpi suggested 189 | # parameter values might be: 190 | # 191 | # [orientim, reliability] = ridgeorient(im, 1, 3, 3); 192 | # 193 | # See also: RIDGESEGMENT, RIDGEFREQ, RIDGEFILTER 194 | 195 | ### REFERENCES 196 | 197 | # May 2003 Original version by Raymond Thai, 198 | # January 2005 Reworked by Peter Kovesi 199 | # October 2011 Added coherence computation and orientsmoothsigma made optional 200 | # 201 | # School of Computer Science & Software Engineering 202 | # The University of Western Australia 203 | # pk at csse uwa edu au 204 | # http://www.csse.uwa.edu.au/~pk 205 | 206 | # Calculate image gradients. 207 | sze = np.fix(6 * self.gradient_sigma) 208 | if np.remainder(sze, 2) == 0: 209 | sze = sze + 1 210 | 211 | gauss = cv2.getGaussianKernel(int(sze), self.gradient_sigma) 212 | filter_gauss = gauss * gauss.T 213 | 214 | filter_grad_y, filter_grad_x = np.gradient(filter_gauss) # Gradient of Gaussian 215 | 216 | gradient_x = signal.convolve2d(self._normim, filter_grad_x, mode="same") 217 | gradient_y = signal.convolve2d(self._normim, filter_grad_y, mode="same") 218 | 219 | grad_x2 = np.power(gradient_x, 2) 220 | grad_y2 = np.power(gradient_y, 2) 221 | grad_xy = gradient_x * gradient_y 222 | 223 | # Now smooth the covariance data to perform a weighted summation of the data. 224 | sze = np.fix(6 * self.block_sigma) 225 | 226 | gauss = cv2.getGaussianKernel(int(sze), self.block_sigma) 227 | filter_gauss = gauss * gauss.T 228 | 229 | grad_x2 = ndimage.convolve(grad_x2, filter_gauss) 230 | grad_y2 = ndimage.convolve(grad_y2, filter_gauss) 231 | grad_xy = 2 * ndimage.convolve(grad_xy, filter_gauss) 232 | 233 | # Analytic solution of principal direction 234 | denom = np.sqrt(np.power(grad_xy, 2) + np.power((grad_x2 - grad_y2), 2)) + np.finfo(float).eps 235 | 236 | sin_2_theta = grad_xy / denom # Sine and cosine of doubled angles 237 | cos_2_theta = (grad_x2 - grad_y2) / denom 238 | 239 | if self.orient_smooth_sigma: 240 | sze = np.fix(6 * self.orient_smooth_sigma) 241 | if np.remainder(sze, 2) == 0: 242 | sze = sze + 1 243 | gauss = cv2.getGaussianKernel(int(sze), self.orient_smooth_sigma) 244 | filter_gauss = gauss * gauss.T 245 | cos_2_theta = ndimage.convolve(cos_2_theta, filter_gauss) # Smoothed sine and cosine of 246 | sin_2_theta = ndimage.convolve(sin_2_theta, filter_gauss) # doubled angles 247 | 248 | self._orientim = np.pi / 2 + np.arctan2(sin_2_theta, cos_2_theta) / 2 249 | 250 | def __ridge_freq(self): 251 | # RIDGEFREQ - Calculates a ridge frequency image 252 | # 253 | # Function to estimate the fingerprint ridge frequency across a 254 | # fingerprint image. This is done by considering blocks of the image and 255 | # determining a ridgecount within each block by a call to FREQEST. 256 | # 257 | # Usage: 258 | # [freqim, medianfreq] = ridgefreq(im, mask, orientim, blksze, windsze, ... 259 | # minWaveLength, maxWaveLength) 260 | # 261 | # Arguments: 262 | # im - Image to be processed. 263 | # mask - Mask defining ridge regions (obtained from RIDGESEGMENT) 264 | # orientim - Ridge orientation image (obtained from RIDGORIENT) 265 | # blksze - Size of image block to use (say 32) 266 | # windsze - Window length used to identify peaks. This should be 267 | # an odd integer, say 3 or 5. 268 | # minWaveLength, maxWaveLength - Minimum and maximum ridge 269 | # wavelengths, in pixels, considered acceptable. 270 | # 271 | # Output: 272 | # freqim - An image the same size as im with values set to 273 | # the estimated ridge spatial frequency within each 274 | # image block. If a ridge frequency cannot be 275 | # found within a block, or cannot be found within the 276 | # limits set by min and max Wavlength freqim is set 277 | # to zeros within that block. 278 | # medianfreq - Median frequency value evaluated over all the 279 | # valid regions of the image. 280 | # 281 | # Suggested parameters for a 500dpi fingerprint image 282 | # [freqim, medianfreq] = ridgefreq(im,orientim, 32, 5, 5, 15); 283 | # 284 | 285 | # See also: RIDGEORIENT, FREQEST, RIDGESEGMENT 286 | 287 | # Reference: 288 | # Hong, L., Wan, Y., and Jain, A. K. Fingerprint image enhancement: 289 | # Algorithm and performance evaluation. IEEE Transactions on Pattern 290 | # Analysis and Machine Intelligence 20, 8 (1998), 777 789. 291 | 292 | ### REFERENCES 293 | 294 | # Peter Kovesi 295 | # School of Computer Science & Software Engineering 296 | # The University of Western Australia 297 | # pk at csse uwa edu au 298 | # http://www.csse.uwa.edu.au/~pk 299 | 300 | rows, cols = self._normim.shape 301 | freq = np.zeros((rows, cols)) 302 | 303 | for i in range(0, rows - self.ridge_freq_blksze, self.ridge_freq_blksze): 304 | for j in range(0, cols - self.ridge_freq_blksze, self.ridge_freq_blksze): 305 | blkim = self._normim[i : i + self.ridge_freq_blksze][:, j : j + self.ridge_freq_blksze] 306 | blkor = self._orientim[i : i + self.ridge_freq_blksze][:, j : j + self.ridge_freq_blksze] 307 | 308 | freq[i : i + self.ridge_freq_blksze][:, j : j + self.ridge_freq_blksze] = self.__frequest(blkim, blkor) 309 | 310 | self._freq = freq * self._mask 311 | freq_1d = np.reshape(self._freq, (1, rows * cols)) 312 | ind = np.where(freq_1d > 0) 313 | 314 | ind = np.array(ind) 315 | ind = ind[1, :] 316 | 317 | non_zero_elems_in_freq = freq_1d[0][ind] 318 | 319 | self._mean_freq = np.mean(non_zero_elems_in_freq) 320 | self._median_freq = np.median(non_zero_elems_in_freq) # does not work properly 321 | 322 | self._freq = self._mean_freq * self._mask 323 | 324 | def __frequest(self, blkim: np.ndarray, blkor: np.ndarray) -> np.ndarray: 325 | # FREQEST - Estimate fingerprint ridge frequency within image block 326 | # 327 | # Function to estimate the fingerprint ridge frequency within a small block 328 | # of a fingerprint image. This function is used by RIDGEFREQ 329 | # 330 | # Usage: 331 | # freqim = freqest(im, orientim, windsze, minWaveLength, maxWaveLength) 332 | # 333 | # Arguments: 334 | # im - Image block to be processed. 335 | # orientim - Ridge orientation image of image block. 336 | # windsze - Window length used to identify peaks. This should be 337 | # an odd integer, say 3 or 5. 338 | # minWaveLength, maxWaveLength - Minimum and maximum ridge 339 | # wavelengths, in pixels, considered acceptable. 340 | # 341 | # Output: 342 | # freqim - An image block the same size as im with all values 343 | # set to the estimated ridge spatial frequency. If a 344 | # ridge frequency cannot be found, or cannot be found 345 | # within the limits set by min and max Wavlength 346 | # freqim is set to zeros. 347 | # 348 | # Suggested parameters for a 500dpi fingerprint image 349 | # freqim = freqest(im,orientim, 5, 5, 15); 350 | # 351 | # See also: RIDGEFREQ, RIDGEORIENT, RIDGESEGMENT 352 | 353 | ### REFERENCES 354 | 355 | # Peter Kovesi 356 | # School of Computer Science & Software Engineering 357 | # The University of Western Australia 358 | # pk at csse uwa edu au 359 | # http://www.csse.uwa.edu.au/~pk 360 | 361 | rows, _ = np.shape(blkim) 362 | 363 | # Find mean orientation within the block. This is done by averaging the 364 | # sines and cosines of the doubled angles before reconstructing the 365 | # angle again. This avoids wraparound problems at the origin. 366 | 367 | cosorient = np.mean(np.cos(2 * blkor)) 368 | sinorient = np.mean(np.sin(2 * blkor)) 369 | orient = math.atan2(sinorient, cosorient) / 2 370 | 371 | # Rotate the image block so that the ridges are vertical 372 | 373 | # ROT_mat = cv2.getRotationMatrix2D((cols/2,rows/2),orient/np.pi*180 + 90,1) 374 | # rotim = cv2.warpAffine(im,ROT_mat,(cols,rows)) 375 | rotim = scipy.ndimage.rotate(blkim, orient / np.pi * 180 + 90, axes=(1, 0), reshape=False, order=3, mode="nearest") 376 | 377 | # Now crop the image so that the rotated image does not contain any 378 | # invalid regions. This prevents the projection down the columns 379 | # from being mucked up. 380 | 381 | cropsze = int(np.fix(rows / np.sqrt(2))) 382 | offset = int(np.fix((rows - cropsze) / 2)) 383 | rotim = rotim[offset : offset + cropsze][:, offset : offset + cropsze] 384 | 385 | # Sum down the columns to get a projection of the grey values down 386 | # the ridges. 387 | 388 | proj = np.sum(rotim, axis=0) 389 | dilation = scipy.ndimage.grey_dilation(proj, self.ridge_freq_windsze, structure=np.ones(self.ridge_freq_windsze)) 390 | 391 | temp = np.abs(dilation - proj) 392 | 393 | peak_thresh = 2 394 | 395 | maxpts = (temp < peak_thresh) & (proj > np.mean(proj)) 396 | maxind = np.where(maxpts) 397 | 398 | _, cols_maxind = np.shape(maxind) 399 | 400 | # Determine the spatial frequency of the ridges by divinding the 401 | # distance between the 1st and last peaks by the (No of peaks-1). If no 402 | # peaks are detected, or the wavelength is outside the allowed bounds, 403 | # the frequency image is set to 0 404 | 405 | if cols_maxind < 2: 406 | return np.zeros(blkim.shape) 407 | no_of_peaks = cols_maxind 408 | wave_length = (maxind[0][cols_maxind - 1] - maxind[0][0]) / (no_of_peaks - 1) 409 | if self.min_wave_length <= wave_length <= self.max_wave_length: 410 | return 1 / np.double(wave_length) * np.ones(blkim.shape) 411 | return np.zeros(blkim.shape) 412 | 413 | def __ridge_filter(self): 414 | # RIDGEFILTER - enhances fingerprint image via oriented filters 415 | # 416 | # Function to enhance fingerprint image via oriented filters 417 | # 418 | # Usage: 419 | # newim = ridgefilter(im, orientim, freqim, kx, ky, showfilter) 420 | # 421 | # Arguments: 422 | # im - Image to be processed. 423 | # orientim - Ridge orientation image, obtained from RIDGEORIENT. 424 | # freqim - Ridge frequency image, obtained from RIDGEFREQ. 425 | # kx, ky - Scale factors specifying the filter sigma relative 426 | # to the wavelength of the filter. This is done so 427 | # that the shapes of the filters are invariant to the 428 | # scale. kx controls the sigma in the x direction 429 | # which is along the filter, and hence controls the 430 | # bandwidth of the filter. ky controls the sigma 431 | # across the filter and hence controls the 432 | # orientational selectivity of the filter. A value of 433 | # 0.5 for both kx and ky is a good starting point. 434 | # showfilter - An optional flag 0/1. When set an image of the 435 | # largest scale filter is displayed for inspection. 436 | # 437 | # Output: 438 | # newim - The enhanced image 439 | # 440 | # See also: RIDGEORIENT, RIDGEFREQ, RIDGESEGMENT 441 | 442 | # Reference: 443 | # Hong, L., Wan, Y., and Jain, A. K. Fingerprint image enhancement: 444 | # Algorithm and performance evaluation. IEEE Transactions on Pattern 445 | # Analysis and Machine Intelligence 20, 8 (1998), 777 789. 446 | 447 | ### REFERENCES 448 | 449 | # Peter Kovesi 450 | # School of Computer Science & Software Engineering 451 | # The University of Western Australia 452 | # pk at csse uwa edu au 453 | # http://www.csse.uwa.edu.au/~pk 454 | 455 | norm_im = np.double(self._normim) 456 | rows, cols = norm_im.shape 457 | newim = np.zeros((rows, cols)) 458 | 459 | freq_1d = np.reshape(self._freq, (1, rows * cols)) 460 | ind = np.where(freq_1d > 0) 461 | 462 | ind = np.array(ind) 463 | ind = ind[1, :] 464 | 465 | # Round the array of frequencies to the nearest 0.01 to reduce the 466 | # number of distinct frequencies we have to deal with. 467 | 468 | non_zero_elems_in_freq = freq_1d[0][ind] 469 | non_zero_elems_in_freq = np.double(np.round((non_zero_elems_in_freq * 100))) / 100 470 | 471 | unfreq = np.unique(non_zero_elems_in_freq) 472 | 473 | # Generate filters corresponding to these distinct frequencies and 474 | # orientations in 'angle_inc' increments. 475 | 476 | sigmax = 1 / unfreq[0] * self.relative_scale_factor_x 477 | sigmay = 1 / unfreq[0] * self.relative_scale_factor_y 478 | 479 | sze = int(np.round(3 * np.max([sigmax, sigmay]))) 480 | 481 | mesh_x, mesh_y = np.meshgrid(np.linspace(-sze, sze, (2 * sze + 1)), np.linspace(-sze, sze, (2 * sze + 1))) 482 | 483 | reffilter = np.exp(-(((np.power(mesh_x, 2)) / (sigmax * sigmax) + (np.power(mesh_y, 2)) / (sigmay * sigmay)))) * np.cos( 484 | 2 * np.pi * unfreq[0] * mesh_x 485 | ) # this is the original gabor filter 486 | 487 | filt_rows, filt_cols = reffilter.shape 488 | 489 | angle_range = int(180 / self.angle_inc) 490 | 491 | gabor_filter = np.array(np.zeros((angle_range, filt_rows, filt_cols))) 492 | 493 | for filter_idx in range(0, angle_range): 494 | # Generate rotated versions of the filter. Note orientation 495 | # image provides orientation *along* the ridges, hence +90 496 | # degrees, and imrotate requires angles +ve anticlockwise, hence 497 | # the minus sign. 498 | 499 | rot_filt = scipy.ndimage.rotate(reffilter, -(filter_idx * self.angle_inc + 90), reshape=False) 500 | gabor_filter[filter_idx] = rot_filt 501 | 502 | # Find indices of matrix points greater than maxsze from the image 503 | # boundary 504 | 505 | maxsze = int(sze) 506 | 507 | temp = self._freq > 0 508 | validr, validc = np.where(temp) 509 | 510 | temp1 = validr > maxsze 511 | temp2 = validr < rows - maxsze 512 | temp3 = validc > maxsze 513 | temp4 = validc < cols - maxsze 514 | 515 | final_temp = temp1 & temp2 & temp3 & temp4 516 | 517 | finalind = np.where(final_temp) 518 | 519 | # Convert orientation matrix values from radians to an index value 520 | # that corresponds to round(degrees/angle_inc) 521 | 522 | maxorientindex = np.round(180 / self.angle_inc) 523 | orientindex = np.round(self._orientim / np.pi * 180 / self.angle_inc) 524 | 525 | # do the filtering 526 | for i in range(0, rows): 527 | for j in range(0, cols): 528 | if orientindex[i][j] < 1: 529 | orientindex[i][j] = orientindex[i][j] + maxorientindex 530 | if orientindex[i][j] > maxorientindex: 531 | orientindex[i][j] = orientindex[i][j] - maxorientindex 532 | _, finalind_cols = np.shape(finalind) 533 | sze = int(sze) 534 | for k in range(0, finalind_cols): 535 | cur_r = validr[finalind[0][k]] 536 | cur_c = validc[finalind[0][k]] 537 | 538 | img_block = norm_im[cur_r - sze : cur_r + sze + 1][:, cur_c - sze : cur_c + sze + 1] 539 | 540 | newim[cur_r][cur_c] = np.sum(img_block * gabor_filter[int(orientindex[cur_r][cur_c]) - 1]) 541 | 542 | self._binim = newim < self.ridge_filter_thresh 543 | 544 | def save_enhanced_image(self, path: str) -> None: 545 | """Save the enhanced image to the path specified. 546 | 547 | Args: 548 | path (str): image name. 549 | """ 550 | os.makedirs(os.path.dirname(path), exist_ok=True) 551 | # saves the enhanced image at the specified path 552 | cv2.imwrite(path, (255 * self._binim)) 553 | 554 | def enhance(self, img: np.ndarray, resize: bool = True, invert_output=False) -> np.ndarray: 555 | """Enhance the input image. 556 | 557 | Args: 558 | img (np.ndarray): input image. 559 | resize (bool, optional): resize the input image. Defaults to True. 560 | invert_output (bool, optional): invert the output 561 | 562 | Returns: 563 | np.ndarray: return the enhanced image. 564 | """ 565 | if resize: 566 | rows, cols = np.shape(img) 567 | aspect_ratio = np.double(rows) / np.double(cols) 568 | 569 | new_rows = 350 # randomly selected number 570 | new_cols = new_rows / aspect_ratio 571 | 572 | img = cv2.resize(img, (int(new_cols), int(new_rows))) 573 | 574 | self.__ridge_segment(img) # normalise the image and find a ROI 575 | self.__ridge_orient() # compute orientation image 576 | self.__ridge_freq() # compute major frequency of ridges 577 | self.__ridge_filter() # filter the image using oriented gabor filter 578 | if invert_output: 579 | self._binim ^= True 580 | return self._binim 581 | 582 | 583 | # pylint: disable = too-many-arguments 584 | def enhance_fingerprint( 585 | img: np.ndarray, 586 | resize: bool = False, 587 | ridge_segment_blksze: int = 16, 588 | ridge_segment_thresh: float = 0.1, 589 | gradient_sigma: int = 1, 590 | block_sigma: int = 7, 591 | orient_smooth_sigma: int = 7, 592 | ridge_freq_blksze: int = 38, 593 | ridge_freq_windsze: int = 5, 594 | min_wave_length: int = 5, 595 | max_wave_length: int = 15, 596 | relative_scale_factor_x: float = 0.65, 597 | relative_scale_factor_y: float = 0.65, 598 | angle_inc: float = 3.0, 599 | ridge_filter_thresh: int = -3, 600 | invert_output: bool = False, 601 | ) -> np.ndarray: 602 | """enhance the input image. 603 | 604 | Args: 605 | img (np.ndarray): input image 606 | resize (bool, optional): resize the input image to a fixed size. Defaults to False. 607 | ridge_segment_blksze (int, optional): ridge_segment_blksze. Defaults to 16. 608 | ridge_segment_thresh (float, optional): ridge_segment_thresh. Defaults to 0.1. 609 | gradient_sigma (int, optional): gradient_sigma. Defaults to 1. 610 | block_sigma (int, optional): block_sigma. Defaults to 7. 611 | orient_smooth_sigma (int, optional): orient_smooth_sigma. Defaults to 7. 612 | ridge_freq_blksze (int, optional): block size for ridge_freq calculation. Defaults to 38. 613 | ridge_freq_windsze (int, optional): window size for ridge_freq calculation. Defaults to 5. 614 | min_wave_length (int, optional): min_wave_length. Defaults to 5. 615 | max_wave_length (int, optional): max_wave_length. Defaults to 15. 616 | relative_scale_factor_x (float, optional): relative_scale_factor_x. Defaults to 0.65. 617 | relative_scale_factor_y (float, optional): relative_scale_factor_x. Defaults to 0.65. 618 | angle_inc (float, optional): angle increment for gabor filtering. Defaults to 3.0. 619 | ridge_filter_thresh (int, optional): ridge filter threshold. Defaults to -3. 620 | invert_output (bool, optional): flag to invert the enhanced-output. Defaults to False. 621 | 622 | Returns: 623 | np.ndarray: _description_ 624 | """ 625 | image_enhancer = FingerprintImageEnhancer( 626 | ridge_segment_blksze, 627 | ridge_segment_thresh, 628 | gradient_sigma, 629 | block_sigma, 630 | orient_smooth_sigma, 631 | ridge_freq_blksze, 632 | ridge_freq_windsze, 633 | min_wave_length, 634 | max_wave_length, 635 | relative_scale_factor_x, 636 | relative_scale_factor_y, 637 | angle_inc, 638 | ridge_filter_thresh, 639 | ) # Create object called image_enhancer 640 | enhanced_output = image_enhancer.enhance(img, resize, invert_output=invert_output) 641 | return enhanced_output 642 | --------------------------------------------------------------------------------