├── README.md ├── LICENSE ├── datasets.py ├── .gitignore ├── recognition.py ├── coding.py └── segmentation.py /README.md: -------------------------------------------------------------------------------- 1 | # iris-recognition 2 | An iris recognition system implemented with Python. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tomasz Danel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /datasets.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | 5 | def load_utiris(): 6 | """Fetches NIR images from UTIRIS dataset. 7 | 8 | Retrieves image paths and labels for each NIR image in the dataset. There should already exist a directory named 9 | 'UTIRIS V.1'. If it does not exist then download the dataset from the official page (https://utiris.wordpress.com/). 10 | 11 | :return: A dictionary with two keys: 'data' contains all images paths, 'target' contains the image labels - each eye 12 | gets its unique number. 13 | """ 14 | data = [] 15 | target = [] 16 | target_i = 0 17 | index_used = False 18 | for dirpath, dirnames, filenames in os.walk('UTIRIS V.1\\Infrared Images'): 19 | for f in filenames: 20 | if f.endswith('.bmp'): 21 | data.append('{}\{}'.format(dirpath, f)) 22 | target.append(target_i) 23 | index_used = True 24 | if index_used: 25 | target_i += 1 26 | index_used = False 27 | return {'data': np.array(data), 28 | 'target': np.array(target)} 29 | 30 | 31 | # Example usage 32 | if __name__ == '__main__': 33 | import cv2 34 | 35 | data = load_utiris()['data'] 36 | image = cv2.imread(data[0]) 37 | cv2.imshow('test', image) 38 | cv2.waitKey(0) 39 | -------------------------------------------------------------------------------- /.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 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /recognition.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from segmentation import * 3 | from coding import * 4 | import os 5 | 6 | 7 | def compare_codes(a, b, mask_a, mask_b, rotation=False): 8 | """Compares two codes and calculates Jaccard index. 9 | 10 | :param a: Code of the first iris 11 | :param b: Code of the second iris 12 | :param mask_a: Mask of the first iris 13 | :param mask_b: Mask of the second iris 14 | :param rotation: Maximum cyclic rotation of the code. If this argument is greater than zero, the function will 15 | return minimal distance of all code rotations. If this argument is False, no rotations are calculated. 16 | 17 | :return: Distance between two codes. 18 | """ 19 | if rotation: 20 | d = [] 21 | for i in range(-rotation, rotation + 1): 22 | c = np.roll(b, i, axis=1) 23 | mask_c = np.roll(mask_b, i, axis=1) 24 | d.append(np.sum(np.remainder(a + c, 2) * mask_a * mask_c) / np.sum(mask_a * mask_c)) 25 | return np.min(d) 26 | return np.sum(np.remainder(a + b, 2) * mask_a * mask_b) / np.sum(mask_a * mask_b) 27 | 28 | 29 | def encode_photo(image): 30 | """Finds the pupil and iris of the eye, and then encodes the unravelled iris. 31 | 32 | :param image: Image of an eye 33 | :return: Encoded iris (code, mask) 34 | :rtype: tuple (ndarray, ndarray) 35 | """ 36 | img = preprocess(image) 37 | x, y, r = find_pupil_hough(img) 38 | x_iris, y_iris, r_iris = find_iris_id(img, x, y, r) 39 | iris = unravel_iris(image, x, y, r, x_iris, y_iris, r_iris) 40 | return iris_encode(iris) 41 | 42 | 43 | def save_codes(data): 44 | """Takes data, and saves encoded images to 'codes' directory. 45 | 46 | :param data: Data formatted as returned by load_* functions from datasets.py module (dictionary with keys 'data' and 47 | 'target') 48 | :type data: dict 49 | """ 50 | for i in range(len(data['data'])): 51 | print("{}/{}".format(i, len(data['data']))) 52 | image = cv2.imread(data['data'][i]) 53 | try: 54 | code, mask = encode_photo(image) 55 | np.save('codes\\code{}'.format(i), np.array(code)) 56 | np.save('codes\\mask{}'.format(i), np.array(mask)) 57 | np.save('codes\\target{}'.format(i), data['target'][i]) 58 | except: 59 | np.save('codes\\code{}'.format(i), np.zeros(1)) 60 | np.save('codes\\mask{}'.format(i), np.zeros(1)) 61 | np.save('codes\\target{}'.format(i), data['target'][i]) 62 | 63 | 64 | def load_codes(): 65 | """Loads codes saved by save_codes function. 66 | 67 | :return: Codes, masks, and targets of saved images 68 | :rtype: tuple (ndarray, ndarray, ndarray) 69 | """ 70 | codes = [] 71 | masks = [] 72 | targets = [] 73 | i = 0 74 | while os.path.isfile('codes\\code{}.npy'.format(i)): 75 | code = np.load('codes\\code{}.npy'.format(i)) 76 | if code.shape[0] != 1: 77 | codes.append(code) 78 | masks.append(np.load('codes\\mask{}.npy'.format(i))) 79 | targets.append(np.load('codes\\target{}.npy'.format(i))) 80 | i += 1 81 | return np.array(codes), np.array(masks), np.array(targets) 82 | 83 | 84 | def split_codes(codes, masks, targets): 85 | """Splits data for testing purposes. 86 | 87 | The first piece of data (code, mask, target) for each target is separated from the rest. 88 | 89 | :param codes: Array of codes 90 | :param masks: Array of masks 91 | :param targets: Array of targets 92 | :return: All codes, masks, and targets without the first instance of each target, then codes, masks, and targets of 93 | containing test examples 94 | :rtype: 6-tuple of ndarrays 95 | """ 96 | X_test = [] 97 | X_base = [] 98 | M_test = [] 99 | M_base = [] 100 | y_test = [] 101 | y_base = [] 102 | for i in range(targets.max() + 1): 103 | X = codes[targets == i] 104 | M = masks[targets == i] 105 | X_test.append(X[0]) 106 | X_base.append(X[1:]) 107 | M_test.append(M[0]) 108 | M_base.append(M[1:]) 109 | y_test.append(i) 110 | y_base += [i] * X[1:].shape[0] 111 | return np.vstack(X_base), np.vstack(M_base), np.array(y_base), np.array(X_test), np.array(M_test), np.array(y_test) 112 | 113 | 114 | if __name__ == '__main__': 115 | data = load_utiris()['data'] 116 | image = cv2.imread(data[0]) 117 | image2 = cv2.imread(data[6]) 118 | print(image.shape) 119 | print(image2.shape) 120 | code, mask = encode_photo(image) 121 | code2, mask2 = encode_photo(image2) 122 | print(compare_codes(code, code2, mask, mask2)) -------------------------------------------------------------------------------- /coding.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | 4 | from skimage.util import view_as_blocks 5 | 6 | 7 | def polar2cart(r, x0, y0, theta): 8 | """Changes polar coordinates to cartesian coordinate system. 9 | 10 | :param r: Radius 11 | :param x0: x coordinate of the origin 12 | :param y0: y coordinate of the origin 13 | :param theta: Angle 14 | :return: Cartesian coordinates 15 | :rtype: tuple (int, int) 16 | """ 17 | x = int(x0 + r * math.cos(theta)) 18 | y = int(y0 + r * math.sin(theta)) 19 | return x, y 20 | 21 | 22 | def unravel_iris(img, xp, yp, rp, xi, yi, ri, phase_width=300, iris_width=150): 23 | """Unravels the iris from the image and transforms it to a straightened representation. 24 | 25 | :param img: Image of an eye 26 | :param xp: x coordinate of the pupil centre 27 | :param yp: y coordinate of the pupil centre 28 | :param rp: Radius of the pupil 29 | :param xi: x coordinate of the iris centre 30 | :param yi: y coordinate of the iris centre 31 | :param ri: Radius of the iris 32 | :param phase_width: Length of the transformed iris 33 | :param iris_width: Width of the transformed iris 34 | :return: Straightened image of the iris 35 | :rtype: ndarray 36 | """ 37 | if img.ndim > 2: 38 | img = img[:, :, 0].copy() 39 | iris = np.zeros((iris_width, phase_width)) 40 | theta = np.linspace(0, 2 * np.pi, phase_width) 41 | for i in range(phase_width): 42 | begin = polar2cart(rp, xp, yp, theta[i]) 43 | end = polar2cart(ri, xi, yi, theta[i]) 44 | xspace = np.linspace(begin[0], end[0], iris_width) 45 | yspace = np.linspace(begin[1], end[1], iris_width) 46 | iris[:, i] = [255 - img[int(y), int(x)] 47 | if 0 <= int(x) < img.shape[1] and 0 <= int(y) < img.shape[0] 48 | else 0 49 | for x, y in zip(xspace, yspace)] 50 | return iris 51 | 52 | 53 | def gabor(rho, phi, w, theta0, r0, alpha, beta): 54 | """Calculates gabor wavelet. 55 | 56 | :param rho: Radius of the input coordinates 57 | :param phi: Angle of the input coordinates 58 | :param w: Gabor wavelet parameter (see the formula) 59 | :param theta0: Gabor wavelet parameter (see the formula) 60 | :param r0: Gabor wavelet parameter (see the formula) 61 | :param alpha: Gabor wavelet parameter (see the formula) 62 | :param beta: Gabor wavelet parameter (see the formula) 63 | :return: Gabor wavelet value at (rho, phi) 64 | """ 65 | return np.exp(-w * 1j * (theta0 - phi)) * np.exp(-(rho - r0) ** 2 / alpha ** 2) * \ 66 | np.exp(-(phi - theta0) ** 2 / beta ** 2) 67 | 68 | 69 | def gabor_convolve(img, w, alpha, beta): 70 | """Uses gabor wavelets to extract iris features. 71 | 72 | :param img: Image of an iris 73 | :param w: w parameter of Gabor wavelets 74 | :param alpha: alpha parameter of Gabor wavelets 75 | :param beta: beta parameter of Gabor wavelets 76 | :return: Transformed image of the iris (real and imaginary) 77 | :rtype: tuple (ndarray, ndarray) 78 | """ 79 | rho = np.array([np.linspace(0, 1, img.shape[0]) for i in range(img.shape[1])]).T 80 | x = np.linspace(0, 1, img.shape[0]) 81 | y = np.linspace(-np.pi, np.pi, img.shape[1]) 82 | xx, yy = np.meshgrid(x, y) 83 | return rho * img * np.real(gabor(xx, yy, w, 0, 0.5, alpha, beta).T), \ 84 | rho * img * np.imag(gabor(xx, yy, w, 0, 0.5, alpha, beta).T) 85 | 86 | 87 | def iris_encode(img, dr=15, dtheta=15, alpha=0.4): 88 | """Encodes the straightened representation of an iris with gabor wavelets. 89 | 90 | :param img: Image of an iris 91 | :param dr: Width of image patches producing one feature 92 | :param dtheta: Length of image patches producing one feature 93 | :param alpha: Gabor wavelets modifier (beta parameter of Gabor wavelets becomes inverse of this number) 94 | :return: Iris code and its mask 95 | :rtype: tuple (ndarray, ndarray) 96 | """ 97 | # mean = np.mean(img) 98 | # std = img.std() 99 | mask = view_as_blocks(np.logical_and(100 < img, img < 230), (dr, dtheta)) 100 | norm_iris = (img - img.mean()) / img.std() 101 | patches = view_as_blocks(norm_iris, (dr, dtheta)) 102 | code = np.zeros((patches.shape[0] * 3, patches.shape[1] * 2)) 103 | code_mask = np.zeros((patches.shape[0] * 3, patches.shape[1] * 2)) 104 | for i, row in enumerate(patches): 105 | for j, p in enumerate(row): 106 | for k, w in enumerate([8, 16, 32]): 107 | wavelet = gabor_convolve(p, w, alpha, 1 / alpha) 108 | code[3 * i + k, 2 * j] = np.sum(wavelet[0]) 109 | code[3 * i + k, 2 * j + 1] = np.sum(wavelet[1]) 110 | code_mask[3 * i + k, 2 * j] = code_mask[3 * i + k, 2 * j + 1] = \ 111 | 1 if mask[i, j].sum() > dr * dtheta * 3 / 4 else 0 112 | code[code >= 0] = 1 113 | code[code < 0] = 0 114 | return code, code_mask 115 | 116 | 117 | if __name__ == '__main__': 118 | import cv2 119 | from datasets import load_utiris 120 | import matplotlib.pyplot as plt 121 | 122 | data = load_utiris()['data'] 123 | image = cv2.imread(data[0]) 124 | 125 | iris = unravel_iris(image, 444, 334, 66, 450, 352, 245) 126 | code, mask = iris_encode(iris) 127 | 128 | plt.subplot(211) 129 | plt.imshow(iris, cmap=plt.cm.gray) 130 | plt.subplot(223) 131 | plt.imshow(code, cmap=plt.cm.gray, interpolation='none') 132 | plt.subplot(224) 133 | plt.imshow(mask, cmap=plt.cm.gray, interpolation='none') 134 | plt.show() 135 | -------------------------------------------------------------------------------- /segmentation.py: -------------------------------------------------------------------------------- 1 | from datasets import load_utiris 2 | import matplotlib.pyplot as plt 3 | import cv2 4 | import math 5 | import numpy as np 6 | from scipy.ndimage.filters import gaussian_filter 7 | 8 | 9 | def show_segment(img, x, y, r, x2=None, y2=None, r2=None): 10 | """Shows an image with pupil and iris marked with circles. 11 | 12 | :param img: Image of an eye 13 | :param x: x coordinate of a segment 14 | :param y: y coordinate of a segment 15 | :param r: radius of a segment 16 | :param x2: x coordinate of another segment (optional, can be None) 17 | :param y2: y coordinate of another segment (optional, can be None) 18 | :param r2: r coordinate of another segment (optional, can be None) 19 | """ 20 | ax = plt.subplot() 21 | ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) 22 | segment = plt.Circle((x, y), r, color='b', fill=False) 23 | ax.add_artist(segment) 24 | if r2 is not None: 25 | segment2 = plt.Circle((x2, y2), r2, color='r', fill=False) 26 | ax.add_artist(segment2) 27 | plt.show() 28 | 29 | 30 | def integrate(img, x0, y0, r, arc_start=0, arc_end=1, n=8): 31 | """Calculates line integral in the image. 32 | 33 | :param img: Image of an eye 34 | :param x0: x coordinate of the centre of the segment 35 | :param y0: y coordinate of the centre of the segment 36 | :param r: radius of the segment 37 | :param arc_start: From which point on the arc should the calculation start 38 | :param arc_end: At which point on the arc should the calculation end 39 | :param n: Number of points at which intergral is calculated along the line (the more points, the more accurate the 40 | result is) 41 | :return: Line integral value 42 | """ 43 | theta = 2 * math.pi / n 44 | integral = 0 45 | for step in np.arange(arc_start * n, arc_end * n, arc_end - arc_start): 46 | x = int(x0 + r * math.cos(step * theta)) 47 | y = int(y0 + r * math.sin(step * theta)) 48 | integral += img[x, y] 49 | return integral / n 50 | 51 | 52 | def find_segment(img, x0, y0, minr=0, maxr=500, step=1, sigma=5., center_margin=30, segment_type='iris', jump=1): 53 | """Finds the segment (pupil or iris) in the image. 54 | 55 | :param img: Image of an eye 56 | :param x0: Starting x coordinate 57 | :param y0: Starting y coordinate 58 | :param minr: Minimal radius 59 | :param maxr: Maximal radius 60 | :param step: The difference between two consecutive radii in the search space 61 | :param sigma: The amount of blur of integral values before selecting the optimal radius 62 | :param center_margin: The maximum distance from x0, y0 reached to find the optimal segment centre 63 | :param segment_type: Either 'iris' ot 'pupil' used to optimize the search 64 | :param jump: The difference between two consecutive segment centres in the search space 65 | :return: x, y of the segment centre, radius of the segment and integral value matching the optimal result 66 | """ 67 | max_o = 0 68 | max_l = [] 69 | 70 | if img.ndim > 2: 71 | img = img[:, :, 0] 72 | margin_img = np.pad(img, maxr, 'edge') 73 | x0 += maxr 74 | y0 += maxr 75 | for x in range(x0 - center_margin, x0 + center_margin + 1, jump): 76 | for y in range(y0 - center_margin, y0 + center_margin + 1, jump): 77 | if segment_type == 'pupil': 78 | l = np.array([integrate(margin_img, y, x, r) for r in range(minr, maxr, step)]) 79 | else: 80 | l = np.array([integrate(margin_img, y, x, r, 1 / 8, 3 / 8, n=8) + 81 | integrate(margin_img, y, x, r, 5 / 8, 7 / 8, n=8) 82 | for r in range(minr + abs(x0 - x) + abs(y0 - y), maxr, step)]) 83 | l = (l[2:] - l[:-2]) / 2 84 | l = gaussian_filter(l, sigma) 85 | l = np.abs(l) 86 | max_c = np.max(l) 87 | if max_c > max_o: 88 | max_o = max_c 89 | max_l = l 90 | max_x, max_y = x, y 91 | r = np.argmax(l) * step + minr + abs(x0 - x) + abs(y0 - y) 92 | 93 | return max_x - maxr, max_y - maxr, r, max_l 94 | 95 | 96 | def _layer_to_full_image(layer): 97 | """Makes a full RGB image in grayscale from one layer. 98 | 99 | :param layer: One channel of the image 100 | :return: RGB image with the layer repeated in every channel 101 | """ 102 | return np.transpose(np.array([layer, layer, layer]), (1, 2, 0)) 103 | 104 | 105 | def find_pupil_center(img): 106 | """Calculates the centre of the pupil using a naive method. 107 | 108 | :param img: Image of an eye 109 | :return: x, y coordinates of the centre of the pupil 110 | """ 111 | P = np.percentile(img[:, :, 0], 1) 112 | bin_pupil = np.where(img[:, :, 0] > P, 0, 255) 113 | kernel = np.ones((16, 16), np.uint8) 114 | pupil = cv2.morphologyEx(_layer_to_full_image(bin_pupil).astype('uint8'), cv2.MORPH_OPEN, kernel) 115 | x, y, c = 0, 0, 0 116 | for i in range(pupil.shape[0]): 117 | for j in range(pupil.shape[1]): 118 | if pupil[i, j, 0] > 0: 119 | x += i 120 | y += j 121 | c += 1 122 | return x / c, y / c 123 | 124 | 125 | def preprocess(image): 126 | """Preprocesses the image to enhance the process of finding the iris. Crops high values of the image and blurs it. 127 | 128 | :param image: Image of an eye 129 | :return: Preprocessed image 130 | """ 131 | img = image[:, :, 0].copy() 132 | img[img > 225] = 30 133 | return cv2.medianBlur(img, 21) 134 | 135 | 136 | def find_pupil_hough(img): 137 | """Finds the pupil using Hugh transform. 138 | 139 | :param img: Image of an eye 140 | :return: x, y coordinates of the centre of the pupil and its radius 141 | """ 142 | circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 20, 143 | param1=50, param2=30, minRadius=10, maxRadius=200) 144 | circles = np.uint16(np.around(circles)) 145 | return circles[0, 0][0], circles[0, 0][1], circles[0, 0][2] 146 | 147 | 148 | def find_iris_id(img, x, y, r): 149 | """Finds the iris in the image usind integro-differential operator. 150 | 151 | :param img: Image of an eye 152 | :param x: Starting x coordinate 153 | :param y: Starting y coordinate 154 | :param r: Starting radius 155 | :return: x, y coordinates of the centre of the iris and its radius 156 | """ 157 | x, y, r, l = find_segment(img, x, y, minr=max(int(1.25 * r), 100), 158 | sigma=5, center_margin=30, jump=5) 159 | x, y, r, l = find_segment(img, x, y, minr=r - 10, maxr=r + 10, 160 | sigma=2, center_margin=5, jump=1) 161 | return x, y, r 162 | 163 | 164 | # Example usage 165 | if __name__ == '__main__': 166 | data = load_utiris()['data'] 167 | image = cv2.imread(data[0]) 168 | 169 | img = preprocess(image) 170 | x, y, r = find_pupil_hough(img) 171 | x_iris, y_iris, r_iris = find_iris_id(img, x, y, r) 172 | show_segment(image, x, y, r, x_iris, y_iris, r_iris) 173 | --------------------------------------------------------------------------------