├── .gitignore ├── images ├── app.gif ├── eye.jpg ├── output.png ├── ir_conv.png ├── color_conv.png ├── pupil_steps.jpg ├── 2019-11-22-17-33-27_Color.jpeg └── 2019-11-22-17-33-27_Infrared.jpeg ├── README.md └── pupil_dilation.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /images/app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/app.gif -------------------------------------------------------------------------------- /images/eye.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/eye.jpg -------------------------------------------------------------------------------- /images/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/output.png -------------------------------------------------------------------------------- /images/ir_conv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/ir_conv.png -------------------------------------------------------------------------------- /images/color_conv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/color_conv.png -------------------------------------------------------------------------------- /images/pupil_steps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/pupil_steps.jpg -------------------------------------------------------------------------------- /images/2019-11-22-17-33-27_Color.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/2019-11-22-17-33-27_Color.jpeg -------------------------------------------------------------------------------- /images/2019-11-22-17-33-27_Infrared.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnowledgePending/Pupil-Dilation-Measurement/HEAD/images/2019-11-22-17-33-27_Infrared.jpeg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 6 | 10 | 14 |
15 | 16 | # Pupil Dilation Measurement using Near Infrared 17 | ### 👁️🔬📏 Pupil Dilation Measurement without Machine Learning 18 |
19 | You can see the code running with more examples at the colab link below 20 |
21 | 22 |
23 | 24 | 25 | ### Prerequisites 26 | ``` 27 | pip3 install opencv-contrib-python numpy 28 | ``` 29 | ## Algorithm: 30 | 1. Load Color and Infrared Images 31 | 2. Threshold Infrared Image to help locate eyes 32 | 3. Get infrared co-ordinates for an eye 33 | 4. Convert co-ordinates to lie on Color Plane 34 | 5. Convert image to YIQ for skin detection 35 | 6. Use this to verify that co-ordinates point to an eye 36 | 7. Create a sub-image containing only the eye 37 | 8. Detect Pupil and Iris using Circle Hough 38 | 9. Deduce ratio and dilation from measurements 39 | 10. Display Stats, Graphs and Images 40 | 41 | ### Steps as Images 42 | 43 | 44 | ## How Images Were Captured 45 | I wrote a C# application to capture from both the Front Camera and Backlit Near-Infrared Camera simultaneously. 46 | An early version of my code can be found [here](https://github.com/KnowledgePending/OpenCV_Surface_Near_Infrared). 47 | 48 | 49 | 50 | ## Output of the Code 51 | Along with the actually measurement we display key stats and visualisations. 52 | 53 | 54 | ## Contributors 55 | This was created as team project. 56 | Authors: [Bryan](https://github.com/KnowledgePending), [Cian](https://github.com/CMorar143) and [Val](https://github.com/ValentinCiceu) -------------------------------------------------------------------------------- /pupil_dilation.py: -------------------------------------------------------------------------------- 1 | ## Program Description: Pupil Dilation Measurement 2 | 3 | # Running the Code: 4 | # Simply change the file path to point to the location of your image 5 | # This program outputs images and data on one screen 6 | 7 | 8 | ## Algorithm 9 | # 1. Load Color and Infrared Images 10 | # 2. Threshold Infrared Image to help locate eyes 11 | # 3. Get infrared co-ordinates for an eye 12 | # 4. Convert co-ordinates to lie on Color Plane 13 | # 5. Convert image to YIQ for skin detection 14 | # 6. Use this to verify that co-ordinates point to an eye 15 | # 7. Create a sub-image containing only the eye 16 | # 8. Detect Pupil and Iris using Circle Hough 17 | # 9. Deduce ratio and dilation from measurements 18 | # 10.Display Stats, Graphs and Images 19 | 20 | import cv2 21 | import numpy as np 22 | import matplotlib.pyplot as plt 23 | from fractions import Fraction 24 | 25 | def get_infrared_eye_coords(binary_image): 26 | # Derive coords from mask 27 | y_index, x_index = np.where(binary_image != 0) 28 | eye_x = x_index[0] 29 | eye_y = y_index[0] 30 | return eye_x, eye_y 31 | 32 | def threshold_infrared_for_coords(image): 33 | # Threshold for eyes 34 | mask = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 35 | mask[mask < 235] = 0 36 | mask[mask != 0] = 255 37 | eye_x, eye_y = get_infrared_eye_coords(mask) 38 | return eye_x, eye_y, mask 39 | 40 | # Convert Infrared Coordinates to lie on Color Plane 41 | def infrared_to_color(x, y): 42 | if y < 175 or y > 472 or x > 470 or x < 20 : 43 | raise IndexError("Coordinates do not lie on Color Image") 44 | 45 | x -= 20 46 | x = int(x/.275) 47 | x += 210 48 | 49 | y -= 175 50 | y = int(y/.275) 51 | 52 | return x, y 53 | 54 | # This detects and measures pupil 55 | def detect_and_measure_pupil(infrared_image_input, color_image_input): 56 | if infrared_image_input is None or color_image_input is None : 57 | print("2 non-empty images required") 58 | return False 59 | infrared_image = infrared_image_input.copy() 60 | color_image = color_image_input.copy() 61 | 62 | # Threshold for eyes 63 | try: 64 | infrared_eye_x, infrared_eye_y, image_mask = threshold_infrared_for_coords(infrared_image) 65 | except: 66 | print("Unable to create Mask from infrared image") 67 | return False 68 | 69 | # Convert Infrared Coordinates to lie on Color Plane 70 | try: 71 | color_eye_x, color_eye_y = infrared_to_color(infrared_eye_x, infrared_eye_y) 72 | except IndexError: 73 | print("Coordinates do not lie on Color Image") 74 | return False 75 | 76 | return color_eye_x, color_eye_y, infrared_eye_x, infrared_eye_y, image_mask 77 | 78 | 79 | 80 | 81 | def get_eye_bounding_box(image, eye_x, eye_y, offset=80): 82 | validation = image.copy() 83 | return validation[eye_y - offset : eye_y + offset, eye_x - offset : eye_x + offset], eye_x - offset, eye_y - offset 84 | 85 | 86 | def yiq_conversion(rgb_vector): 87 | height, width, _ = np.shape(rgb_vector) 88 | 89 | rgb_vector = np.asarray(rgb_vector) 90 | yiq_matrix = np.array([[0.299 , 0.587 , 0.114 ], 91 | [0.5959 , -0.2746 , -0.3213 ], 92 | [0.2115 , -0.5227 , 0.3112 ]]) 93 | b = rgb_vector[:, :, 0] 94 | g = rgb_vector[:, :, 1] 95 | r = rgb_vector[:, :, 2] 96 | 97 | for x in range(0, width): 98 | for y in range(0,height): 99 | rgb_vector[x][y] = yiq_matrix.dot([b[x,y],g[x,y],r[x,y]]) 100 | return rgb_vector 101 | 102 | 103 | def median_color_of_image(image): 104 | mask = image.copy() 105 | median_array = np.asarray(mask) 106 | sort = np.sort(median_array) 107 | medians = np.median(sort, axis=0) 108 | return medians[len(medians) // 2] 109 | 110 | 111 | def draw_median_circle(image, eye_x, eye_y, median): 112 | median_image = image.copy() 113 | cv2.circle(median_image, (eye_x, eye_y), 40, (int(median[0]), int(median[2]), int(median[1])), -1) 114 | return median_image 115 | 116 | 117 | def validate_region(image): 118 | mean, std = cv2.meanStdDev(image) 119 | 120 | if mean[1] > 200 and mean[0] < 100 and mean[2] < 50: 121 | message = "The surrounding region is dominated by skin, therefore it has a good chance of being the eye" 122 | return True, mean, std, message 123 | else: 124 | message = "Region is not dominant by skin therefore poor chance of being eye region" 125 | return False, mean, std, message 126 | 127 | def validator(image, eye_x, eye_y): 128 | original = image.copy() 129 | 130 | bounded_image, cropped_x, cropped_y = get_eye_bounding_box(original, eye_x, eye_y) 131 | 132 | yiq_image = yiq_conversion(bounded_image) 133 | 134 | median_color = median_color_of_image(yiq_image) 135 | 136 | bounded = draw_median_circle(yiq_image, eye_x - cropped_x, eye_y - cropped_y, median_color) 137 | 138 | validation, mean, std, message = validate_region(bounded) 139 | 140 | return validation, mean, std, message, yiq_image 141 | 142 | 143 | # Crops down to a 250x250 region centred on the eye given by Bryan's code 144 | def crop_to_eye(image, cian_x, cian_y): 145 | return image[cian_y-125:cian_y+125, cian_x-125:cian_x+125] 146 | 147 | # Convert an image to grayscale and apply a blur for the Circle Hough Transform 148 | def blur_grayscale(image, blur): 149 | blurred_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 150 | return cv2.medianBlur(blurred_image, blur) 151 | 152 | # Detect the iris or pupil 153 | def draw_detection(image, detection): 154 | try: 155 | detection = np.uint16(np.around(detection)) 156 | x, y, r = detection[0, 0] 157 | cv2.circle(image, (x, y), 2, (0, 0, 255), -1) 158 | cv2.circle(image, (x, y), r, (255, 0, 0), 1) 159 | except: 160 | print("No circles were found") 161 | 162 | return image, x, y, r 163 | 164 | # Calculate the ratio, simplify it and then return the numerator and denominator 165 | def get_ratio(pr, ir): 166 | # Radius in millimeters 167 | iris_mm = 6.5 168 | 169 | ratio = Fraction(pr, ir) 170 | pupil_px = ratio.numerator 171 | iris_px = ratio.denominator 172 | 173 | # Using the ratio of millimeters : pixels for the iris, 174 | # we can use this factor to find the size in mm of the pupil 175 | # since we know its size in pixels 176 | px_to_mm_factor = iris_mm / iris_px 177 | pupil_mm = pr * px_to_mm_factor 178 | 179 | return pupil_px, iris_px, pupil_mm, iris_mm 180 | 181 | # This detects the iris and pupil and returns the final image 182 | # It takes in the original color image and the x and y co-ordinates returned from Bryan's and Valentin's code 183 | def detect_iris_and_pupil(image, cian_x, cian_y): 184 | if image is None: 185 | print("Image is empty") 186 | return 187 | 188 | # Crop down to the eye region 189 | cropped_image = crop_to_eye(image, cian_x, cian_y) 190 | 191 | # Use one image for detecting the iris, pupil 192 | iris_detection = cropped_image.copy() 193 | pupil_detection = cropped_image.copy() 194 | final_detection = cropped_image.copy() 195 | 196 | # Find the iris 197 | gray_cropped = blur_grayscale(iris_detection, 17) 198 | iris = cv2.HoughCircles(gray_cropped, cv2.HOUGH_GRADIENT, 1, iris_detection.shape[0], param1=50, param2=20, minRadius=0, maxRadius=48) 199 | iris_detection, ix, iy, ir = draw_detection(iris_detection, iris) 200 | 201 | # Find the pupil 202 | gray_cropped = blur_grayscale(pupil_detection, 19) 203 | pupil = cv2.HoughCircles(gray_cropped, cv2.HOUGH_GRADIENT, 1, pupil_detection.shape[0], param1=50, param2=15, minRadius=0, maxRadius=ir-4) 204 | pupil_detection, px, py, pr = draw_detection(pupil_detection, pupil) 205 | 206 | # draw the final detection 207 | cv2.circle(final_detection, (ix, iy), 2, (0, 0, 255), 2) 208 | cv2.circle(final_detection, (ix, iy), ir, (255, 0, 0), 2) 209 | cv2.circle(final_detection, (px, py), pr, (255, 0, 0), 2) 210 | 211 | # Get ratio in pixels and millimeters 212 | pupil_px, iris_px, pupil_mm, iris_mm = get_ratio(pr, ir) 213 | 214 | return final_detection, pupil_px, iris_px, pupil_mm, iris_mm, gray_cropped 215 | 216 | # Plot code here 217 | def display_results(infrared, image_mask, yiq_image, mean, cropped_normal_eye, grey_cropped, final_detection, eye_x, eye_y, message, pupil_px, iris_px, pupil_mm, iris_mm): 218 | # ploting the image 219 | plt.figure(figsize=(30,20)) 220 | grid = plt.GridSpec(3, 3) 221 | plt.title("Step by step process") 222 | plt.subplot(grid[0,0]) 223 | plt.imshow(cv2.cvtColor(infrared , cv2.COLOR_BGR2RGB)) 224 | plt.xlabel('Infrared image') 225 | 226 | 227 | plt.subplot(grid[0,1]) 228 | plt.imshow(image_mask) 229 | plt.xlabel('mask of the infrared image') 230 | 231 | plt.subplot(grid[0,2]) 232 | plt.imshow(cv2.cvtColor(yiq_image , cv2.COLOR_BGR2RGB)) 233 | plt.xlabel('YIQ of the cropped section of the eye') 234 | 235 | # this is for the bar chart of the mean 236 | names = ["Blue scale" , "Green scale" , "Red scale"] 237 | values = np.ravel(mean) 238 | plt.subplot(grid[1,0]) 239 | plt.bar(names, values, color=["blue" , "green" , "red"]) 240 | plt.xlabel('Colours') 241 | plt.ylabel('Colour scale') 242 | 243 | plt.subplot(grid[1,1]) 244 | plt.imshow(cropped_normal_eye) 245 | plt.xlabel('Cropped normal eye') 246 | 247 | plt.subplot(grid[1,2]) 248 | plt.imshow(cv2.cvtColor(grey_cropped , cv2.COLOR_GRAY2RGB)) 249 | plt.xlabel('Grey eye') 250 | 251 | 252 | plt.subplot(grid[2,0]) 253 | plt.imshow(cv2.cvtColor(final_detection , cv2.COLOR_BGR2RGB)) 254 | plt.xlabel('Circle Hough') 255 | 256 | plt.subplot(grid[2, 1:3]) 257 | plt.axis("off") 258 | plt.xlabel('') 259 | plt.ylabel('') 260 | plt.text(0, 0.5, f'Eye co-ordinate: X{eye_x} , Y{eye_y}\n{message}\nRatio of dilation in pixels is {pupil_px}px : {iris_px}px\nRatio of dilation in mm is {pupil_mm}mm : {iris_mm}mm', style='italic', 261 | bbox={'facecolor': 'blue', 'alpha': 0.5, 'pad': 20}, fontsize=10) 262 | plt.show() 263 | 264 | def measure_dilation(infrared_path="./images/2019-11-22-17-33-27_Infrared.jpeg", color_path="./images/2019-11-22-17-33-27_Color.jpeg"): 265 | infrared = cv2.imread(infrared_path, 1) 266 | color = cv2.imread(color_path, 1) 267 | 268 | eye_x, eye_y, infrared_eye_x, infrared_eye_y, image_mask = detect_and_measure_pupil(infrared, color) 269 | 270 | validation, mean, std, message, yiq_image = validator(color, eye_x, eye_y) 271 | if validation is False: 272 | print("Doesn't pass validation") 273 | return False 274 | 275 | cropped_normal_eye = infrared[int(infrared_eye_y*.9):int(infrared_eye_y*1.1), int(infrared_eye_x*.9):int(infrared_eye_x*1.1)] 276 | 277 | final_detection, pupil_px, iris_px, pupil_mm, iris_mm, grey_cropped = detect_iris_and_pupil(color, eye_x, eye_y) 278 | display_results(infrared, image_mask, yiq_image, mean, cropped_normal_eye, grey_cropped, final_detection, eye_x, eye_y, message, pupil_px, iris_px, pupil_mm, iris_mm) 279 | 280 | 281 | 282 | 283 | measure_dilation() 284 | --------------------------------------------------------------------------------