Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Computational pathology toolkit for whole-slide images (WSIs): load slides, extract tiles, stain normalization, nuclear segmentation, feature extraction, and ML training. Supports H&E and multiplex. For end-to-end pipelines from raw WSIs to quantitative outputs.
.claude/skills/jaechang-hits-pathml/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 60% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 76% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 60% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 78% | 0% |
PathML is a Python toolkit designed for computational pathology workflows on whole-slide images (WSIs). It provides a unified pipeline from raw slide files (SVS, NDPI, MRXS, TIFF) through tile extraction, preprocessing (stain normalization, nuclear segmentation, tissue detection), feature extraction, and machine learning. PathML integrates with popular Python ML and image processing libraries while abstracting the complexity of WSI handling through its SlideData and Pipeline abstractions.
scikit-image or cellpose directly without PathML overhead.pathml, torch, torchvision, numpy, scikit-image, openslide-pythonbash# Install system dependency first conda install -c conda-forge openslide # Install PathML pip install pathml # For GPU support pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cu118
pythonfrom pathml.core import SlideData from pathml.preprocessing import Pipeline from pathml.preprocessing.transforms import BoxBlur, TissueDetectionHE # Load → build pipeline → tile → preprocess slide = SlideData("tumor.svs", name="demo") pipeline = Pipeline([BoxBlur(kernel_size=3), TissueDetectionHE(mask_name="tissue")]) slide.run(pipeline, tile_size=256, tile_stride=256) # Inspect tiles from pathml.core import Tile tiles = [t for t in slide.tiles if t.masks["tissue"].any()] print(f"Tissue tiles: {len(tiles)} of {len(slide.tiles)}")
pythonfrom pathml.core import SlideData # Load an H&E whole-slide image slide = SlideData("path/to/slide.svs", name="tumor_slide_001") print(f"Slide name: {slide.name}") print(f"Slide shape: {slide.slide.shape}") print(f"Slide properties: {slide.slide.properties}")
pythonfrom pathml.preprocessing import Pipeline from pathml.preprocessing.transforms import ( BoxBlur, TissueDetectionHE, HEStainNormalization, ) # Build a preprocessing pipeline for H&E slides pipeline = Pipeline([ BoxBlur(kernel_size=5), # smooth image TissueDetectionHE(mask_name="tissue"), # detect tissue regions HEStainNormalization(target="normalize"), # normalize H&E staining ]) print(f"Pipeline steps: {len(pipeline.transforms)}")
pythonfrom pathml.core import TileDataset # Tile the slide into 256x256 patches at 20x magnification slide.generate_tiles( shape=(256, 256), stride=(256, 256), pad=False, level=0, # pyramid level 0 = highest resolution coords_format="fractional", ) print(f"Total tiles generated: {len(slide.tiles)}")
python# Apply preprocessing pipeline to all tiles slide.run(pipeline, distributed=False, tile_pad=False) print("Pipeline complete — tiles preprocessed") # Inspect a single tile tile = slide.tiles[0] print(f"Tile shape: {tile.image.shape}") # (256, 256, 3) print(f"Tile masks: {list(tile.masks.keys())}")
pythonfrom pathml.preprocessing.transforms import NuclearSegmentation # Run Hematoxylin-channel nuclear segmentation seg_pipeline = Pipeline([ TissueDetectionHE(mask_name="tissue"), NuclearSegmentation(mask_name="nuclei"), ]) slide.run(seg_pipeline, distributed=False) # Count nuclei per tile for tile in list(slide.tiles)[:5]: n_nuclei = tile.masks["nuclei"].max() print(f"Tile {tile.coords}: {n_nuclei} nuclei detected")
pythonimport numpy as np from pathml.core import SlideDataset features = [] for tile in slide.tiles: if "tissue" in tile.masks and tile.masks["tissue"].any(): img = tile.image feat = { "mean_r": img[:, :, 0].mean(), "mean_g": img[:, :, 1].mean(), "mean_b": img[:, :, 2].mean(), "std_r": img[:, :, 0].std(), "n_nuclei": int(tile.masks["nuclei"].max()) if "nuclei" in tile.masks else 0, "tile_x": tile.coords[0], "tile_y": tile.coords[1], } features.append(feat) import pandas as pd df = pd.DataFrame(features) df.to_csv("slide_features.csv", index=False) print(f"Extracted features from {len(df)} tissue tiles -> slide_features.csv")
pythonimport h5py # Save slide data (tiles + masks) to HDF5 slide.write("processed_slide.h5") print("Slide saved to processed_slide.h5") # Reload for downstream use from pathml.core import SlideData slide_loaded = SlideData.read("processed_slide.h5") print(f"Reloaded: {len(slide_loaded.tiles)} tiles")
| Parameter | Default | Range / Options | Effect | |-----------|---------|-----------------|--------| | shape | (256, 256) | (64,64) – (1024,1024) | Tile dimensions in pixels | | stride | equals shape | any tuple ≤ shape | Step between tile centers; stride < shape gives overlapping tiles | | level | 0 | 0 – max pyramid level | Pyramid resolution level (0 = full resolution) | | kernel_size | 5 | odd integers 3–21 | Smoothing kernel size in BoxBlur | | mask_name | required | any string | Name of output mask stored in tile.masks | | distributed | False | True, False | Enable Dask distributed processing for large slides | | pad | False | True, False | Pad edge tiles to full shape size |
When to use: Exclude background tiles to reduce memory and computation in downstream steps.
python# Filter tiles to only tissue regions after running tissue detection pipeline tissue_tiles = [t for t in slide.tiles if "tissue" in t.masks and t.masks["tissue"].mean() > 0.5] print(f"Tissue tiles: {len(tissue_tiles)} / {len(slide.tiles)} total")
When to use: Create a labeled tile dataset for training a custom classifier in PyTorch.
pythonfrom PIL import Image import numpy as np from pathlib import Path output_dir = Path("tiles_png") output_dir.mkdir(exist_ok=True) for i, tile in enumerate(slide.tiles): if "tissue" in tile.masks and tile.masks["tissue"].mean() > 0.5: img = Image.fromarray(tile.image.astype(np.uint8)) img.save(output_dir / f"tile_{i:05d}_x{tile.coords[0]}_y{tile.coords[1]}.png") print(f"Saved {i+1} tiles to {output_dir}/")
When to use: Running the same preprocessing pipeline on a directory of WSI files.
pythonfrom pathlib import Path from pathml.core import SlideData from pathml.preprocessing import Pipeline from pathml.preprocessing.transforms import TissueDetectionHE, HEStainNormalization pipeline = Pipeline([ TissueDetectionHE(mask_name="tissue"), HEStainNormalization(target="normalize"), ]) wsi_dir = Path("slides/") for wsi_path in sorted(wsi_dir.glob("*.svs")): slide = SlideData(str(wsi_path), name=wsi_path.stem) slide.generate_tiles(shape=(256, 256), stride=(256, 256), level=0) slide.run(pipeline, distributed=False) slide.write(f"processed/{wsi_path.stem}.h5") print(f"Processed {wsi_path.name}: {len(slide.tiles)} tiles")
slide.tiles — iterable of Tile objects, each with .image (numpy array) and .masks (dict of numpy arrays)slide_features.csv — tabular per-tile features (color statistics, nucleus counts, coordinates)processed_slide.h5 — HDF5 file with tiles, masks, and metadata for downstream useImageFolder dataset loading| Problem | Cause | Solution | |---------|-------|----------| | openslide.lowlevel.OpenSlideUnsupportedFormatError | OpenSlide C library not installed or WSI format unsupported | conda install -c conda-forge openslide; check format compatibility | | CUDA out of memory during segmentation | Tile size too large for GPU | Reduce tile shape to (128, 128) or run with distributed=False on CPU | | slide.tiles is empty after generate_tiles | Level index out of range or all tiles filtered | Use level=0; check slide pyramid with slide.slide.level_count | | Stain normalization produces black tiles | Source slide too low contrast or failed tissue detection | Apply TissueDetectionHE before normalization; inspect tissue mask coverage | | KeyError: 'nuclei' in tile.masks | Segmentation pipeline not yet run | Run the NuclearSegmentation pipeline with slide.run() before accessing masks | | Very slow tile generation | High-resolution level 0 on large SVS | Use a lower pyramid level (level=1 or level=2) for faster prototyping | | AttributeError: SlideData has no attribute 'write' | Old PathML version | pip install --upgrade pathml to get HDF5 save/load support |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 17,644 | 20,537 | +16% | 1 | 1 | 0% | 3,564 | 4,856 | +36% | 0 | 0 | — |
case-02 | fail→pass | 16,898 | 10,142 | -40% | 1 | 1 | 0% | 3,144 | 5,015 | +60% | 0 | 0 | — |
case-03 | fail→pass | 32,635 | 11,380 | -65% | 1 | 1 | 0% | 2,975 | 5,241 | +76% | 0 | 0 | — |
case-04 | pass→pass | 15,977 | 10,933 | -32% | 1 | 1 | 0% | 2,713 | 4,970 | +83% | 0 | 0 | — |
case-05 | fail→pass | 17,748 | 10,681 | -40% | 1 | 1 | 0% | 2,999 | 4,805 | +60% | 0 | 0 | — |
case-06 | fail→pass | 14,486 | 9,131 | -37% | 1 | 1 | 0% | 2,560 | 4,563 | +78% | 0 | 0 | — |
case-07 | pass→pass | 5,613 | 3,975 | -29% | 1 | 1 | 0% | 915 | 3,699 | +304% | 0 | 0 | — |
case-08 | pass→pass | 7,112 | 20,232 | +184% | 1 | 1 | 0% | 1,277 | 3,831 | +200% | 0 | 0 | — |
case-09 | fail→pass | 13,342 | 10,051 | -25% | 1 | 1 | 0% | 2,210 | 4,631 | +110% | 0 | 0 | — |
case-10 | fail→pass | 9,650 | 3,423 | -65% | 1 | 1 | 0% | 1,730 | 3,580 | +107% | 0 | 0 | — |
case-11 | fail→pass | 16,467 | 10,886 | -34% | 1 | 1 | 0% | 2,796 | 4,938 | +77% | 0 | 0 | — |
case-12 | pass→pass | 7,222 | 3,941 | -45% | 1 | 1 | 0% | 1,417 | 3,775 | +166% | 0 | 0 | — |
case-13 | fail→pass | 13,793 | 3,911 | -72% | 1 | 1 | 0% | 2,466 | 3,655 | +48% | 0 | 0 | — |
case-14 | fail→pass | 5,354 | 3,141 | -41% | 1 | 1 | 0% | 982 | 3,526 | +259% | 0 | 0 | — |
case-15 | fail→pass | 11,708 | 4,515 | -61% | 1 | 1 | 0% | 2,080 | 3,709 | +78% | 0 | 0 | — |
case-16 | pass→pass | 11,824 | 7,263 | -39% | 1 | 1 | 0% | 2,244 | 4,318 | +92% | 0 | 0 | — |
case-17 | pass→pass | 9,804 | 5,074 | -48% | 1 | 1 | 0% | 1,592 | 3,853 | +142% | 0 | 0 | — |
case-18 | fail→pass | 20,544 | 3,396 | -83% | 1 | 1 | 0% | 3,454 | 3,484 | +1% | 0 | 0 | — |
case-19 | fail→pass | 7,726 | 5,210 | -33% | 1 | 1 | 0% | 1,584 | 4,022 | +154% | 0 | 0 | — |
case-20 | fail→pass | 8,902 | 5,107 | -43% | 1 | 1 | 0% | 1,687 | 3,947 | +134% | 0 | 0 | — |
case-21 | pass→pass | 17,321 | 7,417 | -57% | 1 | 1 | 0% | 2,766 | 4,336 | +57% | 0 | 0 | — |
case-22 | fail→fail | 14,658 | 7,087 | -52% | 1 | 1 | 0% | 2,436 | 4,273 | +75% | 0 | 0 | — |
case-23 | fail→pass | 7,265 | 3,131 | -57% | 1 | 1 | 0% | 1,227 | 3,505 | +186% | 0 | 0 | — |
case-24 | fail→pass | 19,452 | 6,801 | -65% | 1 | 1 | 0% | 3,589 | 4,201 | +17% | 0 | 0 | — |
case-25 | fail→fail | 8,365 | 10,655 | +27% | 1 | 1 | 0% | 1,419 | 4,809 | +239% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +64 percentage points is the difference between those two pass rates over the 25 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.