TDHarshithReddy commited on
Commit
c4c873f
·
1 Parent(s): d73e2bd

Add teeth CT sample, upgrade Gradio to 4.44.1, and fix app config

Browse files
download_samples.py CHANGED
@@ -1,167 +1,118 @@
1
  """
2
  Download sample CT scans for the Hugging Face Space demo.
3
- Uses publicly available data from Zenodo (TotalSegmentator sample subset).
4
  """
5
 
6
  import os
7
  import urllib.request
8
  import zipfile
9
  import shutil
10
- import gzip
11
 
12
- # Sample CT scans from public sources
13
- SAMPLE_URLS = [
14
- # TotalSegmentator sample from Zenodo (small sample subset)
15
  {
16
- "url": "https://zenodo.org/records/10047292/files/Totalsegmentator_dataset_small_v201.zip?download=1",
17
- "filename": "ts_sample.zip",
18
- "type": "zip",
19
- "description": "TotalSegmentator sample dataset (102 subjects)"
 
 
 
 
20
  }
21
  ]
22
 
23
- # Alternative: Direct links to individual sample scans from open datasets
24
- DIRECT_SAMPLES = [
25
- # These would be actual CT scan URLs if available
26
- # For now, we'll create a placeholder that downloads on first run
27
- ]
28
-
29
 
30
- def download_file(url: str, dest_path: str, description: str = ""):
31
- """Download a file with progress indication"""
32
  print(f"Downloading {description}...")
33
- print(f" URL: {url}")
34
- print(f" Destination: {dest_path}")
35
-
36
  try:
37
- urllib.request.urlretrieve(url, dest_path)
38
- print(f" ✓ Downloaded successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  return True
40
  except Exception as e:
41
  print(f" ✗ Failed: {e}")
42
  return False
43
 
44
-
45
- def extract_sample_from_zip(zip_path: str, examples_dir: str, max_samples: int = 2):
46
- """Extract a few sample CT scans from the dataset zip"""
47
- print(f"Extracting samples from {zip_path}...")
48
-
49
- with zipfile.ZipFile(zip_path, 'r') as zf:
50
- # List all files to find CT scans
51
- all_files = zf.namelist()
52
-
53
- # Find .nii.gz files (CT images)
54
- ct_files = [f for f in all_files if f.endswith('.nii.gz') and 'ct.' in f.lower()]
55
-
56
- if not ct_files:
57
- # Try alternative patterns
58
- ct_files = [f for f in all_files if f.endswith('.nii.gz') and 'image' in f.lower()]
59
-
60
- if not ct_files:
61
- # Just get any .nii.gz files
62
- ct_files = [f for f in all_files if f.endswith('.nii.gz')][:max_samples * 2]
63
-
64
- # Extract a few samples
65
- extracted = 0
66
- for ct_file in ct_files[:max_samples]:
67
- try:
68
- # Extract to examples directory
69
- output_name = f"sample_ct_{extracted + 1}.nii.gz"
70
- output_path = os.path.join(examples_dir, output_name)
71
-
72
- # Extract the file
73
- with zf.open(ct_file) as source:
74
- with open(output_path, 'wb') as target:
75
- target.write(source.read())
76
-
77
- print(f" ✓ Extracted: {output_name}")
78
- extracted += 1
79
-
80
- if extracted >= max_samples:
81
- break
82
- except Exception as e:
83
- print(f" ✗ Failed to extract {ct_file}: {e}")
84
-
85
- return extracted
86
-
87
-
88
- def create_synthetic_sample(examples_dir: str):
89
- """Create a small synthetic NIfTI file for testing"""
90
- import numpy as np
91
- import nibabel as nib
92
-
93
- print("Creating synthetic sample CT for testing...")
94
-
95
- # Create a simple 3D volume (small for demo)
96
- shape = (128, 128, 64)
97
- data = np.random.randn(*shape).astype(np.float32) * 100 - 500
98
-
99
- # Add some structure (spheres to simulate organs)
100
- center = np.array(shape) // 2
101
-
102
- for i in range(3):
103
- offset = np.array([20 * (i - 1), 0, 0])
104
- pos = center + offset
105
- x, y, z = np.ogrid[:shape[0], :shape[1], :shape[2]]
106
- mask = ((x - pos[0])**2 + (y - pos[1])**2 + (z - pos[2])**2) < 400
107
- data[mask] = 50 + i * 30 # Different intensities
108
-
109
- # Create NIfTI
110
- affine = np.diag([3.0, 3.0, 3.0, 1.0]) # 3mm spacing
111
- img = nib.Nifti1Image(data, affine)
112
-
113
- output_path = os.path.join(examples_dir, "sample_synthetic.nii.gz")
114
- nib.save(img, output_path)
115
- print(f" ✓ Created: {output_path}")
116
-
117
- return output_path
118
-
119
-
120
  def setup_examples():
121
- """Download and set up example CT scans"""
122
  examples_dir = os.path.join(os.path.dirname(__file__), "examples")
123
  os.makedirs(examples_dir, exist_ok=True)
124
 
125
- # Check if examples already exist
126
- existing = [f for f in os.listdir(examples_dir) if f.endswith('.nii.gz')]
127
- if len(existing) >= 2:
128
- print(f"Examples already exist: {existing}")
129
- return existing
130
-
131
- print("Setting up example CT scans...")
132
- print("=" * 50)
133
-
134
- # Try to download from Zenodo
135
- temp_dir = os.path.join(examples_dir, "temp")
136
- os.makedirs(temp_dir, exist_ok=True)
137
-
138
- downloaded = False
139
- for sample in SAMPLE_URLS:
140
- zip_path = os.path.join(temp_dir, sample["filename"])
141
- if download_file(sample["url"], zip_path, sample["description"]):
142
- if sample["type"] == "zip":
143
- extracted = extract_sample_from_zip(zip_path, examples_dir)
144
- if extracted > 0:
145
- downloaded = True
146
- break
147
-
148
- # Clean up temp files
149
- if os.path.exists(temp_dir):
150
- shutil.rmtree(temp_dir)
151
-
152
- # If download failed, create synthetic sample
153
- if not downloaded:
154
- print("\nDownload failed, creating synthetic sample for testing...")
155
- try:
156
- create_synthetic_sample(examples_dir)
157
- except ImportError:
158
- print(" nibabel not available, skipping synthetic sample creation")
159
-
160
- # List final examples
161
- final_examples = [f for f in os.listdir(examples_dir) if f.endswith('.nii.gz')]
162
- print(f"\nFinal examples: {final_examples}")
163
- return final_examples
164
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  if __name__ == "__main__":
167
  setup_examples()
 
1
  """
2
  Download sample CT scans for the Hugging Face Space demo.
 
3
  """
4
 
5
  import os
6
  import urllib.request
7
  import zipfile
8
  import shutil
9
+ import time
10
 
11
+ # Direct samples from MONAI Model Zoo
12
+ MONAI_SAMPLES = [
 
13
  {
14
+ "url": "https://raw.githubusercontent.com/Project-MONAI/model-zoo/dev/models/wholeBody_ct_segmentation/sampledata/imagesTr/s0037.nii.gz",
15
+ "filename": "sample_ct_s0037.nii.gz",
16
+ "description": "MONAI Model Zoo Sample s0037"
17
+ },
18
+ {
19
+ "url": "https://raw.githubusercontent.com/Project-MONAI/model-zoo/dev/models/wholeBody_ct_segmentation/sampledata/imagesTr/s0038.nii.gz",
20
+ "filename": "sample_ct_s0038.nii.gz",
21
+ "description": "MONAI Model Zoo Sample s0038"
22
  }
23
  ]
24
 
25
+ # Fallback: Zenodo Small Subset
26
+ ZENODO_URL = "https://zenodo.org/records/10047263/files/Totalsegmentator_dataset_small_v201.zip?download=1"
 
 
 
 
27
 
28
+ def download_file(url, output_path, description):
 
29
  print(f"Downloading {description}...")
30
+ print(f" Url: {url}")
 
 
31
  try:
32
+ # User-Agent needed for some servers
33
+ opener = urllib.request.build_opener()
34
+ opener.addheaders = [('User-agent', 'Mozilla/5.0')]
35
+ urllib.request.install_opener(opener)
36
+
37
+ urllib.request.urlretrieve(url, output_path)
38
+
39
+ # Verify file size (sometimes GitHub returns 404 text file)
40
+ size = os.path.getsize(output_path)
41
+ if size < 1000: # < 1KB likely error text
42
+ with open(output_path, 'r') as f:
43
+ content = f.read(100)
44
+ if "404: Not Found" in content or "Not Found" in content:
45
+ print(f" ✗ Downloaded file appears to be a 404 page.")
46
+ os.remove(output_path)
47
+ return False
48
+
49
+ print(f" ✓ Success! Saved to {output_path} ({size/1024/1024:.2f} MB)")
50
  return True
51
  except Exception as e:
52
  print(f" ✗ Failed: {e}")
53
  return False
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  def setup_examples():
 
56
  examples_dir = os.path.join(os.path.dirname(__file__), "examples")
57
  os.makedirs(examples_dir, exist_ok=True)
58
 
59
+ success_count = 0
60
+
61
+ # 1. Try Direct MONAI Samples
62
+ print("\n--- Attempting to download direct samples from MONAI Model Zoo ---")
63
+ for sample in MONAI_SAMPLES:
64
+ dest = os.path.join(examples_dir, sample["filename"])
65
+ if not os.path.exists(dest):
66
+ if download_file(sample["url"], dest, sample["description"]):
67
+ success_count += 1
68
+ else:
69
+ print(f" ✓ {sample['filename']} already exists")
70
+ success_count += 1
71
+
72
+ # 2. If NO samples found/downloaded, try Zenodo Zip
73
+ # We only do this if we really need data, as it's 3GB
74
+ if success_count == 0:
75
+ print("\n--- Direct downloads failed. Downloading Zenodo subset (WARNING: ~3.2GB) ---")
76
+ zip_path = os.path.join(examples_dir, "temp_zenodo.zip")
77
+
78
+ print(f"Downloading Zenodo zip to {zip_path}...")
79
+ # Note: This might timeout on some systems, simpler logic here
80
+ if download_file(ZENODO_URL, zip_path, "Zenodo TotalSegmentator Small Subset"):
81
+ try:
82
+ print("Extracting random samples from zip...")
83
+ with zipfile.ZipFile(zip_path, 'r') as zf:
84
+ # Find ct.nii.gz files inside the structure
85
+ # Structure is usually: Totalsegmentator_dataset_small_v201/subject_id/ct.nii.gz
86
+ files = zf.namelist()
87
+ ct_files = [f for f in files if f.endswith('ct.nii.gz')]
88
+
89
+ extracted = 0
90
+ for i, ct_file in enumerate(ct_files[:3]): # Get first 3
91
+ out_name = f"sample_ct_zenodo_{i+1}.nii.gz"
92
+ out_path = os.path.join(examples_dir, out_name)
93
+
94
+ with zf.open(ct_file) as source, open(out_path, 'wb') as target:
95
+ shutil.copyfileobj(source, target)
96
+
97
+ print(f" ✓ Extracted {out_name}")
98
+ extracted += 1
99
+
100
+ if extracted > 0:
101
+ success_count += extracted
102
+ except Exception as e:
103
+ print(f" ✗ Extraction failed: {e}")
104
+
105
+ # Cleanup zip
106
+ if os.path.exists(zip_path):
107
+ print("Cleaning up zip file...")
108
+ os.remove(zip_path)
109
+
110
+ # 3. Check what we have
111
+ final_files = [f for f in os.listdir(examples_dir) if f.endswith('.nii.gz')]
112
+ print(f"\nTotal example files in {examples_dir}: {len(final_files)}")
113
+ print(final_files)
114
+
115
+ return final_files
116
 
117
  if __name__ == "__main__":
118
  setup_examples()
download_slicer_samples.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import urllib.request
2
+ import os
3
+ import SimpleITK as sitk
4
+
5
+ SAMPLES = [
6
+ {
7
+ "name": "sample_ct_chest.nii.gz",
8
+ "url": "https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e"
9
+ },
10
+ {
11
+ "name": "sample_ct_abdomen_panoramix.nii.gz",
12
+ "url": "https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433"
13
+ },
14
+ {
15
+ "name": "sample_ct_cardio.nii.gz",
16
+ "url": "https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2"
17
+ },
18
+ {
19
+ "name": "sample_ct_liver.nii.gz",
20
+ "url": "https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e"
21
+ },
22
+ {
23
+ "name": "sample_ct_teeth.nii.gz",
24
+ "url": "https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968",
25
+ "source_ext": ".gipl.gz"
26
+ }
27
+ ]
28
+
29
+ def download_and_convert():
30
+ examples_dir = "examples"
31
+ os.makedirs(examples_dir, exist_ok=True)
32
+
33
+ for sample in SAMPLES:
34
+ output_path = os.path.join(examples_dir, sample["name"])
35
+
36
+ if "source_ext" in sample:
37
+ temp_ext = sample["source_ext"]
38
+ elif "gipl.gz" in sample["name"] or "gipl.gz" in sample["url"]:
39
+ temp_ext = ".gipl.gz"
40
+ elif "nii.gz" in sample["name"]:
41
+ temp_ext = ".nii.gz"
42
+ else:
43
+ temp_ext = ".nrrd"
44
+
45
+ print(f"DEBUG: sample={sample['name']}, source_ext={sample.get('source_ext')}, temp_ext={temp_ext}")
46
+
47
+ temp_file = os.path.join(examples_dir, f"temp_download{temp_ext}")
48
+
49
+ if os.path.exists(output_path):
50
+ print(f"Skipping {sample['name']}, already exists.")
51
+ continue
52
+
53
+ print(f"Downloading {sample['name']} from {sample['url']}...")
54
+ try:
55
+ # Use a custom generic compatible header to avoid 403 blocks sometimes
56
+ opener = urllib.request.build_opener()
57
+ opener.addheaders = [('User-agent', 'Mozilla/5.0')]
58
+ urllib.request.install_opener(opener)
59
+
60
+ urllib.request.urlretrieve(sample['url'], temp_file)
61
+
62
+ print(f" Converting {temp_ext} to NIfTI...")
63
+ img = sitk.ReadImage(temp_file)
64
+ sitk.WriteImage(img, output_path)
65
+
66
+ os.remove(temp_file)
67
+ print(f" ✓ Success: {output_path}")
68
+
69
+ except Exception as e:
70
+ print(f" ✗ Failed: {e}")
71
+ if os.path.exists(temp_file):
72
+ os.remove(temp_file)
73
+
74
+ if __name__ == "__main__":
75
+ download_and_convert()
examples/sample_ct_teeth.nii.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2efad5322a5e4859a3f0d0713f6bb5064e10ac450aa4f36958785df71cf2b1d2
3
+ size 24422912
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  # Pin compatible versions to avoid HfFolder import error
2
- gradio==4.44.0
3
  huggingface_hub==0.24.7
4
  monai[nibabel]>=1.3.0
5
  torch>=2.0.0
 
1
  # Pin compatible versions to avoid HfFolder import error
2
+ gradio==4.44.1
3
  huggingface_hub==0.24.7
4
  monai[nibabel]>=1.3.0
5
  torch>=2.0.0
slicer_samples.py ADDED
@@ -0,0 +1,1486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import textwrap
4
+
5
+ import ctk
6
+ import qt
7
+ import vtk
8
+
9
+ import slicer
10
+ from slicer.i18n import tr as _
11
+ from slicer.i18n import translate
12
+ from slicer.ScriptedLoadableModule import *
13
+ from slicer.util import computeChecksum, extractAlgoAndDigest, TESTING_DATA_URL
14
+
15
+
16
+ #
17
+ # SampleData methods
18
+
19
+ def downloadFromURL(uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None,
20
+ customDownloader=None, loadFileTypes=None, loadFileProperties={}):
21
+ """Download and optionally load data into the application.
22
+
23
+ :param uris: Download URL(s).
24
+ :param fileNames: File name(s) that will be downloaded (and loaded).
25
+ :param nodeNames: Node name(s) in the scene.
26
+ :param checksums: Checksum(s) formatted as ``<algo>:<digest>`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
27
+ :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
28
+ :param customDownloader: Custom function for downloading.
29
+ :param loadFileTypes: file format name(s) (if not specified then the default file reader will be used).
30
+ :param loadFileProperties: custom properties passed to the IO plugin.
31
+
32
+ If the given ``fileNames`` are not found in the application cache directory, they
33
+ are downloaded using the associated URIs.
34
+ See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()``
35
+
36
+ If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are
37
+ guessed based on the corresponding filename extensions.
38
+
39
+ If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded
40
+ by default. To ensure the file is loaded, ``loadFiles`` must be set.
41
+
42
+ The ``loadFileProperties`` are common for all files. If different properties
43
+ need to be associated with files of different types, downloadFromURL must
44
+ be called for each.
45
+ """
46
+ return SampleDataLogic().downloadFromURL(
47
+ uris, fileNames, nodeNames, checksums, loadFiles, customDownloader, loadFileTypes, loadFileProperties)
48
+
49
+
50
+ def downloadSample(sampleName):
51
+ """For a given sample name this will search the available sources
52
+ and load it if it is available. Returns the first loaded node.
53
+ """
54
+ return SampleDataLogic().downloadSamples(sampleName)[0]
55
+
56
+
57
+ def downloadSamples(sampleName):
58
+ """For a given sample name this will search the available sources
59
+ and load it if it is available. Returns the loaded nodes.
60
+ """
61
+ return SampleDataLogic().downloadSamples(sampleName)
62
+
63
+
64
+ #
65
+ # SampleData
66
+ #
67
+
68
+
69
+ class SampleData(ScriptedLoadableModule):
70
+ """Uses ScriptedLoadableModule base class, available at:
71
+ https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py
72
+ """
73
+
74
+ def __init__(self, parent):
75
+ ScriptedLoadableModule.__init__(self, parent)
76
+ self.parent.title = _("Sample Data")
77
+ self.parent.categories = [translate("qSlicerAbstractCoreModule", "Informatics")]
78
+ self.parent.dependencies = []
79
+ self.parent.contributors = ["Steve Pieper (Isomics), Benjamin Long (Kitware), Jean-Christophe Fillion-Robin (Kitware)"]
80
+ self.parent.helpText = _("""
81
+ This module provides data sets that can be used for testing 3D Slicer.
82
+ """)
83
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
84
+ self.parent.acknowledgementText = _("""
85
+ <p>This work was was funded in part by Cancer Care Ontario
86
+ and the Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO)</p>
87
+
88
+ <p>MRHead, CBCT-MR Head, and CT-MR Brain data sets were donated to 3D Slicer project by the persons visible in the images, to be used without any restrictions.</p>
89
+
90
+ <p>CTLiver dataset comes from <a href="http://medicaldecathlon.com/">Medical Decathlon project</a> (imagesTr/liver_100.nii.gz in Task03_Liver collection)
91
+ with a permissive copyright-license (<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA 4.0</a>), allowing for data to be shared, distributed and improved upon.</p>
92
+
93
+ <p>CTA abdomen (Panoramix) dataset comes from <a href="https://www.osirix-viewer.com/resources/dicom-image-library/">Osirix DICOM image library</a>
94
+ and is exclusively available for research and teaching. You are not authorized to redistribute or sell it, or
95
+ use it for commercial purposes.</p>
96
+ """)
97
+
98
+ if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != "ScriptedModule":
99
+ slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
100
+
101
+ # Trigger the menu to be added when application has started up
102
+ if not slicer.app.commandOptions().noMainWindow:
103
+ slicer.app.connect("startupCompleted()", self.addMenu)
104
+
105
+ # allow other modules to register sample data sources by appending
106
+ # instances or subclasses SampleDataSource objects on this list
107
+ try:
108
+ slicer.modules.sampleDataSources
109
+ except AttributeError:
110
+ slicer.modules.sampleDataSources = {}
111
+
112
+ def addMenu(self):
113
+ a = qt.QAction(_("Download Sample Data"), slicer.util.mainWindow())
114
+ a.setToolTip(_("Go to the SampleData module to download data from the network"))
115
+ a.connect("triggered()", self.select)
116
+
117
+ fileMenu = slicer.util.lookupTopLevelWidget("FileMenu")
118
+ if fileMenu:
119
+ for action in fileMenu.actions():
120
+ if action.objectName == "FileSaveSceneAction":
121
+ fileMenu.insertAction(action, a)
122
+ fileMenu.insertSeparator(action)
123
+
124
+ def select(self):
125
+ m = slicer.util.mainWindow()
126
+ m.moduleSelector().selectModule("SampleData")
127
+
128
+
129
+ #
130
+ # SampleDataSource
131
+ #
132
+ class SampleDataSource:
133
+ """Describe a set of sample data associated with one or multiple URIs and filenames.
134
+
135
+ Example::
136
+
137
+ import SampleData
138
+ from slicer.util import TESTING_DATA_URL
139
+ dataSource = SampleData.SampleDataSource(
140
+ nodeNames='fixed',
141
+ fileNames='fixed.nrrd',
142
+ uris=TESTING_DATA_URL + 'SHA256/b757f9c61c1b939f104e5d7861130bb28d90f33267a012eb8bb763a435f29d37')
143
+ loadedNode = SampleData.SampleDataLogic().downloadFromSource(dataSource)[0]
144
+ """
145
+
146
+ def __init__(self, sampleName=None, sampleDescription=None, uris=None, fileNames=None, nodeNames=None,
147
+ checksums=None, loadFiles=None,
148
+ customDownloader=None, thumbnailFileName=None,
149
+ loadFileTypes=None, loadFileProperties=None,
150
+ loadFileType=None):
151
+ """
152
+ :param sampleName: Name identifying the data set.
153
+ :param sampleDescription: Displayed name of data set in SampleData module GUI. (default is ``sampleName``)
154
+ :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI,
155
+ :param uris: Download URL(s).
156
+ :param fileNames: File name(s) that will be downloaded (and loaded).
157
+ :param nodeNames: Node name(s) in the scene.
158
+ :param checksums: Checksum(s) formatted as ``<algo>:<digest>`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
159
+ :param loadFiles: Boolean indicating if file(s) should be loaded.
160
+ :param customDownloader: Custom function for downloading.
161
+ :param loadFileTypes: file format name(s) (if not specified then the default file reader will be used).
162
+ :param loadFileProperties: custom properties passed to the IO plugin.
163
+ :param loadFileType: deprecated, use ``loadFileTypes`` instead.
164
+ """
165
+
166
+ # For backward compatibility (allow using "loadFileType" instead of "loadFileTypes")
167
+ if (loadFileType is not None) and (loadFileTypes is not None):
168
+ raise ValueError("loadFileType and loadFileTypes cannot be specified at the same time")
169
+ if (loadFileType is not None) and (loadFileTypes is None):
170
+ loadFileTypes = loadFileType
171
+
172
+ self.sampleName = sampleName
173
+ if sampleDescription is None:
174
+ sampleDescription = sampleName
175
+ self.sampleDescription = sampleDescription
176
+ if isinstance(uris, list) or isinstance(uris, tuple):
177
+ if isinstance(loadFileTypes, str) or loadFileTypes is None:
178
+ loadFileTypes = [loadFileTypes] * len(uris)
179
+ if nodeNames is None:
180
+ nodeNames = [None] * len(uris)
181
+ if loadFiles is None:
182
+ loadFiles = [None] * len(uris)
183
+ if checksums is None:
184
+ checksums = [None] * len(uris)
185
+ elif isinstance(uris, str):
186
+ uris = [uris]
187
+ fileNames = [fileNames]
188
+ nodeNames = [nodeNames]
189
+ loadFiles = [loadFiles]
190
+ loadFileTypes = [loadFileTypes]
191
+ checksums = [checksums]
192
+
193
+ if loadFileProperties is None:
194
+ loadFileProperties = {}
195
+
196
+ self.uris = uris
197
+ self.fileNames = fileNames
198
+ self.nodeNames = nodeNames
199
+ self.loadFiles = loadFiles
200
+ self.customDownloader = customDownloader
201
+ self.thumbnailFileName = thumbnailFileName
202
+ self.loadFileTypes = loadFileTypes
203
+ self.loadFileProperties = loadFileProperties
204
+ self.checksums = checksums
205
+ if not len(uris) == len(fileNames) == len(nodeNames) == len(loadFiles) == len(loadFileTypes) == len(checksums):
206
+ raise ValueError(
207
+ f"All fields of sample data source must have the same length\n"
208
+ f" uris : {uris}\n"
209
+ f" len(uris) : {len(uris)}\n"
210
+ f" len(fileNames) : {len(fileNames)}\n"
211
+ f" len(nodeNames) : {len(nodeNames)}\n"
212
+ f" len(loadFiles) : {len(loadFiles)}\n"
213
+ f" len(loadFileTypes) : {len(loadFileTypes)}\n"
214
+ f" len(checksums) : {len(checksums)}\n",
215
+ )
216
+
217
+ def __eq__(self, other):
218
+ return str(self) == str(other)
219
+
220
+ def __str__(self):
221
+ output = [
222
+ "sampleName : %s" % self.sampleName,
223
+ "sampleDescription : %s" % self.sampleDescription,
224
+ "thumbnailFileName : %s" % self.thumbnailFileName,
225
+ "loadFileProperties: %s" % self.loadFileProperties,
226
+ "customDownloader : %s" % self.customDownloader,
227
+ "",
228
+ ]
229
+ for fileName, uri, nodeName, loadFile, fileType, checksum in zip(
230
+ self.fileNames, self.uris, self.nodeNames, self.loadFiles, self.loadFileTypes, self.checksums, strict=True):
231
+
232
+ output.extend([
233
+ " fileName : %s" % fileName,
234
+ " uri : %s" % uri,
235
+ " checksum : %s" % checksum,
236
+ " nodeName : %s" % nodeName,
237
+ " loadFile : %s" % loadFile,
238
+ " loadFileType : %s" % fileType,
239
+ "",
240
+ ])
241
+ return "\n".join(output)
242
+
243
+
244
+ #
245
+ # SampleData widget
246
+ #
247
+
248
+ class TextEditFinishEventFilter(qt.QObject):
249
+
250
+ def __init__(self, *args, **kwargs):
251
+ qt.QObject.__init__(self, *args, **kwargs)
252
+ self.textEditFinishedCallback = None
253
+
254
+ def eventFilter(self, object, event):
255
+ if self.textEditFinishedCallback is None:
256
+ return
257
+ if (event.type() == qt.QEvent.KeyPress and event.key() in (qt.Qt.Key_Return, qt.Qt.Key_Enter)
258
+ and (event.modifiers() & qt.Qt.ControlModifier)):
259
+ self.textEditFinishedCallback()
260
+ return True # stop further handling
261
+ return False
262
+
263
+ class SampleDataWidget(ScriptedLoadableModuleWidget):
264
+ """Uses ScriptedLoadableModuleWidget base class, available at:
265
+ https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py
266
+ """
267
+
268
+ def setup(self):
269
+ ScriptedLoadableModuleWidget.setup(self)
270
+
271
+ # This module is often used in developer mode, therefore
272
+ # collapse reload & test section by default.
273
+ if hasattr(self, "reloadCollapsibleButton"):
274
+ self.reloadCollapsibleButton.collapsed = True
275
+
276
+ self.logic = SampleDataLogic(self.logMessage)
277
+
278
+ self.categoryLayout = qt.QVBoxLayout()
279
+ self.categoryLayout.setContentsMargins(0, 0, 0, 0)
280
+ self.layout.addLayout(self.categoryLayout)
281
+
282
+ SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, slicer.modules.sampleDataSources, self.logic)
283
+ if self.developerMode is False:
284
+ self.setCategoryVisible(self.logic.developmentCategoryName, False)
285
+
286
+ customFrame = ctk.ctkCollapsibleGroupBox()
287
+ self.categoryLayout.addWidget(customFrame)
288
+ customFrame.title = _("Load data from URL")
289
+ customFrameLayout = qt.QVBoxLayout()
290
+ customFrame.setLayout(customFrameLayout)
291
+ self.customSampleLabel = qt.QLabel(_("Download URLs:"))
292
+ customFrameLayout.addWidget(self.customSampleLabel)
293
+ self.customSampleUrlEdit = qt.QTextEdit()
294
+ self.customSampleUrlEdit.toolTip = _("Enter one or more URLs (one per line) to download and load the corresponding data sets. "
295
+ "Press Ctrl+Enter or click 'Load' button to start loading.")
296
+
297
+ # Install event filter to catch Ctrl+Enter keypress to start loading
298
+ self.customSampleUrlEditFinishedEventFilter = TextEditFinishEventFilter()
299
+ self.customSampleUrlEditFinishedEventFilter.textEditFinishedCallback = self.onCustomDataDownload
300
+ self.customSampleUrlEdit.installEventFilter(self.customSampleUrlEditFinishedEventFilter)
301
+
302
+ customFrameLayout.addWidget(self.customSampleUrlEdit)
303
+ self.customDownloadButton = qt.QPushButton(_("Load"))
304
+ self.customDownloadButton.toolTip = _("Download the dataset from the given URL and import it into the scene")
305
+ self.customDownloadButton.default = True
306
+ customFrameLayout.addWidget(self.customDownloadButton)
307
+ self.showCustomDataFolderButton = qt.QPushButton(_("Show folder"))
308
+ self.showCustomDataFolderButton.toolTip = _("Show folder where custom data sets are downloaded ({path}).").format(path=slicer.app.cachePath)
309
+ customFrameLayout.addWidget(self.showCustomDataFolderButton)
310
+ customFrame.collapsed = True
311
+ self.customDownloadButton.connect("clicked()", self.onCustomDataDownload)
312
+ self.showCustomDataFolderButton.connect("clicked()", self.onShowCustomDataFolder)
313
+
314
+ self.log = qt.QTextEdit()
315
+ self.log.readOnly = True
316
+ self.layout.addWidget(self.log)
317
+
318
+ # Add spacer to layout
319
+ self.layout.addStretch(1)
320
+
321
+ def cleanup(self):
322
+ SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, {}, self.logic)
323
+
324
+ def onCustomDataDownload(self):
325
+ uris = self.customSampleUrlEdit.toPlainText().strip()
326
+ if not uris:
327
+ return
328
+ uriList = [u.strip() for u in uris.splitlines() if u.strip()]
329
+ totalCount = 0
330
+ errorCount = 0
331
+ for uri in uriList:
332
+ self.logMessage(f"Downloading data from {uri} ...", logging.INFO)
333
+ if not uri:
334
+ continue
335
+ try:
336
+ totalCount += 1
337
+ # Get filename from URL
338
+ import urllib
339
+ parsedUrl = urllib.parse.urlparse(uri)
340
+ basename, ext = os.path.splitext(os.path.basename(parsedUrl.path))
341
+ filename = basename + ext
342
+ # Download
343
+ result = self.logic.downloadFromURL(uris=uri, fileNames=filename, forceDownload=True)
344
+ if not result:
345
+ errorCount += 1
346
+ except Exception as e:
347
+ errorCount += 1
348
+ self.logMessage(f"Data download from {uri} failed: {str(e)}", logging.ERROR)
349
+ import traceback
350
+ traceback.print_exc()
351
+
352
+ if totalCount > 1:
353
+ self.logMessage("", logging.INFO)
354
+ if errorCount == 0:
355
+ self.logMessage(_("All {totalCount} data sets were loaded successfully.").format(totalCount=totalCount), logging.INFO)
356
+ else:
357
+ self.logMessage(_("Failed to load {errorCount} out of {totalCount} data sets.").format(errorCount=errorCount, totalCount=totalCount), logging.ERROR)
358
+
359
+ def onShowCustomDataFolder(self):
360
+ qt.QDesktopServices.openUrl(qt.QUrl("file:///" + slicer.app.cachePath, qt.QUrl.TolerantMode))
361
+
362
+ @staticmethod
363
+ def removeCategories(categoryLayout):
364
+ """Remove all categories from the given category layout."""
365
+ while categoryLayout.count() > 0:
366
+ frame = categoryLayout.itemAt(0).widget()
367
+ frame.visible = False
368
+ categoryLayout.removeWidget(frame)
369
+ frame.setParent(0)
370
+ del frame
371
+
372
+ @staticmethod
373
+ def setCategoriesFromSampleDataSources(categoryLayout, dataSources, logic):
374
+ """Update categoryLayout adding buttons for downloading dataSources.
375
+
376
+ Download buttons are organized in collapsible GroupBox with one GroupBox
377
+ per category.
378
+ """
379
+ iconPath = os.path.join(os.path.dirname(__file__).replace("\\", "/"), "Resources", "Icons")
380
+ mainWindow = slicer.util.mainWindow()
381
+ if mainWindow:
382
+ # Set thumbnail size from default icon size. This results in toolbutton size that makes
383
+ # two columns of buttons fit into the size of the Welcome module's minimum width
384
+ # on screens with a various resolution and scaling (see qt.QDesktopWidget().size,
385
+ # desktop.devicePixelRatioF(), qt.QDesktopWidget().physicalDpiX())
386
+ iconSize = qt.QSize(int(mainWindow.iconSize.width() * 6), int(mainWindow.iconSize.height() * 4))
387
+ else:
388
+ # There is no main window in the automated tests
389
+ screens = slicer.app.screens()
390
+ primaryScreen = screens[0] if screens else None
391
+ if primaryScreen:
392
+ mainScreenSize = primaryScreen.availableGeometry.size()
393
+ iconSize = qt.QSize(
394
+ int(mainScreenSize.width() / 15),
395
+ int(mainScreenSize.height() / 10),
396
+ )
397
+ else:
398
+ # Absolute fallback (should never happen, but keeps tests robust)
399
+ iconSize = qt.QSize(128, 128)
400
+
401
+ categories = sorted(dataSources.keys())
402
+
403
+ # Ensure "builtIn" category is always first
404
+ if logic.builtInCategoryName in categories:
405
+ categories.remove(logic.builtInCategoryName)
406
+ categories.insert(0, logic.builtInCategoryName)
407
+
408
+ # Clear category layout
409
+ SampleDataWidget.removeCategories(categoryLayout)
410
+
411
+ # Populate category layout
412
+ for category in categories:
413
+ frame = ctk.ctkCollapsibleGroupBox(categoryLayout.parentWidget())
414
+ categoryLayout.addWidget(frame)
415
+ frame.title = category
416
+ frame.name = "%sCollapsibleGroupBox" % category
417
+ layout = ctk.ctkFlowLayout()
418
+ layout.preferredExpandingDirections = qt.Qt.Vertical
419
+ frame.setLayout(layout)
420
+ for source in dataSources[category]:
421
+ name = source.sampleDescription
422
+ if not name:
423
+ name = source.nodeNames[0]
424
+
425
+ b = qt.QToolButton()
426
+ b.setText(name)
427
+
428
+ # Set thumbnail
429
+ if source.thumbnailFileName:
430
+ # Thumbnail provided
431
+ thumbnailImage = source.thumbnailFileName
432
+ else:
433
+ # Look for thumbnail image with the name of any node name with .png extension
434
+ thumbnailImage = None
435
+ for nodeName in source.nodeNames:
436
+ if not nodeName:
437
+ continue
438
+ thumbnailImageAttempt = os.path.join(iconPath, nodeName + ".png")
439
+ if os.path.exists(thumbnailImageAttempt):
440
+ thumbnailImage = thumbnailImageAttempt
441
+ break
442
+ if thumbnailImage and os.path.exists(thumbnailImage):
443
+ b.setIcon(qt.QIcon(thumbnailImage))
444
+
445
+ b.setIconSize(iconSize)
446
+ b.setToolButtonStyle(qt.Qt.ToolButtonTextUnderIcon)
447
+ qSize = qt.QSizePolicy()
448
+ qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
449
+ b.setSizePolicy(qSize)
450
+
451
+ b.name = "%sPushButton" % name
452
+ layout.addWidget(b)
453
+ if source.customDownloader:
454
+ b.connect("clicked()", lambda s=source: s.customDownloader(s))
455
+ else:
456
+ b.connect("clicked()", lambda s=source: logic.downloadFromSource(s))
457
+
458
+ def logMessage(self, message, logLevel=logging.DEBUG):
459
+ # Format based on log level
460
+ if logLevel >= logging.ERROR:
461
+ message = '<b><font color="red">' + message + "</font></b>"
462
+ elif logLevel >= logging.WARNING:
463
+ message = '<b><font color="orange">' + message + "</font></b>"
464
+
465
+ # Show message in status bar
466
+ doc = qt.QTextDocument()
467
+ doc.setHtml(message)
468
+ slicer.util.showStatusMessage(doc.toPlainText(), 3000)
469
+ logging.log(logLevel, doc.toPlainText())
470
+
471
+ # Show message in log window at the bottom of the module widget
472
+ self.log.insertHtml(message)
473
+ self.log.insertPlainText("\n")
474
+ self.log.ensureCursorVisible()
475
+ self.log.repaint()
476
+
477
+ slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents)
478
+
479
+ def isCategoryVisible(self, category):
480
+ """Check the visibility of a SampleData category given its name.
481
+
482
+ Returns False if the category is not visible or if it does not exist,
483
+ otherwise returns True.
484
+ """
485
+ if not SampleDataLogic.sampleDataSourcesByCategory(category):
486
+ return False
487
+ return slicer.util.findChild(self.parent, "%sCollapsibleGroupBox" % category).isVisible()
488
+
489
+ def setCategoryVisible(self, category, visible):
490
+ """Update visibility of a SampleData category given its name.
491
+
492
+ The function is a no-op if the category does not exist.
493
+ """
494
+ if not SampleDataLogic.sampleDataSourcesByCategory(category):
495
+ return
496
+ slicer.util.findChild(self.parent, "%sCollapsibleGroupBox" % category).setVisible(visible)
497
+
498
+
499
+ #
500
+ # SampleData logic
501
+ #
502
+
503
+
504
+ class SampleDataLogic:
505
+ """Manage the slicer.modules.sampleDataSources dictionary.
506
+ The dictionary keys are categories of sample data sources.
507
+ The BuiltIn category is managed here. Modules or extensions can
508
+ register their own sample data by creating instances of the
509
+ SampleDataSource class. These instances should be stored in a
510
+ list that is assigned to a category following the model
511
+ used in registerBuiltInSampleDataSources below.
512
+
513
+ Checksums are expected to be formatted as a string of the form
514
+ ``<algo>:<digest>``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
515
+ """
516
+
517
+ @staticmethod
518
+ def registerCustomSampleDataSource(category="Custom",
519
+ sampleName=None, uris=None, fileNames=None, nodeNames=None,
520
+ customDownloader=None, thumbnailFileName=None,
521
+ loadFileTypes=None, loadFiles=None, loadFileProperties={},
522
+ checksums=None,
523
+ loadFileType=None):
524
+ """Adds custom data sets to SampleData.
525
+ :param category: Section title of data set in SampleData module GUI.
526
+ :param sampleName: Displayed name of data set in SampleData module GUI.
527
+ :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI,
528
+ :param uris: Download URL(s).
529
+ :param fileNames: File name(s) that will be loaded.
530
+ :param nodeNames: Node name(s) in the scene.
531
+ :param customDownloader: Custom function for downloading.
532
+ :param loadFileTypes: file format name(s) (if not specified then the default file reader will be used).
533
+ :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
534
+ :param loadFileProperties: custom properties passed to the IO plugin.
535
+ :param checksums: Checksum(s) formatted as ``<algo>:<digest>`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
536
+ :param loadFileType: deprecated, use ``loadFileTypes`` instead.
537
+ """
538
+
539
+ # For backward compatibility (allow using "loadFileType" instead of "loadFileTypes")
540
+ if (loadFileType is not None) and (loadFileTypes is not None):
541
+ raise ValueError("loadFileType and loadFileTypes cannot be specified at the same time")
542
+ if (loadFileType is not None) and (loadFileTypes is None):
543
+ loadFileTypes = loadFileType
544
+
545
+ try:
546
+ slicer.modules.sampleDataSources
547
+ except AttributeError:
548
+ slicer.modules.sampleDataSources = {}
549
+
550
+ if category not in slicer.modules.sampleDataSources:
551
+ slicer.modules.sampleDataSources[category] = []
552
+
553
+ dataSource = SampleDataSource(
554
+ sampleName=sampleName,
555
+ uris=uris,
556
+ fileNames=fileNames,
557
+ nodeNames=nodeNames,
558
+ thumbnailFileName=thumbnailFileName,
559
+ loadFileTypes=loadFileTypes,
560
+ loadFiles=loadFiles,
561
+ loadFileProperties=loadFileProperties,
562
+ checksums=checksums,
563
+ customDownloader=customDownloader,
564
+ )
565
+
566
+ if SampleDataLogic.isSampleDataSourceRegistered(category, dataSource):
567
+ return
568
+
569
+ slicer.modules.sampleDataSources[category].append(dataSource)
570
+
571
+ @staticmethod
572
+ def sampleDataSourcesByCategory(category=None):
573
+ """Return the registered SampleDataSources for with the given category.
574
+
575
+ If no category is specified, returns all registered SampleDataSources.
576
+ """
577
+ try:
578
+ slicer.modules.sampleDataSources
579
+ except AttributeError:
580
+ slicer.modules.sampleDataSources = {}
581
+
582
+ if category is None:
583
+ return slicer.modules.sampleDataSources
584
+ else:
585
+ return slicer.modules.sampleDataSources.get(category, [])
586
+
587
+ @staticmethod
588
+ def isSampleDataSourceRegistered(category, sampleDataSource):
589
+ """Returns True if the sampleDataSource is registered with the category."""
590
+ try:
591
+ slicer.modules.sampleDataSources
592
+ except AttributeError:
593
+ slicer.modules.sampleDataSources = {}
594
+
595
+ if not isinstance(sampleDataSource, SampleDataSource):
596
+ raise TypeError(f"unsupported sampleDataSource type '{type(sampleDataSource)}': '{str(SampleDataSource)}' is expected")
597
+
598
+ return sampleDataSource in slicer.modules.sampleDataSources.get(category, [])
599
+
600
+ @staticmethod
601
+ def registerUrlRewriteRule(name, rewriteFunction):
602
+ """Adds a URL rewrite rule for custom data sets in SampleData.
603
+ :param name: Name of the rewrite rule.
604
+ :param rewriteFunction: Function that implements the rewrite logic.
605
+ """
606
+ if not callable(rewriteFunction):
607
+ raise TypeError(f"Expected 'rewriteFunction' to be callable, got {type(rewriteFunction)}")
608
+
609
+ try:
610
+ slicer.modules.sampleDataUrlRewriteRules
611
+ except AttributeError:
612
+ slicer.modules.sampleDataUrlRewriteRules = {}
613
+
614
+ slicer.modules.sampleDataUrlRewriteRules[name] = rewriteFunction
615
+
616
+ def __init__(self, logMessage=None):
617
+ if logMessage:
618
+ self.logMessage = logMessage
619
+ self.builtInCategoryName = _("General")
620
+ self.developmentCategoryName = _("Development")
621
+ self.registerBuiltInSampleDataSources()
622
+ self.registerDevelopmentSampleDataSources()
623
+ if slicer.app.testingEnabled():
624
+ self.registerTestingDataSources()
625
+ self.registerBuiltInUrlRewriteRules()
626
+ self.downloadPercent = 0
627
+
628
+ def registerBuiltInSampleDataSources(self):
629
+ """Fills in the pre-define sample data sources"""
630
+
631
+ # Arguments:
632
+ # sampleName=None, sampleDescription=None,
633
+ # uris=None,
634
+ # fileNames=None, nodeNames=None,
635
+ # checksums=None,
636
+ # loadFiles=None, customDownloader=None, thumbnailFileName=None, loadFileTypes=None, loadFileProperties=None
637
+ sourceArguments = (
638
+ ("MRHead", None, TESTING_DATA_URL + "SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93",
639
+ "MR-head.nrrd", "MRHead", "SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93"),
640
+ ("CTChest", None, TESTING_DATA_URL + "SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e",
641
+ "CT-chest.nrrd", "CTChest", "SHA256:4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e"),
642
+ ("CTACardio", None, TESTING_DATA_URL + "SHA256/3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2",
643
+ "CTA-cardio.nrrd", "CTACardio", "SHA256:3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2"),
644
+ ("DTIBrain", None, TESTING_DATA_URL + "SHA256/5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a",
645
+ "DTI-Brain.nrrd", "DTIBrain", "SHA256:5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a"),
646
+ ("MRBrainTumor1", None, TESTING_DATA_URL + "SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95",
647
+ "RegLib_C01_1.nrrd", "MRBrainTumor1", "SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95"),
648
+ ("MRBrainTumor2", None, TESTING_DATA_URL + "SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97",
649
+ "RegLib_C01_2.nrrd", "MRBrainTumor2", "SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97"),
650
+ ("BaselineVolume", None, TESTING_DATA_URL + "SHA256/dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2",
651
+ "BaselineVolume.nrrd", "BaselineVolume", "SHA256:dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2"),
652
+ ("DTIVolume", None,
653
+ (TESTING_DATA_URL + "SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d",
654
+ TESTING_DATA_URL + "SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe" ),
655
+ ("DTIVolume.raw.gz", "DTIVolume.nhdr"), (None, "DTIVolume"),
656
+ ("SHA256:d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d",
657
+ "SHA256:67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe")),
658
+ ("DWIVolume", None,
659
+ (TESTING_DATA_URL + "SHA256/cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692",
660
+ TESTING_DATA_URL + "SHA256/7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed"),
661
+ ("dwi.raw.gz", "dwi.nhdr"), (None, "dwi"),
662
+ ("SHA256:cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692",
663
+ "SHA256:7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed")),
664
+ ("CTAAbdomenPanoramix", "CTA abdomen\n(Panoramix)", TESTING_DATA_URL + "SHA256/146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433",
665
+ "Panoramix-cropped.nrrd", "Panoramix-cropped", "SHA256:146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433"),
666
+ ("CBCTDentalSurgery", None,
667
+ (TESTING_DATA_URL + "SHA256/7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968",
668
+ TESTING_DATA_URL + "SHA256/4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd"),
669
+ ("PreDentalSurgery.gipl.gz", "PostDentalSurgery.gipl.gz"), ("PreDentalSurgery", "PostDentalSurgery"),
670
+ ("SHA256:7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968",
671
+ "SHA256:4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd")),
672
+ ("MRUSProstate", "MR-US Prostate",
673
+ (TESTING_DATA_URL + "SHA256/4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa",
674
+ TESTING_DATA_URL + "SHA256/34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2"),
675
+ ("Case10-MR.nrrd", "case10_US_resampled.nrrd"), ("MRProstate", "USProstate"),
676
+ ("SHA256:4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa",
677
+ "SHA256:34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2")),
678
+ ("CTMRBrain", "CT-MR Brain",
679
+ (TESTING_DATA_URL + "SHA256/6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1",
680
+ TESTING_DATA_URL + "SHA256/2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593",
681
+ TESTING_DATA_URL + "SHA256/fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7"),
682
+ ("CT-brain.nrrd", "MR-brain-T1.nrrd", "MR-brain-T2.nrrd"),
683
+ ("CTBrain", "MRBrainT1", "MRBrainT2"),
684
+ ("SHA256:6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1",
685
+ "SHA256:2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593",
686
+ "SHA256:fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7")),
687
+ ("CBCTMRHead", "CBCT-MR Head",
688
+ (TESTING_DATA_URL + "SHA256/4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db",
689
+ TESTING_DATA_URL + "SHA256/b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842"),
690
+ ("DZ-CBCT.nrrd", "DZ-MR.nrrd"),
691
+ ("DZ-CBCT", "DZ-MR"),
692
+ ("SHA256:4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db",
693
+ "SHA256:b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842")),
694
+ ("CTLiver", None, TESTING_DATA_URL + "SHA256/e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e",
695
+ "CTLiver.nrrd", "CTLiver", "SHA256:e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e"),
696
+ ("CTPCardioSeq", "CTP Cardio Sequence",
697
+ "https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2",
698
+ "CTP-cardio.seq.nrrd", "CTPCardioSeq",
699
+ "SHA256:7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2",
700
+ None, None, None, "SequenceFile"),
701
+ ("CTCardioSeq", "CT Cardio Sequence",
702
+ "https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93",
703
+ "CT-cardio.seq.nrrd", "CTCardioSeq",
704
+ "SHA256:d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93",
705
+ None, None, None, "SequenceFile"),
706
+ )
707
+
708
+ if self.builtInCategoryName not in slicer.modules.sampleDataSources:
709
+ slicer.modules.sampleDataSources[self.builtInCategoryName] = []
710
+ for sourceArgument in sourceArguments:
711
+ dataSource = SampleDataSource(*sourceArgument)
712
+ if SampleDataLogic.isSampleDataSourceRegistered(self.builtInCategoryName, dataSource):
713
+ continue
714
+ slicer.modules.sampleDataSources[self.builtInCategoryName].append(dataSource)
715
+
716
+ def registerDevelopmentSampleDataSources(self):
717
+ """Fills in the sample data sources displayed only if developer mode is enabled."""
718
+ iconPath = os.path.join(os.path.dirname(__file__).replace("\\", "/"), "Resources", "Icons")
719
+ self.registerCustomSampleDataSource(
720
+ category=self.developmentCategoryName, sampleName="TinyPatient",
721
+ uris=[TESTING_DATA_URL + "SHA256/c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b",
722
+ TESTING_DATA_URL + "SHA256/3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470"],
723
+ fileNames=["TinyPatient_CT.nrrd", "TinyPatient_Structures.seg.nrrd"],
724
+ nodeNames=["TinyPatient_CT", "TinyPatient_Segments"],
725
+ thumbnailFileName=os.path.join(iconPath, "TinyPatient.png"),
726
+ loadFileTypes=["VolumeFile", "SegmentationFile"],
727
+ checksums=["SHA256:c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b", "SHA256:3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470"],
728
+ )
729
+
730
+ def registerTestingDataSources(self):
731
+ """Register sample data sources used by SampleData self-test to test module functionalities."""
732
+ self.registerCustomSampleDataSource(**SampleDataTest.CustomDownloaderDataSource)
733
+
734
+ def registerBuiltInUrlRewriteRules(self):
735
+ """Register built-in URL rewrite rules."""
736
+
737
+ def githubFileWebpageUrlRewriteRule(url):
738
+ """Rewrite GitHub file webpage URL to download URL.
739
+ Example:
740
+ https://github.com/SlicerMorph/terms-and-colors/blob/main/JaimiGrayTetrapodSkulls.csv
741
+ is automatically converted to
742
+ https://raw.githubusercontent.com/SlicerMorph/terms-and-colors/refs/heads/main/JaimiGrayTetrapodSkulls.csv
743
+ """
744
+ import urllib.parse as urlparse
745
+ parsedUrl = urlparse.urlparse(url)
746
+ if parsedUrl.scheme != "https" or parsedUrl.netloc != "github.com":
747
+ return url
748
+ pathParts = parsedUrl.path.split("/")
749
+ if len(pathParts) <= 4:
750
+ return url
751
+ organization = pathParts[1]
752
+ repository = pathParts[2]
753
+ rootFolder = pathParts[3]
754
+ filePath = "/".join(pathParts[4:])
755
+ if rootFolder != "blob":
756
+ return url
757
+ parsedUrl = parsedUrl._replace(netloc="raw.githubusercontent.com")
758
+ parsedUrl = parsedUrl._replace(path="/".join([organization, repository, "refs", "heads"]) + "/" + filePath)
759
+ return parsedUrl.geturl()
760
+
761
+ def dropboxDownloadUrlRewriteRule(url):
762
+ """Rewrite Dropbox shared link to direct download link.
763
+ Example:
764
+ https://www.dropbox.com/s/abcd1234efgh5678/myfile.nrrd
765
+ is automatically converted to
766
+ https://dl.dropbox.com/s/abcd1234efgh5678/myfile.nrrd
767
+ """
768
+ import urllib.parse as urlparse
769
+ parsedUrl = urlparse.urlparse(url)
770
+ if parsedUrl.scheme != "https" or parsedUrl.netloc != "www.dropbox.com":
771
+ return url
772
+ parsedUrl = parsedUrl._replace(netloc="dl.dropbox.com")
773
+ return parsedUrl.geturl()
774
+
775
+ self.registerUrlRewriteRule("GitHubFileWebpage", githubFileWebpageUrlRewriteRule)
776
+ self.registerUrlRewriteRule("DropboxDownloadLink", dropboxDownloadUrlRewriteRule)
777
+
778
+ def downloadFileIntoCache(self, uri, name, checksum=None, forceDownload=False):
779
+ """Given a uri and a filename, download the data into
780
+ a file of the given name in the scene's cache.
781
+ If checksum is provided then it is used to decide if the file has to be downloaded again.
782
+ If checksum is not provided then the file is not downloaded again unless forceDownload is True.
783
+ """
784
+ destFolderPath = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()
785
+
786
+ if not os.access(destFolderPath, os.W_OK):
787
+ try:
788
+ os.makedirs(destFolderPath, exist_ok=True)
789
+ except:
790
+ self.logMessage(_("Failed to create cache folder {path}").format(path=destFolderPath), logging.ERROR)
791
+ if not os.access(destFolderPath, os.W_OK):
792
+ self.logMessage(_("Cache folder {path} is not writable").format(path=destFolderPath), logging.ERROR)
793
+ return self.downloadFile(uri, destFolderPath, name, checksum, forceDownload=forceDownload)
794
+
795
+ def downloadSourceIntoCache(self, source):
796
+ """Download all files for the given source and return a
797
+ list of file paths for the results
798
+ """
799
+ filePaths = []
800
+ for uri, fileName, checksum in zip(source.uris, source.fileNames, source.checksums, strict=False):
801
+ filePaths.append(self.downloadFileIntoCache(uri, fileName, checksum))
802
+ return filePaths
803
+
804
+ def downloadFromSource(self, source, maximumAttemptsCount=3, forceDownload=False):
805
+ """Given an instance of SampleDataSource, downloads the associated data and
806
+ load them into Slicer if it applies.
807
+
808
+ The function always returns a list.
809
+
810
+ Based on the fileType(s), nodeName(s) and loadFile(s) associated with
811
+ the source, different values may be appended to the returned list:
812
+
813
+ - if nodeName is specified, appends loaded nodes but if ``loadFile`` is False appends downloaded filepath
814
+ - if fileType is ``SceneFile``, appends downloaded filepath
815
+ - if fileType is ``ZipFile``, appends directory of extracted archive but if ``loadFile`` is False appends downloaded filepath
816
+
817
+ If no ``nodeNames`` and no ``fileTypes`` are specified or if ``loadFiles`` are all False,
818
+ returns the list of all downloaded filepaths.
819
+
820
+ If ``forceDownload`` is True then existing files are downloaded again (if no hash is provided).
821
+ """
822
+
823
+ # Input may contain urls without associated node names, which correspond to additional data files
824
+ # (e.g., .raw file for a .nhdr header file). Therefore we collect nodes and file paths separately
825
+ # and we only return file paths if no node names have been provided.
826
+ resultNodes = []
827
+ resultFilePaths = []
828
+
829
+ # If some node names are defined and some are left empty then we assume that it is intentional
830
+ # (e.g., you only want to load the image.nhdr file and not the image.raw file).
831
+ # In this case default node names are not generated.
832
+ generateDefaultNodeNames = all(n is None for n in source.nodeNames)
833
+
834
+ for uri, fileName, nodeName, checksum, loadFile, loadFileType in zip(
835
+ source.uris, source.fileNames, source.nodeNames, source.checksums, source.loadFiles, source.loadFileTypes, strict=False):
836
+
837
+ if nodeName is None or fileName is None:
838
+ # Determine file basename and extension from URL or path
839
+ import urllib
840
+ import uuid
841
+ if fileName is not None:
842
+ basename, ext = os.path.splitext(os.path.basename(fileName))
843
+ else:
844
+ p = urllib.parse.urlparse(uri)
845
+ basename, ext = os.path.splitext(os.path.basename(p.path))
846
+
847
+ # Generate default node name (we only need this if we want to load the file into the scene and no node name is provided)
848
+ if (nodeName is None) and (loadFile is not False) and generateDefaultNodeNames:
849
+ nodeName = basename
850
+
851
+ # Generate default file name (we always need this, even for just downloading)
852
+ if fileName is None:
853
+ # Generate a unique filename to avoid overwriting existing file with the same name
854
+ fileName = f"{nodeName if nodeName else basename}-{uuid.uuid4().hex}{ext}"
855
+
856
+ current_source = SampleDataSource(
857
+ uris=uri,
858
+ fileNames=fileName,
859
+ nodeNames=nodeName,
860
+ checksums=checksum,
861
+ loadFiles=loadFile,
862
+ loadFileTypes=loadFileType,
863
+ loadFileProperties=source.loadFileProperties)
864
+
865
+ for attemptsCount in range(maximumAttemptsCount):
866
+
867
+ # Download
868
+ try:
869
+ filePath = self.downloadFileIntoCache(uri, fileName, checksum, forceDownload=forceDownload)
870
+ except ValueError:
871
+ self.logMessage(_("Download failed (attempt {current} of {total})...").format(
872
+ current=attemptsCount + 1, total=maximumAttemptsCount), logging.ERROR)
873
+ continue
874
+
875
+ # Special behavior (how `loadFileType` is used and what is returned in `resultNodes` ) is implemented
876
+ # for scene and zip file loading, for preserving backward compatible behavior.
877
+ # - ZipFile: If `loadFile` is explicitly set to `False` then the zip file is just downloaded. Otherwise, the zip file is extracted.
878
+ # By default `loadFile` is set to `None`, so by default the zip file is extracted.
879
+ # Nodes are not loaded from the .zip file in either case. To load a scene from a .zip file, `loadFileType` has to be set explicitly to `SceneFile`.
880
+ # Path is returned in `resultNodes`.
881
+ # - SceneFile: If `loadFile` is not explicitly set or it is set to `False` then the scene is just downloaded (not loaded).
882
+ # Path is returned in `resultNodes`.
883
+ if (loadFileType is None) and (nodeName is None):
884
+ ext = os.path.splitext(fileName.lower())[1]
885
+ if ext in [".mrml", ".mrb"]:
886
+ loadFileType = "SceneFile"
887
+ elif ext in [".zip"]:
888
+ loadFileType = "ZipFile"
889
+
890
+ if loadFileType == "ZipFile":
891
+ if loadFile is False:
892
+ resultNodes.append(filePath)
893
+ break
894
+ outputDir = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() + "/" + os.path.splitext(os.path.basename(filePath))[0]
895
+ qt.QDir().mkpath(outputDir)
896
+ if slicer.util.extractArchive(filePath, outputDir):
897
+ # Success
898
+ resultNodes.append(outputDir)
899
+ break
900
+ elif loadFileType == "SceneFile":
901
+ if not loadFile:
902
+ resultNodes.append(filePath)
903
+ break
904
+ if self.loadScene(filePath, source.loadFileProperties.copy()):
905
+ # Success
906
+ resultNodes.append(filePath)
907
+ break
908
+ elif nodeName:
909
+ if loadFile is False:
910
+ resultNodes.append(filePath)
911
+ break
912
+ loadedNode = self.loadNode(filePath, nodeName, loadFileType, source.loadFileProperties.copy())
913
+ if loadedNode:
914
+ # Success
915
+ resultNodes.append(loadedNode)
916
+ break
917
+ else:
918
+ # no need to load node
919
+ resultFilePaths.append(filePath)
920
+ break
921
+
922
+ # Failed. Clean up downloaded file (it might have been a partial download)
923
+ file = qt.QFile(filePath)
924
+ if file.exists() and not file.remove():
925
+ self.logMessage(_("Load failed (attempt {current} of {total}). Unable to delete and try again loading {path}").format(
926
+ current=attemptsCount + 1, total=maximumAttemptsCount, path=filePath), logging.ERROR)
927
+ resultNodes.append(loadedNode)
928
+ break
929
+ self.logMessage(_("Load failed (attempt {current} of {total})...").format(
930
+ current=attemptsCount + 1, total=maximumAttemptsCount), logging.ERROR)
931
+
932
+ if resultNodes:
933
+ return resultNodes
934
+ else:
935
+ return resultFilePaths
936
+
937
+ def sourceForSampleName(self, sampleName):
938
+ """For a given sample name this will search the available sources.
939
+ Returns SampleDataSource instance.
940
+ """
941
+ for category in slicer.modules.sampleDataSources.keys():
942
+ for source in slicer.modules.sampleDataSources[category]:
943
+ if sampleName == source.sampleName:
944
+ return source
945
+ return None
946
+
947
+ def categoryForSource(self, a_source):
948
+ """For a given SampleDataSource return the associated category name."""
949
+ for category in slicer.modules.sampleDataSources.keys():
950
+ for source in slicer.modules.sampleDataSources[category]:
951
+ if a_source == source:
952
+ return category
953
+ return None
954
+
955
+ def downloadFromURL(self, uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None,
956
+ customDownloader=None, loadFileTypes=None, loadFileProperties={}, forceDownload=False):
957
+ """Download and optionally load data into the application.
958
+
959
+ :param uris: Download URL(s).
960
+ :param fileNames: File name(s) that will be downloaded (and loaded).
961
+ :param nodeNames: Node name(s) in the scene.
962
+ :param checksums: Checksum(s) formatted as ``<algo>:<digest>`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
963
+ :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
964
+ :param customDownloader: Custom function for downloading.
965
+ :param loadFileTypes: file format name(s) (if not specified then the default file reader will be used).
966
+ :param loadFileProperties: custom properties passed to the IO plugin.
967
+
968
+ If the given ``fileNames`` are not found in the application cache directory, they
969
+ are downloaded using the associated URIs.
970
+ See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()``
971
+
972
+ If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are
973
+ guessed based on the corresponding filename extensions and file content.
974
+
975
+ Special behavior for scene and archive files:
976
+ - If a ``fileName`` has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded
977
+ by default. To ensure the file is loaded, ``loadFiles`` must be set.
978
+ - If a ``fileName`` has the ``.zip`` extension and ``loadFiles`` is set to ``True`` (or left at default value)
979
+ then the archive is extracted and the folder that contains the files is returned.
980
+ If ``loadFiles`` is set to ``False`` then the archive is not extracted and the filepath of the downloaded
981
+ zip file is returned.
982
+
983
+ The ``loadFileProperties`` are common for all files. If different properties
984
+ need to be associated with files of different types, downloadFromURL must
985
+ be called for each.
986
+
987
+ If ``forceDownload`` is True then existing files are downloaded again (if no hash is provided).
988
+ """
989
+ source = SampleDataSource(
990
+ uris=uris, fileNames=fileNames, nodeNames=nodeNames, loadFiles=loadFiles,
991
+ loadFileTypes=loadFileTypes, loadFileProperties=loadFileProperties, checksums=checksums,
992
+ customDownloader=customDownloader)
993
+ return self.downloadFromSource(source, forceDownload=forceDownload)
994
+
995
+ def downloadSample(self, sampleName):
996
+ """For a given sample name this will search the available sources
997
+ and load it if it is available. Returns the first loaded node.
998
+ """
999
+ return self.downloadSamples(sampleName)[0]
1000
+
1001
+ def downloadSamples(self, sampleName):
1002
+ """For a given sample name this will search the available sources
1003
+ and load it if it is available. Returns the loaded nodes.
1004
+ """
1005
+ source = self.sourceForSampleName(sampleName)
1006
+ nodes = []
1007
+ if source:
1008
+ nodes = self.downloadFromSource(source)
1009
+ return nodes
1010
+
1011
+ def logMessage(self, message, logLevel=logging.DEBUG):
1012
+ logging.log(logLevel, message)
1013
+
1014
+ """Utility methods for backwards compatibility"""
1015
+
1016
+ def downloadMRHead(self):
1017
+ return self.downloadSample("MRHead")
1018
+
1019
+ def downloadCTChest(self):
1020
+ return self.downloadSample("CTChest")
1021
+
1022
+ def downloadCTACardio(self):
1023
+ return self.downloadSample("CTACardio")
1024
+
1025
+ def downloadDTIBrain(self):
1026
+ return self.downloadSample("DTIBrain")
1027
+
1028
+ def downloadMRBrainTumor1(self):
1029
+ return self.downloadSample("MRBrainTumor1")
1030
+
1031
+ def downloadMRBrainTumor2(self):
1032
+ return self.downloadSample("MRBrainTumor2")
1033
+
1034
+ def downloadWhiteMatterExplorationBaselineVolume(self):
1035
+ return self.downloadSample("BaselineVolume")
1036
+
1037
+ def downloadWhiteMatterExplorationDTIVolume(self):
1038
+ return self.downloadSample("DTIVolume")
1039
+
1040
+ def downloadDiffusionMRIDWIVolume(self):
1041
+ return self.downloadSample("DWIVolume")
1042
+
1043
+ def downloadAbdominalCTVolume(self):
1044
+ return self.downloadSample("CTAAbdomenPanoramix")
1045
+
1046
+ def downloadDentalSurgery(self):
1047
+ # returns list since that's what earlier method did
1048
+ return self.downloadSamples("CBCTDentalSurgery")
1049
+
1050
+ def downloadMRUSPostate(self):
1051
+ # returns list since that's what earlier method did
1052
+ return self.downloadSamples("MRUSProstate")
1053
+
1054
+ def humanFormatSize(self, size):
1055
+ """from https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size"""
1056
+ for x in ["bytes", "KB", "MB", "GB"]:
1057
+ if size < 1024.0 and size > -1024.0:
1058
+ return f"{size:3.1f} {x}"
1059
+ size /= 1024.0
1060
+ return "{:3.1f} {}".format(size, "TB")
1061
+
1062
+ def reportHook(self, blocksSoFar, blockSize, totalSize):
1063
+ # we clamp to 100% because the blockSize might be larger than the file itself
1064
+ percent = min(int((100.0 * blocksSoFar * blockSize) / totalSize), 100)
1065
+ if percent == 100 or (percent - self.downloadPercent >= 10):
1066
+ # we clamp to totalSize when blockSize is larger than totalSize
1067
+ humanSizeSoFar = self.humanFormatSize(min(blocksSoFar * blockSize, totalSize))
1068
+ humanSizeTotal = self.humanFormatSize(totalSize)
1069
+ self.logMessage("<i>" + _("Downloaded {sizeCompleted} ({percentCompleted}% of {sizeTotal})...").format(
1070
+ sizeCompleted=humanSizeSoFar, percentCompleted=percent, sizeTotal=humanSizeTotal) + "</i>")
1071
+ self.downloadPercent = percent
1072
+
1073
+ @staticmethod
1074
+ def rewriteUrl(url):
1075
+ """Apply all registered URL rewrite rules to the given URL."""
1076
+
1077
+ try:
1078
+ slicer.modules.sampleDataUrlRewriteRules
1079
+ except AttributeError:
1080
+ # No rules are registered
1081
+ return url
1082
+
1083
+ for rule in slicer.modules.sampleDataUrlRewriteRules.values():
1084
+ url = rule(url)
1085
+ return url
1086
+
1087
+ def downloadFile(self, uri, destFolderPath, name, checksum=None, forceDownload=False):
1088
+ """
1089
+ :param uri: Download URL. It is rewritten using registered URL rewrite rules before download.
1090
+ :param destFolderPath: Folder to download the file into.
1091
+ :param name: File name that will be downloaded.
1092
+ :param checksum: Checksum formatted as ``<algo>:<digest>`` to verify the downloaded file. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
1093
+ :param forceDownload: If True then the file is always downloaded even if it already exists in the destination folder.
1094
+ """
1095
+
1096
+ uri = self.rewriteUrl(uri)
1097
+
1098
+ self.downloadPercent = 0
1099
+ filePath = destFolderPath + "/" + name
1100
+ (algo, digest) = extractAlgoAndDigest(checksum)
1101
+ if forceDownload or not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
1102
+ import urllib.request, urllib.parse, urllib.error
1103
+
1104
+ self.logMessage(_("Requesting download {name} from {uri} ...").format(name=name, uri=uri))
1105
+ try:
1106
+ urllib.request.urlretrieve(uri, filePath, self.reportHook)
1107
+ self.logMessage(_("Download finished"))
1108
+ except OSError as e:
1109
+ self.logMessage("\t" + _("Download failed: {errorMessage}").format(errorMessage=e), logging.ERROR)
1110
+ raise ValueError(_("Failed to download {uri} to {filePath}").format(uri=uri, filePath=filePath))
1111
+
1112
+ if algo is not None:
1113
+ self.logMessage(_("Verifying checksum"))
1114
+ current_digest = computeChecksum(algo, filePath)
1115
+ if current_digest != digest:
1116
+ self.logMessage(
1117
+ _("Checksum verification failed. Computed checksum {currentChecksum} different from expected checksum {expectedChecksum}").format(
1118
+ currentChecksum=current_digest, expectedChecksum=digest))
1119
+ qt.QFile(filePath).remove()
1120
+ else:
1121
+ self.downloadPercent = 100
1122
+ self.logMessage(_("Checksum OK"))
1123
+ else:
1124
+ if algo is not None:
1125
+ self.logMessage(_("Verifying checksum"))
1126
+ current_digest = computeChecksum(algo, filePath)
1127
+ if current_digest != digest:
1128
+ self.logMessage(_("File already exists in cache but checksum is different - re-downloading it."))
1129
+ qt.QFile(filePath).remove()
1130
+ return self.downloadFile(uri, destFolderPath, name, checksum)
1131
+ else:
1132
+ self.downloadPercent = 100
1133
+ self.logMessage(_("File already exists and checksum is OK - reusing it."))
1134
+ else:
1135
+ self.downloadPercent = 100
1136
+ self.logMessage(_("File already exists in cache - reusing it."))
1137
+ return filePath
1138
+
1139
+ def loadScene(self, uri, fileProperties={}):
1140
+ """Returns True is scene loading was successful, False if failed."""
1141
+ loadedNode = self.loadNode(uri, None, "SceneFile", fileProperties)
1142
+ success = loadedNode is not None
1143
+ return success
1144
+
1145
+ def loadNode(self, uri, name, fileType=None, fileProperties={}):
1146
+ """Returns the first loaded node (or the scene if the reader did not provide a specific node) on success.
1147
+ Returns None if failed.
1148
+ """
1149
+ self.logMessage("<b>" + _("Requesting load {name} from {uri} ...").format(name=name, uri=uri) + "</b>")
1150
+
1151
+ fileProperties["fileName"] = uri
1152
+ if name:
1153
+ fileProperties["name"] = name
1154
+ if not fileType:
1155
+ fileType = slicer.app.coreIOManager().fileType(fileProperties["fileName"])
1156
+ firstLoadedNode = None
1157
+ loadedNodes = vtk.vtkCollection()
1158
+ success = slicer.app.coreIOManager().loadNodes(fileType, fileProperties, loadedNodes)
1159
+ if not success:
1160
+ if loadedNodes.GetNumberOfItems() < 1:
1161
+ self.logMessage("\t" + _("Load failed!"), logging.ERROR)
1162
+ return None
1163
+ else:
1164
+ # Loading did not fail, because some nodes were loaded, proceed with a warning
1165
+ self.logMessage(_("Error was reported while loading {count} nodes from {path}").format(
1166
+ count=loadedNodes.GetNumberOfItems(), path=uri), logging.WARNING)
1167
+
1168
+ self.logMessage("<b>" + _("Load finished") + "</b><p></p>")
1169
+
1170
+ # since nodes were read from a temp directory remove the storage nodes
1171
+ for i in range(loadedNodes.GetNumberOfItems()):
1172
+ loadedNode = loadedNodes.GetItemAsObject(i)
1173
+ if not loadedNode.IsA("vtkMRMLStorableNode"):
1174
+ continue
1175
+ storageNode = loadedNode.GetStorageNode()
1176
+ if not storageNode:
1177
+ continue
1178
+ slicer.mrmlScene.RemoveNode(storageNode)
1179
+ loadedNode.SetAndObserveStorageNodeID(None)
1180
+
1181
+ firstLoadedNode = loadedNodes.GetItemAsObject(0)
1182
+ if firstLoadedNode:
1183
+ return firstLoadedNode
1184
+ else:
1185
+ # If a reader does not report loading of any specific node (it may happen for example with a scene reader)
1186
+ # then return the scene to distinguish from a load error.
1187
+ return slicer.mrmlScene
1188
+
1189
+
1190
+ class SampleDataTest(ScriptedLoadableModuleTest):
1191
+ """
1192
+ This is the test case for your scripted module.
1193
+ Uses ScriptedLoadableModuleTest base class, available at:
1194
+ https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py
1195
+ """
1196
+
1197
+ customDownloads = []
1198
+
1199
+ def setUp(self):
1200
+ slicer.mrmlScene.Clear(0)
1201
+ SampleDataTest.customDownloads = []
1202
+
1203
+ def runTest(self):
1204
+ for test in [
1205
+ self.test_downloadFromSource_downloadFiles,
1206
+ self.test_downloadFromSource_downloadZipFile,
1207
+ self.test_downloadFromSource_loadMRBFile,
1208
+ self.test_downloadFromSource_loadMRMLFile,
1209
+ self.test_downloadFromSource_downloadMRBFile,
1210
+ self.test_downloadFromSource_downloadMRMLFile,
1211
+ self.test_downloadFromSource_loadNode,
1212
+ self.test_downloadFromSource_loadNodeFromMultipleFiles,
1213
+ self.test_downloadFromSource_loadNodes,
1214
+ self.test_downloadFromSource_loadNodesWithLoadFileFalse,
1215
+ self.test_sampleDataSourcesByCategory,
1216
+ self.test_categoryVisibility,
1217
+ self.test_setCategoriesFromSampleDataSources,
1218
+ self.test_isSampleDataSourceRegistered,
1219
+ self.test_defaultFileType,
1220
+ self.test_customDownloader,
1221
+ self.test_categoryForSource,
1222
+ ]:
1223
+ self.setUp()
1224
+ test()
1225
+
1226
+ @staticmethod
1227
+ def path2uri(path):
1228
+ """Gets a URI from a local file path.
1229
+ Typically it prefixes the received path by file:// or file:///.
1230
+ """
1231
+ import urllib.parse, urllib.request, urllib.parse, urllib.error
1232
+
1233
+ return urllib.parse.urljoin("file:", urllib.request.pathname2url(path))
1234
+
1235
+ def test_downloadFromSource_downloadFiles(self):
1236
+ """Specifying URIs and fileNames without nodeNames is expected to download the files
1237
+ without loading into Slicer.
1238
+ """
1239
+ logic = SampleDataLogic()
1240
+
1241
+ sceneMTime = slicer.mrmlScene.GetMTime()
1242
+ filePaths = logic.downloadFromSource(SampleDataSource(
1243
+ uris=TESTING_DATA_URL + "SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93",
1244
+ fileNames="MR-head.nrrd", loadFiles=False))
1245
+ self.assertEqual(len(filePaths), 1)
1246
+ self.assertTrue(os.path.exists(filePaths[0]))
1247
+ self.assertTrue(os.path.isfile(filePaths[0]))
1248
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
1249
+
1250
+ sceneMTime = slicer.mrmlScene.GetMTime()
1251
+ filePaths = logic.downloadFromSource(SampleDataSource(
1252
+ uris=[TESTING_DATA_URL + "SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93",
1253
+ TESTING_DATA_URL + "SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e"],
1254
+ fileNames=["MR-head.nrrd", "CT-chest.nrrd"],
1255
+ loadFiles=[False, False]))
1256
+ self.assertEqual(len(filePaths), 2)
1257
+ self.assertTrue(os.path.exists(filePaths[0]))
1258
+ self.assertTrue(os.path.isfile(filePaths[0]))
1259
+ self.assertTrue(os.path.exists(filePaths[1]))
1260
+ self.assertTrue(os.path.isfile(filePaths[1]))
1261
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
1262
+
1263
+ def test_downloadFromSource_downloadZipFile(self):
1264
+ logic = SampleDataLogic()
1265
+ sceneMTime = slicer.mrmlScene.GetMTime()
1266
+ filePaths = logic.downloadFromSource(SampleDataSource(
1267
+ uris=TESTING_DATA_URL + "SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7",
1268
+ fileNames="TinyPatient_Seg.zip",
1269
+ loadFileTypes="ZipFile"))
1270
+ self.assertEqual(len(filePaths), 1)
1271
+ self.assertTrue(os.path.exists(filePaths[0]))
1272
+ self.assertTrue(os.path.isdir(filePaths[0]))
1273
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
1274
+
1275
+ def test_downloadFromSource_loadMRBFile(self):
1276
+ logic = SampleDataLogic()
1277
+ sceneMTime = slicer.mrmlScene.GetMTime()
1278
+ nodes = logic.downloadFromSource(SampleDataSource(
1279
+ uris=TESTING_DATA_URL + "SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8",
1280
+ loadFiles=True, fileNames="slicer4minute.mrb"))
1281
+ self.assertEqual(len(nodes), 1)
1282
+ self.assertIsInstance(nodes[0], slicer.vtkMRMLCameraNode)
1283
+ self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime())
1284
+
1285
+ def test_downloadFromSource_loadMRMLFile(self):
1286
+ logic = SampleDataLogic()
1287
+ tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml")
1288
+ tempFile.open()
1289
+ tempFile.write(textwrap.dedent("""
1290
+ <?xml version="1.0" encoding="UTF-8"?>
1291
+ <MRML version="Slicer4.4.0" userTags="">
1292
+ </MRML>
1293
+ """).strip())
1294
+ tempFile.close()
1295
+ sceneMTime = slicer.mrmlScene.GetMTime()
1296
+ nodes = logic.downloadFromSource(SampleDataSource(
1297
+ uris=self.path2uri(tempFile.fileName()), loadFiles=True, fileNames="scene.mrml"))
1298
+ self.assertEqual(len(nodes), 1)
1299
+ self.assertIsInstance(nodes[0], slicer.vtkMRMLScene)
1300
+ self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime())
1301
+
1302
+ def test_downloadFromSource_downloadMRBFile(self):
1303
+ logic = SampleDataLogic()
1304
+ sceneMTime = slicer.mrmlScene.GetMTime()
1305
+ filePaths = logic.downloadFromSource(SampleDataSource(
1306
+ uris=TESTING_DATA_URL + "SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8",
1307
+ fileNames="slicer4minute.mrb",
1308
+ loadFileTypes="SceneFile"))
1309
+ self.assertEqual(len(filePaths), 1)
1310
+ self.assertTrue(os.path.exists(filePaths[0]))
1311
+ self.assertTrue(os.path.isfile(filePaths[0]))
1312
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
1313
+
1314
+ def test_downloadFromSource_downloadMRMLFile(self):
1315
+ logic = SampleDataLogic()
1316
+ tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml")
1317
+ tempFile.open()
1318
+ tempFile.write(textwrap.dedent("""
1319
+ <?xml version="1.0" encoding="UTF-8"?>
1320
+ <MRML version="Slicer4.4.0" userTags="">
1321
+ </MRML>
1322
+ """).strip())
1323
+ tempFile.close()
1324
+ sceneMTime = slicer.mrmlScene.GetMTime()
1325
+ filePaths = logic.downloadFromSource(SampleDataSource(
1326
+ uris=self.path2uri(tempFile.fileName()), fileNames="scene.mrml", loadFileTypes="SceneFile"))
1327
+ self.assertEqual(len(filePaths), 1)
1328
+ self.assertTrue(os.path.exists(filePaths[0]))
1329
+ self.assertTrue(os.path.isfile(filePaths[0]))
1330
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
1331
+
1332
+ def test_downloadFromSource_loadNode(self):
1333
+ logic = SampleDataLogic()
1334
+ nodes = logic.downloadFromSource(SampleDataSource(
1335
+ uris=TESTING_DATA_URL + "MD5/39b01631b7b38232a220007230624c8e",
1336
+ fileNames="MR-head.nrrd", nodeNames="MRHead"))
1337
+ self.assertEqual(len(nodes), 1)
1338
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead"))
1339
+
1340
+ def test_downloadFromSource_loadNodeWithoutNodeName(self):
1341
+ logic = SampleDataLogic()
1342
+ nodes = logic.downloadFromSource(SampleDataSource(
1343
+ uris=TESTING_DATA_URL + "MD5/39b01631b7b38232a220007230624c8e",
1344
+ fileNames="MR-head.nrrd"))
1345
+ self.assertEqual(len(nodes), 1)
1346
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MR-head"))
1347
+
1348
+ def test_downloadFromSource_loadNodeFromMultipleFiles(self):
1349
+ logic = SampleDataLogic()
1350
+ nodes = logic.downloadFromSource(SampleDataSource(
1351
+ uris=[TESTING_DATA_URL + "SHA256/bbfd8dd1914e9d7af09df3f6e254374064f6993a37552cc587581623a520a11f",
1352
+ TESTING_DATA_URL + "SHA256/33825585b01a506e532934581a7bddd9de9e7b898e24adfed5454ffc6dfe48ea"],
1353
+ fileNames=["MRHeadResampled.raw.gz", "MRHeadResampled.nhdr"],
1354
+ nodeNames=[None, "MRHeadResampled"],
1355
+ loadFiles=[False, True]))
1356
+ self.assertEqual(len(nodes), 1)
1357
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHeadResampled"))
1358
+
1359
+ def test_downloadFromSource_loadNodesWithLoadFileFalse(self):
1360
+ logic = SampleDataLogic()
1361
+ nodes = logic.downloadFromSource(SampleDataSource(
1362
+ uris=[TESTING_DATA_URL + "SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93",
1363
+ TESTING_DATA_URL + "SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e"],
1364
+ fileNames=["MR-head.nrrd", "CT-chest.nrrd"],
1365
+ nodeNames=["MRHead", "CTChest"],
1366
+ loadFiles=[False, True]))
1367
+ self.assertEqual(len(nodes), 2)
1368
+ self.assertTrue(os.path.exists(nodes[0]))
1369
+ self.assertTrue(os.path.isfile(nodes[0]))
1370
+ self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest"))
1371
+
1372
+ def test_downloadFromSource_loadNodes(self):
1373
+ logic = SampleDataLogic()
1374
+ nodes = logic.downloadFromSource(SampleDataSource(
1375
+ uris=[TESTING_DATA_URL + "SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93",
1376
+ TESTING_DATA_URL + "SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e"],
1377
+ fileNames=["MR-head.nrrd", "CT-chest.nrrd"],
1378
+ nodeNames=["MRHead", "CTChest"]))
1379
+ self.assertEqual(len(nodes), 2)
1380
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead"))
1381
+ self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest"))
1382
+
1383
+ def test_downloadFromSource_loadNodesWithoutNodeNames(self):
1384
+ logic = SampleDataLogic()
1385
+ nodes = logic.downloadFromSource(SampleDataSource(
1386
+ uris=[TESTING_DATA_URL + "SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93",
1387
+ TESTING_DATA_URL + "SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e"],
1388
+ fileNames=["MR-head.nrrd", "CT-chest.nrrd"]))
1389
+ self.assertEqual(len(nodes), 2)
1390
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MR-head"))
1391
+ self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CT-chest"))
1392
+
1393
+ def test_sampleDataSourcesByCategory(self):
1394
+ self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory()) > 0)
1395
+ self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory("General")) > 0)
1396
+ self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory("Not_A_Registered_Category")) == 0)
1397
+
1398
+ def test_categoryVisibility(self):
1399
+ slicer.util.selectModule("SampleData")
1400
+ widget = slicer.modules.SampleDataWidget
1401
+ widget.setCategoryVisible("General", False)
1402
+ self.assertFalse(widget.isCategoryVisible("General"))
1403
+ widget.setCategoryVisible("General", True)
1404
+ self.assertTrue(widget.isCategoryVisible("General"))
1405
+
1406
+ def test_setCategoriesFromSampleDataSources(self):
1407
+ slicer.util.selectModule("SampleData")
1408
+ widget = slicer.modules.SampleDataWidget
1409
+ self.assertGreater(widget.categoryLayout.count(), 0)
1410
+
1411
+ SampleDataWidget.removeCategories(widget.categoryLayout)
1412
+ self.assertEqual(widget.categoryLayout.count(), 0)
1413
+
1414
+ SampleDataWidget.setCategoriesFromSampleDataSources(widget.categoryLayout, slicer.modules.sampleDataSources, widget.logic)
1415
+ self.assertGreater(widget.categoryLayout.count(), 0)
1416
+
1417
+ def test_isSampleDataSourceRegistered(self):
1418
+ if not slicer.app.testingEnabled():
1419
+ return
1420
+ sourceArguments = {
1421
+ "sampleName": "isSampleDataSourceRegistered",
1422
+ "uris": "https://slicer.org",
1423
+ "fileNames": "volume.nrrd",
1424
+ "loadFileTypes": "VolumeFile",
1425
+ }
1426
+ self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments)))
1427
+ SampleDataLogic.registerCustomSampleDataSource(**sourceArguments, category="Testing")
1428
+ self.assertTrue(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments)))
1429
+ self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Other", SampleDataSource(**sourceArguments)))
1430
+
1431
+ def test_defaultFileType(self):
1432
+ """Test that file type is guessed correctly when not specified."""
1433
+
1434
+ logic = SampleDataLogic()
1435
+
1436
+ nodes = logic.downloadFromSource(SampleDataSource(
1437
+ uris=[TESTING_DATA_URL + "MD5/958737f8621437b18e4d8ab16eb65ad7"],
1438
+ fileNames=["brainMesh.vtk"],
1439
+ nodeNames=["brainMesh"]))
1440
+ self.assertEqual(len(nodes), 1)
1441
+ self.assertEqual(nodes[0].GetClassName(), "vtkMRMLModelNode")
1442
+
1443
+ nodes = logic.downloadFromSource(SampleDataSource(
1444
+ uris=[TESTING_DATA_URL + "SHA256/3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470"],
1445
+ fileNames=["TinyPatient_Structures.seg.nrrd"],
1446
+ nodeNames=["TinyPatient_Segments"]))
1447
+ self.assertEqual(len(nodes), 1)
1448
+ self.assertEqual(nodes[0].GetClassName(), "vtkMRMLSegmentationNode")
1449
+
1450
+ nodes = logic.downloadFromSource(SampleDataSource(
1451
+ uris=[TESTING_DATA_URL + "SHA256/72fbc8c8e0e4fc7c3f628d833e4a6fb7adbe15b0bac2f8669f296e052414578c"],
1452
+ fileNames=["FastNonrigidBSplineregistrationTransform.tfm"],
1453
+ nodeNames=["FastNonrigidBSplineregistrationTransform"]))
1454
+ self.assertEqual(len(nodes), 1)
1455
+ self.assertEqual(nodes[0].GetClassName(), "vtkMRMLBSplineTransformNode")
1456
+
1457
+ class CustomDownloader:
1458
+ def __call__(self, source):
1459
+ SampleDataTest.customDownloads.append(source)
1460
+
1461
+ CustomDownloaderDataSource = {
1462
+ "category": "Testing",
1463
+ "sampleName": "customDownloader",
1464
+ "uris": "http://down.load/test",
1465
+ "fileNames": "cust.om",
1466
+ "customDownloader": CustomDownloader(),
1467
+ }
1468
+
1469
+ def test_customDownloader(self):
1470
+ if not slicer.app.testingEnabled():
1471
+ return
1472
+ slicer.util.selectModule("SampleData")
1473
+ widget = slicer.modules.SampleDataWidget
1474
+ button = slicer.util.findChild(widget.parent, "customDownloaderPushButton")
1475
+
1476
+ self.assertEqual(self.customDownloads, [])
1477
+
1478
+ button.click()
1479
+
1480
+ self.assertEqual(len(self.customDownloads), 1)
1481
+ self.assertEqual(self.customDownloads[0].sampleName, "customDownloader")
1482
+
1483
+ def test_categoryForSource(self):
1484
+ logic = SampleDataLogic()
1485
+ source = slicer.modules.sampleDataSources[logic.builtInCategoryName][0]
1486
+ self.assertEqual(logic.categoryForSource(source), logic.builtInCategoryName)