retinal-thin-vessels 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.
- retinal_thin_vessels-1.0/LICENSE +21 -0
- retinal_thin_vessels-1.0/PKG-INFO +100 -0
- retinal_thin_vessels-1.0/README.md +74 -0
- retinal_thin_vessels-1.0/pyproject.toml +41 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/__init__.py +6 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/core.py +182 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/external/DSE_skeleton_pruning/dsepruning/__init__.py +1 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/external/DSE_skeleton_pruning/dsepruning/dsepruning.py +128 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/external/DSE_skeleton_pruning/setup.py +14 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/get_relation.py +35 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/input_transformation.py +88 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels/metrics.py +172 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels.egg-info/PKG-INFO +100 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels.egg-info/SOURCES.txt +17 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels.egg-info/dependency_links.txt +1 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels.egg-info/requires.txt +4 -0
- retinal_thin_vessels-1.0/retinal_thin_vessels.egg-info/top_level.txt +1 -0
- retinal_thin_vessels-1.0/setup.cfg +4 -0
- retinal_thin_vessels-1.0/tests/tests.py +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 J-Linaris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: retinal_thin_vessels
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: A Python package for analyzing thin retinal vessels and creating vessels-thickness based weight maps
|
|
5
|
+
Author-email: João Paulo Menezes Linaris <joaolinaris@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/J-Linaris/retinal_thin_vessels
|
|
8
|
+
Project-URL: Repository, https://github.com/J-Linaris/retinal_thin_vessels
|
|
9
|
+
Keywords: retina,retinal,retinal-vessels,vessel-segmentation,image-analysis,medical-imaging,deep-learning,weight-map,pytorch,thin-vessels,image-segmentation,binary-masks,binary-mask
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: torch
|
|
23
|
+
Requires-Dist: scikit-image
|
|
24
|
+
Requires-Dist: Pillow
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# retinal_thin_vessels
|
|
28
|
+
|
|
29
|
+
A Python package for computing the recall and precision scores specifically on thin vessels in retinal images, as detailed in the paper "Vessel-Width-Based Metrics and Weight Masks for Retinal Blood Vessel Segmentation", published in WUW-SIBGRAPI 2025. The package also includes a function for visualizing thickness-based filtered masks, the basic structure for computing the proposed metrics.
|
|
30
|
+
|
|
31
|
+
## Package installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install retinal_thin_vessels
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage Demonstration with DRIVE and CHASEDB1
|
|
38
|
+
|
|
39
|
+
To ensure the metrics are reliable, it is important to visualize the specific thin-vessel mask used by the given functions in their calculations. Therefore, a core function, get_thin_vessels_mask(), is also provided. This function takes a standard segmentation mask and returns a new mask containing only the thin vessels.
|
|
40
|
+
|
|
41
|
+
The following code demonstrates how to generate this filtered mask using images from two public datasets: DRIVE and CHASEDB1.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from PIL import Image
|
|
45
|
+
from retinal_thin_vessels.core import get_thin_vessels_mask
|
|
46
|
+
from retinal_thin_vessels.metrics import recall_thin_vessels, precision_thin_vessels
|
|
47
|
+
from sklearn.metrics import recall_score, precision_score
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# Import the original segmentation masks
|
|
52
|
+
seg_DRIVE = Image.open(f"tests/imgs/DRIVE_seg_example.png")
|
|
53
|
+
seg_CDB1 = Image.open(f"tests/imgs/CHASEDB1_seg_example.png")
|
|
54
|
+
|
|
55
|
+
# generates new masks containing only thin vessels
|
|
56
|
+
thin_vessels_seg_DRIVE = get_thin_vessels_mask(seg_DRIVE)
|
|
57
|
+
thin_vessels_seg_CDB1 = get_thin_vessels_mask(seg_CDB1)
|
|
58
|
+
|
|
59
|
+
# Display the original segmentation mask and the resulting thin-vessel-only mask for comparison
|
|
60
|
+
seg_DRIVE.show()
|
|
61
|
+
img_DRIVE = Image.fromarray(thin_vessels_seg_DRIVE)
|
|
62
|
+
img_DRIVE.show()
|
|
63
|
+
|
|
64
|
+
seg_CDB1.show()
|
|
65
|
+
img_CDB1 = Image.fromarray(thin_vessels_seg_CDB1)
|
|
66
|
+
img_CDB1.show()
|
|
67
|
+
```
|
|
68
|
+
<img src="tests/imgs/DRIVE_seg_example.png" alt="DRIVE_thin_vessels_example" width=300/>
|
|
69
|
+
<img src="tests/imgs/DRIVE_seg_thin_example.png" alt="DRIVE_thin_vessels_example" width=300/>
|
|
70
|
+
<img src="tests/imgs/CHASEDB1_seg_example.png" alt="CHASEDB1_thin_vessels_example" width=300/>
|
|
71
|
+
<img src="tests/imgs/CHASEDB1_seg_thin_example.png" alt="CHASEDB1_thin_vessels_example" width=300/>
|
|
72
|
+
|
|
73
|
+
Furthermore, to demonstrate the metric calculation functions, you can run the code below. It compares the overall metrics (calculated with scikit-learn) to the thin-vessel-specific metrics calculated by this package.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Load the ground truth segmentation mask and a sample prediction
|
|
77
|
+
pred = Image.open(f"tests/imgs/DRIVE_pred_example.png")
|
|
78
|
+
seg_DRIVE = Image.open(f"tests/imgs/DRIVE_seg_example.png").resize((pred.size), Image.NEAREST)
|
|
79
|
+
|
|
80
|
+
# Binarize images to a 0/1 format for scikit-learn compatibility
|
|
81
|
+
seg_DRIVE = np.where(np.array(seg_DRIVE) > 0, 1, 0)
|
|
82
|
+
pred = np.where(np.array(pred) > 0, 1, 0)
|
|
83
|
+
|
|
84
|
+
# Compute and print the metrics
|
|
85
|
+
print(f"Overall Recall score: {recall_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
86
|
+
print(f"Recall score on thin vessels: {recall_thin_vessels(seg_DRIVE, pred)}")
|
|
87
|
+
print("-" * 30)
|
|
88
|
+
print(f"Overall Precision score: {precision_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
89
|
+
print(f"Precision score on thin Vessels: {precision_thin_vessels(seg_DRIVE, pred)}")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If the program is running correctly with the provided sample images, the results should be similar to this:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
Overall Recall score: 0.8553852359822509
|
|
96
|
+
Recall score on thin vessels: 0.751244555071562
|
|
97
|
+
------------------------------
|
|
98
|
+
Overall Precision score: 0.8422369623068674
|
|
99
|
+
Precision score on thin Vessels: 0.6527915897144481
|
|
100
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# retinal_thin_vessels
|
|
2
|
+
|
|
3
|
+
A Python package for computing the recall and precision scores specifically on thin vessels in retinal images, as detailed in the paper "Vessel-Width-Based Metrics and Weight Masks for Retinal Blood Vessel Segmentation", published in WUW-SIBGRAPI 2025. The package also includes a function for visualizing thickness-based filtered masks, the basic structure for computing the proposed metrics.
|
|
4
|
+
|
|
5
|
+
## Package installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install retinal_thin_vessels
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage Demonstration with DRIVE and CHASEDB1
|
|
12
|
+
|
|
13
|
+
To ensure the metrics are reliable, it is important to visualize the specific thin-vessel mask used by the given functions in their calculations. Therefore, a core function, get_thin_vessels_mask(), is also provided. This function takes a standard segmentation mask and returns a new mask containing only the thin vessels.
|
|
14
|
+
|
|
15
|
+
The following code demonstrates how to generate this filtered mask using images from two public datasets: DRIVE and CHASEDB1.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from PIL import Image
|
|
19
|
+
from retinal_thin_vessels.core import get_thin_vessels_mask
|
|
20
|
+
from retinal_thin_vessels.metrics import recall_thin_vessels, precision_thin_vessels
|
|
21
|
+
from sklearn.metrics import recall_score, precision_score
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# Import the original segmentation masks
|
|
26
|
+
seg_DRIVE = Image.open(f"tests/imgs/DRIVE_seg_example.png")
|
|
27
|
+
seg_CDB1 = Image.open(f"tests/imgs/CHASEDB1_seg_example.png")
|
|
28
|
+
|
|
29
|
+
# generates new masks containing only thin vessels
|
|
30
|
+
thin_vessels_seg_DRIVE = get_thin_vessels_mask(seg_DRIVE)
|
|
31
|
+
thin_vessels_seg_CDB1 = get_thin_vessels_mask(seg_CDB1)
|
|
32
|
+
|
|
33
|
+
# Display the original segmentation mask and the resulting thin-vessel-only mask for comparison
|
|
34
|
+
seg_DRIVE.show()
|
|
35
|
+
img_DRIVE = Image.fromarray(thin_vessels_seg_DRIVE)
|
|
36
|
+
img_DRIVE.show()
|
|
37
|
+
|
|
38
|
+
seg_CDB1.show()
|
|
39
|
+
img_CDB1 = Image.fromarray(thin_vessels_seg_CDB1)
|
|
40
|
+
img_CDB1.show()
|
|
41
|
+
```
|
|
42
|
+
<img src="tests/imgs/DRIVE_seg_example.png" alt="DRIVE_thin_vessels_example" width=300/>
|
|
43
|
+
<img src="tests/imgs/DRIVE_seg_thin_example.png" alt="DRIVE_thin_vessels_example" width=300/>
|
|
44
|
+
<img src="tests/imgs/CHASEDB1_seg_example.png" alt="CHASEDB1_thin_vessels_example" width=300/>
|
|
45
|
+
<img src="tests/imgs/CHASEDB1_seg_thin_example.png" alt="CHASEDB1_thin_vessels_example" width=300/>
|
|
46
|
+
|
|
47
|
+
Furthermore, to demonstrate the metric calculation functions, you can run the code below. It compares the overall metrics (calculated with scikit-learn) to the thin-vessel-specific metrics calculated by this package.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# Load the ground truth segmentation mask and a sample prediction
|
|
51
|
+
pred = Image.open(f"tests/imgs/DRIVE_pred_example.png")
|
|
52
|
+
seg_DRIVE = Image.open(f"tests/imgs/DRIVE_seg_example.png").resize((pred.size), Image.NEAREST)
|
|
53
|
+
|
|
54
|
+
# Binarize images to a 0/1 format for scikit-learn compatibility
|
|
55
|
+
seg_DRIVE = np.where(np.array(seg_DRIVE) > 0, 1, 0)
|
|
56
|
+
pred = np.where(np.array(pred) > 0, 1, 0)
|
|
57
|
+
|
|
58
|
+
# Compute and print the metrics
|
|
59
|
+
print(f"Overall Recall score: {recall_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
60
|
+
print(f"Recall score on thin vessels: {recall_thin_vessels(seg_DRIVE, pred)}")
|
|
61
|
+
print("-" * 30)
|
|
62
|
+
print(f"Overall Precision score: {precision_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
63
|
+
print(f"Precision score on thin Vessels: {precision_thin_vessels(seg_DRIVE, pred)}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If the program is running correctly with the provided sample images, the results should be similar to this:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
Overall Recall score: 0.8553852359822509
|
|
70
|
+
Recall score on thin vessels: 0.751244555071562
|
|
71
|
+
------------------------------
|
|
72
|
+
Overall Precision score: 0.8422369623068674
|
|
73
|
+
Precision score on thin Vessels: 0.6527915897144481
|
|
74
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "retinal_thin_vessels"
|
|
7
|
+
version = "1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "João Paulo Menezes Linaris", email = "joaolinaris@gmail.com"},
|
|
10
|
+
]
|
|
11
|
+
description = "A Python package for analyzing thin retinal vessels and creating vessels-thickness based weight maps"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
keywords = ["retina", "retinal", "retinal-vessels", "vessel-segmentation",
|
|
17
|
+
"image-analysis", "medical-imaging", "deep-learning",
|
|
18
|
+
"weight-map", "pytorch", "thin-vessels", "image-segmentation", "binary-masks",
|
|
19
|
+
"binary-mask"]
|
|
20
|
+
classifiers = [
|
|
21
|
+
'Intended Audience :: Science/Research',
|
|
22
|
+
'Operating System :: POSIX :: Linux',
|
|
23
|
+
'Programming Language :: Python :: 3',
|
|
24
|
+
'Programming Language :: Python :: 3.8',
|
|
25
|
+
'Programming Language :: Python :: 3.9',
|
|
26
|
+
'Programming Language :: Python :: 3.10',
|
|
27
|
+
'Programming Language :: Python :: 3.11',
|
|
28
|
+
'Programming Language :: Python :: 3.12',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
dependencies = [
|
|
32
|
+
"numpy",
|
|
33
|
+
"torch",
|
|
34
|
+
"scikit-image",
|
|
35
|
+
"Pillow",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/J-Linaris/retinal_thin_vessels"
|
|
40
|
+
Repository = "https://github.com/J-Linaris/retinal_thin_vessels"
|
|
41
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from skimage.morphology import medial_axis, area_closing
|
|
3
|
+
from PIL import Image
|
|
4
|
+
from retinal_thin_vessels.input_transformation import prepare_ground_truth, prepare_prediction
|
|
5
|
+
from retinal_thin_vessels.external.DSE_skeleton_pruning.dsepruning.dsepruning import skel_pruning_DSE
|
|
6
|
+
|
|
7
|
+
def __get_shift_tuples(value):
|
|
8
|
+
|
|
9
|
+
# Sets the radius
|
|
10
|
+
radius = int(np.ceil(value))
|
|
11
|
+
|
|
12
|
+
# Creates all combinations of shifts based on the raidus
|
|
13
|
+
x_shifts, y_shifts = np.meshgrid(np.arange(-radius, radius + 1), np.arange(-radius, radius + 1))
|
|
14
|
+
|
|
15
|
+
# Stacks all shifts together on a list
|
|
16
|
+
shifts = np.column_stack((x_shifts.ravel(), y_shifts.ravel()))
|
|
17
|
+
# shifts = [(dx, dy) for dx, dy in shifts if (dx, dy) != (0, 0)] # Returns without (0,0)
|
|
18
|
+
|
|
19
|
+
return shifts
|
|
20
|
+
|
|
21
|
+
def __get_filtered_mask(seg_mask, ceil=1.0):
|
|
22
|
+
"""
|
|
23
|
+
Returns a [H,W] shaped numpy array containing the thin-vessels only
|
|
24
|
+
mask.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Application of closing on the segmentation mask
|
|
28
|
+
closed_seg_mask = area_closing(seg_mask)
|
|
29
|
+
|
|
30
|
+
# Obtaining the skeleton
|
|
31
|
+
skeleton_medial_axis, distances = medial_axis(closed_seg_mask, return_distance=True)
|
|
32
|
+
|
|
33
|
+
# Skeleton pruning
|
|
34
|
+
skeleton_medial_axis = skel_pruning_DSE(skeleton_medial_axis, distances, np.ceil(distances.max()))
|
|
35
|
+
|
|
36
|
+
# Compute the skeleton with the values of the distances
|
|
37
|
+
dist_skel = np.where(skeleton_medial_axis>0, distances, 0)
|
|
38
|
+
|
|
39
|
+
# Get unique values of dist_skel excluding 0 (values of the radius of vessels)
|
|
40
|
+
values_dist_skel = np.unique(dist_skel)[1:]
|
|
41
|
+
|
|
42
|
+
#~~~~~~~~~~~~~~~~~~~~~~~Segmentation mask recriation with thin vessels only~~~~~~~~~~~~~~~~~~
|
|
43
|
+
|
|
44
|
+
# Initializes the two necessary masks
|
|
45
|
+
filtered_seg_mask = np.zeros(dist_skel.shape)
|
|
46
|
+
reconstructed_seg_mask = np.zeros(dist_skel.shape)
|
|
47
|
+
|
|
48
|
+
height = len(dist_skel)
|
|
49
|
+
width = len(dist_skel[0])
|
|
50
|
+
|
|
51
|
+
# Reconstructs each mask using a sphere of varying radius
|
|
52
|
+
for value in values_dist_skel:
|
|
53
|
+
shifts = __get_shift_tuples(value)
|
|
54
|
+
|
|
55
|
+
for i in range(height):
|
|
56
|
+
for j in range(width):
|
|
57
|
+
|
|
58
|
+
if dist_skel[i][j] == value :
|
|
59
|
+
|
|
60
|
+
if value <= ceil:
|
|
61
|
+
for dx, dy in shifts:
|
|
62
|
+
if 0 <= i+dx < height and 0 <= j+dy < width:
|
|
63
|
+
filtered_seg_mask[i+dx][j+dy] = 255
|
|
64
|
+
|
|
65
|
+
for dx, dy in shifts:
|
|
66
|
+
if 0 <= i+dx < height and 0 <= j+dy < width:
|
|
67
|
+
reconstructed_seg_mask[i+dx][j+dy] = 255
|
|
68
|
+
|
|
69
|
+
# Filtering to get exactly the shape of the vessels intead of something rounded
|
|
70
|
+
filtered_seg_mask = np.where((seg_mask>0) & (filtered_seg_mask>0), 255, 0).astype(np.uint8)
|
|
71
|
+
reconstructed_seg_mask = np.where((seg_mask>0) & (reconstructed_seg_mask>0), 255, 0).astype(np.uint8)
|
|
72
|
+
|
|
73
|
+
# Gets exactly the excluded vessels
|
|
74
|
+
excluded_vessels = np.where((seg_mask>0) & (reconstructed_seg_mask==0), 255, 0).astype(np.uint8)
|
|
75
|
+
|
|
76
|
+
# Concatenation of excluded_vessels seg mask with the thin vessels mask (we garantee they are small due to
|
|
77
|
+
# their exclusion in the prunning/closing process)
|
|
78
|
+
filtered_seg_mask = np.where((filtered_seg_mask>0) | (excluded_vessels>0), 255, 0).astype(np.uint8)
|
|
79
|
+
|
|
80
|
+
return filtered_seg_mask
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_thin_vessels_mask(seg_mask, ceil=1.0, mask_type="ground_truth"):
|
|
84
|
+
"""
|
|
85
|
+
Returns a numpy array containing the thin-vessels only mask.
|
|
86
|
+
|
|
87
|
+
The Input is expected to be a segmentation mask, therefore, the
|
|
88
|
+
function accepts inputs with:
|
|
89
|
+
|
|
90
|
+
-> shape: (H,W); (1,H,W); (N,1,H,W)
|
|
91
|
+
|
|
92
|
+
-> values: BINARY
|
|
93
|
+
|
|
94
|
+
-> mask_type: "ground_truth" or "prediction" (affects how we
|
|
95
|
+
transform the provided mask beofre computing the
|
|
96
|
+
filtered one. If equal to "groun_truth", will
|
|
97
|
+
apply a threshold of 0 for classifying pixels in the
|
|
98
|
+
image)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
Thin vessels are defined as those whose radius is less than or
|
|
102
|
+
equal to 'ceil'.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# Input value verification
|
|
106
|
+
accepted_mask_type_values = ["ground_truth", "prediction"]
|
|
107
|
+
|
|
108
|
+
if mask_type not in accepted_mask_type_values:
|
|
109
|
+
raise ValueError(f"Expected valid mask type. Accepted values: {accepted_mask_type_values}. Got: {mask_type}")
|
|
110
|
+
|
|
111
|
+
# Input preparation
|
|
112
|
+
if mask_type == "ground_truth":
|
|
113
|
+
seg_mask = prepare_ground_truth(seg_mask)
|
|
114
|
+
else:
|
|
115
|
+
seg_mask = prepare_prediction(seg_mask)
|
|
116
|
+
|
|
117
|
+
input_dimension = seg_mask.ndim
|
|
118
|
+
|
|
119
|
+
# Get the filtered mask(s)
|
|
120
|
+
filtered_seg_mask = []
|
|
121
|
+
if input_dimension == 4:
|
|
122
|
+
|
|
123
|
+
# Obtain each mask separately
|
|
124
|
+
for i in range(len(seg_mask)):
|
|
125
|
+
#[H,W] ---> [1,H,W]
|
|
126
|
+
unique_filtered_seg_mask = np.expand_dims(__get_filtered_mask(seg_mask[i][0], ceil), axis=0)
|
|
127
|
+
|
|
128
|
+
# Appends to the filtered_seg_mask vector (naturally, this vector will be [N,1,H,W])
|
|
129
|
+
filtered_seg_mask.append(unique_filtered_seg_mask)
|
|
130
|
+
|
|
131
|
+
# Converts into a numpy array
|
|
132
|
+
filtered_seg_mask = np.array(filtered_seg_mask)
|
|
133
|
+
|
|
134
|
+
elif input_dimension == 3:
|
|
135
|
+
|
|
136
|
+
# Obtains the mask using the reduced dimension mask
|
|
137
|
+
filtered_seg_mask = __get_filtered_mask(seg_mask[0], ceil)
|
|
138
|
+
|
|
139
|
+
# Expands the dimension again for maintaining consistency with input shape
|
|
140
|
+
filtered_seg_mask = np.expand_dims(filtered_seg_mask, axis=0)
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
# Obtains the mask
|
|
144
|
+
filtered_seg_mask = __get_filtered_mask(seg_mask, ceil)
|
|
145
|
+
|
|
146
|
+
return filtered_seg_mask
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
|
|
150
|
+
# Loads the data
|
|
151
|
+
example_components_path = "../tests/imgs/"
|
|
152
|
+
seg = Image.open(f"{example_components_path}DRIVE_seg_example.png")
|
|
153
|
+
|
|
154
|
+
# Gets the filtered mask with only thin vessels
|
|
155
|
+
|
|
156
|
+
#-----> for a [N,1,H,W] shape input
|
|
157
|
+
seg_4_dims = np.expand_dims(np.expand_dims(np.array(seg), axis=0),axis=0)
|
|
158
|
+
seg_4_dims = np.concatenate((seg_4_dims,seg_4_dims,seg_4_dims), axis=0)
|
|
159
|
+
|
|
160
|
+
thin_vessels_seg = get_thin_vessels_mask(seg_4_dims)
|
|
161
|
+
print(f"Test 1: original shape: {seg_4_dims.shape} ---> filtered mask shape: {thin_vessels_seg.shape}")
|
|
162
|
+
|
|
163
|
+
#-----> for a [1,H,W] shape input
|
|
164
|
+
seg_3_dims = np.expand_dims(np.array(seg), axis=0)
|
|
165
|
+
|
|
166
|
+
thin_vessels_seg = get_thin_vessels_mask(seg_3_dims)
|
|
167
|
+
print(f"Test 2: original shape: {seg_3_dims.shape} ---> filtered mask shape: {thin_vessels_seg.shape}")
|
|
168
|
+
|
|
169
|
+
#-----> for a [H,W] shape input
|
|
170
|
+
thin_vessels_seg = get_thin_vessels_mask(seg)
|
|
171
|
+
print(f"Test 3: original shape: {np.array(seg).shape} ---> filtered mask shape: {thin_vessels_seg.shape}")
|
|
172
|
+
|
|
173
|
+
# Shows the filtered segmentation mask
|
|
174
|
+
print("Showing the filtered segmentation mask with thin vessels only.")
|
|
175
|
+
img = Image.fromarray(thin_vessels_seg)
|
|
176
|
+
img.show()
|
|
177
|
+
|
|
178
|
+
exit(0)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|
retinal_thin_vessels-1.0/retinal_thin_vessels/external/DSE_skeleton_pruning/dsepruning/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from dsepruning.dsepruning import skel_pruning_DSE
|
retinal_thin_vessels-1.0/retinal_thin_vessels/external/DSE_skeleton_pruning/dsepruning/dsepruning.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import sknw
|
|
2
|
+
import numpy as np
|
|
3
|
+
from skimage.draw import line
|
|
4
|
+
from dsepruning.dse_helper import recnstrc_by_disk, get_weight
|
|
5
|
+
# from .dse_helper import recnstrc_by_disk, get_weight -----> original (didn't work)
|
|
6
|
+
|
|
7
|
+
def flatten(l):
|
|
8
|
+
return [item for sublist in l for item in sublist]
|
|
9
|
+
|
|
10
|
+
def _remove_branch_by_DSE(G, recn, dist, max_px_weight, checked_terminal=set()):
|
|
11
|
+
deg = dict(G.degree())
|
|
12
|
+
terminal_points = [i for i, d in deg.items() if d == 1]
|
|
13
|
+
edges = list(G.edges())
|
|
14
|
+
# temporary branch reconstruction mask
|
|
15
|
+
branch_recn = np.zeros_like(recn, dtype=np.int32)
|
|
16
|
+
branch_recn = np.zeros_like(recn, dtype=np.int32)
|
|
17
|
+
for s, e in edges:
|
|
18
|
+
if s == e:
|
|
19
|
+
G.remove_edge(s, e)
|
|
20
|
+
continue
|
|
21
|
+
vals = flatten([[v] for v in G[s][e].values()])
|
|
22
|
+
for ix, val in enumerate(vals):
|
|
23
|
+
if s not in terminal_points and e not in terminal_points:
|
|
24
|
+
continue
|
|
25
|
+
if s in checked_terminal or e in checked_terminal:
|
|
26
|
+
continue
|
|
27
|
+
pts = val.get('pts').tolist()
|
|
28
|
+
pts.append(G.nodes[s]['o'].astype(np.int32).tolist())
|
|
29
|
+
pts.append(G.nodes[e]['o'].astype(np.int32).tolist())
|
|
30
|
+
recnstrc_by_disk(np.array(pts, dtype=np.int32), dist, branch_recn)
|
|
31
|
+
weight = get_weight(recn, branch_recn)
|
|
32
|
+
if s in terminal_points:
|
|
33
|
+
checked_terminal.add(s)
|
|
34
|
+
if weight < max_px_weight:
|
|
35
|
+
G.remove_node(s)
|
|
36
|
+
recn = recn - branch_recn
|
|
37
|
+
if e in terminal_points:
|
|
38
|
+
checked_terminal.add(e)
|
|
39
|
+
if weight < max_px_weight:
|
|
40
|
+
G.remove_node(e)
|
|
41
|
+
recn = recn - branch_recn
|
|
42
|
+
return G, recn
|
|
43
|
+
|
|
44
|
+
def _remove_mid_node(G):
|
|
45
|
+
start_index = 0
|
|
46
|
+
while True:
|
|
47
|
+
nodes = [x for x in G.nodes() if G.degree(x) == 2]
|
|
48
|
+
if len(nodes) == start_index:
|
|
49
|
+
break
|
|
50
|
+
i = nodes[start_index]
|
|
51
|
+
nbs = list(G[i])
|
|
52
|
+
# assert len(nbs)==2, 'degree not match'
|
|
53
|
+
if len(nbs) != 2:
|
|
54
|
+
start_index = start_index + 1
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
edge1 = G[i][nbs[0]][0]
|
|
58
|
+
edge2 = G[i][nbs[1]][0]
|
|
59
|
+
|
|
60
|
+
s1, e1 = edge1['pts'][0], edge1['pts'][-1]
|
|
61
|
+
s2, e2 = edge2['pts'][0], edge2['pts'][-1]
|
|
62
|
+
dist = np.array(list(map(np.linalg.norm, [s1-s2, e1-e2, s1-e2, s2-e1])))
|
|
63
|
+
if dist.argmin() == 0:
|
|
64
|
+
line = np.concatenate([edge1['pts'][::-1], [G.nodes[i]['o'].astype(np.int32)], edge2['pts']], axis=0)
|
|
65
|
+
elif dist.argmin() == 1:
|
|
66
|
+
line = np.concatenate([edge1['pts'], [G.nodes[i]['o'].astype(np.int32)], edge2['pts'][::-1]], axis=0)
|
|
67
|
+
elif dist.argmin() == 2:
|
|
68
|
+
line = np.concatenate([edge2['pts'], [G.nodes[i]['o'].astype(np.int32)], edge1['pts']], axis=0)
|
|
69
|
+
elif dist.argmin() == 3:
|
|
70
|
+
line = np.concatenate([edge1['pts'], [G.nodes[i]['o'].astype(np.int32)], edge2['pts']], axis=0)
|
|
71
|
+
G.add_edge(nbs[0], nbs[1], weight=edge1['weight']+edge2['weight'], pts=line)
|
|
72
|
+
G.remove_node(i)
|
|
73
|
+
return G
|
|
74
|
+
|
|
75
|
+
def skel_pruning_DSE(skel, dist, min_area_px=100, return_graph=False):
|
|
76
|
+
"""Skeleton pruning using dse
|
|
77
|
+
|
|
78
|
+
Arguments:
|
|
79
|
+
skel {ndarray} -- skeleton obtained from skeletonization algorithm
|
|
80
|
+
dist {ndarray} -- distance transfrom map
|
|
81
|
+
|
|
82
|
+
Keyword Arguments:
|
|
83
|
+
min_area_px {int} -- branch reconstruction weights, measured by pixel area. Branch reconstruction weights smaller than this threshold will be pruned. (default: {100})
|
|
84
|
+
return_graph {bool} -- return graph
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
ndarray -- pruned skeleton map
|
|
88
|
+
"""
|
|
89
|
+
graph = sknw.build_sknw(skel, multi=True)
|
|
90
|
+
dist = dist.astype(np.int32)
|
|
91
|
+
graph = _remove_mid_node(graph)
|
|
92
|
+
edges = list(set(graph.edges()))
|
|
93
|
+
pts = []
|
|
94
|
+
for s, e in edges:
|
|
95
|
+
vals = flatten([[v] for v in graph[s][e].values()])
|
|
96
|
+
for ix, val in enumerate(vals):
|
|
97
|
+
pts.extend(val.get('pts').tolist())
|
|
98
|
+
pts.append(graph.nodes[s]['o'].astype(np.int32).tolist())
|
|
99
|
+
pts.append(graph.nodes[e]['o'].astype(np.int32).tolist())
|
|
100
|
+
recnstrc = np.zeros_like(dist, dtype=np.int32)
|
|
101
|
+
recnstrc_by_disk(np.array(pts, dtype=np.int32), dist, recnstrc)
|
|
102
|
+
num_nodes = len(graph.nodes())
|
|
103
|
+
checked_terminal = set()
|
|
104
|
+
while True:
|
|
105
|
+
# cannot combine with other pruning method because the reconstruction map is not updated in other approach
|
|
106
|
+
graph, recnstrc = _remove_branch_by_DSE(graph, recnstrc, dist, min_area_px, checked_terminal=checked_terminal)
|
|
107
|
+
if len(graph.nodes()) == num_nodes:
|
|
108
|
+
break
|
|
109
|
+
graph = _remove_mid_node(graph)
|
|
110
|
+
num_nodes = len(graph.nodes())
|
|
111
|
+
if return_graph:
|
|
112
|
+
return graph2im(graph, skel.shape), graph
|
|
113
|
+
else:
|
|
114
|
+
return graph2im(graph, skel.shape)
|
|
115
|
+
|
|
116
|
+
def graph2im(graph, shape):
|
|
117
|
+
mask = np.zeros(shape, dtype=bool)
|
|
118
|
+
for s,e in graph.edges():
|
|
119
|
+
vals = flatten([[v] for v in graph[s][e].values()])
|
|
120
|
+
for val in vals:
|
|
121
|
+
coords = val.get('pts')
|
|
122
|
+
coords_1 = np.roll(coords, -1, axis=0)
|
|
123
|
+
for i in range(len(coords)-1):
|
|
124
|
+
rr, cc = line(*coords[i], *coords_1[i])
|
|
125
|
+
mask[rr, cc] = True
|
|
126
|
+
mask[tuple(graph.nodes[s]['pts'].T.tolist())] = True
|
|
127
|
+
mask[tuple(graph.nodes[e]['pts'].T.tolist())] = True
|
|
128
|
+
return mask
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from setuptools import setup, Extension
|
|
2
|
+
import numpy
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
ext_modules=[
|
|
6
|
+
Extension(
|
|
7
|
+
'dsepruning.dse_helper',
|
|
8
|
+
['dsepruning/dse_helper.pyx'],
|
|
9
|
+
include_dirs=[numpy.get_include()],
|
|
10
|
+
extra_compile_args=['-fopenmp'],
|
|
11
|
+
extra_link_args=['-fopenmp']
|
|
12
|
+
)
|
|
13
|
+
]
|
|
14
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from skimage.morphology import medial_axis, area_closing
|
|
3
|
+
from PIL import Image
|
|
4
|
+
from retinal_thin_vessels.external.DSE_skeleton_pruning.dsepruning.dsepruning import skel_pruning_DSE
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
|
|
8
|
+
seg_mask = np.array(Image.open("imgs/DRIVE_seg_example.png"))
|
|
9
|
+
print(seg_mask.shape)
|
|
10
|
+
exit(0)
|
|
11
|
+
# Application of closing on the segmentation mask
|
|
12
|
+
closed_seg_mask = area_closing(seg_mask)
|
|
13
|
+
|
|
14
|
+
# Obtaining the skeleton
|
|
15
|
+
skeleton_medial_axis, distances = medial_axis(closed_seg_mask, return_distance=True)
|
|
16
|
+
|
|
17
|
+
# Skeleton prunning
|
|
18
|
+
skeleton_medial_axis = skel_pruning_DSE(skeleton_medial_axis, distances, np.ceil(distances.max()))
|
|
19
|
+
|
|
20
|
+
# Compute the skeleton with the values of the distances
|
|
21
|
+
dist_skel = np.where(skeleton_medial_axis>0, distances, 0)
|
|
22
|
+
|
|
23
|
+
# Get unique values of dist_skel excluding 0 (values of the radius of vessels)
|
|
24
|
+
values_dist_skel = np.unique(dist_skel)[1:]
|
|
25
|
+
|
|
26
|
+
#~~~~~~~~~~~~~~~~~~~~~~~Segmentation mask recriation with thin vessels only~~~~~~~~~~~~~~~~~~
|
|
27
|
+
|
|
28
|
+
# Initializes the two necessary masks
|
|
29
|
+
filtered_seg_mask = np.zeros(dist_skel.shape)
|
|
30
|
+
reconstructed_seg_mask = np.zeros(dist_skel.shape)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
exit(0)
|
|
34
|
+
|
|
35
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import numpy as np
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
def _to_numpy(arr):
|
|
6
|
+
"""
|
|
7
|
+
Converts torch.Tensor or PIL.Image
|
|
8
|
+
to numpy array if needed.
|
|
9
|
+
"""
|
|
10
|
+
if isinstance(arr, Image.Image):
|
|
11
|
+
arr = np.array(arr, copy=True)
|
|
12
|
+
elif isinstance(arr, torch.Tensor):
|
|
13
|
+
arr = arr.detach().cpu().numpy()
|
|
14
|
+
elif not isinstance(arr, np.ndarray):
|
|
15
|
+
raise TypeError(f"Unsupported input type: {type(arr)}")
|
|
16
|
+
return arr
|
|
17
|
+
|
|
18
|
+
def _verify_input_validity(seg_mask, input_type="y_true"):
|
|
19
|
+
"""
|
|
20
|
+
Inputs can be NumPy arrays,
|
|
21
|
+
torch.Tensors, or PIL Images.
|
|
22
|
+
|
|
23
|
+
(1, H, W) or (N,1,H,W) or (H,W)
|
|
24
|
+
Inputs must have shape: (1, H, W), with values in {0, 1}, {0, 255},
|
|
25
|
+
or [0.0, 1.0] for probability maps.
|
|
26
|
+
|
|
27
|
+
Thin vessels are defined as those with radius less than or
|
|
28
|
+
equal to ceil.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
seg_mask = _to_numpy(seg_mask)
|
|
32
|
+
|
|
33
|
+
# Ensures inputs have valid shape
|
|
34
|
+
if (int(seg_mask.ndim) not in [2,3,4]) or (seg_mask.ndim == 3 and seg_mask.shape[0] != 1) or (seg_mask.ndim == 4 and seg_mask.shape[1] != 1):
|
|
35
|
+
raise ValueError(f"Accepted shapes: (1, H, W) or (N,1,H,W) or (H,W). Got seg_mask: {seg_mask.shape} for {input_type}")
|
|
36
|
+
|
|
37
|
+
# Verifies the validity of the values in the passed array
|
|
38
|
+
input_values = np.unique(seg_mask)
|
|
39
|
+
|
|
40
|
+
if len(input_values) != 2:
|
|
41
|
+
raise ValueError(f"Expected binary input for {input_type}. Got input values: {input_values}")
|
|
42
|
+
|
|
43
|
+
# Binarize input if necessary
|
|
44
|
+
if (input_type == "y_true" and int(input_values[0]) != 0) or (input_type == "y_pred" and int(input_values[0]) != 0):
|
|
45
|
+
seg_mask = (seg_mask - seg_mask.min())/(seg_mask.max()-seg_mask.min())
|
|
46
|
+
|
|
47
|
+
return seg_mask
|
|
48
|
+
|
|
49
|
+
def prepare_ground_truth(y_true):
|
|
50
|
+
|
|
51
|
+
# Verifies validity of y_true
|
|
52
|
+
y_true = _verify_input_validity(y_true, "y_true")
|
|
53
|
+
|
|
54
|
+
# # Removes the channel dimension for input of shape [1,H,W]
|
|
55
|
+
# if y_true.ndim == 3:
|
|
56
|
+
# y_true = y_true[0] #[1,H,W] ---> [H,W]
|
|
57
|
+
|
|
58
|
+
# Ensures the values belong to {0,1}
|
|
59
|
+
y_true = (y_true > 0).astype(np.uint8)
|
|
60
|
+
|
|
61
|
+
return y_true
|
|
62
|
+
|
|
63
|
+
def prepare_prediction(y_pred):
|
|
64
|
+
|
|
65
|
+
# Verifies validity of y_true
|
|
66
|
+
y_pred = _verify_input_validity(y_pred, "y_pred")
|
|
67
|
+
|
|
68
|
+
# # Removes the channel dimension for input of shape [1,H,W]
|
|
69
|
+
# if y_pred.ndim == 3:
|
|
70
|
+
# y_pred = y_pred[0] #[1,H,W] ---> [H,W]
|
|
71
|
+
|
|
72
|
+
# Ensures the values belong to {0,1}
|
|
73
|
+
y_pred = (y_pred > 0.5).astype(np.uint8)
|
|
74
|
+
|
|
75
|
+
return y_pred
|
|
76
|
+
|
|
77
|
+
def prepare_input(y_true, y_pred):
|
|
78
|
+
|
|
79
|
+
# Prepare both inputs
|
|
80
|
+
y_true, y_pred = prepare_ground_truth(y_true), prepare_prediction(y_pred)
|
|
81
|
+
|
|
82
|
+
# Ensures inputs have the same dimensions (Height and Width)
|
|
83
|
+
# if (y_true.shape[-1] != y_pred.shape[-1] or y_true.shape[-2] != y_pred.shape[-2]):
|
|
84
|
+
if y_true.shape != y_pred.shape:
|
|
85
|
+
raise ValueError(f"Expected both inputs to have the same dimensions. Got y_true: {y_true.shape}, y_pred: {y_pred.shape}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return y_true, y_pred
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PIL import Image
|
|
3
|
+
from retinal_thin_vessels.core import get_thin_vessels_mask
|
|
4
|
+
from retinal_thin_vessels.input_transformation import prepare_input
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
def __recall_thin_vessels_single_image(y_true, y_pred, ceil=1.0):
|
|
8
|
+
|
|
9
|
+
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Input preparation~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
10
|
+
|
|
11
|
+
true_copy, pred_copy = prepare_input(y_true, y_pred)
|
|
12
|
+
|
|
13
|
+
new_seg_true = get_thin_vessels_mask(true_copy, ceil)
|
|
14
|
+
|
|
15
|
+
# Calculates Recall
|
|
16
|
+
tp = np.sum((new_seg_true > 0) & (pred_copy > 0))
|
|
17
|
+
fn = np.sum((new_seg_true > 0) & (pred_copy == 0))
|
|
18
|
+
|
|
19
|
+
return tp/(tp+fn)
|
|
20
|
+
|
|
21
|
+
def __precision_thin_vessels_single_image(y_true, y_pred, ceil=1.0):
|
|
22
|
+
|
|
23
|
+
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Input preparation~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
24
|
+
|
|
25
|
+
true_copy, pred_copy = prepare_input(y_true, y_pred)
|
|
26
|
+
|
|
27
|
+
new_seg_pred = get_thin_vessels_mask(pred_copy, ceil)
|
|
28
|
+
|
|
29
|
+
# Calculates Precision
|
|
30
|
+
tp = np.sum((new_seg_pred > 0) & (true_copy > 0))
|
|
31
|
+
fp = np.sum((new_seg_pred > 0) & (true_copy == 0))
|
|
32
|
+
|
|
33
|
+
return tp/(tp+fp)
|
|
34
|
+
|
|
35
|
+
def __f1_thin_vessels_single_image(y_true, y_pred, ceil=1.0):
|
|
36
|
+
|
|
37
|
+
r = __recall_thin_vessels_single_image(y_true, y_pred, ceil)
|
|
38
|
+
p = __precision_thin_vessels_single_image(y_true, y_pred, ceil)
|
|
39
|
+
f1 = 2*p*r/(p+r)
|
|
40
|
+
|
|
41
|
+
return f1
|
|
42
|
+
|
|
43
|
+
def recall_thin_vessels(y_true, y_pred, ceil=1.0):
|
|
44
|
+
"""
|
|
45
|
+
Returns the recall score on thin vessels given the predicted and
|
|
46
|
+
the ground-truth segmentation masks. Inputs can be NumPy arrays,
|
|
47
|
+
torch.Tensors, or PIL Images.
|
|
48
|
+
|
|
49
|
+
Inputs must have shape: (1, H, W), with values in {0, 1}, {0, 255},
|
|
50
|
+
or [0.0, 1.0] for probability maps.
|
|
51
|
+
|
|
52
|
+
Thin vessels are defined as those whose radius is less than or
|
|
53
|
+
equal to 'ceil'.
|
|
54
|
+
"""
|
|
55
|
+
# Prepares the input
|
|
56
|
+
|
|
57
|
+
y_true, y_pred = prepare_input(y_true, y_pred)
|
|
58
|
+
|
|
59
|
+
# Computes the metric
|
|
60
|
+
inputs_dimension = y_true.ndim
|
|
61
|
+
num_imgs = len(y_true)
|
|
62
|
+
|
|
63
|
+
if inputs_dimension == 4:
|
|
64
|
+
recall = 0
|
|
65
|
+
for i in range(num_imgs):
|
|
66
|
+
recall+=__recall_thin_vessels_single_image(y_true[i][0], y_pred[i][0], ceil)
|
|
67
|
+
recall/=num_imgs
|
|
68
|
+
|
|
69
|
+
else:
|
|
70
|
+
if inputs_dimension == 3:
|
|
71
|
+
y_true = y_true[0]
|
|
72
|
+
y_pred = y_pred[0]
|
|
73
|
+
|
|
74
|
+
recall = __recall_thin_vessels_single_image(y_true, y_pred, ceil)
|
|
75
|
+
|
|
76
|
+
return recall
|
|
77
|
+
|
|
78
|
+
def precision_thin_vessels(y_true, y_pred, ceil=1.0):
|
|
79
|
+
"""
|
|
80
|
+
Returns the precision score on thin vessels given the predicted and
|
|
81
|
+
the ground-truth segmentation masks. Inputs can be NumPy arrays,
|
|
82
|
+
torch.Tensors, or PIL Images.
|
|
83
|
+
|
|
84
|
+
Inputs must have shape: (1, H, W), with values in {0, 1}, {0, 255},
|
|
85
|
+
or [0.0, 1.0] for probability maps.
|
|
86
|
+
|
|
87
|
+
Thin vessels are defined as those whose radius is less than or
|
|
88
|
+
equal to 'ceil'.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# Prepares the input
|
|
92
|
+
|
|
93
|
+
y_true, y_pred = prepare_input(y_true, y_pred)
|
|
94
|
+
|
|
95
|
+
# Computes the metric
|
|
96
|
+
inputs_dimension = y_true.ndim
|
|
97
|
+
num_imgs = len(y_true)
|
|
98
|
+
|
|
99
|
+
if inputs_dimension == 4:
|
|
100
|
+
precision = 0
|
|
101
|
+
for i in range(num_imgs):
|
|
102
|
+
precision+=__precision_thin_vessels_single_image(y_true[i][0], y_pred[i][0], ceil)
|
|
103
|
+
precision/=num_imgs
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
if inputs_dimension == 3:
|
|
107
|
+
y_true = y_true[0]
|
|
108
|
+
y_pred = y_pred[0]
|
|
109
|
+
|
|
110
|
+
precision = __precision_thin_vessels_single_image(y_true, y_pred, ceil)
|
|
111
|
+
|
|
112
|
+
return precision
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def f1_thin_vessels(y_true, y_pred, ceil):
|
|
116
|
+
"""
|
|
117
|
+
Returns the f1-score on thin vessels given the predicted and
|
|
118
|
+
the ground-truth segmentation masks. Inputs can be NumPy arrays,
|
|
119
|
+
torch.Tensors, or PIL Images.
|
|
120
|
+
|
|
121
|
+
Inputs must have shape: (1, H, W), with values in {0, 1}, {0, 255},
|
|
122
|
+
or [0.0, 1.0] for probability maps.
|
|
123
|
+
|
|
124
|
+
Thin vessels are defined as those whose radius is less than or
|
|
125
|
+
equal to 'ceil'.
|
|
126
|
+
"""
|
|
127
|
+
# Prepares the input
|
|
128
|
+
y_true, y_pred = prepare_input(y_true, y_pred)
|
|
129
|
+
|
|
130
|
+
# Computes the metric
|
|
131
|
+
inputs_dimension = y_true.ndim
|
|
132
|
+
num_imgs = len(y_true)
|
|
133
|
+
|
|
134
|
+
if inputs_dimension == 4:
|
|
135
|
+
f1 = 0
|
|
136
|
+
for i in range(num_imgs):
|
|
137
|
+
f1 += __f1_thin_vessels_single_image(y_true[i][0], y_pred[i][0], ceil)
|
|
138
|
+
|
|
139
|
+
f1 /= num_imgs
|
|
140
|
+
|
|
141
|
+
else:
|
|
142
|
+
if inputs_dimension == 3:
|
|
143
|
+
y_true = y_true[0]
|
|
144
|
+
y_pred = y_pred[0]
|
|
145
|
+
|
|
146
|
+
f1 = __f1_thin_vessels_single_image(y_true, y_pred, ceil)
|
|
147
|
+
|
|
148
|
+
return f1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main():
|
|
152
|
+
|
|
153
|
+
example_components_path = "../tests/imgs/"
|
|
154
|
+
seg = Image.open(f"{example_components_path}DRIVE_seg_example.png")
|
|
155
|
+
pred = Image.open(f"{example_components_path}DRIVE_pred_example.png")
|
|
156
|
+
|
|
157
|
+
ti = time.time()
|
|
158
|
+
print(f"Precision on thin vessels: {precision_thin_vessels(seg.resize(pred.size, Image.NEAREST), pred)}")
|
|
159
|
+
tf = time.time()
|
|
160
|
+
delta = tf-ti
|
|
161
|
+
print(f"Running time for image of shape {seg.size}: {delta:.4f} sec")
|
|
162
|
+
|
|
163
|
+
ti = time.time()
|
|
164
|
+
print(f"Recall on thin vessels: {recall_thin_vessels(seg.resize(pred.size, Image.NEAREST), pred)}")
|
|
165
|
+
tf = time.time()
|
|
166
|
+
delta = tf-ti
|
|
167
|
+
print(f"Running time for image of shape {seg.size}: {delta:.4f} sec")
|
|
168
|
+
exit(0)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
main()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: retinal_thin_vessels
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: A Python package for analyzing thin retinal vessels and creating vessels-thickness based weight maps
|
|
5
|
+
Author-email: João Paulo Menezes Linaris <joaolinaris@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/J-Linaris/retinal_thin_vessels
|
|
8
|
+
Project-URL: Repository, https://github.com/J-Linaris/retinal_thin_vessels
|
|
9
|
+
Keywords: retina,retinal,retinal-vessels,vessel-segmentation,image-analysis,medical-imaging,deep-learning,weight-map,pytorch,thin-vessels,image-segmentation,binary-masks,binary-mask
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: torch
|
|
23
|
+
Requires-Dist: scikit-image
|
|
24
|
+
Requires-Dist: Pillow
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# retinal_thin_vessels
|
|
28
|
+
|
|
29
|
+
A Python package for computing the recall and precision scores specifically on thin vessels in retinal images, as detailed in the paper "Vessel-Width-Based Metrics and Weight Masks for Retinal Blood Vessel Segmentation", published in WUW-SIBGRAPI 2025. The package also includes a function for visualizing thickness-based filtered masks, the basic structure for computing the proposed metrics.
|
|
30
|
+
|
|
31
|
+
## Package installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install retinal_thin_vessels
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage Demonstration with DRIVE and CHASEDB1
|
|
38
|
+
|
|
39
|
+
To ensure the metrics are reliable, it is important to visualize the specific thin-vessel mask used by the given functions in their calculations. Therefore, a core function, get_thin_vessels_mask(), is also provided. This function takes a standard segmentation mask and returns a new mask containing only the thin vessels.
|
|
40
|
+
|
|
41
|
+
The following code demonstrates how to generate this filtered mask using images from two public datasets: DRIVE and CHASEDB1.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from PIL import Image
|
|
45
|
+
from retinal_thin_vessels.core import get_thin_vessels_mask
|
|
46
|
+
from retinal_thin_vessels.metrics import recall_thin_vessels, precision_thin_vessels
|
|
47
|
+
from sklearn.metrics import recall_score, precision_score
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# Import the original segmentation masks
|
|
52
|
+
seg_DRIVE = Image.open(f"tests/imgs/DRIVE_seg_example.png")
|
|
53
|
+
seg_CDB1 = Image.open(f"tests/imgs/CHASEDB1_seg_example.png")
|
|
54
|
+
|
|
55
|
+
# generates new masks containing only thin vessels
|
|
56
|
+
thin_vessels_seg_DRIVE = get_thin_vessels_mask(seg_DRIVE)
|
|
57
|
+
thin_vessels_seg_CDB1 = get_thin_vessels_mask(seg_CDB1)
|
|
58
|
+
|
|
59
|
+
# Display the original segmentation mask and the resulting thin-vessel-only mask for comparison
|
|
60
|
+
seg_DRIVE.show()
|
|
61
|
+
img_DRIVE = Image.fromarray(thin_vessels_seg_DRIVE)
|
|
62
|
+
img_DRIVE.show()
|
|
63
|
+
|
|
64
|
+
seg_CDB1.show()
|
|
65
|
+
img_CDB1 = Image.fromarray(thin_vessels_seg_CDB1)
|
|
66
|
+
img_CDB1.show()
|
|
67
|
+
```
|
|
68
|
+
<img src="tests/imgs/DRIVE_seg_example.png" alt="DRIVE_thin_vessels_example" width=300/>
|
|
69
|
+
<img src="tests/imgs/DRIVE_seg_thin_example.png" alt="DRIVE_thin_vessels_example" width=300/>
|
|
70
|
+
<img src="tests/imgs/CHASEDB1_seg_example.png" alt="CHASEDB1_thin_vessels_example" width=300/>
|
|
71
|
+
<img src="tests/imgs/CHASEDB1_seg_thin_example.png" alt="CHASEDB1_thin_vessels_example" width=300/>
|
|
72
|
+
|
|
73
|
+
Furthermore, to demonstrate the metric calculation functions, you can run the code below. It compares the overall metrics (calculated with scikit-learn) to the thin-vessel-specific metrics calculated by this package.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Load the ground truth segmentation mask and a sample prediction
|
|
77
|
+
pred = Image.open(f"tests/imgs/DRIVE_pred_example.png")
|
|
78
|
+
seg_DRIVE = Image.open(f"tests/imgs/DRIVE_seg_example.png").resize((pred.size), Image.NEAREST)
|
|
79
|
+
|
|
80
|
+
# Binarize images to a 0/1 format for scikit-learn compatibility
|
|
81
|
+
seg_DRIVE = np.where(np.array(seg_DRIVE) > 0, 1, 0)
|
|
82
|
+
pred = np.where(np.array(pred) > 0, 1, 0)
|
|
83
|
+
|
|
84
|
+
# Compute and print the metrics
|
|
85
|
+
print(f"Overall Recall score: {recall_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
86
|
+
print(f"Recall score on thin vessels: {recall_thin_vessels(seg_DRIVE, pred)}")
|
|
87
|
+
print("-" * 30)
|
|
88
|
+
print(f"Overall Precision score: {precision_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
89
|
+
print(f"Precision score on thin Vessels: {precision_thin_vessels(seg_DRIVE, pred)}")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If the program is running correctly with the provided sample images, the results should be similar to this:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
Overall Recall score: 0.8553852359822509
|
|
96
|
+
Recall score on thin vessels: 0.751244555071562
|
|
97
|
+
------------------------------
|
|
98
|
+
Overall Precision score: 0.8422369623068674
|
|
99
|
+
Precision score on thin Vessels: 0.6527915897144481
|
|
100
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
retinal_thin_vessels/__init__.py
|
|
5
|
+
retinal_thin_vessels/core.py
|
|
6
|
+
retinal_thin_vessels/get_relation.py
|
|
7
|
+
retinal_thin_vessels/input_transformation.py
|
|
8
|
+
retinal_thin_vessels/metrics.py
|
|
9
|
+
retinal_thin_vessels.egg-info/PKG-INFO
|
|
10
|
+
retinal_thin_vessels.egg-info/SOURCES.txt
|
|
11
|
+
retinal_thin_vessels.egg-info/dependency_links.txt
|
|
12
|
+
retinal_thin_vessels.egg-info/requires.txt
|
|
13
|
+
retinal_thin_vessels.egg-info/top_level.txt
|
|
14
|
+
retinal_thin_vessels/external/DSE_skeleton_pruning/setup.py
|
|
15
|
+
retinal_thin_vessels/external/DSE_skeleton_pruning/dsepruning/__init__.py
|
|
16
|
+
retinal_thin_vessels/external/DSE_skeleton_pruning/dsepruning/dsepruning.py
|
|
17
|
+
tests/tests.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
retinal_thin_vessels
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from PIL import Image
|
|
2
|
+
from retinal_thin_vessels.metrics import recall_thin_vessels, precision_thin_vessels
|
|
3
|
+
from retinal_thin_vessels.core import get_thin_vessels_mask
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.metrics import recall_score, precision_score
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
|
|
9
|
+
example_components_path = "imgs/"
|
|
10
|
+
# DRIVE IMAGES
|
|
11
|
+
seg = Image.open(f"{example_components_path}DRIVE_seg_example.png")
|
|
12
|
+
pred = Image.open(f"{example_components_path}DRIVE_pred_example.png")
|
|
13
|
+
|
|
14
|
+
# # Gets the filtered mask with only thin vessels
|
|
15
|
+
# thin_vessels_seg = get_thin_vessels_mask(seg)
|
|
16
|
+
|
|
17
|
+
# print("Showing the filtered segmentation mask with thin vessels only. DRIVE")
|
|
18
|
+
# img = Image.fromarray(thin_vessels_seg)
|
|
19
|
+
# # img.show()
|
|
20
|
+
# img.save("DRIVE_seg_thin_example.png")
|
|
21
|
+
|
|
22
|
+
# # CHASEDB1 IMAGES
|
|
23
|
+
# seg = Image.open(f"{example_components_path}CHASEDB1_seg_example.png")
|
|
24
|
+
|
|
25
|
+
# # Gets the filtered mask with only thin vessels
|
|
26
|
+
# thin_vessels_seg = get_thin_vessels_mask(seg)
|
|
27
|
+
|
|
28
|
+
# print("Showing the filtered segmentation mask with thin vessels only. CHASEDB1")
|
|
29
|
+
# img = Image.fromarray(thin_vessels_seg)
|
|
30
|
+
# # img.show()
|
|
31
|
+
# img.save("CHASEDB1_seg_thin_example.png")
|
|
32
|
+
|
|
33
|
+
print(np.array(seg.resize(pred.size, Image.NEAREST)).shape)
|
|
34
|
+
print(np.array(pred).shape)
|
|
35
|
+
print(np.unique(np.array(seg.resize(pred.size, Image.NEAREST)).astype(np.uint8)))
|
|
36
|
+
print(np.unique((np.array(pred)/255).astype(np.uint8)))
|
|
37
|
+
|
|
38
|
+
# Load the ground truth segmentation mask and a sample prediction
|
|
39
|
+
pred = Image.open(f"imgs/DRIVE_pred_example.png")
|
|
40
|
+
seg_DRIVE = seg.resize((pred.size), Image.NEAREST)
|
|
41
|
+
|
|
42
|
+
# Binarize images to a 0/1 format for scikit-learn compatibility
|
|
43
|
+
seg_DRIVE = np.where(np.array(seg_DRIVE) > 0, 1, 0)
|
|
44
|
+
pred = np.where(np.array(pred) > 0, 1, 0)
|
|
45
|
+
|
|
46
|
+
# Compute and print the metrics
|
|
47
|
+
print(f"Overall Recall score: {recall_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
48
|
+
print(f"Recall score on thin vessels: {recall_thin_vessels(seg_DRIVE, pred)}")
|
|
49
|
+
print("-" * 30)
|
|
50
|
+
print(f"Overall Precision score: {precision_score(seg_DRIVE.flatten(), pred.flatten())}")
|
|
51
|
+
print(f"Precision score on thin Vessels: {precision_thin_vessels(seg_DRIVE, pred)}")
|
|
52
|
+
|
|
53
|
+
exit(0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|