File size: 7,423 Bytes
1009e1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import cv2
import glob
import pydicom
import numpy as np
import pandas as pd
from collections import Counter


class dotdict(dict):
    """
    A dictionary that allows accessing its elements as attributes.
    """

    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

    def __getattr__(self, name):
        """
        Get an attribute from the dictionary.

        Args:
            name (str): The name of the attribute to get.

        Returns:
            The value of the attribute.

        Raises:
            AttributeError: If the attribute does not exist.
        """
        try:
            return self[name]
        except KeyError:
            raise AttributeError(name)


def read_series_metadata(
    study_id,
    series_id,
    series_description,
    data_path="../input/train_images/",
    return_imgs=True,
):
    """
    Reads the metadata and images from a DICOM series.

    Args:
        study_id (int): The ID of the study.
        series_id (int): The ID of the series.
        series_description (str): The description of the series.
        data_path (str, optional): The path to the input data. Defaults to "../input/train_images/".
        return_imgs (bool, optional): Whether to return the images. Defaults to True.

    Returns:
        tuple: A tuple containing the DataFrame with metadata and the list of images.
    """
    dicom_dir = data_path + f"{study_id}/{series_id}"

    # read dicom file
    dicom_file = sorted(
        glob.glob(f"{dicom_dir}/*.dcm"), key=lambda x: int(x.split("/")[-1][:-4])
    )
    instance_number = [int(f.split("/")[-1].split(".")[0]) for f in dicom_file]
    dicom = [pydicom.dcmread(f, stop_before_pixels=False) for f in dicom_file]

    dicom_df, images = [], {}
    for i, d in zip(instance_number, dicom):  # d__.dict__
        images[i] = d.pixel_array if return_imgs else None

        try:
            dicom_df.append(
                dotdict(
                    study_id=study_id,
                    series_id=series_id,
                    series_description=series_description,
                    instance_number=i,
                    ImagePositionPatient=[float(v) for v in d.ImagePositionPatient],
                    ImageOrientationPatient=[
                        float(v) for v in d.ImageOrientationPatient
                    ],
                    PixelSpacing=[float(v) for v in d.PixelSpacing],
                    SpacingBetweenSlices=float(d.SpacingBetweenSlices),
                    SliceThickness=float(d.SliceThickness),
                    grouping=0,
                )
            )
        except Exception:  # Missing fields
            dicom_df.append(
                dotdict(
                    study_id=study_id,
                    series_id=series_id,
                    series_description=series_description,
                    instance_number=i,
                    ImagePositionPatient=[float(v) for v in d.ImagePositionPatient],
                    ImageOrientationPatient=-1,
                    PixelSpacing=-1,
                    SpacingBetweenSlices=-1,
                    SliceThickness=-1,
                    grouping=0,
                )
            )

    dicom_df = pd.DataFrame(dicom_df)

    # Sort slices
    # More robust orientation check based on ImageOrientationPatient if description fails
    if "sagittal" in series_description.lower():
        dicom_df["order"] = dicom_df["ImagePositionPatient"].apply(lambda x: x[0])
    elif "axial" in series_description.lower():
        dicom_df["order"] = dicom_df["ImagePositionPatient"].apply(lambda x: x[2])
    else:
        # Fallback using ImageOrientationPatient
        # Assuming ImageOrientationPatient is [r_x, r_y, r_z, c_x, c_y, c_z]
        # Cross product gives the normal vector (slice direction)
        orientations = dicom_df["ImageOrientationPatient"].values
        orders = []
        for i, pos in enumerate(dicom_df["ImagePositionPatient"].values):
            if type(orientations[i]) is list and len(orientations[i]) == 6:
                r = np.array(orientations[i][:3])
                c = np.array(orientations[i][3:])
                normal = np.cross(r, c)
                # the position along the normal vector
                orders.append(np.dot(normal, pos))
            else:
                orders.append(pos[0]) # naive fallback
        dicom_df["order"] = orders

    df = dicom_df.sort_values("order", ignore_index=True)
    df["group"] = 0

    df.loc[:, "z"] = np.arange(len(df))

    images = [images[k] for k in df["instance_number"]]
    return df.reset_index(drop=True), images


def process_2(study, series, orient, data_path="", on_gpu=False):
    """
    Processes the DICOM series and returns the images and metadata.

    Args:
        study (int): The ID of the study.
        series (int): The ID of the series.
        orient (str): The orientation of the series.
        data_path (str, optional): The path to the input data directory. Defaults to "".
        on_gpu (bool, optional): Whether to process on GPU. Defaults to False.

    Returns:
        tuple: A tuple containing the processed images and the DataFrame with metadata.
    """
    df, imgs = read_series_metadata(
        study,
        series,
        orient,
        data_path=data_path,
    )

    try:
        imgs = np.array(imgs)
    except Exception:
        shapes = Counter([img.shape for img in imgs])
        shape = shapes.most_common()[0][0]
        #         print("Different shapes:", shapes, f"resize to {shape} - {orient}")
        imgs = np.array(
            [cv2.resize(img, shape) if img.shape != shape else img for img in imgs]
        )
    return imgs, df


def process_and_save(
    study,
    series,
    orient,
    data_path,
    save_folder="",
    save_meta=False,
    save_middle_frame=False,
):
    """
    Processes the DICOM series and saves the images and metadata.

    Args:
        study (int): The ID of the study.
        series (int): The ID of the series.
        orient (str): The orientation of the series.
        data_path (str): The path to the input data directory.
        save_folder (str, optional): Path to the saving folder. Defaults to "".
        save_meta (bool, optional): Whether to save the metadata. Defaults to False.
        save_middle_frame (bool, optional): Whether to save the middle frame. Defaults to False.

    Returns:
        dict: A dictionary containing the study ID, series ID, and the list of frame numbers.
    """
    imgs, df_series = process_2(int(study), int(series), orient, data_path=data_path)
    if save_folder:
        np.save(save_folder + f"npy/{study}_{series}.npy", imgs)
        if save_meta:
            df_series.to_csv(save_folder + f"csv/{study}_{series}.csv", index=False)
        if save_middle_frame:
            img = imgs[len(imgs) // 2]
            img = np.clip(
                img, np.percentile(img.flatten(), 0), np.percentile(img.flatten(), 98)
            )
            max_, min_ = img.max(), img.min()
            if max_ != min_:
                img = (img - min_) / (max_ - min_)
            else:
                img = img - min_
            img = (img * 255).astype(np.uint8)
            cv2.imwrite(save_folder + f"mid/{study}_{series}.png", img)

    return {
        "study_id": study,
        "series_id": series,
        "frames": df_series.instance_number.values.tolist(),
    }