Skip to content
This repository was archived by the owner on Mar 21, 2024. It is now read-only.
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
3 changes: 2 additions & 1 deletion .github/workflows/check_changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ name: Check Changelog
on:
pull_request:
jobs:
check:
check_changelog:
name: Check Changelog
runs-on: ubuntu-latest
if: ${{ contains(github.event.pull_request.labels.*.name, 'no changelog needed') == 0 }}
steps:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ on:
- cron: '45 4 * * 1'

jobs:
analyze:
name: Analyze
codeql_analyze:
name: CodeQL Analyze
runs-on: ubuntu-latest

strategy:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/issues_to_ado.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ on:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]

jobs:
alert:
issues_to_ado:
name: Sync issues with Azure DevOps
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/linting_and_hello_world.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ on:
pull_request:

jobs:
linux:
flake_mypy_helloworld_linux:
name: Flake8, MyPy, HelloWorld on Linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -53,7 +54,8 @@ jobs:
PYTHONPATH: ${{ github.workspace }}
if: always()

windows:
hello_world_windows:
name: HelloWorld on Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ in inference-only runs when using lightning containers.
- ([#628](https://github.com/microsoft/InnerEye-DeepLearning/pull/628)) SSL SimCLR using the wrong LR schedule when running on multiple nodes
- ([#638](https://github.com/microsoft/InnerEye-DeepLearning/pull/638)) SimClr cosine LR scheduler was using wrong length information when using with long linear head datasets
- ([#612](https://github.com/microsoft/InnerEye-DeepLearning/pull/612)) SSL online evaluator was not doing distributed training
- ([#652](https://github.com/microsoft/InnerEye-DeepLearning/pull/652)) Run pytest build on Windows after Linux agent version upgrade

### Removed

Expand Down
12 changes: 9 additions & 3 deletions InnerEye/ML/Histopathology/datasets/panda_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
# ------------------------------------------------------------------------------------------

import logging
from pathlib import Path
from typing import Any, Dict, Union, Optional

import pandas as pd
from cucim import CuImage
from health_ml.utils import box_utils
from monai.config import KeysCollection
from monai.data.image_reader import ImageReader, WSIReader
from monai.transforms import MapTransform

from InnerEye.ML.Histopathology.datasets.base_dataset import SlidesDataset

try:
from cucim import CuImage
except:
logging.warning("cucim library not available, code may fail.")


class PandaDataset(SlidesDataset):
"""Dataset class for loading files from the PANDA challenge dataset.
Expand Down Expand Up @@ -48,6 +52,7 @@ def __init__(self,
# MONAI's convention is that dictionary transforms have a 'd' suffix in the class name
class ReadImaged(MapTransform):
"""Basic transform to read image files."""

def __init__(self, reader: ImageReader, keys: KeysCollection,
allow_missing_keys: bool = False, **kwargs: Any) -> None:
super().__init__(keys, allow_missing_keys=allow_missing_keys)
Expand All @@ -71,6 +76,7 @@ class LoadPandaROId(MapTransform):
- `'level'` (int): chosen magnification level
- `'scale'` (float): corresponding scale, loaded from the file
"""

def __init__(self, reader: WSIReader, image_key: str = 'image', mask_key: str = 'mask',
level: int = 0, margin: int = 0, **kwargs: Any) -> None:
"""
Expand All @@ -88,7 +94,7 @@ def __init__(self, reader: WSIReader, image_key: str = 'image', mask_key: str =
self.margin = margin
self.kwargs = kwargs

def _get_bounding_box(self, mask_obj: CuImage) -> box_utils.Box:
def _get_bounding_box(self, mask_obj: 'CuImage') -> box_utils.Box:
# Estimate bounding box at the lowest resolution (i.e. highest level)
highest_level = mask_obj.resolutions['level_count'] - 1
scale = mask_obj.resolutions['level_downsamples'][highest_level]
Expand Down
13 changes: 10 additions & 3 deletions InnerEye/ML/Histopathology/preprocessing/loading.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import logging
from typing import Dict, Optional, Tuple

import numpy as np
import skimage.filters
from cucim import CuImage
from health_ml.utils import box_utils
from monai.data.image_reader import WSIReader
from monai.transforms import MapTransform

from InnerEye.ML.Histopathology.utils.naming import SlideKey

try:
from cucim import CuImage
except:
logging.warning("cucim library not available, code may fail.")


def get_luminance(slide: np.ndarray) -> np.ndarray:
"""Compute a grayscale version of the input slide.
Expand All @@ -35,7 +40,7 @@ def segment_foreground(slide: np.ndarray, threshold: Optional[float] = None) \
return luminance < threshold, threshold


def load_slide_at_level(reader: WSIReader, slide_obj: CuImage, level: int) -> np.ndarray:
def load_slide_at_level(reader: WSIReader, slide_obj: 'CuImage', level: int) -> np.ndarray:
"""Load full slide array at the given magnification level.

This is a manual workaround for a MONAI bug (https://github.com/Project-MONAI/MONAI/issues/3415)
Expand All @@ -60,6 +65,7 @@ class LoadROId(MapTransform):
- `SlideKey.SCALE` (float): corresponding scale, loaded from the file
- `SlideKey.FOREGROUND_THRESHOLD` (float): threshold used to segment the foreground
"""

def __init__(self, reader: WSIReader, image_key: str = SlideKey.IMAGE, level: int = 0,
margin: int = 0, foreground_threshold: Optional[float] = None) -> None:
"""
Expand All @@ -77,7 +83,7 @@ def __init__(self, reader: WSIReader, image_key: str = SlideKey.IMAGE, level: in
self.margin = margin
self.foreground_threshold = foreground_threshold

def _get_bounding_box(self, slide_obj: CuImage) -> Tuple[box_utils.Box, float]:
def _get_bounding_box(self, slide_obj: 'CuImage') -> Tuple[box_utils.Box, float]:
# Estimate bounding box at the lowest resolution (i.e. highest level)
highest_level = slide_obj.resolutions['level_count'] - 1
scale = slide_obj.resolutions['level_downsamples'][highest_level]
Expand All @@ -88,6 +94,7 @@ def _get_bounding_box(self, slide_obj: CuImage) -> Tuple[box_utils.Box, float]:
return bbox, threshold

def __call__(self, data: Dict) -> Dict:
from cucim import CuImage
image_obj: CuImage = self.reader.read(data[self.image_key])

level0_bbox, threshold = self._get_bounding_box(image_obj)
Expand Down
7 changes: 5 additions & 2 deletions Tests/Azure/test_azure_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from InnerEye.Azure.azure_config import AzureConfig
from InnerEye.Azure.azure_runner import create_dataset_configs
from InnerEye.Common.common_util import is_linux
from InnerEye.ML.deep_learning_config import DatasetParams
from Tests.ML.util import get_default_azure_config

Expand Down Expand Up @@ -65,8 +66,10 @@ def test_dataset_consumption2() -> None:
assert datasets[1].name == "2"
assert datasets[0].local_folder == Path("l1")
assert datasets[1].local_folder == Path("l2")
assert datasets[0].target_folder == PosixPath("mp1")
assert datasets[1].target_folder == PosixPath("mp2")
if is_linux():
# PosixPath cannot be instantiated on Windows
assert datasets[0].target_folder == PosixPath("mp1")
assert datasets[1].target_folder == PosixPath("mp2")


def test_dataset_consumption3() -> None:
Expand Down
18 changes: 11 additions & 7 deletions Tests/ML/histopathology/models/test_deepmil.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Callable, Dict, List, Type # noqa

import pytest
import torch
from torch import Tensor, argmax, nn, rand, randint, randn, round, stack, allclose
from torchvision.models import resnet18

Expand All @@ -29,7 +30,7 @@
)
from InnerEye.ML.Histopathology.models.deepmil import DeepMILModule
from InnerEye.ML.Histopathology.models.encoders import ImageNetEncoder, TileEncoder
from InnerEye.ML.Histopathology.utils.naming import ResultsKey
from InnerEye.ML.Histopathology.utils.naming import MetricsKey, ResultsKey


def get_supervised_imagenet_encoder() -> TileEncoder:
Expand All @@ -38,10 +39,10 @@ def get_supervised_imagenet_encoder() -> TileEncoder:

@pytest.mark.parametrize("n_classes", [1, 3])
@pytest.mark.parametrize("pooling_layer", [AttentionLayer, GatedAttentionLayer])
@pytest.mark.parametrize("batch_size", [1, 15])
@pytest.mark.parametrize("max_bag_size", [1, 7])
@pytest.mark.parametrize("pool_hidden_dim", [1, 5])
@pytest.mark.parametrize("pool_out_dim", [1, 6])
@pytest.mark.parametrize("batch_size", [1, 2])
@pytest.mark.parametrize("max_bag_size", [1, 3])
@pytest.mark.parametrize("pool_hidden_dim", [1, 4])
@pytest.mark.parametrize("pool_out_dim", [1, 5])
def test_lightningmodule(
n_classes: int,
pooling_layer: Callable[[int, int, int], nn.Module],
Expand Down Expand Up @@ -108,9 +109,12 @@ def test_lightningmodule(
assert preds.shape[0] == batch_size

for metric_name, metric_object in module.train_metrics.items():
if (batch_size > 1) or (not metric_name == "auroc"):
if metric_name == MetricsKey.CONF_MATRIX or metric_name == MetricsKey.AUROC:
continue
if batch_size > 1:
score = metric_object(preds.view(-1, 1), bag_labels.view(-1, 1))
assert score >= 0 and score <= 1
assert torch.all(score >= 0)
assert torch.all(score <= 1)


def move_batch_to_expected_device(batch: Dict[str, List], use_gpu: bool) -> Dict:
Expand Down
14 changes: 12 additions & 2 deletions Tests/ML/histopathology/preprocessing/test_slide_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@

import numpy as np
import pytest
from cucim import CuImage
from monai.data.image_reader import WSIReader

from InnerEye.Common.common_util import is_windows
from InnerEye.Common.fixed_paths_for_tests import tests_root_directory
from InnerEye.ML.Histopathology.preprocessing.tiling import tile_array_2d
from InnerEye.ML.Histopathology.preprocessing.loading import LoadROId, get_luminance, load_slide_at_level, segment_foreground
from InnerEye.ML.Histopathology.preprocessing.loading import (LoadROId, get_luminance, load_slide_at_level,
segment_foreground)
from InnerEye.ML.Histopathology.utils.naming import SlideKey
from Tests.ML.histopathology.datasets.test_slides_dataset import MockSlidesDataset

TEST_IMAGE_PATH = str(tests_root_directory("ML/histopathology/test_data/panda_wsi_example.tiff"))


@pytest.mark.skipif(is_windows(), reason="cucim package is not available on Windows")
def test_load_slide() -> None:
level = 2
reader = WSIReader('cuCIM')
from cucim import CuImage
slide_obj: CuImage = reader.read(TEST_IMAGE_PATH)
dims = slide_obj.resolutions['level_dimensions'][level][::-1]

Expand All @@ -39,9 +42,11 @@ def test_load_slide() -> None:
assert np.array_equiv(larger_slide[:, :, dims[1]:], empty_fill_value)


@pytest.mark.skipif(is_windows(), reason="cucim package is not available on Windows")
def test_get_luminance() -> None:
level = 2 # here we only need to test at a single resolution
reader = WSIReader('cuCIM')
from cucim import CuImage
slide_obj: CuImage = reader.read(TEST_IMAGE_PATH)

slide = load_slide_at_level(reader, slide_obj, level)
Expand All @@ -61,9 +66,11 @@ def test_get_luminance() -> None:
assert np.array_equal(slide_luminance_tiles.squeeze(1), tiles_luminance)


@pytest.mark.skipif(is_windows(), reason="cucim package is not available on Windows")
def test_segment_foreground() -> None:
level = 2 # here we only need to test at a single resolution
reader = WSIReader('cuCIM')
from cucim import CuImage
slide_obj: CuImage = reader.read(TEST_IMAGE_PATH)
slide = load_slide_at_level(reader, slide_obj, level)

Expand Down Expand Up @@ -95,11 +102,13 @@ def test_segment_foreground() -> None:

@pytest.mark.parametrize('level', [1, 2])
@pytest.mark.parametrize('foreground_threshold', [None, 215])
@pytest.mark.skipif(is_windows(), reason="cucim package is not available on Windows")
def test_get_bounding_box(level: int, foreground_threshold: Optional[float]) -> None:
margin = 0
reader = WSIReader('cuCIM')
loader = LoadROId(reader, image_key=SlideKey.IMAGE, level=level, margin=margin,
foreground_threshold=foreground_threshold)
from cucim import CuImage
slide_obj: CuImage = reader.read(TEST_IMAGE_PATH)
level0_bbox, _ = loader._get_bounding_box(slide_obj)

Expand Down Expand Up @@ -130,6 +139,7 @@ def test_get_bounding_box(level: int, foreground_threshold: Optional[float]) ->
@pytest.mark.parametrize('level', [1, 2])
@pytest.mark.parametrize('margin', [0, 42])
@pytest.mark.parametrize('foreground_threshold', [None, 215])
@pytest.mark.skipif(is_windows(), reason="cucim package is not available on Windows")
def test_load_roi(level: int, margin: int, foreground_threshold: Optional[float]) -> None:
dataset = MockSlidesDataset()
sample = dataset[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

is this moving the tests to Windows and skipping them a temporary workaround?
skipping all tests related to cucim is a problem for the histo pipeline. For now these functions are used less frequently because they are used for patching and we store the patches. But if we want to move towards online patching this will prevent testing on the end-to-end pipeline

Expand Down
Loading