torch_em.data.datasets.util
1import os 2import hashlib 3import inspect 4import zipfile 5import requests 6from tqdm import tqdm 7from warnings import warn 8from subprocess import run 9from xml.dom import minidom 10from packaging import version 11from shutil import copyfileobj, which 12 13from typing import Optional, Tuple, Literal 14 15import numpy as np 16from skimage.draw import polygon 17 18import torch 19 20import torch_em 21from torch_em.transform import get_raw_transform 22from torch_em.transform.generic import ResizeLongestSideInputs, Compose 23 24try: 25 import gdown 26except ImportError: 27 gdown = None 28 29try: 30 from tcia_utils import nbia 31except ModuleNotFoundError: 32 nbia = None 33 34try: 35 from cryoet_data_portal import Client, Dataset 36except ImportError: 37 Client, Dataset = None, None 38 39try: 40 import synapseclient 41 import synapseutils 42except ImportError: 43 synapseclient, synapseutils = None, None 44 45 46BIOIMAGEIO_IDS = { 47 "covid_if": "ilastik/covid_if_training_data", 48 "cremi": "ilastik/cremi_training_data", 49 "dsb": "ilastik/stardist_dsb_training_data", 50 "hpa": "", # not on bioimageio yet 51 "isbi2012": "ilastik/isbi2012_neuron_segmentation_challenge", 52 "kasthuri": "", # not on bioimageio yet: 53 "livecell": "ilastik/livecell_dataset", 54 "lucchi": "", # not on bioimageio yet: 55 "mitoem": "ilastik/mitoem_segmentation_challenge", 56 "monuseg": "deepimagej/monuseg_digital_pathology_miccai2018", 57 "ovules": "", # not on bioimageio yet 58 "plantseg_root": "ilastik/plantseg_root", 59 "plantseg_ovules": "ilastik/plantseg_ovules", 60 "platynereis": "ilastik/platynereis_em_training_data", 61 "snemi": "", # not on bioimagegio yet 62 "uro_cell": "", # not on bioimageio yet: https://doi.org/10.1016/j.compbiomed.2020.103693 63 "vnc": "ilastik/vnc", 64} 65"""@private 66""" 67 68 69def get_bioimageio_dataset_id(dataset_name): 70 """@private 71 """ 72 assert dataset_name in BIOIMAGEIO_IDS 73 return BIOIMAGEIO_IDS[dataset_name] 74 75 76def get_checksum(filename: str) -> str: 77 """Get the SHA256 checksum of a file. 78 79 Args: 80 filename: The filepath. 81 82 Returns: 83 The checksum. 84 """ 85 # The file is hashed in chunks, so that datasets with multi-GB archives do not run out of memory. 86 hasher = hashlib.sha256() 87 with open(filename, "rb") as f: 88 for chunk in iter(lambda: f.read(64 * 1024 * 1024), b""): 89 hasher.update(chunk) 90 return hasher.hexdigest() 91 92 93def _check_checksum(path, checksum): 94 if checksum is not None: 95 this_checksum = get_checksum(path) 96 if this_checksum != checksum: 97 raise RuntimeError( 98 "The checksum of the download does not match the expected checksum." 99 f"Expected: {checksum}, got: {this_checksum}" 100 ) 101 print("Download successful and checksums agree.") 102 else: 103 warn("The file was downloaded, but no checksum was provided, so the file may be corrupted.") 104 105 106# this needs to be extended to support download from s3 via boto, 107# if we get a resource that is available via s3 without support for http 108def download_source(path: str, url: str, download: bool, checksum: Optional[str] = None, verify: bool = True) -> None: 109 """Download data via https. 110 111 Args: 112 path: The path for saving the data. 113 url: The url of the data. 114 download: Whether to download the data if it is not saved at `path` yet. 115 checksum: The expected checksum of the data. 116 verify: Whether to verify the https address. 117 """ 118 if os.path.exists(path): 119 return 120 if not download: 121 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False") 122 123 # The data is downloaded to a temporary path and only moved to `path` once it is complete and verified. 124 # Otherwise an interrupted download would be mistaken for a complete one by the check above. 125 tmp_path = f"{path}.incomplete" 126 with requests.get(url, stream=True, allow_redirects=True, verify=verify) as r: 127 r.raise_for_status() # check for error 128 # Compute checksums on the file content rather than its HTTP transfer encoding. 129 r.raw.decode_content = True 130 file_size = int(r.headers.get("Content-Length", 0)) 131 desc = f"Download {url} to {path}" 132 if file_size == 0: 133 desc += " (unknown file size)" 134 with tqdm.wrapattr(r.raw, "read", total=file_size, desc=desc) as r_raw, open(tmp_path, "wb") as f: 135 copyfileobj(r_raw, f) 136 137 _check_checksum(tmp_path, checksum) 138 os.replace(tmp_path, path) 139 140 141def download_source_gdrive( 142 path: str, 143 url: str, 144 download: bool, 145 checksum: Optional[str] = None, 146 download_type: Literal["zip", "folder"] = "zip", 147 expected_samples: int = 10000, 148 quiet: bool = True, 149) -> None: 150 """Download data from google drive. 151 152 Args: 153 path: The path for saving the data. 154 url: The url of the data. 155 download: Whether to download the data if it is not saved at `path` yet. 156 checksum: The expected checksum of the data. 157 download_type: The download type, either 'zip' or 'folder'. 158 expected_samples: The maximal number of samples in the folder. 159 quiet: Whether to download quietly. 160 """ 161 if os.path.exists(path): 162 return 163 164 if not download: 165 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False") 166 167 if gdown is None: 168 raise RuntimeError( 169 "Need gdown library to download data from google drive. " 170 "Please install gdown: 'conda install -c conda-forge gdown==4.6.3'." 171 ) 172 173 print("Downloading the files. Might take a few minutes...") 174 175 if download_type == "zip": 176 gdown.download(url, path, quiet=quiet) 177 _check_checksum(path, checksum) 178 elif download_type == "folder": 179 assert version.parse(gdown.__version__) == version.parse("4.6.3"), "Please install 'gdown==4.6.3'." 180 gdown.download_folder.__globals__["MAX_NUMBER_FILES"] = expected_samples 181 gdown.download_folder(url=url, output=path, quiet=quiet, remaining_ok=True) 182 else: 183 raise ValueError("`download_path` argument expects either `zip`/`folder`") 184 185 print("Download completed.") 186 187 188def download_source_empiar(path: str, access_id: str, download: bool) -> str: 189 """Download data from EMPIAR. 190 191 Requires the ascp command from the aspera CLI. 192 193 Args: 194 path: The path for saving the data. 195 access_id: The EMPIAR accession id of the data to download. 196 download: Whether to download the data if it is not saved at `path` yet. 197 198 Returns: 199 The path to the downloaded data. 200 """ 201 download_path = os.path.join(path, access_id) 202 203 if os.path.exists(download_path): 204 return download_path 205 if not download: 206 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False") 207 208 if which("ascp") is None: 209 raise RuntimeError( 210 "Need aspera-cli to download data from empiar. You can install it via 'conda install -c hcc aspera-cli'." 211 ) 212 213 key_file = os.path.expanduser("~/.aspera/cli/etc/asperaweb_id_dsa.openssh") 214 if not os.path.exists(key_file): 215 conda_root = os.environ["CONDA_PREFIX"] 216 key_file = os.path.join(conda_root, "etc/asperaweb_id_dsa.openssh") 217 218 if not os.path.exists(key_file): 219 raise RuntimeError("Could not find the aspera ssh keyfile") 220 221 cmd = ["ascp", "-QT", "-l", "200M", "-P33001", "-i", key_file, f"emp_ext2@fasp.ebi.ac.uk:/{access_id}", path] 222 run(cmd) 223 224 return download_path 225 226 227def download_source_kaggle(path: str, dataset_name: str, download: bool, competition: bool = False): 228 """Download data from Kaggle. 229 230 Requires the Kaggle API. 231 232 Args: 233 path: The path for saving the data. 234 dataset_name: The name of the dataset to download. 235 download: Whether to download the data if it is not saved at `path` yet. 236 competition: Whether this data is from a competition and requires the kaggle.competition API. 237 """ 238 if not download: 239 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 240 241 try: 242 from kaggle.api.kaggle_api_extended import KaggleApi 243 except ModuleNotFoundError: 244 msg = "Please install the Kaggle API. You can do this using 'pip install kaggle'. " 245 msg += "After you have installed kaggle, you would need an API token. " 246 msg += "Follow the instructions at https://www.kaggle.com/docs/api." 247 raise ModuleNotFoundError(msg) 248 249 api = KaggleApi() 250 api.authenticate() 251 252 if competition: 253 api.competition_download_files(competition=dataset_name, path=path, quiet=False) 254 else: 255 api.dataset_download_files(dataset=dataset_name, path=path, quiet=False) 256 257 258def download_source_tcia(path, url, dst, csv_filename, download): 259 """Download data from TCIA. 260 261 Requires the tcia_utils python package. 262 263 Args: 264 path: The path for saving the data. 265 url: The URL to the TCIA dataset. 266 dst: 267 csv_filename: 268 download: Whether to download the data if it is not saved at `path` yet. 269 """ 270 if nbia is None: 271 raise RuntimeError("Requires the tcia_utils python package.") 272 if not download: 273 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 274 assert url.endswith(".tcia"), f"{url} is not a TCIA Manifest." 275 276 # Downloads the manifest file from the collection page. 277 manifest = requests.get(url=url) 278 with open(path, "wb") as f: 279 f.write(manifest.content) 280 281 # This part extracts the UIDs from the manifests and downloads them. 282 nbia.downloadSeries(series_data=path, input_type="manifest", path=dst, csv_filename=csv_filename) 283 284 285def download_source_synapse(path: str, entity: str, download: bool) -> None: 286 """Download data from synapse. 287 288 Requires the synapseclient python library. 289 290 Args: 291 path: The path for saving the data. 292 entity: The name of the data to download from synapse. 293 download: Whether to download the data if it is not saved at `path` yet. 294 """ 295 if not download: 296 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 297 298 if synapseclient is None: 299 raise RuntimeError( 300 "You must install 'synapseclient' to download files from 'synapse'. " 301 "Remember to create an account and generate an authentication code for your account. " 302 "Please follow the documentation for details on creating the '~/.synapseConfig' file here: " 303 "https://python-docs.synapse.org/tutorials/authentication/." 304 ) 305 306 assert entity.startswith("syn"), "The entity name does not look as expected. It should be something like 'syn123'." 307 308 # Download all files in the folder. 309 syn = synapseclient.Synapse() 310 syn.login() # Since we do not pass any credentials here, it fetches all details from '~/.synapseConfig'. 311 synapseutils.syncFromSynapse(syn=syn, entity=entity, path=path) 312 313 314def update_kwargs(kwargs, key, value, msg=None): 315 """@private 316 """ 317 if key in kwargs: 318 msg = f"{key} will be over-ridden in loader kwargs." if msg is None else msg 319 warn(msg) 320 kwargs[key] = value 321 return kwargs 322 323 324def unzip_tarfile(tar_path: str, dst: str, remove: bool = True) -> None: 325 """Unpack a tar archive. 326 327 Args: 328 tar_path: Path to the tar file. 329 dst: Where to unpack the archive. 330 remove: Whether to remove the tar file after unpacking. 331 """ 332 import tarfile 333 334 if tar_path.endswith(".tar.gz") or tar_path.endswith(".tgz"): 335 access_mode = "r:gz" 336 elif tar_path.endswith(".tar"): 337 access_mode = "r:" 338 else: 339 raise ValueError(f"The provided file isn't a supported archive to unpack. Please check the file: {tar_path}.") 340 341 tar = tarfile.open(tar_path, access_mode) 342 tar.extractall(dst) 343 tar.close() 344 345 if remove: 346 os.remove(tar_path) 347 348 349def unzip_rarfile(rar_path: str, dst: str, remove: bool = True, use_rarfile: bool = True) -> None: 350 """Unpack a rar archive. 351 352 Args: 353 rar_path: Path to the rar file. 354 dst: Where to unpack the archive. 355 remove: Whether to remove the tar file after unpacking. 356 use_rarfile: Whether to use the rarfile library or aspose.zip. 357 """ 358 def _extract_with_rarfile(): 359 import rarfile 360 with rarfile.RarFile(rar_path) as archive: 361 archive.extractall(path=dst) 362 363 def _extract_with_aspose(): 364 import aspose.zip as az 365 with az.rar.RarArchive(rar_path) as archive: 366 archive.extract_to_directory(dst) 367 368 extractors = [ 369 ('rarfile', _extract_with_rarfile), ('aspose.zip', _extract_with_aspose), 370 ] if use_rarfile else [('aspose.zip', _extract_with_aspose)] 371 372 errors = [] 373 for name, extractor in extractors: 374 try: 375 extractor() 376 break 377 except Exception as err: 378 errors.append((name, err)) 379 if len(errors) < len(extractors): 380 next_name = extractors[len(errors)][0] 381 warn(f"Extraction with '{name}' failed for {rar_path} ({err}). Falling back to '{next_name}'.") 382 else: 383 backends = ', '.join(f"'{name}'" for name, _ in extractors) 384 raise RuntimeError( 385 f"Failed to extract rar archive {rar_path} with {backends}. " 386 "Please ensure one of the supported backends is installed and can read this archive." 387 ) from errors[-1][1] 388 389 if remove: 390 os.remove(rar_path) 391 392 393def unzip(zip_path: str, dst: str, remove: bool = True) -> None: 394 """Unpack a zip archive. 395 396 Args: 397 zip_path: Path to the zip file. 398 dst: Where to unpack the archive. 399 remove: Whether to remove the tar file after unpacking. 400 """ 401 with zipfile.ZipFile(zip_path, "r") as f: 402 f.extractall(dst) 403 if remove: 404 os.remove(zip_path) 405 406 407def split_kwargs(function, **kwargs): 408 """@private 409 """ 410 function_parameters = inspect.signature(function).parameters 411 parameter_names = list(function_parameters.keys()) 412 other_kwargs = {k: v for k, v in kwargs.items() if k not in parameter_names} 413 kwargs = {k: v for k, v in kwargs.items() if k in parameter_names} 414 return kwargs, other_kwargs 415 416 417# this adds the default transforms for 'raw_transform' and 'transform' 418# in case these were not specified in the kwargs 419# this is NOT necessary if 'default_segmentation_dataset' is used, only if a dataset class 420# is used directly, e.g. in the LiveCell Loader 421def ensure_transforms(ndim, **kwargs): 422 """@private 423 """ 424 if "raw_transform" not in kwargs: 425 kwargs = update_kwargs(kwargs, "raw_transform", torch_em.transform.get_raw_transform()) 426 if "transform" not in kwargs: 427 kwargs = update_kwargs(kwargs, "transform", torch_em.transform.get_augmentations(ndim=ndim)) 428 return kwargs 429 430 431def add_instance_label_transform( 432 kwargs, add_binary_target, label_dtype=None, binary=False, boundaries=False, offsets=None, binary_is_exclusive=True, 433): 434 """@private 435 """ 436 if binary_is_exclusive: 437 assert sum((offsets is not None, boundaries, binary)) <= 1 438 else: 439 assert sum((offsets is not None, boundaries)) <= 1 440 if offsets is not None: 441 label_transform2 = torch_em.transform.label.AffinityTransform(offsets=offsets, 442 add_binary_target=add_binary_target, 443 add_mask=True) 444 msg = "Offsets are passed, but 'label_transform2' is in the kwargs. It will be over-ridden." 445 kwargs = update_kwargs(kwargs, "label_transform2", label_transform2, msg=msg) 446 label_dtype = torch.float32 447 elif boundaries: 448 label_transform = torch_em.transform.label.BoundaryTransform(add_binary_target=add_binary_target) 449 msg = "Boundaries is set to true, but 'label_transform' is in the kwargs. It will be over-ridden." 450 kwargs = update_kwargs(kwargs, "label_transform", label_transform, msg=msg) 451 label_dtype = torch.float32 452 elif binary: 453 label_transform = torch_em.transform.label.labels_to_binary 454 msg = "Binary is set to true, but 'label_transform' is in the kwargs. It will be over-ridden." 455 kwargs = update_kwargs(kwargs, "label_transform", label_transform, msg=msg) 456 label_dtype = torch.float32 457 return kwargs, label_dtype 458 459 460def update_kwargs_for_resize_trafo(kwargs, patch_shape, resize_inputs, resize_kwargs=None, ensure_rgb=None): 461 """@private 462 """ 463 # Checks for raw_transform and label_transform incoming values. 464 # If yes, it will automatically merge these two transforms to apply them together. 465 if resize_inputs: 466 assert isinstance(resize_kwargs, dict) 467 468 target_shape = resize_kwargs.get("patch_shape") 469 if len(resize_kwargs["patch_shape"]) == 3: 470 # we only need the XY dimensions to reshape the inputs along them. 471 target_shape = target_shape[1:] 472 # we provide the Z dimension value to return the desired number of slices and not the whole volume 473 kwargs["z_ext"] = resize_kwargs["patch_shape"][0] 474 475 raw_trafo = ResizeLongestSideInputs(target_shape=target_shape, is_rgb=resize_kwargs["is_rgb"]) 476 label_trafo = ResizeLongestSideInputs(target_shape=target_shape, is_label=True) 477 478 # The patch shape provided to the dataset. Here, "None" means that the entire volume will be loaded. 479 patch_shape = None 480 481 if ensure_rgb is None: 482 raw_trafos = [] 483 else: 484 assert not isinstance(ensure_rgb, bool), "'ensure_rgb' is expected to be a function." 485 raw_trafos = [ensure_rgb] 486 487 if "raw_transform" in kwargs: 488 raw_trafos.extend([raw_trafo, kwargs["raw_transform"]]) 489 else: 490 raw_trafos.extend([raw_trafo, get_raw_transform()]) 491 492 kwargs["raw_transform"] = Compose(*raw_trafos, is_multi_tensor=False) 493 494 if "label_transform" in kwargs: 495 trafo = Compose(label_trafo, kwargs["label_transform"], is_multi_tensor=False) 496 kwargs["label_transform"] = trafo 497 else: 498 kwargs["label_transform"] = label_trafo 499 500 return kwargs, patch_shape 501 502 503def generate_labeled_array_from_xml(shape: Tuple[int, ...], xml_file: str) -> np.ndarray: 504 """Generate a label mask from a contour defined in a xml annotation file. 505 506 Function taken from: https://github.com/rshwndsz/hover-net/blob/master/lightning_hovernet.ipynb 507 508 Args: 509 shape: The image shape. 510 xml_file: The path to the xml file with contour annotations. 511 512 Returns: 513 The label mask. 514 """ 515 # DOM object created by the minidom parser 516 xDoc = minidom.parse(xml_file) 517 518 # List of all Region tags 519 regions = xDoc.getElementsByTagName('Region') 520 521 # List which will store the vertices for each region 522 xy = [] 523 for region in regions: 524 # Loading all the vertices in the region 525 vertices = region.getElementsByTagName('Vertex') 526 527 # The vertices of a region will be stored in a array 528 vw = np.zeros((len(vertices), 2)) 529 530 for index, vertex in enumerate(vertices): 531 # Storing the values of x and y coordinate after conversion 532 vw[index][0] = float(vertex.getAttribute('X')) 533 vw[index][1] = float(vertex.getAttribute('Y')) 534 535 # Append the vertices of a region 536 xy.append(np.int32(vw)) 537 538 # Creating a completely black image 539 mask = np.zeros(shape, np.float32) 540 541 for i, contour in enumerate(xy): 542 r, c = polygon(np.array(contour)[:, 1], np.array(contour)[:, 0], shape=shape) 543 mask[r, c] = i 544 return mask 545 546 547# This function could be extended to convert WSIs (or modalities with multiple resolutions). 548def convert_svs_to_array( 549 path: str, location: Tuple[int, int] = (0, 0), level: int = 0, img_size: Tuple[int, int] = None, 550) -> np.ndarray: 551 """Convert a .svs file for WSI imagging to a numpy array. 552 553 Requires the tiffslide python library. 554 The function can load multi-resolution images. You can specify the resolution level via `level`. 555 556 Args: 557 path: File path ath to the svs file. 558 location: Pixel location (x, y) in level 0 of the image. 559 level: Target level used to read the image. 560 img_size: Size of the image. If None, the shape of the image at `level` is used. 561 562 Returns: 563 The image as numpy array. 564 """ 565 from tiffslide import TiffSlide 566 567 assert path.endswith(".svs"), f"The provided file ({path}) isn't in svs format" 568 _slide = TiffSlide(path) 569 if img_size is None: 570 img_size = _slide.level_dimensions[0] 571 return _slide.read_region(location=location, level=level, size=img_size, as_array=True) 572 573 574def download_from_cryo_et_portal(path: str, dataset_id: int, download: bool) -> str: 575 """Download data from the CryoET Data Portal. 576 577 Requires the cryoet-data-portal python library. 578 579 Args: 580 path: The path for saving the data. 581 dataset_id: The id of the data to download from the portal. 582 download: Whether to download the data if it is not saved at `path` yet. 583 584 Returns: 585 The file path to the downloaded data. 586 """ 587 if Client is None or Dataset is None: 588 raise RuntimeError("Please install CryoETDataPortal via 'pip install cryoet-data-portal'") 589 590 output_path = os.path.join(path, str(dataset_id)) 591 if os.path.exists(output_path): 592 return output_path 593 594 if not download: 595 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 596 597 client = Client() 598 dataset = Dataset.get_by_id(client, dataset_id) 599 dataset.download_everything(dest_path=path) 600 601 return output_path
77def get_checksum(filename: str) -> str: 78 """Get the SHA256 checksum of a file. 79 80 Args: 81 filename: The filepath. 82 83 Returns: 84 The checksum. 85 """ 86 # The file is hashed in chunks, so that datasets with multi-GB archives do not run out of memory. 87 hasher = hashlib.sha256() 88 with open(filename, "rb") as f: 89 for chunk in iter(lambda: f.read(64 * 1024 * 1024), b""): 90 hasher.update(chunk) 91 return hasher.hexdigest()
Get the SHA256 checksum of a file.
Arguments:
- filename: The filepath.
Returns:
The checksum.
109def download_source(path: str, url: str, download: bool, checksum: Optional[str] = None, verify: bool = True) -> None: 110 """Download data via https. 111 112 Args: 113 path: The path for saving the data. 114 url: The url of the data. 115 download: Whether to download the data if it is not saved at `path` yet. 116 checksum: The expected checksum of the data. 117 verify: Whether to verify the https address. 118 """ 119 if os.path.exists(path): 120 return 121 if not download: 122 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False") 123 124 # The data is downloaded to a temporary path and only moved to `path` once it is complete and verified. 125 # Otherwise an interrupted download would be mistaken for a complete one by the check above. 126 tmp_path = f"{path}.incomplete" 127 with requests.get(url, stream=True, allow_redirects=True, verify=verify) as r: 128 r.raise_for_status() # check for error 129 # Compute checksums on the file content rather than its HTTP transfer encoding. 130 r.raw.decode_content = True 131 file_size = int(r.headers.get("Content-Length", 0)) 132 desc = f"Download {url} to {path}" 133 if file_size == 0: 134 desc += " (unknown file size)" 135 with tqdm.wrapattr(r.raw, "read", total=file_size, desc=desc) as r_raw, open(tmp_path, "wb") as f: 136 copyfileobj(r_raw, f) 137 138 _check_checksum(tmp_path, checksum) 139 os.replace(tmp_path, path)
Download data via https.
Arguments:
- path: The path for saving the data.
- url: The url of the data.
- download: Whether to download the data if it is not saved at
pathyet. - checksum: The expected checksum of the data.
- verify: Whether to verify the https address.
142def download_source_gdrive( 143 path: str, 144 url: str, 145 download: bool, 146 checksum: Optional[str] = None, 147 download_type: Literal["zip", "folder"] = "zip", 148 expected_samples: int = 10000, 149 quiet: bool = True, 150) -> None: 151 """Download data from google drive. 152 153 Args: 154 path: The path for saving the data. 155 url: The url of the data. 156 download: Whether to download the data if it is not saved at `path` yet. 157 checksum: The expected checksum of the data. 158 download_type: The download type, either 'zip' or 'folder'. 159 expected_samples: The maximal number of samples in the folder. 160 quiet: Whether to download quietly. 161 """ 162 if os.path.exists(path): 163 return 164 165 if not download: 166 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False") 167 168 if gdown is None: 169 raise RuntimeError( 170 "Need gdown library to download data from google drive. " 171 "Please install gdown: 'conda install -c conda-forge gdown==4.6.3'." 172 ) 173 174 print("Downloading the files. Might take a few minutes...") 175 176 if download_type == "zip": 177 gdown.download(url, path, quiet=quiet) 178 _check_checksum(path, checksum) 179 elif download_type == "folder": 180 assert version.parse(gdown.__version__) == version.parse("4.6.3"), "Please install 'gdown==4.6.3'." 181 gdown.download_folder.__globals__["MAX_NUMBER_FILES"] = expected_samples 182 gdown.download_folder(url=url, output=path, quiet=quiet, remaining_ok=True) 183 else: 184 raise ValueError("`download_path` argument expects either `zip`/`folder`") 185 186 print("Download completed.")
Download data from google drive.
Arguments:
- path: The path for saving the data.
- url: The url of the data.
- download: Whether to download the data if it is not saved at
pathyet. - checksum: The expected checksum of the data.
- download_type: The download type, either 'zip' or 'folder'.
- expected_samples: The maximal number of samples in the folder.
- quiet: Whether to download quietly.
189def download_source_empiar(path: str, access_id: str, download: bool) -> str: 190 """Download data from EMPIAR. 191 192 Requires the ascp command from the aspera CLI. 193 194 Args: 195 path: The path for saving the data. 196 access_id: The EMPIAR accession id of the data to download. 197 download: Whether to download the data if it is not saved at `path` yet. 198 199 Returns: 200 The path to the downloaded data. 201 """ 202 download_path = os.path.join(path, access_id) 203 204 if os.path.exists(download_path): 205 return download_path 206 if not download: 207 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False") 208 209 if which("ascp") is None: 210 raise RuntimeError( 211 "Need aspera-cli to download data from empiar. You can install it via 'conda install -c hcc aspera-cli'." 212 ) 213 214 key_file = os.path.expanduser("~/.aspera/cli/etc/asperaweb_id_dsa.openssh") 215 if not os.path.exists(key_file): 216 conda_root = os.environ["CONDA_PREFIX"] 217 key_file = os.path.join(conda_root, "etc/asperaweb_id_dsa.openssh") 218 219 if not os.path.exists(key_file): 220 raise RuntimeError("Could not find the aspera ssh keyfile") 221 222 cmd = ["ascp", "-QT", "-l", "200M", "-P33001", "-i", key_file, f"emp_ext2@fasp.ebi.ac.uk:/{access_id}", path] 223 run(cmd) 224 225 return download_path
Download data from EMPIAR.
Requires the ascp command from the aspera CLI.
Arguments:
- path: The path for saving the data.
- access_id: The EMPIAR accession id of the data to download.
- download: Whether to download the data if it is not saved at
pathyet.
Returns:
The path to the downloaded data.
228def download_source_kaggle(path: str, dataset_name: str, download: bool, competition: bool = False): 229 """Download data from Kaggle. 230 231 Requires the Kaggle API. 232 233 Args: 234 path: The path for saving the data. 235 dataset_name: The name of the dataset to download. 236 download: Whether to download the data if it is not saved at `path` yet. 237 competition: Whether this data is from a competition and requires the kaggle.competition API. 238 """ 239 if not download: 240 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 241 242 try: 243 from kaggle.api.kaggle_api_extended import KaggleApi 244 except ModuleNotFoundError: 245 msg = "Please install the Kaggle API. You can do this using 'pip install kaggle'. " 246 msg += "After you have installed kaggle, you would need an API token. " 247 msg += "Follow the instructions at https://www.kaggle.com/docs/api." 248 raise ModuleNotFoundError(msg) 249 250 api = KaggleApi() 251 api.authenticate() 252 253 if competition: 254 api.competition_download_files(competition=dataset_name, path=path, quiet=False) 255 else: 256 api.dataset_download_files(dataset=dataset_name, path=path, quiet=False)
Download data from Kaggle.
Requires the Kaggle API.
Arguments:
- path: The path for saving the data.
- dataset_name: The name of the dataset to download.
- download: Whether to download the data if it is not saved at
pathyet. - competition: Whether this data is from a competition and requires the kaggle.competition API.
259def download_source_tcia(path, url, dst, csv_filename, download): 260 """Download data from TCIA. 261 262 Requires the tcia_utils python package. 263 264 Args: 265 path: The path for saving the data. 266 url: The URL to the TCIA dataset. 267 dst: 268 csv_filename: 269 download: Whether to download the data if it is not saved at `path` yet. 270 """ 271 if nbia is None: 272 raise RuntimeError("Requires the tcia_utils python package.") 273 if not download: 274 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 275 assert url.endswith(".tcia"), f"{url} is not a TCIA Manifest." 276 277 # Downloads the manifest file from the collection page. 278 manifest = requests.get(url=url) 279 with open(path, "wb") as f: 280 f.write(manifest.content) 281 282 # This part extracts the UIDs from the manifests and downloads them. 283 nbia.downloadSeries(series_data=path, input_type="manifest", path=dst, csv_filename=csv_filename)
Download data from TCIA.
Requires the tcia_utils python package.
Arguments:
- path: The path for saving the data.
- url: The URL to the TCIA dataset.
- dst:
- csv_filename:
- download: Whether to download the data if it is not saved at
pathyet.
286def download_source_synapse(path: str, entity: str, download: bool) -> None: 287 """Download data from synapse. 288 289 Requires the synapseclient python library. 290 291 Args: 292 path: The path for saving the data. 293 entity: The name of the data to download from synapse. 294 download: Whether to download the data if it is not saved at `path` yet. 295 """ 296 if not download: 297 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 298 299 if synapseclient is None: 300 raise RuntimeError( 301 "You must install 'synapseclient' to download files from 'synapse'. " 302 "Remember to create an account and generate an authentication code for your account. " 303 "Please follow the documentation for details on creating the '~/.synapseConfig' file here: " 304 "https://python-docs.synapse.org/tutorials/authentication/." 305 ) 306 307 assert entity.startswith("syn"), "The entity name does not look as expected. It should be something like 'syn123'." 308 309 # Download all files in the folder. 310 syn = synapseclient.Synapse() 311 syn.login() # Since we do not pass any credentials here, it fetches all details from '~/.synapseConfig'. 312 synapseutils.syncFromSynapse(syn=syn, entity=entity, path=path)
Download data from synapse.
Requires the synapseclient python library.
Arguments:
- path: The path for saving the data.
- entity: The name of the data to download from synapse.
- download: Whether to download the data if it is not saved at
pathyet.
325def unzip_tarfile(tar_path: str, dst: str, remove: bool = True) -> None: 326 """Unpack a tar archive. 327 328 Args: 329 tar_path: Path to the tar file. 330 dst: Where to unpack the archive. 331 remove: Whether to remove the tar file after unpacking. 332 """ 333 import tarfile 334 335 if tar_path.endswith(".tar.gz") or tar_path.endswith(".tgz"): 336 access_mode = "r:gz" 337 elif tar_path.endswith(".tar"): 338 access_mode = "r:" 339 else: 340 raise ValueError(f"The provided file isn't a supported archive to unpack. Please check the file: {tar_path}.") 341 342 tar = tarfile.open(tar_path, access_mode) 343 tar.extractall(dst) 344 tar.close() 345 346 if remove: 347 os.remove(tar_path)
Unpack a tar archive.
Arguments:
- tar_path: Path to the tar file.
- dst: Where to unpack the archive.
- remove: Whether to remove the tar file after unpacking.
350def unzip_rarfile(rar_path: str, dst: str, remove: bool = True, use_rarfile: bool = True) -> None: 351 """Unpack a rar archive. 352 353 Args: 354 rar_path: Path to the rar file. 355 dst: Where to unpack the archive. 356 remove: Whether to remove the tar file after unpacking. 357 use_rarfile: Whether to use the rarfile library or aspose.zip. 358 """ 359 def _extract_with_rarfile(): 360 import rarfile 361 with rarfile.RarFile(rar_path) as archive: 362 archive.extractall(path=dst) 363 364 def _extract_with_aspose(): 365 import aspose.zip as az 366 with az.rar.RarArchive(rar_path) as archive: 367 archive.extract_to_directory(dst) 368 369 extractors = [ 370 ('rarfile', _extract_with_rarfile), ('aspose.zip', _extract_with_aspose), 371 ] if use_rarfile else [('aspose.zip', _extract_with_aspose)] 372 373 errors = [] 374 for name, extractor in extractors: 375 try: 376 extractor() 377 break 378 except Exception as err: 379 errors.append((name, err)) 380 if len(errors) < len(extractors): 381 next_name = extractors[len(errors)][0] 382 warn(f"Extraction with '{name}' failed for {rar_path} ({err}). Falling back to '{next_name}'.") 383 else: 384 backends = ', '.join(f"'{name}'" for name, _ in extractors) 385 raise RuntimeError( 386 f"Failed to extract rar archive {rar_path} with {backends}. " 387 "Please ensure one of the supported backends is installed and can read this archive." 388 ) from errors[-1][1] 389 390 if remove: 391 os.remove(rar_path)
Unpack a rar archive.
Arguments:
- rar_path: Path to the rar file.
- dst: Where to unpack the archive.
- remove: Whether to remove the tar file after unpacking.
- use_rarfile: Whether to use the rarfile library or aspose.zip.
394def unzip(zip_path: str, dst: str, remove: bool = True) -> None: 395 """Unpack a zip archive. 396 397 Args: 398 zip_path: Path to the zip file. 399 dst: Where to unpack the archive. 400 remove: Whether to remove the tar file after unpacking. 401 """ 402 with zipfile.ZipFile(zip_path, "r") as f: 403 f.extractall(dst) 404 if remove: 405 os.remove(zip_path)
Unpack a zip archive.
Arguments:
- zip_path: Path to the zip file.
- dst: Where to unpack the archive.
- remove: Whether to remove the tar file after unpacking.
504def generate_labeled_array_from_xml(shape: Tuple[int, ...], xml_file: str) -> np.ndarray: 505 """Generate a label mask from a contour defined in a xml annotation file. 506 507 Function taken from: https://github.com/rshwndsz/hover-net/blob/master/lightning_hovernet.ipynb 508 509 Args: 510 shape: The image shape. 511 xml_file: The path to the xml file with contour annotations. 512 513 Returns: 514 The label mask. 515 """ 516 # DOM object created by the minidom parser 517 xDoc = minidom.parse(xml_file) 518 519 # List of all Region tags 520 regions = xDoc.getElementsByTagName('Region') 521 522 # List which will store the vertices for each region 523 xy = [] 524 for region in regions: 525 # Loading all the vertices in the region 526 vertices = region.getElementsByTagName('Vertex') 527 528 # The vertices of a region will be stored in a array 529 vw = np.zeros((len(vertices), 2)) 530 531 for index, vertex in enumerate(vertices): 532 # Storing the values of x and y coordinate after conversion 533 vw[index][0] = float(vertex.getAttribute('X')) 534 vw[index][1] = float(vertex.getAttribute('Y')) 535 536 # Append the vertices of a region 537 xy.append(np.int32(vw)) 538 539 # Creating a completely black image 540 mask = np.zeros(shape, np.float32) 541 542 for i, contour in enumerate(xy): 543 r, c = polygon(np.array(contour)[:, 1], np.array(contour)[:, 0], shape=shape) 544 mask[r, c] = i 545 return mask
Generate a label mask from a contour defined in a xml annotation file.
Function taken from: https://github.com/rshwndsz/hover-net/blob/master/lightning_hovernet.ipynb
Arguments:
- shape: The image shape.
- xml_file: The path to the xml file with contour annotations.
Returns:
The label mask.
549def convert_svs_to_array( 550 path: str, location: Tuple[int, int] = (0, 0), level: int = 0, img_size: Tuple[int, int] = None, 551) -> np.ndarray: 552 """Convert a .svs file for WSI imagging to a numpy array. 553 554 Requires the tiffslide python library. 555 The function can load multi-resolution images. You can specify the resolution level via `level`. 556 557 Args: 558 path: File path ath to the svs file. 559 location: Pixel location (x, y) in level 0 of the image. 560 level: Target level used to read the image. 561 img_size: Size of the image. If None, the shape of the image at `level` is used. 562 563 Returns: 564 The image as numpy array. 565 """ 566 from tiffslide import TiffSlide 567 568 assert path.endswith(".svs"), f"The provided file ({path}) isn't in svs format" 569 _slide = TiffSlide(path) 570 if img_size is None: 571 img_size = _slide.level_dimensions[0] 572 return _slide.read_region(location=location, level=level, size=img_size, as_array=True)
Convert a .svs file for WSI imagging to a numpy array.
Requires the tiffslide python library.
The function can load multi-resolution images. You can specify the resolution level via level.
Arguments:
- path: File path ath to the svs file.
- location: Pixel location (x, y) in level 0 of the image.
- level: Target level used to read the image.
- img_size: Size of the image. If None, the shape of the image at
levelis used.
Returns:
The image as numpy array.
575def download_from_cryo_et_portal(path: str, dataset_id: int, download: bool) -> str: 576 """Download data from the CryoET Data Portal. 577 578 Requires the cryoet-data-portal python library. 579 580 Args: 581 path: The path for saving the data. 582 dataset_id: The id of the data to download from the portal. 583 download: Whether to download the data if it is not saved at `path` yet. 584 585 Returns: 586 The file path to the downloaded data. 587 """ 588 if Client is None or Dataset is None: 589 raise RuntimeError("Please install CryoETDataPortal via 'pip install cryoet-data-portal'") 590 591 output_path = os.path.join(path, str(dataset_id)) 592 if os.path.exists(output_path): 593 return output_path 594 595 if not download: 596 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 597 598 client = Client() 599 dataset = Dataset.get_by_id(client, dataset_id) 600 dataset.download_everything(dest_path=path) 601 602 return output_path
Download data from the CryoET Data Portal.
Requires the cryoet-data-portal python library.
Arguments:
- path: The path for saving the data.
- dataset_id: The id of the data to download from the portal.
- download: Whether to download the data if it is not saved at
pathyet.
Returns:
The file path to the downloaded data.