Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
044fa01
feat: try to migrate changes to RatioPath from RationAI masks
AdamBajger Jan 9, 2026
12aaf8f
refactor: split into files
AdamBajger Jan 9, 2026
d29feb7
Update ratiopath/masks/write_big_tiff.py
vejtek Jan 9, 2026
f8002d4
fix: add numpy and jaxtyping imports to mask builder modules
AdamBajger Jan 9, 2026
dc4b7be
Update ratiopath/masks/mask_builders/receptive_field_manipulation.py
AdamBajger Jan 9, 2026
f4ec8c7
Update pyproject.toml
AdamBajger Jan 9, 2026
3ded496
fix: overlap naming convention
AdamBajger Jan 9, 2026
215b962
fix: all imports correction
AdamBajger Jan 9, 2026
a4e8202
refactor: move __all__ exports to top in mask builder module __init__
AdamBajger Jan 9, 2026
c5405cf
chore: sync lockfile
AdamBajger Jan 9, 2026
87723c3
fix: typo bracket
AdamBajger Jan 9, 2026
39c34c6
chore: ruff check and format
AdamBajger Jan 9, 2026
40807ee
debug: install libvips and openslide
AdamBajger Jan 9, 2026
e285b6a
fix: test filenames
AdamBajger Jan 9, 2026
561cb6a
fix: update geopandas dependency version and refactor GeoJSONParser l…
Jan 9, 2026
6531f87
fix: update lock + freeze sync
matejpekar Jan 10, 2026
c2042d0
fix: mypy
matejpekar Jan 10, 2026
fd1effb
fix: unlink overlaps file in tests which was previously left linked
AdamBajger Jan 13, 2026
74d2180
Initial plan
Copilot Jan 15, 2026
f52a84e
fix: correct module and class names in example code
Copilot Jan 15, 2026
d5747e3
fix: update docstring Args to match constructor signature
Copilot Jan 15, 2026
ee62864
Update tests/test_mask_builders.py
AdamBajger Jan 15, 2026
a62c3c8
Update ratiopath/masks/mask_builders/__init__.py
AdamBajger Jan 15, 2026
3d67b05
Initial plan
Copilot Jan 15, 2026
75f443a
fix: correct typo in test docstring (SImple → Simple)
Copilot Jan 15, 2026
93f06dd
Merge pull request #23 from RationAI/copilot/sub-pr-22
AdamBajger Jan 15, 2026
6c7bf95
Merge pull request #24 from RationAI/copilot/sub-pr-22-again
AdamBajger Jan 15, 2026
b5a60df
Apply suggestions from code review
AdamBajger Jan 15, 2026
35565d5
Update tests/test_mask_builders.py
AdamBajger Jan 15, 2026
bc94f4e
Initial plan
Copilot Jan 15, 2026
b9874c2
chore: run ruff format to fix linting issues
Copilot Jan 15, 2026
c4519e3
Merge pull request #25 from RationAI/copilot/sub-pr-22-another-one
AdamBajger Jan 16, 2026
d766442
docs: fix example code
AdamBajger Jan 19, 2026
1645cd0
refactor: remove obsolete field
AdamBajger Jan 19, 2026
51921b1
chore: replace ellipsis by pass
AdamBajger Jan 21, 2026
2c15c1c
Initial plan
Copilot Jan 21, 2026
cb02638
fix: correct docstring example - remove duplicate import, add numpy, …
Copilot Jan 21, 2026
736853a
fix: update all mask builder docstring examples with correct API sign…
Copilot Jan 21, 2026
bb29f92
docs: add mask builders documentation and run ruff format
Copilot Jan 21, 2026
1da68db
docs: validate mkdocs builds successfully
Copilot Jan 21, 2026
6b27da0
chore: add site/ to gitignore and remove from git
Copilot Jan 21, 2026
cf42a1d
docs: clarify generate_tiles_from_slide is a placeholder function
Copilot Jan 21, 2026
5388463
Merge pull request #26 from RationAI/copilot/sub-pr-22-yet-again
AdamBajger Jan 21, 2026
cc6d41b
fix: remove unnecessary pass to satisfylinter ruff
AdamBajger Jan 21, 2026
e207fcf
Initial plan
Copilot Jan 21, 2026
45db890
chore: run ruff format to fix linting errors
Copilot Jan 21, 2026
3177f5d
Merge pull request #27 from RationAI/copilot/sub-pr-22-one-more-time
AdamBajger Jan 21, 2026
af4fd45
fix: add explicit dtype parameter
AdamBajger Jan 22, 2026
3972bf9
docs: add docstrings
AdamBajger Jan 22, 2026
466dfca
fix: inheritance param mismatches
AdamBajger Jan 22, 2026
5022161
fix: bump numpy version
AdamBajger Jan 22, 2026
b87ce27
fix: ruff formatting and linting
AdamBajger Jan 22, 2026
8552d3c
docs: fix OpenSLide level_dimensions use in examples
AdamBajger Jan 23, 2026
aa959d4
fix: enhance memory setup in AutoScalingAveragingClippingNumpyMemMapM…
AdamBajger Jan 23, 2026
915cd41
chore: ruff format
AdamBajger Jan 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ jobs:
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install dependencies
run: uv sync --locked --extra docs
run: uv sync --frozen --extra docs

- name: Cache MkDocs / Material
run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
uses: astral-sh/setup-uv@v6

- name: Install dependencies
run: uv sync --locked
run: uv sync --frozen

- name: Build package
run: uv build

Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,20 @@ jobs:
steps:
- uses: actions/checkout@v4

# Install Openslide dependencies
- name: Install Openslide dependencies
run: |
sudo apt update
sudo apt install python3-openslide

- name: Setup LibVips
run: sudo apt install -y libvips-dev

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Install dependencies
run: uv sync --dev --extra tests
run: uv sync --frozen --dev --extra tests

- name: Run Pytest
run: uv run pytest
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ wheels/
.mypy_cache/

# VS Code
.vscode/
.vscode/

# MkDocs
site/
254 changes: 254 additions & 0 deletions docs/reference/masks/mask_builders.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# Mask Builders

Mask builders are tools for assembling feature masks from neural network predictions or other tile-level data. They handle the complexity of combining overlapping tiles, scaling between coordinate spaces, and managing memory for large output masks.

## Overview

When processing whole-slide images with neural networks, you often need to:

1. Extract tiles from a slide
2. Run inference to get predictions or features for each tile
3. Assemble these predictions back into a full-resolution mask

Mask builders automate step 3, handling:

- **Coordinate transformation**: Converting from tile coordinates to mask coordinates
- **Overlap handling**: Averaging or taking the maximum when tiles overlap
- **Memory management**: Using in-memory arrays or memory-mapped files for large masks
- **Edge clipping**: Removing boundary artifacts from tiles

## Available Builders

### AveragingScalarUniformTiledNumpyMaskBuilder

::: ratiopath.masks.mask_builders.AveragingScalarUniformTiledNumpyMaskBuilder

**Use case**: You have scalar predictions (e.g., class probabilities) for each tile and want to create a mask where each prediction is uniformly expanded to fill the tile's footprint. Overlapping regions are averaged.

**Example**: Creating a heatmap of tumor probability predictions.

```python
import numpy as np
import openslide
from ratiopath.masks.mask_builders import (
AveragingScalarUniformTiledNumpyMaskBuilder,
)
import matplotlib.pyplot as plt

# Open slide and set up tiling parameters
LEVEL = 3
tile_extents = (512, 512)
tile_strides = (256, 256)
slide = openslide.OpenSlide("path/to/slide.mrxs")
slide_extent_x, slide_extent_y = slide.level_dimensions[LEVEL]

# Load your model
vgg16_model = load_vgg16_model(...) # load your pretrained model here

# Initialize mask builder
mask_builder = AveragingScalarUniformTiledNumpyMaskBuilder(
mask_extents=(slide_extent_y, slide_extent_x),
channels=1, # for binary classification
mask_tile_extents=tile_extents,
mask_tile_strides=tile_strides,
)

# Process tiles
# Note: generate_tiles_from_slide is a placeholder - you must implement your own tile extraction logic
for tiles, xs, ys in generate_tiles_from_slide(
slide, LEVEL, tile_extents, tile_strides, batch_size=32
):
# tiles has shape (B, C, H, W)
features = vgg16_model.predict(tiles) # features has shape (B, channels)
# Stack ys and xs into coords_batch with shape (N, B) where N=2 (y, x dimensions)
coords_batch = np.stack([ys, xs], axis=0)
mask_builder.update_batch(features, coords_batch)

# Finalize and visualize
assembled_mask, overlap = mask_builder.finalize()
plt.imshow(assembled_mask[0], cmap="gray", interpolation="nearest")
plt.axis("off")
plt.show()
```

---

### MaxScalarUniformTiledNumpyMaskBuilder

::: ratiopath.masks.mask_builders.MaxScalarUniformTiledNumpyMaskBuilder

**Use case**: Similar to the averaging builder, but takes the maximum value at each pixel instead of averaging. Useful when you want to preserve the strongest signal.

**Example**: Creating activation maps from intermediate network layers.

```python
import numpy as np
import openslide
from ratiopath.masks.mask_builders import MaxScalarUniformTiledNumpyMaskBuilder
import matplotlib.pyplot as plt
from rationai.explainability.model_probing import HookedModule

LEVEL = 3
tile_extents = (512, 512)
tile_strides = (256, 256)
slide = openslide.OpenSlide("path/to/slide.mrxs")
slide_extent_x, slide_extent_y = slide.level_dimensions[LEVEL]

# Set up model with hooks to extract intermediate activations
vgg16_model = load_vgg16_model(...)
hooked_model = HookedModule(vgg16_model, layer_name="backbone.9")

mask_builder = MaxScalarUniformTiledNumpyMaskBuilder(
mask_extents=(slide_extent_y, slide_extent_x),
channels=1,
mask_tile_extents=tile_extents,
mask_tile_strides=tile_strides,
)

# Note: generate_tiles_from_slide is a placeholder - you must implement your own tile extraction logic
for tiles, xs, ys in generate_tiles_from_slide(
slide, LEVEL, tile_extents, tile_strides, batch_size=32
):
# tiles has shape (B, C, H, W)
outputs = hooked_model.predict(tiles) # outputs are not used directly
features = hooked_model.get_activations("backbone.9") # shape (B, C, H, W)
# Stack ys and xs into coords_batch with shape (N, B) where N=2 (y, x dimensions)
coords_batch = np.stack([ys, xs], axis=0)
mask_builder.update_batch(features, coords_batch)

(assembled_mask,) = mask_builder.finalize()
plt.imshow(assembled_mask[0], cmap="gray", interpolation="nearest")
plt.axis("off")
plt.show()
```

---

### AutoScalingAveragingClippingNumpyMemMapMaskBuilder2D

::: ratiopath.masks.mask_builders.AutoScalingAveragingClippingNumpyMemMapMaskBuilder2D

**Use case**: You have high-resolution feature maps from a network and need to:
- Handle masks too large for RAM (using memory-mapped files)
- Automatically scale coordinates from input to output space
- Remove edge artifacts from tiles

**Example**: Building attention maps with edge clipping to remove boundary artifacts.

```python
import numpy as np
import openslide
from ratiopath.masks.mask_builders import (
AutoScalingAveragingClippingNumpyMemMapMaskBuilder2D,
)
from rationai.explainability.model_probing import HookedModule
import matplotlib.pyplot as plt

LEVEL = 3
tile_extents = (512, 512)
tile_strides = (256, 256)
slide = openslide.OpenSlide("path/to/slide.mrxs")
slide_extent_x, slide_extent_y = slide.level_dimensions[LEVEL]

vgg16_model = load_vgg16_model(...)
hooked_model = HookedModule(vgg16_model, layer_name="backbone.9")

# This builder handles coordinate scaling and uses memory-mapped storage
mask_builder = AutoScalingAveragingClippingNumpyMemMapMaskBuilder2D(
source_extents=(slide_extent_y, slide_extent_x),
source_tile_extents=tile_extents,
source_tile_strides=tile_strides,
mask_tile_extents=(64, 64), # output resolution per tile
channels=3, # for RGB masks
clip=(4, 4, 4, 4), # clip 4 pixels from each edge
)

# Note: generate_tiles_from_slide is a placeholder - you must implement your own tile extraction logic
for tiles, xs, ys in generate_tiles_from_slide(
slide, LEVEL, tile_extents, tile_strides, batch_size=32
):
# tiles has shape (B, C, H, W)
output = vgg16_model.predict(tiles) # outputs are not used directly
features = hooked_model.get_activations("backbone.9") # shape (B, C, H, W)
# Stack ys and xs into coords_batch with shape (N, B) where N=2 (y, x dimensions)
coords_batch = np.stack([ys, xs], axis=0)
mask_builder.update_batch(features, coords_batch)

assembled_mask, overlap = mask_builder.finalize()
plt.imshow(assembled_mask[0], cmap="gray", interpolation="nearest")
plt.axis("off")
plt.show()
```

---

### AutoScalingScalarUniformValueConstantStrideMaskBuilder

::: ratiopath.masks.mask_builders.AutoScalingScalarUniformValueConstantStrideMaskBuilder

**Use case**: Your network outputs scalar predictions per tile, and you want each prediction to represent a fixed-size region in the output mask, automatically handling coordinate scaling.

**Example**: Creating a low-resolution classification map where each tile's prediction covers a 64×64 region.

```python
import numpy as np
import openslide
from ratiopath.masks.mask_builders import (
AutoScalingScalarUniformValueConstantStrideMaskBuilder,
)
import matplotlib.pyplot as plt

LEVEL = 3
tile_extents = (512, 512)
tile_strides = (256, 256)
slide = openslide.OpenSlide("path/to/slide.mrxs")
slide_extent_x, slide_extent_y = slide.level_dimensions[LEVEL]
classifier_model = load_classifier_model(...)

# Build a mask where each scalar prediction covers 64x64 pixels in output
mask_builder = AutoScalingScalarUniformValueConstantStrideMaskBuilder(
source_extents=(slide_extent_y, slide_extent_x),
source_tile_extents=tile_extents,
source_tile_strides=tile_strides,
mask_tile_extents=(64, 64), # each scalar value expands to 64x64
channels=3, # for multi-class predictions
)

# Note: generate_tiles_from_slide is a placeholder - you must implement your own tile extraction logic
for tiles, xs, ys in generate_tiles_from_slide(
slide, LEVEL, tile_extents, tile_strides, batch_size=32
):
# tiles has shape (B, C, H, W)
predictions = classifier_model.predict(tiles) # predictions has shape (B, channels)
# Stack ys and xs into coords_batch with shape (N, B) where N=2 (y, x dimensions)
coords_batch = np.stack([ys, xs], axis=0)
mask_builder.update_batch(predictions, coords_batch)

assembled_mask, overlap = mask_builder.finalize()
plt.imshow(assembled_mask[0], cmap="viridis", interpolation="nearest")
plt.axis("off")
plt.show()
```

## Choosing a Mask Builder

| Builder | Scalar/Feature Map | Aggregation | Memory | Auto-scaling | Edge Clipping |
|---------|-------------------|-------------|---------|--------------|---------------|
| `AveragingScalarUniformTiledNumpyMaskBuilder` | Scalar | Average | RAM | No | No |
| `MaxScalarUniformTiledNumpyMaskBuilder` | Feature Map | Max | RAM | No | No |
| `AutoScalingAveragingClippingNumpyMemMapMaskBuilder2D` | Feature Map | Average | Disk (memmap) | Yes | Yes |
| `AutoScalingScalarUniformValueConstantStrideMaskBuilder` | Scalar | Average | RAM | Yes | No |

## Coordinate System Notes

All mask builders expect coordinates in the format `(N, B)` where:
- `N` is the number of spatial dimensions (typically 2 for height and width)
- `B` is the batch size

When implementing your own tile extraction logic (such as the `generate_tiles_from_slide` placeholder shown in examples), you should provide `xs` and `ys` arrays representing tile coordinates. Stack them as:

```python
coords_batch = np.stack([ys, xs], axis=0) # Shape: (2, B)
```

Note the order: `[ys, xs]` not `[xs, ys]`, as the first dimension represents height (y) and the second represents width (x).
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ nav:
- read_slide_tile: reference/tiling/read_slide_tile.md
- tilers: reference/tiling/tilers.md
- utils: reference/tiling/utils.md
- Masks:
- mask_builders: reference/masks/mask_builders.md
- Parsers:
- ASAPParser: reference/parsers/asap.md
- GeoJSONParser: reference/parsers/geojson.md
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ratiopath"
version = "1.0.3"
version = "1.1.0"
description = "A library for efficient processing and analysis of whole-slide pathology images."
authors = [
{ name = "Matěj Pekár", email = "matejpekar@mail.muni.cz" },
Expand All @@ -25,8 +25,9 @@ dependencies = [
"shapely>=2.0.0",
"torch>=2.6.0",
"zarr>=3.1.1",
"geopandas>=1.1.1",
"geopandas>=1.1.2",
"rasterio>=1.4.3",
"jaxtyping",
Copy link

@coderabbitai coderabbitai bot Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files pyproject.toml

Repository: RationAI/ratiopath

Length of output: 76


🏁 Script executed:

cat -n pyproject.toml | head -60

Repository: RationAI/ratiopath

Length of output: 1595


🌐 Web query:

jaxtyping latest stable version release documentation

💡 Result:

Latest stable jaxtyping release: 0.3.5 — uploaded to PyPI on January 5, 2026. Install: pip install jaxtyping. Documentation: https://docs.kidger.site/jaxtyping/ [1][2]

Sources:

  • PyPI jaxtyping release info (jaxtyping 0.3.5, uploaded Jan 5, 2026). [1]
  • jaxtyping GitHub / docs page. [2]

Add version constraint for jaxtyping to match project conventions.

Line 30: jaxtyping is the only dependency without a version constraint. All other dependencies specify minimum versions (e.g., numpy>=2.2.2, torch>=2.6.0). Add jaxtyping>=0.3.5 to maintain consistency and prevent unexpected breaking changes in future releases.

🤖 Prompt for AI Agents
In @pyproject.toml at line 30, The dependency entry for jaxtyping is missing a
version constraint; update the dependency list by replacing the bare "jaxtyping"
entry with a minimum-version specifier "jaxtyping>=0.3.5" so it matches the
project's convention of pinning minimum versions like numpy>=2.2.2 and
torch>=2.6.0.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Adames4, thanks for acknowledging! Just a friendly reminder to add the version constraint jaxtyping>=0.3.5 to maintain consistency with the other dependencies when you get a chance.

]

[dependency-groups]
Expand Down
Loading