preprocessing.md
376 lines · skills/pathml/references/preprocessing.md at 5f2bc03
Most marks come from matching words in the file, so treat them as pointers.
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.
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)andPipeline.apply(tile)are public.Pipelinehas norun()method.SlideData.run()andSlideDataset.run()apply pipelines.distributed=Trueis the default and may create a local Dask cluster using available cores. Start withdistributed=False.- Existing tiles are protected unless
overwrite_existing_tiles=True. write_dircauses a<slide.name>.h5pathwrite after processing.Pipeline.save()writes a pickle. A pickle is executable on load; never use a pipeline file from an untrusted source.
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.
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:
- uses HSV saturation or greyscale;
- median-blurs;
- applies Otsu or the explicit threshold;
- performs morphological opening and closing;
- keeps foreground regions under the configured area/hole policy; and
- 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.50Choose the coverage rule on training data and preserve rejected-tile counts.
Raw lines 79 to 120Whitespace 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")LabelWhiteSpaceHElabels a tile when the proportion of greyscale pixels above the threshold exceedsproportion_threshold.LabelArtifactTileHEis a fixed rule-based HSI heuristic for whitespace, dark regions, and pen-like colors. It exposes onlylabel_name; older examples withpen_thresholdorbubble_thresholdare 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 157H&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 197Simple 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 215Binary 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.
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 273Bounded 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 1000000The 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.
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=Trueintroduces 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.
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 345Reproducibility 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.
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