Skip to content

Commit fc61f32

Browse files
authored
Merge pull request #186 from worldcoin/dev
Dev merge
2 parents 645a5b5 + 459f16b commit fc61f32

13 files changed

Lines changed: 165 additions & 76 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ inherit = false
2222
ignore = ["D100", "D104", "D203", "D205", "D211", "D213", "D406", "D407", "D413"]
2323
match = ".*.py"
2424

25-
[tool.pytest]
26-
minversion = 6.0
25+
[tool.pytest.ini_options]
26+
minversion = "6.0"
2727
xfail_strict = true
2828
log_auto_indent = true
29-
30-
[tool.pytest.ini_options]
3129
testpaths = ["tests"]
3230

3331
[tool.black]

src/iris/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.9.7"
1+
__version__ = "1.9.8"

src/iris/nodes/eye_properties_estimation/bisectors_method.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,12 @@ def _calculate_perpendicular_bisectors(
9797
Returns:
9898
Tuple[np.ndarray, np.ndarray]: Calculated perpendicular bisectors.
9999
"""
100-
np.random.seed(142857)
100+
rng = np.random.RandomState(142857)
101101

102102
bisectors_first_points = np.empty([0, 2])
103103
bisectors_second_points = np.empty([0, 2])
104104
for _ in range(self.params.max_iterations):
105-
random_indices = np.random.choice(len(polygon), size=(self.params.num_bisectors, 2))
105+
random_indices = rng.choice(len(polygon), size=(self.params.num_bisectors, 2))
106106

107107
first_drawn_points = polygon[random_indices[:, 0]]
108108
second_drawn_points = polygon[random_indices[:, 1]]

src/iris/nodes/eye_properties_estimation/occlusion_calculator.py

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from typing import List, Tuple
1+
from typing import List
22

33
import numpy as np
44
from pydantic import Field
55

66
from iris.callbacks.callback_interface import Callback
77
from iris.io.class_configs import Algorithm
88
from iris.io.dataclasses import EyeCenters, EyeOcclusion, EyeOrientation, GeometryPolygons, NoiseMask
9-
from iris.utils import common, math
9+
from iris.utils import common
1010

1111

1212
class OcclusionCalculator(Algorithm):
@@ -52,7 +52,6 @@ def run(
5252
extrapolated_polygons (GeometryPolygons): Extrapolated polygons contours.
5353
noise_mask (NoiseMask): Noise mask.
5454
eye_orientation (EyeOrientation): Eye orientation angle.
55-
eye_centers (EyeCenters): Eye centers.
5655
5756
Returns:
5857
EyeOcclusion: Visible iris fraction.
@@ -78,10 +77,8 @@ def run(
7877
pads = np.array([(-offset[1], overflow[1]), (-offset[0], overflow[0])]).astype(int)
7978
offseted_noise_mask = np.pad(noise_mask.mask, pads)
8079

81-
xs2mask, ys2mask = self._get_quantile_points(extrapolated_polygons.iris_array, eye_orientation, eye_centers)
82-
iris_mask_quantile = common.contour_to_mask(
83-
np.column_stack([xs2mask, ys2mask]) - offset, mask_shape=total_mask_shape
84-
)
80+
iris_quantile_polygon = self._get_quantile_points(extrapolated_polygons.iris_array, eye_orientation)
81+
iris_mask_quantile = common.contour_to_mask(iris_quantile_polygon - offset, mask_shape=total_mask_shape)
8582
pupil_mask = common.contour_to_mask(extrapolated_polygons.pupil_array - offset, mask_shape=total_mask_shape)
8683
eyeball_mask = common.contour_to_mask(extrapolated_polygons.eyeball_array - offset, mask_shape=total_mask_shape)
8784
frame_mask = common.contour_to_mask(frame - offset, mask_shape=total_mask_shape)
@@ -97,46 +94,30 @@ def run(
9794
return EyeOcclusion(visible_fraction=visible_fraction)
9895

9996
def _get_quantile_points(
100-
self, iris_coords: np.ndarray, eye_orientation: EyeOrientation, eye_centers: EyeCenters
101-
) -> Tuple[np.ndarray, np.ndarray]:
97+
self,
98+
iris_coords: np.ndarray,
99+
eye_orientation: EyeOrientation,
100+
) -> np.ndarray:
102101
"""Get those iris's points which fall into a specified quantile.
103102
104103
Args:
105104
iris_coords (np.ndarray): Iris polygon coordinates.
106105
eye_orientation: (EyeOrientation): Eye orientation.
107-
eye_centers: (EyeCenters): Eye centers.
108106
109107
Returns:
110-
Tuple[np.ndarray, np.ndarray]: Tuple with xs and ys that falls into quantile region.
108+
np.ndarray: Iris polygon coordinates that fall within the quantile regions.
111109
"""
112110
orientation_angle = np.degrees(eye_orientation.angle)
113111
num_rotations = -round(orientation_angle * len(iris_coords) / 360.0)
114-
115-
iris_xs, iris_ys = iris_coords[:, 0], iris_coords[:, 1]
116-
iris_rhos, iris_phis = math.cartesian2polar(iris_xs, iris_ys, eye_centers.iris_x, eye_centers.iris_y)
117-
118-
iris_phis = np.roll(iris_phis, num_rotations, axis=0)
119-
iris_rhos = np.roll(iris_rhos, num_rotations, axis=0)
112+
iris_coords = np.roll(iris_coords, num_rotations, axis=0)
120113

121114
scaled_quantile = round(self.params.quantile_angle * len(iris_coords) / 360.0)
122-
123-
phis2mask = np.concatenate(
124-
[
125-
iris_phis[:scaled_quantile],
126-
iris_phis[-scaled_quantile:],
127-
iris_phis[len(iris_phis) // 2 : len(iris_phis) // 2 + scaled_quantile],
128-
iris_phis[len(iris_phis) // 2 - scaled_quantile : len(iris_phis) // 2],
129-
]
130-
)
131-
rhos2mask = np.concatenate(
115+
iris_quantile_coords = np.concatenate(
132116
[
133-
iris_rhos[:scaled_quantile],
134-
iris_rhos[-scaled_quantile:],
135-
iris_rhos[len(iris_rhos) // 2 : len(iris_rhos) // 2 + scaled_quantile],
136-
iris_rhos[len(iris_rhos) // 2 - scaled_quantile : len(iris_rhos) // 2],
117+
iris_coords[0:scaled_quantile],
118+
iris_coords[len(iris_coords) // 2 - scaled_quantile : len(iris_coords) // 2 + scaled_quantile],
119+
iris_coords[len(iris_coords) - scaled_quantile :],
137120
]
138121
)
139-
phis2mask, rhos2mask = zip(*sorted(zip(phis2mask, rhos2mask)))
140-
xs2mask, ys2mask = math.polar2cartesian(rhos2mask, phis2mask, eye_centers.iris_x, eye_centers.iris_y)
141122

142-
return xs2mask, ys2mask
123+
return iris_quantile_coords

src/iris/nodes/geometry_estimation/fusion_extrapolation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def run(self, input_polygons: GeometryPolygons, eye_center: EyeCenters) -> Geome
113113
circle_poly = self.params.circle_extrapolation(input_polygons, eye_center)
114114
ellipse_poly = self.params.ellipse_fit(input_polygons)
115115

116+
if ellipse_poly is None:
117+
return circle_poly
118+
116119
circle_iris = circle_poly.iris_array
117120
circle_pupil = circle_poly.pupil_array
118121
ellipse_iris = ellipse_poly.iris_array

src/iris/nodes/geometry_estimation/lsq_ellipse_fit_with_refinement.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List
1+
from typing import List, Union
22

33
import cv2
44
import numpy as np
@@ -33,45 +33,89 @@ def __init__(self, dphi: float = 1.0, callbacks: List[Callback] = []) -> None:
3333
"""
3434
super().__init__(dphi=dphi, callbacks=callbacks)
3535

36-
def run(self, input_polygons: GeometryPolygons) -> GeometryPolygons:
36+
def run(self, input_polygons: GeometryPolygons) -> Union[GeometryPolygons, None]:
3737
"""Estimate extrapolated polygons with OpenCV's method fitEllipse.
3838
3939
Args:
4040
input_polygons (GeometryPolygons): Smoothed polygons.
4141
4242
Returns:
43-
GeometryPolygons: Extrapolated polygons.
43+
Union[GeometryPolygons, None]: Extrapolated polygons or None if pupil is not inside iris.
4444
"""
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
4749

4850
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
5054

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)
5486

55-
def _extrapolate(self, polygon_points: np.ndarray) -> np.ndarray:
87+
def _extrapolate(self, input_polygons: GeometryPolygons) -> Union[GeometryPolygons, None]:
5688
"""Perform extrapolation for points in an array.
5789
5890
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.
6092
6193
Returns:
62-
np.ndarray: Estimated extrapolated polygon.
94+
Union[GeometryPolygons, None]: Extrapolated polygons or None if pupil is not inside iris.
6395
"""
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)
6598

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)
68106
)
69107

70108
# 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)
73113

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+
)
75119

76120
def _find_correspondence(self, src_point: np.ndarray, dst_points: np.ndarray) -> int:
77121
"""Find correspondence with Euclidean distance.

src/iris/pipelines/confs/multiframe_iris_pipeline.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
metadata:
22
pipeline_name: multiframe_iris_pipeline
3-
iris_version: 1.9.7
3+
iris_version: 1.9.8
44

55
# Configuration for individual image processing (IRISPipeline)
66
iris_pipeline:
77
metadata:
88
pipeline_name: iris_pipeline
9-
iris_version: 1.9.7
9+
iris_version: 1.9.8
1010

1111
pipeline:
1212
- name: segmentation
@@ -315,7 +315,7 @@ iris_pipeline:
315315
# Configuration for template aggregation (TemplatesAggregationPipeline)
316316
templates_aggregation_pipeline:
317317
metadata:
318-
iris_version: 1.9.7
318+
iris_version: 1.9.8
319319
pipeline_name: templates_aggregation
320320

321321
pipeline:

src/iris/pipelines/confs/pipeline.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
metadata:
22
pipeline_name: iris_pipeline
3-
iris_version: 1.9.7
3+
iris_version: 1.9.8
44

55
pipeline:
66
- name: segmentation

src/iris/pipelines/confs/templates_aggregation_pipeline.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
metadata:
22
pipeline_name: iris_pipeline
3-
iris_version: 1.9.7
3+
iris_version: 1.9.8
44

55
pipeline:
66
- name: segmentation
@@ -307,7 +307,7 @@ pipeline:
307307

308308
templates_aggregation:
309309
metadata:
310-
iris_version: 1.9.7
310+
iris_version: 1.9.8
311311
pipeline_name: templates_aggregation
312312

313313
pipeline:
Binary file not shown.

0 commit comments

Comments
 (0)