hs2p 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hs2p-1.0.0/PKG-INFO +122 -0
- hs2p-1.0.0/README.md +79 -0
- hs2p-1.0.0/hs2p/__init__.py +1 -0
- hs2p-1.0.0/hs2p/configs/__init__.py +17 -0
- hs2p-1.0.0/hs2p/sampling.py +349 -0
- hs2p-1.0.0/hs2p/tiling.py +238 -0
- hs2p-1.0.0/hs2p/utils/__init__.py +9 -0
- hs2p-1.0.0/hs2p/utils/config.py +75 -0
- hs2p-1.0.0/hs2p/utils/log_utils.py +91 -0
- hs2p-1.0.0/hs2p/utils/utils.py +179 -0
- hs2p-1.0.0/hs2p/wsi/__init__.py +581 -0
- hs2p-1.0.0/hs2p/wsi/utils.py +111 -0
- hs2p-1.0.0/hs2p/wsi/wsi.py +1007 -0
- hs2p-1.0.0/hs2p.egg-info/PKG-INFO +122 -0
- hs2p-1.0.0/hs2p.egg-info/SOURCES.txt +21 -0
- hs2p-1.0.0/hs2p.egg-info/dependency_links.txt +1 -0
- hs2p-1.0.0/hs2p.egg-info/not-zip-safe +1 -0
- hs2p-1.0.0/hs2p.egg-info/requires.txt +17 -0
- hs2p-1.0.0/hs2p.egg-info/top_level.txt +1 -0
- hs2p-1.0.0/pyproject.toml +42 -0
- hs2p-1.0.0/setup.cfg +50 -0
- hs2p-1.0.0/setup.py +15 -0
hs2p-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hs2p
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Embedding of whole slide images with Foundation Models
|
|
5
|
+
Home-page: https://github.com/clemsgrs/hs2p
|
|
6
|
+
Author: Clément Grisi
|
|
7
|
+
Author-email: clement.grisi@radboudumc.nl
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/clemsgrs/hs2p/issues
|
|
9
|
+
Platform: unix
|
|
10
|
+
Platform: linux
|
|
11
|
+
Platform: osx
|
|
12
|
+
Platform: cygwin
|
|
13
|
+
Platform: win32
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: omegaconf
|
|
23
|
+
Requires-Dist: numpy<2
|
|
24
|
+
Requires-Dist: pandas
|
|
25
|
+
Requires-Dist: h5py
|
|
26
|
+
Requires-Dist: tqdm
|
|
27
|
+
Requires-Dist: wandb
|
|
28
|
+
Requires-Dist: pillow
|
|
29
|
+
Requires-Dist: matplotlib
|
|
30
|
+
Requires-Dist: opencv-python
|
|
31
|
+
Requires-Dist: wholeslidedata
|
|
32
|
+
Provides-Extra: testing
|
|
33
|
+
Requires-Dist: pytest>=6.0; extra == "testing"
|
|
34
|
+
Requires-Dist: pytest-cov>=2.0; extra == "testing"
|
|
35
|
+
Requires-Dist: mypy>=0.910; extra == "testing"
|
|
36
|
+
Requires-Dist: flake8>=3.9; extra == "testing"
|
|
37
|
+
Requires-Dist: tox>=3.24; extra == "testing"
|
|
38
|
+
Dynamic: author-email
|
|
39
|
+
Dynamic: description
|
|
40
|
+
Dynamic: description-content-type
|
|
41
|
+
Dynamic: home-page
|
|
42
|
+
Dynamic: project-url
|
|
43
|
+
|
|
44
|
+
<h1 align="center">Histopathology Slide Pre-processing Pipeline</h1>
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/hs2p/)
|
|
47
|
+
[](https://hub.docker.com/r/waticlems/hs2p)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
HS2P is an open-source project largely based on [CLAM](https://github.com/mahmoodlab/CLAM) tissue segmentation and patching code.
|
|
51
|
+
|
|
52
|
+
<p>
|
|
53
|
+
<a href="https://github.com/psf/black"><img alt="empty" src=https://img.shields.io/badge/code%20style-black-000000.svg></a>
|
|
54
|
+
<a href="https://github.com/PyCQA/pylint"><img alt="empty" src=https://img.shields.io/github/stars/clemsgrs/hs2p?style=social></a>
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
## 🛠️ Installation
|
|
58
|
+
|
|
59
|
+
System requirements: Linux-based OS (e.g., Ubuntu 22.04) with Python 3.11+ and Docker installed.
|
|
60
|
+
|
|
61
|
+
We recommend running the script inside a container using the latest `hs2p` image from Docker Hub:
|
|
62
|
+
|
|
63
|
+
```shell
|
|
64
|
+
docker pull waticlems/hs2p:latest
|
|
65
|
+
docker run --rm -it \
|
|
66
|
+
-v /path/to/your/data:/data \
|
|
67
|
+
waticlems/hs2p:latest
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Replace `/path/to/your/data` with your local data directory.
|
|
71
|
+
|
|
72
|
+
Alternatively, you can install `hs2p` via pip:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
pip install hs2p
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Patch Extraction: Step-by-step guide
|
|
79
|
+
|
|
80
|
+
<img src="illustrations/extraction_illu.png" width="1000px" align="center" />
|
|
81
|
+
|
|
82
|
+
1. Create a `.csv` file containing paths to the desired slides. Optionally, you can provide paths to pre-computed tissue masks under the 'mask_path' column
|
|
83
|
+
|
|
84
|
+
```csv
|
|
85
|
+
wsi_path,mask_path
|
|
86
|
+
/path/to/slide1.tif,/path/to/mask1.tif
|
|
87
|
+
/path/to/slide2.tif,/path/to/mask2.tif
|
|
88
|
+
...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
2. Create a configuration file
|
|
92
|
+
|
|
93
|
+
A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
|
|
94
|
+
|
|
95
|
+
3. Kick off slide tiling
|
|
96
|
+
|
|
97
|
+
```shell
|
|
98
|
+
python3 -m hs2p.tiling --config-file </path/to/config.yaml>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Patch Sampling: Step-by-step guide
|
|
102
|
+
|
|
103
|
+
<img src="illustrations/sampling_illu.png" width="1000px" align="center" />
|
|
104
|
+
|
|
105
|
+
1. Create a `.csv` file containing paths to the desired slides & associated annotation masks:
|
|
106
|
+
|
|
107
|
+
```csv
|
|
108
|
+
wsi_path,mask_path
|
|
109
|
+
/path/to/slide1.tif,/path/to/mask1.tif
|
|
110
|
+
/path/to/slide2.tif,/path/to/mask2.tif
|
|
111
|
+
...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
2. Create a configuration file
|
|
115
|
+
|
|
116
|
+
A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
|
|
117
|
+
|
|
118
|
+
3. Kick off tile sampling
|
|
119
|
+
|
|
120
|
+
```shell
|
|
121
|
+
python3 -m hs2p.sampling --config-file </path/to/config.yaml>
|
|
122
|
+
```
|
hs2p-1.0.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<h1 align="center">Histopathology Slide Pre-processing Pipeline</h1>
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/hs2p/)
|
|
4
|
+
[](https://hub.docker.com/r/waticlems/hs2p)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
HS2P is an open-source project largely based on [CLAM](https://github.com/mahmoodlab/CLAM) tissue segmentation and patching code.
|
|
8
|
+
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://github.com/psf/black"><img alt="empty" src=https://img.shields.io/badge/code%20style-black-000000.svg></a>
|
|
11
|
+
<a href="https://github.com/PyCQA/pylint"><img alt="empty" src=https://img.shields.io/github/stars/clemsgrs/hs2p?style=social></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
## 🛠️ Installation
|
|
15
|
+
|
|
16
|
+
System requirements: Linux-based OS (e.g., Ubuntu 22.04) with Python 3.11+ and Docker installed.
|
|
17
|
+
|
|
18
|
+
We recommend running the script inside a container using the latest `hs2p` image from Docker Hub:
|
|
19
|
+
|
|
20
|
+
```shell
|
|
21
|
+
docker pull waticlems/hs2p:latest
|
|
22
|
+
docker run --rm -it \
|
|
23
|
+
-v /path/to/your/data:/data \
|
|
24
|
+
waticlems/hs2p:latest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Replace `/path/to/your/data` with your local data directory.
|
|
28
|
+
|
|
29
|
+
Alternatively, you can install `hs2p` via pip:
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
pip install hs2p
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Patch Extraction: Step-by-step guide
|
|
36
|
+
|
|
37
|
+
<img src="illustrations/extraction_illu.png" width="1000px" align="center" />
|
|
38
|
+
|
|
39
|
+
1. Create a `.csv` file containing paths to the desired slides. Optionally, you can provide paths to pre-computed tissue masks under the 'mask_path' column
|
|
40
|
+
|
|
41
|
+
```csv
|
|
42
|
+
wsi_path,mask_path
|
|
43
|
+
/path/to/slide1.tif,/path/to/mask1.tif
|
|
44
|
+
/path/to/slide2.tif,/path/to/mask2.tif
|
|
45
|
+
...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
2. Create a configuration file
|
|
49
|
+
|
|
50
|
+
A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
|
|
51
|
+
|
|
52
|
+
3. Kick off slide tiling
|
|
53
|
+
|
|
54
|
+
```shell
|
|
55
|
+
python3 -m hs2p.tiling --config-file </path/to/config.yaml>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Patch Sampling: Step-by-step guide
|
|
59
|
+
|
|
60
|
+
<img src="illustrations/sampling_illu.png" width="1000px" align="center" />
|
|
61
|
+
|
|
62
|
+
1. Create a `.csv` file containing paths to the desired slides & associated annotation masks:
|
|
63
|
+
|
|
64
|
+
```csv
|
|
65
|
+
wsi_path,mask_path
|
|
66
|
+
/path/to/slide1.tif,/path/to/mask1.tif
|
|
67
|
+
/path/to/slide2.tif,/path/to/mask2.tif
|
|
68
|
+
...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
2. Create a configuration file
|
|
72
|
+
|
|
73
|
+
A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
|
|
74
|
+
|
|
75
|
+
3. Kick off tile sampling
|
|
76
|
+
|
|
77
|
+
```shell
|
|
78
|
+
python3 -m hs2p.sampling --config-file </path/to/config.yaml>
|
|
79
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from omegaconf import OmegaConf
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_config(config_name: str):
|
|
7
|
+
config_filename = config_name + ".yaml"
|
|
8
|
+
return OmegaConf.load(pathlib.Path(__file__).parent.resolve() / config_filename)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
default_config = load_config("default")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_and_merge_config(config_name: str):
|
|
15
|
+
default_config = OmegaConf.create(default_config)
|
|
16
|
+
loaded_config = load_config(config_name)
|
|
17
|
+
return OmegaConf.merge(default_config, loaded_config)
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tqdm
|
|
3
|
+
import argparse
|
|
4
|
+
import traceback
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import seaborn as sns
|
|
8
|
+
import multiprocessing as mp
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from hs2p.utils import setup, load_csv, fix_random_seeds
|
|
12
|
+
from hs2p.wsi import extract_coordinates, filter_coordinates, sample_coordinates, save_coordinates, visualize_coordinates, overlay_mask_on_slide, SamplingParameters
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_args_parser(add_help: bool = True):
|
|
16
|
+
parser = argparse.ArgumentParser("hs2p", add_help=add_help)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--config-file", default="", metavar="FILE", help="path to config file"
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--skip-datetime", action="store_true", help="skip run id datetime prefix"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--output-dir",
|
|
25
|
+
type=str,
|
|
26
|
+
help="output directory to save logs and checkpoints",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"opts",
|
|
30
|
+
help="Modify config options at the end of the command using \"path.key=value\".",
|
|
31
|
+
default=None,
|
|
32
|
+
nargs=argparse.REMAINDER,
|
|
33
|
+
)
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def process_slide_wrapper(kwargs):
|
|
38
|
+
return process_slide(**kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def process_slide(
|
|
42
|
+
*,
|
|
43
|
+
wsi_path: Path,
|
|
44
|
+
mask_path: Path,
|
|
45
|
+
cfg,
|
|
46
|
+
mask_visualize_dir,
|
|
47
|
+
sampling_visualize_dir,
|
|
48
|
+
sampling_params: SamplingParameters,
|
|
49
|
+
disable_tqdm: bool = False,
|
|
50
|
+
num_workers: int = 4,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Process a single slide: sample tile coordinates and visualize if needed.
|
|
54
|
+
"""
|
|
55
|
+
wsi_name = wsi_path.stem.replace(" ", "_")
|
|
56
|
+
try:
|
|
57
|
+
|
|
58
|
+
if cfg.visualize and sampling_visualize_dir is not None:
|
|
59
|
+
preview_palette = np.zeros(shape=768, dtype=int)
|
|
60
|
+
if sampling_params.color_mapping is None:
|
|
61
|
+
ncat = len(sampling_params.pixel_mapping)
|
|
62
|
+
if ncat <= 10:
|
|
63
|
+
color_palette = sns.color_palette("tab10")[:ncat]
|
|
64
|
+
elif ncat <= 20:
|
|
65
|
+
color_palette = sns.color_palette("tab20")[:ncat]
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Implementation supports up to 20 categories (provided pixel_mapping has {ncat})"
|
|
69
|
+
)
|
|
70
|
+
color_mapping = {
|
|
71
|
+
k: tuple(255 * x for x in color_palette[i])
|
|
72
|
+
for i, k in enumerate(sampling_params.pixel_mapping.keys())
|
|
73
|
+
}
|
|
74
|
+
else:
|
|
75
|
+
color_mapping = sampling_params.color_mapping
|
|
76
|
+
p = [0] * 3 * len(color_mapping)
|
|
77
|
+
for k, v in sampling_params.pixel_mapping.items():
|
|
78
|
+
if color_mapping[k] is not None:
|
|
79
|
+
p[v * 3 : v * 3 + 3] = color_mapping[k]
|
|
80
|
+
n = len(p)
|
|
81
|
+
preview_palette[0:n] = np.array(p).astype(int)
|
|
82
|
+
else:
|
|
83
|
+
color_mapping = None
|
|
84
|
+
preview_palette = None
|
|
85
|
+
|
|
86
|
+
if not cfg.tiling.sampling_params.independant_sampling:
|
|
87
|
+
tissue_mask_visu_path = None
|
|
88
|
+
if cfg.visualize and mask_visualize_dir is not None:
|
|
89
|
+
tissue_mask_visu_path = Path(mask_visualize_dir, f"{wsi_name}-tissue.png")
|
|
90
|
+
coordinates, tile_level, resize_factor, tile_size_lv0 = extract_coordinates(
|
|
91
|
+
wsi_path=wsi_path,
|
|
92
|
+
mask_path=mask_path,
|
|
93
|
+
backend=cfg.tiling.backend,
|
|
94
|
+
tiling_params=cfg.tiling.params,
|
|
95
|
+
segment_params=cfg.tiling.seg_params,
|
|
96
|
+
filter_params=cfg.tiling.filter_params,
|
|
97
|
+
sampling_params=sampling_params,
|
|
98
|
+
mask_visu_path=tissue_mask_visu_path,
|
|
99
|
+
disable_tqdm=disable_tqdm,
|
|
100
|
+
num_workers=num_workers,
|
|
101
|
+
)
|
|
102
|
+
filtered_coordinates = filter_coordinates(
|
|
103
|
+
wsi_path=wsi_path,
|
|
104
|
+
mask_path=mask_path,
|
|
105
|
+
backend=cfg.tiling.backend,
|
|
106
|
+
coordinates=coordinates,
|
|
107
|
+
tile_level=tile_level,
|
|
108
|
+
segment_params=cfg.tiling.seg_params,
|
|
109
|
+
tiling_params=cfg.tiling.params,
|
|
110
|
+
sampling_params=sampling_params,
|
|
111
|
+
disable_tqdm=disable_tqdm,
|
|
112
|
+
) # a dict mapping annotation -> coordinates
|
|
113
|
+
for annotation, coordinates in filtered_coordinates.items():
|
|
114
|
+
if len(coordinates) == 0:
|
|
115
|
+
continue
|
|
116
|
+
coordinates_dir = Path(cfg.output_dir, "coordinates", annotation)
|
|
117
|
+
coordinates_dir.mkdir(exist_ok=True, parents=True)
|
|
118
|
+
coordinates_path = Path(coordinates_dir, f"{wsi_name}.npy")
|
|
119
|
+
save_coordinates(
|
|
120
|
+
coordinates=coordinates,
|
|
121
|
+
target_spacing=cfg.tiling.params.spacing,
|
|
122
|
+
tile_level=tile_level,
|
|
123
|
+
tile_size=cfg.tiling.params.tile_size,
|
|
124
|
+
resize_factor=resize_factor,
|
|
125
|
+
tile_size_lv0=tile_size_lv0,
|
|
126
|
+
save_path=coordinates_path,
|
|
127
|
+
)
|
|
128
|
+
if cfg.visualize and sampling_visualize_dir is not None:
|
|
129
|
+
visualize_coordinates(
|
|
130
|
+
wsi_path=wsi_path,
|
|
131
|
+
coordinates=coordinates,
|
|
132
|
+
tile_size_lv0=tile_size_lv0,
|
|
133
|
+
save_dir=sampling_visualize_dir,
|
|
134
|
+
downsample=cfg.tiling.visu_params.downsample,
|
|
135
|
+
backend=cfg.tiling.backend,
|
|
136
|
+
mask_path=mask_path,
|
|
137
|
+
annotation=annotation,
|
|
138
|
+
palette=preview_palette,
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
for annotation in sampling_params.pixel_mapping.keys():
|
|
142
|
+
if sampling_params.tissue_percentage[annotation] is None:
|
|
143
|
+
continue
|
|
144
|
+
annotation_mask_dir = mask_visualize_dir / annotation
|
|
145
|
+
annotation_mask_dir.mkdir(exist_ok=True, parents=True)
|
|
146
|
+
tissue_mask_visu_path = None
|
|
147
|
+
if cfg.visualize and mask_visualize_dir is not None:
|
|
148
|
+
tissue_mask_visu_path = Path(annotation_mask_dir, f"{wsi_name}.jpg")
|
|
149
|
+
coordinates, tile_level, resize_factor, tile_size_lv0 = sample_coordinates(
|
|
150
|
+
wsi_path=wsi_path,
|
|
151
|
+
mask_path=mask_path,
|
|
152
|
+
backend=cfg.tiling.backend,
|
|
153
|
+
tiling_params=cfg.tiling.params,
|
|
154
|
+
segment_params=cfg.tiling.seg_params,
|
|
155
|
+
filter_params=cfg.tiling.filter_params,
|
|
156
|
+
sampling_params=sampling_params,
|
|
157
|
+
annotation=annotation,
|
|
158
|
+
mask_visu_path=tissue_mask_visu_path,
|
|
159
|
+
disable_tqdm=disable_tqdm,
|
|
160
|
+
num_workers=num_workers,
|
|
161
|
+
)
|
|
162
|
+
if len(coordinates) == 0:
|
|
163
|
+
continue
|
|
164
|
+
coordinates_dir = Path(cfg.output_dir, "coordinates", annotation)
|
|
165
|
+
coordinates_dir.mkdir(exist_ok=True, parents=True)
|
|
166
|
+
coordinates_path = Path(coordinates_dir, f"{wsi_name}.npy")
|
|
167
|
+
save_coordinates(
|
|
168
|
+
coordinates=coordinates,
|
|
169
|
+
target_spacing=cfg.tiling.params.spacing,
|
|
170
|
+
tile_level=tile_level,
|
|
171
|
+
tile_size=cfg.tiling.params.tile_size,
|
|
172
|
+
resize_factor=resize_factor,
|
|
173
|
+
tile_size_lv0=tile_size_lv0,
|
|
174
|
+
save_path=coordinates_path,
|
|
175
|
+
)
|
|
176
|
+
if cfg.visualize and sampling_visualize_dir is not None:
|
|
177
|
+
visualize_coordinates(
|
|
178
|
+
wsi_path=wsi_path,
|
|
179
|
+
coordinates=coordinates,
|
|
180
|
+
tile_size_lv0=tile_size_lv0,
|
|
181
|
+
save_dir=sampling_visualize_dir,
|
|
182
|
+
downsample=cfg.tiling.visu_params.downsample,
|
|
183
|
+
backend=cfg.tiling.backend,
|
|
184
|
+
mask_path=mask_path,
|
|
185
|
+
annotation=annotation,
|
|
186
|
+
palette=preview_palette,
|
|
187
|
+
)
|
|
188
|
+
if cfg.visualize and mask_visualize_dir is not None:
|
|
189
|
+
mask_visu_path = Path(mask_visualize_dir, f"{wsi_name}.png")
|
|
190
|
+
overlay_mask = overlay_mask_on_slide(
|
|
191
|
+
wsi_path=wsi_path,
|
|
192
|
+
annotation_mask_path=mask_path,
|
|
193
|
+
downsample=cfg.tiling.visu_params.downsample,
|
|
194
|
+
palette=preview_palette,
|
|
195
|
+
pixel_mapping=sampling_params.pixel_mapping,
|
|
196
|
+
color_mapping=color_mapping,
|
|
197
|
+
)
|
|
198
|
+
overlay_mask.save(mask_visu_path)
|
|
199
|
+
return str(wsi_path), {"status": "success"}
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
return str(wsi_path), {
|
|
203
|
+
"status": "failed",
|
|
204
|
+
"error": str(e),
|
|
205
|
+
"traceback": str(traceback.format_exc()),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def main(args):
|
|
210
|
+
|
|
211
|
+
cfg = setup(args)
|
|
212
|
+
output_dir = Path(cfg.output_dir)
|
|
213
|
+
|
|
214
|
+
fix_random_seeds(cfg.seed)
|
|
215
|
+
|
|
216
|
+
wsi_paths, mask_paths = load_csv(cfg)
|
|
217
|
+
|
|
218
|
+
parallel_workers = min(mp.cpu_count(), cfg.speed.num_workers)
|
|
219
|
+
if "SLURM_JOB_CPUS_PER_NODE" in os.environ:
|
|
220
|
+
parallel_workers = min(
|
|
221
|
+
parallel_workers, int(os.environ["SLURM_JOB_CPUS_PER_NODE"])
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
process_list = output_dir / "process_list.csv"
|
|
225
|
+
if process_list.is_file() and cfg.resume:
|
|
226
|
+
process_df = pd.read_csv(process_list)
|
|
227
|
+
else:
|
|
228
|
+
data = {
|
|
229
|
+
"wsi_name": [p.stem for p in wsi_paths],
|
|
230
|
+
"wsi_path": [str(p) for p in wsi_paths],
|
|
231
|
+
"mask_path": [str(p) if p is not None else p for p in mask_paths],
|
|
232
|
+
"sampling_status": ["tbp"] * len(wsi_paths),
|
|
233
|
+
"error": [str(np.nan)] * len(wsi_paths),
|
|
234
|
+
"traceback": [str(np.nan)] * len(wsi_paths),
|
|
235
|
+
}
|
|
236
|
+
process_df = pd.DataFrame(data)
|
|
237
|
+
|
|
238
|
+
skip_sampling = process_df["sampling_status"].str.contains("success").all()
|
|
239
|
+
|
|
240
|
+
pixel_mapping = {k: v for e in cfg.tiling.sampling_params.pixel_mapping for k, v in e.items()}
|
|
241
|
+
tissue_percentage = {k: v for e in cfg.tiling.sampling_params.tissue_percentage for k, v in e.items()}
|
|
242
|
+
if "tissue" not in tissue_percentage:
|
|
243
|
+
tissue_percentage["tissue"] = cfg.tiling.params.min_tissue_percentage
|
|
244
|
+
if cfg.tiling.sampling_params.color_mapping is not None:
|
|
245
|
+
color_mapping = {k: v for e in cfg.tiling.sampling_params.color_mapping for k, v in e.items()}
|
|
246
|
+
else:
|
|
247
|
+
color_mapping = None
|
|
248
|
+
|
|
249
|
+
sampling_params = SamplingParameters(
|
|
250
|
+
pixel_mapping=pixel_mapping,
|
|
251
|
+
color_mapping=color_mapping,
|
|
252
|
+
tissue_percentage=tissue_percentage,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if not skip_sampling:
|
|
256
|
+
|
|
257
|
+
mask = process_df["sampling_status"] != "success"
|
|
258
|
+
process_stack = process_df[mask]
|
|
259
|
+
total = len(process_stack)
|
|
260
|
+
|
|
261
|
+
wsi_paths_to_process = [
|
|
262
|
+
Path(x) for x in process_stack.wsi_path.values.tolist()
|
|
263
|
+
]
|
|
264
|
+
mask_paths_to_process = [
|
|
265
|
+
Path(x) if x is not None else x
|
|
266
|
+
for x in process_stack.mask_path.values.tolist()
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
# setup directories for coordinates and visualization
|
|
270
|
+
coordinates_dir = output_dir / "coordinates"
|
|
271
|
+
coordinates_dir.mkdir(exist_ok=True, parents=True)
|
|
272
|
+
mask_visualize_dir = None
|
|
273
|
+
sampling_visualize_dir = None
|
|
274
|
+
if cfg.visualize:
|
|
275
|
+
visualize_dir = output_dir / "visualization"
|
|
276
|
+
mask_visualize_dir = Path(visualize_dir, "mask")
|
|
277
|
+
sampling_visualize_dir = Path(visualize_dir, "sampling")
|
|
278
|
+
mask_visualize_dir.mkdir(exist_ok=True, parents=True)
|
|
279
|
+
sampling_visualize_dir.mkdir(exist_ok=True, parents=True)
|
|
280
|
+
|
|
281
|
+
sampling_updates = {}
|
|
282
|
+
with mp.Pool(processes=parallel_workers) as pool:
|
|
283
|
+
args_list = [
|
|
284
|
+
{
|
|
285
|
+
"wsi_path": wsi_fp,
|
|
286
|
+
"mask_path": mask_fp,
|
|
287
|
+
"cfg": cfg,
|
|
288
|
+
"mask_visualize_dir": mask_visualize_dir,
|
|
289
|
+
"sampling_visualize_dir": sampling_visualize_dir,
|
|
290
|
+
"sampling_params": sampling_params,
|
|
291
|
+
"disable_tqdm": True,
|
|
292
|
+
"num_workers": parallel_workers,
|
|
293
|
+
}
|
|
294
|
+
for wsi_fp, mask_fp in zip(
|
|
295
|
+
wsi_paths_to_process, mask_paths_to_process
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
results = list(
|
|
299
|
+
tqdm.tqdm(
|
|
300
|
+
pool.imap(process_slide_wrapper, args_list),
|
|
301
|
+
total=total,
|
|
302
|
+
desc="Slide sampling",
|
|
303
|
+
unit="slide",
|
|
304
|
+
leave=True,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
for wsi_path, status_info in results:
|
|
308
|
+
sampling_updates[wsi_path] = status_info
|
|
309
|
+
|
|
310
|
+
for wsi_path, status_info in sampling_updates.items():
|
|
311
|
+
process_df.loc[
|
|
312
|
+
process_df["wsi_path"] == wsi_path, "sampling_status"
|
|
313
|
+
] = status_info["status"]
|
|
314
|
+
if "error" in status_info:
|
|
315
|
+
process_df.loc[
|
|
316
|
+
process_df["wsi_path"] == wsi_path, "error"
|
|
317
|
+
] = status_info["error"]
|
|
318
|
+
process_df.loc[
|
|
319
|
+
process_df["wsi_path"] == wsi_path, "traceback"
|
|
320
|
+
] = status_info["traceback"]
|
|
321
|
+
process_df.to_csv(process_list, index=False)
|
|
322
|
+
|
|
323
|
+
# summary logging
|
|
324
|
+
total_slides = len(process_df)
|
|
325
|
+
failed_sampling = process_df[process_df["sampling_status"] == "failed"]
|
|
326
|
+
print("=+=" * 10)
|
|
327
|
+
print(f"Total number of slides: {total_slides}")
|
|
328
|
+
print(f"Failed sampling: {len(failed_sampling)}")
|
|
329
|
+
for annotation, pct in tissue_percentage.items():
|
|
330
|
+
if pct is None:
|
|
331
|
+
continue
|
|
332
|
+
slides_with_tiles = [
|
|
333
|
+
str(p)
|
|
334
|
+
for p in wsi_paths
|
|
335
|
+
if Path(coordinates_dir, annotation, f"{p.stem}.npy").is_file()
|
|
336
|
+
]
|
|
337
|
+
no_tiles = process_df[~process_df["wsi_path"].isin(slides_with_tiles)]
|
|
338
|
+
print(f"No {annotation} tiles after sampling step: {len(no_tiles)}")
|
|
339
|
+
print("=+=" * 10)
|
|
340
|
+
|
|
341
|
+
else:
|
|
342
|
+
print("=+=" * 10)
|
|
343
|
+
print("All slides have been sampled. Skipping sampling step.")
|
|
344
|
+
print("=+=" * 10)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
args = get_args_parser(add_help=True).parse_args()
|
|
349
|
+
main(args)
|