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.
Files changed (26) hide show
  1. paros-2.0.0/.github/assets/screenshot.png +0 -0
  2. {paros-1.0.2 → paros-2.0.0}/.github/workflows/ci.yml +1 -1
  3. paros-2.0.0/PAROS/data/calibration/Topcon/TRC_50DX.csv +10 -0
  4. paros-2.0.0/PAROS/data/calibration/Topcon/TRC_50IX.csv +11 -0
  5. {paros-1.0.2 → paros-2.0.0}/PAROS/eyephantom.py +0 -2
  6. {paros-1.0.2 → paros-2.0.0}/PAROS/fundscale.py +268 -65
  7. {paros-1.0.2 → paros-2.0.0}/PKG-INFO +13 -6
  8. {paros-1.0.2 → paros-2.0.0}/README.md +10 -4
  9. {paros-1.0.2 → paros-2.0.0}/examples/calculate_ocular_magnification.ipynb +75 -79
  10. {paros-1.0.2 → paros-2.0.0}/examples/fundus_camera_calibration.ipynb +25 -62
  11. {paros-1.0.2 → paros-2.0.0}/pyproject.toml +9 -2
  12. {paros-1.0.2 → paros-2.0.0}/uv.lock +5 -230
  13. paros-1.0.2/.github/assets/screenshot.png +0 -0
  14. paros-1.0.2/PAROS/data/calibration/Topcon/TRC_50DX.csv +0 -10
  15. paros-1.0.2/PAROS/data/calibration/Topcon/TRC_50IX.csv +0 -11
  16. {paros-1.0.2 → paros-2.0.0}/.github/workflows/release.yml +0 -0
  17. {paros-1.0.2 → paros-2.0.0}/.gitignore +0 -0
  18. {paros-1.0.2 → paros-2.0.0}/CITATION.cff +0 -0
  19. {paros-1.0.2 → paros-2.0.0}/CONTRIBUTING.md +0 -0
  20. {paros-1.0.2 → paros-2.0.0}/LICENSE +0 -0
  21. {paros-1.0.2 → paros-2.0.0}/PAROS/__init__.py +0 -0
  22. {paros-1.0.2 → paros-2.0.0}/PAROS/data/__init__.py +0 -0
  23. {paros-1.0.2 → paros-2.0.0}/PAROS/data/calibration/__init__.py +0 -0
  24. {paros-1.0.2 → paros-2.0.0}/PAROS/data/lenses/__init__.py +0 -0
  25. {paros-1.0.2 → paros-2.0.0}/PAROS/data/lenses/phantom.csv +0 -0
  26. {paros-1.0.2 → paros-2.0.0}/ruff_defaults.toml +0 -0
@@ -8,7 +8,7 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
  strategy:
10
10
  matrix:
11
- python: &python-versions ["3.9", "3.10", "3.11", "3.12"]
11
+ python: &python-versions ["3.10", "3.11", "3.12"]
12
12
  steps:
13
13
  - uses: actions/checkout@v6
14
14
  - name: Set up uv
@@ -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
@@ -15,8 +15,6 @@ import sympy as sp
15
15
 
16
16
  from PAROS.fundscale import spherical_interface, uniform_medium
17
17
 
18
- sp.init_printing(use_unicode=True)
19
-
20
18
 
21
19
  class Lens:
22
20
  """Ray transfer model of a lens."""
@@ -24,20 +24,32 @@ from __future__ import annotations
24
24
 
25
25
  import copy
26
26
  import math
27
- from typing import Literal, NamedTuple, Union
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
- sp.init_printing(use_unicode=True)
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
- def _round_expr(expr: sp.Expr, num_digits: int = 3) -> sp.Expr:
40
- return expr.xreplace({n: round(n, num_digits) for n in expr.atoms(sp.Number)})
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: dict[str, float] | None = None,
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 : dict[str, float]
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
- if NType == "Navarro":
251
- self.refractive_indices = {
252
- "cor": 1.3777,
253
- "aq": 1.3391,
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.axial_length = (
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, partial_geometry: dict[str, float] | None = None
288
- ):
289
- if model_type == "Navarro":
290
- geometry = {
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
- A structure on the central retina is depicted `magnification` times larger
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
- [[1 + self.a1 / self.R_foc, 0], [0, 1.0 / (1 + self.a1 / self.R_foc)]]
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.M_camera_alg.evalf(
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 use dbetween the eye and camera. This can be done by changing the use of uniform_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 is depicted `magnification` times larger
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
- glasses_curvature, glasses_power = eye.calculate_refraction(display=False)
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: 1.0.2
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.9
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
  [![Try PAROS online](.github/assets/screenshot.png)](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
- | Camera type | CCD type | Condenser lens power (D) | First order calibration term |
77
- | --------------- | -------- | ------------------------ | ---------------------------- |
78
- | Topcon TRC-50DX | | 37.6 | 0.03481 |
79
- | Topcon TRC-50IX | | 37.7 | 0.03226 |
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
  [![Try PAROS online](.github/assets/screenshot.png)](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
- | Camera type | CCD type | Condenser lens power (D) | First order calibration term |
37
- | --------------- | -------- | ------------------------ | ---------------------------- |
38
- | Topcon TRC-50DX | | 37.6 | 0.03481 |
39
- | Topcon TRC-50IX | | 37.7 | 0.03226 |
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