Skill Tracer
Skills › Scientific Agent Skills › pathml › preprocessing.md

preprocessing.md

376 lines · skills/pathml/references/preprocessing.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Preprocessing pipelines, masks, stain handling, and QC
2
3This reference describes **PathML 3.0.8 stable**. It corrects older examples that
4called nonexistent `Pipeline.run()`, omitted required mask/label names, or passed
5unsupported transform arguments.
6

Preprocessing pipelines, masks, stain handling, and QC

This reference describes PathML 3.0.8 stable. It corrects older examples that called nonexistent Pipeline.run(), omitted required mask/label names, or passed unsupported transform arguments.

Raw lines 1 to 6
7## Pipeline execution model
8
9A `Pipeline` is an ordered list of `Transform` objects. `Pipeline.apply(tile)`
10modifies one `Tile` in place and returns it. A slide or dataset owns execution:
12```python
13from pathml.core import HESlide
14from pathml.preprocessing import BoxBlur, Pipeline, TissueDetectionHE
16slide = HESlide("data/slide-001.svs", backend="openslide")
17pipeline = Pipeline(
18 [
19 BoxBlur(kernel_size=5),
20 TissueDetectionHE(mask_name="tissue"),
21 ]
24slide.run(
25 pipeline,
26 distributed=False,
27 tile_size=512,
28 tile_stride=512,
29 level=0,
30 tile_pad=False,
32```
34Stable facts:
36- `Pipeline(transform_sequence=None)` and `Pipeline.apply(tile)` are public.
37- `Pipeline` has no `run()` method.
38- `SlideData.run()` and `SlideDataset.run()` apply pipelines.
39- `distributed=True` is the default and may create a local Dask cluster using
40 available cores. Start with `distributed=False`.
41- Existing tiles are protected unless `overwrite_existing_tiles=True`.
42- `write_dir` causes a `<slide.name>.h5path` write after processing.
43- `Pipeline.save()` writes a pickle. A pickle is executable on load; never use a
44 pipeline file from an untrusted source.

Pipeline execution model

A Pipeline is an ordered list of Transform objects. Pipeline.apply(tile) modifies one Tile in place and returns it. A slide or dataset owns execution:

from pathml.core import HESlide
from pathml.preprocessing import BoxBlur, Pipeline, TissueDetectionHE

slide = HESlide("data/slide-001.svs", backend="openslide")
pipeline = Pipeline(
    [
        BoxBlur(kernel_size=5),
        TissueDetectionHE(mask_name="tissue"),
    ]
)

slide.run(
    pipeline,
    distributed=False,
    tile_size=512,
    tile_stride=512,
    level=0,
    tile_pad=False,
)

Stable facts:

  • Pipeline(transform_sequence=None) and Pipeline.apply(tile) are public.
  • Pipeline has no run() method.
  • SlideData.run() and SlideDataset.run() apply pipelines.
  • distributed=True is the default and may create a local Dask cluster using available cores. Start with distributed=False.
  • Existing tiles are protected unless overwrite_existing_tiles=True.
  • write_dir causes a <slide.name>.h5path write after processing.
  • Pipeline.save() writes a pickle. A pickle is executable on load; never use a pipeline file from an untrusted source.
Raw lines 7 to 45
46## Stable transform imports
48```python
49from pathml.preprocessing import (
50 AdaptiveHistogramEqualization,
51 BinaryThreshold,
52 BoxBlur,
53 CollapseRunsCODEX,
54 CollapseRunsVectra,
55 ForegroundDetection,
56 GaussianBlur,
57 HistogramEqualization,
58 LabelArtifactTileHE,
59 LabelWhiteSpaceHE,
60 MedianBlur,
61 MorphClose,
62 MorphOpen,
63 NucleusDetectionHE,
64 Pipeline,
65 QuantifyMIF,
66 RescaleIntensity,
67 SegmentMIF,
68 SegmentMIFRemote,
69 StainNormalizationHE,
70 SuperpixelInterpolation,
71 TissueDetectionHE,
73```
75There is no stable `transform='...'` registry and no safe reason to construct
76transforms from arbitrary Python expressions. Parse a strict allowlisted config,
77then instantiate known classes explicitly.

Stable transform imports

from pathml.preprocessing import (
    AdaptiveHistogramEqualization,
    BinaryThreshold,
    BoxBlur,
    CollapseRunsCODEX,
    CollapseRunsVectra,
    ForegroundDetection,
    GaussianBlur,
    HistogramEqualization,
    LabelArtifactTileHE,
    LabelWhiteSpaceHE,
    MedianBlur,
    MorphClose,
    MorphOpen,
    NucleusDetectionHE,
    Pipeline,
    QuantifyMIF,
    RescaleIntensity,
    SegmentMIF,
    SegmentMIFRemote,
    StainNormalizationHE,
    SuperpixelInterpolation,
    TissueDetectionHE,
)

There is no stable transform='...' registry and no safe reason to construct transforms from arbitrary Python expressions. Parse a strict allowlisted config, then instantiate known classes explicitly.

Raw lines 46 to 78
79## Tissue detection
81```python
82from pathml.preprocessing import TissueDetectionHE
84tissue = TissueDetectionHE(
85 mask_name="tissue",
86 use_saturation=True,
87 blur_ksize=17,
88 threshold=None, # Otsu when None
89 morph_n_iter=3,
90 morph_k_size=7,
91 min_region_size=5000,
92 max_hole_size=1500,
93 outer_contours_only=False,
95```
97The transform expects an H&E `uint8` tile. It:
991. uses HSV saturation or greyscale;
1002. median-blurs;
1013. applies Otsu or the explicit threshold;
1024. performs morphological opening and closing;
1035. keeps foreground regions under the configured area/hole policy; and
1046. writes `tile.masks["tissue"]`.
106`mask_name` is required in practice; `None` fails when `apply()` runs.
107`min_region_size`, `max_hole_size`, and morphology kernels are measured in pixels
108at the processing level. Re-tune if level or MPP changes.
110Tissue detection is tile-local. It can disagree at tile edges, and PathML does not
111automatically remove background tiles from the pipeline. Compute and record tissue
112coverage after the mask exists:
114```python
115coverage = float((tile.masks["tissue"] > 0).mean())
116keep = coverage >= 0.50
117```
119Choose the coverage rule on training data and preserve rejected-tile counts.

Tissue detection

from pathml.preprocessing import TissueDetectionHE

tissue = TissueDetectionHE(
    mask_name="tissue",
    use_saturation=True,
    blur_ksize=17,
    threshold=None,          # Otsu when None
    morph_n_iter=3,
    morph_k_size=7,
    min_region_size=5000,
    max_hole_size=1500,
    outer_contours_only=False,
)

The transform expects an H&E uint8 tile. It:

  1. uses HSV saturation or greyscale;
  2. median-blurs;
  3. applies Otsu or the explicit threshold;
  4. performs morphological opening and closing;
  5. keeps foreground regions under the configured area/hole policy; and
  6. writes tile.masks["tissue"].

mask_name is required in practice; None fails when apply() runs. min_region_size, max_hole_size, and morphology kernels are measured in pixels at the processing level. Re-tune if level or MPP changes.

Tissue detection is tile-local. It can disagree at tile edges, and PathML does not automatically remove background tiles from the pipeline. Compute and record tissue coverage after the mask exists:

coverage = float((tile.masks["tissue"] > 0).mean())
keep = coverage >= 0.50

Choose the coverage rule on training data and preserve rejected-tile counts.

Raw lines 79 to 120
121## Whitespace and artifact QC
123These transforms write **tile labels**, not pixel masks:
125```python
126from pathml.preprocessing import LabelArtifactTileHE, LabelWhiteSpaceHE
128whitespace = LabelWhiteSpaceHE(
129 label_name="mostly_white",
130 greyscale_threshold=230,
131 proportion_threshold=0.5,
133artifact = LabelArtifactTileHE(label_name="artifact")
134```
136- `LabelWhiteSpaceHE` labels a tile when the proportion of greyscale pixels above
137 the threshold exceeds `proportion_threshold`.
138- `LabelArtifactTileHE` is a fixed rule-based HSI heuristic for whitespace,
139 dark regions, and pen-like colors. It exposes only `label_name`; older examples
140 with `pen_threshold` or `bubble_threshold` are invalid.
142Neither is a complete slide-quality system. Review representative overlays and
143track blur, folds, bubbles, pen, tissue coverage, clipping, color drift, missing
144channels, and focus separately. Do not convert a heuristic QC flag into a clinical
145quality judgment.
147The dependency-free helper provides a deliberately simple synthetic/local check,
148not a replacement for PathML:
150```bash
151python scripts/image_qc.py synthetic --width 256 --height 256
152python scripts/image_qc.py inspect --image tests/fixtures/synthetic.ppm --root .
153```
155It reports brightness/saturation and a coarse tissue-like mask using bounded
156pixels. PNG/JPEG/TIFF input needs Pillow, imported only after argument validation.

Whitespace and artifact QC

These transforms write tile labels, not pixel masks:

from pathml.preprocessing import LabelArtifactTileHE, LabelWhiteSpaceHE

whitespace = LabelWhiteSpaceHE(
    label_name="mostly_white",
    greyscale_threshold=230,
    proportion_threshold=0.5,
)
artifact = LabelArtifactTileHE(label_name="artifact")
  • LabelWhiteSpaceHE labels a tile when the proportion of greyscale pixels above the threshold exceeds proportion_threshold.
  • LabelArtifactTileHE is a fixed rule-based HSI heuristic for whitespace, dark regions, and pen-like colors. It exposes only label_name; older examples with pen_threshold or bubble_threshold are invalid.

Neither is a complete slide-quality system. Review representative overlays and track blur, folds, bubbles, pen, tissue coverage, clipping, color drift, missing channels, and focus separately. Do not convert a heuristic QC flag into a clinical quality judgment.

The dependency-free helper provides a deliberately simple synthetic/local check, not a replacement for PathML:

python scripts/image_qc.py synthetic --width 256 --height 256
python scripts/image_qc.py inspect --image tests/fixtures/synthetic.ppm --root .

It reports brightness/saturation and a coarse tissue-like mask using bounded pixels. PNG/JPEG/TIFF input needs Pillow, imported only after argument validation.

Raw lines 121 to 157
Runs code python scripts/image_qc.py :151
158## H&E stain normalization and separation
160```python
161from pathml.preprocessing import StainNormalizationHE
163normalizer = StainNormalizationHE(
164 target="normalize", # normalize | hematoxylin | eosin
165 stain_estimation_method="macenko", # macenko | vahadane
166 optical_density_threshold=0.15,
167 regularizer=0.1,
168 angular_percentile=0.01,
169 background_intensity=245,
171```
173Stable `StainNormalizationHE` does **not** accept `tissue_mask_name`,
174`target_od`, or `target_concentrations`. It accepts `stain_matrix_target_od` and
175`max_c_target`, and supplies fixed defaults.
177To fit a reference:
179```python
180normalizer.fit_to_reference(training_reference_rgb)
181normalized_rgb = normalizer.F(source_rgb)
182```
184Leakage controls:
186- Select the reference and tune OD parameters using training slides only.
187- Freeze the fitted stain matrix and target concentration before validation/test.
188- Do not choose a reference because test performance looks better.
189- Record source slide pseudonym, region coordinates, level, MPP, method, and all
190 fitted arrays without direct identifiers.
191- Fit on tissue-rich, artifact-free RGB regions. Stable API does not consume a
192 tissue mask directly, so crop/filter the reference beforehand.
194Macenko and Vahadane are model-based color standardization methods, not guarantees
195that biological staining becomes comparable. Preserve raw inputs and assess
196whether normalization removes task-relevant signal or amplifies artifacts.

H&E stain normalization and separation

from pathml.preprocessing import StainNormalizationHE

normalizer = StainNormalizationHE(
    target="normalize",             # normalize | hematoxylin | eosin
    stain_estimation_method="macenko",  # macenko | vahadane
    optical_density_threshold=0.15,
    regularizer=0.1,
    angular_percentile=0.01,
    background_intensity=245,
)

Stable StainNormalizationHE does not accept tissue_mask_name, target_od, or target_concentrations. It accepts stain_matrix_target_od and max_c_target, and supplies fixed defaults.

To fit a reference:

normalizer.fit_to_reference(training_reference_rgb)
normalized_rgb = normalizer.F(source_rgb)

Leakage controls:

  • Select the reference and tune OD parameters using training slides only.
  • Freeze the fitted stain matrix and target concentration before validation/test.
  • Do not choose a reference because test performance looks better.
  • Record source slide pseudonym, region coordinates, level, MPP, method, and all fitted arrays without direct identifiers.
  • Fit on tissue-rich, artifact-free RGB regions. Stable API does not consume a tissue mask directly, so crop/filter the reference beforehand.

Macenko and Vahadane are model-based color standardization methods, not guarantees that biological staining becomes comparable. Preserve raw inputs and assess whether normalization removes task-relevant signal or amplifies artifacts.

Raw lines 158 to 197
198## Simple H&E nucleus mask
200```python
201from pathml.preprocessing import NucleusDetectionHE
203nuclei = NucleusDetectionHE(
204 mask_name="nuclei",
205 stain_estimation_method="vahadane",
206 superpixel_region_size=10,
207 n_iter=30,
209```
211This is a simple transform: hematoxylin separation, superpixel interpolation, and
212Otsu thresholding. It writes a binary tile mask. It is not HoVer-Net, does not
213assign nucleus classes, and should not be treated as a validated cell count.
214Inspect touching objects, fragments, necrosis, stain failure, and tile boundaries.

Simple H&E nucleus mask

from pathml.preprocessing import NucleusDetectionHE

nuclei = NucleusDetectionHE(
    mask_name="nuclei",
    stain_estimation_method="vahadane",
    superpixel_region_size=10,
    n_iter=30,
)

This is a simple transform: hematoxylin separation, superpixel interpolation, and Otsu thresholding. It writes a binary tile mask. It is not HoVer-Net, does not assign nucleus classes, and should not be treated as a validated cell count. Inspect touching objects, fragments, necrosis, stain failure, and tile boundaries.

Raw lines 198 to 215
216## Binary and morphology building blocks
218Useful stable signatures:
220```python
221from pathml.preprocessing import BinaryThreshold, MorphClose, MorphOpen
223threshold = BinaryThreshold(
224 mask_name="foreground",
225 use_otsu=True,
226 threshold=0,
227 inverse=False,
229opened = MorphOpen(mask_name="foreground", kernel_size=5, n_iterations=1)
230closed = MorphClose(mask_name="foreground", kernel_size=5, n_iterations=1)
231```
233`BinaryThreshold.apply()` creates a named mask. `MorphOpen` and `MorphClose`
234modify the named mask. Match dtype, polarity, and dimensions explicitly.

Binary and morphology building blocks

Useful stable signatures:

from pathml.preprocessing import BinaryThreshold, MorphClose, MorphOpen

threshold = BinaryThreshold(
    mask_name="foreground",
    use_otsu=True,
    threshold=0,
    inverse=False,
)
opened = MorphOpen(mask_name="foreground", kernel_size=5, n_iterations=1)
closed = MorphClose(mask_name="foreground", kernel_size=5, n_iterations=1)

BinaryThreshold.apply() creates a named mask. MorphOpen and MorphClose modify the named mask. Match dtype, polarity, and dimensions explicitly.

Raw lines 216 to 235
236## Recommended pilot pipeline
238```python
239from pathml.preprocessing import (
240 BoxBlur,
241 LabelArtifactTileHE,
242 LabelWhiteSpaceHE,
243 Pipeline,
244 StainNormalizationHE,
245 TissueDetectionHE,
248pipeline = Pipeline(
249 [
250 LabelWhiteSpaceHE(
251 label_name="mostly_white",
252 greyscale_threshold=230,
253 proportion_threshold=0.8,
254 ),
255 LabelArtifactTileHE(label_name="artifact"),
256 BoxBlur(kernel_size=3),
257 TissueDetectionHE(
258 mask_name="tissue",
259 min_region_size=5000,
260 outer_contours_only=False,
261 ),
262 StainNormalizationHE(
263 target="normalize",
264 stain_estimation_method="macenko",
265 ),
266 ]
268```
270QC labels do not short-circuit later transforms. If expensive stages should run
271only on accepted tiles, write an explicit custom transform or bounded manual loop
272with a documented policy. Keep the logic deterministic and test it.

Recommended pilot pipeline

from pathml.preprocessing import (
    BoxBlur,
    LabelArtifactTileHE,
    LabelWhiteSpaceHE,
    Pipeline,
    StainNormalizationHE,
    TissueDetectionHE,
)

pipeline = Pipeline(
    [
        LabelWhiteSpaceHE(
            label_name="mostly_white",
            greyscale_threshold=230,
            proportion_threshold=0.8,
        ),
        LabelArtifactTileHE(label_name="artifact"),
        BoxBlur(kernel_size=3),
        TissueDetectionHE(
            mask_name="tissue",
            min_region_size=5000,
            outer_contours_only=False,
        ),
        StainNormalizationHE(
            target="normalize",
            stain_estimation_method="macenko",
        ),
    ]
)

QC labels do not short-circuit later transforms. If expensive stages should run only on accepted tiles, write an explicit custom transform or bounded manual loop with a documented policy. Keep the logic deterministic and test it.

Raw lines 236 to 273
274## Bounded dry run
276PathML has no `max_tiles` argument. Use `islice` for a local pilot:
278```python
279from itertools import islice
281sampled = []
282for tile in islice(
283 slide.generate_tiles(shape=512, stride=512, pad=False, level=0),
284 16,
285):
286 pipeline.apply(tile)
287 sampled.append(
288 {
289 "coords_ij": tile.coords,
290 "shape": tuple(tile.image.shape),
291 "tissue_fraction": float((tile.masks["tissue"] > 0).mean()),
292 "mostly_white": bool(tile.labels["mostly_white"]),
293 "artifact": bool(tile.labels["artifact"]),
294 }
295 )
296```
298Plan first:
300```bash
301python scripts/plan_pipeline.py \
302 --width 100000 --height 80000 \
303 --tile-size 512 --stride 512 \
304 --pipeline TissueDetectionHE,LabelWhiteSpaceHE,StainNormalizationHE \
305 --max-tiles 1000000
306```
308The planner never opens the slide or imports PathML. Supply dimensions from a
309trusted technical metadata inspection. Its storage estimate uses h5path's
310float16 image/mask payload, excludes counts/labels/metadata, and assumes the
311supplied channel count remains unchanged. It is not peak RAM or a guarantee of
312available disk. At nonzero levels supply exact `--level-width`/`--level-height`.

Bounded dry run

PathML has no max_tiles argument. Use islice for a local pilot:

from itertools import islice

sampled = []
for tile in islice(
    slide.generate_tiles(shape=512, stride=512, pad=False, level=0),
    16,
):
    pipeline.apply(tile)
    sampled.append(
        {
            "coords_ij": tile.coords,
            "shape": tuple(tile.image.shape),
            "tissue_fraction": float((tile.masks["tissue"] > 0).mean()),
            "mostly_white": bool(tile.labels["mostly_white"]),
            "artifact": bool(tile.labels["artifact"]),
        }
    )

Plan first:

python scripts/plan_pipeline.py \
  --width 100000 --height 80000 \
  --tile-size 512 --stride 512 \
  --pipeline TissueDetectionHE,LabelWhiteSpaceHE,StainNormalizationHE \
  --max-tiles 1000000

The planner never opens the slide or imports PathML. Supply dimensions from a trusted technical metadata inspection. Its storage estimate uses h5path's float16 image/mask payload, excludes counts/labels/metadata, and assumes the supplied channel count remains unchanged. It is not peak RAM or a guarantee of available disk. At nonzero levels supply exact --level-width/--level-height.

Raw lines 274 to 313
Runs code python scripts/plan_pipeline.py :301
314## Masks, labels, padding, and overlap
316- A tile mask's first two dimensions must match the tile image.
317- Use distinct semantic names (`tissue`, `nuclei`, `cell_segmentation`) and record
318 whether each mask is binary, semantic, or instance-labeled.
319- Instance masks use 0 for background and positive integer object IDs.
320- `tile_pad=True` introduces zeros. Document whether padded pixels are ignored in
321 QC, stain fitting, loss, and stitching.
322- Stable `SlideData.generate_tiles()` cannot slice slide-level masks into padded
323 tiles.
324- Overlap duplicates tissue/cells. Deduplicate by slide coordinates or use an
325 explicit blending/cropping policy before counting.
326- Keep masks at the same level as their coordinates. Resampling an instance mask
327 requires nearest-neighbor interpolation and relabel/QC.

Masks, labels, padding, and overlap

  • A tile mask's first two dimensions must match the tile image.
  • Use distinct semantic names (tissue, nuclei, cell_segmentation) and record whether each mask is binary, semantic, or instance-labeled.
  • Instance masks use 0 for background and positive integer object IDs.
  • tile_pad=True introduces zeros. Document whether padded pixels are ignored in QC, stain fitting, loss, and stitching.
  • Stable SlideData.generate_tiles() cannot slice slide-level masks into padded tiles.
  • Overlap duplicates tissue/cells. Deduplicate by slide coordinates or use an explicit blending/cropping policy before counting.
  • Keep masks at the same level as their coordinates. Resampling an instance mask requires nearest-neighbor interpolation and relabel/QC.
Raw lines 314 to 328
329## Train/validation/test leakage checklist
331Do all splitting before:
333- choosing stain references;
334- estimating QC or tissue thresholds;
335- fitting feature scalers;
336- learning augmentations or color distributions;
337- selecting segmentation parameters;
338- extracting overlapping tiles;
339- constructing graphs; or
340- calibrating model thresholds.
342All tiles, regions, serial sections, and repeat scans from one patient belong to
343one split. If site/scanner generalization is the target, reserve entire sites or
344scanners as designed. Record exclusions before viewing test outcomes.

Train/validation/test leakage checklist

Do all splitting before:

  • choosing stain references;
  • estimating QC or tissue thresholds;
  • fitting feature scalers;
  • learning augmentations or color distributions;
  • selecting segmentation parameters;
  • extracting overlapping tiles;
  • constructing graphs; or
  • calibrating model thresholds.

All tiles, regions, serial sections, and repeat scans from one patient belong to one split. If site/scanner generalization is the target, reserve entire sites or scanners as designed. Record exclusions before viewing test outcomes.

Raw lines 329 to 345
346## Reproducibility record
348For each run, retain:
350- PathML and dependency lock versions;
351- source SHA-256 and pseudonymous IDs;
352- backend, level, downsample, MPP, tile size/stride/pad;
353- ordered transform names and all constructor values;
354- fitted stain arrays and training-only reference provenance;
355- QC/mask definitions and per-slide accept/reject totals;
356- Dask configuration, worker count, CPU/GPU, and failure/retry policy;
357- code revision, random seeds, split manifest hash, and output hashes.

Reproducibility record

For each run, retain:

  • PathML and dependency lock versions;
  • source SHA-256 and pseudonymous IDs;
  • backend, level, downsample, MPP, tile size/stride/pad;
  • ordered transform names and all constructor values;
  • fitted stain arrays and training-only reference provenance;
  • QC/mask definitions and per-slide accept/reject totals;
  • Dask configuration, worker count, CPU/GPU, and failure/retry policy;
  • code revision, random seeds, split manifest hash, and output hashes.
Raw lines 346 to 358
359## Sources and further reading
361API baseline reviewed 2026-10-01 using the released wheel/tag; hosted docs may lag.
363- Stable pipeline guide:
364 https://pathml.readthedocs.io/en/stable/creating_pipelines.html
365- Stable execution guide:
366 https://pathml.readthedocs.io/en/stable/running_pipelines.html
367- Stable preprocessing API:
368 https://pathml.readthedocs.io/en/stable/api_preprocessing_reference.html
369- Stable transforms source:
370 https://github.com/Dana-Farber-AIOS/pathml/blob/v3.0.8/pathml/preprocessing/transforms.py
371- Stable pipeline source:
372 https://github.com/Dana-Farber-AIOS/pathml/blob/v3.0.8/pathml/preprocessing/pipeline.py
373- Macenko et al. (2009):
374 https://doi.org/10.1109/ISBI.2009.5193250
375- Vahadane et al. (2016):
376 https://doi.org/10.1109/TMI.2016.2529665

Sources and further reading

API baseline reviewed 2026-10-01 using the released wheel/tag; hosted docs may lag.

  • Stable pipeline guide: https://pathml.readthedocs.io/en/stable/creating_pipelines.html
  • Stable execution guide: https://pathml.readthedocs.io/en/stable/running_pipelines.html
  • Stable preprocessing API: https://pathml.readthedocs.io/en/stable/api_preprocessing_reference.html
  • Stable transforms source: https://github.com/Dana-Farber-AIOS/pathml/blob/v3.0.8/pathml/preprocessing/transforms.py
  • Stable pipeline source: https://github.com/Dana-Farber-AIOS/pathml/blob/v3.0.8/pathml/preprocessing/pipeline.py
  • Macenko et al. (2009): https://doi.org/10.1109/ISBI.2009.5193250
  • Vahadane et al. (2016): https://doi.org/10.1109/TMI.2016.2529665
Raw lines 359 to 376