torch_em.data.datasets.light_microscopy.xenium
The Xenium datasets contain cell and nucleus segmentation masks for whole-slide fluorescence images of human and mouse tissue, from the 10x Genomics in situ platform.
Every sample pairs a set of morphology images with the masks that the Xenium Onboard Analysis (XOA) pipeline produced. Channel 0 always holds the DAPI nuclear stain. The other channels hold the boundary and interior stains of the multi-tissue stain mix, which XOA uses to draw the cell outlines.
NOTE: The masks are the output of XOA, not a manual annotation. XOA segments the nuclei
on DAPI, then grows each cell from its nucleus using the boundary and interior stains.
Where no stain supports a boundary it falls back to expanding the nucleus by 5 micrometer,
so a share of the cells are dilated nuclei rather than observed outlines. It runs between three
and five percent on the samples here. XOA reports that share from version 3.0 on, and each h5
file passes it through as the 'nuc_expansion_fraction' attribute, which stays 'nan' for the
earlier versions. Treat this data as a weak label source for pretraining, not as a benchmark.
For expert annotations on Xenium images see the SPATCH dataset in
torch_em.data.datasets.histopathology.spatch.
NOTE: A cell can hold more than one nucleus, so the two label sets do not match one to one and their ids do not correspond.
NOTE: The layout of the bundle changed with the XOA version. Version 2 and 3 name the
morphology channels 'morphology_focus_000N.ome.tif' and version 4 names them
'ch000N_
NOTE: The images are whole slides that hold large empty regions. Pass a sampler such as
torch_em.data.MinInstanceSampler to avoid drawing empty patches.
The data is hosted at https://www.10xgenomics.com/datasets under the CC BY 4.0 license. Please cite the corresponding dataset page if you use this data in your research.
1"""The Xenium datasets contain cell and nucleus segmentation masks for whole-slide 2fluorescence images of human and mouse tissue, from the 10x Genomics in situ platform. 3 4Every sample pairs a set of morphology images with the masks that the Xenium Onboard 5Analysis (XOA) pipeline produced. Channel 0 always holds the DAPI nuclear stain. The 6other channels hold the boundary and interior stains of the multi-tissue stain mix, 7which XOA uses to draw the cell outlines. 8 9NOTE: The masks are the output of XOA, not a manual annotation. XOA segments the nuclei 10on DAPI, then grows each cell from its nucleus using the boundary and interior stains. 11Where no stain supports a boundary it falls back to expanding the nucleus by 5 micrometer, 12so a share of the cells are dilated nuclei rather than observed outlines. It runs between three 13and five percent on the samples here. XOA reports that share from version 3.0 on, and each h5 14file passes it through as the 'nuc_expansion_fraction' attribute, which stays 'nan' for the 15earlier versions. Treat this data as a weak label source for pretraining, not as a benchmark. 16For expert annotations on Xenium images see the SPATCH dataset in 17`torch_em.data.datasets.histopathology.spatch`. 18 19NOTE: A cell can hold more than one nucleus, so the two label sets do not match one to one 20and their ids do not correspond. 21 22NOTE: The layout of the bundle changed with the XOA version. Version 2 and 3 name the 23morphology channels 'morphology_focus_000N.ome.tif' and version 4 names them 24'ch000N_<stain>.ome.tif'. This loader sorts the files by name, which puts DAPI first in 25both schemes, and stores the names it used in the 'channel_files' attribute. Versions 26before 2.0 ship a different layout and this loader does not cover them. 27 28NOTE: The images are whole slides that hold large empty regions. Pass a sampler such as 29`torch_em.data.MinInstanceSampler` to avoid drawing empty patches. 30 31The data is hosted at https://www.10xgenomics.com/datasets under the CC BY 4.0 license. 32Please cite the corresponding dataset page if you use this data in your research. 33""" 34 35import os 36import json 37import zlib 38import shutil 39import struct 40import hashlib 41import zipfile 42from glob import glob 43from natsort import natsorted 44from contextlib import contextmanager 45from typing import Dict, List, Literal, Optional, Sequence, Tuple, Union 46 47import requests 48 49from torch.utils.data import DataLoader, Dataset 50 51import torch_em 52 53from .. import util 54 55 56URLS = { 57 "human_pancreas": "https://cf.10xgenomics.com/samples/xenium/2.0.0/Xenium_V1_human_Pancreas_FFPE/Xenium_V1_human_Pancreas_FFPE_xe_outs.zip", # noqa 58 "human_lung_cancer": "https://cf.10xgenomics.com/samples/xenium/2.0.0/Xenium_V1_humanLung_Cancer_FFPE/Xenium_V1_humanLung_Cancer_FFPE_xe_outs.zip", # noqa 59 "mouse_colon": "https://cf.10xgenomics.com/samples/xenium/2.0.0/Xenium_V1_mouse_Colon_FF/Xenium_V1_mouse_Colon_FF_xe_outs.zip", # noqa 60 "human_skin": "https://cf.10xgenomics.com/samples/xenium/3.0.0/Xenium_Prime_Human_Skin_FFPE/Xenium_Prime_Human_Skin_FFPE_xe_outs.zip", # noqa 61 "human_prostate": "https://cf.10xgenomics.com/samples/xenium/3.0.0/Xenium_Prime_Human_Prostate_FFPE/Xenium_Prime_Human_Prostate_FFPE_xe_outs.zip", # noqa 62 "human_breast": "https://cf.10xgenomics.com/samples/xenium/4.0.0/Human_Breast_Biomarkers_S1_Top/Human_Breast_Biomarkers_S1_Top_xe_outs.zip", # noqa 63} 64 65# The sha256 of the two members that every download reads, keyed by the member name. 66# The optional stain channels carry no entry here and rest on their crc32 alone. 67CHECKSUMS = { 68 "human_pancreas": { 69 "cells.zarr.zip": "5747730a532f130b41f83e970784302623a784dfc9fe571dbd11e9179366387c", 70 "morphology_focus/morphology_focus_0000.ome.tif": 71 "3a7c669ebe6cbbd7e90ebab790bc8e666b9165e972bc528e134037eef632b3bd", 72 }, 73 "human_lung_cancer": { 74 "cells.zarr.zip": "f274590902f9162295d292e540b631ba37c03574a8db754a5c27f101c8c94ef1", 75 "morphology_focus/morphology_focus_0000.ome.tif": 76 "eb0ef6aa2c7adfa5e70323b4b9530771f8679bd9c0eb6dbd1af40faabc2ca398", 77 }, 78 "mouse_colon": { 79 "cells.zarr.zip": "cc5f811511d349955b15b6e4ebb4205a4208418f900ada5853231918d929a17e", 80 "morphology_focus/morphology_focus_0000.ome.tif": 81 "2a723a59b01c943d98a31aa47fc0297e6e9414b391c1d4d3949df0b27e510ef2", 82 }, 83 "human_skin": { 84 "cells.zarr.zip": "ef805ec5c7e733f7c82a0e88652a04550abfd9f7a6c60c52339aa34ff780316d", 85 "morphology_focus/morphology_focus_0000.ome.tif": 86 "435caf0d766d6226ab8a8c3be551c9a87ad2be1997d37767c992aa36bd7d09f3", 87 }, 88 "human_prostate": { 89 "cells.zarr.zip": "7ce2e92b1b085b35051f8568a983c8f8a8622e73bfae6060efdf7b47de6371e4", 90 "morphology_focus/morphology_focus_0000.ome.tif": 91 "a095080cc946f734545db51300d7ba24700c03b02cba48204e680c556dd867ed", 92 }, 93 "human_breast": { 94 "cells.zarr.zip": "61d09dd84509a09cf2f6c1252e7d12772f6b3d6f33dd02ea25555f8ad84b6758", 95 "morphology_focus/ch0000_dapi.ome.tif": 96 "75476f9be2b0f936022611d79717cb77356e90ec40902531098b984a54d9c4a0", 97 }, 98} 99 100# The tissue of every sample, for the h5 attributes. 101TISSUES = { 102 "human_pancreas": "human pancreas", 103 "human_lung_cancer": "human lung adenocarcinoma", 104 "mouse_colon": "mouse colon", 105 "human_skin": "human skin melanoma", 106 "human_prostate": "human prostate adenocarcinoma", 107 "human_breast": "human breast", 108} 109 110LABEL_CHANNELS = {"nuclei": "0", "cells": "1"} 111 112BLOCK = 4096 113 114 115def _read_range(url: str, start: int, end: int) -> bytes: 116 """Read the bytes from `start` to `end`, both included.""" 117 response = requests.get(url, headers={"Range": f"bytes={start}-{end}"}, stream=True) 118 response.raise_for_status() 119 return response.content 120 121 122def _read_zip64_extra(extra: bytes, uncompressed: int, compressed: int, offset: int) -> Tuple[int, int, int]: 123 """Replace the fields that overflowed the 32 bit form with their zip64 values.""" 124 position = 0 125 while position + 4 <= len(extra): 126 tag, size = struct.unpack("<HH", extra[position:position + 4]) 127 if tag == 0x0001: 128 values = list(struct.unpack(f"<{size // 8}Q", extra[position + 4:position + 4 + size])) 129 if uncompressed == 0xFFFFFFFF: 130 uncompressed = values.pop(0) 131 if compressed == 0xFFFFFFFF: 132 compressed = values.pop(0) 133 if offset == 0xFFFFFFFF: 134 offset = values.pop(0) 135 break 136 position += 4 + size 137 return uncompressed, compressed, offset 138 139 140def _remote_zip_index(url: str) -> Dict[str, Dict[str, int]]: 141 """Read the central directory of a remote zip, which lists every member and where it sits. 142 143 This turns the bundle into a set of files that can be read one by one, so that a download 144 can skip the transcripts and the expression matrix, which segmentation never touches. 145 """ 146 response = requests.head(url) 147 response.raise_for_status() 148 size = int(response.headers["Content-Length"]) 149 150 tail = _read_range(url, max(0, size - 262144), size - 1) 151 position = tail.rfind(b"PK\x06\x06") 152 if position == -1: 153 position = tail.rfind(b"PK\x05\x06") 154 if position == -1: 155 raise RuntimeError(f"Could not find the central directory of the zip at {url}.") 156 directory_size, directory_offset = struct.unpack("<II", tail[position + 12:position + 20]) 157 else: 158 directory_size, directory_offset = struct.unpack("<QQ", tail[position + 40:position + 56]) 159 160 directory = _read_range(url, directory_offset, directory_offset + directory_size + 128) 161 index, position = {}, 0 162 while position + 46 <= len(directory) and directory[position:position + 4] == b"PK\x01\x02": 163 method = struct.unpack("<H", directory[position + 10:position + 12])[0] 164 crc = struct.unpack("<I", directory[position + 16:position + 20])[0] 165 compressed, uncompressed = struct.unpack("<II", directory[position + 20:position + 28]) 166 name_length, extra_length, comment_length = struct.unpack("<HHH", directory[position + 28:position + 34]) 167 offset = struct.unpack("<I", directory[position + 42:position + 46])[0] 168 name = directory[position + 46:position + 46 + name_length].decode("utf-8", errors="replace") 169 extra = directory[position + 46 + name_length:position + 46 + name_length + extra_length] 170 uncompressed, compressed, offset = _read_zip64_extra(extra, uncompressed, compressed, offset) 171 index[name] = {"method": method, "crc": crc, "compressed": compressed, "offset": offset} 172 position += 46 + name_length + extra_length + comment_length 173 return index 174 175 176def _download_member(url: str, name: str, entry: Dict[str, int], output_path: str, expected_sha: Optional[str]) -> None: 177 """Read one member out of a remote zip and write it, checking its crc32 and its sha256.""" 178 from tqdm import tqdm 179 180 # The local header repeats the name and carries its own extra field, so the data of the 181 # member starts behind a header whose length only the header itself gives. 182 header = _read_range(url, entry["offset"], entry["offset"] + 29) 183 if header[:4] != b"PK\x03\x04": 184 raise RuntimeError(f"The member '{name}' does not start with a local header.") 185 name_length, extra_length = struct.unpack("<HH", header[26:30]) 186 start = entry["offset"] + 30 + name_length + extra_length 187 188 if entry["method"] not in (0, 8): 189 raise RuntimeError(f"The member '{name}' uses the unsupported compression method {entry['method']}.") 190 decompressor = zlib.decompressobj(-zlib.MAX_WBITS) if entry["method"] == 8 else None 191 192 crc, digest = 0, hashlib.sha256() 193 temporary_path = f"{output_path}.incomplete" 194 os.makedirs(os.path.dirname(output_path), exist_ok=True) 195 196 headers = {"Range": f"bytes={start}-{start + entry['compressed'] - 1}"} 197 with requests.get(url, headers=headers, stream=True) as response: 198 response.raise_for_status() 199 description = f"Download {name}" 200 with open(temporary_path, "wb") as f: 201 with tqdm(total=entry["compressed"], unit="B", unit_scale=True, desc=description) as progress: 202 for chunk in response.iter_content(chunk_size=1 << 20): 203 progress.update(len(chunk)) 204 block = decompressor.decompress(chunk) if decompressor is not None else chunk 205 crc = zlib.crc32(block, crc) 206 digest.update(block) 207 f.write(block) 208 if decompressor is not None: 209 block = decompressor.flush() 210 crc = zlib.crc32(block, crc) 211 digest.update(block) 212 f.write(block) 213 214 if crc != entry["crc"]: 215 os.remove(temporary_path) 216 raise RuntimeError(f"The member '{name}' has the crc32 {crc}, but the zip lists {entry['crc']}.") 217 if expected_sha is not None and digest.hexdigest() != expected_sha: 218 os.remove(temporary_path) 219 raise RuntimeError(f"The member '{name}' has the sha256 {digest.hexdigest()}, expected {expected_sha}.") 220 os.replace(temporary_path, output_path) 221 222 223def _download_bundle(url: str, bundle_dir: str, sample: str, download: bool, with_stains: bool) -> str: 224 """Download the members that this loader reads, and skip the rest of the bundle.""" 225 if os.path.exists(bundle_dir): 226 return bundle_dir 227 if not download: 228 raise RuntimeError(f"Cannot find the data at {bundle_dir}, but download was set to False") 229 230 index = _remote_zip_index(url) 231 channels = natsorted(n for n in index if n.startswith("morphology_focus/") and n.endswith(".ome.tif")) 232 if not channels: 233 raise RuntimeError(f"The bundle for '{sample}' holds no morphology channel.") 234 if not with_stains: 235 channels = channels[:1] 236 237 names = ["experiment.xenium", "cells.zarr.zip"] + channels 238 missing = [n for n in names if n not in index] 239 if missing: 240 raise RuntimeError(f"The bundle for '{sample}' is missing {missing}.") 241 242 temporary_dir = f"{bundle_dir}.tmp" 243 if os.path.exists(temporary_dir): 244 shutil.rmtree(temporary_dir) 245 for name in names: 246 _download_member(url, name, index[name], os.path.join(temporary_dir, name), CHECKSUMS[sample].get(name)) 247 248 os.replace(temporary_dir, bundle_dir) 249 return bundle_dir 250 251 252def _channel_paths(bundle_dir: str) -> List[str]: 253 """Return the morphology channels, DAPI first. Both naming schemes sort DAPI to the front.""" 254 paths = natsorted(glob(os.path.join(bundle_dir, "morphology_focus", "*.ome.tif"))) 255 if not paths: 256 raise RuntimeError(f"Could not find any morphology channel in {bundle_dir}.") 257 return paths 258 259 260@contextmanager 261def _open_image(path: str): 262 """Open one morphology channel at full resolution, without reading it into memory. 263 264 The channels form a multi file OME pyramid, which the OME reader of tifffile rejects. 265 Reading the file on its own gives the pyramid of this channel alone. 266 """ 267 import zarr 268 import tifffile 269 270 with tifffile.TiffFile(path, is_ome=False) as tif: 271 store = tif.series[0].levels[0].aszarr() 272 try: 273 yield zarr.open(store, mode="r")["0"] 274 finally: 275 store.close() 276 277 278def _copy_blockwise(source, target, channel: Optional[int] = None) -> int: 279 """Copy a large 2d array block by block, to keep the memory use bounded. 280 281 The target must be indexed in one step, because indexing an h5 dataset twice writes 282 into a copy of the data rather than into the file. 283 284 Returns the largest value that was copied, which gives the instance count of a mask 285 without a second pass over the whole array. 286 """ 287 height, width = source.shape[-2:] 288 largest = 0 289 for y in range(0, height, BLOCK): 290 for x in range(0, width, BLOCK): 291 box = (slice(y, min(y + BLOCK, height)), slice(x, min(x + BLOCK, width))) 292 block = source[box] 293 if channel is None: 294 target[box] = block 295 else: 296 target[(channel,) + box] = block 297 largest = max(largest, int(block.max())) 298 return largest 299 300 301def _create_h5(bundle_dir: str, output_path: str, sample: str, with_stains: bool) -> str: 302 import h5py 303 import zarr 304 from tqdm import tqdm 305 306 if os.path.exists(output_path): 307 return output_path 308 309 with open(os.path.join(bundle_dir, "experiment.xenium")) as f: 310 manifest = json.load(f) 311 312 masks_zip = os.path.join(bundle_dir, "cells.zarr.zip") 313 masks_dir = os.path.join(bundle_dir, "cells.zarr") 314 if not os.path.exists(masks_dir): 315 with zipfile.ZipFile(masks_zip) as archive: 316 archive.extractall(masks_dir) 317 masks = zarr.open(masks_dir, mode="r")["masks"] 318 319 channel_paths = _channel_paths(bundle_dir) 320 with _open_image(channel_paths[0]) as dapi: 321 shape = dapi.shape 322 for name, key in LABEL_CHANNELS.items(): 323 if masks[key].shape != shape: 324 raise RuntimeError( 325 f"The {name} mask of '{sample}' has the shape {masks[key].shape}, " 326 f"but its DAPI image has the shape {shape}." 327 ) 328 329 temporary_path = f"{output_path}.tmp" 330 with h5py.File(temporary_path, "w") as f: 331 f.attrs["sample"] = sample 332 f.attrs["tissue"] = TISSUES[sample] 333 f.attrs["axes"] = "yx" 334 f.attrs["pixel_size"] = manifest["pixel_size"] 335 f.attrs["xoa_version"] = manifest["analysis_sw_version"] 336 f.attrs["segmentation_stain"] = manifest.get("segmentation_stain", "none") 337 f.attrs["nuc_expansion_fraction"] = manifest.get("segmented_cell_nuc_expansion_frac", float("nan")) 338 f.attrs["num_cells"] = manifest["num_cells"] 339 f.attrs["channel_files"] = [os.path.basename(p) for p in channel_paths] 340 341 chunks = (min(1024, shape[0]), min(1024, shape[1])) 342 raw = f.create_dataset("raw/dapi", shape=shape, dtype="uint16", chunks=chunks, compression="gzip") 343 raw.attrs["pixel_size"] = manifest["pixel_size"] 344 with _open_image(channel_paths[0]) as dapi: 345 _copy_blockwise(dapi, raw) 346 347 if with_stains: 348 stack_shape = (len(channel_paths),) + shape 349 stack = f.create_dataset( 350 "raw/stack", shape=stack_shape, dtype="uint16", chunks=(1,) + chunks, compression="gzip" 351 ) 352 stack.attrs["pixel_size"] = manifest["pixel_size"] 353 for channel, channel_path in enumerate(tqdm(channel_paths, desc=f"Copy channels of '{sample}'")): 354 with _open_image(channel_path) as image: 355 if image.shape != shape: 356 raise RuntimeError( 357 f"The channel {os.path.basename(channel_path)} of '{sample}' has the shape " 358 f"{image.shape}, but its DAPI image has the shape {shape}." 359 ) 360 _copy_blockwise(image, stack, channel=channel) 361 362 for name, key in LABEL_CHANNELS.items(): 363 labels = f.create_dataset( 364 f"labels/{name}", shape=shape, dtype="uint32", chunks=chunks, compression="gzip" 365 ) 366 labels.attrs["pixel_size"] = manifest["pixel_size"] 367 labels.attrs["num_instances"] = _copy_blockwise(masks[key], labels) 368 369 os.replace(temporary_path, output_path) 370 shutil.rmtree(masks_dir, ignore_errors=True) 371 return output_path 372 373 374def get_xenium_data( 375 path: Union[os.PathLike, str], 376 sample: str, 377 download: bool = False, 378 with_stains: bool = True, 379) -> str: 380 """Download one Xenium sample. 381 382 The source is the Xenium Explorer subset of the output bundle, which holds the masks and 383 the morphology images but not the transcripts. This function does not fetch the whole 384 archive. It reads the central directory of the remote zip, then reads only the members 385 that it needs, which cuts the download by about four times without the stains. 386 387 Args: 388 path: Filepath to a folder where the downloaded data will be saved. 389 sample: The sample to use. See `URLS` for the available samples. 390 download: Whether to download the data if it is not present. 391 with_stains: Whether to also store the boundary and interior stains next to DAPI. 392 The cell masks rest on those stains, so a model that predicts cells needs them. 393 394 Returns: 395 The filepath to the h5 file of the sample. 396 """ 397 if sample not in URLS: 398 raise ValueError(f"'{sample}' is not a valid sample. Choose from {list(URLS)}.") 399 400 output_path = os.path.join(path, "preprocessed", f"{sample}.h5") 401 if os.path.exists(output_path): 402 return output_path 403 404 os.makedirs(os.path.join(path, "preprocessed"), exist_ok=True) 405 bundle_dir = _download_bundle(URLS[sample], os.path.join(path, "bundles", sample), sample, download, with_stains) 406 _create_h5(bundle_dir, output_path, sample, with_stains) 407 shutil.rmtree(bundle_dir, ignore_errors=True) 408 409 return output_path 410 411 412def get_xenium_paths( 413 path: Union[os.PathLike, str], 414 sample: Optional[Union[str, Sequence[str]]] = None, 415 download: bool = False, 416 with_stains: bool = True, 417) -> List[str]: 418 """Get paths to the Xenium data. 419 420 Args: 421 path: Filepath to a folder where the downloaded data will be saved. 422 sample: The sample or samples to use. Defaults to all of them. 423 download: Whether to download the data if it is not present. 424 with_stains: Whether to also store the boundary and interior stains next to DAPI. 425 426 Returns: 427 List of filepaths for the h5 data. 428 """ 429 if sample is None: 430 samples = list(URLS) 431 else: 432 samples = [sample] if isinstance(sample, str) else list(sample) 433 434 return [get_xenium_data(path, name, download, with_stains) for name in samples] 435 436 437def get_xenium_dataset( 438 path: Union[os.PathLike, str], 439 patch_shape: Tuple[int, int], 440 sample: Optional[Union[str, Sequence[str]]] = None, 441 label_channel: Literal["nuclei", "cells"] = "nuclei", 442 raw_channel: Literal["dapi", "stack"] = "dapi", 443 offsets: Optional[List[List[int]]] = None, 444 boundaries: bool = False, 445 binary: bool = False, 446 download: bool = False, 447 **kwargs, 448) -> Dataset: 449 """Get the Xenium dataset for cell and nucleus segmentation. 450 451 Args: 452 path: Filepath to a folder where the downloaded data will be saved. 453 patch_shape: The 2D patch shape to use for training. 454 sample: The sample or samples to use. Defaults to all of them. 455 label_channel: The masks to use as target. Either 'nuclei' or 'cells'. 456 raw_channel: The images to use as input. Either 'dapi' for the nuclear stain alone, 457 or 'stack' for all morphology channels. 458 offsets: Offset values for affinity computation used as target. 459 boundaries: Whether to compute boundaries as the target. 460 binary: Whether to use a binary segmentation target. 461 download: Whether to download the data if it is not present. 462 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`. 463 464 Returns: 465 The segmentation dataset. 466 """ 467 if len(patch_shape) != 2: 468 raise ValueError(f"The Xenium patch shape must be two-dimensional, got {patch_shape}.") 469 if label_channel not in LABEL_CHANNELS: 470 raise ValueError(f"'{label_channel}' is not a valid label channel. Choose from {list(LABEL_CHANNELS)}.") 471 if raw_channel not in ("dapi", "stack"): 472 raise ValueError(f"'{raw_channel}' is not a valid raw channel. Choose from ['dapi', 'stack'].") 473 474 volume_paths = get_xenium_paths(path, sample, download, with_stains=raw_channel == "stack") 475 476 kwargs = util.update_kwargs(kwargs, "with_channels", raw_channel == "stack") 477 kwargs, _ = util.add_instance_label_transform( 478 kwargs, add_binary_target=True, offsets=offsets, boundaries=boundaries, binary=binary, 479 ) 480 kwargs = util.ensure_transforms(ndim=2, **kwargs) 481 482 return torch_em.default_segmentation_dataset( 483 raw_paths=volume_paths, 484 raw_key=f"raw/{raw_channel}", 485 label_paths=volume_paths, 486 label_key=f"labels/{label_channel}", 487 patch_shape=patch_shape, 488 ndim=2, 489 **kwargs, 490 ) 491 492 493def get_xenium_loader( 494 path: Union[os.PathLike, str], 495 batch_size: int, 496 patch_shape: Tuple[int, int], 497 sample: Optional[Union[str, Sequence[str]]] = None, 498 label_channel: Literal["nuclei", "cells"] = "nuclei", 499 raw_channel: Literal["dapi", "stack"] = "dapi", 500 offsets: Optional[List[List[int]]] = None, 501 boundaries: bool = False, 502 binary: bool = False, 503 download: bool = False, 504 **kwargs, 505) -> DataLoader: 506 """Get the Xenium dataloader for cell and nucleus segmentation. 507 508 Args: 509 path: Filepath to a folder where the downloaded data will be saved. 510 batch_size: The batch size for training. 511 patch_shape: The 2D patch shape to use for training. 512 sample: The sample or samples to use. Defaults to all of them. 513 label_channel: The masks to use as target. Either 'nuclei' or 'cells'. 514 raw_channel: The images to use as input. Either 'dapi' or 'stack'. 515 offsets: Offset values for affinity computation used as target. 516 boundaries: Whether to compute boundaries as the target. 517 binary: Whether to use a binary segmentation target. 518 download: Whether to download the data if it is not present. 519 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or the PyTorch DataLoader. 520 521 Returns: 522 The DataLoader. 523 """ 524 ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs) 525 dataset = get_xenium_dataset( 526 path=path, 527 patch_shape=patch_shape, 528 sample=sample, 529 label_channel=label_channel, 530 raw_channel=raw_channel, 531 offsets=offsets, 532 boundaries=boundaries, 533 binary=binary, 534 download=download, 535 **ds_kwargs, 536 ) 537 return torch_em.get_data_loader(dataset, batch_size=batch_size, **loader_kwargs)
375def get_xenium_data( 376 path: Union[os.PathLike, str], 377 sample: str, 378 download: bool = False, 379 with_stains: bool = True, 380) -> str: 381 """Download one Xenium sample. 382 383 The source is the Xenium Explorer subset of the output bundle, which holds the masks and 384 the morphology images but not the transcripts. This function does not fetch the whole 385 archive. It reads the central directory of the remote zip, then reads only the members 386 that it needs, which cuts the download by about four times without the stains. 387 388 Args: 389 path: Filepath to a folder where the downloaded data will be saved. 390 sample: The sample to use. See `URLS` for the available samples. 391 download: Whether to download the data if it is not present. 392 with_stains: Whether to also store the boundary and interior stains next to DAPI. 393 The cell masks rest on those stains, so a model that predicts cells needs them. 394 395 Returns: 396 The filepath to the h5 file of the sample. 397 """ 398 if sample not in URLS: 399 raise ValueError(f"'{sample}' is not a valid sample. Choose from {list(URLS)}.") 400 401 output_path = os.path.join(path, "preprocessed", f"{sample}.h5") 402 if os.path.exists(output_path): 403 return output_path 404 405 os.makedirs(os.path.join(path, "preprocessed"), exist_ok=True) 406 bundle_dir = _download_bundle(URLS[sample], os.path.join(path, "bundles", sample), sample, download, with_stains) 407 _create_h5(bundle_dir, output_path, sample, with_stains) 408 shutil.rmtree(bundle_dir, ignore_errors=True) 409 410 return output_path
Download one Xenium sample.
The source is the Xenium Explorer subset of the output bundle, which holds the masks and the morphology images but not the transcripts. This function does not fetch the whole archive. It reads the central directory of the remote zip, then reads only the members that it needs, which cuts the download by about four times without the stains.
Arguments:
- path: Filepath to a folder where the downloaded data will be saved.
- sample: The sample to use. See
URLSfor the available samples. - download: Whether to download the data if it is not present.
- with_stains: Whether to also store the boundary and interior stains next to DAPI. The cell masks rest on those stains, so a model that predicts cells needs them.
Returns:
The filepath to the h5 file of the sample.
413def get_xenium_paths( 414 path: Union[os.PathLike, str], 415 sample: Optional[Union[str, Sequence[str]]] = None, 416 download: bool = False, 417 with_stains: bool = True, 418) -> List[str]: 419 """Get paths to the Xenium data. 420 421 Args: 422 path: Filepath to a folder where the downloaded data will be saved. 423 sample: The sample or samples to use. Defaults to all of them. 424 download: Whether to download the data if it is not present. 425 with_stains: Whether to also store the boundary and interior stains next to DAPI. 426 427 Returns: 428 List of filepaths for the h5 data. 429 """ 430 if sample is None: 431 samples = list(URLS) 432 else: 433 samples = [sample] if isinstance(sample, str) else list(sample) 434 435 return [get_xenium_data(path, name, download, with_stains) for name in samples]
Get paths to the Xenium data.
Arguments:
- path: Filepath to a folder where the downloaded data will be saved.
- sample: The sample or samples to use. Defaults to all of them.
- download: Whether to download the data if it is not present.
- with_stains: Whether to also store the boundary and interior stains next to DAPI.
Returns:
List of filepaths for the h5 data.
438def get_xenium_dataset( 439 path: Union[os.PathLike, str], 440 patch_shape: Tuple[int, int], 441 sample: Optional[Union[str, Sequence[str]]] = None, 442 label_channel: Literal["nuclei", "cells"] = "nuclei", 443 raw_channel: Literal["dapi", "stack"] = "dapi", 444 offsets: Optional[List[List[int]]] = None, 445 boundaries: bool = False, 446 binary: bool = False, 447 download: bool = False, 448 **kwargs, 449) -> Dataset: 450 """Get the Xenium dataset for cell and nucleus segmentation. 451 452 Args: 453 path: Filepath to a folder where the downloaded data will be saved. 454 patch_shape: The 2D patch shape to use for training. 455 sample: The sample or samples to use. Defaults to all of them. 456 label_channel: The masks to use as target. Either 'nuclei' or 'cells'. 457 raw_channel: The images to use as input. Either 'dapi' for the nuclear stain alone, 458 or 'stack' for all morphology channels. 459 offsets: Offset values for affinity computation used as target. 460 boundaries: Whether to compute boundaries as the target. 461 binary: Whether to use a binary segmentation target. 462 download: Whether to download the data if it is not present. 463 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`. 464 465 Returns: 466 The segmentation dataset. 467 """ 468 if len(patch_shape) != 2: 469 raise ValueError(f"The Xenium patch shape must be two-dimensional, got {patch_shape}.") 470 if label_channel not in LABEL_CHANNELS: 471 raise ValueError(f"'{label_channel}' is not a valid label channel. Choose from {list(LABEL_CHANNELS)}.") 472 if raw_channel not in ("dapi", "stack"): 473 raise ValueError(f"'{raw_channel}' is not a valid raw channel. Choose from ['dapi', 'stack'].") 474 475 volume_paths = get_xenium_paths(path, sample, download, with_stains=raw_channel == "stack") 476 477 kwargs = util.update_kwargs(kwargs, "with_channels", raw_channel == "stack") 478 kwargs, _ = util.add_instance_label_transform( 479 kwargs, add_binary_target=True, offsets=offsets, boundaries=boundaries, binary=binary, 480 ) 481 kwargs = util.ensure_transforms(ndim=2, **kwargs) 482 483 return torch_em.default_segmentation_dataset( 484 raw_paths=volume_paths, 485 raw_key=f"raw/{raw_channel}", 486 label_paths=volume_paths, 487 label_key=f"labels/{label_channel}", 488 patch_shape=patch_shape, 489 ndim=2, 490 **kwargs, 491 )
Get the Xenium dataset for cell and nucleus segmentation.
Arguments:
- path: Filepath to a folder where the downloaded data will be saved.
- patch_shape: The 2D patch shape to use for training.
- sample: The sample or samples to use. Defaults to all of them.
- label_channel: The masks to use as target. Either 'nuclei' or 'cells'.
- raw_channel: The images to use as input. Either 'dapi' for the nuclear stain alone, or 'stack' for all morphology channels.
- offsets: Offset values for affinity computation used as target.
- boundaries: Whether to compute boundaries as the target.
- binary: Whether to use a binary segmentation target.
- download: Whether to download the data if it is not present.
- kwargs: Additional keyword arguments for
torch_em.default_segmentation_dataset.
Returns:
The segmentation dataset.
494def get_xenium_loader( 495 path: Union[os.PathLike, str], 496 batch_size: int, 497 patch_shape: Tuple[int, int], 498 sample: Optional[Union[str, Sequence[str]]] = None, 499 label_channel: Literal["nuclei", "cells"] = "nuclei", 500 raw_channel: Literal["dapi", "stack"] = "dapi", 501 offsets: Optional[List[List[int]]] = None, 502 boundaries: bool = False, 503 binary: bool = False, 504 download: bool = False, 505 **kwargs, 506) -> DataLoader: 507 """Get the Xenium dataloader for cell and nucleus segmentation. 508 509 Args: 510 path: Filepath to a folder where the downloaded data will be saved. 511 batch_size: The batch size for training. 512 patch_shape: The 2D patch shape to use for training. 513 sample: The sample or samples to use. Defaults to all of them. 514 label_channel: The masks to use as target. Either 'nuclei' or 'cells'. 515 raw_channel: The images to use as input. Either 'dapi' or 'stack'. 516 offsets: Offset values for affinity computation used as target. 517 boundaries: Whether to compute boundaries as the target. 518 binary: Whether to use a binary segmentation target. 519 download: Whether to download the data if it is not present. 520 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or the PyTorch DataLoader. 521 522 Returns: 523 The DataLoader. 524 """ 525 ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs) 526 dataset = get_xenium_dataset( 527 path=path, 528 patch_shape=patch_shape, 529 sample=sample, 530 label_channel=label_channel, 531 raw_channel=raw_channel, 532 offsets=offsets, 533 boundaries=boundaries, 534 binary=binary, 535 download=download, 536 **ds_kwargs, 537 ) 538 return torch_em.get_data_loader(dataset, batch_size=batch_size, **loader_kwargs)
Get the Xenium dataloader for cell and nucleus segmentation.
Arguments:
- path: Filepath to a folder where the downloaded data will be saved.
- batch_size: The batch size for training.
- patch_shape: The 2D patch shape to use for training.
- sample: The sample or samples to use. Defaults to all of them.
- label_channel: The masks to use as target. Either 'nuclei' or 'cells'.
- raw_channel: The images to use as input. Either 'dapi' or 'stack'.
- offsets: Offset values for affinity computation used as target.
- boundaries: Whether to compute boundaries as the target.
- binary: Whether to use a binary segmentation target.
- download: Whether to download the data if it is not present.
- kwargs: Additional keyword arguments for
torch_em.default_segmentation_datasetor the PyTorch DataLoader.
Returns:
The DataLoader.