Skip to content

Commit 597fc44

Browse files
committed
Refactor project and add metrics MLCube
1 parent 6c9536c commit 597fc44

27 files changed

+398
-31
lines changed

brats/metrics/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
mlcube/workspace/results.yaml

brats/metrics/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# BraTS Challenge 2020 - MLCube integration - Metrics
2+
3+
Original implementation: ["BraTS Instructions Repo"](https://github.com/BraTS/Instructions)
4+
5+
## Dataset
6+
7+
Please refer to the [BraTS challenge page](http://braintumorsegmentation.org/) and follow the instructions in the data section.
8+
9+
## Project setup
10+
11+
```bash
12+
# Create Python environment and install MLCube Docker runner
13+
virtualenv -p python3 ./env && source ./env/bin/activate && pip install mlcube-docker
14+
15+
# Fetch the boston housing example from GitHub
16+
git clone https://github.com/mlcommons/mlcube_examples && cd ./mlcube_examples
17+
git fetch origin pull/39/head:feature/brats && git checkout feature/brats
18+
cd ./brats/metrics/mlcube
19+
```
20+
21+
## Important files
22+
23+
These are the most important files on this project:
24+
25+
```bash
26+
27+
├── mlcube
28+
│ ├── mlcube.yaml # MLCube configuration file, it defines the project, author, platform, docker and tasks.
29+
│ └── workspace
30+
│ ├── data
31+
│ │ ├── ground_truth
32+
│ │ │ └── BraTS_example_seg.nii.gz # Ground truth example file
33+
│ │ └── predictions
34+
│ │ └── BraTS_example_seg.nii.gz # Prediction example file
35+
│ ├── parameters.yaml
36+
│ └── results.yaml # Final output file containing result metrics.
37+
└── project
38+
├── Dockerfile # Docker file with instructions to create the image for the project.
39+
├── metrics.py # Python file that contains the main logic of the project.
40+
├── mlcube.py # Python entrypoint used by MLCube, contains the logic for MLCube tasks.
41+
└── requirements.txt # Python requirements needed to run the project inside Docker.
42+
```
43+
44+
## How to modify this project
45+
46+
You can change each file described above in order to add your own implementation.
47+
48+
### Requirements file
49+
50+
In this file (`requirements.txt`) you can add all the python dependencies needed for running your implementation, these dependencies will be installed during the creation of the docker image, this happens when you run the ```mlcube run ...``` command.
51+
52+
### Dockerfile
53+
54+
You can use both, CPU or GPU version for the dockerfile (`Dockerfile_CPU`, `Dockerfile_GPU`), also, you can add or modify any steps inside the file, this comes handy when you need to install some OS dependencies or even when you want to change the base docker image, inside the file you can find some information about the existing steps.
55+
56+
### Parameters file
57+
58+
This is a yaml file (`parameters.yaml`)that contains all extra parameters that aren't files or directories, for example, here you can place all the hyperparameters that you will use for training a model. This file will be passed as an **input parameter** in the MLCube tasks and then it will be read inside the MLCube container.
59+
60+
### MLCube yaml file
61+
62+
In this file (`mlcube.yaml`) you can find the instructions about the docker image and platform that will be used, information about the project (name, description, authors), and also the tasks defined for the project.
63+
64+
In the existing implementation you will find 1 task:
65+
66+
* evaluate:
67+
68+
This task takes the following parameters:
69+
70+
* Input parameters:
71+
* predictions: Folder path containing predictions
72+
* ground_truth: Folder path containing ground truth data
73+
* parameters_file: Extra parameters
74+
* Output parameters:
75+
* output_path: File path where output metrics will be stored
76+
77+
This task takes the input predictions and ground truth data, perform the evaluation and then save the output result in the output_path.
78+
79+
### MLCube python file
80+
81+
The `mlcube.py` file is the handler file and entrypoint described in the dockerfile, here you can find all the logic related to how to process each MLCube task. If you want to add a new task first you must define it inside the `mlcube.yaml` file with its input and output parameters and then you need to add the logic to handle this new task inside the `mlcube.py` file.
82+
83+
### Metrics file
84+
85+
The `metrics.py` file contains the main logic of the project, you can modify this file and write your implementation here to calculate different metrics, this metrics file is called from the `mlcube.py` file and there are other ways to link your implementation and shown in the [MLCube examples repo](https://github.com/mlcommons/mlcube_examples).
86+
87+
## Tasks execution
88+
89+
```bash
90+
# Run evaluate task.
91+
mlcube run --mlcube=mlcube_cpu.yaml --task=evaluate
92+
```
93+
94+
We are targeting pull-type installation, so MLCube images should be available on Docker Hub. If not, try this:
95+
96+
```Bash
97+
mlcube run ... -Pdocker.build_strategy=always
98+
```

brats/metrics/mlcube/mlcube.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: MLCommons Brats metrics
2+
description: MLCommons Brats integration for metrics
3+
authors:
4+
- {name: "MLCommons Best Practices Working Group"}
5+
6+
platform:
7+
accelerator_count: 0
8+
9+
docker:
10+
# Image name.
11+
image: mlcommons/brats_metrics:0.0.1
12+
# Docker build context relative to $MLCUBE_ROOT. Default is `build`.
13+
build_context: "../project"
14+
# Docker file name within docker build context, default is `Dockerfile`.
15+
build_file: "Dockerfile"
16+
17+
tasks:
18+
evaluate:
19+
# Executes a number of metrics specified by the params file
20+
parameters:
21+
inputs: {predictions: data/predictions/, ground_truth: data/ground_truth/, parameters_file: parameters.yaml}
22+
outputs: {output_path: {type: "file", default: "results.yaml"}}
Binary file not shown.
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
treshold: 0.5
2+
eps: 0

brats/metrics/project/metrics.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""Logic file"""
2+
import argparse
3+
import glob
4+
import yaml
5+
from pkgutil import get_data
6+
import nibabel as nib
7+
import numpy as np
8+
9+
10+
def dice_coef_metric(
11+
probabilities: np.ndarray, truth: np.ndarray, treshold: float = 0.5, eps: float = 0
12+
) -> np.ndarray:
13+
"""
14+
Calculate Dice score for data batch.
15+
Params:
16+
probobilities: model outputs after activation function.
17+
truth: truth values.
18+
threshold: threshold for probabilities.
19+
eps: additive to refine the estimate.
20+
Returns: dice score aka f1.
21+
"""
22+
scores = []
23+
num = probabilities.shape[0]
24+
predictions = probabilities >= treshold
25+
assert predictions.shape == truth.shape
26+
for i in range(num):
27+
prediction = predictions[i]
28+
truth_ = truth[i]
29+
intersection = 2.0 * (truth_ * prediction).sum()
30+
union = truth_.sum() + prediction.sum()
31+
if truth_.sum() == 0 and prediction.sum() == 0:
32+
scores.append(1.0)
33+
else:
34+
scores.append((intersection + eps) / union)
35+
return np.mean(scores)
36+
37+
38+
def jaccard_coef_metric(
39+
probabilities: np.ndarray, truth: np.ndarray, treshold: float = 0.5, eps: float = 0
40+
) -> np.ndarray:
41+
"""
42+
Calculate Jaccard index for data batch.
43+
Params:
44+
probobilities: model outputs after activation function.
45+
truth: truth values.
46+
threshold: threshold for probabilities.
47+
eps: additive to refine the estimate.
48+
Returns: jaccard score aka iou."
49+
"""
50+
scores = []
51+
num = probabilities.shape[0]
52+
predictions = probabilities >= treshold
53+
assert predictions.shape == truth.shape
54+
55+
for i in range(num):
56+
prediction = predictions[i]
57+
truth_ = truth[i]
58+
intersection = (prediction * truth_).sum()
59+
union = (prediction.sum() + truth_.sum()) - intersection + eps
60+
if truth_.sum() == 0 and prediction.sum() == 0:
61+
scores.append(1.0)
62+
else:
63+
scores.append((intersection + eps) / union)
64+
return np.mean(scores)
65+
66+
67+
def preprocess_mask_labels(mask: np.ndarray):
68+
69+
mask_WT = mask.copy()
70+
mask_WT[mask_WT == 1] = 1
71+
mask_WT[mask_WT == 2] = 1
72+
mask_WT[mask_WT == 4] = 1
73+
74+
mask_TC = mask.copy()
75+
mask_TC[mask_TC == 1] = 1
76+
mask_TC[mask_TC == 2] = 0
77+
mask_TC[mask_TC == 4] = 1
78+
79+
mask_ET = mask.copy()
80+
mask_ET[mask_ET == 1] = 0
81+
mask_ET[mask_ET == 2] = 0
82+
mask_ET[mask_ET == 4] = 1
83+
84+
mask = np.stack([mask_WT, mask_TC, mask_ET])
85+
mask = np.moveaxis(mask, (0, 1, 2, 3), (0, 3, 2, 1))
86+
87+
return mask
88+
89+
90+
def load_img(file_path):
91+
data = nib.load(file_path)
92+
data = np.asarray(data.dataobj)
93+
return data
94+
95+
96+
def get_data_arr(predictions_path, ground_truth_path):
97+
predictions = glob.glob(predictions_path + "/*")
98+
ground_truth = glob.glob(ground_truth_path + "/*")
99+
if not len(predictions) == len(ground_truth):
100+
raise ValueError(
101+
"Number of predictions should be the same of ground truth labels"
102+
)
103+
gt_arr, prediction_arr = [], []
104+
for gt_path, prediction_path in zip(ground_truth, predictions):
105+
gt = load_img(gt_path)
106+
gt = preprocess_mask_labels(gt)
107+
prediction = load_img(prediction_path)
108+
prediction = preprocess_mask_labels(prediction)
109+
gt_arr.append(gt)
110+
prediction_arr.append(prediction)
111+
gt_arr = np.concatenate(gt_arr)
112+
prediction_arr = np.concatenate(prediction_arr)
113+
return gt_arr, prediction_arr
114+
115+
116+
def create_metrics_file(output_file, results):
117+
with open(output_file, "w") as f:
118+
yaml.dump(results, f)
119+
120+
121+
def main():
122+
parser = argparse.ArgumentParser()
123+
parser.add_argument(
124+
"--ground_truth",
125+
type=str,
126+
required=True,
127+
help="Directory containing the ground truth data",
128+
)
129+
parser.add_argument(
130+
"--predictions",
131+
type=str,
132+
required=True,
133+
help="Directory containing the predictions",
134+
)
135+
parser.add_argument(
136+
"--output_file",
137+
"--output-file",
138+
type=str,
139+
required=True,
140+
help="file to store metrics results as YAML",
141+
)
142+
parser.add_argument(
143+
"--parameters_file",
144+
"--parameters-file",
145+
type=str,
146+
required=True,
147+
help="File containing parameters for evaluation",
148+
)
149+
args = parser.parse_args()
150+
151+
with open(args.parameters_file, "r") as f:
152+
params = yaml.full_load(f)
153+
154+
gt_arr, pred_arr = get_data_arr(args.predictions, args.ground_truth)
155+
156+
treshold = float(params["treshold"])
157+
eps = float(params["eps"])
158+
159+
dice_coef = dice_coef_metric(pred_arr, gt_arr, treshold, eps)
160+
jaccard_coef = jaccard_coef_metric(pred_arr, gt_arr, treshold, eps)
161+
162+
results = {
163+
"dice_coef": str(dice_coef),
164+
"jaccard_coef": str(jaccard_coef),
165+
}
166+
167+
print(results)
168+
create_metrics_file(args.output_file, results)
169+
170+
171+
if __name__ == "__main__":
172+
main()

brats/metrics/project/mlcube.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""MLCube handler file"""
2+
import os
3+
import typer
4+
import subprocess
5+
6+
7+
app = typer.Typer()
8+
9+
10+
class EvaluateTask(object):
11+
"""Runs evaluation metrics given the predictions and label files
12+
Args:
13+
object ([type]): [description]
14+
"""
15+
16+
@staticmethod
17+
def run(
18+
ground_truth: str, predictions: str, parameters_file: str, output_file: str
19+
) -> None:
20+
cmd = f"python3 metrics.py --ground_truth={ground_truth} --predictions={predictions} --parameters_file={parameters_file} --output_file={output_file}"
21+
splitted_cmd = cmd.split()
22+
23+
process = subprocess.Popen(splitted_cmd, cwd=".")
24+
process.wait()
25+
26+
27+
@app.command("evaluate")
28+
def evaluate(
29+
ground_truth: str = typer.Option(..., "--ground_truth"),
30+
predictions: str = typer.Option(..., "--predictions"),
31+
parameters_file: str = typer.Option(..., "--parameters_file"),
32+
output_path: str = typer.Option(..., "--output_path"),
33+
):
34+
EvaluateTask.run(ground_truth, predictions, parameters_file, output_path)
35+
36+
37+
@app.command("test")
38+
def test():
39+
pass
40+
41+
42+
if __name__ == "__main__":
43+
app()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
PyYAML
2+
typer
3+
numpy
4+
nibabel

0 commit comments

Comments
 (0)