Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/issue 131 #133

Merged
merged 3 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu
python -m pip install --editable .[dev] --find-links https://girder.github.io/large_image_wheels
python -m pip install --editable .[dev]
- name: Run tests
run: python -m pytest --verbose tests/
test-pytorch-nightly:
Expand All @@ -37,7 +37,7 @@ jobs:
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install --pre torch torchvision --extra-index-url https://download.pytorch.org/whl/nightly/cpu
python -m pip install --editable .[dev] --find-links https://girder.github.io/large_image_wheels
python -m pip install --editable .[dev]
- name: Check types
run: python -m mypy --install-types --non-interactive wsinfer/
- name: Run tests
Expand Down Expand Up @@ -77,7 +77,7 @@ jobs:
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu
python -m pip install . --find-links https://girder.github.io/large_image_wheels
python -m pip install .
- name: Run the wsinfer command in a new directory
run: |
mkdir newdir && cd newdir
Expand All @@ -101,7 +101,7 @@ jobs:
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu
python -m pip install .[dev] --find-links https://girder.github.io/large_image_wheels
python -m pip install .[dev]
- name: Check style (flake8)
run: python -m flake8 wsinfer/
- name: Check style (black)
Expand All @@ -120,7 +120,7 @@ jobs:
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu
python -m pip install .[docs] --find-links https://girder.github.io/large_image_wheels
python -m pip install .[docs]
- name: Build docs
run: |
cd docs
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ build:
jobs:
post_create_environment:
- python -m pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu
- python -m pip install .[docs] --find-links https://girder.github.io/large_image_wheels
- python -m pip install .[docs]
post_install:
# Re-run the installation to ensure we have an appropriate version of sphinx.
# We might not want to use the latest version.
Expand Down
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ COPY . .
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc g++ git libopenslide0 \
&& python -m pip install --no-cache-dir --editable . \
--find-links https://girder.github.io/large_image_wheels \
&& rm -rf /var/lib/apt/lists/*
# Use a writable directory for downloading model weights. Default is ~/.cache, which is
# not guaranteed to be writable in a Docker container.
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@ We do not install these dependencies automatically because their installation ca
on a user's system. Then use the command below to install this package.

```
python -m pip install --find-links https://girder.github.io/large_image_wheels wsinfer
python -m pip install wsinfer
```

To use the _bleeding edge_, use

```
python -m pip install \
--find-links https://girder.github.io/large_image_wheels \
git+https:/SBU-BMI/wsinfer.git
python -m pip install git+https:/SBU-BMI/wsinfer.git
```

## Developers
Expand All @@ -41,7 +39,7 @@ Clone this GitHub repository and install the package (in editable mode with the
```
git clone https:/SBU-BMI/wsinfer.git
cd wsinfer
python -m pip install --editable .[dev] --find-links https://girder.github.io/large_image_wheels
python -m pip install --editable .[dev]
```

# Cutting a release
Expand Down
6 changes: 2 additions & 4 deletions docs/installing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ the type of hardware a user has.
Manual installation
-------------------

After having installed PyTorch, install releases of WSInfer from `PyPI <https://pypi.org/project/wsinfer/>`_.
Be sure to include the line :code:`--find-links https://girder.github.io/large_image_wheels` to ensure
dependencies are installed properly. ::
After having installed PyTorch, install releases of WSInfer from `PyPI <https://pypi.org/project/wsinfer/>`_. ::

pip install wsinfer --find-links https://girder.github.io/large_image_wheels
pip install wsinfer

This installs the :code:`wsinfer` Python package and the :code:`wsinfer` command line program. ::

Expand Down
8 changes: 5 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ install_requires =
click>=8.0,<9
h5py
# OpenSlide and TIFF readers should handle all images we will encounter.
large-image[openslide,tiff]>=1.8.0
numpy
opencv-python-headless>=4.0.0
pandas
Expand Down Expand Up @@ -83,8 +82,6 @@ exclude = wsinfer/_version.py
[mypy]
[mypy-h5py]
ignore_missing_imports = True
[mypy-large_image]
ignore_missing_imports = True
[mypy-cv2]
ignore_missing_imports = True
[mypy-torchvision.*]
Expand All @@ -105,3 +102,8 @@ versionfile_source = wsinfer/_version.py
versionfile_build = wsinfer/_version.py
tag_prefix = v
parentdir_prefix = wsinfer


[isort]
profile = black
force_single_line = True
3 changes: 2 additions & 1 deletion wsinfer/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import re
import subprocess
import sys
from typing import Callable, Dict
from typing import Callable
from typing import Dict


def get_keywords():
Expand Down
43 changes: 19 additions & 24 deletions wsinfer/cli/convert_csv_to_sbubmi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

Output directory tree for single class outputs:
├── heatmap_jsons
  ├── heatmap-SLIDEID.json
  └── meta-SLIDEID.json
├── heatmap-SLIDEID.json
└── meta-SLIDEID.json
└── heatmap_txt
├── color-SLIDEID
└── prediction-SLIDEID

Output directory tree for multi-class outputs:
├── heatmap_jsons
  └── CLASS_LABEL
  ├── heatmap-SLIDEID.json
  └── meta-SLIDEID.json
└── CLASS_LABEL
├── heatmap-SLIDEID.json
└── meta-SLIDEID.json
└── heatmap_txt
└── CLASS_LABEL
├── color-SLIDEID
Expand All @@ -29,8 +29,8 @@
from pathlib import Path

import click
import large_image
import numpy as np
import openslide
import pandas as pd
import tqdm

Expand Down Expand Up @@ -184,7 +184,7 @@ def write_heatmap_txt(input: PathType, output: PathType, class_names: typing.Lis
def write_color_txt(
input: PathType,
output: PathType,
ts: large_image.tilesource.TileSource,
oslide: openslide.OpenSlide,
num_processes: int = 6,
):
def whiteness(arr):
Expand All @@ -204,15 +204,12 @@ def redness(arr):
global get_color # Hack to please multiprocessing.

def get_color(row: pd.Series):
arr, _ = ts.getRegion(
format=large_image.constants.TILE_FORMAT_NUMPY,
region=dict(
left=row["minx"],
top=row["miny"],
width=row["width"],
height=row["height"],
),
patch_im = oslide.read_region(
location=(row["minx"], row["miny"]),
level=0,
size=(row["width"], row["height"]),
)
arr = np.asarray(patch_im)
white = whiteness(arr)
black = blackness(arr)
red = redness(arr)
Expand Down Expand Up @@ -356,11 +353,9 @@ def tosbu(
click.secho(f"WSI file not found: {wsi_file}", bg="red")
click.secho("Skipping...", bg="red")
continue
ts = large_image.getTileSource(wsi_file)
if ts.sizeX is None or ts.sizeY is None:
click.secho(f"Unknown size for WSI: {wsi_file}", bg="red")
click.secho("Skipping...", bg="red")
continue
oslide = openslide.OpenSlide(wsi_file)

slide_width, slide_height = oslide.level_dimensions[0]

for class_name in class_names:
if len(class_names) == 1:
Expand All @@ -382,8 +377,8 @@ def tosbu(
input=input_csv,
output_heatmap=output_heatmap,
output_meta=output_meta,
slide_width=ts.sizeX,
slide_height=ts.sizeY,
slide_width=slide_width,
slide_height=slide_height,
execution_id=execution_id,
study_id=study_id,
case_id=slide_id, # TODO: should case_id be different?
Expand Down Expand Up @@ -418,7 +413,7 @@ def tosbu(
write_color_txt(
input=input_csv,
output=output_color,
ts=ts,
oslide=oslide,
num_processes=num_processes,
)
else:
Expand All @@ -429,7 +424,7 @@ def tosbu(
write_color_txt(
input=input_csv,
output=output_color,
ts=ts,
oslide=oslide,
num_processes=num_processes,
)
# Copy this color file to all class-specific dirs.
Expand Down
6 changes: 4 additions & 2 deletions wsinfer/cli/infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional, Union
from typing import Optional
from typing import Union

import click
import wsinfer_zoo
import wsinfer_zoo.client
import yaml
from wsinfer_zoo.client import HFModel, ModelConfiguration
from wsinfer_zoo.client import HFModel
from wsinfer_zoo.client import ModelConfiguration

from ..modellib import models
from ..modellib.run_inference import run_inference
Expand Down
12 changes: 8 additions & 4 deletions wsinfer/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ class UnknownArchitectureError(WsinferException):
"""Architecture is unknown and cannot be found."""


class WholeSlideImageDirectoryNotFound(FileNotFoundError):
class WholeSlideImageDirectoryNotFound(WsinferException, FileNotFoundError):
...


class WholeSlideImagesNotFound(FileNotFoundError):
class WholeSlideImagesNotFound(WsinferException, FileNotFoundError):
...


class ResultsDirectoryNotFound(FileNotFoundError):
class ResultsDirectoryNotFound(WsinferException, FileNotFoundError):
...


class PatchDirectoryNotFound(FileNotFoundError):
class PatchDirectoryNotFound(WsinferException, FileNotFoundError):
...


class CannotReadSpacing(WsinferException):
...
3 changes: 2 additions & 1 deletion wsinfer/modellib/custom_models/inceptionv4_no_batchnorm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
from timm.data import IMAGENET_INCEPTION_MEAN, IMAGENET_INCEPTION_STD
from timm.data import IMAGENET_INCEPTION_MEAN
from timm.data import IMAGENET_INCEPTION_STD
from timm.models import register_model
from timm.models.helpers import build_model_with_cfg
from timm.models.layers import create_classifier
Expand Down
2 changes: 1 addition & 1 deletion wsinfer/modellib/custom_models/resnet_preact.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Pre-activation ResNet."""

import torch
import torch # noqa
import torch.nn as nn
import torch.nn.functional as F

Expand Down
45 changes: 21 additions & 24 deletions wsinfer/modellib/data.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from pathlib import Path
from typing import Callable, Optional, Sequence, Tuple, Union
from typing import Callable
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union

import h5py
import large_image
import numpy as np
import openslide
import torch
from PIL import Image

# Set the maximum number of TileSource objects to cache. We use 1 to minimize how many
# file handles we keep open.
large_image.config.setConfig("cache_tilesource_maximum", 1)
from ..slide_utils import get_avg_mpp

PathType = Union[str, Path]

Expand Down Expand Up @@ -83,18 +85,13 @@ def __init__(
assert Path(wsi_path).exists(), "wsi path not found"
assert Path(patch_path).exists(), "patch path not found"

self.tilesource: large_image.tilesource.TileSource = large_image.getTileSource(
self.wsi_path
)
# Disable the tile cache. We wrap this in a try-except because we are accessing
# a private attribute. It is possible that this attribute will change names
# in the future, and if that happens, we do not want to raise errors.
try:
self.tilesource.cache._Cache__maxsize = 0
except AttributeError:
pass

self.oslide = openslide.OpenSlide(self.wsi_path)
self.slide_mpp = get_avg_mpp(self.wsi_path)
self.patches = _read_patch_coords(self.patch_path)

# The factor by which to resize the patches.
self.size_factor = self.slide_mpp / self.um_px

assert self.patches.ndim == 2, "expected 2D array of patch coordinates"
# x, y, width, height
assert self.patches.shape[1] == 4, "expected second dimension to have len 4"
Expand All @@ -108,17 +105,17 @@ def __getitem__(
coords: Sequence[int] = self.patches[idx]
assert len(coords) == 4, "expected 4 coords (minx, miny, width, height)"
minx, miny, width, height = coords
source_region = dict(
left=minx, top=miny, width=width, height=height, units="base_pixels"
)
target_scale = dict(mm_x=self.um_px / 1000)

patch_im, _ = self.tilesource.getRegionAtAnotherScale(
sourceRegion=source_region,
targetScale=target_scale,
format=large_image.tilesource.TILE_FORMAT_PIL,
patch_im = self.oslide.read_region(
location=(minx, miny), level=0, size=(width, height)
)
patch_im = patch_im.convert("RGB")
# Resize to the expected spacing. We extract the patches at their highest
# resolution and resize to the prescribed MPP here.
patch_im = patch_im.resize(
(self.size_factor * width, self.size_factor * height)
)

if self.transform is not None:
patch_im = self.transform(patch_im)
if not isinstance(patch_im, (Image.Image, torch.Tensor)):
Expand Down
Loading