Skip to content

Commit e413591

Browse files
committed
Merge branch 'development_robert' of github.com:Hendrik-code/TPTBox into development_robert
2 parents 230293f + 436fe16 commit e413591

7 files changed

Lines changed: 59 additions & 24 deletions

File tree

README.md

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,36 @@ nii.shape #shape
8787
```
8888
### Stitching
8989
Python function and script for arbitrary image stitching. [See Details](TPTBox/stitching/)
90+
91+
![Example of a stitching](TPTBox/stitching/stitching.jpg)
9092
### Spineps and Points of Interests (POI) integration
93+
94+
![Example of two lumbar vertebrae. The left example is derived from 1 mm isotropic CT, the right from sagittal MRI with a resolution of 3.3 mm in the left–right direction. Top row: Subregion of the vertebra used for analysis. Middle row: Extreme points. Bottom row: Corpus edge and ligamentum flavum points.](TPTBox/images/poi_preview.png)
9195
For our Spine segmentation pipline follow the installation of [SPINEPS](https://github.com/Hendrik-code/spineps).
96+
Image Source: Rule-based Key-Point Extraction for MR-Guided Biomechanical Digital Twins of the Spine;
97+
98+
9299

93100
SPINEPS will produce two mask: instance and semantic labels. With these we can compute our POIs. There are either center of mass points or surface points with bioloical meaning. See [Validation of a Patient-Specific Musculoskeletal Model for Lumbar Load Estimation Generated by an Automated Pipeline From Whole Body CT](https://pubmed.ncbi.nlm.nih.gov/35898642/)
94101
```python
95-
from TPTBox import NII, POI, Location, calc_poi_from_subreg_vert
102+
from TPTBox import NII, POI, Location, POI_Global, calc_poi_from_subreg_vert
96103
from TPTBox.core.vert_constants import v_name2idx
104+
from TPTBox.segmentation.spineps import run_spineps_single
105+
106+
# This requires that spineps is installed
107+
output_paths = run_spineps_single(
108+
"file-path-of_T2w.nii.gz",
109+
model_semantic="t2w",
110+
ignore_compatibility_issues=True,
111+
)
112+
out_spine = output_paths["out_spine"]
113+
out_vert = output_paths["out_vert"]
114+
semantic_nii = NII.load(out_spine, seg=True)
115+
instance_nii = NII.load(out_vert, seg=True)
97116

98-
p = "/dataset-DATASET/derivatives/A/"
99-
semantic_nii = NII.load(f"{p}sub-A_sequ-stitched_acq-sag_mod-T2w_seg-spine_msk.nii.gz", seg=True)
100-
instance_nii = NII.load(f"{p}sub-A_sequ-stitched_acq-sag_mod-T2w_seg-vert_msk.nii.gz", seg=True)
101-
poi_path = f"{p}sub-A_sequ-stitched_acq-sag_mod-T2w_seg-spine_ctd.json"
102-
poi = POI.load(poi_path)
103117
poi = calc_poi_from_subreg_vert(
104118
instance_nii,
105119
semantic_nii,
106-
# buffer_file=poi_path,
107120
subreg_id=[
108121
Location.Vertebra_Full,
109122
Location.Arcus_Vertebrae,
@@ -157,18 +170,34 @@ poi = calc_poi_from_subreg_vert(
157170
Location.Vertebra_Direction_Right,
158171
],
159172
)
160-
# poi.save(poi_path)
161173
poi = poi.round(2)
162174
print("Vertebra T4 Vertebra Corpus Center of mass:", poi[v_name2idx["T4"], Location.Vertebra_Corpus])
163-
# rescale/reorante like nii
175+
print("The id number of T4 Vertebra_Corpus is ", v_name2idx["T4"], Location.Vertebra_Corpus.value)
176+
177+
# rescale/reorante local poi like nii
164178
poi_new = poi.reorient(("P", "I", "R")).rescale((1, 1, 1))
179+
# Local and global POIs can be rescaled to a target spacing with:
165180
poi_new = poi.resample_from_to(other_nii_or_poi)
166181

182+
# local to global poi
183+
global_poi = poi.to_global(itk_coords=True)
184+
# You can save global pois in mrk.json format for import and editing in slicer.
185+
global_poi.save_mrk("FILE.mrk.json", glyphScale=3.0)
186+
# Import as a Markup in slicer; To make points editable you must click on the "lock" symbol under Markups - Control Points - Interaction
187+
188+
# Save in our format:
189+
poi.save(poi_path)
190+
# Loading local/global Poi
191+
poi = POI.load(poi_path)
192+
poi = POI_Global.load(poi_path)
193+
194+
195+
167196
```
168197

169198

170199
### Snapshot2D Spine
171-
200+
![Snapshot2D Spine example](TPTBox/images/snp2D_example.png)
172201
The snapshot function automatically generates sag, cor, axial cuts in the center of a segmentation.
173202

174203
```python
@@ -188,17 +217,21 @@ create_snapshot(snp_path="snapshot.jpg", frames=[ct_frame, mr_frame])
188217

189218

190219
### Snapshot3D
191-
220+
![Snapshot3D example](TPTBox/images/snp3D_example.jpg)
192221
Requires additonal python packages: vtk fury xvfbwrapper
193222

194223
```python
195-
from TPTBox.mesh3D.snapshot3D import make_snapshot3D
224+
from TPTBox.mesh3D.snapshot3D import make_snapshot3D, make_snapshot3D_parallel
225+
196226
# all segmentation; view give the rotation of an image
197-
make_snapshot3D("sub-101000_msk.nii.gz","snapshot3D.png",view=["A", "L", "P", "R"])
227+
make_snapshot3D("sub-101000_msk.nii.gz", "snapshot3D.png", view=["A", "L", "P", "R"])
198228
# Select witch segmentation per panel are chosen.
199-
make_snapshot3D("sub-101000_msk.nii.gz","snapshot3D_v2.png",view=["A"], ids_list=[[1,2],[3]])
229+
make_snapshot3D("sub-101000_msk.nii.gz", "snapshot3D_v2.png", view=["A"], ids_list=[[1, 2], [3]])
230+
# we proviede a implementation to process multiple images at the same time.
231+
make_snapshot3D_parallel(["a.nii.gz", "b.nii.gz"], ["snp_a.png", "snp_b.png"], view=["A"])
200232
```
201233

234+
<!---
202235
### Logger
203236
204237
```python
@@ -219,3 +252,4 @@ TBD
219252
220253
> [!IMPORTANT]
221254
> Importantly
255+
-->

TPTBox/core/nii_wrapper.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ def reorient_same_as(self, img_as: Nifti1Image | Self, verbose:logging=False, in
880880
return self.reorient(axcodes_to=axcodes_to, verbose=verbose, inplace=inplace)
881881
def reorient_same_as_(self, img_as: Nifti1Image | Self, verbose:logging=False) -> Self:
882882
return self.reorient_same_as(img_as=img_as,verbose=verbose,inplace=True)
883-
def rescale(self, voxel_spacing:float|tuple[float,...]=(1, 1, 1), c_val:float|None=None, verbose:logging=False, inplace=False,mode:MODES='nearest',order: int |None = None,align_corners:bool=False):
883+
def rescale(self, voxel_spacing:float|tuple[float,...]=(1, 1, 1), c_val:float|None=None, verbose:logging=False, inplace=False,mode:MODES='nearest',order: int |None = None,align_corners:bool=False,atol=0.001):
884884
"""
885885
Rescales the NIfTI image to a new voxel spacing.
886886
@@ -895,6 +895,7 @@ def rescale(self, voxel_spacing:float|tuple[float,...]=(1, 1, 1), c_val:float|No
895895
mode (str, optional): One of the supported modes by scipy.ndimage.interpolation (e.g., "constant", "nearest",
896896
"reflect", "wrap"). See the documentation for more details. Defaults to "constant".
897897
align_corners (bool|default): If True or not set and seg==True. Aline corners for scaling. This prevents segmentation mask to shift in a direction.
898+
atol: absolute tolerance for skipping if already close in voxel_spacing
898899
Returns:
899900
NII: A new NII object with the resampled image data.
900901
"""
@@ -916,7 +917,7 @@ def rescale(self, voxel_spacing:float|tuple[float,...]=(1, 1, 1), c_val:float|No
916917
order = 0 if self.seg else 3
917918
#print(aff.shape,shp,zms,voxel_spacing)
918919
voxel_spacing = tuple([v if v != -1 else z for v,z in zip_strict(voxel_spacing,zms)])
919-
if voxel_spacing == self.zoom:
920+
if np.isclose(voxel_spacing, self.zoom,atol=atol).all():
920921
log.print(f"Image already resampled to voxel size {self.zoom}",verbose=verbose)
921922
return self.copy() if inplace else self
922923

TPTBox/images/poi_preview.png

1.9 MB
Loading

TPTBox/images/snp2D_example.png

1.18 MB
Loading

TPTBox/images/snp3D_example.jpg

142 KB
Loading

TPTBox/mesh3D/snapshot3D.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,9 @@ def make_snapshot3D(
121121

122122

123123
def make_snapshot3D_parallel(
124-
imgs: list[Path],
124+
imgs: list[Path | str],
125125
output_paths: list[Image_Reference],
126-
orientation: VIEW | list[VIEW] = "A",
126+
view: VIEW | list[VIEW] = "A",
127127
ids_list: list[Sequence[int]] | None = None,
128128
smoothing=20,
129129
resolution: float = 2,
@@ -141,7 +141,7 @@ def make_snapshot3D_parallel(
141141
kwds={
142142
"output_path": out_path,
143143
"img": img,
144-
"view": orientation,
144+
"view": view,
145145
"ids_list": ids_list,
146146
"smoothing": smoothing,
147147
"resolution": resolution,

TPTBox/segmentation/TotalVibeSeg/inference_nnunet.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,18 @@ def run_inference_on_file(
7979
nnunet_path = next(next(iter(model_path.glob(f"*{idx:03}*"))).glob("*__nnUNet*ResEnc*"))
8080
except StopIteration:
8181
nnunet_path = next(next(iter(model_path.glob(f"*{idx:03}*"))).glob("*__nnUNetPlans*"))
82-
folds = [int(f.name.split("fold_")[-1]) for f in nnunet_path.glob("fold*")]
82+
folds = sorted([int(f.name.split("fold_")[-1]) for f in nnunet_path.glob("fold*")])
8383
if max_folds is not None:
8484
folds = max_folds if isinstance(max_folds, list) else folds[:max_folds]
8585

8686
# if idx in _unets:
8787
# nnunet = _unets[idx]
8888
# else:
8989
print("load model", nnunet_path.name, "; folds", folds) if verbose else None
90+
with open(Path(nnunet_path, "plans.json")) as f:
91+
plans_info = json.load(f)
92+
with open(Path(nnunet_path, "dataset.json")) as f:
93+
ds_info = json.load(f)
9094
nnunet = load_inf_model(
9195
nnunet_path,
9296
allow_non_final=True,
@@ -101,10 +105,6 @@ def run_inference_on_file(
101105
)
102106

103107
# _unets[idx] = nnunet
104-
with open(Path(nnunet_path, "plans.json")) as f:
105-
plans_info = json.load(f)
106-
with open(Path(nnunet_path, "dataset.json")) as f:
107-
ds_info = json.load(f)
108108
if "orientation" in ds_info:
109109
orientation = ds_info["orientation"]
110110
zoom = None

0 commit comments

Comments
 (0)