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

visualization.md

181 lines · skills/histolab/references/visualization.md at 5f2bc03

Open on GitHub
1# Visualization (Histolab 0.7.0)
2
3Matplotlib is optional for these recipes. Histolab's `locate_mask` and
4`locate_tiles` return Pillow images; they do not draw into an existing Matplotlib
5axes or display a window automatically. Source:
6[Slide](https://github.com/histolab/histolab/blob/v0.7.0/histolab/slide.py) and
7[Tiler](https://github.com/histolab/histolab/blob/v0.7.0/histolab/tiler.py).
8

Visualization (Histolab 0.7.0)

Matplotlib is optional for these recipes. Histolab's locate_mask and locate_tiles return Pillow images; they do not draw into an existing Matplotlib axes or display a window automatically. Source: Slide and Tiler.

Raw lines 1 to 8
9## Save and display previews
11```python
12from pathlib import Path
13import matplotlib.pyplot as plt
14from histolab.masks import TissueMask
15from histolab.tiler import RandomTiler
17output = Path("output/previews")
18output.mkdir(parents=True, exist_ok=True)
19mask = TissueMask()
20tiler = RandomTiler(tile_size=(256, 256), n_tiles=20, seed=42)
21mask_preview = slide.locate_mask(mask)
22tile_preview = tiler.locate_tiles(slide, extraction_mask=mask)
23mask_preview.save(output / "mask.png")
24tile_preview.save(output / "tiles.png")
25fig, axes = plt.subplots(1, 2, figsize=(12, 6))
26axes[0].imshow(mask_preview)
27axes[1].imshow(tile_preview)
28for ax in axes:
29 ax.axis("off")
30fig.savefig(output / "previews.png", dpi=150, bbox_inches="tight")
31plt.close(fig)
32```
34Grid and score tilers have the same preview signature; no preview method accepts
35`n_tiles`. Set the count in the RandomTiler/ScoreTiler constructor. ScoreTiler
36previews rank the full eligible grid even when only a few boxes are shown.

Save and display previews

from pathlib import Path
import matplotlib.pyplot as plt
from histolab.masks import TissueMask
from histolab.tiler import RandomTiler

output = Path("output/previews")
output.mkdir(parents=True, exist_ok=True)
mask = TissueMask()
tiler = RandomTiler(tile_size=(256, 256), n_tiles=20, seed=42)
mask_preview = slide.locate_mask(mask)
tile_preview = tiler.locate_tiles(slide, extraction_mask=mask)
mask_preview.save(output / "mask.png")
tile_preview.save(output / "tiles.png")
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
axes[0].imshow(mask_preview)
axes[1].imshow(tile_preview)
for ax in axes:
    ax.axis("off")
fig.savefig(output / "previews.png", dpi=150, bbox_inches="tight")
plt.close(fig)

Grid and score tilers have the same preview signature; no preview method accepts n_tiles. Set the count in the RandomTiler/ScoreTiler constructor. ScoreTiler previews rank the full eligible grid even when only a few boxes are shown.

Raw lines 9 to 37
38## Correctly align mask overlays
40The mask shape can exceed the thumbnail shape in 0.7.0. Resample labels with
41nearest-neighbor interpolation and use the same image extent for both layers.
43<!-- recipe: mask-overlay -->
44```python
45import numpy as np
46from PIL import Image
47import matplotlib.pyplot as plt
48from histolab.masks import TissueMask
50def plot_mask_overlay(slide, mask=None):
51 mask = TissueMask() if mask is None else mask
52 thumbnail = slide.thumbnail
53 raw_mask = mask(slide)
54 resized = Image.fromarray(raw_mask.astype(np.uint8)).resize(
55 thumbnail.size, Image.Resampling.NEAREST,
56 )
57 labels = np.asarray(resized, dtype=bool)
58 fig, ax = plt.subplots()
59 ax.imshow(thumbnail)
60 ax.imshow(np.ma.masked_where(~labels, labels), cmap="Reds", alpha=0.35, vmin=0, vmax=1)
61 ax.set_title("Detected tissue")
62 ax.axis("off")
63 return fig
64```
66For custom coordinate overlays, scale X and Y independently from level-0 slide
67bounds to the displayed image. Use the actual saved coordinate box width and
68height; a fixed 512-pixel box is wrong at other levels or resolutions.

Correctly align mask overlays

The mask shape can exceed the thumbnail shape in 0.7.0. Resample labels with nearest-neighbor interpolation and use the same image extent for both layers.

<!-- recipe: mask-overlay -->

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from histolab.masks import TissueMask

def plot_mask_overlay(slide, mask=None):
    mask = TissueMask() if mask is None else mask
    thumbnail = slide.thumbnail
    raw_mask = mask(slide)
    resized = Image.fromarray(raw_mask.astype(np.uint8)).resize(
        thumbnail.size, Image.Resampling.NEAREST,
    )
    labels = np.asarray(resized, dtype=bool)
    fig, ax = plt.subplots()
    ax.imshow(thumbnail)
    ax.imshow(np.ma.masked_where(~labels, labels), cmap="Reds", alpha=0.35, vmin=0, vmax=1)
    ax.set_title("Detected tissue")
    ax.axis("off")
    return fig

For custom coordinate overlays, scale X and Y independently from level-0 slide bounds to the displayed image. Use the actual saved coordinate box width and height; a fixed 512-pixel box is wrong at other levels or resolutions.

Raw lines 38 to 69
70## Tile mosaic (not a spatial reconstruction)
72<!-- recipe: mosaic -->
73```python
74from pathlib import Path
75from PIL import Image
76import matplotlib.pyplot as plt
78def create_tile_mosaic(tile_dir, grid_size=(4, 4), pattern="*.png"):
79 rows, columns = grid_size
80 if rows <= 0 or columns <= 0:
81 raise ValueError("Mosaic dimensions must be positive")
82 paths = sorted(Path(tile_dir).glob(pattern))[:rows * columns]
83 fig, axes = plt.subplots(rows, columns, figsize=(3 * columns, 3 * rows), squeeze=False)
84 for ax in axes.ravel():
85 ax.axis("off")
86 for ax, path in zip(axes.ravel(), paths):
87 with Image.open(path) as tile_image:
88 ax.imshow(tile_image.convert("RGB"))
89 ax.set_title(path.stem, fontsize=7)
90 fig.tight_layout()
91 return fig
92```
94Use a tile filename pattern or a directory containing only tiles so previews
95are not counted as samples. Sorted filename order is not slide spatial order.
96The function also handles 1x1 grids, empty directories, and fewer files than cells.

Tile mosaic (not a spatial reconstruction)

<!-- recipe: mosaic -->

from pathlib import Path
from PIL import Image
import matplotlib.pyplot as plt

def create_tile_mosaic(tile_dir, grid_size=(4, 4), pattern="*.png"):
    rows, columns = grid_size
    if rows <= 0 or columns <= 0:
        raise ValueError("Mosaic dimensions must be positive")
    paths = sorted(Path(tile_dir).glob(pattern))[:rows * columns]
    fig, axes = plt.subplots(rows, columns, figsize=(3 * columns, 3 * rows), squeeze=False)
    for ax in axes.ravel():
        ax.axis("off")
    for ax, path in zip(axes.ravel(), paths):
        with Image.open(path) as tile_image:
            ax.imshow(tile_image.convert("RGB"))
        ax.set_title(path.stem, fontsize=7)
    fig.tight_layout()
    return fig

Use a tile filename pattern or a directory containing only tiles so previews are not counted as samples. Sorted filename order is not slide spatial order. The function also handles 1x1 grids, empty directories, and fewer files than cells.

Raw lines 70 to 97
98## Tile mask visualization
100```python
101# tile is a Tile returned by slide.extract_tile(...).
102import matplotlib.pyplot as plt
104tile.calculate_tissue_mask()
105fig, axes = plt.subplots(1, 2)
106axes[0].imshow(tile.image)
107axes[1].imshow(tile.tissue_mask, cmap="gray")
108axes[1].set_title(f"Detected tissue: {tile.tissue_ratio:.1%}")
109for ax in axes:
110 ax.axis("off")
111plt.close(fig)
112```
114Mask fraction is an algorithm-dependent area estimate, not histological cell
115fraction. `Tile` construction needs a four-coordinate `CoordinatePair`, as in
116the filters reference.

Tile mask visualization

# tile is a Tile returned by slide.extract_tile(...).
import matplotlib.pyplot as plt

tile.calculate_tissue_mask()
fig, axes = plt.subplots(1, 2)
axes[0].imshow(tile.image)
axes[1].imshow(tile.tissue_mask, cmap="gray")
axes[1].set_title(f"Detected tissue: {tile.tissue_ratio:.1%}")
for ax in axes:
    ax.axis("off")
plt.close(fig)

Mask fraction is an algorithm-dependent area estimate, not histological cell fraction. Tile construction needs a four-coordinate CoordinatePair, as in the filters reference.

Raw lines 98 to 117
118## Score plots and saved tile inspection
120Only `filename`, `score` and `scaled_score` are supplied in ScoreTiler CSVs.
121There is no `tissue_percent` or `tile_name` column. Use the raw score histogram;
122normalized scores can be non-finite when candidate scores are constant and are
123not comparable probabilities across slides.
125<!-- recipe: score-plot -->
126```python
127import csv
128import math
129import matplotlib.pyplot as plt
131def plot_score_report(report_path):
132 with open(report_path, newline="") as stream:
133 rows = list(csv.DictReader(stream))
134 scores = [float(row["score"]) for row in rows]
135 if not scores or not all(math.isfinite(value) for value in scores):
136 raise ValueError("Report needs nonempty, finite raw scores")
137 fig, ax = plt.subplots()
138 ax.hist(scores, bins=min(20, len(scores)))
139 ax.set_xlabel("Raw tile score")
140 ax.set_ylabel("Saved tile count")
141 return fig
142```
144To inspect extremes, sort these rows by `float(row["score"])` and open
145`Path(slide.processed_path) / row["filename"]`. The report contains only saved
146winners: its lowest row may still be high-ranked in the full candidate pool.
147Calculate any tissue-coverage scatter data separately with a named algorithm.

Score plots and saved tile inspection

Only filename, score and scaled_score are supplied in ScoreTiler CSVs. There is no tissue_percent or tile_name column. Use the raw score histogram; normalized scores can be non-finite when candidate scores are constant and are not comparable probabilities across slides.

<!-- recipe: score-plot -->

import csv
import math
import matplotlib.pyplot as plt

def plot_score_report(report_path):
    with open(report_path, newline="") as stream:
        rows = list(csv.DictReader(stream))
    scores = [float(row["score"]) for row in rows]
    if not scores or not all(math.isfinite(value) for value in scores):
        raise ValueError("Report needs nonempty, finite raw scores")
    fig, ax = plt.subplots()
    ax.hist(scores, bins=min(20, len(scores)))
    ax.set_xlabel("Raw tile score")
    ax.set_ylabel("Saved tile count")
    return fig

To inspect extremes, sort these rows by float(row["score"]) and open Path(slide.processed_path) / row["filename"]. The report contains only saved winners: its lowest row may still be high-ranked in the full candidate pool. Calculate any tissue-coverage scatter data separately with a named algorithm.

Raw lines 118 to 148
149## Slide comparisons, figures and PDF reports
151For cohort QC, record each slide's mask fraction (`mask.mean() * 100`) alongside
152stain, scanner, mask resolution and parameters; differing mask resolution can
153change morphology and the resulting fraction. Do not hold all slides open at
154once. A high DPI export improves labels but adds no WSI image detail to a small
155thumbnail.
157<!-- recipe: pdf-preview -->
158```python
159import matplotlib.pyplot as plt
160from matplotlib.backends.backend_pdf import PdfPages
162def save_preview_pdf(slide, mask, tiler, path):
163 images = [
164 ("Slide thumbnail", slide.thumbnail),
165 ("Mask", slide.locate_mask(mask)),
166 ("Tile positions", tiler.locate_tiles(slide, extraction_mask=mask)),
167 ]
168 with PdfPages(path) as pdf:
169 for title, image in images:
170 fig, ax = plt.subplots(figsize=(8, 8))
171 ax.imshow(image)
172 ax.set_title(title)
173 ax.axis("off")
174 pdf.savefig(fig, bbox_inches="tight")
175 plt.close(fig)
176```
178This explicitly draws the returned preview on each axes before saving, avoiding
179blank tile-location PDF pages. Notebook users may call `display(preview)`.
180Optional widget UIs should rebuild a `Compose` chain from `image_filters` when
181parameters change; they were not executed in this review.

Slide comparisons, figures and PDF reports

For cohort QC, record each slide's mask fraction (mask.mean() * 100) alongside stain, scanner, mask resolution and parameters; differing mask resolution can change morphology and the resulting fraction. Do not hold all slides open at once. A high DPI export improves labels but adds no WSI image detail to a small thumbnail.

<!-- recipe: pdf-preview -->

import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages

def save_preview_pdf(slide, mask, tiler, path):
    images = [
        ("Slide thumbnail", slide.thumbnail),
        ("Mask", slide.locate_mask(mask)),
        ("Tile positions", tiler.locate_tiles(slide, extraction_mask=mask)),
    ]
    with PdfPages(path) as pdf:
        for title, image in images:
            fig, ax = plt.subplots(figsize=(8, 8))
            ax.imshow(image)
            ax.set_title(title)
            ax.axis("off")
            pdf.savefig(fig, bbox_inches="tight")
            plt.close(fig)

This explicitly draws the returned preview on each axes before saving, avoiding blank tile-location PDF pages. Notebook users may call display(preview). Optional widget UIs should rebuild a Compose chain from image_filters when parameters change; they were not executed in this review.

Raw lines 149 to 181