Skip to content

Commit

Permalink
Merge pull request #2 from tomasprinda/ocontours_load
Browse files Browse the repository at this point in the history
Loading ocontours added
  • Loading branch information
tomasprinda committed Apr 14, 2018
2 parents 192f936 + f1540c8 commit d1226ac
Show file tree
Hide file tree
Showing 17 changed files with 248 additions and 113 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ python scripts/train.py --train_dir DATA_DIR/datasets/train/ --dev_dir DATA_DIR/

which starts training of the net and it stores experiment data to [experiments/exp01/](experiments/exp01/) folder.

## Phase 01

## Answers to questions
### Answers to questions

How did you verify that you are parsing the contours correctly?
- First I converted contours to mask by untested method for some random images
Expand Down Expand Up @@ -63,7 +64,15 @@ Given the pipeline you have built, can you see any deficiencies that you would c
- Automatic hyperparameter tuning
- Better logging

## Notes
### Notes
- The aim of this project was to prepare data for training and create training pipeline. Training phase has not been tuned.
More images would probably be neccessary.

## Phase 2

### Answers to questions

After building the pipeline, please discuss any changes that you made to the pipeline you built in Phase 1, and why you made those changes.



Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
1;0.0068206787109375;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/119.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0119-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0119-ocontour-manual.txt
2;0.0070953369140625;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/139.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0139-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0139-ocontour-manual.txt
3;0.0076446533203125;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/159.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0159-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0159-ocontour-manual.txt
4;0.0059356689453125;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/179.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0179-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0179-ocontour-manual.txt
5;0.0039825439453125;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/199.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0199-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0199-ocontour-manual.txt
6;0.003265380859375;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/219.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0219-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0219-ocontour-manual.txt
7;0.007568359375;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/59.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0059-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0059-ocontour-manual.txt
8;0.0051422119140625;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/79.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0079-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0079-ocontour-manual.txt
9;0.0067596435546875;/www/data/prinda/ventricle_segmentation/final_data/dicoms/SCD0000501/99.dcm;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/i-contours/IM-0001-0099-icontour-manual.txt;/www/data/prinda/ventricle_segmentation/final_data/contourfiles/SC-HF-I-6/o-contours/IM-0001-0099-ocontour-manual.txt
106 changes: 46 additions & 60 deletions notebooks/tests.ipynb

Large diffs are not rendered by default.

97 changes: 81 additions & 16 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import unittest

import os
from pprint import pprint

import matplotlib
matplotlib.use('Agg') # prevents _tkinter error
import matplotlib.pyplot as plt

from ventricle_segmentation import cfg
from ventricle_segmentation.parsing import parse_dicom_file, np, polygon_to_mask, parse_contour_file, load_all_scans
from ventricle_segmentation.utils import print_info, pickle_load, iou
from ventricle_segmentation.core import AnnotatedScan, ContourMask
from ventricle_segmentation.parsing import parse_dicom_file, np, polygon_to_mask, parse_contour_file, load_all_scans, get_contour_mask
from ventricle_segmentation.utils import print_info, pickle_load, iou, prepare_exp_dir, csv_dump, plot_annotated_scan


class TestParsing(unittest.TestCase):
Expand All @@ -24,30 +30,89 @@ def test_parse_dicom_file(self):
dicom_img = parse_dicom_file(dicom_file)
self.assertIsInstance(dicom_img, np.ndarray)

def test_poly_to_mask(self):
for test_mask_file in os.listdir(cfg.TEST_MASKS_DIR):
def test_get_contour_mask(self):
test_files = list(os.listdir(cfg.TEST_MASKS_DIR))

# Test if some test images
self.assertGreater(len(test_files), 0)

for test_mask_file in test_files:
test_mask_file = os.path.join(cfg.TEST_MASKS_DIR, test_mask_file)
expected_annotated_scan = pickle_load(test_mask_file)
expected_annotated_scan = pickle_load(test_mask_file) # type: AnnotatedScan

# Test masks
masks_dict = {"imask": expected_annotated_scan.imask, "omask": expected_annotated_scan.omask}
for mask_type, expected_mask in masks_dict.items():
if expected_mask is not None:
shape = expected_annotated_scan.dicom_img.shape
imask = get_contour_mask(expected_mask.contours_file, shape)

self.assertEqual(imask.mask.tolist(), expected_mask.mask.tolist(), mask_type)
self.assertEqual(imask.contours_file, expected_mask.contours_file, mask_type)
else:
self.assertIsNone(expected_mask, mask_type)

non_existing_path = "/not/existing/file.txt"
mask = get_contour_mask(non_existing_path, (100, 100))
self.assertIsNone(mask)

def test_imask_inside_omask(self):
prepare_exp_dir("test_imask_inside_omask", clean_dir=True)
i_wrong = 0

expected_mask = expected_annotated_scan.imask
tolerance = 0.001
wrong_scores = []
for annotated_scan in load_all_scans(cfg.LINKS_FILE, cfg.DICOMS_DIR, cfg.CONTOURS_DIR):

contours = parse_contour_file(expected_annotated_scan.icontours_file)
mask = polygon_to_mask(contours, *expected_mask.shape)
if annotated_scan.imask is None or annotated_scan.omask is None:
continue # Skip if some mask not available

result = iou(mask, expected_mask)
expected_result = 1
tolerance = 0.05
wrong_pixels = annotated_scan.imask.mask > annotated_scan.omask.mask
wrong_pixels = np.mean(np.ravel(wrong_pixels))

self.assertLess(abs(result - expected_result), tolerance)
# Plot incorrect data
if wrong_pixels > tolerance:
i_wrong += 1
row = [i_wrong, wrong_pixels, annotated_scan.dicom_file, annotated_scan.imask.contours_file, annotated_scan.omask.contours_file]
csv_dump([row], os.path.join(cfg.EXP_DIR, "wrong_files.csv"), append=True)
plot_annotated_scan(annotated_scan)
plt.savefig(os.path.join(cfg.EXP_DIR, "{}.png".format(i_wrong)))
plt.clf()

wrong_scores.append(wrong_pixels)

# Test it
for wrong_pixels in wrong_scores:
self.assertLess(wrong_pixels, tolerance)

def test_load_all_scans(self):
annotated_scans = list(load_all_scans(cfg.LINKS_FILE, cfg.DICOMS_DIR, cfg.CONTOURS_DIR, n=20))
n = 20
annotated_scans = list(load_all_scans(cfg.LINKS_FILE, cfg.DICOMS_DIR, cfg.CONTOURS_DIR, n=n))

# Test if some test images
self.assertEqual(len(annotated_scans), n)

for annotated_scan in annotated_scans:
self.assertIsInstance(annotated_scan.imask, np.ndarray)
self.assertIsInstance(annotated_scan.dicom_img, np.ndarray)
self.assertTupleEqual(annotated_scan.dicom_img.shape, annotated_scan.imask.shape)

imask_loaded = isinstance(annotated_scan.imask, ContourMask)
omask_loaded = isinstance(annotated_scan.omask, ContourMask)
self.assertTrue(imask_loaded or omask_loaded)

self.assertIsInstance(annotated_scan.dicom_img, np.ndarray)

if imask_loaded:
self.assertTupleEqual(annotated_scan.dicom_img.shape, annotated_scan.imask.mask.shape)
self.assertIsInstance(annotated_scan.imask.contours_file, str)
self.assertNotEqual(annotated_scan.imask.contours_file, "")
else:
self.assertIsNone(annotated_scan.imask)

if omask_loaded:
self.assertTupleEqual(annotated_scan.dicom_img.shape, annotated_scan.omask.mask.shape)
self.assertIsInstance(annotated_scan.omask.contours_file, str)
self.assertNotEqual(annotated_scan.omask.contours_file, "")
else:
self.assertIsNone(annotated_scan.omask)


if __name__ == '__main__':
Expand Down
24 changes: 19 additions & 5 deletions ventricle_segmentation/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@ class AnnotatedScan:
Dataset example holder
"""

def __init__(self, dicom_img, imask, dicom_file, icontours_file):
def __init__(self, dicom_img, dicom_file, imask, omask):
"""
:param np.ndarray dicom_img:
:param np.ndarray imask: dtype=bool Mask that defines left ventricular blood pool in dicom image
:param str dicom_file: Path to dicom file
:param str icontours_file: Path to countours file (before converting to mask)
:param ContourMask|None imask: Mask that separates the left ventricular blood pool from the heart muscle (myocardium)
:param ContourMask|None omask: Mask that defines the outer border of the left ventricular heart muscle
"""
self.dicom_img = dicom_img
self.imask = imask
self.dicom_file = dicom_file
self.icontours_file = icontours_file
self.imask = imask
self.omask = omask


class ContourMask:
"""
i-contour / o-contour mask holder
"""

def __init__(self, mask, contours_file):
"""
:param np.ndarray mask: dtype=bool i/o-contour boolean mask
:param str contours_file: Path to i/o-countours file (before converting to mask)
"""
self.mask = mask
self.contours_file = contours_file
4 changes: 2 additions & 2 deletions ventricle_segmentation/dataset_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def __getitem__(self, index):
dicom_img = (annotated_scan.dicom_img.astype(np.float32) - 500) / 1000
dicom_img = np.expand_dims(dicom_img, 0) # adding channel at dimension 0, required dims in batch (N,C_in,H_in,W_in); N is not present yet

imask = annotated_scan.imask.astype(np.int64)
imask = annotated_scan.imask.mask.astype(np.int64)

return dicom_img, imask, annotated_scan.dicom_file, annotated_scan.icontours_file
return dicom_img, imask, annotated_scan.dicom_file, annotated_scan.imask.contours_file

def __len__(self):
return len(self.scan_pkl_files)
74 changes: 58 additions & 16 deletions ventricle_segmentation/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from PIL import Image, ImageDraw
from pydicom.errors import InvalidDicomError

from ventricle_segmentation.core import AnnotatedScan
from ventricle_segmentation.core import AnnotatedScan, ContourMask


def load_all_scans(links_file, dicoms_dir, contours_dir, n=None):
Expand All @@ -24,10 +24,16 @@ def load_all_scans(links_file, dicoms_dir, contours_dir, n=None):
i = 0
for (dicom_id, contours_id) in parse_links_file(links_file):

if dicom_id == "SCD0000501":
# o-contours failed TestParsing.test_imask_inside_omask().
# See experiments/test_imask_inside_omask_without_skipping
continue

dicom_patients_dir = os.path.join(dicoms_dir, dicom_id)
contour_patients_dir = os.path.join(contours_dir, contours_id, "i-contours")
icontour_patients_dir = os.path.join(contours_dir, contours_id, "i-contours")
ocontour_patients_dir = os.path.join(contours_dir, contours_id, "o-contours")

for annotated_scan in load_patient_scans(dicom_patients_dir, contour_patients_dir):
for annotated_scan in load_patient_scans(dicom_patients_dir, icontour_patients_dir, ocontour_patients_dir):

if n is not None and i >= n:
return
Expand All @@ -53,32 +59,69 @@ def parse_links_file(filename):
return links_lst


def load_patient_scans(dicom_patient_dir, icontour_patient_dir):
pattern_dicom = re.compile("^(\d+)\.dcm")


def load_patient_scans(dicom_patient_dir, icontour_patient_dir, ocontour_patient_dir):
"""
Loads contours from icontour_patient_dir and loads also corresponding dicom images for annotations from dicom_patient_dir.
Loads dicom files with corresponding i-contours and o-contours files if at least one of the contours exists
for the dicom file.
Make sure dicom_patient_dir matches icontour_patient_dir (by link.csv)
:param str dicom_patient_dir: dir containing dicom files {:d}.dcm files
:param icontour_patient_dir: dir contating contour files IM-0001-{:04d}-icontour-manual.txt
:param str icontour_patient_dir: dir contating i-contour files IM-0001-{:04d}-icontour-manual.txt
:param str ocontour_patient_dir: dir contating o-contour files IM-0001-{:04d}-ocontour-manual.txt
:return list[AnnotatedScan]: iterable
"""
pattern = re.compile("^IM-0001-(\d+)-icontour-manual\.txt")
for icontour_file in os.listdir(icontour_patient_dir):
match = pattern.match(icontour_file)
icontour_file = os.path.join(icontour_patient_dir, icontour_file)

for dicom_file in os.listdir(dicom_patient_dir):
match = pattern_dicom.match(dicom_file)

if match: # Correct icontour file

dicom_nr = int(match.group(1))
dicom_file = os.path.join(dicom_patient_dir, "{}.dcm".format(dicom_nr))
dicom_img = parse_dicom_file(dicom_file)
dicom_file = os.path.join(dicom_patient_dir, dicom_file)
icontour_file = os.path.join(icontour_patient_dir, "IM-0001-{:04d}-icontour-manual.txt".format(dicom_nr))
ocontour_file = os.path.join(ocontour_patient_dir, "IM-0001-{:04d}-ocontour-manual.txt".format(dicom_nr))

contours = parse_contour_file(icontour_file)
imask = polygon_to_mask(contours, *dicom_img.shape)
if not os.path.isfile(icontour_file) and not os.path.isfile(ocontour_file):
continue # Don't load when no annotation data

annotated_scan = AnnotatedScan(dicom_img, imask, dicom_file, icontour_file)
annotated_scan = load_annotated_scan(dicom_file, icontour_file, ocontour_file)
yield annotated_scan


def load_annotated_scan(dicom_file, icontour_file, ocontour_file):
"""
Loads AnnotatedScan from dicom and i/o-contours files.
If i/o-contours files deosn't exists, AnnotatedScan.imask/omask is None
:param str dicom_file: Path to dicom file
:param str icontour_file: Path to icontour file (might not exists)
:param str ocontour_file: Path to icontour file (might not exists)
:return AnnotatedScan :
"""
dicom_img = parse_dicom_file(dicom_file)
imask = get_contour_mask(icontour_file, dicom_img.shape)
omask = get_contour_mask(ocontour_file, dicom_img.shape)

return AnnotatedScan(dicom_img, dicom_file, imask, omask)


def get_contour_mask(contour_file, shape):
"""
Loads contours file and converts it to mask
:param str contour_file: Contours file to load
:param (int, int) shape: Size of mask to create == size of a dicom file
:return ContourMask|None: Returns None if file doesn't exists
"""
if not os.path.isfile(contour_file):
return None

contours = parse_contour_file(contour_file)
mask = polygon_to_mask(contours, *shape)
return ContourMask(mask, contour_file)


def parse_contour_file(filename):
"""
Parse the given contour filename
Expand Down Expand Up @@ -146,4 +189,3 @@ def polygon_to_mask(polygon, width, height):
ImageDraw.Draw(img).polygon(xy=polygon, outline=0, fill=1)
mask = np.array(img).astype(bool)
return mask

34 changes: 22 additions & 12 deletions ventricle_segmentation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,33 @@ def plot_annotated_scan(annotated_scan, plot_mask=True):
:param AnnotatedScan annotated_scan:
"""
plt.figure(figsize=(12, 12))
plt.imshow(annotated_scan.dicom_img.astype(float))
plt.imshow(annotated_scan.dicom_img.astype(float), cmap=pl.cm.hot)
if plot_mask:
plt.figure(figsize=(12, 12))
plt.imshow(annotated_scan.dicom_img.astype(float))
# Choose colormap
cmap = pl.cm.Reds
if annotated_scan.imask is not None:
plot_contour_mask(annotated_scan.dicom_img, annotated_scan.imask, cmap=pl.cm.Blues)
if annotated_scan.omask is not None:
plot_contour_mask(annotated_scan.dicom_img, annotated_scan.omask, cmap=pl.cm.Greens)
plt.title("\n".join([
annotated_scan.dicom_file,
annotated_scan.imask.contours_file if annotated_scan.imask else "no imask",
annotated_scan.omask.contours_file if annotated_scan.omask else "no omask"
]))

# Get the colormap colors
my_cmap = cmap(np.arange(cmap.N))

# Set alpha
my_cmap[:, -1] = np.linspace(0, 1, cmap.N)
def plot_contour_mask(dicom_img, contour_mask, cmap):
# plt.figure(figsize=(12, 12))
# plt.imshow(dicom_img.astype(float), cmap=pl.cm.hot)

# Create new colormap
my_cmap = ListedColormap(my_cmap)
# Get the colormap colors
my_cmap = cmap(np.arange(cmap.N))

plt.imshow(annotated_scan.imask.astype(float) / 4, cmap=my_cmap, alpha=.5)
# Set alpha
my_cmap[:, -1] = np.linspace(0, 1, cmap.N)

# Create new colormap
my_cmap = ListedColormap(my_cmap)

plt.imshow(contour_mask.mask.astype(float) / 4, cmap=my_cmap, alpha=.5)


def prepare_exp_dir(exp_name, clean_dir):
Expand Down

0 comments on commit d1226ac

Please sign in to comment.