Skill Tracer
Skills › Scientific Agent Skills › histolab › filters_preprocessing.md

filters_preprocessing.md

165 lines · skills/histolab/references/filters_preprocessing.md at 5f2bc03

Open on GitHub
1# Filters and preprocessing (Histolab 0.7.0)
2
3Sources: [image filters](https://github.com/histolab/histolab/blob/v0.7.0/histolab/filters/image_filters.py),
4[morphology](https://github.com/histolab/histolab/blob/v0.7.0/histolab/filters/morphological_filters.py),
5[Tile](https://github.com/histolab/histolab/blob/v0.7.0/histolab/tile.py), and
6[stain normalizers](https://github.com/histolab/histolab/blob/v0.7.0/histolab/stain_normalizer.py).
7

Filters and preprocessing (Histolab 0.7.0)

Sources: image filters, morphology, Tile, and stain normalizers.

Raw lines 1 to 7
8## Types matter
9
10Import `Compose` from **`histolab.filters.image_filters`**, not
11`histolab.filters.compositions`. The latter provides `FiltersComposition`, an
12internal/default-pipeline dispatcher. `Compose` applies a list of callables in
13sequence; it does not adapt incompatible return/input types automatically.
15| Filter | Output | Practical use |
16| --- | --- | --- |
17| `RgbToGrayscale()` | Pillow L image | Input to intensity thresholds |
18| `RgbToHsv()`, `RgbToHed()` | Floating NumPy array | Color or stain analysis; not RGB display data |
19| `HematoxylinChannel()`, `EosinChannel()`, `DABChannel()` | Pillow RGB image | Visualized stain channel |
20| `OtsuThreshold()` | Boolean array, darker pixels by default | Binary foreground mask |
21| `LocalOtsuThreshold(disk_size=3)` | Pillow local threshold map | Compare grayscale intensities to thresholds explicitly |
22| `StretchContrast()`, `HistogramEqualization()`, `AdaptiveEqualization()` | Pillow image | Contrast visualization, not stain calibration |
23| `Invert()` | Pillow image | Intensity inversion |
24| `BinaryDilation`, `BinaryErosion`, `BinaryOpening`, `BinaryClosing` | Boolean array | Mask morphology; `disk_size` in input mask pixels |
25| `RemoveSmallObjects`, `RemoveSmallHoles` | Boolean array | Area threshold in mask pixels |
27Histolab 0.7.0 has no `AdaptiveThreshold` class. `LocalOtsuThreshold` is not
28adaptive mean thresholding with `block_size`/`offset`. Its implementation returns
29the local threshold values, despite docstrings suggesting a binary mask:
31<!-- recipe: local-otsu -->
32```python
33import numpy as np
34from histolab.filters.image_filters import RgbToGrayscale, LocalOtsuThreshold
36def local_dark_mask(rgb_image):
37 gray = RgbToGrayscale()(rgb_image)
38 thresholds = LocalOtsuThreshold(disk_size=3)(gray)
39 return np.asarray(gray) < np.asarray(thresholds)
40```

Types matter

Import Compose from **histolab.filters.image_filters**, not histolab.filters.compositions. The latter provides FiltersComposition, an internal/default-pipeline dispatcher. Compose applies a list of callables in sequence; it does not adapt incompatible return/input types automatically.

FilterOutputPractical use
RgbToGrayscale()Pillow L imageInput to intensity thresholds
RgbToHsv(), RgbToHed()Floating NumPy arrayColor or stain analysis; not RGB display data
HematoxylinChannel(), EosinChannel(), DABChannel()Pillow RGB imageVisualized stain channel
OtsuThreshold()Boolean array, darker pixels by defaultBinary foreground mask
LocalOtsuThreshold(disk_size=3)Pillow local threshold mapCompare grayscale intensities to thresholds explicitly
StretchContrast(), HistogramEqualization(), AdaptiveEqualization()Pillow imageContrast visualization, not stain calibration
Invert()Pillow imageIntensity inversion
BinaryDilation, BinaryErosion, BinaryOpening, BinaryClosingBoolean arrayMask morphology; disk_size in input mask pixels
RemoveSmallObjects, RemoveSmallHolesBoolean arrayArea threshold in mask pixels

Histolab 0.7.0 has no AdaptiveThreshold class. LocalOtsuThreshold is not adaptive mean thresholding with block_size/offset. Its implementation returns the local threshold values, despite docstrings suggesting a binary mask:

<!-- recipe: local-otsu -->

import numpy as np
from histolab.filters.image_filters import RgbToGrayscale, LocalOtsuThreshold

def local_dark_mask(rgb_image):
    gray = RgbToGrayscale()(rgb_image)
    thresholds = LocalOtsuThreshold(disk_size=3)(gray)
    return np.asarray(gray) < np.asarray(thresholds)
Raw lines 8 to 41
42## Tissue pipeline
44<!-- recipe: filter-pipeline -->
45```python
46from histolab.filters.image_filters import Compose, RgbToGrayscale, OtsuThreshold
47from histolab.filters.morphological_filters import (
48 BinaryDilation, RemoveSmallHoles, RemoveSmallObjects,
50from histolab.masks import TissueMask
52tissue_filters = [
53 RgbToGrayscale(), OtsuThreshold(), BinaryDilation(disk_size=2),
54 RemoveSmallHoles(area_threshold=100),
55 RemoveSmallObjects(min_size=64, avoid_overmask=False),
57tissue_detection = Compose(tissue_filters)
58custom_mask = TissueMask(*tissue_filters)
59```
61Apply `tissue_detection(rgb_image)` to a Pillow RGB image. These thresholds are
62illustrative, not universal. Larger removal thresholds discard all components
63below the size cutoff, including small real tissue, not just artifacts.
64`RemoveSmallObjects` uses `min_size`; `RemoveSmallHoles` uses `area_threshold`.
65With `avoid_overmask=True` (the default), `RemoveSmallObjects` can recursively
66reduce its cutoff; disable that behavior for a fixed-size rule.

Tissue pipeline

<!-- recipe: filter-pipeline -->

from histolab.filters.image_filters import Compose, RgbToGrayscale, OtsuThreshold
from histolab.filters.morphological_filters import (
    BinaryDilation, RemoveSmallHoles, RemoveSmallObjects,
)
from histolab.masks import TissueMask

tissue_filters = [
    RgbToGrayscale(), OtsuThreshold(), BinaryDilation(disk_size=2),
    RemoveSmallHoles(area_threshold=100),
    RemoveSmallObjects(min_size=64, avoid_overmask=False),
]
tissue_detection = Compose(tissue_filters)
custom_mask = TissueMask(*tissue_filters)

Apply tissue_detection(rgb_image) to a Pillow RGB image. These thresholds are illustrative, not universal. Larger removal thresholds discard all components below the size cutoff, including small real tissue, not just artifacts. RemoveSmallObjects uses min_size; RemoveSmallHoles uses area_threshold. With avoid_overmask=True (the default), RemoveSmallObjects can recursively reduce its cutoff; disable that behavior for a fixed-size rule.

Raw lines 42 to 67
68## Lambda filters and stain channels
70<!-- recipe: lambda-filters -->
71```python
72import numpy as np
73from PIL import Image
74from histolab.filters.image_filters import Lambda, Compose, HematoxylinChannel, RgbToGrayscale, HistogramEqualization
76brightness_filter = Lambda(lambda image: Image.fromarray(
77 np.clip(np.asarray(image, dtype=float) * 1.2, 0, 255).astype(np.uint8)
78))
79red_channel_filter = Lambda(lambda image: np.asarray(image)[:, :, 0])
80nuclei_view = Compose([
81 HematoxylinChannel(), RgbToGrayscale(), HistogramEqualization(),
82])
83```
85Pillow images do not support NumPy-style slicing or multiplication; convert
86explicitly. Do not pass an HED ndarray directly into a filter expecting Pillow.
87An HED array needs the matching inverse color transformation to return to RGB.
88Arbitrary per-channel means/stds without a fitted target, finite checks, and an
89inverse transformation are not stain normalization.
91The `BluePenFilter`, `GreenPenFilter` and `RedPenFilter` return RGB images with
92detected ink pixels blacked out. Derive a Boolean exclusion mask by comparing
93filtered and original pixels; see the tissue-mask reference. Simple color rules
94can remove real stain, and blacked-out pixels are not restored tissue.

Lambda filters and stain channels

<!-- recipe: lambda-filters -->

import numpy as np
from PIL import Image
from histolab.filters.image_filters import Lambda, Compose, HematoxylinChannel, RgbToGrayscale, HistogramEqualization

brightness_filter = Lambda(lambda image: Image.fromarray(
    np.clip(np.asarray(image, dtype=float) * 1.2, 0, 255).astype(np.uint8)
))
red_channel_filter = Lambda(lambda image: np.asarray(image)[:, :, 0])
nuclei_view = Compose([
    HematoxylinChannel(), RgbToGrayscale(), HistogramEqualization(),
])

Pillow images do not support NumPy-style slicing or multiplication; convert explicitly. Do not pass an HED ndarray directly into a filter expecting Pillow. An HED array needs the matching inverse color transformation to return to RGB. Arbitrary per-channel means/stds without a fitted target, finite checks, and an inverse transformation are not stain normalization.

The BluePenFilter, GreenPenFilter and RedPenFilter return RGB images with detected ink pixels blacked out. Derive a Boolean exclusion mask by comparing filtered and original pixels; see the tissue-mask reference. Simple color rules can remove real stain, and blacked-out pixels are not restored tissue.

Raw lines 68 to 95
96## Tile filtering
98<!-- recipe: tile-filter -->
99```python
100from histolab.tile import Tile
101from histolab.types import CoordinatePair
102from histolab.filters.image_filters import Compose, RgbToGrayscale, StretchContrast
104# pil_image is an existing Pillow RGB tile at level 0.
105width, height = pil_image.size
106tile = Tile(pil_image, CoordinatePair(0, 0, width, height), level=0)
107processed_tile = tile.apply_filters(Compose([RgbToGrayscale(), StretchContrast()]))
108assert processed_tile.coords == tile.coords
109```
111`apply_filters` returns a new Tile and preserves coordinates/level. A binary
112filter chain produces a mask image in the returned Tile, not a normalized RGB
113tile. Do not replace model RGB inputs with masks accidentally.

Tile filtering

<!-- recipe: tile-filter -->

from histolab.tile import Tile
from histolab.types import CoordinatePair
from histolab.filters.image_filters import Compose, RgbToGrayscale, StretchContrast

# pil_image is an existing Pillow RGB tile at level 0.
width, height = pil_image.size
tile = Tile(pil_image, CoordinatePair(0, 0, width, height), level=0)
processed_tile = tile.apply_filters(Compose([RgbToGrayscale(), StretchContrast()]))
assert processed_tile.coords == tile.coords

apply_filters returns a new Tile and preserves coordinates/level. A binary filter chain produces a mask image in the returned Tile, not a normalized RGB tile. Do not replace model RGB inputs with masks accidentally.

Raw lines 96 to 114
115## Stain normalization
117Both normalizers fit a representative **target** image and transform source
118images. Fit on training data only and preserve the target image/hash and method
119parameters. This is tested with synthetic varying-color images; that verifies
120API behavior, not stain fidelity on a real cohort.
122<!-- recipe: normalize -->
123```python
124import numpy as np
125from histolab.stain_normalizer import MacenkoStainNormalizer, ReinhardStainNormalizer
127# target_image and source_image are Pillow RGB images with varied tissue colors.
128normalized_images = {}
129for normalizer_class in (MacenkoStainNormalizer, ReinhardStainNormalizer):
130 normalizer = normalizer_class()
131 normalizer.fit(target_image.convert("RGB"))
132 normalized = normalizer.transform(source_image.convert("RGB"))
133 pixels = np.asarray(normalized)
134 if normalized.size != source_image.size or not np.isfinite(pixels).all():
135 raise ValueError("Normalization produced an invalid image")
136 normalized_images[normalizer_class.__name__] = normalized
137```
139Check meaningful tissue coverage and color variation before fitting. Blank or
140nearly constant channels can cause singular stain estimates or zero variance.
141Finite uint8 output alone does not detect clipped or implausible color transfer;
142visually inspect the result and compare downstream performance. Macenko is an
143H&E-oriented stain-matrix method; validate any other staining protocol separately.
144Keep original tiles. Contrast equalization and normalization can alter quantitative
145intensity measurements and should not be applied blindly to biomarker analysis.

Stain normalization

Both normalizers fit a representative target image and transform source images. Fit on training data only and preserve the target image/hash and method parameters. This is tested with synthetic varying-color images; that verifies API behavior, not stain fidelity on a real cohort.

<!-- recipe: normalize -->

import numpy as np
from histolab.stain_normalizer import MacenkoStainNormalizer, ReinhardStainNormalizer

# target_image and source_image are Pillow RGB images with varied tissue colors.
normalized_images = {}
for normalizer_class in (MacenkoStainNormalizer, ReinhardStainNormalizer):
    normalizer = normalizer_class()
    normalizer.fit(target_image.convert("RGB"))
    normalized = normalizer.transform(source_image.convert("RGB"))
    pixels = np.asarray(normalized)
    if normalized.size != source_image.size or not np.isfinite(pixels).all():
        raise ValueError("Normalization produced an invalid image")
    normalized_images[normalizer_class.__name__] = normalized

Check meaningful tissue coverage and color variation before fitting. Blank or nearly constant channels can cause singular stain estimates or zero variance. Finite uint8 output alone does not detect clipped or implausible color transfer; visually inspect the result and compare downstream performance. Macenko is an H&E-oriented stain-matrix method; validate any other staining protocol separately. Keep original tiles. Contrast equalization and normalization can alter quantitative intensity measurements and should not be applied blindly to biomarker analysis.

Raw lines 115 to 146
147## Tissue percentage QC
149<!-- recipe: coverage -->
150```python
151import numpy as np
152from histolab.filters.image_filters import Compose, RgbToGrayscale, OtsuThreshold
154def tissue_coverage(image):
155 mask = Compose([RgbToGrayscale(), OtsuThreshold()])(image)
156 return float(np.count_nonzero(mask) / mask.size * 100)
157```
159This is a simple Otsu dark-pixel fraction. It differs from `Tile.tissue_ratio`
160and `has_enough_tissue`, which use the tile mask pipeline and additional tissue
161checks. Record the definition with the number; do not interchange these metrics.
163For local contrast experiments and interactive sliders, keep display-only
164transforms separate from extraction and record parameter choices. The optional
165OpenCV blur recipe appears in the tile-extraction reference.

Tissue percentage QC

<!-- recipe: coverage -->

import numpy as np
from histolab.filters.image_filters import Compose, RgbToGrayscale, OtsuThreshold

def tissue_coverage(image):
    mask = Compose([RgbToGrayscale(), OtsuThreshold()])(image)
    return float(np.count_nonzero(mask) / mask.size * 100)

This is a simple Otsu dark-pixel fraction. It differs from Tile.tissue_ratio and has_enough_tissue, which use the tile mask pipeline and additional tissue checks. Record the definition with the number; do not interchange these metrics.

For local contrast experiments and interactive sliders, keep display-only transforms separate from extraction and record parameter choices. The optional OpenCV blur recipe appears in the tile-extraction reference.

Raw lines 147 to 165