diff --git a/CHANGELOG.md b/CHANGELOG.md index 06407b7..27c2b94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support for opening files using [fsspec](https://filesystem-spec.readthedocs.io/). + ## [0.18.3] - 2024-01-22 ### Fixed diff --git a/README.md b/README.md index f6215d5..23fe067 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,14 @@ Please note that this is an early release and the API is not frozen yet. Functio ```python from wsidicom import WsiDicom -slide = WsiDicom.open(path_to_folder) +slide = WsiDicom.open("path_to_folder") +``` + +***Load a WSI dataset from remote url.*** + +```python +from wsidicom import WsiDicom +slide = WsiDicom.open("s3://bucket/key", file_options={"s3": "anon": True}) ``` ***Or load a WSI dataset from opened streams.*** @@ -71,7 +78,7 @@ slide = WsiDicom.open(path_to_folder) ```python from wsidicom import WsiDicom -slide = WsiDicom.open([file_stream_1, file_stream_2, ... ]) +slide = WsiDicom.open_streams([file_stream_1, file_stream_2, ... ]) ``` ***Or load a WSI dataset from a DICOMDIR.*** @@ -79,7 +86,7 @@ slide = WsiDicom.open([file_stream_1, file_stream_2, ... ]) ```python from wsidicom import WsiDicom -slide = WsiDicom.open_dicomdir(path_to_dicom_dir) +slide = WsiDicom.open_dicomdir("path_to_dicom_dir") ``` ***Or load a WSI dataset from DICOMWeb.*** @@ -107,8 +114,8 @@ Alternatively, if you have already created an instance of `WsiDicomWebClient` like so: ```python -dw_client = DICOMwebClient(url) -client = WsiDicomWebClient(dw_client) +dicomweb_client = DICOMwebClient("url") +client = WsiDicomWebClient(dicomweb_client) ``` Then proceed to call `WsiDicom.open_web()` with this as in the first example. @@ -117,7 +124,7 @@ Then proceed to call `WsiDicom.open_web()` with this as in the first example. ```python from wsidicom import WsiDicom -with WsiDicom.open(path_to_folder) as slide: +with WsiDicom.open("path_to_folder") as slide: ... ``` @@ -194,7 +201,7 @@ The WsiDicom API is similar to OpenSlide, but with some important differences: Conversion between OpenSlide `location` and `level` parameters to WsiDicom can be performed: ```python -with WsiDicom.open(test_wsi) as wsi: +with WsiDicom.open("path_to_folder") as wsi: level = wsi.levels[openslide_level_index] x = openslide_x // 2**(level.level) y = openslide_y // 2**(level.level) @@ -206,7 +213,7 @@ with WsiDicom.open(test_wsi) as wsi: WsiDicom parses the DICOM metadata in the opened image into easy-to-use dataclasses, see `wsidicom\metadata`. ```python -with WsiDicom.open(path_to_folder) as wsi: +with WsiDicom.open("path_to_folder") as wsi: metadata = wsi.metadata ``` @@ -227,7 +234,7 @@ with the [VL Whole Slide Microscopy Image CIOD](https://dicom.innolitics.com/cio Note that not all DICOM attributes are represented in the defined metadata model. Instead the full ´pydicom´ Datasets can be accessed per level, for example: ```python -with WsiDicom.open(path_to_folder) as wsi: +with WsiDicom.open("path_to_folder") as wsi: wsi.levels.base_level.datasets[0] ``` @@ -294,7 +301,7 @@ The metadata can be exported to json: ```python from wsidicom.metadata.schema.json import WsiMetadataJsonSchema -with WsiDicom.open(path_to_folder) as wsi: +with WsiDicom.open("path_to_folder") as wsi: metadata = wsi.metadata schema = WsiMetadataJsonSchema() @@ -319,8 +326,8 @@ An opened WsiDicom instance can be saved to a new path using the save()-method. By default frames are copied as-is, i.e. without re-compression. ```python -with WsiDicom.open(path_to_folder) as slide: - slide.save(path_to_output) +with WsiDicom.open("path_to_folder") as slide: + slide.save("path_to_output") ``` The output folder must already exists. Be careful to specify a unique folder folder to avoid mixing files from different images. @@ -330,8 +337,8 @@ Optionally frames can be transcoded, either by a encoder setting or an encoder: ```python from wsidicom.codec import JpegSettings -with WsiDicom.open(path_to_folder) as slide: - slide.save(path_to_output, transcoding=JpegSettings()) +with WsiDicom.open("path_to_folder") as slide: + slide.save("path_to_output", transcoding=JpegSettings()) ``` ## Settings @@ -363,7 +370,7 @@ Codes that are defined in the 222-draft can be created using the create(source, ```python from wsidicom import WsiDicom -slide = WsiDicom.open(path_to_folder) +slide = WsiDicom.open("path_to_folder") ``` ***Create a point annotation at x=10.0, y=20.0 mm.*** @@ -414,7 +421,7 @@ annotations.save('path_to_dicom_dir/annotation.dcm') ***Reopen the slide and access the annotation instance.*** ```python -slide = WsiDicom.open(path_to_folder) +slide = WsiDicom.open("path_to_folder") annotations = slide.annotations ``` @@ -465,7 +472,7 @@ Labels and overviews are structured similarly to levels, but with somewhat diffe A Source is used to create WsiInstances, either from files (*WsiDicomFileSource*) or DICOMWeb (*WsiDicomWebSource*), and can be used to to Initiate a *WsiDicom* object. A source is easiest created with the open() and open_web() helper functions, e.g.: ```python -slide = WsiDicom.open(path_to_folder) +slide = WsiDicom.open("path_to_folder") ``` ## Code structure diff --git a/wsidicom/file/wsidicom_file_target.py b/wsidicom/file/wsidicom_file_target.py index 7be4abb..eda29be 100644 --- a/wsidicom/file/wsidicom_file_target.py +++ b/wsidicom/file/wsidicom_file_target.py @@ -98,7 +98,7 @@ def __init__( """ self._output_path = UPath(output_path) self._offset_table = offset_table - self._filepaths: List[Path] = [] + self._filepaths: List[UPath] = [] self._opened_files: List[WsiDicomReader] = [] self._file_options = file_options super().__init__( @@ -112,7 +112,7 @@ def __init__( ) @property - def filepaths(self) -> List[Path]: + def filepaths(self) -> List[UPath]: """Return filepaths for created files.""" return self._filepaths diff --git a/wsidicom/wsidicom.py b/wsidicom/wsidicom.py index ee52798..4e71047 100644 --- a/wsidicom/wsidicom.py +++ b/wsidicom/wsidicom.py @@ -95,7 +95,8 @@ def open( ---------- path: Union[str, Path, UPath, Iterable[Union[str, Path, UPath]]] Files to open. Can be a path for a single file, a list of paths for multiple - files, or a path to a folder containing files. + files, or a path to a folder containing files. Path can be local or an URL + supported by fsspec. file_options: Optional[Dict[str, Any]] = None Optional options for when opening files. @@ -116,7 +117,8 @@ def open_dicomdir( Parameters ---------- path: UPath - Path to DICOMDIR file or directory with a DICOMDIR file. + Path to DICOMDIR file or directory with a DICOMDIR file. Path can be local + or an URL supported by fsspec. file_options: Optional[Dict[str, Any]] = None Optional options for when opening files. @@ -215,7 +217,7 @@ def save( label: Optional[Union[Image, Union[str, Path, UPath]]] = None, transcoding: Optional[Union[EncoderSettings, Encoder]] = None, file_options: Optional[Dict[str, Any]] = None, - ) -> List[Path]: + ) -> List[UPath]: """ Save wsi as DICOM-files in path. Instances for the same pyramid level will be combined when possible to one file (e.g. not split @@ -227,7 +229,7 @@ def save( ---------- output_path: Union[str, Path, UPath] Output folder to write files to. Should preferably be an dedicated folder - for the wsi. + for the wsi. Path can be local or an URL supported by fsspec. uid_generator: Callable[..., UID] = pydicom.uid.generate_uid Function that can generate unique identifiers. workers: Optional[int] = None @@ -262,7 +264,7 @@ def save( Returns ------- - List[Path] + List[UPath] List of paths of created files. """ if workers is None: @@ -318,8 +320,9 @@ def is_ready_for_viewing( Parameters ---------- path: Union[str, Path, UPath, Iterable[Union[str, Path, UPath]]] - Files to open. Can be a path or stream for a single file, a list of paths or - streams for multiple files, or a path to a folder containing files. + Files to open. Can be a path for a single file, a list of paths for multiple + files, or a path to a folder containing files. Path can be local or an URL + supported by fsspec. file_options: Optional[Dict[str, Any]] = None Optional options for when opening files. @@ -341,8 +344,10 @@ def is_supported( Parameters ---------- - path: Union[str, Iterable[str], Path, Iterable[Path]] - Path to files to test. + path: Union[str, Path, UPath, Iterable[Union[str, Path, UPath]]] + Path to files to test. Path can be local or an URL supported by fsspec. + file_options: Optional[Dict[str, Any]] = None + Optional options for when opening files. Returns True if files in path have one level that can be read with WsiDicom.