@@ -102,9 +102,11 @@ def __init__(
102102 images_dir : str = "." ,
103103 labels_dir : str = "labels" ,
104104 datastore_config : str = "datastore_v2.json" ,
105- extensions = ("*.nii.gz" , "*.nii" ),
105+ extensions = ("*.nii.gz" , "*.nii" , "*.nrrd" ),
106106 auto_reload = False ,
107107 read_only = False ,
108+ multichannel : bool = False ,
109+ multi_file : bool = False ,
108110 ):
109111 """
110112 Creates a `LocalDataset` object
@@ -124,6 +126,14 @@ def __init__(
124126 self ._ignore_event_config = False
125127 self ._config_ts = 0
126128 self ._auto_reload = auto_reload
129+ if multichannel and multi_file :
130+ raise ValueError (
131+ "multichannel and multi_file are mutually exclusive: "
132+ "multichannel expects a single 4D NIfTI volume per sample, "
133+ "while multi_file expects a directory of separate modality files."
134+ )
135+ self ._multichannel : bool = multichannel
136+ self ._multi_file : bool = multi_file
127137
128138 logging .getLogger ("filelock" ).setLevel (logging .ERROR )
129139
@@ -256,6 +266,18 @@ def datalist(self, full_path=True) -> List[Dict[str, Any]]:
256266 ds = json .loads (json .dumps (ds ).replace (f"{ self ._datastore_path .rstrip (os .pathsep )} { os .pathsep } " , "" ))
257267 return ds
258268
269+ def get_is_multichannel (self ) -> bool :
270+ """
271+ Returns whether the dataset is multichannel or not
272+ """
273+ return self ._multichannel
274+
275+ def get_is_multi_file (self ) -> bool :
276+ """
277+ Returns whether the dataset is multi-file or not
278+ """
279+ return self ._multi_file
280+
259281 def get_image (self , image_id : str , params = None ) -> Any :
260282 """
261283 Retrieve image object based on image id
@@ -431,6 +453,43 @@ def refresh(self):
431453 """
432454 self ._reconcile_datastore ()
433455
456+ def add_directory (self , directory_id : str , filename : str , info : Dict [str , Any ]) -> str :
457+ """
458+ Add a directory to the datastore
459+
460+ :param directory_id: the directory id
461+ :param filename: the filename
462+ :param info: additional info
463+
464+ :return: directory id
465+ """
466+ id = os .path .basename (os .path .normpath (filename ))
467+ if not directory_id :
468+ directory_id = id
469+
470+ logger .info (f"Adding Image: { directory_id } => { filename } " )
471+ name = directory_id
472+ dest = os .path .realpath (os .path .join (self ._datastore .image_path (), name ))
473+
474+ with FileLock (self ._lock_file ):
475+ logger .debug ("Acquired the lock!" )
476+ if os .path .isdir (filename ):
477+ if os .path .exists (dest ):
478+ shutil .rmtree (dest )
479+ shutil .copytree (filename , dest )
480+ else :
481+ shutil .copy2 (filename , dest )
482+
483+ info = info if info else {}
484+ info ["ts" ] = int (time .time ())
485+ info ["name" ] = name
486+
487+ # images = get_directory_contents(filename)
488+ self ._datastore .objects [directory_id ] = ImageLabelModel (image = DataModel (info = info , ext = "" ))
489+ self ._update_datastore_file (lock = False )
490+ logger .debug ("Released the lock!" )
491+ return directory_id
492+
434493 def add_image (self , image_id : str , image_filename : str , image_info : Dict [str , Any ]) -> str :
435494 id , image_ext = self ._to_id (os .path .basename (image_filename ))
436495 if not image_id :
@@ -552,10 +611,17 @@ def _list_files(self, path, patterns):
552611 files = os .listdir (path )
553612
554613 filtered = dict ()
555- for pattern in patterns :
556- matching = fnmatch .filter (files , pattern )
557- for file in matching :
558- filtered [os .path .basename (file )] = file
614+ if not self ._multi_file :
615+ for pattern in patterns :
616+ matching = fnmatch .filter (files , pattern )
617+ for file in matching :
618+ filtered [os .path .basename (file )] = file
619+ else :
620+ ignored = {"labels" , ".lock" , os .path .basename (self ._datastore_config_path ).lower ()}
621+ for file in files :
622+ abs_file = os .path .join (path , file )
623+ if os .path .isdir (abs_file ) and file .lower () not in ignored :
624+ filtered [os .path .basename (file )] = file
559625 return filtered
560626
561627 def _reconcile_datastore (self ):
@@ -585,24 +651,26 @@ def _add_non_existing_images(self) -> int:
585651 invalidate = 0
586652 self ._init_from_datastore_file ()
587653
588- local_images = self ._list_files (self ._datastore .image_path (), self ._extensions )
654+ local_files = self ._list_files (self ._datastore .image_path (), self ._extensions )
589655
590- image_ids = list (self ._datastore .objects .keys ())
591- for image_file in local_images :
592- image_id , image_ext = self ._to_id (image_file )
593- if image_id not in image_ids :
594- logger .info (f"Adding New Image: { image_id } => { image_file } " )
656+ ids = list (self ._datastore .objects .keys ())
657+ for file in local_files :
658+ if self ._multi_file :
659+ # Directories have no extension — use the name as-is
660+ file_id = file
661+ file_ext_str = ""
662+ else :
663+ file_id , file_ext_str = self ._to_id (file )
595664
596- name = self ._filename (image_id , image_ext )
597- image_info = {
665+ if file_id not in ids :
666+ logger .info (f"Adding New Image: { file_id } => { file } " )
667+ name = self ._filename (file_id , file_ext_str )
668+ file_info = {
598669 "ts" : int (time .time ()),
599- # "checksum": file_checksum(os.path.join(self._datastore.image_path(), name)),
600670 "name" : name ,
601671 }
602-
603672 invalidate += 1
604- self ._datastore .objects [image_id ] = ImageLabelModel (image = DataModel (info = image_info , ext = image_ext ))
605-
673+ self ._datastore .objects [file_id ] = ImageLabelModel (image = DataModel (info = file_info , ext = file_ext_str ))
606674 return invalidate
607675
608676 def _add_non_existing_labels (self , tag ) -> int :
0 commit comments