thigh-us-segmentation 0.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.
- thigh_us_segmentation-0.1.0/PKG-INFO +123 -0
- thigh_us_segmentation-0.1.0/README.md +106 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/__init__.py +11 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/distances.py +299 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/io_conversion.py +81 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/model_download.py +126 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/nnunet_inference.py +203 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/pipeline.py +103 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/preprocessing.py +193 -0
- thigh_us_segmentation-0.1.0/ThighUSSegmentation/textures.py +189 -0
- thigh_us_segmentation-0.1.0/pyproject.toml +31 -0
- thigh_us_segmentation-0.1.0/setup.cfg +4 -0
- thigh_us_segmentation-0.1.0/thigh_us_segmentation.egg-info/PKG-INFO +123 -0
- thigh_us_segmentation-0.1.0/thigh_us_segmentation.egg-info/SOURCES.txt +15 -0
- thigh_us_segmentation-0.1.0/thigh_us_segmentation.egg-info/dependency_links.txt +1 -0
- thigh_us_segmentation-0.1.0/thigh_us_segmentation.egg-info/requires.txt +9 -0
- thigh_us_segmentation-0.1.0/thigh_us_segmentation.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thigh-us-segmentation
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pipeline for thigh ultrasound segmentation using nnU-Net and feature extraction
|
|
5
|
+
Author: Mara Concepción Alvarez, Paula Crespo Ortega, Arantxa Villanueva Larre, Rafael Cabeza Laguna
|
|
6
|
+
Requires-Python: <3.12,>=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pandas
|
|
10
|
+
Requires-Dist: SimpleITK
|
|
11
|
+
Requires-Dist: opencv-python
|
|
12
|
+
Requires-Dist: spicy
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Requires-Dist: pyradiomics
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: nnunetv2==2.6.4
|
|
17
|
+
|
|
18
|
+
# ThighUSSegmentation
|
|
19
|
+
|
|
20
|
+
Python library for automatic thigh ultrasound segmentation and analysis using nnU-Net.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Automatic image conversion to `.mha`
|
|
25
|
+
- Preprocessing (active region detection)
|
|
26
|
+
- nnU-Net inference
|
|
27
|
+
- Muscle thickness computation
|
|
28
|
+
- Radiomics feature extraction
|
|
29
|
+
- Export of anatomical landmarks (`.mrk.json`)
|
|
30
|
+
- Automatic model download from Zenodo
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install thigh-us-segmentation
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from ThighUSSegmentation import run_full_pipeline
|
|
42
|
+
|
|
43
|
+
result = run_full_pipeline(
|
|
44
|
+
input_image_path="image.dcm",
|
|
45
|
+
output_root="outputs",
|
|
46
|
+
case_id = "case001"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
print(result["df_results"])
|
|
50
|
+
|
|
51
|
+
# OR
|
|
52
|
+
result = run_full_pipeline(
|
|
53
|
+
input_image_path="image.mha",
|
|
54
|
+
output_root="outputs",
|
|
55
|
+
models_root="D:/mis_modelos", #You can also specify your own model path.
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Output
|
|
60
|
+
|
|
61
|
+
outputs/
|
|
62
|
+
└── case001/
|
|
63
|
+
├── image_converted.mha
|
|
64
|
+
├── image_preprocessed.mha
|
|
65
|
+
├── case001_labelmap.mha
|
|
66
|
+
├── markups/
|
|
67
|
+
└── inference_log.txt
|
|
68
|
+
|
|
69
|
+
## Returned Results
|
|
70
|
+
|
|
71
|
+
The pipeline returns a dictionary:
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
"case_id": str,
|
|
75
|
+
"segmentation_path": str,
|
|
76
|
+
"mrk_paths": dict,
|
|
77
|
+
"df_distances": pd.DataFrame,
|
|
78
|
+
"df_textures": pd.DataFrame,
|
|
79
|
+
"df_results": pd.DataFrame
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
## Model
|
|
83
|
+
|
|
84
|
+
The pretrained nnU-Net model is automatically downloaded from Zenodo on first use.
|
|
85
|
+
[](https://doi.org/10.5281/zenodo.19914473)
|
|
86
|
+
|
|
87
|
+
Expected strucutre:
|
|
88
|
+
|
|
89
|
+
Dataset001_ThighUS/
|
|
90
|
+
└── nnUNetTrainer__nnUNetPlans__2d/
|
|
91
|
+
├── dataset.json
|
|
92
|
+
├── plans.json
|
|
93
|
+
├── fold_0/
|
|
94
|
+
├── fold_1/
|
|
95
|
+
├── fold_2/
|
|
96
|
+
├── fold_3/
|
|
97
|
+
└── fold_4/
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## Requirements
|
|
101
|
+
|
|
102
|
+
· Python ≥ 3.9
|
|
103
|
+
· SimpleITK
|
|
104
|
+
· PyRadiomics
|
|
105
|
+
· OpenCV
|
|
106
|
+
· NumPy / SciPy / Pandas
|
|
107
|
+
|
|
108
|
+
## Citation
|
|
109
|
+
|
|
110
|
+
If you use this work, please cite:
|
|
111
|
+
|
|
112
|
+
Isensee et al., nnU-Net: a self-configuring method for deep learning-based biomedical image segmentation. Nature Methods (2021)
|
|
113
|
+
|
|
114
|
+
Mara Concepción Alavarez. (2026).
|
|
115
|
+
Thigh Ultrasound Segmentation Model (nnU-Net).
|
|
116
|
+
Zenodo. https://doi.org/10.5281/zenodo.19914473
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
This project uses the following licenses:
|
|
121
|
+
· Code: MIT License (or the one you choose)
|
|
122
|
+
· Model weights (Zenodo): CC-BY 4.0
|
|
123
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ThighUSSegmentation
|
|
2
|
+
|
|
3
|
+
Python library for automatic thigh ultrasound segmentation and analysis using nnU-Net.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Automatic image conversion to `.mha`
|
|
8
|
+
- Preprocessing (active region detection)
|
|
9
|
+
- nnU-Net inference
|
|
10
|
+
- Muscle thickness computation
|
|
11
|
+
- Radiomics feature extraction
|
|
12
|
+
- Export of anatomical landmarks (`.mrk.json`)
|
|
13
|
+
- Automatic model download from Zenodo
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install thigh-us-segmentation
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from ThighUSSegmentation import run_full_pipeline
|
|
25
|
+
|
|
26
|
+
result = run_full_pipeline(
|
|
27
|
+
input_image_path="image.dcm",
|
|
28
|
+
output_root="outputs",
|
|
29
|
+
case_id = "case001"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
print(result["df_results"])
|
|
33
|
+
|
|
34
|
+
# OR
|
|
35
|
+
result = run_full_pipeline(
|
|
36
|
+
input_image_path="image.mha",
|
|
37
|
+
output_root="outputs",
|
|
38
|
+
models_root="D:/mis_modelos", #You can also specify your own model path.
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Output
|
|
43
|
+
|
|
44
|
+
outputs/
|
|
45
|
+
└── case001/
|
|
46
|
+
├── image_converted.mha
|
|
47
|
+
├── image_preprocessed.mha
|
|
48
|
+
├── case001_labelmap.mha
|
|
49
|
+
├── markups/
|
|
50
|
+
└── inference_log.txt
|
|
51
|
+
|
|
52
|
+
## Returned Results
|
|
53
|
+
|
|
54
|
+
The pipeline returns a dictionary:
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
"case_id": str,
|
|
58
|
+
"segmentation_path": str,
|
|
59
|
+
"mrk_paths": dict,
|
|
60
|
+
"df_distances": pd.DataFrame,
|
|
61
|
+
"df_textures": pd.DataFrame,
|
|
62
|
+
"df_results": pd.DataFrame
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
## Model
|
|
66
|
+
|
|
67
|
+
The pretrained nnU-Net model is automatically downloaded from Zenodo on first use.
|
|
68
|
+
[](https://doi.org/10.5281/zenodo.19914473)
|
|
69
|
+
|
|
70
|
+
Expected strucutre:
|
|
71
|
+
|
|
72
|
+
Dataset001_ThighUS/
|
|
73
|
+
└── nnUNetTrainer__nnUNetPlans__2d/
|
|
74
|
+
├── dataset.json
|
|
75
|
+
├── plans.json
|
|
76
|
+
├── fold_0/
|
|
77
|
+
├── fold_1/
|
|
78
|
+
├── fold_2/
|
|
79
|
+
├── fold_3/
|
|
80
|
+
└── fold_4/
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
· Python ≥ 3.9
|
|
86
|
+
· SimpleITK
|
|
87
|
+
· PyRadiomics
|
|
88
|
+
· OpenCV
|
|
89
|
+
· NumPy / SciPy / Pandas
|
|
90
|
+
|
|
91
|
+
## Citation
|
|
92
|
+
|
|
93
|
+
If you use this work, please cite:
|
|
94
|
+
|
|
95
|
+
Isensee et al., nnU-Net: a self-configuring method for deep learning-based biomedical image segmentation. Nature Methods (2021)
|
|
96
|
+
|
|
97
|
+
Mara Concepción Alavarez. (2026).
|
|
98
|
+
Thigh Ultrasound Segmentation Model (nnU-Net).
|
|
99
|
+
Zenodo. https://doi.org/10.5281/zenodo.19914473
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
This project uses the following licenses:
|
|
104
|
+
· Code: MIT License (or the one you choose)
|
|
105
|
+
· Model weights (Zenodo): CC-BY 4.0
|
|
106
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .pipeline import run_full_pipeline
|
|
2
|
+
from .textures import extract_textures_dataframe
|
|
3
|
+
from .distances import extract_distances_dataframe
|
|
4
|
+
from .preprocessing import preprocess_active_region
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"run_full_pipeline",
|
|
8
|
+
"extract_textures_dataframe",
|
|
9
|
+
"extract_distances_dataframe",
|
|
10
|
+
"preprocess_active_region",
|
|
11
|
+
]
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import SimpleITK as sitk
|
|
7
|
+
import scipy.ndimage as ndi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def largest_component(mask):
|
|
11
|
+
labeled, num = ndi.label(mask)
|
|
12
|
+
if num == 0:
|
|
13
|
+
return mask
|
|
14
|
+
|
|
15
|
+
sizes = ndi.sum(mask, labeled, range(1, num + 1))
|
|
16
|
+
largest = np.argmax(sizes) + 1
|
|
17
|
+
return labeled == largest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def clean_rf_mask(mask_rf):
|
|
21
|
+
rf = largest_component(mask_rf)
|
|
22
|
+
rf = ndi.binary_erosion(rf, iterations=2)
|
|
23
|
+
|
|
24
|
+
dist = ndi.distance_transform_edt(rf)
|
|
25
|
+
rf_valid = dist > 2
|
|
26
|
+
|
|
27
|
+
return rf, rf_valid
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compute_thickness(masks, img2d):
|
|
31
|
+
rf = masks["rf_clean"]
|
|
32
|
+
femur = masks["femur"]
|
|
33
|
+
epi = masks["epi"]
|
|
34
|
+
|
|
35
|
+
H, W = rf.shape
|
|
36
|
+
|
|
37
|
+
rf_vals = []
|
|
38
|
+
rf_max_vals = []
|
|
39
|
+
muscle_vals = []
|
|
40
|
+
sat_vals = []
|
|
41
|
+
vi_vals = []
|
|
42
|
+
lines = []
|
|
43
|
+
|
|
44
|
+
coords = np.column_stack(np.where(femur))
|
|
45
|
+
|
|
46
|
+
if len(coords) == 0:
|
|
47
|
+
return {"lines": []}
|
|
48
|
+
|
|
49
|
+
x_center = int(np.mean(coords[:, 1]))
|
|
50
|
+
|
|
51
|
+
y_min = np.min(coords[:, 0])
|
|
52
|
+
top_band = coords[coords[:, 0] < y_min + 3]
|
|
53
|
+
|
|
54
|
+
dist = np.abs(top_band[:, 1] - x_center)
|
|
55
|
+
idx = np.argmin(dist)
|
|
56
|
+
|
|
57
|
+
_, x_femur_c = top_band[idx]
|
|
58
|
+
|
|
59
|
+
window = 10
|
|
60
|
+
xmin = max(0, x_femur_c - window)
|
|
61
|
+
xmax = min(W, x_femur_c + window)
|
|
62
|
+
|
|
63
|
+
for x in range(xmin, xmax):
|
|
64
|
+
col_rf = rf[:, x]
|
|
65
|
+
col_femur = femur[:, x]
|
|
66
|
+
col_epi = epi[:, x]
|
|
67
|
+
|
|
68
|
+
if np.sum(col_rf) == 0:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
col_img = img2d[:, x]
|
|
72
|
+
y_rf = np.where(col_rf)[0]
|
|
73
|
+
|
|
74
|
+
if len(y_rf) < 5:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
y_rf_top = y_rf.min()
|
|
78
|
+
y_rf_bot = y_rf.max()
|
|
79
|
+
|
|
80
|
+
rf_max_vals.append(y_rf_bot - y_rf_top)
|
|
81
|
+
|
|
82
|
+
top_start = max(0, y_rf_top - 20)
|
|
83
|
+
top_end = y_rf_top
|
|
84
|
+
search_top = col_img[top_start:top_end]
|
|
85
|
+
y_top = np.argmax(search_top) + top_start if len(search_top) > 0 else y_rf_top
|
|
86
|
+
|
|
87
|
+
bot_start = y_rf_bot
|
|
88
|
+
bot_end = min(H, y_rf_bot + 20)
|
|
89
|
+
search_bot = col_img[bot_start:bot_end]
|
|
90
|
+
y_bot = np.argmax(search_bot) + bot_start if len(search_bot) > 0 else y_rf_bot
|
|
91
|
+
|
|
92
|
+
y_femur = np.where(col_femur)[0].min() if np.any(col_femur) else None
|
|
93
|
+
y_epi = np.where(col_epi)[0].max() if np.any(col_epi) else None
|
|
94
|
+
|
|
95
|
+
rf_vals.append(y_bot - y_top)
|
|
96
|
+
|
|
97
|
+
if y_femur is not None:
|
|
98
|
+
vi_vals.append(y_femur - y_bot)
|
|
99
|
+
muscle_vals.append(y_femur - y_top)
|
|
100
|
+
|
|
101
|
+
if y_epi is not None:
|
|
102
|
+
sat_vals.append(y_top - y_epi)
|
|
103
|
+
|
|
104
|
+
lines.append({
|
|
105
|
+
"x": int(x),
|
|
106
|
+
"y_top": int(y_top),
|
|
107
|
+
"y_bot": int(y_bot),
|
|
108
|
+
"y_femur": None if y_femur is None else int(y_femur),
|
|
109
|
+
"y_epi": None if y_epi is None else int(y_epi),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"rf_mean": np.mean(rf_vals) if rf_vals else None,
|
|
114
|
+
"rf_median": np.median(rf_vals) if rf_vals else None,
|
|
115
|
+
"rf_alt": np.max(rf_max_vals) if rf_max_vals else None,
|
|
116
|
+
"vi_mean": np.mean(vi_vals) if vi_vals else None,
|
|
117
|
+
"muscle_mean": np.mean(muscle_vals) if muscle_vals else None,
|
|
118
|
+
"sat_mean": np.mean(sat_vals) if sat_vals else None,
|
|
119
|
+
"lines": lines,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def make_point(label, x, y, img_sitk, description=""):
|
|
124
|
+
position = img_sitk.TransformIndexToPhysicalPoint((int(x), int(y), 0))
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"label": label,
|
|
128
|
+
"description": description,
|
|
129
|
+
"position": [
|
|
130
|
+
float(position[0]),
|
|
131
|
+
float(position[1]),
|
|
132
|
+
float(position[2]),
|
|
133
|
+
],
|
|
134
|
+
"orientation": [
|
|
135
|
+
1.0, 0.0, 0.0,
|
|
136
|
+
0.0, 1.0, 0.0,
|
|
137
|
+
0.0, 0.0, 1.0,
|
|
138
|
+
],
|
|
139
|
+
"selected": True,
|
|
140
|
+
"locked": False,
|
|
141
|
+
"visibility": True,
|
|
142
|
+
"positionStatus": "defined",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def save_single_mrk(output_path, markup_type, control_points, color):
|
|
147
|
+
output_path = Path(output_path)
|
|
148
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
|
|
150
|
+
if output_path.exists():
|
|
151
|
+
output_path.unlink()
|
|
152
|
+
|
|
153
|
+
mrk = {
|
|
154
|
+
"@schema": "https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Markups/Resources/Schema/markups-schema-v1.0.3.json#",
|
|
155
|
+
"markups": [
|
|
156
|
+
{
|
|
157
|
+
"type": markup_type,
|
|
158
|
+
"coordinateSystem": "LPS",
|
|
159
|
+
"locked": False,
|
|
160
|
+
"labelFormat": "%N-%d",
|
|
161
|
+
"controlPoints": control_points,
|
|
162
|
+
"measurements": [],
|
|
163
|
+
"display": {
|
|
164
|
+
"visibility": True,
|
|
165
|
+
"color": color,
|
|
166
|
+
"selectedColor": color,
|
|
167
|
+
"glyphType": "Sphere3D",
|
|
168
|
+
"glyphScale": 5.0,
|
|
169
|
+
"textScale": 4.5,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
176
|
+
json.dump(mrk, f, indent=4)
|
|
177
|
+
|
|
178
|
+
return output_path
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def save_points_mrk_json(thickness, output_dir, case_id, img_sitk):
|
|
182
|
+
output_dir = Path(output_dir)
|
|
183
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
|
|
185
|
+
lines = thickness.get("lines", [])
|
|
186
|
+
|
|
187
|
+
if len(lines) == 0:
|
|
188
|
+
raise ValueError("No hay líneas de medición para guardar puntos.")
|
|
189
|
+
|
|
190
|
+
line_center = lines[len(lines) // 2]
|
|
191
|
+
|
|
192
|
+
x_c = line_center["x"]
|
|
193
|
+
y_top_c = line_center["y_top"]
|
|
194
|
+
y_bot_c = line_center["y_bot"]
|
|
195
|
+
y_femur_c = line_center["y_femur"]
|
|
196
|
+
y_epi_c = line_center["y_epi"]
|
|
197
|
+
|
|
198
|
+
saved_paths = {}
|
|
199
|
+
|
|
200
|
+
points = {
|
|
201
|
+
"epidermis": ("Epidermis", y_epi_c, [1.0, 0.0, 0.0]),
|
|
202
|
+
"fascia_lata": ("Fascia Lata", y_top_c, [0.0, 1.0, 0.0]),
|
|
203
|
+
"aponeurosis": ("Aponeurosis", y_bot_c, [0.0, 0.0, 1.0]),
|
|
204
|
+
"femur": ("Femur", y_femur_c, [1.0, 1.0, 0.0]),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for key, (label, y, color) in points.items():
|
|
208
|
+
if y is None:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
path = output_dir / f"{case_id}_{key}.mrk.json"
|
|
212
|
+
|
|
213
|
+
saved_paths[key] = save_single_mrk(
|
|
214
|
+
output_path=path,
|
|
215
|
+
markup_type="Fiducial",
|
|
216
|
+
control_points=[
|
|
217
|
+
make_point(label, x_c, y, img_sitk)
|
|
218
|
+
],
|
|
219
|
+
color=color,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if y_epi_c is not None and y_femur_c is not None:
|
|
223
|
+
path = output_dir / f"{case_id}_central_line.mrk.json"
|
|
224
|
+
|
|
225
|
+
saved_paths["central_line"] = save_single_mrk(
|
|
226
|
+
output_path=path,
|
|
227
|
+
markup_type="Line",
|
|
228
|
+
control_points=[
|
|
229
|
+
make_point("Central_start_epi", x_c, y_epi_c, img_sitk, "Central blanca"),
|
|
230
|
+
make_point("Central_end_femur", x_c, y_femur_c, img_sitk, "Central blanca"),
|
|
231
|
+
],
|
|
232
|
+
color=[1.0, 1.0, 1.0],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return saved_paths
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def extract_distances_dataframe(
|
|
239
|
+
image_path: str,
|
|
240
|
+
mask_path: str,
|
|
241
|
+
output_mrk_json_path: str,
|
|
242
|
+
case_id: str | None = None,
|
|
243
|
+
epi_label: int = 1,
|
|
244
|
+
femur_label: int = 2,
|
|
245
|
+
rf_label: int = 3,
|
|
246
|
+
):
|
|
247
|
+
image_path = Path(image_path)
|
|
248
|
+
mask_path = Path(mask_path)
|
|
249
|
+
|
|
250
|
+
if case_id is None:
|
|
251
|
+
case_id = mask_path.name.replace(".nii.gz", "").replace(".mha", "")
|
|
252
|
+
|
|
253
|
+
img_sitk = sitk.ReadImage(str(image_path))
|
|
254
|
+
lab_sitk = sitk.ReadImage(str(mask_path))
|
|
255
|
+
|
|
256
|
+
img = sitk.GetArrayFromImage(img_sitk)
|
|
257
|
+
lab = sitk.GetArrayFromImage(lab_sitk)
|
|
258
|
+
|
|
259
|
+
img2d = img[0]
|
|
260
|
+
lab2d = lab[0]
|
|
261
|
+
|
|
262
|
+
masks = {
|
|
263
|
+
"epi": lab2d == epi_label,
|
|
264
|
+
"femur": lab2d == femur_label,
|
|
265
|
+
"rf": lab2d == rf_label,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
rf_clean, rf_valid = clean_rf_mask(masks["rf"])
|
|
269
|
+
masks["rf_clean"] = rf_clean
|
|
270
|
+
masks["rf_valid"] = rf_valid
|
|
271
|
+
|
|
272
|
+
thickness = compute_thickness(masks, img2d)
|
|
273
|
+
|
|
274
|
+
spacing = img_sitk.GetSpacing()
|
|
275
|
+
pixel_size_y = spacing[1]
|
|
276
|
+
|
|
277
|
+
def to_mm(value):
|
|
278
|
+
return None if value is None else float(value * pixel_size_y)
|
|
279
|
+
|
|
280
|
+
row = {
|
|
281
|
+
"case_id": case_id,
|
|
282
|
+
"rf_mm": to_mm(thickness.get("rf_mean")),
|
|
283
|
+
"rf_median_mm": to_mm(thickness.get("rf_median")),
|
|
284
|
+
"rf_alt_mm": to_mm(thickness.get("rf_alt")),
|
|
285
|
+
"vi_mm": to_mm(thickness.get("vi_mean")),
|
|
286
|
+
"sat_mm": to_mm(thickness.get("sat_mean")),
|
|
287
|
+
"muscle_mm": to_mm(thickness.get("muscle_mean")),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
df = pd.DataFrame([row])
|
|
291
|
+
|
|
292
|
+
mrk_paths = save_points_mrk_json(
|
|
293
|
+
thickness=thickness,
|
|
294
|
+
output_dir=output_mrk_json_path,
|
|
295
|
+
case_id=case_id,
|
|
296
|
+
img_sitk=img_sitk,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return df, mrk_paths
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import shutil
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import SimpleITK as sitk
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def convert_to_mha(
|
|
12
|
+
input_path: str,
|
|
13
|
+
output_dir: str,
|
|
14
|
+
output_filename: str = "image_converted.mha",
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Convierte una imagen de entrada a .mha y la guarda dentro de output_dir.
|
|
18
|
+
|
|
19
|
+
Soporta:
|
|
20
|
+
- .mha
|
|
21
|
+
- .nii
|
|
22
|
+
- .nii.gz
|
|
23
|
+
- .dcm
|
|
24
|
+
- carpeta DICOM
|
|
25
|
+
|
|
26
|
+
La salida será:
|
|
27
|
+
output_dir / image_converted.mha
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
input_path = Path(input_path)
|
|
31
|
+
output_dir = Path(output_dir)
|
|
32
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
if not input_path.exists():
|
|
35
|
+
raise FileNotFoundError(f"No existe la entrada: {input_path}")
|
|
36
|
+
|
|
37
|
+
if not output_filename.endswith(".mha"):
|
|
38
|
+
raise ValueError("output_filename debe terminar en .mha")
|
|
39
|
+
|
|
40
|
+
output_path = output_dir / output_filename
|
|
41
|
+
|
|
42
|
+
if output_path.exists():
|
|
43
|
+
output_path.unlink()
|
|
44
|
+
|
|
45
|
+
if input_path.is_file() and input_path.suffix.lower() == ".mha":
|
|
46
|
+
shutil.copy2(input_path, output_path)
|
|
47
|
+
return str(output_path)
|
|
48
|
+
|
|
49
|
+
if input_path.is_file():
|
|
50
|
+
logger.info(f"Convirtiendo archivo a .mha: {input_path}")
|
|
51
|
+
img = sitk.ReadImage(str(input_path))
|
|
52
|
+
sitk.WriteImage(img, str(output_path))
|
|
53
|
+
return str(output_path)
|
|
54
|
+
|
|
55
|
+
if input_path.is_dir():
|
|
56
|
+
logger.info(f"Leyendo carpeta DICOM: {input_path}")
|
|
57
|
+
|
|
58
|
+
reader = sitk.ImageSeriesReader()
|
|
59
|
+
series_ids = reader.GetGDCMSeriesIDs(str(input_path))
|
|
60
|
+
|
|
61
|
+
if not series_ids:
|
|
62
|
+
raise ValueError(f"No se encontró ninguna serie DICOM en: {input_path}")
|
|
63
|
+
|
|
64
|
+
if len(series_ids) > 1:
|
|
65
|
+
logger.warning(
|
|
66
|
+
f"Se encontraron {len(series_ids)} series DICOM. "
|
|
67
|
+
f"Se usará la primera: {series_ids[0]}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
series_files = reader.GetGDCMSeriesFileNames(
|
|
71
|
+
str(input_path),
|
|
72
|
+
series_ids[0],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
reader.SetFileNames(series_files)
|
|
76
|
+
img = reader.Execute()
|
|
77
|
+
sitk.WriteImage(img, str(output_path))
|
|
78
|
+
|
|
79
|
+
return str(output_path)
|
|
80
|
+
|
|
81
|
+
raise ValueError(f"Formato no soportado: {input_path}")
|