├── .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 |
6 |
10 |
14 |
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 |
--------------------------------------------------------------------------------