wsi-tile-processor 0.1.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Federico Carollo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,439 @@
1
+ Metadata-Version: 2.4
2
+ Name: wsi-tile-processor
3
+ Version: 0.1.1
4
+ Summary: Tile-by-tile inference on Whole Slide Images (WSI) with pyramidal OME-TIFF output
5
+ Author: Federico Carollo
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/FedeCarollo/wsi-tile-processor
8
+ Project-URL: Repository, https://github.com/FedeCarollo/wsi-tile-processor
9
+ Project-URL: Issues, https://github.com/FedeCarollo/wsi-tile-processor/issues
10
+ Keywords: WSI,whole slide image,pathology,OME-TIFF,tiling,inference,histology
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
19
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy
24
+ Requires-Dist: tifffile
25
+ Requires-Dist: imagecodecs
26
+ Requires-Dist: openslide-python
27
+ Requires-Dist: scipy
28
+ Requires-Dist: Pillow
29
+ Requires-Dist: pyvips
30
+ Provides-Extra: full
31
+ Requires-Dist: zarr; extra == "full"
32
+ Requires-Dist: scikit-image; extra == "full"
33
+ Dynamic: license-file
34
+
35
+ # wsi-tile-processor
36
+
37
+ [![PyPI version](https://badge.fury.io/py/wsi-tile-processor.svg)](https://pypi.org/project/wsi-tile-processor/)
38
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
40
+
41
+ > Tile-by-tile inference on Whole Slide Images (WSI) with pyramidal OME-TIFF output.
42
+
43
+ `wsi-tile-processor` is a Python library for running any deep learning or image-processing model on whole slide images (WSI) in a memory-efficient, tiled manner, and saving the result as a multi-resolution pyramidal OME-TIFF ready for viewing in QuPath, OMERO, or any TIFF-compatible viewer.
44
+
45
+ **Key features:**
46
+
47
+ - **Two processors** — `FastWSIProcessor` (non-overlapping, fast) and `GaussianWSIProcessor` (overlapping tiles with smooth Gaussian blending, no seam artifacts)
48
+ - **Pluggable background filters** — swap or subclass `BackgroundFilter` to implement custom tissue/background detection
49
+ - **Pluggable tissue mask detectors** — coarse-level background skipping with `TissueMaskDetector`
50
+ - **Multi-backend slide reading** — `openslide`, `tifffile` (for OME-TIFF sub-IFD pyramids), or `auto`
51
+ - **Batch inference** — feed multiple tiles per forward pass
52
+ - **BigTIFF + JPEG pyramid output** via pyvips
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ **From PyPI (Recommended):**
59
+ ```bash
60
+ pip install wsi-tile-processor
61
+ ```
62
+
63
+ For full support (zarr-backed tifffile reads + scikit-image tissue detection):
64
+ ```bash
65
+ pip install wsi-tile-processor[full]
66
+ ```
67
+
68
+ **From GitHub (Latest development version):**
69
+ ```bash
70
+ pip install git+https://github.com/FedeCarollo/wsi-tile-processor.git
71
+ ```
72
+
73
+ ### System dependencies
74
+
75
+ #### OpenSlide
76
+
77
+ [OpenSlide](https://openslide.org/download/) must be installed on your system:
78
+
79
+ ```bash
80
+ # Ubuntu / Debian
81
+ sudo apt install openslide-tools
82
+
83
+ # macOS
84
+ brew install openslide
85
+ ```
86
+
87
+ #### libvips (for pyramid generation)
88
+
89
+ `pyvips` is a Python binding for [libvips](https://www.libvips.org/) and **does not bundle it** by default.
90
+ You have two options:
91
+
92
+ **Option A — system package (recommended for servers/HPC):**
93
+
94
+ ```bash
95
+ # Ubuntu / Debian
96
+ sudo apt install libvips
97
+
98
+ # macOS
99
+ brew install vips
100
+ ```
101
+
102
+ **Option B — bundled binary via pip (no system install needed):**
103
+
104
+ ```bash
105
+ pip install "pyvips[binary]"
106
+ ```
107
+
108
+ This pulls in `pyvips-binary`, which ships pre-compiled libvips binaries and runs in faster CFFI API mode. Useful for Docker containers or environments where you can't install system packages.
109
+
110
+ > **ℹ️ Note:** Without libvips installed (system or binary), `pyvips` falls back to ABI mode which may be ~20% slower. Pyramidization will still work correctly.
111
+
112
+ ---
113
+
114
+ ## Quick Start
115
+
116
+ ### Non-overlapping tiles — `FastWSIProcessor`
117
+
118
+ Use this for most use cases: virtual staining, classification heatmaps, segmentation masks.
119
+
120
+ ```python
121
+ import numpy as np
122
+ from wsi_tile_processor import FastWSIProcessor
123
+
124
+ # 'generate' maps an HxWx3 uint8 tile to an HxWx3 output.
125
+ # Here we use an identity function as a placeholder.
126
+ def my_model(tile: np.ndarray) -> np.ndarray:
127
+ return tile # replace with your model
128
+
129
+ processor = FastWSIProcessor(
130
+ wsi_path="input.svs",
131
+ tiff_path="output.ome.tiff",
132
+ level=1, # pyramid level to process (0 = full res)
133
+ tile_size=512, # tile side length in pixels
134
+ generate=my_model,
135
+ output_channels=3,
136
+ verbose=True,
137
+ )
138
+ processor.process()
139
+ ```
140
+
141
+ ### Overlapping tiles with blending — `GaussianWSIProcessor`
142
+
143
+ Use this when your model produces boundary artifacts. Overlapping tiles are averaged with a Gaussian weight kernel, eliminating grid patterns.
144
+
145
+ ```python
146
+ from wsi_tile_processor import GaussianWSIProcessor
147
+
148
+ processor = GaussianWSIProcessor(
149
+ wsi_path="input.svs",
150
+ tiff_path="output.ome.tiff",
151
+ level=1,
152
+ tile_size=512,
153
+ stride=256, # 50% overlap
154
+ blur_sigma=64.0, # Gaussian kernel sigma in pixels
155
+ generate=my_model,
156
+ output_channels=3,
157
+ verbose=True,
158
+ )
159
+ processor.process()
160
+ ```
161
+
162
+
163
+ ### Batched inference
164
+
165
+ ```python
166
+ import numpy as np
167
+
168
+ def batched_model(batch: np.ndarray) -> np.ndarray:
169
+ # batch shape: (B, H, W, 3) uint8
170
+ # return shape: (B, H, W, 3)
171
+ return batch
172
+
173
+ processor = FastWSIProcessor(
174
+ wsi_path="input.svs",
175
+ tiff_path="output.ome.tiff",
176
+ level=1,
177
+ tile_size=512,
178
+ generate=batched_model,
179
+ batch_size=8, # tiles per forward pass
180
+ verbose=True,
181
+ )
182
+ processor.process()
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Architecture
188
+
189
+ ```
190
+ WSIProcessor (abstract)
191
+ ├── FastWSIProcessor — stride == tile_size, uint8 memmap
192
+ └── GaussianWSIProcessor — stride < tile_size, float16 acc + wgt memmaps
193
+ ```
194
+
195
+ ### `FastWSIProcessor`
196
+
197
+ - Iterates tiles in a regular grid (no overlap).
198
+ - Each non-background tile is passed to `generate`, and the uint8 result is written directly into a memory-mapped binary file.
199
+ - After all tiles, writes a BigTIFF and pyramidizes in-place with pyvips.
200
+ - **Disk usage:** 1× the output image size.
201
+ - **Best for:** virtual staining, classification maps, any task where tile alignment is fine.
202
+
203
+ ### `GaussianWSIProcessor`
204
+
205
+ - Iterates tiles with a configurable stride (overlap).
206
+ - Each tile's output is multiplied by a Gaussian weight kernel and accumulated into `float16` memmaps (`acc` and `wgt`).
207
+ - After all tiles, every pixel is normalized by its total accumulated weight, then converted to uint8.
208
+ - **Disk usage:** ~5–6× the output image size during processing.
209
+ - **Best for:** models that produce boundary artifacts, or when smooth spatial continuity is critical.
210
+
211
+ ### Custom Processor
212
+
213
+ Both processors share the same abstract contract defined by `WSIProcessor`.
214
+ You can subclass it to implement a completely different accumulation strategy — for example, writing tiles to HDF5, streaming to a remote store, or applying multi-scale blending.
215
+
216
+ The only method you must implement is `process()`:
217
+
218
+ ```python
219
+ import numpy as np
220
+ from wsi_tile_processor import WSIProcessor, _open_slide, _get_mpp
221
+
222
+ class MyProcessor(WSIProcessor):
223
+ def process(self) -> None:
224
+ slide = _open_slide(self.wsi_path, self.backend)
225
+ level_w, level_h = slide.level_dimensions[self.level]
226
+ downsample = slide.level_downsamples[self.level]
227
+
228
+ for ty in range(0, level_h, self.tile_size):
229
+ for tx in range(0, level_w, self.tile_size):
230
+ read_w = min(self.tile_size, level_w - tx)
231
+ read_h = min(self.tile_size, level_h - ty)
232
+ x0, y0 = int(tx * downsample), int(ty * downsample)
233
+
234
+ pil_tile = slide.read_region((x0, y0), self.level, (read_w, read_h))
235
+ tile = np.array(pil_tile.convert("RGB"), dtype=np.uint8)
236
+
237
+ if self._is_tile_background(tile):
238
+ continue # uses whatever BackgroundFilter was configured
239
+
240
+ result = self.generate(tile)
241
+ # ... write result to your custom output format ...
242
+
243
+ slide.close()
244
+
245
+ # Use it like any other processor
246
+ proc = MyProcessor(
247
+ wsi_path="input.svs",
248
+ tiff_path="output.ome.tiff",
249
+ level=1,
250
+ tile_size=512,
251
+ generate=my_model,
252
+ )
253
+ proc.process()
254
+ ```
255
+
256
+ > **💡 Tip:** Your custom processor automatically inherits `_is_tile_background()`, `_compute_tissue_mask()`, and `_build_pyramid_pyvips()` from the base class, so you can reuse all the built-in filtering and pyramidization logic.
257
+
258
+ ---
259
+
260
+ ## Background Filters
261
+
262
+ Background tiles are skipped by default: `generate` is never called on them,
263
+ and they are filled with `background_value` (default: white, 1.0).
264
+
265
+ ### Built-in filters
266
+
267
+ #### `BrightnessBackgroundFilter` *(default)*
268
+
269
+ Brightness + optional saturation heuristic. Tuned for H&E and IHC slides.
270
+ A tile is background when more than `threshold` (default 70%) of its pixels
271
+ have all three channels > `threshold_intensity` (default 215).
272
+
273
+ ```python
274
+ from wsi_tile_processor import BrightnessBackgroundFilter, FastWSIProcessor
275
+
276
+ bg_filter = BrightnessBackgroundFilter(
277
+ threshold=0.75, # stricter: 75% bright pixels
278
+ threshold_intensity=220,
279
+ min_variance=100.0, # also skip low-variance tiles
280
+ )
281
+
282
+ processor = FastWSIProcessor(
283
+ wsi_path="input.svs",
284
+ tiff_path="output.ome.tiff",
285
+ level=1,
286
+ tile_size=512,
287
+ generate=my_model,
288
+ background_filter=bg_filter,
289
+ )
290
+ processor.process()
291
+ ```
292
+
293
+ #### `OtsuBackgroundFilter`
294
+
295
+ Uses scikit-image Otsu thresholding on the grayscale image.
296
+ Requires `pip install scikit-image` (or `pip install wsi-tile-processor[full]`).
297
+
298
+ ```python
299
+ from wsi_tile_processor import OtsuBackgroundFilter
300
+
301
+ bg_filter = OtsuBackgroundFilter(background_fraction=0.65)
302
+ ```
303
+
304
+ #### `SaturationBackgroundFilter`
305
+
306
+ Classifies a tile as background when its mean HSV saturation is below a threshold.
307
+ Fast and robust for highly stained tissue.
308
+
309
+ ```python
310
+ from wsi_tile_processor import SaturationBackgroundFilter
311
+
312
+ bg_filter = SaturationBackgroundFilter(min_mean_saturation=20.0)
313
+ ```
314
+
315
+ ### Custom background filter
316
+
317
+ Subclass `BackgroundFilter` and implement `__call__`:
318
+
319
+ ```python
320
+ import numpy as np
321
+ from wsi_tile_processor import BackgroundFilter, FastWSIProcessor
322
+
323
+ class MyPenMarkFilter(BackgroundFilter):
324
+ """Skip tiles dominated by blue pen marks."""
325
+
326
+ def __call__(self, tile_rgb: np.ndarray) -> bool:
327
+ r, g, b = tile_rgb[..., 0], tile_rgb[..., 1], tile_rgb[..., 2]
328
+ # Blue pen: b >> r and b >> g
329
+ blue_mask = (b.astype(int) - r.astype(int) > 50) & \
330
+ (b.astype(int) - g.astype(int) > 50)
331
+ return float(blue_mask.mean()) > 0.30 # >30% blue → skip
332
+
333
+ processor = FastWSIProcessor(
334
+ wsi_path="input.svs",
335
+ tiff_path="output.ome.tiff",
336
+ level=1,
337
+ tile_size=512,
338
+ generate=my_model,
339
+ background_filter=MyPenMarkFilter(),
340
+ )
341
+ processor.process()
342
+ ```
343
+
344
+ You can also pass any plain callable with signature `(tile_rgb: np.ndarray) -> bool`:
345
+
346
+ ```python
347
+ processor = FastWSIProcessor(
348
+ ...,
349
+ background_filter=lambda t: t.mean() > 230,
350
+ )
351
+ ```
352
+
353
+ ---
354
+
355
+ ## Tissue Mask Detection
356
+
357
+ For very large slides, computing the background filter on every tile can be
358
+ slow. A `TissueMaskDetector` provides a coarse, one-time mask computed on a
359
+ low-resolution thumbnail. Tiles that fall entirely outside the tissue mask are
360
+ skipped *without* reading the full-resolution data.
361
+
362
+ ### Built-in detector
363
+
364
+ ```python
365
+ from wsi_tile_processor import BrightnessTissueMaskDetector, FastWSIProcessor
366
+
367
+ detector = BrightnessTissueMaskDetector(
368
+ intensity_threshold=215, # pixels below this are tissue
369
+ max_side=4000, # thumbnail max dimension
370
+ open_radius=2,
371
+ close_radius=4,
372
+ verbose=True,
373
+ )
374
+
375
+ processor = FastWSIProcessor(
376
+ wsi_path="input.svs",
377
+ tiff_path="output.ome.tiff",
378
+ level=1,
379
+ tile_size=512,
380
+ generate=my_model,
381
+ tissue_mask_detector=detector, # supersedes use_tissue_mask=True
382
+ )
383
+ processor.process()
384
+ ```
385
+
386
+ Requires `scikit-image` (`pip install wsi-tile-processor[full]`).
387
+
388
+ ### Custom tissue mask detector
389
+
390
+ ```python
391
+ import numpy as np
392
+ from wsi_tile_processor import TissueMaskDetector
393
+
394
+ class MyDLTissueMask(TissueMaskDetector):
395
+ def __init__(self, model):
396
+ self.model = model
397
+
398
+ def __call__(self, slide) -> tuple[np.ndarray, float]:
399
+ # Use the smallest level for speed
400
+ level = slide.level_count - 1
401
+ dims = slide.level_dimensions[level]
402
+ downsample = slide.level_downsamples[level]
403
+ thumb = np.array(slide.read_region((0, 0), level, dims).convert("RGB"))
404
+ mask = self.model.predict(thumb) # → bool H×W
405
+ return mask, downsample
406
+ ```
407
+
408
+ ---
409
+
410
+ ## Slide Backends
411
+
412
+ | Backend | When to use |
413
+ |---------|-------------|
414
+ | `"auto"` *(default)* | Tries openslide first; falls back to tifffile if tifffile exposes more pyramid levels. Best for mixed environments. |
415
+ | `"openslide"` | SVS, NDPI, SCN, and other formats natively supported by OpenSlide. |
416
+ | `"tifffile"` | OME-TIFF files with sub-IFD pyramid levels (e.g. output of [VALIS](https://github.com/MathOnco/valis)). OpenSlide may only see the base level in these files. |
417
+
418
+ ```python
419
+ processor = FastWSIProcessor(
420
+ ...,
421
+ backend="tifffile",
422
+ )
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Output Format
428
+
429
+ The output is a **BigTIFF OME-TIFF** with:
430
+
431
+ - A full JPEG-compressed pyramid (sub-IFD layout, readable by QuPath, OMERO, pyvips, tifffile, …)
432
+ - Physical resolution metadata (`XResolution` / `YResolution` in pixels/cm, derived from the input slide's `mpp-x`)
433
+ - Configurable JPEG quality (default 95) and internal tile size (default 512 px)
434
+
435
+ ---
436
+
437
+ ## License
438
+
439
+ MIT © 2026 Federico Carollo — see [LICENSE](LICENSE) for details.