-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Tumor FROC Evaluation #1878
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
Merged
Merged
Tumor FROC Evaluation #1878
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
8bb3d6f
Implement MaskedInferenceWSIDataset for pathology inference
bhashemian 52e28da
Update pathology init
bhashemian d0fb5d3
Update docs
bhashemian 53d3edd
Remove last elemnt of cum_num_patches
bhashemian dcba583
Add unittest with multiple cases for MaskedInferenceWSIDataset
bhashemian 096d3ab
Merge branch 'master' into pathology_inference
bhashemian 93d51ea
sort imports in init
bhashemian bce2fa4
Merge branch 'pathology_inference' of https://github.com/behxyz/MONAI…
bhashemian 8d7e353
Remove list dataset
bhashemian 2ea8874
Merge branch 'master' into pathology_inference
bhashemian d0abacd
Remove try/except and add type hint
bhashemian d450da2
Convert the sample output to a list
bhashemian de2ac9a
Merge branch 'master' into pathology_inference
bhashemian 1d02f04
Remove some type hints
bhashemian 3105c82
Implement FROC calcualtion for pathology
bhashemian 56a002d
Update ProbNMS doctring
bhashemian 3d776e4
Update docs and change namings
bhashemian c3cd6af
Fix a bug and minor changes
bhashemian bddcb4f
Minor changes
bhashemian d6eca07
Fix docstring formatting
bhashemian 44dd729
Add a type hint
bhashemian 90d86dc
Implement unittests for EvaluateTumorFROC
bhashemian 11a8b6d
Ignore type for np.amax
bhashemian c9e5a3f
Remove space
bhashemian 486399a
Ignore type for range instead of np.amax
bhashemian 52fcc26
Skip test if PIL is not available
bhashemian a5a7500
Update docstring
bhashemian c98f803
Skip ground truth generating if PIL is not available
bhashemian dbc0e24
Update unittest
bhashemian 9eb21c8
Merge branch 'master' into calculate_froc
bhashemian 5d8de3e
Remove print
bhashemian 340087f
Merge branch 'master' into calculate_froc
yiheng-wang-nv 721f88f
Merge branch 'master' into calculate_froc
yiheng-wang-nv fd7c161
Rename TumorFROC and add few type hints
bhashemian e83dcda
Rename evaluators to metrics
bhashemian 39c3ef8
Merge branch 'master' of https://github.com/Project-MONAI/MONAI into …
bhashemian 57d86ef
Remove non-relevant files
bhashemian 2e4b5fd
Rename to LesionFROC and minor changes
bhashemian f130796
Update test
bhashemian 695be38
Merge branch 'calculate_froc' of https://github.com/behxyz/MONAI into…
bhashemian 0089702
Merge branch 'master' into calculate_froc
bhashemian 68e033f
Merge branch 'master' into calculate_froc
bhashemian 9bfbc75
Address PR comments
bhashemian 2611362
Update nms
bhashemian e7baa7a
Merge branch 'master' into calculate_froc
bhashemian 7e7fe68
Update docs
bhashemian 07190f4
Merge branch 'master' into calculate_froc
bhashemian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| # Copyright 2020 - 2021 MONAI Consortium | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| import json | ||
| from typing import Dict, List, Tuple, Union | ||
|
|
||
| import numpy as np | ||
|
|
||
| from monai.apps.pathology.utils import PathologyProbNMS, compute_isolated_tumor_cells, compute_multi_instance_mask | ||
| from monai.data.image_reader import WSIReader | ||
| from monai.metrics import compute_fp_tp_probs, compute_froc_curve_data, compute_froc_score | ||
|
|
||
|
|
||
| class LesionFROC: | ||
| """ | ||
| Evaluate with Free Response Operating Characteristic (FROC) score. | ||
|
|
||
| Args: | ||
| data: either the list of dictionaries containing probability maps (inference result) and | ||
| tumor mask (ground truth), as below, or the path to a json file containing such list. | ||
| `{ | ||
| "prob_map": "path/to/prob_map_1.npy", | ||
| "tumor_mask": "path/to/ground_truth_1.tiff", | ||
| "level": 6, | ||
| "pixel_spacing": 0.243 | ||
| }` | ||
| grow_distance: Euclidean distance (in micrometer) by which to grow the label the ground truth's tumors. | ||
| Defaults to 75, which is the equivalent size of 5 tumor cells. | ||
| itc_diameter: the maximum diameter of a region (in micrometer) to be considered as an isolated tumor cell. | ||
| Defaults to 200. | ||
| eval_thresholds: the false positive rates for calculating the average sensitivity. | ||
| Defaults to (0.25, 0.5, 1, 2, 4, 8) which is the same as the CAMELYON 16 Challenge. | ||
| nms_sigma: the standard deviation for gaussian filter of non-maximal suppression. Defaults to 0.0. | ||
| nms_prob_threshold: the probability threshold of non-maximal suppression. Defaults to 0.5. | ||
| nms_box_size: the box size (in pixel) to be removed around the the pixel for non-maximal suppression. | ||
| image_reader_name: the name of library to be used for loading whole slide imaging, either CuCIM or OpenSlide. | ||
| Defaults to CuCIM. | ||
|
|
||
| Note: | ||
| For more info on `nms_*` parameters look at monai.utils.prob_nms.ProbNMS`. | ||
|
|
||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| data: Union[List[Dict], str], | ||
| grow_distance: int = 75, | ||
| itc_diameter: int = 200, | ||
| eval_thresholds: Tuple = (0.25, 0.5, 1, 2, 4, 8), | ||
| nms_sigma: float = 0.0, | ||
| nms_prob_threshold: float = 0.5, | ||
| nms_box_size: int = 48, | ||
| image_reader_name: str = "cuCIM", | ||
| ) -> None: | ||
|
|
||
| if isinstance(data, str): | ||
| self.data = self._load_data(data) | ||
| else: | ||
| self.data = data | ||
| self.grow_distance = grow_distance | ||
| self.itc_diameter = itc_diameter | ||
| self.eval_thresholds = eval_thresholds | ||
| self.image_reader = WSIReader(image_reader_name) | ||
| self.nms = PathologyProbNMS( | ||
| sigma=nms_sigma, | ||
| prob_threshold=nms_prob_threshold, | ||
| box_size=nms_box_size, | ||
| ) | ||
|
|
||
| def _load_data(self, file_path: str) -> List[Dict]: | ||
| with open(file_path, "r") as f: | ||
| data: List[Dict] = json.load(f) | ||
| return data | ||
|
|
||
| def prepare_inference_result(self, sample: Dict): | ||
| """ | ||
| Prepare the probability map for detection evaluation. | ||
|
|
||
| """ | ||
| # load the probability map (the result of model inference) | ||
| prob_map = np.load(sample["prob_map"]) | ||
|
|
||
| # apply non-maximal suppression | ||
| nms_outputs = self.nms(probs_map=prob_map, resolution_level=sample["level"]) | ||
|
|
||
| # separate nms outputs | ||
| if nms_outputs: | ||
| probs, x_coord, y_coord = zip(*nms_outputs) | ||
| else: | ||
| probs, x_coord, y_coord = [], [], [] | ||
|
|
||
| return np.array(probs), np.array(x_coord), np.array(y_coord) | ||
|
|
||
| def prepare_ground_truth(self, sample): | ||
| """ | ||
| Prepare the ground truth for evaluation based on the binary tumor mask | ||
|
|
||
| """ | ||
| # load binary tumor masks | ||
| img_obj = self.image_reader.read(sample["tumor_mask"]) | ||
| tumor_mask = self.image_reader.get_data(img_obj, level=sample["level"])[0][0] | ||
|
|
||
| # calculate pixel spacing at the mask level | ||
| mask_pixel_spacing = sample["pixel_spacing"] * pow(2, sample["level"]) | ||
|
|
||
| # compute multi-instance mask from a binary mask | ||
| grow_pixel_threshold = self.grow_distance / (mask_pixel_spacing * 2) | ||
| tumor_mask = compute_multi_instance_mask(mask=tumor_mask, threshold=grow_pixel_threshold) | ||
|
|
||
| # identify isolated tumor cells | ||
| itc_threshold = (self.itc_diameter + self.grow_distance) / mask_pixel_spacing | ||
| itc_labels = compute_isolated_tumor_cells(tumor_mask=tumor_mask, threshold=itc_threshold) | ||
|
|
||
| return tumor_mask, itc_labels | ||
|
|
||
| def compute_fp_tp(self): | ||
| """ | ||
| Compute false positive and true positive probabilities for tumor detection, | ||
| by comparing the model outputs with the prepared ground truths for all samples | ||
|
|
||
| """ | ||
| total_fp_probs, total_tp_probs = [], [] | ||
| total_num_targets = 0 | ||
| num_images = len(self.data) | ||
|
|
||
| for sample in self.data: | ||
| probs, y_coord, x_coord = self.prepare_inference_result(sample) | ||
| ground_truth, itc_labels = self.prepare_ground_truth(sample) | ||
| # compute FP and TP probabilities for a pair of an image and an ground truth mask | ||
| fp_probs, tp_probs, num_targets = compute_fp_tp_probs( | ||
| probs=probs, | ||
| y_coord=y_coord, | ||
| x_coord=x_coord, | ||
| evaluation_mask=ground_truth, | ||
| labels_to_exclude=itc_labels, | ||
| resolution_level=sample["level"], | ||
| ) | ||
| total_fp_probs.extend(fp_probs) | ||
| total_tp_probs.extend(tp_probs) | ||
| total_num_targets += num_targets | ||
|
|
||
| return ( | ||
| np.array(total_fp_probs), | ||
| np.array(total_tp_probs), | ||
| total_num_targets, | ||
| num_images, | ||
| ) | ||
|
|
||
| def evaluate(self): | ||
| """ | ||
| Evaluate the detection performance of a model based on the model probability map output, | ||
| the ground truth tumor mask, and their associated metadata (e.g., pixel_spacing, level) | ||
| """ | ||
| # compute false positive (FP) and true positive (TP) probabilities for all images | ||
| fp_probs, tp_probs, num_targets, num_images = self.compute_fp_tp() | ||
|
|
||
| # compute FROC curve given the evaluation of all images | ||
| fps_per_image, total_sensitivity = compute_froc_curve_data( | ||
| fp_probs=fp_probs, | ||
| tp_probs=tp_probs, | ||
| num_targets=num_targets, | ||
| num_images=num_images, | ||
| ) | ||
|
|
||
| # compute FROC score give specific evaluation threshold | ||
| froc_score = compute_froc_score( | ||
| fps_per_image=fps_per_image, | ||
| total_sensitivity=total_sensitivity, | ||
| eval_thresholds=self.eval_thresholds, | ||
| ) | ||
|
|
||
| return froc_score | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you remove
_load_data, this data list should be read before this metric computation, to be consistent with the other metric modulesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This metrics supports the case that that is loaded too.
_load_datais optional in case the data is not loaded and can provide the json path only.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is to be consistent across the project. the data working group is proposing prototypes for data representation, before we have a concrete design we shouldn't assume a JSON encoded file here