11from __future__ import annotations
22
3- import ast
43import enum
54import multiprocessing
6- import shutil
75from pathlib import Path
86from typing import TYPE_CHECKING
97from typing import Literal
108from typing import Union
119
12- import numpy as np
13- import SimpleITK as sitk
14- from tqdm .auto import tqdm
15- from tqdm .contrib .concurrent import process_map
1610from tqdm .contrib .concurrent import thread_map
1711
1812from ..data .dataset import SubjectsDataset
@@ -43,8 +37,8 @@ class MetadataIndexColumn(str, enum.Enum):
4337class CtRate (SubjectsDataset ):
4438 """CT-RATE dataset.
4539
46- This class provides access to
47- `CT-RATE <https://huggingface.co/datasets/ibrahimhamamci/CT-RATE>`_,
40+ This class helps loading the `CT-RATE dataset
41+ <https://huggingface.co/datasets/ibrahimhamamci/CT-RATE>`_,
4842 which contains chest CT scans with associated radiology reports and
4943 abnormality labels.
5044
@@ -53,12 +47,14 @@ class CtRate(SubjectsDataset):
5347 Args:
5448 root: Root directory where the dataset has been downloaded.
5549 split: Dataset split to use, either ``'train'`` or ``'validation'``.
56- token: Hugging Face token for accessing gated repositories. Alternatively,
57- login using `huggingface-cli login` to cache the token.
5850 num_subjects: Optional limit on the number of subjects to load (useful for
59- testing ). If ``None``, all subjects in the split are loaded.
51+ debugging ). If ``None``, all subjects in the split are loaded.
6052 report_key: Key to use for storing radiology reports in the Subject metadata.
61- sizes: List of image sizes (in pixels) to include. Default: [512, 768, 1024].
53+ sizes: List of image sizes (in-plane, in voxels) to include.
54+ load_fixed: If ``True``, load the files with fixed spatial metadata
55+ added in `this pull request
56+ <https://huggingface.co/datasets/ibrahimhamamci/CT-RATE/discussions/85>`_.
57+ Otherwise, load the original files with incorrect spatial metadata.
6258 **kwargs: Additional arguments for SubjectsDataset.
6359
6460 Examples:
@@ -88,26 +84,32 @@ class CtRate(SubjectsDataset):
8884 'Bronchiectasis' ,
8985 'Interlobular septal thickening' ,
9086 ]
87+ REPORT_KEYS = [
88+ 'ClinicalInformation_EN' ,
89+ 'Findings_EN' ,
90+ 'Impressions_EN' ,
91+ 'Technique_EN' ,
92+ ]
9193
9294 def __init__ (
9395 self ,
9496 root : TypePath ,
9597 split : TypeSplit = 'train' ,
9698 * ,
97- token : str | None = None ,
9899 num_subjects : int | None = None ,
99100 report_key : str = 'report' ,
100101 sizes : list [int ] | None = None ,
102+ load_fixed : bool = True ,
101103 ** kwargs ,
102104 ):
103105 self ._root_dir = Path (root )
104- self ._token = token
105106 self ._num_subjects = num_subjects
106107 self ._report_key = report_key
107108 self ._sizes = self ._SIZES if sizes is None else sizes
108109
109110 self ._split = self ._parse_split (split )
110111 self .metadata = self ._get_metadata ()
112+ self ._load_fixed = load_fixed
111113 subjects_list = self ._get_subjects_list (self .metadata )
112114 super ().__init__ (subjects_list , ** kwargs )
113115
@@ -313,7 +315,11 @@ def _instantiate_image(self, image_row: pd.Series) -> ScalarImage:
313315 """
314316 image_dict = image_row .to_dict ()
315317 filename = image_dict [self ._FILENAME_KEY ]
316- image_path = self ._root_dir / self ._get_image_path (filename )
318+ relative_image_path = self ._get_image_path (
319+ filename ,
320+ load_fixed = self ._load_fixed ,
321+ )
322+ image_path = self ._root_dir / relative_image_path
317323 report_dict = self ._extract_report_dict (image_dict )
318324 image_dict [self ._report_key ] = report_dict
319325 image = ScalarImage (image_path , ** image_dict )
@@ -332,19 +338,13 @@ def _extract_report_dict(self, subject_dict: dict[str, str]) -> dict[str, str]:
332338 Note:
333339 This method modifies the input subject_dict by removing the report keys.
334340 """
335- report_keys = [
336- 'ClinicalInformation_EN' ,
337- 'Findings_EN' ,
338- 'Impressions_EN' ,
339- 'Technique_EN' ,
340- ]
341341 report_dict = {}
342- for key in report_keys :
342+ for key in self . REPORT_KEYS :
343343 report_dict [key ] = subject_dict .pop (key )
344344 return report_dict
345345
346346 @staticmethod
347- def _get_image_path (filename : str ) -> Path :
347+ def _get_image_path (filename : str , load_fixed : bool ) -> Path :
348348 """Construct the relative path to an image file within the dataset structure.
349349
350350 Parses the filename to determine the hierarchical directory structure
@@ -363,131 +363,8 @@ def _get_image_path(filename: str) -> Path:
363363 parts = filename .split ('_' )
364364 base_dir = 'dataset'
365365 split_dir = parts [0 ]
366+ if load_fixed :
367+ split_dir = f'{ split_dir } _fixed'
366368 level1 = f'{ parts [0 ]} _{ parts [1 ]} '
367369 level2 = f'{ level1 } _{ parts [2 ]} '
368370 return Path (base_dir , split_dir , level1 , level2 , filename )
369-
370- @staticmethod
371- def _fix_image (image : ScalarImage , out_path : Path , * , force : bool = False ) -> None :
372- """Fix the spatial metadata of a CT-RATE image file.
373-
374- The original NIfTI files in the CT-RATE dataset have incorrect spatial
375- metadata. This method reads the image, fixes the spacing, origin, and
376- orientation based on the metadata provided in the CSV, and applies the correct
377- rescaling to convert to Hounsfield units.
378-
379- Args:
380- in_path: The path to the image file to fix.
381- out_path: The path where the fixed image will be saved.
382-
383- Note:
384- This method overwrites the original file with the fixed version.
385- The fixed image is stored as INT16 with proper HU values.
386- """
387- # Adapted from https://huggingface.co/datasets/ibrahimhamamci/CT-RATE/blob/main/download_scripts/fix_metadata.py
388- if not force and out_path .exists ():
389- return
390- spacing_x , spacing_y = map (float , ast .literal_eval (image ['XYSpacing' ]))
391- spacing_z = image ['ZSpacing' ]
392- image_sitk = sitk .ReadImage (str (image .path ))
393- image_sitk .SetSpacing ((spacing_x , spacing_y , spacing_z ))
394-
395- image_sitk .SetOrigin (ast .literal_eval (image ['ImagePositionPatient' ]))
396-
397- orientation = ast .literal_eval (image ['ImageOrientationPatient' ])
398- row_cosine , col_cosine = orientation [:3 ], orientation [3 :6 ]
399- z_cosine = np .cross (row_cosine , col_cosine ).tolist ()
400- image_sitk .SetDirection (row_cosine + col_cosine + z_cosine )
401-
402- RescaleIntercept = image ['RescaleIntercept' ]
403- RescaleSlope = image ['RescaleSlope' ]
404- adjusted_hu = image_sitk * RescaleSlope + RescaleIntercept
405- cast_int16 = sitk .Cast (adjusted_hu , sitk .sitkInt16 )
406-
407- out_path .parent .mkdir (parents = True , exist_ok = True )
408- sitk .WriteImage (cast_int16 , str (out_path ))
409- return cast_int16
410-
411- def _copy_not_images (self , out_dir : Path ) -> None :
412- """Copy all files from the root directory except the images."""
413- for path in self ._root_dir .iterdir ():
414- if path .name == 'dataset' :
415- for subdirectory in path .iterdir ():
416- if subdirectory .name in ['train' , 'valid' ]:
417- continue
418- print (
419- f'Copying { subdirectory } to { out_dir / subdirectory .relative_to (self ._root_dir )} '
420- )
421- shutil .copytree (
422- subdirectory ,
423- out_dir / subdirectory .relative_to (self ._root_dir ),
424- dirs_exist_ok = True ,
425- )
426- elif path .name .startswith ('.' ):
427- continue
428- elif path .is_dir ():
429- print (f'Copying { path } to { out_dir / path .name } ' )
430- shutil .copytree (
431- path ,
432- out_dir / path .name ,
433- dirs_exist_ok = True ,
434- )
435- else :
436- print (f'Copying { path } to { out_dir / path .name } ' )
437- shutil .copy (path , out_dir / path .name )
438-
439- def fix_metadata (
440- self ,
441- out_dir : str | Path ,
442- parallelism : TypeParallelism = None ,
443- ) -> CtRate :
444- """Fix the metadata of all images in the dataset.
445-
446- Reads each image, applies the correct spatial metadata, and saves the fixed
447- image to the specified output directory.
448-
449- Args:
450- out_dir: The directory where the fixed images will be saved.
451- """
452- out_dir = Path (out_dir )
453- out_dir .mkdir (parents = True , exist_ok = True )
454- # self._copy_not_images(out_dir)
455- images = []
456- out_paths = []
457- for subject in self .dry_iter ():
458- for image in subject .get_images ():
459- out_path = out_dir / image .path .relative_to (self ._root_dir )
460- images .append (image )
461- out_paths .append (out_path )
462- if parallelism == 'thread' :
463- thread_map (
464- self ._fix_image ,
465- images ,
466- out_paths ,
467- max_workers = multiprocessing .cpu_count (),
468- desc = 'Fixing metadata' ,
469- )
470- elif parallelism == 'process' :
471- process_map (
472- self ._fix_image ,
473- images ,
474- out_paths ,
475- max_workers = multiprocessing .cpu_count (),
476- desc = 'Fixing metadata' ,
477- )
478- else :
479- zipped = zip (images , out_paths )
480- with tqdm (total = len (images ), desc = 'Fixing metadata' ) as pbar :
481- for image , out_path in zipped :
482- pbar .set_description (f'Fixing { image .path .name } ' )
483- self ._fix_image (image , out_path )
484- pbar .update (1 )
485- new_dataset = CtRate (
486- out_dir ,
487- split = self ._split ,
488- token = self ._token ,
489- num_subjects = self ._num_subjects ,
490- report_key = self ._report_key ,
491- sizes = self ._sizes ,
492- )
493- return new_dataset
0 commit comments