|
1 | | -from typing import List |
| 1 | +from typing import List, Union |
2 | 2 |
|
3 | 3 | import cv2 |
4 | 4 | import numpy as np |
@@ -33,45 +33,89 @@ def __init__(self, dphi: float = 1.0, callbacks: List[Callback] = []) -> None: |
33 | 33 | """ |
34 | 34 | super().__init__(dphi=dphi, callbacks=callbacks) |
35 | 35 |
|
36 | | - def run(self, input_polygons: GeometryPolygons) -> GeometryPolygons: |
| 36 | + def run(self, input_polygons: GeometryPolygons) -> Union[GeometryPolygons, None]: |
37 | 37 | """Estimate extrapolated polygons with OpenCV's method fitEllipse. |
38 | 38 |
|
39 | 39 | Args: |
40 | 40 | input_polygons (GeometryPolygons): Smoothed polygons. |
41 | 41 |
|
42 | 42 | Returns: |
43 | | - GeometryPolygons: Extrapolated polygons. |
| 43 | + Union[GeometryPolygons, None]: Extrapolated polygons or None if pupil is not inside iris. |
44 | 44 | """ |
45 | | - extrapolated_pupil = self._extrapolate(input_polygons.pupil_array) |
46 | | - extrapolated_iris = self._extrapolate(input_polygons.iris_array) |
| 45 | + extrapolated_polygons = self._extrapolate(input_polygons) |
| 46 | + |
| 47 | + if extrapolated_polygons is None: |
| 48 | + return None |
47 | 49 |
|
48 | 50 | for point in input_polygons.pupil_array: |
49 | | - extrapolated_pupil[self._find_correspondence(point, extrapolated_pupil)] = point |
| 51 | + extrapolated_polygons.pupil_array[ |
| 52 | + self._find_correspondence(point, extrapolated_polygons.pupil_array) |
| 53 | + ] = point |
50 | 54 |
|
51 | | - return GeometryPolygons( |
52 | | - pupil_array=extrapolated_pupil, iris_array=extrapolated_iris, eyeball_array=input_polygons.eyeball_array |
53 | | - ) |
| 55 | + return extrapolated_polygons |
| 56 | + |
| 57 | + def _is_pupil_inside_iris_ellipses( |
| 58 | + self, px0: float, py0: float, pa: float, pb: float, ix0: float, iy0: float, ia: float, ib: float |
| 59 | + ) -> bool: |
| 60 | + """Fast conservative check using bounding circles. |
| 61 | +
|
| 62 | + Args: |
| 63 | + px0 (float): elliptical fit pupil center x-coordinate |
| 64 | + py0 (float): elliptical fit pupil center y-coordinate |
| 65 | + pa (float): elliptical fit pupil major axis length |
| 66 | + pb (float): elliptical fit pupil minor axis length |
| 67 | + ix0 (float): elliptical fit iris center x-coordinate |
| 68 | + iy0 (float): elliptical fit iris center y-coordinate |
| 69 | + ia (float): elliptical fit iris major axis length |
| 70 | + ib (float): elliptical fit iris minor axis length |
| 71 | +
|
| 72 | + Returns: |
| 73 | + bool: True if pupil is likely inside iris (conservative estimate) |
| 74 | + """ |
| 75 | + # Use max radius for pupil (worst case) and min radius for iris (best case) |
| 76 | + pupil_max_radius = max(pa, pb) / 2 |
| 77 | + iris_min_radius = min(ia, ib) / 2 |
| 78 | + |
| 79 | + # Distance between centers |
| 80 | + center_dist = np.sqrt((px0 - ix0) ** 2 + (py0 - iy0) ** 2) |
| 81 | + |
| 82 | + # Conservative check: pupil's max reach < iris's min reach |
| 83 | + # Add small safety margin (e.g., 5%) |
| 84 | + safety_factor = 0.95 |
| 85 | + return (center_dist + pupil_max_radius) < (iris_min_radius * safety_factor) |
54 | 86 |
|
55 | | - def _extrapolate(self, polygon_points: np.ndarray) -> np.ndarray: |
| 87 | + def _extrapolate(self, input_polygons: GeometryPolygons) -> Union[GeometryPolygons, None]: |
56 | 88 | """Perform extrapolation for points in an array. |
57 | 89 |
|
58 | 90 | Args: |
59 | | - polygon_points (np.ndarray): Smoothed polygon ready for applying extrapolation algorithm on it. |
| 91 | + polygon_points (np.ndarray): Smoothed polygons ready for applying extrapolation algorithm on it. |
60 | 92 |
|
61 | 93 | Returns: |
62 | | - np.ndarray: Estimated extrapolated polygon. |
| 94 | + Union[GeometryPolygons, None]: Extrapolated polygons or None if pupil is not inside iris. |
63 | 95 | """ |
64 | | - (x0, y0), (a, b), theta = cv2.fitEllipse(polygon_points) |
| 96 | + (px0, py0), (pa, pb), ptheta = cv2.fitEllipse(input_polygons.pupil_array) |
| 97 | + (ix0, iy0), (ia, ib), itheta = cv2.fitEllipse(input_polygons.iris_array) |
65 | 98 |
|
66 | | - extrapolated_polygon = LSQEllipseFitWithRefinement.parametric_ellipsis( |
67 | | - a / 2, b / 2, x0, y0, np.radians(theta), round(360 / self.params.dphi) |
| 99 | + if not self._is_pupil_inside_iris_ellipses(px0, py0, pa, pb, ix0, iy0, ia, ib): |
| 100 | + return None |
| 101 | + extrapolated_pupil_polygon = LSQEllipseFitWithRefinement.parametric_ellipsis( |
| 102 | + pa / 2, pb / 2, px0, py0, np.radians(ptheta), round(360 / self.params.dphi) |
| 103 | + ) |
| 104 | + extrapolated_iris_polygon = LSQEllipseFitWithRefinement.parametric_ellipsis( |
| 105 | + ia / 2, ib / 2, ix0, iy0, np.radians(itheta), round(360 / self.params.dphi) |
68 | 106 | ) |
69 | 107 |
|
70 | 108 | # Rotate such that 0 degree is parallel with x-axis and array is clockwise |
71 | | - roll_amount = round((-theta - 90) / self.params.dphi) |
72 | | - extrapolated_polygon = np.flip(np.roll(extrapolated_polygon, roll_amount, axis=0), axis=0) |
| 109 | + roll_amount = round((-ptheta - 90) / self.params.dphi) |
| 110 | + extrapolated_pupil_polygon = np.flip(np.roll(extrapolated_pupil_polygon, roll_amount, axis=0), axis=0) |
| 111 | + roll_amount = round((-itheta - 90) / self.params.dphi) |
| 112 | + extrapolated_iris_polygon = np.flip(np.roll(extrapolated_iris_polygon, roll_amount, axis=0), axis=0) |
73 | 113 |
|
74 | | - return extrapolated_polygon |
| 114 | + return GeometryPolygons( |
| 115 | + pupil_array=extrapolated_pupil_polygon, |
| 116 | + iris_array=extrapolated_iris_polygon, |
| 117 | + eyeball_array=input_polygons.eyeball_array, |
| 118 | + ) |
75 | 119 |
|
76 | 120 | def _find_correspondence(self, src_point: np.ndarray, dst_points: np.ndarray) -> int: |
77 | 121 | """Find correspondence with Euclidean distance. |
|
0 commit comments