-
Notifications
You must be signed in to change notification settings - Fork 260
Multichannel Model Support for Pre-Aligned Volumes #1896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e01a04d
0070ca2
e244ec2
9dc1c0a
b83e07e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,9 +102,11 @@ def __init__( | |
| images_dir: str = ".", | ||
| labels_dir: str = "labels", | ||
| datastore_config: str = "datastore_v2.json", | ||
| extensions=("*.nii.gz", "*.nii"), | ||
| extensions=("*.nii.gz", "*.nii", "*.nrrd"), | ||
| auto_reload=False, | ||
| read_only=False, | ||
| multichannel: bool = False, | ||
| multi_file: bool = False, | ||
| ): | ||
| """ | ||
| Creates a `LocalDataset` object | ||
|
|
@@ -124,6 +126,14 @@ def __init__( | |
| self._ignore_event_config = False | ||
| self._config_ts = 0 | ||
| self._auto_reload = auto_reload | ||
| if multichannel and multi_file: | ||
| raise ValueError( | ||
| "multichannel and multi_file are mutually exclusive: " | ||
| "multichannel expects a single 4D NIfTI volume per sample, " | ||
| "while multi_file expects a directory of separate modality files." | ||
| ) | ||
| self._multichannel: bool = multichannel | ||
| self._multi_file: bool = multi_file | ||
|
|
||
| logging.getLogger("filelock").setLevel(logging.ERROR) | ||
|
|
||
|
|
@@ -256,6 +266,18 @@ def datalist(self, full_path=True) -> List[Dict[str, Any]]: | |
| ds = json.loads(json.dumps(ds).replace(f"{self._datastore_path.rstrip(os.pathsep)}{os.pathsep}", "")) | ||
| return ds | ||
|
|
||
| def get_is_multichannel(self) -> bool: | ||
| """ | ||
| Returns whether the dataset is multichannel or not | ||
| """ | ||
| return self._multichannel | ||
|
|
||
| def get_is_multi_file(self) -> bool: | ||
| """ | ||
| Returns whether the dataset is multi-file or not | ||
| """ | ||
| return self._multi_file | ||
|
|
||
| def get_image(self, image_id: str, params=None) -> Any: | ||
| """ | ||
| Retrieve image object based on image id | ||
|
|
@@ -431,6 +453,43 @@ def refresh(self): | |
| """ | ||
| self._reconcile_datastore() | ||
|
|
||
| def add_directory(self, directory_id: str, filename: str, info: Dict[str, Any]) -> str: | ||
| """ | ||
| Add a directory to the datastore | ||
|
|
||
| :param directory_id: the directory id | ||
| :param filename: the filename | ||
| :param info: additional info | ||
|
|
||
| :return: directory id | ||
| """ | ||
| id = os.path.basename(os.path.normpath(filename)) | ||
| if not directory_id: | ||
| directory_id = id | ||
|
|
||
| logger.info(f"Adding Image: {directory_id} => {filename}") | ||
| name = directory_id | ||
| dest = os.path.realpath(os.path.join(self._datastore.image_path(), name)) | ||
|
|
||
| with FileLock(self._lock_file): | ||
| logger.debug("Acquired the lock!") | ||
| if os.path.isdir(filename): | ||
| if os.path.exists(dest): | ||
| shutil.rmtree(dest) | ||
| shutil.copytree(filename, dest) | ||
| else: | ||
| shutil.copy2(filename, dest) | ||
|
|
||
| info = info if info else {} | ||
| info["ts"] = int(time.time()) | ||
| info["name"] = name | ||
|
|
||
| # images = get_directory_contents(filename) | ||
| self._datastore.objects[directory_id] = ImageLabelModel(image=DataModel(info=info, ext="")) | ||
| self._update_datastore_file(lock=False) | ||
| logger.debug("Released the lock!") | ||
| return directory_id | ||
|
Comment on lines
+456
to
+491
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's examine the add_directory method and surrounding code
cat -n monailabel/datastore/local.py | sed -n '456,491p'Repository: Project-MONAI/MONAILabel Length of output: 1643 🏁 Script executed: # Now check lines 654-673 mentioned as "Also applies to"
cat -n monailabel/datastore/local.py | sed -n '654,673p'Repository: Project-MONAI/MONAILabel Length of output: 1065 🏁 Script executed: # Find the get_image method in the same file
rg -n "def get_image" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 253 🏁 Script executed: # Find the get_dataset_archive method
rg -n "def get_dataset_archive" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 141 🏁 Script executed: # Look for multi_file discovery path
rg -n "multi_file" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 514 🏁 Script executed: # Check the /datastore/image endpoint
cat -n monailabel/endpoints/datastore.pyRepository: Project-MONAI/MONAILabel Length of output: 13996 🏁 Script executed: # Get get_image() implementation
sed -n '281,310p' monailabel/datastore/local.py | cat -nRepository: Project-MONAI/MONAILabel Length of output: 1377 🏁 Script executed: # Get get_dataset_archive() implementation
sed -n '398,450p' monailabel/datastore/local.py | cat -nRepository: Project-MONAI/MONAILabel Length of output: 2561 🏁 Script executed: # Get the full get_image() method to see what it does
ast-grep --pattern 'def get_image($_) { $$$ }'Repository: Project-MONAI/MONAILabel Length of output: 50 🏁 Script executed: # Better approach - look for get_image implementation with context
rg -A 20 "def get_image\(self, image_id" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 854 🏁 Script executed: # Look for get_dataset_archive implementation with context
rg -A 40 "def get_dataset_archive" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 1838 🏁 Script executed: # Find _to_bytes implementation
rg -A 10 "def _to_bytes" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 441 🏁 Script executed: # Find _filename implementation
rg -A 10 "def _filename" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 431 🏁 Script executed: # Check what datalist() returns - used by get_dataset_archive
rg -A 30 "def datalist" monailabel/datastore/local.pyRepository: Project-MONAI/MONAILabel Length of output: 1180 🏁 Script executed: # Check if there are any tests or usage of multi_file mode
fd -type f "\.py$" | xargs rg -l "multi_file.*True" | head -5Repository: Project-MONAI/MONAILabel Length of output: 491 Directory-backed images leak into APIs that assume file semantics. Both
Test and fix these read/export paths before merging multi_file support. |
||
|
|
||
| def add_image(self, image_id: str, image_filename: str, image_info: Dict[str, Any]) -> str: | ||
| id, image_ext = self._to_id(os.path.basename(image_filename)) | ||
| if not image_id: | ||
|
|
@@ -552,10 +611,17 @@ def _list_files(self, path, patterns): | |
| files = os.listdir(path) | ||
|
|
||
| filtered = dict() | ||
| for pattern in patterns: | ||
| matching = fnmatch.filter(files, pattern) | ||
| for file in matching: | ||
| filtered[os.path.basename(file)] = file | ||
| if not self._multi_file: | ||
| for pattern in patterns: | ||
| matching = fnmatch.filter(files, pattern) | ||
| for file in matching: | ||
| filtered[os.path.basename(file)] = file | ||
| else: | ||
| ignored = {"labels", ".lock", os.path.basename(self._datastore_config_path).lower()} | ||
| for file in files: | ||
| abs_file = os.path.join(path, file) | ||
| if os.path.isdir(abs_file) and file.lower() not in ignored: | ||
| filtered[os.path.basename(file)] = file | ||
| return filtered | ||
|
|
||
| def _reconcile_datastore(self): | ||
|
|
@@ -585,24 +651,26 @@ def _add_non_existing_images(self) -> int: | |
| invalidate = 0 | ||
| self._init_from_datastore_file() | ||
|
|
||
| local_images = self._list_files(self._datastore.image_path(), self._extensions) | ||
| local_files = self._list_files(self._datastore.image_path(), self._extensions) | ||
|
|
||
| image_ids = list(self._datastore.objects.keys()) | ||
| for image_file in local_images: | ||
| image_id, image_ext = self._to_id(image_file) | ||
| if image_id not in image_ids: | ||
| logger.info(f"Adding New Image: {image_id} => {image_file}") | ||
| ids = list(self._datastore.objects.keys()) | ||
| for file in local_files: | ||
| if self._multi_file: | ||
| # Directories have no extension — use the name as-is | ||
| file_id = file | ||
| file_ext_str = "" | ||
| else: | ||
| file_id, file_ext_str = self._to_id(file) | ||
|
|
||
| name = self._filename(image_id, image_ext) | ||
| image_info = { | ||
| if file_id not in ids: | ||
| logger.info(f"Adding New Image: {file_id} => {file}") | ||
| name = self._filename(file_id, file_ext_str) | ||
| file_info = { | ||
| "ts": int(time.time()), | ||
| # "checksum": file_checksum(os.path.join(self._datastore.image_path(), name)), | ||
| "name": name, | ||
| } | ||
|
|
||
| invalidate += 1 | ||
| self._datastore.objects[image_id] = ImageLabelModel(image=DataModel(info=image_info, ext=image_ext)) | ||
|
|
||
| self._datastore.objects[file_id] = ImageLabelModel(image=DataModel(info=file_info, ext=file_ext_str)) | ||
| return invalidate | ||
|
|
||
| def _add_non_existing_labels(self, tag) -> int: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.