PAROS 1.0.2__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paros-2.0.0/.github/assets/screenshot.png +0 -0
- {paros-1.0.2 → paros-2.0.0}/.github/workflows/ci.yml +1 -1
- paros-2.0.0/PAROS/data/calibration/Topcon/TRC_50DX.csv +10 -0
- paros-2.0.0/PAROS/data/calibration/Topcon/TRC_50IX.csv +11 -0
- {paros-1.0.2 → paros-2.0.0}/PAROS/eyephantom.py +0 -2
- {paros-1.0.2 → paros-2.0.0}/PAROS/fundscale.py +268 -65
- {paros-1.0.2 → paros-2.0.0}/PKG-INFO +13 -6
- {paros-1.0.2 → paros-2.0.0}/README.md +10 -4
- {paros-1.0.2 → paros-2.0.0}/examples/calculate_ocular_magnification.ipynb +75 -79
- {paros-1.0.2 → paros-2.0.0}/examples/fundus_camera_calibration.ipynb +25 -62
- {paros-1.0.2 → paros-2.0.0}/pyproject.toml +9 -2
- {paros-1.0.2 → paros-2.0.0}/uv.lock +5 -230
- paros-1.0.2/.github/assets/screenshot.png +0 -0
- paros-1.0.2/PAROS/data/calibration/Topcon/TRC_50DX.csv +0 -10
- paros-1.0.2/PAROS/data/calibration/Topcon/TRC_50IX.csv +0 -11
- {paros-1.0.2 → paros-2.0.0}/.github/workflows/release.yml +0 -0
- {paros-1.0.2 → paros-2.0.0}/.gitignore +0 -0
- {paros-1.0.2 → paros-2.0.0}/CITATION.cff +0 -0
- {paros-1.0.2 → paros-2.0.0}/CONTRIBUTING.md +0 -0
- {paros-1.0.2 → paros-2.0.0}/LICENSE +0 -0
- {paros-1.0.2 → paros-2.0.0}/PAROS/__init__.py +0 -0
- {paros-1.0.2 → paros-2.0.0}/PAROS/data/__init__.py +0 -0
- {paros-1.0.2 → paros-2.0.0}/PAROS/data/calibration/__init__.py +0 -0
- {paros-1.0.2 → paros-2.0.0}/PAROS/data/lenses/__init__.py +0 -0
- {paros-1.0.2 → paros-2.0.0}/PAROS/data/lenses/phantom.csv +0 -0
- {paros-1.0.2 → paros-2.0.0}/ruff_defaults.toml +0 -0
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
experiment_number,lens_1,lens_2,image_size_px,distance_camera_eye_mm,distance_camera_pinhole_mm,distance_pinhole_lens1_mm,distance_lens1_lens2_mm,distance_lens2_grid_mm,axial_length
|
|
2
|
+
0,LB_1471,LB_1945,696.6666667,55,49.61969784,2.880302163,3.027080745,29.69261709,40.71969784
|
|
3
|
+
1,LB_1471,LB_1945,672.6666667,55,49.61969784,2.880302163,3.027080745,31.69261709,42.71969784
|
|
4
|
+
2,LB_1471,LB_1945,660.0,55,49.61969784,2.880302163,3.027080745,33.69261709,44.71969784
|
|
5
|
+
3,LB_1471,LB_1945,637.0,55,49.61969784,2.880302163,3.027080745,35.69261709,46.71969784
|
|
6
|
+
4,LB_1471,LB_1945,625.0,55,49.61969784,2.880302163,3.027080745,37.69261709,48.71969784
|
|
7
|
+
5,LB_1471,LB_1945,608.6666667,55,49.61969784,2.880302163,3.027080745,39.69261709,50.71969784
|
|
8
|
+
6,LB_1471,LB_1945,597.3333333,55,49.61969784,2.880302163,3.027080745,41.69261709,52.71969784
|
|
9
|
+
7,LB_1471,LB_1945,574.6666667,55,49.61969784,2.880302163,3.027080745,43.69261709,54.71969784
|
|
10
|
+
8,LB_1471,LB_1945,561.3333333,55,49.61969784,2.880302163,3.027080745,45.39261709,56.41969784
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
lens_1,lens_2,axial_length,distance_camera_eye_mm,image_size_px,distance_camera_pinhole_mm,distance_pinhole_lens1_mm,distance_lens1_lens2_mm,distance_lens2_grid_mm,experiment_number
|
|
2
|
+
LE_1156,LB_1945,83.9874534,55,331.0,50.2374534,2.262546596,6.344836312,71.54261709,0
|
|
3
|
+
LE_1156,LB_1945,90.7374534,55,313.0,50.2374534,2.262546596,6.344836312,78.29261709,1
|
|
4
|
+
LE_1156,LB_1945,78.0374534,55,346.0,50.2374534,2.262546596,6.344836312,65.59261709,2
|
|
5
|
+
LE_1156,LB_1945,134.6374534,55,224.0,50.2374534,2.262546596,6.344836312,122.1926171,3
|
|
6
|
+
LE_1156,LB_1945,107.9374534,55,269.0,50.2374534,2.262546596,6.344836312,95.49261709,4
|
|
7
|
+
LE_1156,LB_1945,72.0374534,55,371.0,50.2374534,2.262546596,6.344836312,59.59261709,5
|
|
8
|
+
LE_1156,LB_1945,63.6374534,55,408.0,50.2374534,2.262546596,6.344836312,51.19261709,6
|
|
9
|
+
LE_1234,LB_1056,62.51915876,50,428.0,45.31915876,2.180841236,5.205295461,51.1138633,7
|
|
10
|
+
LE_1234,LB_1056,75.91915876,50,366.0,45.31915876,2.180841236,5.205295461,64.5138633,8
|
|
11
|
+
LE_1234,LB_1056,96.51915876,50,298.0,45.31915876,2.180841236,5.205295461,85.1138633,9
|
|
@@ -24,20 +24,32 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import copy
|
|
26
26
|
import math
|
|
27
|
-
from
|
|
27
|
+
from sys import version_info
|
|
28
|
+
from typing import Literal, NamedTuple, Union, cast
|
|
28
29
|
from warnings import warn
|
|
29
30
|
|
|
30
31
|
import numpy as np
|
|
31
32
|
import sympy as sp
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
if version_info <= (3, 11):
|
|
35
|
+
from typing_extensions import NotRequired, TypedDict, Unpack
|
|
36
|
+
else:
|
|
37
|
+
from typing import NotRequired, TypedDict, Unpack
|
|
34
38
|
|
|
35
|
-
NumberOrSymbol = Union[int, float, sp.Symbol]
|
|
36
|
-
EyeModelType = Literal["Navarro", "VughtIOL"]
|
|
37
39
|
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Eye",
|
|
42
|
+
"Camera",
|
|
43
|
+
"calculate_magnification",
|
|
44
|
+
"spherical_interface",
|
|
45
|
+
"uniform_medium",
|
|
46
|
+
"medium_change",
|
|
47
|
+
"calculate_piol_curvature",
|
|
48
|
+
"calculate_piol_matrix",
|
|
49
|
+
]
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
NumberOrSymbol = Union[int, float, sp.Symbol]
|
|
52
|
+
EyeModelType = Literal["Navarro", "VughtIOL"]
|
|
41
53
|
|
|
42
54
|
|
|
43
55
|
def uniform_medium(thickness: float) -> sp.Matrix:
|
|
@@ -177,6 +189,67 @@ def calculate_piol_matrix(
|
|
|
177
189
|
return piol_matrix * distance_matrix
|
|
178
190
|
|
|
179
191
|
|
|
192
|
+
class EyeGeometry(TypedDict, total=True):
|
|
193
|
+
"""Eye geometry parameters.
|
|
194
|
+
|
|
195
|
+
All curvatures and distances are specified in meters and the spherical equivalent in diopters.
|
|
196
|
+
PAROS uses an inverted eye model, so for a normal eye the radii of curvature for the cornea and
|
|
197
|
+
lens front surfaces are negative.
|
|
198
|
+
|
|
199
|
+
Attributes
|
|
200
|
+
----------
|
|
201
|
+
R_corF : float
|
|
202
|
+
Cornea front radius.
|
|
203
|
+
R_corB : float
|
|
204
|
+
Cornea back radius.
|
|
205
|
+
R_lensF : float
|
|
206
|
+
Lens front radius.
|
|
207
|
+
R_lensB : float
|
|
208
|
+
Lens back radius.
|
|
209
|
+
D_cor : float
|
|
210
|
+
Cornea thickness.
|
|
211
|
+
D_ACD : float
|
|
212
|
+
Anterior chamber depth.
|
|
213
|
+
D_lens : float
|
|
214
|
+
Lens thickness.
|
|
215
|
+
D_vitr : float
|
|
216
|
+
Vitreous thickness.
|
|
217
|
+
SE : float
|
|
218
|
+
Spherical equivalent of refraction of the eye model.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
R_corF: float
|
|
222
|
+
R_corB: float
|
|
223
|
+
R_lensF: float
|
|
224
|
+
R_lensB: float
|
|
225
|
+
D_cor: float
|
|
226
|
+
D_ACD: float
|
|
227
|
+
D_lens: float
|
|
228
|
+
D_vitr: float
|
|
229
|
+
SE: NotRequired[float]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class _PartialEyeGeometry(EyeGeometry, total=False):
|
|
233
|
+
"""Eye geometry parameters.
|
|
234
|
+
|
|
235
|
+
This class can be used to define a partial eye geometry, in which only a subset
|
|
236
|
+
of the parameters are specified.
|
|
237
|
+
|
|
238
|
+
See Also
|
|
239
|
+
--------
|
|
240
|
+
EyeGeometry : Full eye geometry parameters.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
R_corF: float
|
|
244
|
+
R_corB: float
|
|
245
|
+
R_lensF: float
|
|
246
|
+
R_lensB: float
|
|
247
|
+
D_cor: float
|
|
248
|
+
D_ACD: float
|
|
249
|
+
D_lens: float
|
|
250
|
+
D_vitr: float
|
|
251
|
+
|
|
252
|
+
|
|
180
253
|
class PhakicIOL(NamedTuple):
|
|
181
254
|
"""Parameters of a phakic IOL.
|
|
182
255
|
|
|
@@ -198,15 +271,93 @@ class PhakicIOL(NamedTuple):
|
|
|
198
271
|
lens_distance: NumberOrSymbol
|
|
199
272
|
|
|
200
273
|
|
|
274
|
+
class RefractiveIndices(TypedDict):
|
|
275
|
+
"""Refractive indices of the eye media.
|
|
276
|
+
|
|
277
|
+
Attributes
|
|
278
|
+
----------
|
|
279
|
+
cor : float
|
|
280
|
+
Refractive index of the cornea.
|
|
281
|
+
aq : float
|
|
282
|
+
Refractive index of the aqueous humor.
|
|
283
|
+
lens : float
|
|
284
|
+
Refractive index of the crystalline lens.
|
|
285
|
+
vit : float
|
|
286
|
+
Refractive index of the vitreous humor.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
cor: float
|
|
290
|
+
aq: float
|
|
291
|
+
lens: float
|
|
292
|
+
vit: float
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class _PartialRefractiveIndices(RefractiveIndices, total=False):
|
|
296
|
+
"""Refractive indices of the eye media.
|
|
297
|
+
|
|
298
|
+
This class can be used to define a partial set of refractive indices, in which only a
|
|
299
|
+
subset of the indices are specified.
|
|
300
|
+
|
|
301
|
+
See Also
|
|
302
|
+
--------
|
|
303
|
+
RefractiveIndices : Full set of refractive indices.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
cor: float
|
|
307
|
+
aq: float
|
|
308
|
+
lens: float
|
|
309
|
+
vit: float
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
_DEFAULT_GEOMETRIES: dict[EyeModelType, EyeGeometry] = {
|
|
313
|
+
"Navarro": EyeGeometry(
|
|
314
|
+
R_corF=-7.72e-3,
|
|
315
|
+
R_corB=-6.50e-3,
|
|
316
|
+
R_lensF=-10.20e-3,
|
|
317
|
+
R_lensB=+6.00e-3,
|
|
318
|
+
D_cor=0.55e-3,
|
|
319
|
+
D_ACD=3.05e-3,
|
|
320
|
+
D_lens=4.00e-3,
|
|
321
|
+
D_vitr=16.3203e-3,
|
|
322
|
+
),
|
|
323
|
+
"VughtIOL": EyeGeometry(
|
|
324
|
+
R_corF=-7.72e-3,
|
|
325
|
+
R_corB=-6.50e-3,
|
|
326
|
+
R_lensF=-8.16e-3,
|
|
327
|
+
R_lensB=+11.18e-3,
|
|
328
|
+
D_cor=0.55e-3,
|
|
329
|
+
D_ACD=3.05e-3,
|
|
330
|
+
D_lens=0.6896e-3,
|
|
331
|
+
D_vitr=19.3203e-3,
|
|
332
|
+
),
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_DEFAULT_REFRACTIVE_INDICES: dict[EyeModelType, RefractiveIndices] = {
|
|
336
|
+
"Navarro": RefractiveIndices(
|
|
337
|
+
cor=1.3777,
|
|
338
|
+
aq=1.3391,
|
|
339
|
+
lens=1.4222,
|
|
340
|
+
vit=1.3377,
|
|
341
|
+
),
|
|
342
|
+
"VughtIOL": RefractiveIndices(
|
|
343
|
+
cor=1.3777,
|
|
344
|
+
aq=1.3391,
|
|
345
|
+
lens=1.47,
|
|
346
|
+
vit=1.3377,
|
|
347
|
+
),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
201
351
|
class Eye:
|
|
202
352
|
"""Symbolic representation of an eye model."""
|
|
203
353
|
|
|
204
354
|
def __init__(
|
|
205
355
|
self,
|
|
206
356
|
name: str = "testEye",
|
|
207
|
-
geometry:
|
|
357
|
+
geometry: _PartialEyeGeometry | EyeGeometry | None = None,
|
|
208
358
|
model_type: EyeModelType = "Navarro",
|
|
209
359
|
NType: EyeModelType = "Navarro", # noqa: N803
|
|
360
|
+
refractive_indices: _PartialRefractiveIndices | None = None,
|
|
210
361
|
refraction: float | None = None,
|
|
211
362
|
pIOL: PhakicIOL | None = None, # noqa: N803
|
|
212
363
|
) -> None:
|
|
@@ -222,7 +373,7 @@ class Eye:
|
|
|
222
373
|
----------
|
|
223
374
|
name : str
|
|
224
375
|
Name of the eye model.
|
|
225
|
-
geometry :
|
|
376
|
+
geometry : _PartialEyeGeometry | None
|
|
226
377
|
Dictionary with numerical geometric parameters of the eye. If `None`, the
|
|
227
378
|
geometry is based on `model_type`. Can be a partial dictionary, in which
|
|
228
379
|
case missing values are based on the eye model specified by `model_type`.
|
|
@@ -235,6 +386,10 @@ class Eye:
|
|
|
235
386
|
refraction : float
|
|
236
387
|
Spherical equivalent of refraction of the eye model. The lens back curvature
|
|
237
388
|
can be adjusted to obtain this refraction with `Eye.adjust_lens_back`.
|
|
389
|
+
refractive_indices : _PartialRefractiveIndices | None
|
|
390
|
+
Dictionary with numerical values of the refractive indices of the eye media.
|
|
391
|
+
If `None`, the refractive indices are based on `NType`. If specified, the
|
|
392
|
+
refractive indices corresponding to `NType` are updated with the values in this dictionary.
|
|
238
393
|
pIOL : PhakicIOL
|
|
239
394
|
Properties of the pIOL, if present. Tuple of power, thickness, refractive
|
|
240
395
|
index, distance to lens.
|
|
@@ -242,28 +397,20 @@ class Eye:
|
|
|
242
397
|
self.name = name
|
|
243
398
|
self.spherical_equivalent = refraction
|
|
244
399
|
self.is_pseudophakic = model_type == "VughtIOL"
|
|
245
|
-
self.axial_length = None
|
|
246
400
|
self.pIOL = pIOL
|
|
247
401
|
|
|
248
402
|
self.geometry = self._build_geometry_dictionary(model_type, geometry)
|
|
249
403
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"lens": 1.4222,
|
|
255
|
-
"vit": 1.3377,
|
|
256
|
-
}
|
|
257
|
-
elif NType == "VughtIOL":
|
|
258
|
-
self.refractive_indices = {
|
|
259
|
-
"cor": 1.3777,
|
|
260
|
-
"aq": 1.3391,
|
|
261
|
-
"lens": 1.47,
|
|
262
|
-
"vit": 1.3377,
|
|
263
|
-
}
|
|
404
|
+
self.refractive_indices: RefractiveIndices
|
|
405
|
+
|
|
406
|
+
if NType in _DEFAULT_REFRACTIVE_INDICES:
|
|
407
|
+
self.refractive_indices = _DEFAULT_REFRACTIVE_INDICES[NType].copy()
|
|
264
408
|
else:
|
|
265
409
|
raise ValueError(f"Model type {NType} is undefined.")
|
|
266
410
|
|
|
411
|
+
if refractive_indices is not None:
|
|
412
|
+
self.refractive_indices.update(refractive_indices)
|
|
413
|
+
|
|
267
414
|
self.R_corF, self.R_corB, self.R_lensF, self.R_lensB = sp.symbols(
|
|
268
415
|
"R_corF R_corB R_lensF R_lensB"
|
|
269
416
|
)
|
|
@@ -271,46 +418,30 @@ class Eye:
|
|
|
271
418
|
"D_cor D_ACD D_lens D_vitr"
|
|
272
419
|
)
|
|
273
420
|
|
|
274
|
-
self.
|
|
421
|
+
self._update_ray_transfer_matrix()
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def axial_length(self) -> float:
|
|
425
|
+
return (
|
|
275
426
|
self.geometry["D_cor"]
|
|
276
427
|
+ self.geometry["D_ACD"]
|
|
277
428
|
+ self.geometry["D_lens"]
|
|
278
429
|
+ self.geometry["D_vitr"]
|
|
279
430
|
)
|
|
280
|
-
self.ray_transfer_matrix = self.calculate_ray_transfer_matrix()
|
|
281
431
|
|
|
282
432
|
def __str__(self) -> str:
|
|
283
433
|
return f"{self.name}"
|
|
284
434
|
|
|
285
435
|
@staticmethod
|
|
286
436
|
def _build_geometry_dictionary(
|
|
287
|
-
model_type: EyeModelType,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"R_corF": -7.72e-3,
|
|
292
|
-
"R_corB": -6.50e-3,
|
|
293
|
-
"R_lensF": -10.20e-3,
|
|
294
|
-
"R_lensB": +6.00e-3,
|
|
295
|
-
"D_cor": 0.55e-3,
|
|
296
|
-
"D_ACD": 3.05e-3,
|
|
297
|
-
"D_lens": 4.00e-3,
|
|
298
|
-
"D_vitr": 16.3203e-3,
|
|
299
|
-
}
|
|
300
|
-
elif model_type == "VughtIOL":
|
|
301
|
-
geometry = {
|
|
302
|
-
"R_corF": -7.72e-3,
|
|
303
|
-
"R_corB": -6.50e-3,
|
|
304
|
-
"R_lensF": -8.16e-3,
|
|
305
|
-
"R_lensB": +11.18e-3,
|
|
306
|
-
"D_cor": 0.55e-3,
|
|
307
|
-
"D_ACD": 3.05e-3,
|
|
308
|
-
"D_lens": 0.6896e-3,
|
|
309
|
-
"D_vitr": 19.3203e-3,
|
|
310
|
-
}
|
|
311
|
-
else:
|
|
437
|
+
model_type: EyeModelType,
|
|
438
|
+
partial_geometry: _PartialEyeGeometry | EyeGeometry | None = None,
|
|
439
|
+
) -> EyeGeometry:
|
|
440
|
+
if model_type not in _DEFAULT_GEOMETRIES:
|
|
312
441
|
raise ValueError(f"Model type {model_type} is undefined.")
|
|
313
442
|
|
|
443
|
+
geometry = _DEFAULT_GEOMETRIES[model_type].copy()
|
|
444
|
+
|
|
314
445
|
if partial_geometry is not None:
|
|
315
446
|
# Estimate the cornea back curvature if it is unspecified and the cornea
|
|
316
447
|
# front curvature is specified
|
|
@@ -378,6 +509,34 @@ class Eye:
|
|
|
378
509
|
* uniform_medium(self.D_vitr)
|
|
379
510
|
)
|
|
380
511
|
|
|
512
|
+
def update_geometry(self, **geometry: Unpack[_PartialEyeGeometry]) -> None:
|
|
513
|
+
"""Update the eye geometry with new values.
|
|
514
|
+
|
|
515
|
+
Parameters
|
|
516
|
+
----------
|
|
517
|
+
geometry : _PartialEyeGeometry
|
|
518
|
+
Geometry parameters to update as keyword arguments.
|
|
519
|
+
"""
|
|
520
|
+
self.geometry.update(geometry)
|
|
521
|
+
self._update_ray_transfer_matrix()
|
|
522
|
+
|
|
523
|
+
def update_refractive_indices(
|
|
524
|
+
self, **refractive_indices: Unpack[_PartialRefractiveIndices]
|
|
525
|
+
) -> None:
|
|
526
|
+
"""Update the eye's refractive indices with new values.
|
|
527
|
+
|
|
528
|
+
Parameters
|
|
529
|
+
----------
|
|
530
|
+
refractive_indices : _PartialRefractiveIndices
|
|
531
|
+
Refractive indices to update as keyword arguments.
|
|
532
|
+
"""
|
|
533
|
+
self.refractive_indices.update(refractive_indices)
|
|
534
|
+
self._update_ray_transfer_matrix()
|
|
535
|
+
|
|
536
|
+
def _update_ray_transfer_matrix(self) -> None:
|
|
537
|
+
"""Update the eye's ray transfer matrix."""
|
|
538
|
+
self.ray_transfer_matrix = self.calculate_ray_transfer_matrix()
|
|
539
|
+
|
|
381
540
|
def adjust_lens_back(
|
|
382
541
|
self, target_refraction: float, *, update_model: bool = False
|
|
383
542
|
) -> tuple[float, float]:
|
|
@@ -491,8 +650,8 @@ class Eye:
|
|
|
491
650
|
) -> tuple[float, sp.Matrix, float]:
|
|
492
651
|
"""Calculate the total magnification of the eye - camera system.
|
|
493
652
|
|
|
494
|
-
|
|
495
|
-
on the camera sensor.
|
|
653
|
+
The magnification is calculated in units of pixels per millimeter, i.e. a structure
|
|
654
|
+
of 1 mm on the retina has a size of `magnification` pixels on the camera sensor.
|
|
496
655
|
|
|
497
656
|
Parameters
|
|
498
657
|
----------
|
|
@@ -505,7 +664,7 @@ class Eye:
|
|
|
505
664
|
Returns
|
|
506
665
|
-------
|
|
507
666
|
magnification : float
|
|
508
|
-
Central magnification of the eye - camera system.
|
|
667
|
+
Central magnification of the eye - camera system, in pixels per millimeter.
|
|
509
668
|
focused_system_matrix : sympy.Matrix
|
|
510
669
|
Ray transfer matrix for the full system.
|
|
511
670
|
focus_lens_radius : float
|
|
@@ -516,23 +675,47 @@ class Eye:
|
|
|
516
675
|
eye_matrix, distance_eye_camera, return_focus_lens_curvature=True
|
|
517
676
|
)
|
|
518
677
|
|
|
519
|
-
magnification = focused_system_matrix[0, 0]
|
|
678
|
+
magnification = focused_system_matrix[0, 0] * camera.pixel_density
|
|
520
679
|
|
|
521
680
|
return magnification, focused_system_matrix, focus_lens_radius
|
|
522
681
|
|
|
523
682
|
|
|
683
|
+
_MISSING = cast(float, object())
|
|
684
|
+
|
|
685
|
+
|
|
524
686
|
class Camera:
|
|
525
687
|
def __init__(
|
|
526
688
|
self,
|
|
527
|
-
F_cond: NumberOrSymbol = None, # noqa: N803
|
|
528
|
-
a1: NumberOrSymbol = None,
|
|
689
|
+
F_cond: NumberOrSymbol | None = None, # noqa: N803
|
|
690
|
+
a1: NumberOrSymbol | None = None,
|
|
691
|
+
pixel_density: float = _MISSING,
|
|
529
692
|
camera_type: Literal["default"] = "default",
|
|
530
693
|
) -> None:
|
|
694
|
+
"""Create a new camera model.
|
|
695
|
+
|
|
696
|
+
Parameters
|
|
697
|
+
----------
|
|
698
|
+
F_cond : NumberOrSymbol
|
|
699
|
+
Focal length of the condenser lens, in meters.
|
|
700
|
+
a1 : NumberOrSymbol
|
|
701
|
+
First order calibration term.
|
|
702
|
+
camera_type : Literal["default"]
|
|
703
|
+
Type of camera. Currently only "default" is implemented.
|
|
704
|
+
pixel_density : float
|
|
705
|
+
Pixel density of the camera sensor, in pixels per millimeter.
|
|
706
|
+
Defaults to 100 pixels/mm.
|
|
707
|
+
"""
|
|
708
|
+
if pixel_density is _MISSING:
|
|
709
|
+
raise ValueError("Pixel density must be specified.")
|
|
710
|
+
if pixel_density <= 0:
|
|
711
|
+
raise ValueError("Pixel density must be a positive float.")
|
|
712
|
+
|
|
531
713
|
self.camera_type = "lensTaylor"
|
|
532
714
|
self.F_cond = sp.Symbol("F_cond", real=True)
|
|
533
715
|
self.d_CCD = sp.symbols("d_CCD")
|
|
534
716
|
self.R_foc = sp.Symbol("R_foc", real=True) # in m
|
|
535
717
|
self.a1 = sp.Symbol("a1", real=True)
|
|
718
|
+
self.pixel_density = pixel_density # pixels per mm
|
|
536
719
|
|
|
537
720
|
if camera_type == "default":
|
|
538
721
|
self.d_CCD = self.F_cond
|
|
@@ -549,7 +732,10 @@ class Camera:
|
|
|
549
732
|
self.n_glas, 1.0, -self.R_foc
|
|
550
733
|
) * spherical_interface(1.0, self.n_glas, self.R_foc)
|
|
551
734
|
self.correction_term = sp.Matrix(
|
|
552
|
-
[
|
|
735
|
+
[
|
|
736
|
+
[1 + self.a1 / self.R_foc, 0],
|
|
737
|
+
[0, 1.0 / (1 + self.a1 / self.R_foc)],
|
|
738
|
+
]
|
|
553
739
|
)
|
|
554
740
|
self.ray_transfer_matrix = (
|
|
555
741
|
self.correction_term
|
|
@@ -558,13 +744,15 @@ class Camera:
|
|
|
558
744
|
* self.condenser_lens
|
|
559
745
|
)
|
|
560
746
|
|
|
561
|
-
self.M_camera_alg = self.ray_transfer_matrix
|
|
562
|
-
|
|
563
747
|
if F_cond and (a1 is not None):
|
|
564
|
-
self.ray_transfer_matrix = self.
|
|
748
|
+
self.ray_transfer_matrix = self.ray_transfer_matrix.evalf(
|
|
565
749
|
subs={self.F_cond: F_cond, self.a1: a1}
|
|
566
750
|
)
|
|
567
751
|
|
|
752
|
+
@property
|
|
753
|
+
def M_camera_alg(self):
|
|
754
|
+
return self.ray_transfer_matrix
|
|
755
|
+
|
|
568
756
|
def calculate_focus_lens_radius(
|
|
569
757
|
self, eye_matrix: sp.Matrix, distance_eye_camera: float = 0.05
|
|
570
758
|
) -> float:
|
|
@@ -616,7 +804,7 @@ class Camera:
|
|
|
616
804
|
)
|
|
617
805
|
|
|
618
806
|
# For contact cameras such as the Panoret fundus camera, the refractive index (of air) needs to be changed to
|
|
619
|
-
# that of the medium
|
|
807
|
+
# that of the medium used between the eye and camera. This can be done by changing the use of uniform_medium()
|
|
620
808
|
# to medium_change().
|
|
621
809
|
focus_lens_curvature = self.calculate_focus_lens_radius(
|
|
622
810
|
eye_matrix, distance_eye_camera
|
|
@@ -629,6 +817,21 @@ class Camera:
|
|
|
629
817
|
|
|
630
818
|
return system_matrix.evalf(subs={self.R_foc: focus_lens_curvature})
|
|
631
819
|
|
|
820
|
+
def get_size_on_ccd(self, size_pixels: float) -> float:
|
|
821
|
+
"""Convert an image size in pixels to its physical size on the CCD sensor in meters.
|
|
822
|
+
|
|
823
|
+
Parameters
|
|
824
|
+
----------
|
|
825
|
+
size_pixels : float
|
|
826
|
+
Image size on the CCD sensor in pixels.
|
|
827
|
+
|
|
828
|
+
Returns
|
|
829
|
+
-------
|
|
830
|
+
float
|
|
831
|
+
Image size on the CCD sensor in meters.
|
|
832
|
+
"""
|
|
833
|
+
return size_pixels / (self.pixel_density * 1000) # convert mm to m
|
|
834
|
+
|
|
632
835
|
|
|
633
836
|
# Maximum allowed difference between the calculated and specified refraction of the eye model
|
|
634
837
|
_MAXIMUM_REFRACTION_DEVIATION = 0.05
|
|
@@ -647,7 +850,7 @@ def calculate_magnification(
|
|
|
647
850
|
) -> tuple[float, float, Eye]:
|
|
648
851
|
"""Calculate the total magnification of the eye - camera system.
|
|
649
852
|
|
|
650
|
-
A structure on the central retina
|
|
853
|
+
A structure of 1 mm on the central retina has a size of `magnification` pixels
|
|
651
854
|
on the camera sensor.
|
|
652
855
|
|
|
653
856
|
Parameters
|
|
@@ -669,7 +872,7 @@ def calculate_magnification(
|
|
|
669
872
|
Returns
|
|
670
873
|
-------
|
|
671
874
|
magnification : float
|
|
672
|
-
Central magnification of the eye - camera system.
|
|
875
|
+
Central magnification of the eye - camera system, in pixels per millimeter.
|
|
673
876
|
glasses_power : float
|
|
674
877
|
Power of the corrective lens required to correct for the eye's refraction, in
|
|
675
878
|
diopters.
|
|
@@ -681,7 +884,7 @@ def calculate_magnification(
|
|
|
681
884
|
When the calculated glasses power differs significantly from
|
|
682
885
|
`eye.spherical_equivalent`, a warning is displayed.
|
|
683
886
|
"""
|
|
684
|
-
|
|
887
|
+
_, glasses_power = eye.calculate_refraction(display=False)
|
|
685
888
|
|
|
686
889
|
if (
|
|
687
890
|
not suppress_warnings
|
|
@@ -713,7 +916,7 @@ def calculate_magnification(
|
|
|
713
916
|
subs=({camera.R_foc: focus_lens_radius})
|
|
714
917
|
)
|
|
715
918
|
|
|
716
|
-
magnification: float = focused_system_matrix[0, 0]
|
|
919
|
+
magnification: float = focused_system_matrix[0, 0] * camera.pixel_density
|
|
717
920
|
|
|
718
921
|
if abs(focused_system_matrix[0, 1]) > _MAXIMUM_FOCUS_DEVIATION:
|
|
719
922
|
warn(f"focused_system_matrix not in focus for patient {eye.name}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PAROS
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: PARaxial Optical fundus Scaling (PAROS)
|
|
5
5
|
Author-email: "Lennart J. Pors" <l.j.pors@lumc.nl>, "Jan-Willem M. Beenakker" <j.w.m.beenakker@lumc.nl>, Corné Haasjes <c.haasjes@lumc.nl>
|
|
6
6
|
License: MIT License
|
|
@@ -30,12 +30,13 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
30
30
|
Classifier: Programming Language :: Python :: 3
|
|
31
31
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
32
32
|
Classifier: Typing :: Typed
|
|
33
|
-
Requires-Python: >=3.
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
34
|
Requires-Dist: matplotlib
|
|
35
35
|
Requires-Dist: numpy
|
|
36
36
|
Requires-Dist: pandas
|
|
37
37
|
Requires-Dist: scipy
|
|
38
38
|
Requires-Dist: sympy
|
|
39
|
+
Requires-Dist: typing-extensions>=4.12.2; python_full_version < '3.11'
|
|
39
40
|
Description-Content-Type: text/markdown
|
|
40
41
|
|
|
41
42
|
# PARaxial Optical fundus Scaling (PAROS)
|
|
@@ -49,6 +50,7 @@ The code is provided as is, without any warranty, under the [MIT license](LICENS
|
|
|
49
50
|
## Basic usage
|
|
50
51
|
|
|
51
52
|
### Try it online
|
|
53
|
+
|
|
52
54
|
A basic version of PAROS can most easily be run online here: [basic version](https://demo.mreye.nl/paros)
|
|
53
55
|
|
|
54
56
|
[](https://demo.mreye.nl/PAROS)
|
|
@@ -73,10 +75,15 @@ The implementation of PAROS in this repository is functional for the camera in o
|
|
|
73
75
|
## Camera constants
|
|
74
76
|
|
|
75
77
|
Please find here a table of known camera calibration constants; this can be added upon by other contributors.
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| Topcon TRC-
|
|
78
|
+
|
|
79
|
+
| Camera type | CCD type | Condenser lens power [D] | First order calibration term | Pixel density [px/mm] |
|
|
80
|
+
| :-------------- | -------- | -----------------------: | ---------------------------: | --------------------: |
|
|
81
|
+
| Topcon TRC-50DX | | 37.6 | 0.03481 | 100 |
|
|
82
|
+
| Topcon TRC-50IX | | 37.7 | 0.03226 | 100 |
|
|
83
|
+
|
|
84
|
+
> [!NOTE]
|
|
85
|
+
> The pixel density for the Topcon system is estimated.
|
|
86
|
+
> Since this value is only used to fit the other parameters, it does not need to be exact, but the same value should be used for both calibration and magnification calculation.
|
|
80
87
|
|
|
81
88
|
## Referencing
|
|
82
89
|
|
|
@@ -9,6 +9,7 @@ The code is provided as is, without any warranty, under the [MIT license](LICENS
|
|
|
9
9
|
## Basic usage
|
|
10
10
|
|
|
11
11
|
### Try it online
|
|
12
|
+
|
|
12
13
|
A basic version of PAROS can most easily be run online here: [basic version](https://demo.mreye.nl/paros)
|
|
13
14
|
|
|
14
15
|
[](https://demo.mreye.nl/PAROS)
|
|
@@ -33,10 +34,15 @@ The implementation of PAROS in this repository is functional for the camera in o
|
|
|
33
34
|
## Camera constants
|
|
34
35
|
|
|
35
36
|
Please find here a table of known camera calibration constants; this can be added upon by other contributors.
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
| Topcon TRC-
|
|
37
|
+
|
|
38
|
+
| Camera type | CCD type | Condenser lens power [D] | First order calibration term | Pixel density [px/mm] |
|
|
39
|
+
| :-------------- | -------- | -----------------------: | ---------------------------: | --------------------: |
|
|
40
|
+
| Topcon TRC-50DX | | 37.6 | 0.03481 | 100 |
|
|
41
|
+
| Topcon TRC-50IX | | 37.7 | 0.03226 | 100 |
|
|
42
|
+
|
|
43
|
+
> [!NOTE]
|
|
44
|
+
> The pixel density for the Topcon system is estimated.
|
|
45
|
+
> Since this value is only used to fit the other parameters, it does not need to be exact, but the same value should be used for both calibration and magnification calculation.
|
|
40
46
|
|
|
41
47
|
## Referencing
|
|
42
48
|
|