Spaces:
Sleeping
Sleeping
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 +90 -139
- download_slicer_samples.py +75 -0
- examples/sample_ct_teeth.nii.gz +3 -0
- requirements.txt +1 -1
- slicer_samples.py +1486 -0
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
|
| 11 |
|
| 12 |
-
#
|
| 13 |
-
|
| 14 |
-
# TotalSegmentator sample from Zenodo (small sample subset)
|
| 15 |
{
|
| 16 |
-
"url": "https://
|
| 17 |
-
"filename": "
|
| 18 |
-
"
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
]
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
|
| 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
|
| 31 |
-
"""Download a file with progress indication"""
|
| 32 |
print(f"Downloading {description}...")
|
| 33 |
-
print(f"
|
| 34 |
-
print(f" Destination: {dest_path}")
|
| 35 |
-
|
| 36 |
try:
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
downloaded
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 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.
|
| 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)
|