vascx 1.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 (133) hide show
  1. vascx-1.0/.gitattributes +3 -0
  2. vascx-1.0/.gitignore +8 -0
  3. vascx-1.0/PKG-INFO +270 -0
  4. vascx-1.0/README.md +243 -0
  5. vascx-1.0/notebooks/0_preprocess.ipynb +131 -0
  6. vascx-1.0/notebooks/1_segment_preprocessed.ipynb +451 -0
  7. vascx-1.0/notebooks/2_feature_extraction.ipynb +1249 -0
  8. vascx-1.0/notebooks/faz/feature_extraction.ipynb +340 -0
  9. vascx-1.0/notebooks/faz/feature_plots.ipynb +255 -0
  10. vascx-1.0/notebooks/faz/vessel_features.ipynb +257 -0
  11. vascx-1.0/notebooks/features/display_names.ipynb +587 -0
  12. vascx-1.0/notebooks/features/feature_plots.ipynb +251 -0
  13. vascx-1.0/notebooks/features/sparsity.ipynb +233 -0
  14. vascx-1.0/notebooks/graph_analysis/recursive_vessel_resolve.ipynb +83 -0
  15. vascx-1.0/notebooks/playground/debug_splines.ipynb +371 -0
  16. vascx-1.0/notebooks/playground/diameter_ranking.ipynb +561 -0
  17. vascx-1.0/notebooks/playground/disc_fitting.ipynb +59 -0
  18. vascx-1.0/notebooks/playground/intensity_profiles.ipynb +268 -0
  19. vascx-1.0/notebooks/playground/profile.ipynb +365 -0
  20. vascx-1.0/notebooks/playground/profile.py +132 -0
  21. vascx-1.0/notebooks/playground/pruning.ipynb +261 -0
  22. vascx-1.0/notebooks/playground/sample_voronoi.ipynb +476 -0
  23. vascx-1.0/notebooks/playground/sknw.py +226 -0
  24. vascx-1.0/notebooks/playground/spline_intersections.ipynb +234 -0
  25. vascx-1.0/pyproject.toml +57 -0
  26. vascx-1.0/samples/figures/bifurcation_angles.png +0 -0
  27. vascx-1.0/samples/figures/bifurcation_count.png +0 -0
  28. vascx-1.0/samples/figures/caliber.png +0 -0
  29. vascx-1.0/samples/figures/cre.png +0 -0
  30. vascx-1.0/samples/figures/sparsity.png +0 -0
  31. vascx-1.0/samples/figures/sparsity_inner.png +0 -0
  32. vascx-1.0/samples/figures/sparsity_max.png +0 -0
  33. vascx-1.0/samples/figures/temporal_angle.png +0 -0
  34. vascx-1.0/samples/figures/tortuosity.png +0 -0
  35. vascx-1.0/samples/figures/variance_of_laplacian.png +0 -0
  36. vascx-1.0/samples/figures/vascular_density.png +0 -0
  37. vascx-1.0/samples/fundus/original/CHASEDB1_08L.png +0 -0
  38. vascx-1.0/samples/fundus/original/CHASEDB1_12R.png +0 -0
  39. vascx-1.0/samples/fundus/original/DRIVE_22.png +0 -0
  40. vascx-1.0/samples/fundus/original/DRIVE_40.png +0 -0
  41. vascx-1.0/samples/fundus/original/HRF_04_g.jpg +0 -0
  42. vascx-1.0/samples/fundus/original/HRF_07_dr.jpg +0 -0
  43. vascx-1.0/setup.cfg +11 -0
  44. vascx-1.0/tests/__init__.py +1 -0
  45. vascx-1.0/tests/conftest.py +21 -0
  46. vascx-1.0/tests/reference/bergmann.meta.json +16 -0
  47. vascx-1.0/tests/reference/bergmann.overrides.yaml +6 -0
  48. vascx-1.0/tests/reference/bergmann.parquet +0 -0
  49. vascx-1.0/tests/reference/full.meta.json +16 -0
  50. vascx-1.0/tests/reference/full.overrides.yaml +6 -0
  51. vascx-1.0/tests/reference/full.parquet +0 -0
  52. vascx-1.0/tests/reference/full_v2.meta.json +16 -0
  53. vascx-1.0/tests/reference/full_v2.overrides.yaml +6 -0
  54. vascx-1.0/tests/reference/full_v2.parquet +0 -0
  55. vascx-1.0/tests/reference/full_v3.meta.json +16 -0
  56. vascx-1.0/tests/reference/full_v3.overrides.yaml +6 -0
  57. vascx-1.0/tests/reference/full_v3.parquet +0 -0
  58. vascx-1.0/tests/reference/sparsity.meta.json +16 -0
  59. vascx-1.0/tests/reference/sparsity.overrides.yaml +6 -0
  60. vascx-1.0/tests/reference/sparsity.parquet +0 -0
  61. vascx-1.0/tests/regression_helpers.py +268 -0
  62. vascx-1.0/tests/test_feature_plotting.py +63 -0
  63. vascx-1.0/tests/test_feature_regression.py +30 -0
  64. vascx-1.0/tests/test_pipeline_profile.py +55 -0
  65. vascx-1.0/vascx/__init__.py +0 -0
  66. vascx-1.0/vascx/cli.py +345 -0
  67. vascx-1.0/vascx/faz/feature_sets/__init__.py +0 -0
  68. vascx-1.0/vascx/faz/feature_sets/basic.py +41 -0
  69. vascx-1.0/vascx/faz/features/__init__.py +0 -0
  70. vascx-1.0/vascx/faz/features/base.py +26 -0
  71. vascx-1.0/vascx/faz/features/bifurcations.py +33 -0
  72. vascx-1.0/vascx/faz/features/caliber.py +40 -0
  73. vascx-1.0/vascx/faz/features/faz_params.py +50 -0
  74. vascx-1.0/vascx/faz/features/readme.txt +14 -0
  75. vascx-1.0/vascx/faz/features/tortuosity.py +97 -0
  76. vascx-1.0/vascx/faz/features/vascular_densities.py +56 -0
  77. vascx-1.0/vascx/faz/layer.py +259 -0
  78. vascx-1.0/vascx/faz/retina.py +87 -0
  79. vascx-1.0/vascx/fundus/__init__.py +0 -0
  80. vascx-1.0/vascx/fundus/feature_sets/__init__.py +5 -0
  81. vascx-1.0/vascx/fundus/feature_sets/bergmann.py +21 -0
  82. vascx-1.0/vascx/fundus/feature_sets/full.py +73 -0
  83. vascx-1.0/vascx/fundus/feature_sets/full_v2.py +141 -0
  84. vascx-1.0/vascx/fundus/feature_sets/full_v3.py +146 -0
  85. vascx-1.0/vascx/fundus/feature_sets/sparsity.py +51 -0
  86. vascx-1.0/vascx/fundus/features/__init__.py +0 -0
  87. vascx-1.0/vascx/fundus/features/base.py +362 -0
  88. vascx-1.0/vascx/fundus/features/bifurcation_angles.py +136 -0
  89. vascx-1.0/vascx/fundus/features/bifurcation_counts.py +70 -0
  90. vascx-1.0/vascx/fundus/features/caliber.py +121 -0
  91. vascx-1.0/vascx/fundus/features/cre.py +390 -0
  92. vascx-1.0/vascx/fundus/features/disc_features.py +38 -0
  93. vascx-1.0/vascx/fundus/features/length.py +112 -0
  94. vascx-1.0/vascx/fundus/features/sparsity.py +199 -0
  95. vascx-1.0/vascx/fundus/features/temporal_angles.py +232 -0
  96. vascx-1.0/vascx/fundus/features/tortuosity.py +197 -0
  97. vascx-1.0/vascx/fundus/features/variance_of_laplacian.py +79 -0
  98. vascx-1.0/vascx/fundus/features/vascular_densities.py +109 -0
  99. vascx-1.0/vascx/fundus/layer.py +563 -0
  100. vascx-1.0/vascx/fundus/loader.py +117 -0
  101. vascx-1.0/vascx/fundus/metrics/base.py +6 -0
  102. vascx-1.0/vascx/fundus/metrics/bifurcation_map.py +179 -0
  103. vascx-1.0/vascx/fundus/retina.py +282 -0
  104. vascx-1.0/vascx/fundus/vessel_resolve.py +120 -0
  105. vascx-1.0/vascx/fundus/vessels_layer.py +120 -0
  106. vascx-1.0/vascx/inference/__init__.py +2 -0
  107. vascx-1.0/vascx/inference/inference.py +343 -0
  108. vascx-1.0/vascx/inference/utils.py +159 -0
  109. vascx-1.0/vascx/shared/__init__.py +0 -0
  110. vascx-1.0/vascx/shared/aggregators.py +97 -0
  111. vascx-1.0/vascx/shared/base.py +71 -0
  112. vascx-1.0/vascx/shared/diameters.py +164 -0
  113. vascx-1.0/vascx/shared/features.py +88 -0
  114. vascx-1.0/vascx/shared/graph.py +185 -0
  115. vascx-1.0/vascx/shared/masks.py +144 -0
  116. vascx-1.0/vascx/shared/nodes.py +63 -0
  117. vascx-1.0/vascx/shared/segment.py +305 -0
  118. vascx-1.0/vascx/shared/splines.py +240 -0
  119. vascx-1.0/vascx/shared/vessels.py +295 -0
  120. vascx-1.0/vascx/utils/__init__.py +120 -0
  121. vascx-1.0/vascx/utils/analysis.py +118 -0
  122. vascx-1.0/vascx/utils/av_masks.py +35 -0
  123. vascx-1.0/vascx/utils/eval.py +10 -0
  124. vascx-1.0/vascx/utils/feature_docs.py +22 -0
  125. vascx-1.0/vascx/utils/plotting.py +81 -0
  126. vascx-1.0/vascx.egg-info/PKG-INFO +270 -0
  127. vascx-1.0/vascx.egg-info/SOURCES.txt +132 -0
  128. vascx-1.0/vascx.egg-info/dependency_links.txt +1 -0
  129. vascx-1.0/vascx.egg-info/entry_points.txt +2 -0
  130. vascx-1.0/vascx.egg-info/not-zip-safe +1 -0
  131. vascx-1.0/vascx.egg-info/requires.txt +20 -0
  132. vascx-1.0/vascx.egg-info/top_level.txt +1 -0
  133. vascx-1.0/versioneer.py +2109 -0
@@ -0,0 +1,3 @@
1
+ *.ipynb filter=nbstripout
2
+ *.zpln filter=nbstripout
3
+ *.ipynb diff=ipynb
vascx-1.0/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.pyc
2
+ __pycache__
3
+ *.egg-info
4
+ *.zip
5
+ /samples/*
6
+ !/samples/figures
7
+ !/samples/fundus/original
8
+ /paper/
vascx-1.0/PKG-INFO ADDED
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: vascx
3
+ Version: 1.0
4
+ Summary: Retinal analysis toolbox for Python
5
+ Author-email: Jose Vargas <j.vargasquiros@erasmusmc.nl>
6
+ Requires-Python: <3.13,>=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: numpy==2.*
9
+ Requires-Dist: pandas==2.*
10
+ Requires-Dist: scikit-learn==1.*
11
+ Requires-Dist: scipy==1.*
12
+ Requires-Dist: opencv-python==4.*
13
+ Requires-Dist: matplotlib==3.*
14
+ Requires-Dist: joblib==1.*
15
+ Requires-Dist: tqdm==4.*
16
+ Requires-Dist: Pillow==11.*
17
+ Requires-Dist: click==8.*
18
+ Requires-Dist: sknw==0.14
19
+ Requires-Dist: sortedcontainers==2.4.0
20
+ Requires-Dist: scikit-image==0.24.0
21
+ Requires-Dist: retinalysis-enface
22
+ Requires-Dist: retinalysis-fundusprep
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest==8.*; extra == "test"
25
+ Requires-Dist: pyarrow==22.*; extra == "test"
26
+ Requires-Dist: PyYAML==6.*; extra == "test"
27
+
28
+ ## VascX retinal vascular analysis
29
+
30
+ VascX was created for the extraction of vascular features from fundus image segmentations.
31
+
32
+ **Important!** This repository contains the feature extraction part of the VascX pipeline.
33
+
34
+ For details about our segmentation models please refer our [models repository](https://github.com/Eyened/rtnls_vascx_models) an its related open access paper [VascX Models: Deep Ensembles for Retinal Vascular Analysis From Color Fundus Images](https://tvst.arvojournals.org/article.aspx?articleid=2810436)
35
+
36
+ ### Installation
37
+
38
+ To install the entire fundus analysis pipeline including fundus preprocessing, model inference code and vascular biomarker extraction:
39
+
40
+ 1. Create a virtual environment, or otherwise ensure a clean environment.
41
+
42
+ 2. Install VascX:
43
+
44
+ ```
45
+ pip install retinalysis-vascx
46
+ ```
47
+
48
+ ### Usage
49
+
50
+ To speed up re-execution of VascX we recommend to run the segmentation and feature extraction steps separately:
51
+
52
+ To run on the provided samples folder in our git repository:
53
+
54
+ ```
55
+ git clone git@github.com:Eyened/retinalysis-vascx.git rtnls_vascx
56
+ cd rtnls_vascx
57
+ vascx run-models ./samples/fundus/original/ /path/to/segmentations
58
+ vascx calc-biomarkers /path/to/segmentations /path/to/features.csv --feature_set full --n-jobs 8 --logfile /path/to/logfile.txt
59
+ ```
60
+
61
+ Note that `vascx run-models` will write segmentations and other model predictions to `/path/to/segmentations`, which will have the following structure:
62
+
63
+ ```
64
+ /path/to/segmentations
65
+ - preprocessed_rgb/ - preprocessed fundus images
66
+ - artery_vein/ - artery-vein model segmentations
67
+ - vessels/ - vessel model segmentations
68
+ - disc/ - optic disc model segmentations
69
+ - overlays/ - optional overlays showing the segmentations
70
+ - bounds.csv - contains the bounds of the fundus image
71
+ - fovea.csv - model predictions of the fovea locations for each image
72
+ - quality.csv - model estimations of CFI quality
73
+ ```
74
+
75
+ The folders above will contain images with matching filenames.
76
+
77
+
78
+ We also provide notebooks for running the three stages:
79
+
80
+ 1. Preprocessing. See [this notebook](./notebooks/0_preprocess.ipynb). This step is CPU-heavy and benefits from parallelization (see notebook).
81
+
82
+ 2. Inference. See [this notebook](./notebooks/1_segment_preprocessed.ipynb). All models can be ran in a single GPU with >10GB VRAM.
83
+
84
+ 3. Feature extraction. See [this notebook](./notebooks/2_feature_extraction.ipynb). This step is CPU-heavy again and benefits from parallelization (see notebook).
85
+
86
+
87
+
88
+ ### Implementation
89
+
90
+
91
+
92
+ VascX processes vessel segmentations through four main stages, each producing different data representations:
93
+
94
+ - **Input masks**: `np.ndarray[bool]` per layer; optic disc and fovea metadata from segmentation models.
95
+
96
+ - **Stage 1 - Binary/skeleton**:
97
+ - `binary`: filled vessel mask after disc masking
98
+ - `binary_nodisc`: vessel mask without disc region
99
+ - `skeleton`: skeletonized vessel centerlines using skimage skeletonization
100
+
101
+ - **Stage 2 - Undirected graph**:
102
+ - NetworkX `Graph` with skeleton pixels as nodes
103
+ - `Segment` objects stored on edges containing skeleton points and geometric properties
104
+ - Each segment represents a vessel segment between junction points
105
+
106
+ - **Stage 3 - Directed digraph**:
107
+ - NetworkX `DiGraph` with flow direction from optic disc outward
108
+ - `trees`: root nodes representing vessel trees emanating from disc
109
+ - `nodes`: `Endpoint` and `Bifurcation` objects with spatial positions
110
+ - `segments`: directed vessel segments with computed properties (diameter, length, etc.)
111
+
112
+ - **Stage 4 - Resolved vessels**:
113
+ - Merged vessel graph after running vessel resolution algorithm
114
+ - `resolved_segments`: final vessel segments after merging short segments
115
+ - Segment-to-pixel mapping for spatial feature computation
116
+
117
+ Biomarker families use different representations: mask-based features use `binary`; topology features use `digraph` and `nodes`; morphological features use `segments` with computed diameters; spatial features use segment-to-pixel mappings.
118
+
119
+ ### Biomarkers
120
+
121
+ VascX computes retinal vascular biomarkers from standardized representations (binary masks, undirected/directed graphs, resolved vessels). Below we describe each feature with the exact quantity being estimated and the equations used. Throughout, B denotes the stage‑1 binary vessel mask, S the set of eligible directed segments with lengths \(\ell_i\), and R an analysis region of interest; cardinalities count pixels, and distances are in pixels unless noted.
122
+
123
+ **VascularDensity.** The fraction of retinal area occupied by vessels in R, computed on the binary mask B:
124
+
125
+ $$
126
+ D \,=\, \frac{|B \cap R|}{|R|}.
127
+ $$
128
+
129
+ ![](samples/figures/vascular_density.png)
130
+
131
+ **BifurcationCount.** The count of branching points in the directed graph (stage‑3). Let $\mathcal{B}$ be the set of bifurcation nodes with positions $p_b$:
132
+
133
+ $$
134
+ C \,=\, \sum_{b \in \mathcal{B}} \mathbf{1}[p_b \in R].
135
+ $$
136
+
137
+ ![](samples/figures/bifurcation_count.png)
138
+
139
+ **BifurcationAngles.** For each bifurcation $b$ at position $p_b$, outgoing branch directions are estimated by sampling the branches' splines at distance $\delta$ from the node along each branch at points $q_1$ and $q_2$. Unit vectors $(u_b, v_b)$ are defined from the bifurcation point to the sample points:
140
+
141
+ $$
142
+ u_b = \frac{q_1 - p_b}{\|q_1 - p_b\|}, \quad v_b = \frac{q_2 - p_b}{\|q_2 - p_b\|},
143
+ $$
144
+
145
+ and the bifurcation angle is defined as the angle between these vectors:
146
+
147
+ $$
148
+ \theta_b \,=\, \arccos(u_b \cdot v_b), \quad \theta_b \in [0^\circ, 180^\circ].
149
+ $$
150
+
151
+ Angles exceeding 160° are discarded as non-bifurcating continuations. Summary statistics (e.g., mean/median) are reported across valid nodes.
152
+
153
+ ![](samples/figures/bifurcation_angles.png)
154
+
155
+ **Caliber.** For each segment $i$, diameters are sampled along a spline fitted to its skeleton by projecting spline normals to the vessel boundary on B. The per‑segment diameter is the median along its arclength. The reported caliber aggregates over eligible segments (length $\ell_i \ge \ell_{\min}$):
156
+
157
+ $$
158
+ \operatorname{Caliber} \,=\, g\big(\{ d_i : i \in S \}\big),
159
+ $$
160
+
161
+ where \(g\) is a robust statistic (typically the median).
162
+
163
+ ![](samples/figures/caliber.png)
164
+
165
+ **Tortuosity.** Three complementary measures are provided per segment (or per resolved vessel). Let $L_{\text{arc},i}$ be arclength and $L_{\text{chord},i}$ the end‑to‑end Euclidean distance.
166
+
167
+ - Distance factor:
168
+
169
+ $$
170
+ T_i^{\text{DF}} \,=\, \frac{L_{\text{arc},i}}{L_{\text{chord},i}}.
171
+ $$
172
+
173
+ - Curvature‑based measure, using planar curvature $\kappa_i(s)$ and OD–fovea distance $d_{ODF}$ for scale normalization:
174
+
175
+ $$
176
+ T_i^{\kappa} \,=\, \frac{1}{L_{\text{arc},i}} \int_0^{L_{\text{arc},i}} \big|\kappa_i(s)\big| \, ds \; \cdot \; d_{ODF}.
177
+ $$
178
+
179
+ - Inflection count (number of curvature sign changes along the centerline):
180
+
181
+ $$
182
+ T_i^{\text{INF}} \,=\, N^{(i)}_{\text{inflections}}.
183
+ $$
184
+
185
+ When reporting a single score over multiple segments, length‑weighted aggregation may be used for normalization:
186
+
187
+ $$
188
+ T_{\text{tot}} \,=\, \sum_{i \in S} \left( \frac{\ell_i}{\sum_{j \in S} \ell_j} \right) t_i,
189
+ $$
190
+
191
+ with $t_i$ any of the measures above.
192
+
193
+ ![](samples/figures/tortuosity.png)
194
+
195
+ **CRE (Central Retinal Equivalents).** Concentric circles centered at the optic disc are intersected with the vessel network. At each radius $r$, up to $M$ crossings with the largest segment median diameters are retained and recursively reduced via the Hubbard rule with a modality‑dependent constant $c$ (arteries: 0.88; veins: 0.95):
196
+
197
+ $$
198
+ d \leftarrow c\,\sqrt{d_1^2 + d_2^2}
199
+ $$
200
+
201
+ applied pairwise until a single equivalent caliber $d_r$ remains. The final CRE is the median of $\{d_r\}$ across radii.
202
+
203
+ ![](samples/figures/cre.png)
204
+
205
+ **TemporalAngle.** On each concentric circle of radius \(r\), the two dominant temporal vessels are identified by diameter and spatial continuity. The angle at the disc center is
206
+
207
+ $$
208
+ \theta_r \,=\, \angle\big(\overline{OD\,p_1(r)},\, \overline{OD\,p_2(r)}\big),
209
+ $$
210
+
211
+ and the reported value is the median over radii.
212
+
213
+ ![](samples/figures/temporal_angle.png)
214
+
215
+ **Sparsity.** Let $\mathrm{DT}(x)$ represent the distance transform over $R$, ie. the normalized Euclidean distance to the nearest vessel pixel (scaled by $d_{ODF}$). Over pixels in R we report either the mean or the largest local maximum:
216
+
217
+ $$
218
+ S_{\text{mean}} \,=\, \frac{1}{|R|} \sum_{x \in R} \mathrm{DT}(x), \qquad
219
+ S_{\max} \,=\, \max_{x \in R \cap \text{local maxima}} \mathrm{DT}(x).
220
+ $$
221
+
222
+ ![](samples/figures/sparsity_max.png)
223
+
224
+ **VarianceOfLaplacian.** For the fundus image $I$ (grayscale), compute the discrete Laplacian $L = \Delta I$. Image sharpness is summarized as the variance over R:
225
+
226
+ $$
227
+ \operatorname{Var}\{ L(x) : x \in R \}.
228
+ $$
229
+
230
+ **DiscFoveaDistance.** With optic disc center $c_{OD}$ and fovea position $p_f$,
231
+
232
+ $$
233
+ d_{ODF} \,=\, \lVert c_{OD} - p_f \rVert_2.
234
+ $$
235
+
236
+
237
+ ### Feature localisation
238
+
239
+ VascX localises feature computations using anatomical references and predefined grids:
240
+
241
+ - **Anatomical anchoring**
242
+ - The optic disc mask and fovea position orient geometry (e.g., OD–fovea axis) and define a retinal mask.
243
+ - All region masks are intersected with the retinal mask; features operate only on visible retina.
244
+
245
+ - **Predefined grids** (`rtnls_enface/rtnls_enface/grids`)
246
+ - `EllipseGrid`: ellipse centered midway between disc and fovea, major axis along OD–fovea. Fields: `FullGrid`, `Superior`, `Inferior`.
247
+ - `CircleGrid`: disc–fovea–centered circle (radius derived from OD–fovea distance and disc size). Fields: `FullGrid`, `Superior`, `Inferior`.
248
+ - `ETDRSGrid`: classic ETDRS layout with rings (`Center`, `Inner`, `Outer`), quadrants (`Superior`, `Inferior`, `Nasal`, `Temporal`, plus `Left`/`Right`), and subfields (`CSF`, `SIM`, `NIM`, `TIM`, `IIM`, `SOM`, `NOM`, `TOM`, `IOM`).
249
+ - `HemifieldGrid`: superior/inferior half-planes split relative to the OD–fovea axis. Fields: `FullGrid`, `Superior`, `Inferior`.
250
+ - `DiscCenteredGrid`: disc-anchored rings (`inner`, `center`, `outer`) and quadrants (`superior`, `inferior`, `nasal`, `temporal`, plus `left`/`right`), taking laterality into account.
251
+
252
+ - **Bounds and visibility (CFI bounds)**
253
+ - For a chosen field, the platform evaluates the fraction within bounds using `grid_field_fraction_in_bounds` (and `grid_field_masks_and_fraction`).
254
+ - If the fraction in-bounds is too small (typically < 0.5), many features skip computation and return `None` to avoid out-of-frame bias.
255
+ - Visualizers plot the requested field overlayed on the image; computations always respect in-bounds masking.
256
+
257
+ Ready-to-run feature sets are available under `vascx/fundus/feature_sets` (e.g., `full`, `bergmann`, `quality`) and can be selected by name when using `extract_in_parallel`. To generate feature descriptions alongside extraction:
258
+
259
+ Ready-to-run feature sets are available under `vascx/fundus/feature_sets` (e.g., `full`, `bergmann`, `quality`) and can be selected by name when using `extract_in_parallel`. To generate feature descriptions alongside extraction:
260
+
261
+ ```python
262
+ df = extract_in_parallel(examples, "full", n_jobs=8, descriptions_output_path="feature_descriptions_full.txt")
263
+ ```
264
+
265
+
266
+ ## Citation
267
+
268
+ If you use VascX, please cite our paper:
269
+
270
+ Jose Vargas Quiros, Bart Liefers, Karin A. van Garderen, Jeroen P. Vermeulen, Caroline Klaver; VascX Models: Deep Ensembles for Retinal Vascular Analysis From Color Fundus Images. Trans. Vis. Sci. Tech. 2025;14(7):19. https://doi.org/10.1167/tvst.14.7.19.
vascx-1.0/README.md ADDED
@@ -0,0 +1,243 @@
1
+ ## VascX retinal vascular analysis
2
+
3
+ VascX was created for the extraction of vascular features from fundus image segmentations.
4
+
5
+ **Important!** This repository contains the feature extraction part of the VascX pipeline.
6
+
7
+ For details about our segmentation models please refer our [models repository](https://github.com/Eyened/rtnls_vascx_models) an its related open access paper [VascX Models: Deep Ensembles for Retinal Vascular Analysis From Color Fundus Images](https://tvst.arvojournals.org/article.aspx?articleid=2810436)
8
+
9
+ ### Installation
10
+
11
+ To install the entire fundus analysis pipeline including fundus preprocessing, model inference code and vascular biomarker extraction:
12
+
13
+ 1. Create a virtual environment, or otherwise ensure a clean environment.
14
+
15
+ 2. Install VascX:
16
+
17
+ ```
18
+ pip install retinalysis-vascx
19
+ ```
20
+
21
+ ### Usage
22
+
23
+ To speed up re-execution of VascX we recommend to run the segmentation and feature extraction steps separately:
24
+
25
+ To run on the provided samples folder in our git repository:
26
+
27
+ ```
28
+ git clone git@github.com:Eyened/retinalysis-vascx.git rtnls_vascx
29
+ cd rtnls_vascx
30
+ vascx run-models ./samples/fundus/original/ /path/to/segmentations
31
+ vascx calc-biomarkers /path/to/segmentations /path/to/features.csv --feature_set full --n-jobs 8 --logfile /path/to/logfile.txt
32
+ ```
33
+
34
+ Note that `vascx run-models` will write segmentations and other model predictions to `/path/to/segmentations`, which will have the following structure:
35
+
36
+ ```
37
+ /path/to/segmentations
38
+ - preprocessed_rgb/ - preprocessed fundus images
39
+ - artery_vein/ - artery-vein model segmentations
40
+ - vessels/ - vessel model segmentations
41
+ - disc/ - optic disc model segmentations
42
+ - overlays/ - optional overlays showing the segmentations
43
+ - bounds.csv - contains the bounds of the fundus image
44
+ - fovea.csv - model predictions of the fovea locations for each image
45
+ - quality.csv - model estimations of CFI quality
46
+ ```
47
+
48
+ The folders above will contain images with matching filenames.
49
+
50
+
51
+ We also provide notebooks for running the three stages:
52
+
53
+ 1. Preprocessing. See [this notebook](./notebooks/0_preprocess.ipynb). This step is CPU-heavy and benefits from parallelization (see notebook).
54
+
55
+ 2. Inference. See [this notebook](./notebooks/1_segment_preprocessed.ipynb). All models can be ran in a single GPU with >10GB VRAM.
56
+
57
+ 3. Feature extraction. See [this notebook](./notebooks/2_feature_extraction.ipynb). This step is CPU-heavy again and benefits from parallelization (see notebook).
58
+
59
+
60
+
61
+ ### Implementation
62
+
63
+
64
+
65
+ VascX processes vessel segmentations through four main stages, each producing different data representations:
66
+
67
+ - **Input masks**: `np.ndarray[bool]` per layer; optic disc and fovea metadata from segmentation models.
68
+
69
+ - **Stage 1 - Binary/skeleton**:
70
+ - `binary`: filled vessel mask after disc masking
71
+ - `binary_nodisc`: vessel mask without disc region
72
+ - `skeleton`: skeletonized vessel centerlines using skimage skeletonization
73
+
74
+ - **Stage 2 - Undirected graph**:
75
+ - NetworkX `Graph` with skeleton pixels as nodes
76
+ - `Segment` objects stored on edges containing skeleton points and geometric properties
77
+ - Each segment represents a vessel segment between junction points
78
+
79
+ - **Stage 3 - Directed digraph**:
80
+ - NetworkX `DiGraph` with flow direction from optic disc outward
81
+ - `trees`: root nodes representing vessel trees emanating from disc
82
+ - `nodes`: `Endpoint` and `Bifurcation` objects with spatial positions
83
+ - `segments`: directed vessel segments with computed properties (diameter, length, etc.)
84
+
85
+ - **Stage 4 - Resolved vessels**:
86
+ - Merged vessel graph after running vessel resolution algorithm
87
+ - `resolved_segments`: final vessel segments after merging short segments
88
+ - Segment-to-pixel mapping for spatial feature computation
89
+
90
+ Biomarker families use different representations: mask-based features use `binary`; topology features use `digraph` and `nodes`; morphological features use `segments` with computed diameters; spatial features use segment-to-pixel mappings.
91
+
92
+ ### Biomarkers
93
+
94
+ VascX computes retinal vascular biomarkers from standardized representations (binary masks, undirected/directed graphs, resolved vessels). Below we describe each feature with the exact quantity being estimated and the equations used. Throughout, B denotes the stage‑1 binary vessel mask, S the set of eligible directed segments with lengths \(\ell_i\), and R an analysis region of interest; cardinalities count pixels, and distances are in pixels unless noted.
95
+
96
+ **VascularDensity.** The fraction of retinal area occupied by vessels in R, computed on the binary mask B:
97
+
98
+ $$
99
+ D \,=\, \frac{|B \cap R|}{|R|}.
100
+ $$
101
+
102
+ ![](samples/figures/vascular_density.png)
103
+
104
+ **BifurcationCount.** The count of branching points in the directed graph (stage‑3). Let $\mathcal{B}$ be the set of bifurcation nodes with positions $p_b$:
105
+
106
+ $$
107
+ C \,=\, \sum_{b \in \mathcal{B}} \mathbf{1}[p_b \in R].
108
+ $$
109
+
110
+ ![](samples/figures/bifurcation_count.png)
111
+
112
+ **BifurcationAngles.** For each bifurcation $b$ at position $p_b$, outgoing branch directions are estimated by sampling the branches' splines at distance $\delta$ from the node along each branch at points $q_1$ and $q_2$. Unit vectors $(u_b, v_b)$ are defined from the bifurcation point to the sample points:
113
+
114
+ $$
115
+ u_b = \frac{q_1 - p_b}{\|q_1 - p_b\|}, \quad v_b = \frac{q_2 - p_b}{\|q_2 - p_b\|},
116
+ $$
117
+
118
+ and the bifurcation angle is defined as the angle between these vectors:
119
+
120
+ $$
121
+ \theta_b \,=\, \arccos(u_b \cdot v_b), \quad \theta_b \in [0^\circ, 180^\circ].
122
+ $$
123
+
124
+ Angles exceeding 160° are discarded as non-bifurcating continuations. Summary statistics (e.g., mean/median) are reported across valid nodes.
125
+
126
+ ![](samples/figures/bifurcation_angles.png)
127
+
128
+ **Caliber.** For each segment $i$, diameters are sampled along a spline fitted to its skeleton by projecting spline normals to the vessel boundary on B. The per‑segment diameter is the median along its arclength. The reported caliber aggregates over eligible segments (length $\ell_i \ge \ell_{\min}$):
129
+
130
+ $$
131
+ \operatorname{Caliber} \,=\, g\big(\{ d_i : i \in S \}\big),
132
+ $$
133
+
134
+ where \(g\) is a robust statistic (typically the median).
135
+
136
+ ![](samples/figures/caliber.png)
137
+
138
+ **Tortuosity.** Three complementary measures are provided per segment (or per resolved vessel). Let $L_{\text{arc},i}$ be arclength and $L_{\text{chord},i}$ the end‑to‑end Euclidean distance.
139
+
140
+ - Distance factor:
141
+
142
+ $$
143
+ T_i^{\text{DF}} \,=\, \frac{L_{\text{arc},i}}{L_{\text{chord},i}}.
144
+ $$
145
+
146
+ - Curvature‑based measure, using planar curvature $\kappa_i(s)$ and OD–fovea distance $d_{ODF}$ for scale normalization:
147
+
148
+ $$
149
+ T_i^{\kappa} \,=\, \frac{1}{L_{\text{arc},i}} \int_0^{L_{\text{arc},i}} \big|\kappa_i(s)\big| \, ds \; \cdot \; d_{ODF}.
150
+ $$
151
+
152
+ - Inflection count (number of curvature sign changes along the centerline):
153
+
154
+ $$
155
+ T_i^{\text{INF}} \,=\, N^{(i)}_{\text{inflections}}.
156
+ $$
157
+
158
+ When reporting a single score over multiple segments, length‑weighted aggregation may be used for normalization:
159
+
160
+ $$
161
+ T_{\text{tot}} \,=\, \sum_{i \in S} \left( \frac{\ell_i}{\sum_{j \in S} \ell_j} \right) t_i,
162
+ $$
163
+
164
+ with $t_i$ any of the measures above.
165
+
166
+ ![](samples/figures/tortuosity.png)
167
+
168
+ **CRE (Central Retinal Equivalents).** Concentric circles centered at the optic disc are intersected with the vessel network. At each radius $r$, up to $M$ crossings with the largest segment median diameters are retained and recursively reduced via the Hubbard rule with a modality‑dependent constant $c$ (arteries: 0.88; veins: 0.95):
169
+
170
+ $$
171
+ d \leftarrow c\,\sqrt{d_1^2 + d_2^2}
172
+ $$
173
+
174
+ applied pairwise until a single equivalent caliber $d_r$ remains. The final CRE is the median of $\{d_r\}$ across radii.
175
+
176
+ ![](samples/figures/cre.png)
177
+
178
+ **TemporalAngle.** On each concentric circle of radius \(r\), the two dominant temporal vessels are identified by diameter and spatial continuity. The angle at the disc center is
179
+
180
+ $$
181
+ \theta_r \,=\, \angle\big(\overline{OD\,p_1(r)},\, \overline{OD\,p_2(r)}\big),
182
+ $$
183
+
184
+ and the reported value is the median over radii.
185
+
186
+ ![](samples/figures/temporal_angle.png)
187
+
188
+ **Sparsity.** Let $\mathrm{DT}(x)$ represent the distance transform over $R$, ie. the normalized Euclidean distance to the nearest vessel pixel (scaled by $d_{ODF}$). Over pixels in R we report either the mean or the largest local maximum:
189
+
190
+ $$
191
+ S_{\text{mean}} \,=\, \frac{1}{|R|} \sum_{x \in R} \mathrm{DT}(x), \qquad
192
+ S_{\max} \,=\, \max_{x \in R \cap \text{local maxima}} \mathrm{DT}(x).
193
+ $$
194
+
195
+ ![](samples/figures/sparsity_max.png)
196
+
197
+ **VarianceOfLaplacian.** For the fundus image $I$ (grayscale), compute the discrete Laplacian $L = \Delta I$. Image sharpness is summarized as the variance over R:
198
+
199
+ $$
200
+ \operatorname{Var}\{ L(x) : x \in R \}.
201
+ $$
202
+
203
+ **DiscFoveaDistance.** With optic disc center $c_{OD}$ and fovea position $p_f$,
204
+
205
+ $$
206
+ d_{ODF} \,=\, \lVert c_{OD} - p_f \rVert_2.
207
+ $$
208
+
209
+
210
+ ### Feature localisation
211
+
212
+ VascX localises feature computations using anatomical references and predefined grids:
213
+
214
+ - **Anatomical anchoring**
215
+ - The optic disc mask and fovea position orient geometry (e.g., OD–fovea axis) and define a retinal mask.
216
+ - All region masks are intersected with the retinal mask; features operate only on visible retina.
217
+
218
+ - **Predefined grids** (`rtnls_enface/rtnls_enface/grids`)
219
+ - `EllipseGrid`: ellipse centered midway between disc and fovea, major axis along OD–fovea. Fields: `FullGrid`, `Superior`, `Inferior`.
220
+ - `CircleGrid`: disc–fovea–centered circle (radius derived from OD–fovea distance and disc size). Fields: `FullGrid`, `Superior`, `Inferior`.
221
+ - `ETDRSGrid`: classic ETDRS layout with rings (`Center`, `Inner`, `Outer`), quadrants (`Superior`, `Inferior`, `Nasal`, `Temporal`, plus `Left`/`Right`), and subfields (`CSF`, `SIM`, `NIM`, `TIM`, `IIM`, `SOM`, `NOM`, `TOM`, `IOM`).
222
+ - `HemifieldGrid`: superior/inferior half-planes split relative to the OD–fovea axis. Fields: `FullGrid`, `Superior`, `Inferior`.
223
+ - `DiscCenteredGrid`: disc-anchored rings (`inner`, `center`, `outer`) and quadrants (`superior`, `inferior`, `nasal`, `temporal`, plus `left`/`right`), taking laterality into account.
224
+
225
+ - **Bounds and visibility (CFI bounds)**
226
+ - For a chosen field, the platform evaluates the fraction within bounds using `grid_field_fraction_in_bounds` (and `grid_field_masks_and_fraction`).
227
+ - If the fraction in-bounds is too small (typically < 0.5), many features skip computation and return `None` to avoid out-of-frame bias.
228
+ - Visualizers plot the requested field overlayed on the image; computations always respect in-bounds masking.
229
+
230
+ Ready-to-run feature sets are available under `vascx/fundus/feature_sets` (e.g., `full`, `bergmann`, `quality`) and can be selected by name when using `extract_in_parallel`. To generate feature descriptions alongside extraction:
231
+
232
+ Ready-to-run feature sets are available under `vascx/fundus/feature_sets` (e.g., `full`, `bergmann`, `quality`) and can be selected by name when using `extract_in_parallel`. To generate feature descriptions alongside extraction:
233
+
234
+ ```python
235
+ df = extract_in_parallel(examples, "full", n_jobs=8, descriptions_output_path="feature_descriptions_full.txt")
236
+ ```
237
+
238
+
239
+ ## Citation
240
+
241
+ If you use VascX, please cite our paper:
242
+
243
+ Jose Vargas Quiros, Bart Liefers, Karin A. van Garderen, Jeroen P. Vermeulen, Caroline Klaver; VascX Models: Deep Ensembles for Retinal Vascular Analysis From Color Fundus Images. Trans. Vis. Sci. Tech. 2025;14(7):19. https://doi.org/10.1167/tvst.14.7.19.
@@ -0,0 +1,131 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "from pathlib import Path\n",
10
+ "\n",
11
+ "import pandas as pd\n",
12
+ "from matplotlib import pyplot as plt\n",
13
+ "from rtnls_fundusprep.preprocessor import parallel_preprocess"
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "markdown",
18
+ "metadata": {},
19
+ "source": [
20
+ "## Preprocessing\n",
21
+ "\n",
22
+ "This code will preprocess the images and write .png files with the square fundus image and the contrast enhanced version\n",
23
+ "\n",
24
+ "This step is not strictly necessary, but it is useful if you want to run the preprocessing step separately before model inference\n"
25
+ ]
26
+ },
27
+ {
28
+ "cell_type": "markdown",
29
+ "metadata": {},
30
+ "source": [
31
+ "Create a list of files to be preprocessed:"
32
+ ]
33
+ },
34
+ {
35
+ "cell_type": "code",
36
+ "execution_count": 3,
37
+ "metadata": {},
38
+ "outputs": [],
39
+ "source": [
40
+ "ds_path = Path(\"../samples/fundus\")\n",
41
+ "files = list((ds_path / \"original\").glob(\"*\"))"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "markdown",
46
+ "metadata": {},
47
+ "source": [
48
+ "Images with .dcm extension will be read as dicom and the pixel_array will be read as RGB. All other images will be read using PIL's Image.open"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": 3,
54
+ "metadata": {},
55
+ "outputs": [
56
+ {
57
+ "name": "stderr",
58
+ "output_type": "stream",
59
+ "text": [
60
+ "0it [00:00, ?it/s][Parallel(n_jobs=4)]: Using backend LokyBackend with 4 concurrent workers.\n",
61
+ "6it [00:00, 1511.28it/s]"
62
+ ]
63
+ },
64
+ {
65
+ "name": "stderr",
66
+ "output_type": "stream",
67
+ "text": [
68
+ "\n",
69
+ "[Parallel(n_jobs=4)]: Done 2 out of 6 | elapsed: 1.2s remaining: 2.3s\n",
70
+ "[Parallel(n_jobs=4)]: Done 3 out of 6 | elapsed: 1.2s remaining: 1.2s\n",
71
+ "[Parallel(n_jobs=4)]: Done 4 out of 6 | elapsed: 1.2s remaining: 0.6s\n",
72
+ "[Parallel(n_jobs=4)]: Done 6 out of 6 | elapsed: 1.6s finished\n"
73
+ ]
74
+ }
75
+ ],
76
+ "source": [
77
+ "bounds = parallel_preprocess(\n",
78
+ " files, # List of image files\n",
79
+ " rgb_path=ds_path / \"rgb\", # Output path for RGB images\n",
80
+ " ce_path=ds_path / \"ce\", # Output path for Contrast Enhanced images\n",
81
+ " n_jobs=4, # number of preprocessing workers\n",
82
+ ")\n",
83
+ "df_bounds = pd.DataFrame(bounds).set_index(\"id\")"
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "markdown",
88
+ "metadata": {},
89
+ "source": [
90
+ "The preprocessor will produce RGB and contrast-enhanced preprocessed images cropped to a square and return a dataframe with the image bounds that can be used to reconstruct the original image. Output files will be named the same as input images, but with .png extension. Be careful with providing multiple inputs with the same filename without extension as this will result in over-written images. Any exceptions during pre-processing will not stop execution but will print error. Images that failed pre-processing for any reason will be marked with `success=False` in the df_bounds dataframe."
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "code",
95
+ "execution_count": 4,
96
+ "metadata": {},
97
+ "outputs": [],
98
+ "source": [
99
+ "df_bounds.to_csv(ds_path / \"meta.csv\")"
100
+ ]
101
+ },
102
+ {
103
+ "cell_type": "code",
104
+ "execution_count": null,
105
+ "metadata": {},
106
+ "outputs": [],
107
+ "source": []
108
+ }
109
+ ],
110
+ "metadata": {
111
+ "kernelspec": {
112
+ "display_name": "rtnls",
113
+ "language": "python",
114
+ "name": "python3"
115
+ },
116
+ "language_info": {
117
+ "codemirror_mode": {
118
+ "name": "ipython",
119
+ "version": 3
120
+ },
121
+ "file_extension": ".py",
122
+ "mimetype": "text/x-python",
123
+ "name": "python",
124
+ "nbconvert_exporter": "python",
125
+ "pygments_lexer": "ipython3",
126
+ "version": "3.12.12"
127
+ }
128
+ },
129
+ "nbformat": 4,
130
+ "nbformat_minor": 2
131
+ }