Diff of /cmac/loader.py [000000] .. [4cda31]

Switch to unified view

a b/cmac/loader.py
1
import os
2
import glob
3
import pydicom
4
import aux_dicom
5
import numpy as np
6
import pandas as pd
7
import nibabel as nib
8
import vtk
9
from vtk.util.numpy_support import vtk_to_numpy
10
import warnings
11
from skimage.measure import find_contours
12
13
class Tag:
14
    def __init__(self, subject_id=1, data_dir='../datasets/CMAC', work_dir='results/data', results_group='MEVIS'):
15
        
16
        self.subject_id = subject_id
17
18
        self.data_dir = os.path.abspath(data_dir)
19
        self.work_dir = os.path.abspath(work_dir)
20
        self.raw_dir     = os.path.join(self.data_dir, 'raw', f'v{subject_id}', '3DTAG')
21
        self.gt_dir      = os.path.join(self.data_dir, 'GT', '3DTAG', f'v{subject_id}')
22
        self.results_dir = os.path.join(self.data_dir, 'RESULTS', results_group, '3DTAG', f'v{subject_id}')
23
        
24
        self.results_group = results_group
25
26
        self.affine_axial = nib.load(os.path.join(self.raw_dir, 'NIFTI', 'NIFTI00.nii')).affine
27
28
    def load_pts_gt(self, lmks_obs=1, lmks_frame_id=0):
29
        """Load GT mesh and landmarks."""
30
        coords = {
31
            'MEVIS': 'VTK_COORDINATES',
32
            'UPF': 'VTK_COORDINATES',
33
            'IUCL': 'DICOM_COORDINATES',
34
            'INRIA': 'INRIA_COORDINATES'
35
        }[self.results_group]
36
37
        mesh_gt_mm, region_ids, subpart_ids = load_pts_mm(
38
            os.path.join(self.gt_dir, 'MESH', coords, f'v{self.subject_id}.vtk'),
39
            regionID=True, subpartID=True
40
        )
41
        self.mesh_gt_mm = mesh_gt_mm
42
        self.regionIDs = region_ids
43
        self.subpartIDs = subpart_ids
44
45
        self.lmks_gt_mm = load_pts_mm(
46
            os.path.join(self.gt_dir, 'LMKS', coords, f'obs{lmks_obs}_groundTruth{lmks_frame_id:02d}.vtk')
47
        )
48
        self.mesh_gt_mm[:, :2] *= -1
49
        self.lmks_gt_mm[:, :2] *= -1
50
51
    def load_pts(self, frame_id=0, obs=1):
52
        """Load predicted mesh and landmarks."""
53
        self.mesh_pred_mm = load_pts_mm(
54
            os.path.join(self.results_dir, 'MESH', f'finalMesh{frame_id:03d}.vtk')
55
        )
56
        self.lmks_pred_mm = load_pts_mm(
57
            os.path.join(self.results_dir, 'LMKS', f'obs{obs}_results{frame_id:03d}.vtk')
58
        )
59
        self.mesh_pred_mm[:, :2] *= -1
60
        self.lmks_pred_mm[:, :2] *= -1
61
62
    def load_nifti_3d(self, frame_id, set_affine=None):
63
        nifti = nib.load(os.path.join(self.raw_dir, 'NIFTI', f'NIFTI{frame_id:02d}.nii'))
64
        return nib.Nifti1Image(nifti.get_fdata(), set_affine if set_affine is not None else nifti.affine)
65
66
    def load_nifti_4d(self, n_frames=20, set_affine=None):
67
        shape = self.load_nifti_3d(frame_id=0).shape
68
        arr_4d = np.zeros(shape + (n_frames,))
69
        for frame_id in range(n_frames):
70
            arr_3d = self.load_nifti_3d(frame_id=frame_id).get_fdata()
71
            arr_4d[:, :, :, frame_id] = arr_3d
72
        
73
        affine = set_affine if set_affine is not None else self.affine_axial
74
        return nib.Nifti1Image(arr_4d, affine)
75
76
77
class Cine:
78
    def __init__(self, subject_id=1, data_dir='cMAC', work_dir='results/data', results_group='UPF'):
79
        
80
        self.subject_id = subject_id
81
        
82
        self.data_dir    = os.path.abspath(data_dir)
83
        self.work_dir    = os.path.abspath(work_dir)
84
        self.raw_dir     = os.path.join(self.data_dir, 'raw', f'v{subject_id}', 'cSAX')
85
        self.gt_dir      = os.path.join(self.data_dir, 'GT', 'SSFP', f'v{subject_id}')
86
        self.results_dir = os.path.join(self.data_dir, 'RESULTS', results_group, 'SSFP', f'v{subject_id}')
87
88
        self.results_group = results_group
89
90
        self.ijk2ras, self.affine, self.affine_axial = aux_dicom.read_affine_info(os.path.join(self.raw_dir, 'time_1', '*'))
91
92
    def load_pts_gt(self, lmks_frame_id=0, obs=1):
93
        """Load GT mesh and landmarks."""
94
        coords = {
95
            'MEVIS': 'VTK_COORDINATES',
96
            'UPF': 'VTK_COORDINATES',
97
            'IUCL': 'DICOM_COORDINATES',
98
            'INRIA': 'INRIA_COORDINATES'
99
        }[self.results_group]
100
101
        mesh_gt_mm, region_ids, subpart_ids = load_pts_mm(
102
            os.path.join(self.gt_dir, 'MESH', 'VTK_COORDINATES', f'v{self.subject_id}.vtk'),
103
            regionID=True, subpartID=True
104
        )
105
        self.mesh_gt_mm = mesh_gt_mm
106
        self.regionIDs = region_ids
107
        self.subpartIDs = subpart_ids
108
109
        self.lmks_gt_mm = load_pts_mm(
110
            os.path.join(self.gt_dir, 'LMKS', coords, f'obs{obs}_groundTruth{lmks_frame_id:02d}.vtk')
111
        )
112
113
    def load_pts(self, frame_id=0, obs=1):
114
        """Load predicted mesh and landmarks."""
115
        self.mesh_pred_mm = load_pts_mm(
116
            os.path.join(self.results_dir, 'MESH', f'finalMesh{frame_id:03d}.vtk')
117
        )
118
        self.lmks_pred_mm = load_pts_mm(
119
            os.path.join(self.results_dir, 'LMKS', f'obs{obs}_results{frame_id:03d}.vtk')
120
        )
121
122
    def load_nifti_3d(self, frame_id, set_affine=None):
123
        dicom_files = glob.glob(os.path.join(self.raw_dir, f'time_{frame_id + 1}', '*'))
124
        cine_arrs_2d = [pydicom.dcmread(file).pixel_array.T for file in dicom_files]
125
        cine_arr_3d = np.stack(cine_arrs_2d, -1)
126
        return nib.Nifti1Image(cine_arr_3d, set_affine if set_affine is not None else self.affine)
127
128
    def load_nifti_4d(self, n_frames=20, set_affine=None):
129
        shape = self.load_nifti_3d(frame_id=0).shape
130
        arr_4d = np.zeros(shape + (n_frames,))
131
        for frame_id in range(n_frames):
132
            arr_3d = self.load_nifti_3d(frame_id=frame_id).get_fdata()
133
            arr_4d[:, :, :, frame_id] = arr_3d
134
        
135
        affine = set_affine if set_affine is not None else self.affine
136
        return nib.Nifti1Image(arr_4d, affine)
137
138
139
def load_pts_mm(pts_filename, regionID=False, subpartID=False):
140
    """Load points (mm) in 3D space stored in vtk file."""
141
    reader = vtk.vtkDataSetReader()
142
    reader.SetFileName(pts_filename)
143
    reader.Update()
144
    pts_mm = vtk_to_numpy(reader.GetOutput().GetPoints().GetData())
145
    output = [pts_mm]
146
    if regionID:
147
        output.append(vtk_to_numpy(reader.GetOutput().GetPointData().GetArray('regionID')))
148
    if subpartID:
149
        output.append(vtk_to_numpy(reader.GetOutput().GetPointData().GetArray('subpartID')))
150
    return output if len(output) > 1 else pts_mm
151
152
def pts_pix_to_mm(pts_pix, affine):
153
    """Apply affine transformation to pixel coordinates."""
154
    pts_mm = np.dot(np.c_[pts_pix, np.ones(len(pts_pix))], affine.T)[:, :3]
155
    return pts_mm
156
157
def pts_mm_to_pix(pts_mm, affine):
158
    """Apply inverse affine transformation to mm coordinates."""
159
    pts_pix = np.dot(np.c_[pts_mm, np.ones(len(pts_mm))], np.linalg.inv(affine).T)[:, :3]
160
    return pts_pix
161
162
def cine_pix_to_tag_pix(cine_pts_pix, cine_affine, tag_affine):
163
    """Convert cine pixel space points to tag pixel space points."""
164
    return pts_mm_to_pix(pts_pix_to_mm(cine_pts_pix, cine_affine), tag_affine)
165
166
def tag_pix_to_cine_pix(tag_pts_pix, tag_affine, cine_affine):
167
    """Convert tag pixel space points to cine pixel space points."""
168
    return pts_mm_to_pix(pts_pix_to_mm(tag_pts_pix, tag_affine), cine_affine)
169
170
def cine_cnts_pix_to_tag_cnts_pix(cine_cts_pix, cine_affine, tag_affine):
171
    """Convert contours in cine pixel space to points in tag pixel space."""
172
    tag_cts_pix = []
173
    for slice_contours in cine_cts_pix:
174
        slice_tag_cts = [cine_pix_to_tag_pix(contour, cine_affine, tag_affine) for contour in slice_contours]
175
        tag_cts_pix.append(slice_tag_cts)
176
    return tag_cts_pix
177
178
def estimate_affine(ins, outs):
179
    """Estimate affine transformation matrix."""
180
    l = len(ins)
181
    B = np.vstack([np.transpose(ins), np.ones(l)])
182
    D = 1.0 / np.linalg.det(B)
183
    entry = lambda r, d: np.linalg.det(np.delete(np.vstack([r, B]), (d + 1), axis=0))
184
    M = [[(-1) ** i * D * entry(R, i) for i in range(l)] for R in np.transpose(outs)]
185
    return np.array(M + [[0, 0, 0, 1]])
186
187
###############################
188
####### EXTRA FUNCTIONS #######
189
###############################
190
191
def export_pts_mm_to_slicer_fiducials(pts_mm, fiducials_filename_fscv):
192
    """Save points (mm) in 3D space as a slicer fiducial file (.fscv)."""
193
    with open(fiducials_filename_fscv, 'w') as file:
194
        file.write('# Markups fiducial file version = 4.6\n')
195
        file.write('# CoordinateSystem = 0\n')
196
        file.write('# columns = id,x,y,z,ow,ox,oy,oz,vis,sel,lock,label,desc,associatedNodeID\n')
197
        for idx, point in enumerate(pts_mm):
198
            file.write(f'vtkMRMLMarkupsFiducialNode_0,{point[0]},{point[1]},{point[2]},0,0,0,1,1,1,0,F-{idx},,vtkMRMLScalarVolumeNode1\n')
199
200
def import_pts_mm_from_slicer_fiducials(fiducials_filename_fscv):
201
    """Load points (mm) in 3D space from a slicer fiducial file (.fscv)."""
202
    return pd.read_csv(fiducials_filename_fscv, skiprows=3, header=None).iloc[:, 1:4].to_numpy()
203
204
def import_cnts_pix_from_label(label_nifti_filename, label_id=1):
205
    """Load and convert 3D label of myocardial contours to contour points in pixel space."""
206
    label = nib.load(label_nifti_filename).get_fdata()
207
    epicardium, endocardium = [], []
208
    for z in range(label.shape[-1]):
209
        contours = find_contours(label[:, :, z] == label_id, 0.8)
210
        if len(contours) == 2:
211
            epicardium.append(np.c_[contours[0], z * np.ones((len(contours[0]), 1))])
212
            endocardium.append(np.c_[contours[1], z * np.ones((len(contours[1]), 1))])
213
    return epicardium, endocardium