datamint 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of datamint might be problematic. Click here for more details.

@@ -1,45 +1,82 @@
1
1
  import argparse
2
2
  import logging
3
+ import os
4
+ import platform
3
5
  from datamint import configs
4
6
  from datamint.utils.logging_utils import load_cmdline_logging_config
5
-
6
- # Create two loggings: one for the user and one for the developer
7
+ from rich.prompt import Prompt, Confirm
8
+ from rich.console import Console
9
+ from rich.theme import Theme
10
+
11
+ # Create a custom theme that works well on both dark and blue backgrounds
12
+ def _create_console_theme() -> Theme:
13
+ """Create a custom Rich theme optimized for cross-platform terminals."""
14
+ # Detect if we're likely on PowerShell (Windows + PowerShell)
15
+ is_powershell = (
16
+ platform.system() == "Windows" and
17
+ os.environ.get("PSModulePath") is not None
18
+ )
19
+
20
+ if is_powershell:
21
+ # PowerShell blue background - use high contrast colors
22
+ return Theme({
23
+ "warning": "bright_yellow",
24
+ "error": "bright_red on white",
25
+ "success": "bright_green",
26
+ "key": "bright_cyan",
27
+ "accent": "bright_cyan",
28
+ "title": "bold"
29
+ })
30
+ else:
31
+ # Linux/Unix terminals - standard colors
32
+ return Theme({
33
+ "warning": "yellow",
34
+ "error": "red",
35
+ "success": "green",
36
+ "key": "cyan",
37
+ "accent": "bright_blue",
38
+ "title": "bold"
39
+ })
40
+
41
+ # Create console with custom theme
42
+ console = Console(theme=_create_console_theme())
7
43
  _LOGGER = logging.getLogger(__name__)
8
- _USER_LOGGER = logging.getLogger('user_logger')
9
44
 
10
45
 
11
46
  def configure_default_url():
12
47
  """Configure the default API URL interactively."""
13
- _USER_LOGGER.info("Current default URL: %s", configs.get_value(configs.APIURL_KEY, 'Not set'))
14
- url = input("Enter the default API URL (leave empty to abort): ").strip()
48
+ current_url = configs.get_value(configs.APIURL_KEY, 'Not set')
49
+ console.print(f"Current default URL: [key]{current_url}[/key]")
50
+ url = Prompt.ask("Enter the default API URL (leave empty to abort)", console=console).strip()
15
51
  if url == '':
16
52
  return
17
53
 
18
54
  # Basic URL validation
19
55
  if not (url.startswith('http://') or url.startswith('https://')):
20
- _USER_LOGGER.warning("URL should start with http:// or https://")
56
+ console.print("[warning]⚠️ URL should start with http:// or https://[/warning]")
21
57
  return
22
58
 
23
59
  configs.set_value(configs.APIURL_KEY, url)
24
- _USER_LOGGER.info("Default API URL set successfully.")
60
+ console.print("[success]✅ Default API URL set successfully.[/success]")
25
61
 
26
62
 
27
63
  def ask_api_key(ask_to_save: bool) -> str | None:
28
64
  """Ask user for API key with improved guidance."""
29
- _USER_LOGGER.info("💡 Get your API key from your Datamint administrator or the web app (https://app.datamint.io/team)")
65
+ console.print("[info]💡 Get your API key from your Datamint administrator or the web app (https://app.datamint.io/team)[/info]")
30
66
 
31
- api_key = input('API key (leave empty to abort): ').strip()
67
+ api_key = Prompt.ask('API key (leave empty to abort)', console=console).strip()
32
68
  if api_key == '':
33
69
  return None
34
70
 
35
71
  if ask_to_save:
36
- ans = input("Save the API key so it automatically loads next time? (y/n): ")
72
+ ans = Confirm.ask("Save the API key so it automatically loads next time? (y/n): ",
73
+ default=True, console=console)
37
74
  try:
38
- if ans.lower() == 'y':
75
+ if ans:
39
76
  configs.set_value(configs.APIKEY_KEY, api_key)
40
- _USER_LOGGER.info("✅ API key saved.")
77
+ console.print("[success]✅ API key saved.[/success]")
41
78
  except Exception as e:
42
- _USER_LOGGER.error("❌ Error saving API key.")
79
+ console.print("[error]❌ Error saving API key.[/error]")
43
80
  _LOGGER.exception(e)
44
81
  return api_key
45
82
 
@@ -48,85 +85,91 @@ def show_all_configurations():
48
85
  """Display all current configurations in a user-friendly format."""
49
86
  config = configs.read_config()
50
87
  if config is not None and len(config) > 0:
51
- _USER_LOGGER.info("📋 Current configurations:")
88
+ console.print("[title]📋 Current configurations:[/title]")
52
89
  for key, value in config.items():
53
90
  # Mask API key for security
54
91
  if key == configs.APIKEY_KEY and value:
55
92
  masked_value = f"{value[:3]}...{value[-3:]}" if len(value) > 6 else value
56
- _USER_LOGGER.info(f" {key}: {masked_value}")
93
+ console.print(f" [key]{key}[/key]: [dim]{masked_value}[/dim]")
57
94
  else:
58
- _USER_LOGGER.info(f" {key}: {value}")
95
+ console.print(f" [key]{key}[/key]: {value}")
59
96
  else:
60
- _USER_LOGGER.info("No configurations found.")
97
+ console.print("[dim]No configurations found.[/dim]")
61
98
 
62
99
 
63
100
  def clear_all_configurations():
64
101
  """Clear all configurations with confirmation."""
65
- yesno = input('Are you sure you want to clear all configurations? (y/n): ')
66
- if yesno.lower() == 'y':
102
+ yesno = Confirm.ask('Are you sure you want to clear all configurations?',
103
+ default=True, console=console)
104
+ if yesno:
67
105
  configs.clear_all_configurations()
68
- _USER_LOGGER.info("All configurations cleared.")
106
+ console.print("[success]✅ All configurations cleared.[/success]")
69
107
 
70
108
 
71
109
  def configure_api_key():
110
+ """Configure API key interactively."""
72
111
  api_key = ask_api_key(ask_to_save=False)
73
112
  if api_key is None:
74
113
  return
75
114
  configs.set_value(configs.APIKEY_KEY, api_key)
76
- _USER_LOGGER.info("✅ API key saved.")
115
+ console.print("[success]✅ API key saved.[/success]")
77
116
 
78
117
 
79
118
  def test_connection():
80
119
  """Test the API connection with current settings."""
81
120
  try:
82
121
  from datamint import APIHandler
83
- _USER_LOGGER.info("🔄 Testing connection...")
122
+ console.print("[accent]🔄 Testing connection...[/accent]")
84
123
  api = APIHandler()
85
124
  # Simple test - try to get projects
86
125
  projects = api.get_projects()
87
- _USER_LOGGER.info(f"✅ Connection successful! Found {len(projects)} projects.")
126
+ console.print(f"[success]✅ Connection successful! Found {len(projects)} projects.[/success]")
88
127
  except ImportError:
89
- _USER_LOGGER.error("❌ Full API not available. Install with: pip install datamint-python-api[full]")
128
+ console.print("[error]❌ Full API not available. Install with: pip install datamint-python-api[full][/error]")
90
129
  except Exception as e:
91
- _USER_LOGGER.error(f"❌ Connection failed: {e}")
92
- _USER_LOGGER.info("💡 Check your API key and URL settings")
130
+ console.print(f"[error]❌ Connection failed: {e}[/error]")
93
131
 
94
132
 
95
133
  def interactive_mode():
96
- _USER_LOGGER.info("🔧 Datamint Configuration Tool")
97
-
98
- if len(configs.read_config()) == 0:
99
- _USER_LOGGER.info("👋 Welcome! Let's set up your API key first.")
100
- configure_api_key()
101
-
102
- while True:
103
- _USER_LOGGER.info("\n📋 Select the action you want to perform:")
104
- _USER_LOGGER.info(" (1) Configure the API key")
105
- _USER_LOGGER.info(" (2) Configure the default URL")
106
- _USER_LOGGER.info(" (3) Show all configuration settings")
107
- _USER_LOGGER.info(" (4) Clear all configuration settings")
108
- _USER_LOGGER.info(" (5) Test connection")
109
- _USER_LOGGER.info(" (q) Exit")
110
- choice = input("Enter your choice: ").lower().strip()
111
-
112
- if choice == '1':
134
+ """Run the interactive configuration mode."""
135
+ console.print("[title]🔧 Datamint Configuration Tool[/title]")
136
+
137
+ try:
138
+ if len(configs.read_config()) == 0:
139
+ console.print("[warning]👋 Welcome! Let's set up your API key first.[/warning]")
113
140
  configure_api_key()
114
- elif choice == '2':
115
- configure_default_url()
116
- elif choice == '3':
117
- show_all_configurations()
118
- elif choice == '4':
119
- clear_all_configurations()
120
- elif choice == '5':
121
- test_connection()
122
- elif choice in ('q', 'exit', 'quit'):
123
- _USER_LOGGER.info("👋 Goodbye!")
124
- break
125
- else:
126
- _USER_LOGGER.info("❌ Invalid choice. Please enter a number between 1 and 5 or 'q' to quit.")
127
141
 
142
+ while True:
143
+ console.print("\n[title]📋 Select the action you want to perform:[/title]")
144
+ console.print(" [accent](1)[/accent] Configure the API key")
145
+ console.print(" [accent](2)[/accent] Configure the default URL")
146
+ console.print(" [accent](3)[/accent] Show all configuration settings")
147
+ console.print(" [accent](4)[/accent] Clear all configuration settings")
148
+ console.print(" [accent](5)[/accent] Test connection")
149
+ console.print(" [accent](q)[/accent] Exit")
150
+ choice = Prompt.ask("Enter your choice", console=console).lower().strip()
151
+
152
+ if choice == '1':
153
+ configure_api_key()
154
+ elif choice == '2':
155
+ configure_default_url()
156
+ elif choice == '3':
157
+ show_all_configurations()
158
+ elif choice == '4':
159
+ clear_all_configurations()
160
+ elif choice == '5':
161
+ test_connection()
162
+ elif choice in ('q', 'exit', 'quit'):
163
+ break
164
+ else:
165
+ console.print("[error]❌ Invalid choice. Please enter a number between 1 and 5 or 'q' to quit.[/error]")
166
+ except KeyboardInterrupt:
167
+ console.print('')
168
+
169
+ console.print("[success]👋 Goodbye![/success]")
128
170
 
129
171
  def main():
172
+ """Main entry point for the configuration tool."""
130
173
  load_cmdline_logging_config()
131
174
  parser = argparse.ArgumentParser(
132
175
  description='🔧 Datamint API Configuration Tool',
@@ -148,15 +191,15 @@ More Documentation: https://sonanceai.github.io/datamint-python-api/command_line
148
191
 
149
192
  if args.api_key is not None:
150
193
  configs.set_value(configs.APIKEY_KEY, args.api_key)
151
- _USER_LOGGER.info("✅ API key saved.")
194
+ console.print("[success]✅ API key saved.[/success]")
152
195
 
153
196
  if args.default_url is not None:
154
197
  # Basic URL validation
155
198
  if not (args.default_url.startswith('http://') or args.default_url.startswith('https://')):
156
- _USER_LOGGER.error("❌ URL must start with http:// or https://")
199
+ console.print("[error]❌ URL must start with http:// or https://[/error]")
157
200
  return
158
201
  configs.set_value(configs.APIURL_KEY, args.default_url)
159
- _USER_LOGGER.info("✅ Default URL saved.")
202
+ console.print("[success]✅ Default URL saved.[/success]")
160
203
 
161
204
  no_arguments_provided = args.api_key is None and args.default_url is None
162
205
 
@@ -7,13 +7,14 @@ from pathlib import Path
7
7
  import sys
8
8
  from datamint.utils.dicom_utils import is_dicom
9
9
  import fnmatch
10
- from typing import Sequence, Generator, Optional, Any
10
+ from typing import Generator, Optional, Any
11
11
  from collections import defaultdict
12
12
  from datamint import __version__ as datamint_version
13
13
  from datamint import configs
14
14
  from datamint.client_cmd_tools.datamint_config import ask_api_key
15
15
  from datamint.utils.logging_utils import load_cmdline_logging_config
16
16
  import yaml
17
+ from collections.abc import Iterable
17
18
 
18
19
  # Create two loggings: one for the user and one for the developer
19
20
  _LOGGER = logging.getLogger(__name__)
@@ -86,9 +87,9 @@ def _is_system_file(path: Path) -> bool:
86
87
  return any(fnmatch.fnmatch(path.name, pattern) for pattern in ignored_patterns)
87
88
 
88
89
 
89
- def walk_to_depth(path: str,
90
+ def walk_to_depth(path: str | Path,
90
91
  depth: int,
91
- exclude_pattern: str = None) -> Generator[Path, None, None]:
92
+ exclude_pattern: str | None = None) -> Generator[Path, None, None]:
92
93
  path = Path(path)
93
94
  for child in path.iterdir():
94
95
  if _is_system_file(child):
@@ -104,7 +105,7 @@ def walk_to_depth(path: str,
104
105
  yield child
105
106
 
106
107
 
107
- def filter_files(files_path: Sequence[Path],
108
+ def filter_files(files_path: Iterable[Path],
108
109
  include_extensions,
109
110
  exclude_extensions) -> list[Path]:
110
111
  def fix_extension(ext: str) -> str:
@@ -112,7 +113,7 @@ def filter_files(files_path: Sequence[Path],
112
113
  return ext
113
114
  return '.' + ext
114
115
 
115
- def normalize_extensions(exts_list: Sequence[str]) -> list[str]:
116
+ def normalize_extensions(exts_list: Iterable[str]) -> list[str]:
116
117
  # explodes the extensions if they are separated by commas
117
118
  exts_list = [ext.split(',') for ext in exts_list]
118
119
  exts_list = [item for sublist in exts_list for item in sublist]
@@ -140,7 +141,7 @@ def filter_files(files_path: Sequence[Path],
140
141
  return files_path
141
142
 
142
143
 
143
- def handle_api_key() -> str:
144
+ def handle_api_key() -> str | None:
144
145
  """
145
146
  Checks for API keys.
146
147
  If it does not exist, it asks the user to input it.
@@ -267,7 +268,13 @@ def _find_json_metadata(file_path: str | Path) -> Optional[str]:
267
268
  Optional[str]: Path to the JSON metadata file if found, None otherwise
268
269
  """
269
270
  file_path = Path(file_path)
270
- json_path = file_path.with_suffix('.json')
271
+
272
+ # Handle .nii.gz files specially - need to remove both extensions
273
+ if file_path.name.endswith('.nii.gz'):
274
+ base_name = file_path.name[:-7] # Remove .nii.gz
275
+ json_path = file_path.parent / f"{base_name}.json"
276
+ else:
277
+ json_path = file_path.with_suffix('.json')
271
278
 
272
279
  if json_path.exists() and json_path.is_file():
273
280
  _LOGGER.debug(f"Found JSON metadata file: {json_path}")
@@ -313,19 +320,18 @@ def _collect_metadata_files(files_path: list[str], auto_detect_json: bool) -> tu
313
320
  if used_json_files:
314
321
  _LOGGER.debug(f"Filtering out {len(used_json_files)} JSON metadata files from main upload list")
315
322
  filtered_metadata_files = []
316
- filtered_file_index = 0
317
-
323
+
318
324
  for original_file in files_path:
319
325
  if original_file not in used_json_files:
320
- filtered_metadata_files.append(metadata_files[files_path.index(original_file)])
321
- filtered_file_index += 1
326
+ original_index = files_path.index(original_file)
327
+ filtered_metadata_files.append(metadata_files[original_index])
322
328
 
323
329
  metadata_files = filtered_metadata_files
324
330
 
325
331
  return metadata_files, filtered_files_path
326
332
 
327
333
 
328
- def _parse_args() -> tuple[Any, list, Optional[list[dict]], Optional[list[str]]]:
334
+ def _parse_args() -> tuple[Any, list[str], Optional[list[dict]], Optional[list[str]]]:
329
335
  parser = argparse.ArgumentParser(
330
336
  description='DatamintAPI command line tool for uploading DICOM files and other resources')
331
337
 
@@ -386,11 +392,11 @@ def _parse_args() -> tuple[Any, list, Optional[list[dict]], Optional[list[str]]]
386
392
 
387
393
  # Handle path argument priority: positional takes precedence over --path flag
388
394
  if args.path is not None and args.path_flag is not None:
389
- _USER_LOGGER.warning("Both positional path and --path flag provided. Using positional argument.")
390
- final_path = args.path
391
- elif args.path is not None:
395
+ _USER_LOGGER.warning("Both positional path and --path flag provided.")
396
+ raise ValueError("Both positional path and --path flag provided.")
397
+ elif args.path is not None and isinstance(args.path, (str, Path)):
392
398
  final_path = args.path
393
- elif args.path_flag is not None:
399
+ elif args.path_flag is not None and isinstance(args.path_flag, (str, Path)):
394
400
  final_path = args.path_flag
395
401
  else:
396
402
  parser.error("Path argument is required. Provide it as a positional argument or use --path flag.")
@@ -419,6 +425,10 @@ def _parse_args() -> tuple[Any, list, Optional[list[dict]], Optional[list[str]]]
419
425
  else:
420
426
  try:
421
427
  recursive_depth = 0 if args.recursive is None else args.recursive
428
+ if recursive_depth < 0:
429
+ recursive_depth = MAX_RECURSION_LIMIT
430
+ else:
431
+ recursive_depth = min(MAX_RECURSION_LIMIT, recursive_depth)
422
432
  file_path = walk_to_depth(args.path, recursive_depth, args.exclude)
423
433
  file_path = filter_files(file_path, args.include_extensions, args.exclude_extensions)
424
434
  file_path = list(map(str, file_path)) # from Path to str
@@ -528,7 +538,7 @@ def print_input_summary(files_path: list[str],
528
538
 
529
539
 
530
540
  def print_results_summary(files_path: list[str],
531
- results: list[str | Exception]):
541
+ results: list[str | Exception]) -> int:
532
542
  # Check for failed uploads
533
543
  failure_files = [f for f, r in zip(files_path, results) if isinstance(r, Exception)]
534
544
  _USER_LOGGER.info(f"\nUpload summary:")
@@ -536,12 +546,13 @@ def print_results_summary(files_path: list[str],
536
546
  _USER_LOGGER.info(f"\tSuccessful uploads: {len(files_path) - len(failure_files)}")
537
547
  _USER_LOGGER.info(f"\tFailed uploads: {len(failure_files)}")
538
548
  if len(failure_files) > 0:
539
- _USER_LOGGER.warning(f"\tFailed files: {failure_files}")
549
+ _USER_LOGGER.warning(f"\tFailed files: {[os.path.basename(f) for f in failure_files]}")
540
550
  _USER_LOGGER.warning(f"\nFailures:")
541
551
  for f, r in zip(files_path, results):
542
552
  _LOGGER.debug(f"Failure: {f} - {r}")
543
553
  if isinstance(r, Exception):
544
554
  _USER_LOGGER.warning(f"\t{os.path.basename(f)}: {r}")
555
+ return len(failure_files)
545
556
 
546
557
 
547
558
  def main():
@@ -551,7 +562,7 @@ def main():
551
562
  args, files_path, segfiles, metadata_files = _parse_args()
552
563
  except Exception as e:
553
564
  _USER_LOGGER.error(f'Error validating arguments. {e}')
554
- return
565
+ sys.exit(1)
555
566
 
556
567
  print_input_summary(files_path,
557
568
  args=args,
@@ -580,12 +591,14 @@ def main():
580
591
  segmentation_files=segfiles,
581
592
  transpose_segmentation=args.transpose_segmentation,
582
593
  assemble_dicoms=True,
583
- metadata_files=metadata_files
594
+ metadata=metadata_files
584
595
  )
585
596
  _USER_LOGGER.info('Upload finished!')
586
597
  _LOGGER.debug(f"Number of results: {len(results)}")
587
598
 
588
- print_results_summary(files_path, results)
599
+ num_failures = print_results_summary(files_path, results)
600
+ if num_failures > 0:
601
+ sys.exit(1)
589
602
 
590
603
 
591
604
  if __name__ == '__main__':
@@ -803,7 +803,7 @@ class Experiment:
803
803
  Args:
804
804
  resource_id: The resource ID of the sample.
805
805
  predictions: The predictions of the model. One binary mask for each class. Can be a numpy array of shape (H, W) or (N,H,W);
806
- Or a path to a png file; Or a path to a .nii.gz file.
806
+ Or a path to a png file; Or a path to a .nii/.nii.gz file.
807
807
  label_name: The name of the class or a dictionary mapping pixel values to names.
808
808
  Example: ``{1: 'Femur', 2: 'Tibia'}`` means that pixel value 1 is 'Femur' and pixel value 2 is 'Tibia'.
809
809
  frame_index: The frame index of the prediction or a list of frame indexes.
@@ -243,20 +243,20 @@ def assemble_dicoms(files_path: list[str | IO],
243
243
 
244
244
  for file_path in tqdm(files_path, desc="Reading DICOMs metadata", unit="file"):
245
245
  dicom = pydicom.dcmread(file_path,
246
- specific_tags=['FrameOfReferenceUID', 'InstanceNumber', 'Rows', 'Columns'])
247
- fr_uid = dicom.get('FrameOfReferenceUID', None)
248
- if fr_uid is None:
246
+ specific_tags=['SeriesInstanceUID', 'InstanceNumber', 'Rows', 'Columns'])
247
+ series_uid = dicom.get('SeriesInstanceUID', None)
248
+ if series_uid is None:
249
249
  # generate a random uid
250
- fr_uid = pydicom.uid.generate_uid()
250
+ series_uid = pydicom.uid.generate_uid()
251
251
  instance_number = dicom.get('InstanceNumber', 0)
252
252
  rows = dicom.get('Rows', None)
253
253
  columns = dicom.get('Columns', None)
254
- dicoms_map[fr_uid].append((instance_number, file_path, rows, columns))
254
+ dicoms_map[series_uid].append((instance_number, file_path, rows, columns))
255
255
  if hasattr(file_path, "seek"):
256
256
  file_path.seek(0)
257
257
 
258
- # Validate that all DICOMs with the same FrameOfReferenceUID have matching dimensions
259
- for fr_uid, dicom_list in dicoms_map.items():
258
+ # Validate that all DICOMs with the same SeriesInstanceUID have matching dimensions
259
+ for series_uid, dicom_list in dicoms_map.items():
260
260
  if len(dicom_list) <= 1:
261
261
  continue
262
262
 
@@ -268,7 +268,7 @@ def assemble_dicoms(files_path: list[str | IO],
268
268
  for instance_number, file_path, rows, columns in dicom_list:
269
269
  if rows != first_rows or columns != first_columns:
270
270
  msg = (
271
- f"Dimension mismatch in FrameOfReferenceUID {fr_uid}: "
271
+ f"Dimension mismatch in SeriesInstanceUID {series_uid}: "
272
272
  f"Expected {first_rows}x{first_columns}, got {rows}x{columns} "
273
273
  f"for file {file_path} and {dicom_list[0][1]}"
274
274
  )
@@ -360,11 +360,11 @@ def _generate_dicom_name(ds: pydicom.Dataset) -> str:
360
360
  # components.append(os.path.basename(ds.filename))
361
361
  if hasattr(ds, 'SeriesDescription'):
362
362
  components.append(ds.SeriesDescription)
363
- if hasattr(ds, 'SeriesNumber'):
363
+ if len(components) == 0 and hasattr(ds, 'SeriesNumber'):
364
364
  components.append(f"ser{ds.SeriesNumber}")
365
365
  if hasattr(ds, 'StudyDescription'):
366
366
  components.append(ds.StudyDescription)
367
- if hasattr(ds, 'StudyID'):
367
+ elif hasattr(ds, 'StudyID'):
368
368
  components.append(ds.StudyID)
369
369
 
370
370
  # Join components and add extension
@@ -375,8 +375,8 @@ def _generate_dicom_name(ds: pydicom.Dataset) -> str:
375
375
  if len(description) > 0:
376
376
  return description
377
377
 
378
- if hasattr(ds, 'FrameOfReferenceUID'):
379
- return ds.FrameOfReferenceUID + ".dcm"
378
+ if hasattr(ds, 'SeriesInstanceUID'):
379
+ return ds.SeriesInstanceUID + ".dcm"
380
380
 
381
381
  # Fallback to generic name if no attributes found
382
382
  return ds.filename if hasattr(ds, 'filename') else f"merged_dicom_{uuid.uuid4()}.dcm"
@@ -54,17 +54,32 @@ def read_video(file_path: str, index: int = None) -> np.ndarray:
54
54
 
55
55
 
56
56
  def read_nifti(file_path: str) -> np.ndarray:
57
- imgs = nib.load(file_path).get_fdata() # shape: (W, H, #frame) or (W, H)
58
- if imgs.ndim == 2:
59
- imgs = imgs.transpose(1, 0)
60
- imgs = imgs[np.newaxis, np.newaxis]
61
- elif imgs.ndim == 3:
62
- imgs = imgs.transpose(2, 1, 0)
63
- imgs = imgs[:, np.newaxis]
64
- else:
65
- raise ValueError(f"Unsupported number of dimensions in '{file_path}': {imgs.ndim}")
57
+ """
58
+ Read a NIfTI file and return the image data in standardized format.
59
+
60
+ Args:
61
+ file_path: Path to the NIfTI file (.nii or .nii.gz)
62
+
63
+ Returns:
64
+ np.ndarray: Image data with shape (#frames, C, H, W)
65
+ """
66
+ try:
67
+ nii_img = nib.load(file_path)
68
+ imgs = nii_img.get_fdata() # shape: (W, H, #frame) or (W, H)
69
+
70
+ if imgs.ndim == 2:
71
+ imgs = imgs.transpose(1, 0) # (W, H) -> (H, W)
72
+ imgs = imgs[np.newaxis, np.newaxis] # -> (1, 1, H, W)
73
+ elif imgs.ndim == 3:
74
+ imgs = imgs.transpose(2, 1, 0) # (W, H, #frame) -> (#frame, H, W)
75
+ imgs = imgs[:, np.newaxis] # -> (#frame, 1, H, W)
76
+ else:
77
+ raise ValueError(f"Unsupported number of dimensions in '{file_path}': {imgs.ndim}")
66
78
 
67
- return imgs
79
+ return imgs
80
+ except Exception as e:
81
+ _LOGGER.error(f"Failed to read NIfTI file '{file_path}': {e}")
82
+ raise e
68
83
 
69
84
 
70
85
  def read_image(file_path: str) -> np.ndarray:
@@ -123,6 +138,18 @@ def read_array_normalized(file_path: str,
123
138
  else:
124
139
  if mime_type == 'image/x.nifti' or file_path.endswith(NII_EXTS):
125
140
  imgs = read_nifti(file_path)
141
+ # For NIfTI files, try to load associated JSON metadata
142
+ if return_metainfo:
143
+ json_path = file_path.replace('.nii.gz', '.json').replace('.nii', '.json')
144
+ if os.path.exists(json_path):
145
+ try:
146
+ import json
147
+ with open(json_path, 'r') as f:
148
+ metainfo = json.load(f)
149
+ _LOGGER.debug(f"Loaded JSON metadata from {json_path}")
150
+ except Exception as e:
151
+ _LOGGER.warning(f"Failed to load JSON metadata from {json_path}: {e}")
152
+ metainfo = None
126
153
  elif mime_type.startswith('image/') or file_path.endswith(IMAGE_EXTS):
127
154
  imgs = read_image(file_path)
128
155
  elif file_path.endswith('.npy') or mime_type == 'application/x-numpy-data':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: datamint
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: A library for interacting with the Datamint API, designed for efficient data management, processing and Deep Learning workflows.
5
5
  Requires-Python: >=3.10
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,13 +1,13 @@
1
1
  datamint/__init__.py,sha256=7rKCCsaa4RBRTIfuHB708rai1xwDHLtkFNFJGKYG5D4,757
2
- datamint/apihandler/annotation_api_handler.py,sha256=uaxtkFCBfRdhXcNzZrqfoV_5FLe8w7MFE-4fVD1Wu8Y,40084
2
+ datamint/apihandler/annotation_api_handler.py,sha256=NMS4SPcp1QAyee8TqLp2sw9htjGcdrEiW6_Qv4h0JlE,49905
3
3
  datamint/apihandler/api_handler.py,sha256=cdVSddrFCKlF_BJ81LO1aJ0OP49rssjpNEFzJ6Q7YyY,384
4
4
  datamint/apihandler/base_api_handler.py,sha256=XSxZEQEkbQpuixGDu_P9jbxUQht3Z3JgxaeiFKPkVDM,11690
5
5
  datamint/apihandler/dto/annotation_dto.py,sha256=otCIesoqGBlbSOw4ErqFsXp2HwJsPNUQlkynQh_7pHg,7110
6
6
  datamint/apihandler/exp_api_handler.py,sha256=hFUgUgBc5rL7odK7gTW3MnrvMY1pVfJUpUdzRNobMQE,6226
7
- datamint/apihandler/root_api_handler.py,sha256=-dy5IxDP3wJAr2ahhxKKswWfyKSzn6hHrPSYwKKW1pQ,44507
7
+ datamint/apihandler/root_api_handler.py,sha256=OIGq6aHX64B94MmAikcFzF0rdekRH4l1S59x2Pa_DJA,51739
8
8
  datamint/client_cmd_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- datamint/client_cmd_tools/datamint_config.py,sha256=NNWLBaHXYhY1fqherwg0u3bcu0r95ZJIMTH45X-bJ2Q,6279
10
- datamint/client_cmd_tools/datamint_upload.py,sha256=CUqOIKHZNC1KKFS_opMYsFiOALvWh1D9RPLjft_1Qu0,25668
9
+ datamint/client_cmd_tools/datamint_config.py,sha256=md7dnWrbl10lPtXKbmD9yo6onLJsajeG8Vz0ZWH1v4M,8181
10
+ datamint/client_cmd_tools/datamint_upload.py,sha256=yGwEsg5mrAH92UIMl1d5Vv5t0HWUAam4PW-SLJHbYE0,26380
11
11
  datamint/configs.py,sha256=Bdp6NydYwyCJ2dk19_gf_o3M2ZyQOmMHpLi8wEWNHUk,1426
12
12
  datamint/dataset/__init__.py,sha256=4PlUKSvVhdfQvvuq8jQXrkdqnot-iTTizM3aM1vgSwg,47
13
13
  datamint/dataset/base_dataset.py,sha256=EnnIeF3ZaBL2M8qEV39U0ogKptyvezBNoVOvrS12bZ8,38756
@@ -16,14 +16,14 @@ datamint/examples/__init__.py,sha256=zcYnd5nLVme9GCTPYH-1JpGo8xXK2WEYvhzcy_2alZc
16
16
  datamint/examples/example_projects.py,sha256=7Nb_EaIdzJTQa9zopqc-WhTBQWQJSoQZ_KjRS4PB4FI,2931
17
17
  datamint/experiment/__init__.py,sha256=5qQOMzoG17DEd1YnTF-vS0qiM-DGdbNh42EUo91CRhQ,34
18
18
  datamint/experiment/_patcher.py,sha256=ZgbezoevAYhJsbiJTvWPALGTcUiMT371xddcTllt3H4,23296
19
- datamint/experiment/experiment.py,sha256=dP3aQdG44UQss8X-YQrCKK1dM9gxjPNGr-NTAIW6GI8,44541
19
+ datamint/experiment/experiment.py,sha256=aHK9dRFdQTi569xgUg1KqlCZLHZpDmSH3g3ndPIZvXw,44546
20
20
  datamint/logging.yaml,sha256=a5dsATpul7QHeUHB2TjABFjWaPXBMbO--dgn8GlRqwk,483
21
- datamint/utils/dicom_utils.py,sha256=HTuEjwXyTSMaTVGb9pFOO76q2KLTr2CxTDoCRElVHRA,26023
22
- datamint/utils/io_utils.py,sha256=w3loo8C29MI2ERRvrcUc-Bc9X6OcjCnZCa1RixIiEZY,5451
21
+ datamint/utils/dicom_utils.py,sha256=n1CrYg1AgnlbgIktDfVXQ1Logh8lwCqYbjqHu5GElUE,26062
22
+ datamint/utils/io_utils.py,sha256=ebP1atKkhKEf1mUU1LsVwDq0h_so7kVKkD_7hQYn_kM,6754
23
23
  datamint/utils/logging_utils.py,sha256=DvoA35ATYG3JTwfXEXYawDyKRfHeCrH0a9czfkmz8kM,1851
24
24
  datamint/utils/torchmetrics.py,sha256=lwU0nOtsSWfebyp7dvjlAggaqXtj5ohSEUXOg3L0hJE,2837
25
25
  datamint/utils/visualization.py,sha256=yaUVAOHar59VrGUjpAWv5eVvQSfztFG0eP9p5Vt3l-M,4470
26
- datamint-1.4.0.dist-info/METADATA,sha256=og99PrYnYzM6C7V9m3ERFdkndMM8Bpp1QfLVkzp52dI,4065
27
- datamint-1.4.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
- datamint-1.4.0.dist-info/entry_points.txt,sha256=mn5H6jPjO-rY0W0CAZ6Z_KKWhMLvyVaSpoqk77jlTI4,145
29
- datamint-1.4.0.dist-info/RECORD,,
26
+ datamint-1.5.0.dist-info/METADATA,sha256=_JkJij-WrsGHzHO3k39TAFKcX-0WQbvVwi-6POUgetE,4065
27
+ datamint-1.5.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
+ datamint-1.5.0.dist-info/entry_points.txt,sha256=mn5H6jPjO-rY0W0CAZ6Z_KKWhMLvyVaSpoqk77jlTI4,145
29
+ datamint-1.5.0.dist-info/RECORD,,