'''),
+ Annotation not submitted. Please try again.'''),
)
- submit_button.on_click(cb_on_button_clicked)
+ submit_button.on_click(on_button_clicked)
# Display all the widgets.
display(sample, box1, comment, submit_button, output)
-def _format_annotation(
- sample_id: Union[int, str], key: str, keyvalue: Union[int, float, str], comment: str,
-) -> Dict[str, Any]:
+def _format_annotation(sample_id, key, keyvalue, comment):
"""Helper method to clean and reshape info from the widgets and the environment into a dictionary representing the annotation."""
# Programmatically get the identity of the person running this Terra notebook.
current_user = os.getenv('OWNER_EMAIL')
@@ -140,10 +128,11 @@ def _format_annotation(
if current_user is None:
current_user = socket.gethostname() # By convention, we prefix the hostname with our username.
- value_numeric = None
- value_string = None
# Check whether the value is string or numeric.
- if keyvalue is not None:
+ if keyvalue is None:
+ value_numeric = None
+ value_string = None
+ else:
try:
value_numeric = float(keyvalue) # this will fail if the value is text
value_string = None
diff --git a/ml4h/visualization_tools/batch_image_annotations.py b/ml4h/visualization_tools/batch_image_annotations.py
deleted file mode 100644
index 34ff731df..000000000
--- a/ml4h/visualization_tools/batch_image_annotations.py
+++ /dev/null
@@ -1,236 +0,0 @@
-"""Methods for batch annotations of images stored as 3D tensors, such as MRIs, from within notebooks."""
-
-import json
-import os
-import socket
-import tempfile
-from typing import Any, Dict, List
-
-from IPython.display import display
-import numpy as np
-import pandas as pd
-import h5py
-from ipyannotations import PolygonAnnotator
-import ipywidgets as widgets
-from ml4h.visualization_tools.hd5_mri_plots import MRI_TMAPS
-from ml4h.visualization_tools.annotation_storage import AnnotationStorage
-from ml4h.visualization_tools.annotation_storage import TransientAnnotationStorage
-from PIL import Image
-import tensorflow as tf
-
-
-class BatchImageAnnotator():
- """Annotate batches of images with polygons drawn over regions of interest."""
-
- SUBMIT_BUTTON_DESCRIPTION = 'Submit polygons, goto next sample'
- USE_INSTRUCTIONS = '''
-
-
To draw a polygon, click anywhere you'd like to start. Continue to click
- along the edge of the polygon until arrive back where you started. To
- finish, simply click the first point (highlighted in red). It may be
- helpful to increase the point size if you're struggling (using the slider).
-
-
You can change the class of a polygon using the dropdown menu while the
- polygon is still "open", or unfinished. If you make a mistake, use the Undo
- button until the point that's wrong has disappeared.
-
-
You can move, but not add / subtract polygon points, by clicking the "Edit"
- button. Simply drag a point you want to adjust. Again, if you have
- difficulty aiming at the points, you can increase the point size.
-
-
You can increase or decrease the contrast and brightness of the image
- using the sliders to make it easier to annotate. Sometimes you need to see
- what's behind already-created annotations, and for this purpose you can
- make them more see-through using the "Opacity" slider.
-
- '''
- EXPECTED_COLUMN_NAMES = ['sample_id', 'tmap_name', 'instance_number', 'folder']
- DEFAULT_ANNOTATION_CLASSNAME = 'region_of_interest'
- CSS = '''
-
- '''
-
- def __init__(
- self, samples: pd.DataFrame, annotation_categories: List[str] = None,
- zoom: float = 1.5, annotation_storage: AnnotationStorage = TransientAnnotationStorage(),
- ):
- """Initializes an instance of BatchImageAnnotator.
-
- Args:
- samples: A dataframe of samples to annotate. Columns must include those
- in BatchImageAnnotator.EXPECTED_COLUMN_NAMES.
- annotation_categories: A list of one or more strings to serve as tags for the polygons.
- zoom: Desired zoom level for the image.
- annotation_storage: An instance of AnnotationStorage. This faciltates the use of a user-provided
- strategy for the storage and processing of annotations.
-
- Raises:
- ValueError: The provided dataframe does not contain the expected columns.
- """
- if not set(self.EXPECTED_COLUMN_NAMES).issubset(samples.columns):
- raise ValueError(f'samples Dataframe must contain columns {self.EXPECTED_COLUMN_NAMES}')
- self.samples = samples
- self.current_sample = 0
- # TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/11
- self.zoom = zoom
- self.annotation_storage = annotation_storage
- if annotation_categories is None:
- annotation_categories = [self.DEFAULT_ANNOTATION_CLASSNAME]
-
- self.annotation_widget = PolygonAnnotator(
- options=annotation_categories,
- canvas_size=(900, 280 * self.zoom),
- )
- self.annotation_widget.on_submit(self._store_annotations)
- self.annotation_widget.submit_button.description = self.SUBMIT_BUTTON_DESCRIPTION
- self.annotation_widget.submit_button.layout = widgets.Layout(width='300px')
-
- self.title_widget = widgets.HTML('')
- self.results_widget = widgets.HTML('')
-
- def _store_annotations(self, data: Dict[Any, Any]) -> None:
- """Transfer widget state to the annotation storage and advance to the next sample."""
- if self.current_sample >= self.samples.shape[0]:
- self.results_widget.value = '
Annotation batch complete!
Thank you for making the model better.'
- return
-
- # Convert polygon points in canvas coordinates to tensor coordinates.
- image_canvas_position = self.annotation_widget.canvas.image_extent
- x_offset, y_offset, _, _ = image_canvas_position
- tensor_coords = [
- (
- a['label'],
- [(
- int((p[0] - x_offset) / self.zoom),
- int((p[1] - y_offset) / self.zoom),
- ) for p in a['points']],
- ) for a in data
- ]
- # Store the annotation using the provided annotation storage strategy.
- self.annotation_storage.submit_annotation(
- sample_id=self.samples.loc[self.current_sample, 'sample_id'],
- annotator=os.getenv('OWNER_EMAIL') if os.getenv('OWNER_EMAIL') else socket.gethostname(),
- key=self.samples.loc[self.current_sample, 'tmap_name'],
- value_numeric=self.samples.loc[self.current_sample, 'instance_number'],
- value_string=self.samples.loc[self.current_sample, 'folder'],
- comment=json.dumps(tensor_coords),
- )
-
- # Display this annotation at the bottom of the widget.
- results = f'''
-
-
Prior sample's submitted annotations
- The {self.SUBMIT_BUTTON_DESCRIPTION} button is both printing out the polygons below and storing the polygons
- via strategy {self.annotation_storage.__class__.__name__}.
- Details: {self.annotation_storage.describe()}
-
sample info
- {self._format_info_for_current_sample()}
-
canvas coordinates
- image extent {image_canvas_position}
- {[f'
{json.dumps(x)}
' for x in data]}
-
source tensor coordinates
- {[f'
{json.dumps(x)}
' for x in tensor_coords]}
-
- '''
- self.results_widget.value = results
-
- # Advance to the next sample.
- self.current_sample += 1
- self._annotate_image_for_current_sample()
-
- def _format_info_for_current_sample(self) -> str:
- """Convert information about the current sample to an HTML table for display within the widget."""
- headings = ' '.join([f'
- '''
-
- def _annotate_image_for_current_sample(self) -> None:
- """Retrieve the data for the current sample and display its image in the annotation widget.
-
- If all samples have been processed, display the completion message.
- """
- if self.current_sample >= self.samples.shape[0]:
- self.annotation_widget.canvas.clear()
- # Note: the above command clears the canvas, but any incomplete polygons will be redrawn. Call this
- # private method to clear those. TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/15
- self.annotation_widget.canvas._init_empty_data() # pylint: disable=protected-access
- self.title_widget.value = '
Annotation batch complete!
Thank you for making the model better.'
- return
-
- sample_id = self.samples.loc[self.current_sample, 'sample_id']
- tmap_name = self.samples.loc[self.current_sample, 'tmap_name']
- instance_number = self.samples.loc[self.current_sample, 'instance_number']
- folder = self.samples.loc[self.current_sample, 'folder']
-
- with tempfile.TemporaryDirectory() as tmpdirname:
- sample_hd5 = str(sample_id) + '.hd5'
- local_path = os.path.join(tmpdirname, sample_hd5)
- try:
- tf.io.gfile.copy(src=os.path.join(folder, sample_hd5), dst=local_path)
- hd5 = h5py.File(local_path, mode='r')
- except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- self.annotation_widget.canvas.clear()
- # Note: the above command clears the canvas, but any incomplete polygons will be redrawn. Call this
- # private method to clear those. TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/15
- self.annotation_widget.canvas._init_empty_data() # pylint: disable=protected-access
- self.title_widget.value = f'''
-
-
Warning: MRI HD5 file not available for sample {sample_id} in folder {folder}
- Use the folder parameter to read HD5s from a different local directory or Cloud Storage bucket.
-
{e.message}
-
- '''
- return
-
- tensor = MRI_TMAPS[tmap_name].tensor_from_file(MRI_TMAPS[tmap_name], hd5)
- tensor_instance = tensor[:, :, instance_number]
- if self.zoom > 1.0:
- # TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/11
- img = Image.fromarray(tensor_instance)
- zoomed_img = img.resize([int(self.zoom * s) for s in img.size], Image.LANCZOS)
- tensor_instance = np.asarray(zoomed_img)
-
- self.annotation_widget.display(tensor_instance)
- self.title_widget.value = f'''
- {self.CSS}
-
-
Batch annotation of {self.samples.shape[0]} samples
- {self.USE_INSTRUCTIONS}
-
-
Current sample
- {self._format_info_for_current_sample()}
-
- '''
-
- def annotate_images(self) -> None:
- """Begin the batch annotation task by displaying the annotation widget populated with the first sample.
-
- The submit button is used to proceed to the next sample until all samples have been processed.
- """
- self._annotate_image_for_current_sample()
- display(widgets.VBox([self.title_widget, self.annotation_widget, self.results_widget]))
-
- def view_recent_submissions(self, count: int = 10) -> pd.DataFrame:
- """View a dataframe of up to [count] most recent submissions.
-
- Args:
- count: The number of the most recent submissions to return.
-
- Returns:
- A dataframe of the most recent annotations.
- """
- return self.annotation_storage.view_recent_submissions(count=count)
diff --git a/ml4h/visualization_tools/dicom_interactive_plots.py b/ml4h/visualization_tools/dicom_interactive_plots.py
index ec9d63834..d9850e841 100644
--- a/ml4h/visualization_tools/dicom_interactive_plots.py
+++ b/ml4h/visualization_tools/dicom_interactive_plots.py
@@ -1,4 +1,4 @@
-"""Methods for integration of interactive DICOM plots within notebooks.
+"""Methods for integration of interactive dicom plots within notebooks.
TODO:
* Continue to *pragmatically* improve this to make the visualization controls
@@ -8,15 +8,14 @@
import collections
import os
import tempfile
-from typing import Any, DefaultDict, Dict, Optional, Tuple
import zipfile
from IPython.display import display
from IPython.display import HTML
-import numpy as np
import ipywidgets as widgets
import matplotlib.pyplot as plt
from ml4h.runtime_data_defines import get_mri_folders
+import numpy as np
import pydicom
import tensorflow as tf
@@ -28,12 +27,15 @@
MAX_COLOR_RANGE = 6000
-def choose_mri(sample_id, folder: Optional[str] = None) -> None:
+def choose_mri(sample_id, folder=None):
"""Render widget to choose the MRI to plot.
Args:
sample_id: The id of the sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
+
+ Returns:
+ ipywidget or HTML upon error.
"""
if folder is None:
folders = get_mri_folders(sample_id)
@@ -43,26 +45,22 @@ def choose_mri(sample_id, folder: Optional[str] = None) -> None:
sample_mris = []
sample_mri_glob = str(sample_id) + '_*.zip'
try:
- for f in folders:
- sample_mris.extend(tf.io.gfile.glob(pattern=os.path.join(f, sample_mri_glob)))
+ for folder in folders:
+ sample_mris.extend(tf.io.gfile.glob(pattern=os.path.join(folder, sample_mri_glob)))
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- display(
- HTML(f'''
+ return HTML(f'''
+
Warning: MRI not available for sample {sample_id} in {folders}:
{e.message}
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
'''),
- )
- return
+
''')
if not sample_mris:
- display(
- HTML(f'''
+ return HTML(f'''
+
Warning: MRI DICOMs not available for sample {sample_id} in {folders}.
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
'''),
- )
- return
+
''')
mri_chooser = widgets.Dropdown(
options=sample_mris,
@@ -79,11 +77,14 @@ def choose_mri(sample_id, folder: Optional[str] = None) -> None:
display(file_controls_ui, file_controls_output)
-def choose_mri_series(sample_mri: str) -> None:
+def choose_mri_series(sample_mri):
"""Render widgets and interactive plots for MRIs.
Args:
sample_mri: The local or Cloud Storage path to the MRI file.
+
+ Returns:
+ ipywidget or HTML upon error.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
local_path = os.path.join(tmpdirname, os.path.basename(sample_mri))
@@ -92,15 +93,13 @@ def choose_mri_series(sample_mri: str) -> None:
with zipfile.ZipFile(local_path, 'r') as zip_ref:
zip_ref.extractall(tmpdirname)
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- display(
- HTML(f'''
+ return HTML(f'''
+
Warning: Cardiac MRI not available for sample {os.path.basename(sample_mri)}:
{e.message}
-
'''),
- )
- return
+
''')
- unordered_dicoms: DefaultDict[Any, Any] = collections.defaultdict(dict)
+ unordered_dicoms = collections.defaultdict(dict)
for dcm_file in os.listdir(tmpdirname):
if not dcm_file.endswith('.dcm'):
continue
@@ -113,13 +112,8 @@ def choose_mri_series(sample_mri: str) -> None:
unordered_dicoms[key1][key2] = dcm
if not unordered_dicoms:
- display(
- HTML(f'''
- No series available in MRI for sample {os.path.basename(sample_mri)}.
- Try a different MRI.
-
'''),
- )
- return
+ print(f'\n\nNo series available in MRI for sample {os.path.basename(sample_mri)}\n\nTry a different MRI.')
+ return None
# Convert from dict of dicts to dict of ordered lists.
dicoms = {}
@@ -140,7 +134,7 @@ def choose_mri_series(sample_mri: str) -> None:
style={'description_width': 'initial'},
layout=widgets.Layout(width='800px'),
)
- # Slide through DICOM image instances using a slide bar.
+ # Slide through dicom image instances using a slide bar.
instance_chooser = widgets.IntSlider(
continuous_update=True,
value=default_instance_value,
@@ -218,25 +212,25 @@ def on_value_change(change):
display(viz_controls_ui, viz_controls_output)
-def compute_color_range(dicoms: Dict[str, Any], series_name: str) -> Tuple[int, int]:
+def compute_color_range(dicoms, series_name):
"""Compute the mean values for the color ranges of instances in the series."""
vmin = np.mean([np.min(d.pixel_array) for d in dicoms[series_name]])
vmax = np.mean([np.max(d.pixel_array) for d in dicoms[series_name]])
- return (vmin, vmax)
+ return(vmin, vmax)
-def compute_instance_range(dicoms: Dict[str, Any], series_name: str) -> Tuple[int, int]:
+def compute_instance_range(dicoms, series_name):
"""Compute middle and max instances."""
middle_instance = int(len(dicoms[series_name]) / 2)
max_instance = len(dicoms[series_name])
- return (middle_instance, max_instance)
+ return(middle_instance, max_instance)
def dicom_animation(
- dicoms: Dict[str, Any], series_name: str, instance: int, vmin: int, vmax: int, transpose: bool,
- fig_width: int, title_prefix: str = '',
-) -> None:
- """Render one frame of a DICOM animation.
+ dicoms, series_name, instance, vmin, vmax, transpose,
+ fig_width, title_prefix='',
+):
+ """Render one frame of a dicom animation.
Args:
dicoms: the dictionary DICOM series and instances lists
@@ -256,7 +250,7 @@ def dicom_animation(
dcm = dicoms[series_name][instance - 1]
if instance != dcm.InstanceNumber:
# Notice invalid input, but don't throw an error.
- print(f'WARNING: Instance parameter {str(instance)} and instance number {str(dcm.InstanceNumber)} do not match.')
+ print(f'WARNING: Instance parameter {str(instance)} and dicom instance number {str(dcm.InstanceNumber)} do not match.')
if transpose:
height = dcm.pixel_array.T.shape[0]
diff --git a/ml4h/visualization_tools/dicom_plots.py b/ml4h/visualization_tools/dicom_plots.py
index 093691382..ce2b3e083 100644
--- a/ml4h/visualization_tools/dicom_plots.py
+++ b/ml4h/visualization_tools/dicom_plots.py
@@ -1,17 +1,16 @@
-"""Methods for integration of DICOM plots within notebooks."""
+"""Methods for integration of dicom plots within notebooks."""
import collections
import os
import tempfile
-from typing import Dict, List, Optional, Tuple, Union
import zipfile
from IPython.display import display
from IPython.display import HTML
-import numpy as np
import ipywidgets as widgets
import matplotlib.pyplot as plt
from ml4h.runtime_data_defines import get_cardiac_mri_folder
+import numpy as np
import pydicom
from scipy.ndimage.morphology import binary_closing
from scipy.ndimage.morphology import binary_erosion
@@ -28,21 +27,21 @@
MRI_SEGMENTED_CHANNEL_MAP = {'background': 0, 'ventricle': 1, 'myocardium': 2}
-def _is_mitral_valve_segmentation(d: pydicom.FileDataset) -> bool:
- """Determine whether a DICOM has mitral valve segmentation.
+def _is_mitral_valve_segmentation(d): # -> bool:
+ """Determine whether a dicom has mitral valve segmentation.
This is used for visualization of CINE_segmented_SAX_InlineVF.
Args:
- d: the DICOM file
+ d: the dicom file
Returns:
- Whether or not the DICOM has mitral valve segmentation
+ Whether or not the dicom has mitral valve segmentation
"""
return d.SliceThickness == 6
-def _get_overlay_from_dicom(d: pydicom.FileDataset) -> Tuple[int, int, int]:
+def _get_overlay_from_dicom(d):
"""Get an overlay from a DICOM file.
Morphological operators are used to transform the pixel outline of the
@@ -50,7 +49,7 @@ def _get_overlay_from_dicom(d: pydicom.FileDataset) -> Tuple[int, int, int]:
is used for visualization of CINE_segmented_SAX_InlineVF.
Args:
- d: the DICOM file
+ d: the dicom file
Returns:
Raw overlay array with myocardium outline, anatomical mask (a pixel
@@ -78,30 +77,29 @@ def _get_overlay_from_dicom(d: pydicom.FileDataset) -> Tuple[int, int, int]:
byte >>= 1
bit += 1
overlay = overlay[:expected_bit_length]
- if overlay_frames != 1:
- raise ValueError(f'DICOM has {overlay_frames} overlay frames, but only one expected.')
- overlay = overlay.reshape(rows, cols)
- idx = np.where(overlay == 1)
- min_pos = (np.min(idx[0]), np.min(idx[1]))
- max_pos = (np.max(idx[0]), np.max(idx[1]))
- short_side = min((max_pos[0] - min_pos[0]), (max_pos[1] - min_pos[1]))
- small_radius = max(MRI_MIN_RADIUS, short_side * MRI_SMALL_RADIUS_FACTOR)
- big_radius = max(MRI_MIN_RADIUS+1, short_side * MRI_BIG_RADIUS_FACTOR)
- small_structure = _unit_disk(small_radius)
- m1 = binary_closing(overlay, small_structure).astype(np.int)
- big_structure = _unit_disk(big_radius)
- m2 = binary_closing(overlay, big_structure).astype(np.int)
- anatomical_mask = m1 + m2
- ventricle_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['ventricle'])
- myocardium_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['myocardium'])
- if ventricle_pixels == 0 and myocardium_pixels > MRI_MAX_MYOCARDIUM:
- erode_structure = _unit_disk(small_radius*1.5)
- anatomical_mask = anatomical_mask - binary_erosion(m1, erode_structure).astype(np.int)
+ if overlay_frames == 1:
+ overlay = overlay.reshape(rows, cols)
+ idx = np.where(overlay == 1)
+ min_pos = (np.min(idx[0]), np.min(idx[1]))
+ max_pos = (np.max(idx[0]), np.max(idx[1]))
+ short_side = min((max_pos[0] - min_pos[0]), (max_pos[1] - min_pos[1]))
+ small_radius = max(MRI_MIN_RADIUS, short_side * MRI_SMALL_RADIUS_FACTOR)
+ big_radius = max(MRI_MIN_RADIUS+1, short_side * MRI_BIG_RADIUS_FACTOR)
+ small_structure = _unit_disk(small_radius)
+ m1 = binary_closing(overlay, small_structure).astype(np.int)
+ big_structure = _unit_disk(big_radius)
+ m2 = binary_closing(overlay, big_structure).astype(np.int)
+ anatomical_mask = m1 + m2
ventricle_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['ventricle'])
- return overlay, anatomical_mask, ventricle_pixels
+ myocardium_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['myocardium'])
+ if ventricle_pixels == 0 and myocardium_pixels > MRI_MAX_MYOCARDIUM:
+ erode_structure = _unit_disk(small_radius*1.5)
+ anatomical_mask = anatomical_mask - binary_erosion(m1, erode_structure).astype(np.int)
+ ventricle_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['ventricle'])
+ return overlay, anatomical_mask, ventricle_pixels
-def _unit_disk(r: int) -> np.ndarray:
+def _unit_disk(r): # -> np.ndarray:
"""Get the unit disk for a radius.
This is used for visualization of CINE_segmented_SAX_InlineVF.
@@ -116,9 +114,7 @@ def _unit_disk(r: int) -> np.ndarray:
return (x ** 2 + y ** 2 <= r ** 2).astype(np.int)
-def plot_cardiac_long_axis(
- b_series: List[pydicom.FileDataset], sides: int = 7, fig_width: int = 18, title_prefix: str = '',
-) -> None:
+def plot_cardiac_long_axis(b_series, sides=7, fig_width=18, title_prefix=''):
"""Visualize CINE_segmented_SAX_InlineVF series.
Args:
@@ -172,9 +168,9 @@ def plot_cardiac_long_axis(
def plot_cardiac_short_axis(
- series: List[pydicom.FileDataset], transpose: bool = False, fig_width: int = 18,
- title_prefix: str = '',
-) -> None:
+ series, transpose=False, fig_width=18,
+ title_prefix='',
+):
"""Visualize CINE_segmented_LAX series.
Args:
@@ -229,14 +225,14 @@ def plot_cardiac_short_axis(
def plot_mri_series(
- sample_mri: str, dicoms: Dict[str, pydicom.FileDataset], series_name: str, sax_sides: int,
- lax_transpose: bool, fig_width: int,
-) -> None:
+ sample_mri, dicoms, series_name, sax_sides,
+ lax_transpose, fig_width,
+):
"""Visualize the applicable series within this DICOM.
Args:
sample_mri: The local or Cloud Storage path to the MRI file.
- dicoms: A dictionary of DICOMs.
+ dicoms: A dictionary of dicoms.
series_name: The name of the chosen series.
sax_sides: How many sides to display for CINE_segmented_SAX_InlineVF.
lax_transpose: Whether to transpose when plotting CINE_segmented_LAX.
@@ -262,9 +258,10 @@ def plot_mri_series(
)
else:
print(f'Visualization not currently implemented for {series_name}.')
+ return None
-def choose_mri_series(sample_mri: str) -> None:
+def choose_mri_series(sample_mri):
"""Render widgets and plots for cardiac MRIs.
Visualization is supported for CINE_segmented_SAX_InlineVF series and
@@ -272,6 +269,9 @@ def choose_mri_series(sample_mri: str) -> None:
Args:
sample_mri: The local or Cloud Storage path to the MRI file.
+
+ Returns:
+ ipywidget or HTML upon error.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
local_path = os.path.join(tmpdirname, os.path.basename(sample_mri))
@@ -280,13 +280,11 @@ def choose_mri_series(sample_mri: str) -> None:
with zipfile.ZipFile(local_path, 'r') as zip_ref:
zip_ref.extractall(tmpdirname)
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- display(
- HTML(f'''
+ return HTML(f'''
+
Warning: Cardiac MRI not available for sample {os.path.basename(sample_mri)}:
- Neither CINE_segmented_SAX_InlineVF nor CINE_segmented_LAX available in MRI for sample {os.path.basename(sample_mri)}.
- Try a different MRI.
-
'''),
+ print(
+ f'\n\nNeither CINE_segmented_SAX_InlineVF nor CINE_segmented_LAX available in MRI for sample {os.path.basename(sample_mri)}.',
+ '\n\nTry a different MRI.',
)
+ return None
-def choose_cardiac_mri(sample_id: Union[int, str], folder: Optional[str] = None) -> None:
+def choose_cardiac_mri(sample_id, folder=None):
"""Render widget to choose the cardiac MRI to plot.
Args:
sample_id: The id of the ECG sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
+
+ Returns:
+ ipywidget or HTML upon error.
"""
if folder is None:
folder = get_cardiac_mri_folder(sample_id)
@@ -374,23 +374,19 @@ def choose_cardiac_mri(sample_id: Union[int, str], folder: Optional[str] = None)
try:
sample_mris = tf.io.gfile.glob(pattern=os.path.join(folder, sample_mri_glob))
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- display(
- HTML(f'''
+ return HTML(f'''
+
Warning: Cardiac MRI not available for sample {sample_id} in {folder}:
{e.message}
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
'''),
- )
- return
+
''')
if not sample_mris:
- display(
- HTML(f'''
+ return HTML(f'''
+
Warning: Cardiac MRI DICOM not available for sample {sample_id} in {folder}.
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
'''),
- )
- return
+
''')
mri_chooser = widgets.Dropdown(
options=[(os.path.basename(mri), mri) for mri in sample_mris],
diff --git a/ml4h/visualization_tools/ecg_interactive_plots.py b/ml4h/visualization_tools/ecg_interactive_plots.py
index 18ed39a9b..97a4e1547 100644
--- a/ml4h/visualization_tools/ecg_interactive_plots.py
+++ b/ml4h/visualization_tools/ecg_interactive_plots.py
@@ -2,12 +2,10 @@
import os
import tempfile
-from typing import Optional, Union
-from IPython.display import HTML
import altair as alt # Interactive data visualization for plots.
-from ml4h.TensorMap import TensorMap
-from ml4h.visualization_tools.ecg_reshape import DEFAULT_RESTING_ECG_SIGNAL_TMAP
+from IPython.display import HTML
+from ml4h.visualization_tools.ecg_reshape import DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME
from ml4h.visualization_tools.ecg_reshape import reshape_exercise_ecg_to_tidy
from ml4h.visualization_tools.ecg_reshape import reshape_resting_ecg_to_tidy
@@ -33,21 +31,18 @@
)
-def resting_ecg_interactive_plot(
- sample_id: Union[int, str], folder: Optional[str] = None,
- tmap: TensorMap = DEFAULT_RESTING_ECG_SIGNAL_TMAP,
-) -> Union[HTML, alt.Chart]:
+def resting_ecg_interactive_plot(sample_id, folder=None, tmap_name=DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME):
"""Wrangle resting ECG data to tidy and present it as an interactive plot.
Args:
sample_id: The id of the ECG sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
- tmap: The TensorMap to use for ECG input.
+ tmap_name: The name of the TMAP to use for ecg input.
Returns:
An Altair plot or a notebook-friendly error.
"""
- tidy_resting_ecg_signal = reshape_resting_ecg_to_tidy(sample_id, folder, tmap)
+ tidy_resting_ecg_signal = reshape_resting_ecg_to_tidy(sample_id, folder, tmap_name)
if tidy_resting_ecg_signal.shape[0] == 0:
return HTML(f'''
@@ -90,9 +85,7 @@ def resting_ecg_interactive_plot(
return upper & lower
-def exercise_ecg_interactive_plot(
- sample_id: Union[int, str], folder: Optional[str] = None, time_interval_seconds: int = 10,
-) -> Union[HTML, alt.Chart]:
+def exercise_ecg_interactive_plot(sample_id, folder=None, time_interval_seconds=10):
"""Wrangle exercise ECG data to tidy and present it as an interactive plot.
Args:
@@ -147,8 +140,7 @@ def exercise_ecg_interactive_plot(
lead_select,
).transform_filter(
# https://github.com/altair-viz/altair/issues/1960
- f'''((toNumber({brush.name}.time) - {time_interval_seconds/2.0}) < datum.time)
- && (datum.time < toNumber({brush.name}.time) + {time_interval_seconds/2.0})''',
+ f'((toNumber({brush.name}.time) - {time_interval_seconds/2.0}) < datum.time) && (datum.time < toNumber({brush.name}.time) + {time_interval_seconds/2.0})',
)
return trend.encode(y='heartrate:Q') & trend.encode(y='load:Q') & signal
diff --git a/ml4h/visualization_tools/ecg_reshape.py b/ml4h/visualization_tools/ecg_reshape.py
index 167eb5012..b3213d359 100644
--- a/ml4h/visualization_tools/ecg_reshape.py
+++ b/ml4h/visualization_tools/ecg_reshape.py
@@ -1,57 +1,53 @@
"""Methods for reshaping raw ECG signal data for use in the pandas ecosystem."""
import os
import tempfile
-from typing import Any, Dict, Optional, Tuple, Union
-import numpy as np
-import pandas as pd
from biosppy.signals.tools import filter_signal
import h5py
from ml4h.defines import ECG_BIKE_LEADS
from ml4h.defines import ECG_REST_LEADS
from ml4h.runtime_data_defines import get_exercise_ecg_hd5_folder
from ml4h.runtime_data_defines import get_resting_ecg_hd5_folder
-from ml4h.TensorMap import TensorMap
-import ml4h.tensormap.ukb.ecg as ecg_tmaps
+from ml4h.tensor_maps_by_hand import TMAPS
+import numpy as np
+import pandas as pd
import tensorflow as tf
RAW_SCALE = 0.005 # Convert to mV.
SAMPLING_RATE = 500.0
-DEFAULT_RESTING_ECG_SIGNAL_TMAP = ecg_tmaps.ecg_rest
+DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME = 'ecg_rest'
# TODO(deflaux): parameterize exercise ECG by TMAP name if there is similar ECG data from other studies.
-EXERCISE_ECG_SIGNAL_TMAP = ecg_tmaps.ecg_bike_raw_full
+EXERCISE_ECG_SIGNAL_TMAP = TMAPS['ecg-bike-raw-full']
EXERCISE_ECG_TREND_TMAPS = [
- ecg_tmaps.ecg_bike_raw_trend_hr,
- ecg_tmaps.ecg_bike_raw_trend_load,
- ecg_tmaps.ecg_bike_raw_trend_grade,
- ecg_tmaps.ecg_bike_raw_trend_artifact,
- ecg_tmaps.ecg_bike_raw_trend_mets,
- ecg_tmaps.ecg_bike_raw_trend_pacecount,
- ecg_tmaps.ecg_bike_raw_trend_phasename,
- ecg_tmaps.ecg_bike_raw_trend_phasetime,
- ecg_tmaps.ecg_bike_raw_trend_time,
- ecg_tmaps.ecg_bike_raw_trend_vecount,
+ TMAPS['ecg-bike-raw-trend-hr'],
+ TMAPS['ecg-bike-raw-trend-load'],
+ TMAPS['ecg-bike-raw-trend-grade'],
+ TMAPS['ecg-bike-raw-trend-artifact'],
+ TMAPS['ecg-bike-raw-trend-mets'],
+ TMAPS['ecg-bike-raw-trend-pacecount'],
+ TMAPS['ecg-bike-raw-trend-phasename'],
+ TMAPS['ecg-bike-raw-trend-phasetime'],
+ TMAPS['ecg-bike-raw-trend-time'],
+ TMAPS['ecg-bike-raw-trend-vecount'],
]
EXERCISE_PHASES = {0.0: 'Pretest', 1.0: 'Exercise', 2.0: 'Recovery'}
-def _examine_available_keys(hd5: Dict[str, Any]) -> None:
+def _examine_available_keys(hd5):
print(f'hd5 ECG keys {[k for k in hd5.keys() if "ecg" in k]}')
for key in [k for k in hd5.keys() if 'ecg' in k]:
- print(f'hd5 {key} keys {k for k in hd5[key]}')
+ print(f'hd5 {key} keys {[k for k in hd5[key].keys()]}')
-def reshape_resting_ecg_to_tidy(
- sample_id: Union[int, str], folder: Optional[str] = None, tmap: TensorMap = DEFAULT_RESTING_ECG_SIGNAL_TMAP,
-) -> pd.DataFrame:
+def reshape_resting_ecg_to_tidy(sample_id, folder=None, tmap_name=DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME):
"""Wrangle resting ECG data to tidy.
Args:
sample_id: The id of the ECG sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
- tmap: The TensorMap to use for ECG input.
+ tmap_name: The name of the TMAP to use for ecg input.
Returns:
A pandas dataframe in tidy format or print a notebook-friendly error and return an empty dataframe.
@@ -59,7 +55,7 @@ def reshape_resting_ecg_to_tidy(
if folder is None:
folder = get_resting_ecg_hd5_folder(sample_id)
- data: Dict[str, Any] = {'lead': [], 'raw': [], 'ts_reference': [], 'filtered': [], 'filtered_1': [], 'filtered_2': []}
+ data = {'lead': [], 'raw': [], 'ts_reference': [], 'filtered': [], 'filtered_1': [], 'filtered_2': []}
with tempfile.TemporaryDirectory() as tmpdirname:
sample_hd5 = str(sample_id) + '.hd5'
@@ -73,10 +69,10 @@ def reshape_resting_ecg_to_tidy(
with h5py.File(local_path, mode='r') as hd5:
try:
- signals = tmap.tensor_from_file(tmap, hd5)
+ signals = TMAPS[tmap_name].tensor_from_file(TMAPS[tmap_name], hd5)
except (KeyError, ValueError) as e:
- print(f'''Warning: Resting ECG TMAP {tmap.name} not available for sample {sample_id}.
- Use the tmap parameter to choose a different TMAP.\n\n{e}''')
+ print(f'''Warning: Resting ECG TMAP {tmap_name} not available for sample {sample_id}.
+ Use the tmap_name parameter to choose a different TMAP.\n\n{e}''')
_examine_available_keys(hd5)
return pd.DataFrame(data)
for (lead, channel) in ECG_REST_LEADS.items():
@@ -140,9 +136,7 @@ def reshape_resting_ecg_to_tidy(
return tidy_signal_df
-def reshape_exercise_ecg_to_tidy(
- sample_id: Union[int, str], folder: Optional[str] = None,
-) -> Tuple[pd.DataFrame, pd.DataFrame]:
+def reshape_exercise_ecg_to_tidy(sample_id, folder=None):
"""Wrangle exercise ECG signal data to tidy format.
Args:
@@ -214,9 +208,7 @@ def reshape_exercise_ecg_to_tidy(
return (trend_df, tidy_signal_df)
-def reshape_exercise_ecg_and_trend_to_tidy(
- sample_id: Union[int, str], folder: Optional[str] = None,
-) -> Tuple[pd.DataFrame, pd.DataFrame]:
+def reshape_exercise_ecg_and_trend_to_tidy(sample_id, folder=None):
"""Wrangle exercise ECG signal and trend data to tidy format.
Args:
diff --git a/ml4h/visualization_tools/ecg_static_plots.py b/ml4h/visualization_tools/ecg_static_plots.py
index ac7283237..2ebcfc3e1 100644
--- a/ml4h/visualization_tools/ecg_static_plots.py
+++ b/ml4h/visualization_tools/ecg_static_plots.py
@@ -1,18 +1,17 @@
"""Methods for integration of static plots within notebooks."""
import os
import tempfile
-from typing import List, Optional, Union
from IPython.display import HTML
from IPython.display import SVG
-import numpy as np
from ml4h.plots import plot_ecg_rest
from ml4h.runtime_data_defines import get_resting_ecg_hd5_folder
from ml4h.runtime_data_defines import get_resting_ecg_svg_folder
+import numpy as np
import tensorflow as tf
-def display_resting_ecg(sample_id: Union[int, str], folder: Optional[str] = None) -> Union[HTML, SVG]:
+def display_resting_ecg(sample_id, folder=None):
"""Retrieve (or render) and display the SVG of the resting ECG.
Args:
@@ -54,8 +53,8 @@ def display_resting_ecg(sample_id: Union[int, str], folder: Optional[str] = None
try:
# We don't need the resulting SVG, so send it to a temporary directory.
with tempfile.TemporaryDirectory() as tmpdirname:
- return plot_ecg_rest(tensor_paths=[local_path], rows=[0], out_folder=tmpdirname, is_blind=False)
- except Exception as e: # pylint: disable=broad-except
+ plot_ecg_rest(tensor_paths = [local_path], rows=[0], out_folder=tmpdirname, is_blind=False)
+ except Exception as e:
return HTML(f'''
Warning: Unable to render static plot of resting ECG for sample {sample_id} from {hd5_folder}:
@@ -63,7 +62,7 @@ def display_resting_ecg(sample_id: Union[int, str], folder: Optional[str] = None
''')
-def major_breaks_x_resting_ecg(limits: List[float]) -> np.array:
+def major_breaks_x_resting_ecg(limits):
"""Method to compute breaks for plotnine plots of ECG resting data.
Args:
diff --git a/ml4h/visualization_tools/facets.py b/ml4h/visualization_tools/facets.py
index 18f96327d..a45ea88da 100644
--- a/ml4h/visualization_tools/facets.py
+++ b/ml4h/visualization_tools/facets.py
@@ -2,7 +2,6 @@
import base64
import os
-import pandas as pd
from facets_overview.generic_feature_statistics_generator import GenericFeatureStatisticsGenerator
FACETS_DEPENDENCIES = {
@@ -26,10 +25,10 @@
FACETS_DEPENDENCIES[dep] = os.path.basename(url)
-class FacetsOverview():
+class FacetsOverview(object):
"""Methods for Facets Overview notebook integration."""
- def __init__(self, data: pd.DataFrame):
+ def __init__(self, data):
# This takes the dataframe and computes all the inputs to the Facets
# Overview plots such as:
# - numeric variables: histogram bins, mean, min, median, max, etc..
@@ -40,7 +39,7 @@ def __init__(self, data: pd.DataFrame):
[{'name': 'data', 'table': data}],
)
- def _repr_html_(self) -> str:
+ def _repr_html_(self):
"""Html representation of Facets Overview for use in a Jupyter notebook."""
protostr = base64.b64encode(self._proto.SerializeToString()).decode('utf-8')
html_template = '''
@@ -58,14 +57,14 @@ def _repr_html_(self) -> str:
return html
-class FacetsDive():
+class FacetsDive(object):
"""Methods for Facets Dive notebook integration."""
- def __init__(self, data: pd.DataFrame, height: int = 1000):
+ def __init__(self, data, height=1000):
self._data = data
self.height = height
- def _repr_html_(self) -> str:
+ def _repr_html_(self):
"""Html representation of Facets Dive for use in a Jupyter notebook."""
html_template = """
diff --git a/ml4h/visualization_tools/hd5_mri_plots.py b/ml4h/visualization_tools/hd5_mri_plots.py
index d3894b39d..20b3305b1 100644
--- a/ml4h/visualization_tools/hd5_mri_plots.py
+++ b/ml4h/visualization_tools/hd5_mri_plots.py
@@ -1,34 +1,29 @@
"""Methods for integration of plots of mri data processed to 3D tensors from within notebooks."""
-from collections import OrderedDict
from enum import Enum, auto
import os
import tempfile
-from typing import Any, Dict, List, Optional, Tuple, Union
+import h5py
from IPython.display import display
from IPython.display import HTML
-import numpy as np
-import h5py
import ipywidgets as widgets
import matplotlib.pyplot as plt
from ml4h.runtime_data_defines import get_mri_hd5_folder
-import ml4h.tensormap.ukb.mri as ukb_mri
-import ml4h.tensormap.ukb.mri_vtk as ukb_mri_vtk
-from ml4h.TensorMap import Interpretation, TensorMap
+from ml4h.tensor_maps_by_hand import TMAPS
+from ml4h.TensorMap import Interpretation
+import numpy as np
import tensorflow as tf
-# Discover applicable TensorMaps.
-MRI_TMAPS = {
- key: value for key, value in ukb_mri.__dict__.items() if isinstance(value, TensorMap)
- and value.interpretation == Interpretation.CONTINUOUS and value.axes() == 3
-}
-MRI_TMAPS.update(
- {
- key: value for key, value in ukb_mri_vtk.__dict__.items()
- if isinstance(value, TensorMap) and value.interpretation == Interpretation.CONTINUOUS and value.axes() == 3
- },
+# Discover applicable TMAPS.
+CARDIAC_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if ('_lax_' in k or '_sax_' in k) and TMAPS[k].axes() == 3]
+CARDIAC_MRI_TMAP_NAMES.extend(
+ [k for k in TMAPS.keys() if TMAPS[k].path_prefix == 'ukb_cardiac_mri' and TMAPS[k].axes() == 3],
)
+LIVER_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if TMAPS[k].path_prefix == 'ukb_liver_mri' and TMAPS[k].axes() == 3]
+BRAIN_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if TMAPS[k].path_prefix == 'ukb_brain_mri' and TMAPS[k].axes() == 3]
+# This includes more than just MRI TMAPS, it is a best effort.
+BEST_EFFORT_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if TMAPS[k].interpretation == Interpretation.CONTINUOUS and TMAPS[k].axes() == 3]
MIN_IMAGE_WIDTH = 8
DEFAULT_IMAGE_WIDTH = 12
@@ -46,30 +41,42 @@ class PlotType(Enum):
class TensorMapCache:
"""Cache the tensor to display for reuse when re-plotting the same TMAP with different plot parameters."""
- def __init__(self, hd5: Dict[str, Any], tmap: TensorMap):
+ def __init__(self, hd5, tmap_name):
self.hd5 = hd5
- self.tmap: Optional[TensorMap] = None
+ self.tmap_name = None
self.tensor = None
- _ = self.get(tmap)
+ _ = self.get(tmap_name)
- def get(self, tmap: TensorMap) -> np.array:
- if self.tmap != tmap:
- self.tensor = tmap.tensor_from_file(tmap, self.hd5)
- self.tmap = tmap
+ def get(self, tmap_name):
+ if self.tmap_name != tmap_name:
+ self.tensor = TMAPS[tmap_name].tensor_from_file(TMAPS[tmap_name], self.hd5)
+ self.tmap_name = tmap_name
return self.tensor
-def choose_mri_tmap(
- sample_id: Union[int, str], folder: Optional[str] = None, tmap: Optional[TensorMap] = None,
- default_tmaps: Dict[str, TensorMap] = MRI_TMAPS,
-) -> None:
+def choose_cardiac_mri_tmap(sample_id, folder=None, tmap_name='cine_lax_4ch_192', default_tmap_names=CARDIAC_MRI_TMAP_NAMES):
+ choose_mri_tmap(sample_id, folder, tmap_name, default_tmap_names)
+
+
+def choose_brain_mri_tmap(sample_id, folder=None, tmap_name='t2_flair_sag_p2_1mm_fs_ellip_pf78_1', default_tmap_names=BRAIN_MRI_TMAP_NAMES):
+ choose_mri_tmap(sample_id, folder, tmap_name, default_tmap_names)
+
+
+def choose_liver_mri_tmap(sample_id, folder=None, tmap_name='liver_shmolli_segmented', default_tmap_names=LIVER_MRI_TMAP_NAMES):
+ choose_mri_tmap(sample_id, folder, tmap_name, default_tmap_names)
+
+
+def choose_mri_tmap(sample_id, folder=None, tmap_name=None, default_tmap_names=BEST_EFFORT_MRI_TMAP_NAMES):
"""Render widgets and plots for MRI tensors.
Args:
sample_id: The id of the sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
- tmap: The TensorMap for the 3D MRI tensor to visualize.
- default_tmaps: Other TensorMaps to offer for visualization, if present in the hd5.
+ tmap_name: The TMAP name for the 3D MRI tensor to visualize.
+ default_tmap_names: Other TMAP names to offer for visualization, if present in the hd5.
+
+ Returns:
+ ipywidget or HTML upon error.
"""
if folder is None:
folder = get_mri_hd5_folder(sample_id)
@@ -81,45 +88,42 @@ def choose_mri_tmap(
tf.io.gfile.copy(src=os.path.join(folder, sample_hd5), dst=local_path)
hd5 = h5py.File(local_path, mode='r')
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- display(
- HTML(f'''
+ return HTML(f'''
+
Warning: MRI HD5 file not available for sample {sample_id} in folder {folder}:
{e.message}
Use the folder parameter to read HD5s from a different local directory or Cloud Storage bucket.
-
'''),
- )
- return
-
- sample_tmaps = OrderedDict()
- # Add the passed tmap parameter, if it is present in this hd5.
- if tmap:
- if tmap.hd5_key_guess() in hd5:
- if len(tmap.shape) == 3:
- sample_tmaps[tmap.name] = tmap
+
''')
+
+ sample_tmap_names = []
+ # Add the passed tmap_name parameter, if it is present in this hd5.
+ if tmap_name:
+ if TMAPS[tmap_name].hd5_key_guess() in hd5:
+ if len(TMAPS[tmap_name].shape) == 3:
+ sample_tmap_names.append(tmap_name)
else:
- print(f'{tmap} is not a 3D tensor, skipping it')
+ print(f'{tmap_name} is not a 3D tensor, skipping it')
else:
- print(f'{tmap} is not available in {sample_id}')
- # Also discover applicable TensorMaps for this particular sample's HD5 file.
- sample_tmaps.update({n: t for n, t in sorted(default_tmaps.items(), key=lambda t: t[0]) if t.hd5_key_guess() in hd5})
-
- if not sample_tmaps:
- display(
- HTML(f'''
- Neither {tmap.name} nor any of {default_tmaps.keys()} are present in this HD5 for sample {sample_id} in {folder}.
- Use the tmap parameter to try a different TensorMap or the folder parameter to try a different hd5 for the sample.
-
'''),
- )
- return
-
- default_tmap_value = next(iter(sample_tmaps.values()))
+ print(f'{tmap_name} is not available in {sample_id}')
+ # Also discover applicable TMAPS for this particular sample's HD5 file.
+ sample_tmap_names.extend(
+ sorted(set([k for k in default_tmap_names if TMAPS[k].hd5_key_guess() in hd5])),
+ )
+
+ if not sample_tmap_names:
+ return HTML(f'''
+ Neither {tmap_name} nor any of {default_tmap_names} are present in this HD5 for sample {sample_id} in {folder}.
+ Use the tmap_name parameter to try a different TMAP or the folder parameter to try a different hd5 for the sample.
+
'),
- tmap_chooser,
+ tmap_name_chooser,
widgets.HBox([transpose_chooser, fig_width_chooser]),
widgets.HBox([flip_chooser, color_range_chooser]),
widgets.HBox([plot_type_chooser, instance_chooser]),
],
layout=widgets.Layout(width='auto', border='solid 1px grey'),
)
- tmap_cache = TensorMapCache(hd5=hd5, tmap=tmap_chooser.value)
+ tmap_cache = TensorMapCache(hd5=hd5, tmap_name=tmap_name_chooser.value)
viz_controls_output = widgets.interactive_output(
plot_mri_tmap,
{
'sample_id': widgets.fixed(sample_id),
'tmap_cache': widgets.fixed(tmap_cache),
- 'tmap': tmap_chooser,
+ 'tmap_name': tmap_name_chooser,
'plot_type': plot_type_chooser,
'instance': instance_chooser,
'color_range': color_range_chooser,
@@ -205,36 +209,33 @@ def on_plot_type_change(change):
else:
instance_chooser.layout.visibility = 'hidden'
- tmap_chooser.observe(on_tmap_value_change, names='value')
+ tmap_name_chooser.observe(on_tmap_value_change, names='value')
plot_type_chooser.observe(on_plot_type_change, names='value')
display(viz_controls_ui, viz_controls_output)
-def compute_color_range(hd5: Dict[str, Any], tmap: TensorMap) -> List[int]:
+def compute_color_range(hd5, tmap_name):
"""Compute the mean values for the color ranges of instances in the MRI series."""
- mri_tensor = tmap.tensor_from_file(tmap, hd5)
+ mri_tensor = TMAPS[tmap_name].tensor_from_file(TMAPS[tmap_name], hd5)
vmin = np.mean([np.min(mri_tensor[:, :, i]) for i in range(0, mri_tensor.shape[2])])
vmax = np.mean([np.max(mri_tensor[:, :, i]) for i in range(0, mri_tensor.shape[2])])
- return [vmin, vmax]
+ return[vmin, vmax]
-def compute_instance_range(tmap: TensorMap) -> Tuple[int, int]:
+def compute_instance_range(tmap_name):
"""Compute middle and max instances."""
- middle_instance = int(tmap.shape[2] / 2)
- max_instance = tmap.shape[2]
- return (middle_instance, max_instance)
+ middle_instance = int(TMAPS[tmap_name].shape[2] / 2)
+ max_instance = TMAPS[tmap_name].shape[2]
+ return(middle_instance, max_instance)
-def plot_mri_tmap(
- sample_id: Union[int, str], tmap_cache: TensorMapCache, tmap: TensorMap, plot_type: PlotType,
- instance: int, color_range: Tuple[int, int], transpose: bool, flip: bool, fig_width: int,
-) -> None:
+def plot_mri_tmap(sample_id, tmap_cache, tmap_name, plot_type, instance, color_range, transpose, flip, fig_width):
"""Visualize the applicable MRI series within this HD5 file.
Args:
sample_id: The local or Cloud Storage path to the MRI file.
tmap_cache: The cache from which to retrieve the tensor to be plotted.
- tmap: The chosen TensorMap for the MRI series.
+ tmap_name: The name of the chosen TMAP for the MRI series.
plot_type: Whether to display instances interactively or in a panel view.
instance: The particular instance to display, if interactive.
color_range: Array of minimum and maximum value for the color range.
@@ -242,9 +243,12 @@ def plot_mri_tmap(
flip: Whether to flip the image on its vertical axis
fig_width: The desired width of the figure. Note that height computed as
the proportion of the width based on the data to be plotted.
+
+ Returns:
+ The plot or a notebook-friendly error message.
"""
- title_prefix = f'{tmap.name} from MRI {sample_id}'
- mri_tensor = tmap_cache.get(tmap)
+ title_prefix = f'{tmap_name} from MRI {sample_id}'
+ mri_tensor = tmap_cache.get(tmap_name)
if plot_type == PlotType.INTERACTIVE:
plot_mri_tensor_as_animation(
mri_tensor=mri_tensor,
@@ -271,13 +275,10 @@ def plot_mri_tmap(
title_prefix=title_prefix,
)
else:
- HTML(f'''
\n",
- " Terra Users test with the most recent custom Docker image which has all the software dependencies preinstalled. (e.g., more recent than gcr.io/uk-biobank-sek-data/ml4h_terra:20200918_091608)\n",
+ " Terra Users test with the most recent custom Docker image which has all the software dependencies preinstalled. (e.g., more recent than gcr.io/uk-biobank-sek-data/ml4h_terra:20200729_091732)\n",
"
Terra is running custom Docker image gcr.io/uk-biobank-sek-data/ml4h_terra:20200918_091608.
\n",
+ "
Terra is running custom Docker image gcr.io/uk-biobank-sek-data/ml4h_terra:20200729_091732.
\n",
"
ml4h is running custom Docker image gcr.io/broad-ml4cvd/deeplearning:tf2-latest-gpu.
\n",
"
\n",
"
"
@@ -79,7 +79,7 @@
"source": [
"#---[ EDIT THIS VARIABLE VALUE IF YOU LIKE ]---\n",
"# TODO(paolo and team): provide CSV with phenotypes and ML results for fake samples.\n",
- "MODEL_RESULTS_FILE = 'gs://uk-biobank-sek-data-us-east1/phenotypes/ml4cvd/ukbiobank_query_results_plus_four_fake_samples.csv'"
+ "MODEL_RESULTS_FILE = 'gs://uk-biobank-sek-data-us-east1/phenotypes/ml4h/ukbiobank_query_results_plus_four_fake_samples.csv'"
]
},
{
diff --git a/notebooks/terra_featured_workspace/image_annotations_demo.ipynb b/notebooks/terra_featured_workspace/image_annotations_demo.ipynb
deleted file mode 100644
index ce15e4d73..000000000
--- a/notebooks/terra_featured_workspace/image_annotations_demo.ipynb
+++ /dev/null
@@ -1,259 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Image annotations for a batch of samples\n",
- "\n",
- "Using this notebook, cardiologists are able to quickly view and annotate MRI images for a batch of samples. These annotated images become the training data for the next round of modeling."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Setup\n",
- "\n",
- "
\n",
- " This notebook assumes\n",
- "
\n",
- "
Terra is running custom Docker image gcr.io/uk-biobank-sek-data/ml4h_terra:20200918_091608.
\n",
- "
ml4cvd is running custom Docker image gcr.io/broad-ml4cvd/deeplearning:tf2-latest-gpu.
Terra is running custom Docker image gcr.io/uk-biobank-sek-data/ml4h_terra:20200918_091608.
\n",
+ "
Terra is running custom Docker image gcr.io/uk-biobank-sek-data/ml4h_terra:20200729_091732.
\n",
"
ml4h is running custom Docker image gcr.io/broad-ml4cvd/deeplearning:tf2-latest-gpu.
\n",
"
\n",
"
"
@@ -84,7 +84,7 @@
"source": [
"#---[ EDIT THIS VARIABLE VALUE IF YOU LIKE ]---\n",
"# TODO(paolo and team): provide CSV with phenotypes and ML results for fake samples.\n",
- "MODEL_RESULTS_FILE = 'gs://uk-biobank-sek-data-us-east1/phenotypes/ml4cvd/ukbiobank_query_results_plus_four_fake_samples.csv'"
+ "MODEL_RESULTS_FILE = 'gs://uk-biobank-sek-data-us-east1/phenotypes/ml4h/ukbiobank_query_results_plus_four_fake_samples.csv'"
]
},
{
diff --git a/pylintrc b/pylintrc
deleted file mode 100644
index 8a5e40122..000000000
--- a/pylintrc
+++ /dev/null
@@ -1,337 +0,0 @@
-# This configuration was copied from https://github.com/tensorflow/tensorflow/blob/18ebe824d2f6f20b09839cb0a0073032a2d6c5fe/tensorflow/tools/ci_build/pylintrc and then further modified.
-
-[MASTER]
-
-# Specify a configuration file.
-#rcfile=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Profiled execution.
-profile=no
-
-# Add files or directories to the denylist. They should be base names, not
-# paths.
-ignore=CVS
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-
-[MESSAGES CONTROL]
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time. See also the "--disable" option for examples.
-enable=indexing-exception,old-raise-syntax
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once).You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use"--disable=all --enable=classes
-# --disable=W"
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,no-member,no-name-in-module,import-error,unsubscriptable-object,unbalanced-tuple-unpacking,undefined-variable,not-context-manager
-
-
-# Set the cache size for astng objects.
-cache-size=500
-
-
-[REPORTS]
-
-# Set the output format. Available formats are text, parseable, colorized, msvs
-# (visual studio) and html. You can also give a reporter class, eg
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Put messages in a separate file for each module / package specified on the
-# command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]".
-files-output=no
-
-# Tells whether to display a full report or only the messages
-reports=no
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Add a comment according to your evaluation note. This is used by the global
-# evaluation report (RP0004).
-comment=no
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details
-#msg-template=
-
-
-[TYPECHECK]
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# List of classes names for which member attributes should not be checked
-# (useful for classes with attributes dynamically set).
-ignored-classes=SQLObject
-
-# When zope mode is activated, add a predefined set of Zope acquired attributes
-# to generated-members.
-zope=no
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E0201 when accessed. Python regular
-# expressions are accepted.
-generated-members=REQUEST,acl_users,aq_parent
-
-# List of decorators that create context managers from functions, such as
-# contextlib.contextmanager.
-contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
-
-
-[VARIABLES]
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# A regular expression matching the beginning of the name of dummy variables
-# (i.e. not used).
-dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-
-[BASIC]
-
-# Required attributes for module, separated by a comma
-required-attributes=
-
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=apply,input,reduce
-
-
-# Disable the report(s) with the given id(s).
-# All non-Google reports are disabled by default.
-disable-report=R0001,R0002,R0003,R0004,R0101,R0102,R0201,R0202,R0220,R0401,R0402,R0701,R0801,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,R0921,R0922,R0923
-
-# Regular expression which should only match correct module names
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Regular expression which should only match correct module level names
-const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
-
-# Regular expression which should only match correct class names
-class-rgx=^_?[A-Z][a-zA-Z0-9]*$
-
-# Regular expression which should only match correct function names
-function-rgx=^(?:(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$
-
-# Regular expression which should only match correct method names
-method-rgx=^(?:(?P__[a-z0-9_]+__|next)|(?P_{0,2}[A-Z][a-zA-Z0-9]*)|(?P_{0,2}[a-z][a-z0-9_]*))$
-
-# Regular expression which should only match correct instance attribute names
-attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
-
-# Regular expression which should only match correct argument names
-argument-rgx=^[a-z][a-z0-9_]*$
-
-# Regular expression which should only match correct variable names
-variable-rgx=^[a-z][a-z0-9_]*$
-
-# Regular expression which should only match correct attribute names in class
-# bodies
-class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
-
-# Regular expression which should only match correct list comprehension /
-# generator expression variable names
-inlinevar-rgx=^[a-z][a-z0-9_]*$
-
-# Good variable names which should always be accepted, separated by a comma
-good-names=main,_
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=(__.*__|main)
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=10
-
-
-[FORMAT]
-
-# Maximum number of characters on a single line.
-max-line-length=120
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=(?x)
- (^\s*(import|from)\s
- |\$Id:\s\/\/depot\/.+#\d+\s\$
- |^[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*("[^"]\S+"|'[^']\S+')
- |^\s*\#\ LINT\.ThenChange
- |^[^#]*\#\ type:\ [a-zA-Z_][a-zA-Z0-9_.,[\] ]*$
- |pylint
- |"""
- |\#
- |lambda
- |(https?|ftp):)
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=y
-
-# List of optional constructs for which whitespace checking is disabled
-no-space-check=
-
-# Maximum number of lines in a module
-max-module-lines=99999
-
-# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
-# tab).
-indent-string=' '
-
-
-[SIMILARITIES]
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=
-
-
-[IMPORTS]
-
-# Deprecated modules which should not be used, separated by a comma
-deprecated-modules=regsub,TERMIOS,Bastion,rexec,sets
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled)
-import-graph=
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled)
-ext-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled)
-int-import-graph=
-
-
-[CLASSES]
-
-# List of interface methods to ignore, separated by a comma. This is used for
-# instance to not check methods defines in Zope's Interface base class.
-ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,__new__,setUp
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls,class_
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=mcs
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method
-max-args=5
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore
-ignored-argument-names=_.*
-
-# Maximum number of locals for function / method body
-max-locals=15
-
-# Maximum number of return / yield for function / method body
-max-returns=6
-
-# Maximum number of branch for function / method body
-max-branches=12
-
-# Maximum number of statements in function / method body
-max-statements=50
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "Exception"
-overgeneral-exceptions=Exception,StandardError,BaseException
-
-
-[AST]
-
-# Maximum line length for lambdas
-short-func-length=1
-
-# List of module members that should be marked as deprecated.
-# All of the string functions are listed in 4.1.4 Deprecated string functions
-# in the Python 2.4 docs.
-deprecated-members=string.atof,string.atoi,string.atol,string.capitalize,string.expandtabs,string.find,string.rfind,string.index,string.rindex,string.count,string.lower,string.split,string.rsplit,string.splitfields,string.join,string.joinfields,string.lstrip,string.rstrip,string.strip,string.swapcase,string.translate,string.upper,string.ljust,string.rjust,string.center,string.zfill,string.replace,sys.exitfunc
-
-
-[DOCSTRING]
-
-# List of exceptions that do not need to be mentioned in the Raises section of
-# a docstring.
-ignore-exceptions=AssertionError,NotImplementedError,StopIteration,TypeError
-
-
-
-[TOKENS]
-
-# Number of spaces of indent required when the last token on the preceding line
-# is an open (, [, or {.
-indent-after-paren=4
-
-
-[GOOGLE LINES]
-
-# Regexp for a proper copyright notice.
-copyright=Copyright \d{4} The TensorFlow Authors\. +All [Rr]ights [Rr]eserved\.
diff --git a/scripts/jupyter.sh b/scripts/jupyter.sh
index 32edfb1f3..a83fd582d 100755
--- a/scripts/jupyter.sh
+++ b/scripts/jupyter.sh
@@ -54,7 +54,7 @@ while getopts ":ip:ch" opt ; do
;;
c)
DOCKER_IMAGE=${DOCKER_IMAGE_NO_GPU}
- GPU_DEVICE=""
+ GPU_DEVICE=""
;;
:)
echo "ERROR: Option -${OPTARG} requires an argument." 1>&2
@@ -99,7 +99,6 @@ ${DOCKER_COMMAND} run -it \
${GPU_DEVICE} \
--rm \
--ipc=host \
---hostname=$(hostname) \
-v /home/${USER}/:/home/${USER}/ \
-v /mnt/:/mnt/ \
-p 0.0.0.0:${PORT}:${PORT} \
diff --git a/tests/test_models.py b/tests/test_models.py
index 61df950cb..976a6b846 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -7,7 +7,8 @@
from typing import List, Optional, Dict, Tuple, Iterator
from ml4h.TensorMap import TensorMap
-from ml4h.models import make_multimodal_multitask_model, parent_sort, BottleneckType, ACTIVATION_FUNCTIONS, MODEL_EXT, train_model_from_generators, check_no_bottleneck
+from ml4h.models import make_multimodal_multitask_model, parent_sort, BottleneckType, ACTIVATION_FUNCTIONS, MODEL_EXT, train_model_from_generators, \
+ check_no_bottleneck, make_paired_autoencoder_model
from ml4h.test_utils import TMAPS_UP_TO_4D, MULTIMODAL_UP_TO_4D, CATEGORICAL_TMAPS, CONTINUOUS_TMAPS, SEGMENT_IN, SEGMENT_OUT, PARENT_TMAPS, CYCLE_PARENTS
from ml4h.test_utils import LANGUAGE_TMAP_1HOT_WINDOW, LANGUAGE_TMAP_1HOT_SOFTMAX
@@ -18,14 +19,13 @@
'dense_layers': [4, 2],
'dense_blocks': [5, 3],
'block_size': 3,
- 'conv_width': 3,
'learning_rate': 1e-3,
'optimizer': 'adam',
'conv_type': 'conv',
'conv_layers': [6, 5, 3],
- 'conv_x': [3],
- 'conv_y': [3],
- 'conv_z': [2],
+ 'conv_x': [3]*5,
+ 'conv_y': [3]*5,
+ 'conv_z': [2]*5,
'padding': 'same',
'max_pools': [],
'pool_type': 'max',
@@ -39,6 +39,16 @@
'dense_regularize_rate': .1,
'dense_normalize': 'batch_norm',
'bottleneck_type': BottleneckType.FlattenRestructure,
+ 'pair_loss': 'cosine',
+ 'training_steps': 12,
+ 'learning_rate': 0.00001,
+ 'epochs': 6,
+ 'optimizer': 'adam',
+ 'learning_rate_schedule': None,
+ 'model_layers': None,
+ 'model_file': None,
+ 'hidden_layer': 'embed',
+ 'u_connect': {},
}
@@ -54,19 +64,20 @@ def make_training_data(input_tmaps: List[TensorMap], output_tmaps: List[TensorMa
), ])
-def assert_model_trains(input_tmaps: List[TensorMap], output_tmaps: List[TensorMap], m: Optional[tf.keras.Model] = None):
+def assert_model_trains(input_tmaps: List[TensorMap], output_tmaps: List[TensorMap], m: Optional[tf.keras.Model] = None, skip_shape_check: bool = False):
if m is None:
m = make_multimodal_multitask_model(
input_tmaps,
output_tmaps,
**DEFAULT_PARAMS,
)
- for tmap, tensor in zip(input_tmaps, m.inputs):
- assert tensor.shape[1:] == tmap.shape
- assert tensor.shape[1:] == tmap.shape
- for tmap, tensor in zip(parent_sort(output_tmaps), m.outputs):
- assert tensor.shape[1:] == tmap.shape
- assert tensor.shape[1:] == tmap.shape
+ if not skip_shape_check:
+ for tmap, tensor in zip(input_tmaps, m.inputs):
+ assert tensor.shape[1:] == tmap.shape
+ assert tensor.shape[1:] == tmap.shape
+ for tmap, tensor in zip(parent_sort(output_tmaps), m.outputs):
+ assert tensor.shape[1:] == tmap.shape
+ assert tensor.shape[1:] == tmap.shape
data = make_training_data(input_tmaps, output_tmaps)
history = m.fit(data, steps_per_epoch=2, epochs=2, validation_data=data, validation_steps=2)
for tmap in output_tmaps:
@@ -294,8 +305,8 @@ def test_parents(self, output_tmaps):
def test_language_models(self, input_output_tmaps, tmpdir):
params = DEFAULT_PARAMS.copy()
m = make_multimodal_multitask_model(
- input_output_tmaps[0],
- input_output_tmaps[1],
+ tensor_maps_in=input_output_tmaps[0],
+ tensor_maps_out=input_output_tmaps[1],
**params
)
assert_model_trains(input_output_tmaps[0], input_output_tmaps[1], m)
@@ -309,6 +320,36 @@ def test_language_models(self, input_output_tmaps, tmpdir):
**DEFAULT_PARAMS,
)
+ @pytest.mark.parametrize(
+ 'pairs',
+ [
+ [(CONTINUOUS_TMAPS[2], CONTINUOUS_TMAPS[1])],
+ [(CATEGORICAL_TMAPS[2], CATEGORICAL_TMAPS[1])],
+ [(CONTINUOUS_TMAPS[2], CONTINUOUS_TMAPS[1]), (CONTINUOUS_TMAPS[2], CATEGORICAL_TMAPS[3])]
+ ],
+ )
+ def test_paired_models(self, pairs, tmpdir):
+ params = DEFAULT_PARAMS.copy()
+ pair_list = list(set([p[0] for p in pairs] + [p[1] for p in pairs]))
+ params['u_connect'] = {tm: [] for tm in pair_list}
+ m, encoders, decoders = make_paired_autoencoder_model(
+ pairs=pairs,
+ tensor_maps_in=pair_list,
+ tensor_maps_out=pair_list,
+ **params
+ )
+ assert_model_trains(pair_list, pair_list, m, skip_shape_check=True)
+ m.save(os.path.join(tmpdir, 'paired_ae.h5'))
+ path = os.path.join(tmpdir, f'm{MODEL_EXT}')
+ m.save(path)
+ make_paired_autoencoder_model(
+ pairs=pairs,
+ tensor_maps_in=pair_list,
+ tensor_maps_out=pair_list,
+ **params
+ )
+
+
@pytest.mark.parametrize(
'tmaps',
[_rotate(PARENT_TMAPS, i) for i in range(len(PARENT_TMAPS))],
diff --git a/tests/test_recipes.py b/tests/test_recipes.py
index 468dc44c6..df8f4a1e3 100644
--- a/tests/test_recipes.py
+++ b/tests/test_recipes.py
@@ -3,7 +3,7 @@
import pandas as pd
import numpy as np
-from ml4h.recipes import inference_file_name, hidden_inference_file_name
+from ml4h.recipes import inference_file_name, _hidden_file_name
from ml4h.recipes import train_multimodal_multitask, compare_multimodal_multitask_models
from ml4h.recipes import infer_multimodal_multitask, infer_hidden_layer_multimodal_multitask
from ml4h.recipes import compare_multimodal_scalar_task_models, _find_learning_rate
@@ -42,7 +42,7 @@ def test_infer_genetics(self, default_arguments):
def test_infer_hidden(self, default_arguments):
infer_hidden_layer_multimodal_multitask(default_arguments)
- tsv = hidden_inference_file_name(default_arguments.output_folder, default_arguments.id)
+ tsv = _hidden_file_name(default_arguments.output_folder, default_arguments.id)
inferred = pd.read_csv(tsv, sep='\t')
assert len(set(inferred['sample_id'])) == pytest.N_TENSORS
@@ -50,7 +50,7 @@ def test_infer_hidden_genetics(self, default_arguments):
default_arguments.tsv_style = 'genetics'
infer_hidden_layer_multimodal_multitask(default_arguments)
default_arguments.tsv_style = 'standard'
- tsv = hidden_inference_file_name(default_arguments.output_folder, default_arguments.id)
+ tsv = _hidden_file_name(default_arguments.output_folder, default_arguments.id)
inferred = pd.read_csv(tsv, sep='\t')
assert len(set(inferred['FID'])) == pytest.N_TENSORS
From 46fe794081c3d6b42bc20cd7e97ac1ff1d508dad Mon Sep 17 00:00:00 2001
From: Samwell Freeman
Date: Tue, 29 Sep 2020 17:15:39 -0400
Subject: [PATCH 02/21] paired
---
CONTRIBUTING.md | 192 ++++++++++
.../batch_image_annotations.py | 236 ++++++++++++
.../review_results/image_annotations.ipynb | 268 ++++++++++++++
.../image_annotations_demo.ipynb | 259 ++++++++++++++
pylintrc | 337 ++++++++++++++++++
5 files changed, 1292 insertions(+)
create mode 100644 CONTRIBUTING.md
create mode 100644 ml4h/visualization_tools/batch_image_annotations.py
create mode 100644 notebooks/review_results/image_annotations.ipynb
create mode 100644 notebooks/terra_featured_workspace/image_annotations_demo.ipynb
create mode 100644 pylintrc
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..2e2006c07
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,192 @@
+# Contributing
+
+1. Before making a substantial pull request, consider first [filing an issue](https://github.com/broadinstitute/ml/issues) describing the feature addition or change you wish to make.
+1. [Get setup](#setup-for-code-contributions)
+1. [Follow the coding style](#python-coding-style)
+1. [Test your code](#testing)
+1. Send a [pull request](https://github.com/broadinstitute/ml/pulls)
+
+## Setup for code contributions
+
+### Get setup for GitHub
+
+Small typos in code or documentation may be edited directly using the GitHub web interface. Otherwise:
+
+1. If you are new to GitHub, don't start here. Instead, work through a GitHub tutorial such as https://guides.github.com/activities/hello-world/.
+1. Create a fork of https://github.com/broadinstitute/ml
+1. Clone your fork.
+1. Work from a feature branch. See the [Appendix](#appendix) for detailed `git` commands.
+
+### Install precommit
+
+[`pre-commit`](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks.
+
+```
+# Install pre-commit
+pip3 install pre-commit
+# Install the git hook scripts by running this within the git clone directory
+cd ${HOME}/ml
+pre-commit install
+```
+
+See [.pre-commit-config.yaml](https://github.com/broadinstitute/ml/blob/master/.pre-commit-config.yaml) for the currently configured pre-commit hooks for ml4cvd.
+
+### Install git-secrets
+
+```git-secrets``` helps us avoid committing secrets (e.g. private keys) and other critical data (e.g. PHI) to our
+repositories. ```git-secrets``` can be obtained via [github](https://github.com/awslabs/git-secrets) or on MacOS can be
+installed with Homebrew by running ```brew install git-secrets```.
+
+To add hooks to all repositories that you initialize or clone in the future:
+
+```git secrets --install --global```
+
+To add hooks to all local repositories:
+
+```
+git secrets --install ~/.git-templates/git-secrets
+git config --global init.templateDir ~/.git-templates/git-secrets
+```
+
+We maintain our own custom "provider" to cover any private keys or other critical data that we would like to avoid
+committing to our repositories. Feel free to add ```egrep```-compatible regular expressions to
+```git_secrets_provider_ml4cvd.txt``` to match types of critical data that are not currently covered by the patterns in that
+file. To register the patterns in this file with ```git-secrets```:
+
+```
+git secrets --add-provider -- cat ${HOME}/ml/git_secrets_provider_ml4cvd.txt
+```
+
+### Install pylint
+
+[`pylint`](https://www.pylint.org/) is a Python static code analysis tool which looks for programming errors, helps enforcing a coding standard, sniffs for code smells and offers simple refactoring suggestions.
+
+```
+# Install pylint
+pip3 install pylint
+```
+
+See [pylintrc](https://github.com/broadinstitute/ml/blob/master/pylintrc) for the current lint configuration for ml4cvd.
+
+# Python coding style
+
+Changes to ml4cvd should conform to [PEP 8 -- Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/). See also [Google Python Style Guide](https://github.com/google/styleguide/blob/gh-pages/pyguide.md) as another decription of this coding style.
+
+Use `pylint` to check your Python changes:
+
+```bash
+pylint --rcfile=${HOME}/ml/pylintrc myfile.py
+```
+
+Any messages returned by `pylint` are intended to be self-explanatory, but that isn't always the case.
+
+* Search for `pylint ` or `pylint ` for more details on the recommended code change to resolve the lint issue.
+* Or add comment `# pylint: disable=` to the end of the line of code.
+
+# Testing
+
+## Testing of `recipes`
+
+Unit tests can be run in Docker with
+```
+${HOME}/ml/scripts/tf.sh -T ${HOME}/ml/tests
+```
+Unit tests can be run locally in a conda environment with
+```
+python -m pytest ${HOME}/ml/tests
+```
+Some of the unit tests are slow due to creating, saving and loading `tensorflow` models.
+To skip those tests to move quickly, run
+```
+python -m pytest ${HOME}/ml/tests -m "not slow"
+```
+pytest can also run specific tests using `::`. For example
+
+```
+python -m pytest ${HOME}/ml/tests/test_models.py::TestMakeMultimodalMultitaskModel::test_u_connect_segment
+```
+
+For more pytest usage information, checkout the [usage guide](https://docs.pytest.org/en/latest/usage.html).
+
+## Testing of `visualization_tools`
+
+The code in [ml4cvd/visualization_tools](https://github.com/broadinstitute/ml/tree/master/ml4cvd/visualization_tools) is primarily interactive so we add test cases to notebook [test_error_handling_for_notebook_visualizations.ipynb](https://github.com/broadinstitute/ml/blob/master/notebooks/review_results/test_error_handling_for_notebook_visualizations.ipynb) and visually inspect the output of `Cells -> Run all`.
+
+# Appendix
+
+For the ml4cvd GitHub repository, we are doing ‘merge and squash’ of pull requests. So that means your fork does not match upstream after your pull request has been merged. The easiest way to manage this is to always work in a feature branch, instead of checking changes into your fork’s master branch.
+
+
+## How to work on a new feature
+
+(1) Get the latest version of the upstream repo
+
+```
+git fetch upstream
+```
+
+Note: If you get an error saying that upstream is unknown, run the following remote add command and then re-run the fetch command. You only need to do this once per git clone.
+
+```
+git remote add upstream https://github.com/broadinstitute/ml.git
+```
+
+(2) Make sure your master branch is “even” with upstream.
+
+```
+git checkout master
+git merge --ff-only upstream/master
+git push
+```
+
+Now the master branch of your fork on GitHub should say *"This branch is even with broadinstitute:master."*.
+
+
+(3) Create a feature branch for your change.
+
+```
+git checkout -b my-feature-branch-name
+```
+
+Because you created this feature branch from your master branch that was up to date with upstream (step 2), your feature branch is also up to date with upstream. Commit your changes to this branch until you are happy with them.
+
+(4) Push your changes to GitHub and send a pull request.
+
+```
+git push --set-upstream origin my-feature-branch-name
+```
+
+After your pull request is merged, its safe to delete your branch!
+
+## I accidentally checked a new change to my master branch instead of a feature branch. How to fix this?
+
+(1) Soft undo your change(s). This leaves the changes in the files on disk but undoes the commit.
+
+```
+git checkout master
+# Moves pointer back to previous HEAD
+git reset --soft HEAD@{1}
+```
+
+Or if you need to move back several commits to the most recent one in common with upstream, you can change ‘1’ to be however many commits back you need to go.
+
+(2) “stash” your now-unchecked-in changes so that you can get them back later.
+
+```
+git stash
+```
+
+(3) Now do the [How to work on a new feature](#how-to-work-on-a-new-feature) step to bring master up to date and create your new feature branch that is “even” with upstream. Here are those commands again:
+
+```
+git fetch upstream
+git merge --ff-only upstream/master
+git checkout -b my-feature-branch-name
+```
+
+(4) “unstash” your changes.
+
+```
+git stash pop
+```
+Now you can proceed with your work!
diff --git a/ml4h/visualization_tools/batch_image_annotations.py b/ml4h/visualization_tools/batch_image_annotations.py
new file mode 100644
index 000000000..34ff731df
--- /dev/null
+++ b/ml4h/visualization_tools/batch_image_annotations.py
@@ -0,0 +1,236 @@
+"""Methods for batch annotations of images stored as 3D tensors, such as MRIs, from within notebooks."""
+
+import json
+import os
+import socket
+import tempfile
+from typing import Any, Dict, List
+
+from IPython.display import display
+import numpy as np
+import pandas as pd
+import h5py
+from ipyannotations import PolygonAnnotator
+import ipywidgets as widgets
+from ml4h.visualization_tools.hd5_mri_plots import MRI_TMAPS
+from ml4h.visualization_tools.annotation_storage import AnnotationStorage
+from ml4h.visualization_tools.annotation_storage import TransientAnnotationStorage
+from PIL import Image
+import tensorflow as tf
+
+
+class BatchImageAnnotator():
+ """Annotate batches of images with polygons drawn over regions of interest."""
+
+ SUBMIT_BUTTON_DESCRIPTION = 'Submit polygons, goto next sample'
+ USE_INSTRUCTIONS = '''
+
+
To draw a polygon, click anywhere you'd like to start. Continue to click
+ along the edge of the polygon until arrive back where you started. To
+ finish, simply click the first point (highlighted in red). It may be
+ helpful to increase the point size if you're struggling (using the slider).
+
+
You can change the class of a polygon using the dropdown menu while the
+ polygon is still "open", or unfinished. If you make a mistake, use the Undo
+ button until the point that's wrong has disappeared.
+
+
You can move, but not add / subtract polygon points, by clicking the "Edit"
+ button. Simply drag a point you want to adjust. Again, if you have
+ difficulty aiming at the points, you can increase the point size.
+
+
You can increase or decrease the contrast and brightness of the image
+ using the sliders to make it easier to annotate. Sometimes you need to see
+ what's behind already-created annotations, and for this purpose you can
+ make them more see-through using the "Opacity" slider.
+
+ '''
+ EXPECTED_COLUMN_NAMES = ['sample_id', 'tmap_name', 'instance_number', 'folder']
+ DEFAULT_ANNOTATION_CLASSNAME = 'region_of_interest'
+ CSS = '''
+
+ '''
+
+ def __init__(
+ self, samples: pd.DataFrame, annotation_categories: List[str] = None,
+ zoom: float = 1.5, annotation_storage: AnnotationStorage = TransientAnnotationStorage(),
+ ):
+ """Initializes an instance of BatchImageAnnotator.
+
+ Args:
+ samples: A dataframe of samples to annotate. Columns must include those
+ in BatchImageAnnotator.EXPECTED_COLUMN_NAMES.
+ annotation_categories: A list of one or more strings to serve as tags for the polygons.
+ zoom: Desired zoom level for the image.
+ annotation_storage: An instance of AnnotationStorage. This faciltates the use of a user-provided
+ strategy for the storage and processing of annotations.
+
+ Raises:
+ ValueError: The provided dataframe does not contain the expected columns.
+ """
+ if not set(self.EXPECTED_COLUMN_NAMES).issubset(samples.columns):
+ raise ValueError(f'samples Dataframe must contain columns {self.EXPECTED_COLUMN_NAMES}')
+ self.samples = samples
+ self.current_sample = 0
+ # TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/11
+ self.zoom = zoom
+ self.annotation_storage = annotation_storage
+ if annotation_categories is None:
+ annotation_categories = [self.DEFAULT_ANNOTATION_CLASSNAME]
+
+ self.annotation_widget = PolygonAnnotator(
+ options=annotation_categories,
+ canvas_size=(900, 280 * self.zoom),
+ )
+ self.annotation_widget.on_submit(self._store_annotations)
+ self.annotation_widget.submit_button.description = self.SUBMIT_BUTTON_DESCRIPTION
+ self.annotation_widget.submit_button.layout = widgets.Layout(width='300px')
+
+ self.title_widget = widgets.HTML('')
+ self.results_widget = widgets.HTML('')
+
+ def _store_annotations(self, data: Dict[Any, Any]) -> None:
+ """Transfer widget state to the annotation storage and advance to the next sample."""
+ if self.current_sample >= self.samples.shape[0]:
+ self.results_widget.value = '
Annotation batch complete!
Thank you for making the model better.'
+ return
+
+ # Convert polygon points in canvas coordinates to tensor coordinates.
+ image_canvas_position = self.annotation_widget.canvas.image_extent
+ x_offset, y_offset, _, _ = image_canvas_position
+ tensor_coords = [
+ (
+ a['label'],
+ [(
+ int((p[0] - x_offset) / self.zoom),
+ int((p[1] - y_offset) / self.zoom),
+ ) for p in a['points']],
+ ) for a in data
+ ]
+ # Store the annotation using the provided annotation storage strategy.
+ self.annotation_storage.submit_annotation(
+ sample_id=self.samples.loc[self.current_sample, 'sample_id'],
+ annotator=os.getenv('OWNER_EMAIL') if os.getenv('OWNER_EMAIL') else socket.gethostname(),
+ key=self.samples.loc[self.current_sample, 'tmap_name'],
+ value_numeric=self.samples.loc[self.current_sample, 'instance_number'],
+ value_string=self.samples.loc[self.current_sample, 'folder'],
+ comment=json.dumps(tensor_coords),
+ )
+
+ # Display this annotation at the bottom of the widget.
+ results = f'''
+
+
Prior sample's submitted annotations
+ The {self.SUBMIT_BUTTON_DESCRIPTION} button is both printing out the polygons below and storing the polygons
+ via strategy {self.annotation_storage.__class__.__name__}.
+ Details: {self.annotation_storage.describe()}
+
sample info
+ {self._format_info_for_current_sample()}
+
canvas coordinates
+ image extent {image_canvas_position}
+ {[f'
{json.dumps(x)}
' for x in data]}
+
source tensor coordinates
+ {[f'
{json.dumps(x)}
' for x in tensor_coords]}
+
+ '''
+ self.results_widget.value = results
+
+ # Advance to the next sample.
+ self.current_sample += 1
+ self._annotate_image_for_current_sample()
+
+ def _format_info_for_current_sample(self) -> str:
+ """Convert information about the current sample to an HTML table for display within the widget."""
+ headings = ' '.join([f'
+ '''
+
+ def _annotate_image_for_current_sample(self) -> None:
+ """Retrieve the data for the current sample and display its image in the annotation widget.
+
+ If all samples have been processed, display the completion message.
+ """
+ if self.current_sample >= self.samples.shape[0]:
+ self.annotation_widget.canvas.clear()
+ # Note: the above command clears the canvas, but any incomplete polygons will be redrawn. Call this
+ # private method to clear those. TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/15
+ self.annotation_widget.canvas._init_empty_data() # pylint: disable=protected-access
+ self.title_widget.value = '
Annotation batch complete!
Thank you for making the model better.'
+ return
+
+ sample_id = self.samples.loc[self.current_sample, 'sample_id']
+ tmap_name = self.samples.loc[self.current_sample, 'tmap_name']
+ instance_number = self.samples.loc[self.current_sample, 'instance_number']
+ folder = self.samples.loc[self.current_sample, 'folder']
+
+ with tempfile.TemporaryDirectory() as tmpdirname:
+ sample_hd5 = str(sample_id) + '.hd5'
+ local_path = os.path.join(tmpdirname, sample_hd5)
+ try:
+ tf.io.gfile.copy(src=os.path.join(folder, sample_hd5), dst=local_path)
+ hd5 = h5py.File(local_path, mode='r')
+ except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
+ self.annotation_widget.canvas.clear()
+ # Note: the above command clears the canvas, but any incomplete polygons will be redrawn. Call this
+ # private method to clear those. TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/15
+ self.annotation_widget.canvas._init_empty_data() # pylint: disable=protected-access
+ self.title_widget.value = f'''
+
+
Warning: MRI HD5 file not available for sample {sample_id} in folder {folder}
+ Use the folder parameter to read HD5s from a different local directory or Cloud Storage bucket.
+
{e.message}
+
+ '''
+ return
+
+ tensor = MRI_TMAPS[tmap_name].tensor_from_file(MRI_TMAPS[tmap_name], hd5)
+ tensor_instance = tensor[:, :, instance_number]
+ if self.zoom > 1.0:
+ # TODO(deflaux) remove this after https://github.com/janfreyberg/ipyannotations/issues/11
+ img = Image.fromarray(tensor_instance)
+ zoomed_img = img.resize([int(self.zoom * s) for s in img.size], Image.LANCZOS)
+ tensor_instance = np.asarray(zoomed_img)
+
+ self.annotation_widget.display(tensor_instance)
+ self.title_widget.value = f'''
+ {self.CSS}
+
+
Batch annotation of {self.samples.shape[0]} samples
+ {self.USE_INSTRUCTIONS}
+
+
Current sample
+ {self._format_info_for_current_sample()}
+
+ '''
+
+ def annotate_images(self) -> None:
+ """Begin the batch annotation task by displaying the annotation widget populated with the first sample.
+
+ The submit button is used to proceed to the next sample until all samples have been processed.
+ """
+ self._annotate_image_for_current_sample()
+ display(widgets.VBox([self.title_widget, self.annotation_widget, self.results_widget]))
+
+ def view_recent_submissions(self, count: int = 10) -> pd.DataFrame:
+ """View a dataframe of up to [count] most recent submissions.
+
+ Args:
+ count: The number of the most recent submissions to return.
+
+ Returns:
+ A dataframe of the most recent annotations.
+ """
+ return self.annotation_storage.view_recent_submissions(count=count)
diff --git a/notebooks/review_results/image_annotations.ipynb b/notebooks/review_results/image_annotations.ipynb
new file mode 100644
index 000000000..6e644f15a
--- /dev/null
+++ b/notebooks/review_results/image_annotations.ipynb
@@ -0,0 +1,268 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Image annotations for a batch of samples\n",
+ "\n",
+ "Using this notebook, cardiologists are able to quickly view and annotate MRI images for a batch of samples. These annotated images become the training data for the next round of modeling."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Setup\n",
+ "\n",
+ "
\n",
+ " This notebook assumes\n",
+ "
\n",
+ "
Terra is running custom Docker image gcr.io/uk-biobank-sek-data/ml4h_terra:20200918_091608.
\n",
+ "
ml4h is running custom Docker image gcr.io/broad-ml4cvd/deeplearning:tf2-latest-gpu.
\n",
+ " Edit the CSV file path below, if needed, to either a local file or one in Cloud Storage.\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "#---[ EDIT AND RUN THIS CELL TO READ FROM A LOCAL FILE OR A FILE IN CLOUD STORAGE ]---\n",
+ "SAMPLE_BATCH_FILE = None"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if SAMPLE_BATCH_FILE:\n",
+ " samples_df = pd.read_csv(tf.io.gfile.GFile(SAMPLE_BATCH_FILE))\n",
+ "\n",
+ "else:\n",
+ " # Normally these would all be the same or similar TMAP. We are using different ones here just to make it\n",
+ " # more obvious in this demo that we are processing different samples.\n",
+ " samples_df = pd.DataFrame(\n",
+ " columns=BatchImageAnnotator.EXPECTED_COLUMN_NAMES,\n",
+ " data=[\n",
+ " [1655349, 'cine_lax_3ch_192', 25, 'gs://ml4cvd/deflaux/ukbb_tensors/'],\n",
+ " [1655349, 't2_flair_sag_p2_1mm_fs_ellip_pf78_1', 50, 'gs://ml4cvd/deflaux/ukbb_tensors/'],\n",
+ " [1655349, 'cine_lax_4ch_192', 25, 'gs://ml4cvd/deflaux/ukbb_tensors/'],\n",
+ " [1655349, 't2_flair_sag_p2_1mm_fs_ellip_pf78_2', 50, 'gs://ml4cvd/deflaux/ukbb_tensors/'],\n",
+ " [2403657, 'cine_lax_3ch_192', 25, 'gs://ml4cvd/deflaux/ukbb_tensors/'],\n",
+ " ])\n",
+ "\n",
+ "samples_df.shape"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "samples_df.head(n = 10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Annotate the batch! "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Note: a zoom level of 1.0 displays the tensor as-is. For higher zoom levels, this code currently\n",
+ "# use the PIL library to scale the image.\n",
+ "\n",
+ "annotator = BatchImageAnnotator(samples=samples_df,\n",
+ " zoom=2.0,\n",
+ " annotation_categories=['region_of_interest'],\n",
+ " annotation_storage=BIG_QUERY_ANNOTATIONS_STORAGE)\n",
+ "annotator.annotate_images()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# View the stored annotations "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "annotator.view_recent_submissions(count=10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Provenance"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import datetime\n",
+ "print(datetime.datetime.now())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ "pip3 freeze"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Questions about these particular notebooks? Reach out to Puneet Batra pbatra@broadinstitute.org, Paolo Di Achille pdiachil@broadinstitute.org, and Nicole Deflaux deflaux@verily.com."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.8"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "199px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/notebooks/terra_featured_workspace/image_annotations_demo.ipynb b/notebooks/terra_featured_workspace/image_annotations_demo.ipynb
new file mode 100644
index 000000000..ce15e4d73
--- /dev/null
+++ b/notebooks/terra_featured_workspace/image_annotations_demo.ipynb
@@ -0,0 +1,259 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Image annotations for a batch of samples\n",
+ "\n",
+ "Using this notebook, cardiologists are able to quickly view and annotate MRI images for a batch of samples. These annotated images become the training data for the next round of modeling."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Setup\n",
+ "\n",
+ "
\n",
+ " This notebook assumes\n",
+ "
\n",
+ "
Terra is running custom Docker image gcr.io/uk-biobank-sek-data/ml4h_terra:20200918_091608.
\n",
+ "
ml4cvd is running custom Docker image gcr.io/broad-ml4cvd/deeplearning:tf2-latest-gpu.
\n",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "attachments": {
+ "Screen%20Shot%202020-06-22%20at%202.50.48%20PM.png": {
+ "image/png": "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"
+ }
+ },
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "![Screen%20Shot%202020-06-22%20at%202.50.48%20PM.png](attachment:Screen%20Shot%202020-06-22%20at%202.50.48%20PM.png)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from ml4cvd.visualization_tools.batch_image_annotations import BatchImageAnnotator\n",
+ "import pandas as pd"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "code_folding": []
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "// Display cell outputs to full height (no vertical scroll bar)\n",
+ "IPython.OutputArea.auto_scroll_threshold = 9999;"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "pd.set_option('display.max_colwidth', -1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Define the batch of samples to annotate\n",
+ "\n",
+ "In general, we would read in a CSV file but for this demo we define the batch right here."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Normally these would all be the same or similar TMAP. We are using different ones here just to make it\n",
+ "# more obvious in this demo that we are processing different samples.\n",
+ "samples_df = pd.DataFrame(\n",
+ " columns=BatchImageAnnotator.EXPECTED_COLUMN_NAMES,\n",
+ " data=[\n",
+ " ['fake_1', 'cine_lax_3ch_192', 25, 'gs://ml4cvd/projects/fake_hd5s/'],\n",
+ " ['fake_1', 't2_flair_sag_p2_1mm_fs_ellip_pf78_1', 50, 'gs://ml4cvd/projects/fake_hd5s/'],\n",
+ " ['fake_1', 'cine_lax_4ch_192', 25, 'gs://ml4cvd/projects/fake_hd5s/'],\n",
+ " ['fake_1', 't2_flair_sag_p2_1mm_fs_ellip_pf78_2', 50, 'gs://ml4cvd/projects/fake_hd5s/'],\n",
+ " ['fake_2', 'cine_lax_3ch_192', 25, 'gs://ml4cvd/projects/fake_hd5s/'],\n",
+ " ])\n",
+ "\n",
+ "samples_df"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Annotate the batch! "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Note: a zoom level of 1.0 displays the tensor as-is. For higher zoom levels, this code currently\n",
+ "# use the PIL library to scale the image.\n",
+ "\n",
+ "annotator = BatchImageAnnotator(samples=samples_df, zoom=2.0, annotation_categories=['region_of_interest'])\n",
+ "annotator.annotate_images()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## via BigQuery annotation storage "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from ml4cvd.visualization_tools.annotation_storage import BigQueryAnnotationStorage"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "BIG_QUERY_ANNOTATIONS_STORAGE = BigQueryAnnotationStorage('uk-biobank-sek-data.ml_results.annotations')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Note: a zoom level of 1.0 displays the tensor as-is. For higher zoom levels, this code currently\n",
+ "# use the PIL library to scale the image.\n",
+ "\n",
+ "annotator = BatchImageAnnotator(samples=samples_df,\n",
+ " zoom=2.0,\n",
+ " annotation_categories=['region_of_interest'],\n",
+ " annotation_storage=BIG_QUERY_ANNOTATIONS_STORAGE)\n",
+ "annotator.annotate_images()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# View the stored annotations "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "annotator.view_recent_submissions(count=10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Provenance"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import datetime\n",
+ "print(datetime.datetime.now())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ "pip3 freeze"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Questions about these particular notebooks? Reach out to Puneet Batra pbatra@broadinstitute.org, Paolo Di Achille pdiachil@broadinstitute.org, and Nicole Deflaux deflaux@verily.com."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.8"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "199px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 000000000..8a5e40122
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,337 @@
+# This configuration was copied from https://github.com/tensorflow/tensorflow/blob/18ebe824d2f6f20b09839cb0a0073032a2d6c5fe/tensorflow/tools/ci_build/pylintrc and then further modified.
+
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add files or directories to the denylist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time. See also the "--disable" option for examples.
+enable=indexing-exception,old-raise-syntax
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,no-member,no-name-in-module,import-error,unsubscriptable-object,unbalanced-tuple-unpacking,undefined-variable,not-context-manager
+
+
+# Set the cache size for astng objects.
+cache-size=500
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=no
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent
+
+# List of decorators that create context managers from functions, such as
+# contextlib.contextmanager.
+contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=apply,input,reduce
+
+
+# Disable the report(s) with the given id(s).
+# All non-Google reports are disabled by default.
+disable-report=R0001,R0002,R0003,R0004,R0101,R0102,R0201,R0202,R0220,R0401,R0402,R0701,R0801,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,R0921,R0922,R0923
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression which should only match correct class names
+class-rgx=^_?[A-Z][a-zA-Z0-9]*$
+
+# Regular expression which should only match correct function names
+function-rgx=^(?:(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$
+
+# Regular expression which should only match correct method names
+method-rgx=^(?:(?P__[a-z0-9_]+__|next)|(?P_{0,2}[A-Z][a-zA-Z0-9]*)|(?P_{0,2}[a-z][a-z0-9_]*))$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
+
+# Regular expression which should only match correct argument names
+argument-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression which should only match correct variable names
+variable-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression which should only match correct attribute names in class
+# bodies
+class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=^[a-z][a-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=main,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=(__.*__|main)
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=10
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=(?x)
+ (^\s*(import|from)\s
+ |\$Id:\s\/\/depot\/.+#\d+\s\$
+ |^[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*("[^"]\S+"|'[^']\S+')
+ |^\s*\#\ LINT\.ThenChange
+ |^[^#]*\#\ type:\ [a-zA-Z_][a-zA-Z0-9_.,[\] ]*$
+ |pylint
+ |"""
+ |\#
+ |lambda
+ |(https?|ftp):)
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=y
+
+# List of optional constructs for which whitespace checking is disabled
+no-space-check=
+
+# Maximum number of lines in a module
+max-module-lines=99999
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec,sets
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls,class_
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception,StandardError,BaseException
+
+
+[AST]
+
+# Maximum line length for lambdas
+short-func-length=1
+
+# List of module members that should be marked as deprecated.
+# All of the string functions are listed in 4.1.4 Deprecated string functions
+# in the Python 2.4 docs.
+deprecated-members=string.atof,string.atoi,string.atol,string.capitalize,string.expandtabs,string.find,string.rfind,string.index,string.rindex,string.count,string.lower,string.split,string.rsplit,string.splitfields,string.join,string.joinfields,string.lstrip,string.rstrip,string.strip,string.swapcase,string.translate,string.upper,string.ljust,string.rjust,string.center,string.zfill,string.replace,sys.exitfunc
+
+
+[DOCSTRING]
+
+# List of exceptions that do not need to be mentioned in the Raises section of
+# a docstring.
+ignore-exceptions=AssertionError,NotImplementedError,StopIteration,TypeError
+
+
+
+[TOKENS]
+
+# Number of spaces of indent required when the last token on the preceding line
+# is an open (, [, or {.
+indent-after-paren=4
+
+
+[GOOGLE LINES]
+
+# Regexp for a proper copyright notice.
+copyright=Copyright \d{4} The TensorFlow Authors\. +All [Rr]ights [Rr]eserved\.
From 837d019b5090b87827c804e6184ebf5149ca6709 Mon Sep 17 00:00:00 2001
From: Samwell Freeman
Date: Tue, 29 Sep 2020 17:26:25 -0400
Subject: [PATCH 03/21] paired
---
README.md | 32 +---
docker/terra_image/Dockerfile | 8 +-
docker/terra_image/README.md | 8 +-
docker/vm_boot_images/Dockerfile | 5 +-
.../config/tensorflow-requirements.txt | 2 +
ml4h/plots.py | 36 ++--
ml4h/tensorize/tensor_writer_ukbb.py | 181 +++++++++++-------
7 files changed, 154 insertions(+), 118 deletions(-)
diff --git a/README.md b/README.md
index 0335a4885..8996bfe39 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# ml4h
`ml4h` is a project aimed at using machine learning to model multi-modal cardiovascular
time series and imaging data. `ml4h` began as a set of tools to make it easy to work
-with the UK Biobank on the Google Cloud and has since expanded to include other data sources
+with the UK Biobank on Google Cloud Platform and has since expanded to include other data sources
and functionality.
@@ -9,6 +9,7 @@ Getting Started
* [Setting up your local environment](#setting-up-your-local-environment)
* [Setting up a remote VM](#setting-up-a-remote-vm)
* Modeling/Data Sources/Tests [(`ml4h/DATA_MODELING_TESTS.md`)](ml4h/DATA_MODELING_TESTS.md)
+* [Contributing Code](#contributing-code)
Advanced Topics:
* Tensorizing Data (going from raw data to arrays suitable for modeling, in `ml4h/tensorize/README.md, TENSORIZE.md` )
@@ -19,7 +20,7 @@ Clone the repo
```
git clone git@github.com:broadinstitute/ml.git
```
-Make sure you have installed the [google cloud tools (gcloud)](https://cloud.google.com/storage/docs/gsutil_install). With [Homebrew](https://brew.sh/), you can use
+Make sure you have installed the [Google Cloud SDK (gcloud)](https://cloud.google.com/sdk/docs/downloads-interactive). With [Homebrew](https://brew.sh/), you can use
```
brew cask install google-cloud-sdk
```
@@ -145,29 +146,6 @@ If you get a public key error run: `gcloud compute config-ssh`
Now open a browser on your laptop and go to the URL `http://localhost:8888`
+## Contributing code
-### Installing git-secrets
-
-```git-secrets``` helps us avoid committing secrets (e.g. private keys) and other critical data (e.g. PHI) to our
-repositories. ```git-secrets``` can be obtained via [github](https://github.com/awslabs/git-secrets) or on MacOS can be
-installed with Homebrew by running ```brew install git-secrets```.
-
-To add hooks to all repositories that you initialize or clone in the future:
-
-```git secrets --install --global```
-
-To add hooks to all local repositories:
-
-```
-git secrets --install ~/.git-templates/git-secrets
-git config --global init.templateDir ~/.git-templates/git-secrets
-```
-
-We maintain our own custom "provider" to cover any private keys or other critical data that we would like to avoid
-committing to our repositories. Feel free to add ```egrep```-compatible regular expressions to
-```git_secrets_provider_ml4h.txt``` to match types of critical data that are not currently covered by the patterns in that
-file. To register the patterns in this file with ```git-secrets```:
-
-```
-git secrets --add-provider -- cat ${HOME}/ml/git_secrets_provider_ml4h.txt
-```
+Want to contribute code to this project? Please see [CONTRIBUTING](./CONTRIBUTING.md) for developer setup and other details.
diff --git a/docker/terra_image/Dockerfile b/docker/terra_image/Dockerfile
index 721f94500..a59ecd6ae 100644
--- a/docker/terra_image/Dockerfile
+++ b/docker/terra_image/Dockerfile
@@ -1,4 +1,4 @@
-FROM us.gcr.io/broad-dsp-gcr-public/terra-jupyter-gatk:1.0.0
+FROM us.gcr.io/broad-dsp-gcr-public/terra-jupyter-gatk:1.0.6
# https://github.com/DataBiosphere/terra-docker/blob/master/terra-jupyter-gatk/CHANGELOG.md
USER root
@@ -19,6 +19,10 @@ RUN pip3 install --user -r $HOME/ml4h_pkg/config/tensorflow-requirements.txt \
# first few rows of the downloaded dataframe of query results.
# Pin version due to https://github.com/googleapis/google-cloud-python/issues/9965
&& pip3 install --upgrade --user google-cloud-bigquery[pandas]==1.22.0 \
+ # Upgrade to a newer version. The one on the base Terra image was a bit too old.
+ && pip3 install --upgrade --user numpy \
# Configure notebook extensions.
&& jupyter nbextension install --user --py vega \
- && jupyter nbextension enable --user --py vega
+ && jupyter nbextension enable --user --py vega \
+ && jupyter nbextension install --user --py ipycanvas \
+ && jupyter nbextension enable --user --py ipycanvas
diff --git a/docker/terra_image/README.md b/docker/terra_image/README.md
index 71284c0bd..9a81dc74a 100644
--- a/docker/terra_image/README.md
+++ b/docker/terra_image/README.md
@@ -2,13 +2,13 @@
To build and push:
```
-mv ml4cvd ml4cvdBAK_$(date +"%Y%m%d_%H%M%S") \
+mv ml4h ml4hBAK_$(date +"%Y%m%d_%H%M%S") \
&& mv config configBAK_$(date +"%Y%m%d_%H%M%S") \
- && cp -r ../../ml4cvd . \
+ && cp -r ../../ml4h . \
&& cp -r ../vm_boot_images/config . \
&& gcloud --project uk-biobank-sek-data builds submit \
--timeout 20m \
- --tag gcr.io/uk-biobank-sek-data/ml4cvd_terra:`date +"%Y%m%d_%H%M%S"` .
+ --tag gcr.io/uk-biobank-sek-data/ml4h_terra:`date +"%Y%m%d_%H%M%S"` .
```
Notes:
@@ -20,5 +20,5 @@ available to docker.
cd notebooks
find . -name "*.ipynb" -type f -print0 | \
xargs -0 perl -i -pe \
- 's/gcr.io\/uk-biobank-sek-data\/ml4cvd_terra:\d{8}_\d{6}/gcr.io\/uk-biobank-sek-data\/ml4cvd_terra:20200623_145127/g'
+ 's/gcr.io\/uk-biobank-sek-data\/ml4h_terra:\d{8}_\d{6}/gcr.io\/uk-biobank-sek-data\/ml4h_terra:20200623_145127/g'
```
diff --git a/docker/vm_boot_images/Dockerfile b/docker/vm_boot_images/Dockerfile
index a62694ca0..59e5b32be 100644
--- a/docker/vm_boot_images/Dockerfile
+++ b/docker/vm_boot_images/Dockerfile
@@ -34,4 +34,7 @@ RUN apt-get install python3-tk libgl1-mesa-glx libxt-dev -y
# Requirements for the tensorflow project
RUN pip3 install --upgrade pip
RUN pip3 install -r pre_requirements.txt
-RUN pip3 install -r tensorflow-requirements.txt
+RUN pip3 install -r tensorflow-requirements.txt \
+ # Configure notebook extensions.
+ && jupyter nbextension install --user --py ipycanvas \
+ && jupyter nbextension enable --user --py ipycanvas
diff --git a/docker/vm_boot_images/config/tensorflow-requirements.txt b/docker/vm_boot_images/config/tensorflow-requirements.txt
index bb6a1e777..d782967af 100644
--- a/docker/vm_boot_images/config/tensorflow-requirements.txt
+++ b/docker/vm_boot_images/config/tensorflow-requirements.txt
@@ -28,3 +28,5 @@ altair
facets-overview
plotnine
vega
+ipycanvas==0.4.1
+ipyannotations==0.2.0
diff --git a/ml4h/plots.py b/ml4h/plots.py
index c60282803..6b5ad6a78 100755
--- a/ml4h/plots.py
+++ b/ml4h/plots.py
@@ -40,6 +40,9 @@
from scipy.ndimage.filters import gaussian_filter
from scipy import stats
+import ml4h.tensormap.ukb.ecg
+import ml4h.tensormap.mgb.ecg
+from ml4h.tensormap.mgb.dynamic import make_waveform_maps
from ml4h.TensorMap import TensorMap
from ml4h.metrics import concordance_index, coefficient_of_determination
from ml4h.defines import IMAGE_EXT, JOIN_CHAR, PDF_EXT, TENSOR_EXT, ECG_REST_LEADS, ECG_REST_MEDIAN_LEADS, PARTNERS_DATETIME_FORMAT, PARTNERS_DATE_FORMAT, HD5_GROUP_CHAR
@@ -1227,16 +1230,15 @@ def _plot_partners_figure(
def plot_partners_ecgs(args):
plot_tensors = [
- 'partners_ecg_patientid', 'partners_ecg_firstname', 'partners_ecg_lastname',
- 'partners_ecg_sex', 'partners_ecg_dob', 'partners_ecg_age',
- 'partners_ecg_datetime', 'partners_ecg_sitename', 'partners_ecg_location',
- 'partners_ecg_read_md', 'partners_ecg_taxis_md', 'partners_ecg_rate_md',
- 'partners_ecg_pr_md', 'partners_ecg_qrs_md', 'partners_ecg_qt_md',
- 'partners_ecg_paxis_md', 'partners_ecg_raxis_md', 'partners_ecg_qtc_md',
+ ml4h.tensormap.mgb.ecg.partners_ecg_patientid, ml4h.tensormap.mgb.ecg.partners_ecg_firstname, ml4h.tensormap.mgb.ecg.partners_ecg_lastname,
+ ml4h.tensormap.mgb.ecg.partners_ecg_sex, ml4h.tensormap.mgb.ecg.partners_ecg_dob, ml4h.tensormap.mgb.ecg.partners_ecg_age,
+ ml4h.tensormap.mgb.ecg.partners_ecg_datetime, ml4h.tensormap.mgb.ecg.partners_ecg_sitename, ml4h.tensormap.mgb.ecg.partners_ecg_location,
+ ml4h.tensormap.mgb.ecg.partners_ecg_read_md, ml4h.tensormap.mgb.ecg.partners_ecg_taxis_md, ml4h.tensormap.mgb.ecg.partners_ecg_rate_md,
+ ml4h.tensormap.mgb.ecg.partners_ecg_pr_md, ml4h.tensormap.mgb.ecg.partners_ecg_qrs_md, ml4h.tensormap.mgb.ecg.partners_ecg_qt_md,
+ ml4h.tensormap.mgb.ecg.partners_ecg_paxis_md, ml4h.tensormap.mgb.ecg.partners_ecg_raxis_md, ml4h.tensormap.mgb.ecg.partners_ecg_qtc_md,
]
- voltage_tensor = 'partners_ecg_2500_raw'
- from ml4h.tensor_maps_partners_ecg_labels import TMAPS
- tensor_maps_in = [TMAPS[it] for it in plot_tensors + [voltage_tensor]]
+ voltage_tensor = make_waveform_maps('partners_ecg_2500_raw')
+ tensor_maps_in = plot_tensors + [voltage_tensor]
tensor_paths = [os.path.join(args.tensors, tp) for tp in os.listdir(args.tensors) if os.path.splitext(tp)[-1].lower()==TENSOR_EXT]
if 'clinical' == args.plot_mode:
@@ -1503,13 +1505,13 @@ def plot_ecg_rest(
:param is_blind: if True, the plot gets blinded (helpful for review and annotation)
"""
map_fields_to_tmaps = {
- 'ramp': 'ecg_rest_ramplitude_raw',
- 'samp': 'ecg_rest_samplitude_raw',
- 'aVL': 'ecg_rest_lvh_avl',
- 'Sokolow_Lyon': 'ecg_rest_lvh_sokolow_lyon',
- 'Cornell': 'ecg_rest_lvh_cornell',
- }
- from ml4h.tensor_from_file import TMAPS
+ 'ramp': ml4h.tensormap.ukb.ecg.ecg_rest_ramplitude_raw,
+ 'samp': ml4h.tensormap.ukb.ecg.ecg_rest_samplitude_raw,
+ 'aVL': ml4h.tensormap.ukb.ecg.ecg_rest_lvh_avl,
+ 'Sokolow_Lyon': ml4h.tensormap.ukb.ecg.ecg_rest_lvh_sokolow_lyon,
+ 'Cornell': ml4h.tensormap.ukb.ecg.ecg_rest_lvh_cornell,
+ }
+
raw_scale = 0.005 # Conversion from raw to mV
default_yrange = ECG_REST_PLOT_DEFAULT_YRANGE # mV
time_interval = 2.5 # time-interval per plot in seconds. ts_Reference data is in s, voltage measurement is 5 uv per lsb
@@ -1521,7 +1523,7 @@ def plot_ecg_rest(
with h5py.File(tensor_path, 'r') as hd5:
traces, text = _ecg_rest_traces_and_text(hd5)
for field in map_fields_to_tmaps:
- tm = TMAPS[map_fields_to_tmaps[field]]
+ tm = map_fields_to_tmaps[field]
patient_dic[field] = np.zeros(tm.shape)
try:
patient_dic[field][:] = tm.tensor_from_file(tm, hd5)
diff --git a/ml4h/tensorize/tensor_writer_ukbb.py b/ml4h/tensorize/tensor_writer_ukbb.py
index 64242bcbe..ceb4389e8 100644
--- a/ml4h/tensorize/tensor_writer_ukbb.py
+++ b/ml4h/tensorize/tensor_writer_ukbb.py
@@ -87,6 +87,10 @@ def write_tensors(
mri_unzip: str,
mri_field_ids: List[int],
xml_field_ids: List[int],
+ zoom_x: int,
+ zoom_y: int,
+ zoom_width: int,
+ zoom_height: int,
write_pngs: bool,
min_sample_id: int,
max_sample_id: int,
@@ -105,6 +109,13 @@ def write_tensors(
:param mri_unzip: Folder where zipped DICOM will be decompressed
:param mri_field_ids: List of MRI field IDs from UKBB
:param xml_field_ids: List of ECG field IDs from UKBB
+ :param x: Maximum x dimension of MRIs
+ :param y: Maximum y dimension of MRIs
+ :param z: Maximum z dimension of MRIs
+ :param zoom_x: x coordinate of the zoom
+ :param zoom_y: y coordinate of the zoom
+ :param zoom_width: width of the zoom
+ :param zoom_height: height of the zoom
:param write_pngs: write MRIs as PNG images for debugging
:param min_sample_id: Minimum sample id to generate, for parallelization
:param max_sample_id: Maximum sample id to generate, for parallelization
@@ -126,7 +137,7 @@ def write_tensors(
continue
try:
with h5py.File(tp, 'w') as hd5:
- _write_tensors_from_zipped_dicoms(write_pngs, tensors, mri_unzip, mri_field_ids, zip_folder, hd5, sample_id, stats)
+ _write_tensors_from_zipped_dicoms(zoom_x, zoom_y, zoom_width, zoom_height, write_pngs, tensors, mri_unzip, mri_field_ids, zip_folder, hd5, sample_id, stats)
_write_tensors_from_zipped_niftis(zip_folder, mri_field_ids, hd5, sample_id, stats)
_write_tensors_from_xml(xml_field_ids, xml_folder, hd5, sample_id, write_pngs, stats, continuous_stats)
stats['Tensors written'] += 1
@@ -177,26 +188,19 @@ def write_tensors_from_dicom_pngs(
continue
stats[sample_header + '_' + sample_id] += 1
dicom_file = row[dicom_index]
-
try:
png = imageio.imread(os.path.join(png_path, dicom_file + png_postfix))
- if len(png.shape) == 3 and png.mean() == png[:, :, 0].mean():
- png = png[:, :, 0]
- elif len(png.shape) == 3:
- raise ValueError(f'PNG has color information but no method to tensorize it {png.mean()}, 0ch :{png[:, :, 0].mean()}, 1ch :{png[:, :, 1].mean()}, 2ch :{png[:, :, 2].mean()}.')
full_tensor = np.zeros((x, y), dtype=np.float32)
full_tensor[:png.shape[0], :png.shape[1]] = png
tensor_file = os.path.join(tensors, str(sample_id) + TENSOR_EXT)
if not os.path.exists(os.path.dirname(tensor_file)):
os.makedirs(os.path.dirname(tensor_file))
with h5py.File(tensor_file, 'a') as hd5:
- tensor_name = series.lower() + '_annotated_' + row[instance_index]
+ tensor_name = series + '_annotated_' + row[instance_index]
tp = tensor_path(path_prefix, tensor_name)
if tp in hd5:
tensor = first_dataset_at_path(hd5, tp)
- min_x = min(png.shape[0], tensor.shape[0])
- min_y = min(png.shape[1], tensor.shape[1])
- tensor[:min_x, :min_y] = full_tensor[:min_x, :min_y]
+ tensor[:] = full_tensor
stats['updated'] += 1
else:
create_tensor_in_hd5(hd5, path_prefix, tensor_name, full_tensor, stats)
@@ -324,7 +328,7 @@ def _dicts_and_plots_from_tensorization(
continuous = {}
value_counter = Counter()
for k in sorted(list(stats.keys())):
- #logging.info("{} has {}".format(k, stats[k]))
+ logging.info("{} has {}".format(k, stats[k]))
if 'categorical' not in k and 'continuous' not in k:
continue
@@ -342,10 +346,10 @@ def _dicts_and_plots_from_tensorization(
plot_value_counter(list(categories.keys()), value_counter, a_id + '_v_count', os.path.join(output_folder, a_id))
plot_histograms(continuous_stats, a_id, os.path.join(output_folder, a_id))
- # logging.info("Continuous tensor map: {}".format(continuous))
- # logging.info("Continuous Columns: {}".format(len(continuous)))
- # logging.info("Category tensor map: {}".format(categories))
- # logging.info("Categories Columns: {}".format(len(categories)))
+ logging.info("Continuous tensor map: {}".format(continuous))
+ logging.info("Continuous Columns: {}".format(len(continuous)))
+ logging.info("Category tensor map: {}".format(categories))
+ logging.info("Categories Columns: {}".format(len(categories)))
def _to_float_or_false(s):
@@ -363,6 +367,10 @@ def _to_float_or_nan(s):
def _write_tensors_from_zipped_dicoms(
+ zoom_x: int,
+ zoom_y: int,
+ zoom_width: int,
+ zoom_height: int,
write_pngs: bool,
tensors: str,
dicoms: str,
@@ -382,8 +390,10 @@ def _write_tensors_from_zipped_dicoms(
os.makedirs(dicom_folder)
with zipfile.ZipFile(zipped, "r") as zip_ref:
zip_ref.extractall(dicom_folder)
- ukb_instance = zipped.split('_')[2]
- _write_tensors_from_dicoms(write_pngs, tensors, dicom_folder, hd5, sample_str, ukb_instance, stats)
+ _write_tensors_from_dicoms(
+ zoom_x, zoom_y, zoom_width, zoom_height, write_pngs, tensors, dicom_folder,
+ hd5, sample_str, stats,
+ )
stats['MRI fields written'] += 1
shutil.rmtree(dicom_folder)
@@ -400,31 +410,36 @@ def _write_tensors_from_zipped_niftis(zip_folder: str, mri_field_ids: List[str],
def _write_tensors_from_dicoms(
- write_pngs: bool, tensors: str, dicom_folder: str, hd5: h5py.File, sample_str: str, ukb_instance: str, stats: Dict[str, int],
+ zoom_x: int, zoom_y: int, zoom_width: int, zoom_height: int, write_pngs: bool, tensors: str,
+ dicom_folder: str, hd5: h5py.File, sample_str: str, stats: Dict[str, int],
) -> None:
"""Convert a folder of DICOMs from a sample into tensors for each series
Segmented dicoms require special processing and are written to tensor per-slice
Arguments
+ :param x: Width of the tensors (actual MRI width will be padded with 0s or cropped to this number)
+ :param y: Height of the tensors (actual MRI width will be padded with 0s or cropped to this number)
+ :param z: Minimum number of slices to include in the each tensor if more slices are found they will be kept
+ :param zoom_x: x coordinate of the zoom
+ :param zoom_y: y coordinate of the zoom
+ :param zoom_width: width of the zoom
+ :param zoom_height: height of the zoom
:param write_pngs: write MRIs as PNG images for debugging
:param tensors: Folder where hd5 tensor files are being written
:param dicom_folder: Folder with all dicoms associated with one sample.
:param hd5: Tensor file in which to create datasets for each series and each segmented slice
:param sample_str: The current sample ID as a string
- :param ukb_instance: The UK Biobank assessment visit instance number
:param stats: Counter to keep track of summary statistics
"""
views = defaultdict(list)
- series_to_numbers = defaultdict(set)
min_ideal_series = 9e9
for dicom in os.listdir(dicom_folder):
if os.path.splitext(dicom)[-1] != DICOM_EXT:
continue
d = pydicom.read_file(os.path.join(dicom_folder, dicom))
series = d.SeriesDescription.lower().replace(' ', '_')
- series_to_numbers[series].add(int(d.SeriesNumber))
if series + '_12bit' in MRI_LIVER_SERIES_12BIT and d.LargestImagePixelValue > 2048:
views[series + '_12bit'].append(d)
stats[series + '_12bit'] += 1
@@ -447,61 +462,99 @@ def _write_tensors_from_dicoms(
else:
mri_group = 'ukb_mri'
- if len(series_to_numbers[v]) > 1 and v not in MRI_BRAIN_SERIES:
- max_series = max(series_to_numbers[v])
- single_series = [dicom for dicom in views[v] if int(dicom.SeriesNumber) == max_series]
- # for d in views[v]:
- # logging.warning(f'{d.SeriesNumber} with Date: {_datetime_from_dicom(d)} Time {d.AcquisitionTime}')
- logging.warning(f'{v} has {len(views[v])} series:{series_to_numbers[v]} Using only max series: {max_series} with {len(single_series)}')
- views[v] = single_series
if v == MRI_TO_SEGMENT:
- _tensorize_short_and_long_axis_segmented_cardiac_mri(views[v], v, ukb_instance, hd5, mri_date, mri_group, stats)
+ _tensorize_short_and_long_axis_segmented_cardiac_mri(views[v], v, zoom_x, zoom_y, zoom_width, zoom_height, write_pngs, tensors, hd5, mri_date, mri_group, stats)
elif v in MRI_BRAIN_SERIES:
_tensorize_brain_mri(views[v], v, mri_date, mri_group, hd5)
else:
- pass
- # mri_data = np.zeros((views[v][0].Rows, views[v][0].Columns, len(views[v])), dtype=np.float32)
- # for slicer in views[v]:
- # _save_pixel_dimensions_if_missing(slicer, v, hd5)
- # _save_slice_thickness_if_missing(slicer, v, hd5)
- # _save_series_orientation_and_position_if_missing(slicer, v, hd5)
- # slice_index = slicer.InstanceNumber - 1
- # if v in MRI_LIVER_IDEAL_PROTOCOL:
- # slice_index = _slice_index_from_ideal_protocol(slicer, min_ideal_series)
- # mri_data[..., slice_index] = slicer.pixel_array.astype(np.float32)
- # create_tensor_in_hd5(hd5, mri_group, f'{v}/{ukb_instance}', mri_data, stats, mri_date)
+ mri_data = np.zeros((views[v][0].Rows, views[v][0].Columns, len(views[v])), dtype=np.float32)
+ for slicer in views[v]:
+ _save_pixel_dimensions_if_missing(slicer, v, hd5)
+ _save_slice_thickness_if_missing(slicer, v, hd5)
+ _save_series_orientation_and_position_if_missing(slicer, v, hd5)
+ slice_index = slicer.InstanceNumber - 1
+ if v in MRI_LIVER_IDEAL_PROTOCOL:
+ slice_index = _slice_index_from_ideal_protocol(slicer, min_ideal_series)
+ mri_data[..., slice_index] = slicer.pixel_array.astype(np.float32)
+ create_tensor_in_hd5(hd5, mri_group, v, mri_data, stats, mri_date)
def _tensorize_short_and_long_axis_segmented_cardiac_mri(
- slices: List[pydicom.Dataset], series: str, instance: str,
- hd5: h5py.File, mri_date: datetime.datetime, mri_group: str, stats: Dict[str, int],
+ slices: List[pydicom.Dataset], series: str, zoom_x: int, zoom_y: int,
+ zoom_width: int, zoom_height: int, write_pngs: bool, tensors: str,
+ hd5: h5py.File, mri_date: datetime.datetime, mri_group: str,
+ stats: Dict[str, int],
) -> None:
+ systoles = {}
+ diastoles = {}
+ systoles_pix = {}
+ systoles_masks = {}
+ diastoles_masks = {}
+
for slicer in slices:
- #full_slice = np.zeros((slicer.Rows, slicer.Columns), dtype=np.float32)
+ full_mask = np.zeros((slicer.Rows, slicer.Columns), dtype=np.float32)
+ full_slice = np.zeros((slicer.Rows, slicer.Columns), dtype=np.float32)
+
if _has_overlay(slicer):
if _is_mitral_valve_segmentation(slicer):
series = series.replace('sax', 'lax')
else:
series = series.replace('lax', 'sax')
-
series_segmented = f'{series}_segmented'
+ series_zoom = f'{series}_zoom'
+ series_zoom_segmented = f'{series}_zoom_segmented'
+
try:
overlay, mask, ventricle_pixels, _ = _get_overlay_from_dicom(slicer)
except KeyError:
logging.exception(f'Got key error trying to make anatomical mask, skipping.')
continue
- # _save_pixel_dimensions_if_missing(slicer, series, hd5)
- # _save_slice_thickness_if_missing(slicer, series, hd5)
- # _save_series_orientation_and_position_if_missing(slicer, series, hd5, str(slicer.InstanceNumber))
+ _save_pixel_dimensions_if_missing(slicer, series, hd5)
+ _save_slice_thickness_if_missing(slicer, series, hd5)
+ _save_series_orientation_and_position_if_missing(slicer, series, hd5, str(slicer.InstanceNumber))
_save_pixel_dimensions_if_missing(slicer, series_segmented, hd5)
_save_slice_thickness_if_missing(slicer, series_segmented, hd5)
_save_series_orientation_and_position_if_missing(slicer, series_segmented, hd5, str(slicer.InstanceNumber))
- #
- # cur_angle = (slicer.InstanceNumber - 1) // MRI_FRAMES # dicom InstanceNumber is 1-based
- #full_slice[:] = slicer.pixel_array.astype(np.float32)
- #create_tensor_in_hd5(hd5, mri_group, f'{series}{HD5_GROUP_CHAR}{instance}', full_slice, stats, mri_date, slicer.InstanceNumber)
- create_tensor_in_hd5(hd5, mri_group, f'{series_segmented}{HD5_GROUP_CHAR}{instance}', mask, stats, mri_date, slicer.InstanceNumber)
+
+ cur_angle = (slicer.InstanceNumber - 1) // MRI_FRAMES # dicom InstanceNumber is 1-based
+ full_slice[:] = slicer.pixel_array.astype(np.float32)
+ create_tensor_in_hd5(hd5, mri_group, f'{series}{HD5_GROUP_CHAR}{slicer.InstanceNumber}', full_slice, stats, mri_date)
+ create_tensor_in_hd5(hd5, mri_group, f'{series_zoom_segmented}{HD5_GROUP_CHAR}{slicer.InstanceNumber}', mask, stats, mri_date)
+
+ zoom_slice = full_slice[zoom_x: zoom_x + zoom_width, zoom_y: zoom_y + zoom_height]
+ zoom_mask = mask[zoom_x: zoom_x + zoom_width, zoom_y: zoom_y + zoom_height]
+ create_tensor_in_hd5(hd5, mri_group, f'{series_zoom}{HD5_GROUP_CHAR}{slicer.InstanceNumber}', zoom_slice, stats, mri_date)
+ create_tensor_in_hd5(hd5, mri_group, f'{series_zoom_segmented}{HD5_GROUP_CHAR}{slicer.InstanceNumber}', zoom_mask, stats, mri_date)
+
+ if (slicer.InstanceNumber - 1) % MRI_FRAMES == 0: # Diastole frame is always the first
+ diastoles[cur_angle] = slicer
+ diastoles_masks[cur_angle] = mask
+ if cur_angle not in systoles:
+ systoles[cur_angle] = slicer
+ systoles_pix[cur_angle] = ventricle_pixels
+ systoles_masks[cur_angle] = mask
+ else:
+ if ventricle_pixels < systoles_pix[cur_angle]:
+ systoles[cur_angle] = slicer
+ systoles_pix[cur_angle] = ventricle_pixels
+ systoles_masks[cur_angle] = mask
+
+ for angle in diastoles:
+ logging.info(f'Found systole, instance:{systoles[angle].InstanceNumber} ventricle pixels:{systoles_pix[angle]}')
+ full_slice = diastoles[angle].pixel_array.astype(np.float32)
+ create_tensor_in_hd5(hd5, mri_group, f'diastole_frame_b{angle}', full_slice, stats, mri_date)
+ create_tensor_in_hd5(hd5, mri_group, f'diastole_mask_b{angle}', diastoles_masks[angle], stats, mri_date)
+ if write_pngs:
+ plt.imsave(tensors + 'diastole_frame_b' + str(angle) + IMAGE_EXT, full_slice)
+ plt.imsave(tensors + 'diastole_mask_b' + str(angle) + IMAGE_EXT, full_mask)
+
+ full_slice = systoles[angle].pixel_array.astype(np.float32)
+ create_tensor_in_hd5(hd5, mri_group, f'systole_frame_b{angle}', full_slice, stats, mri_date)
+ create_tensor_in_hd5(hd5, mri_group, f'systole_mask_b{angle}', systoles_masks[angle], stats, mri_date)
+ if write_pngs:
+ plt.imsave(tensors + 'systole_frame_b' + str(angle) + IMAGE_EXT, full_slice)
+ plt.imsave(tensors + 'systole_mask_b' + str(angle) + IMAGE_EXT, full_mask)
def _tensorize_brain_mri(slices: List[pydicom.Dataset], series: str, mri_date: datetime.datetime, mri_group: str, hd5: h5py.File) -> None:
@@ -535,16 +588,13 @@ def _save_slice_thickness_if_missing(slicer, series, hd5):
def _save_series_orientation_and_position_if_missing(slicer, series, hd5, instance=None):
orientation_ds_name = MRI_PATIENT_ORIENTATION + '_' + series
position_ds_name = MRI_PATIENT_POSITION + '_' + series
- if instance is not None:
- orientation_ds_name = f'{orientation_ds_name}_{instance}'
- position_ds_name = f'{position_ds_name}_{instance}'
- try:
- if orientation_ds_name not in hd5 and series in MRI_BRAIN_SERIES + MRI_CARDIAC_SERIES + MRI_CARDIAC_SERIES_SEGMENTED + MRI_LIVER_SERIES + MRI_LIVER_SERIES_12BIT:
- hd5.create_dataset(orientation_ds_name, data=[float(x) for x in slicer.ImageOrientationPatient])
- if position_ds_name not in hd5 and series in MRI_BRAIN_SERIES + MRI_CARDIAC_SERIES + MRI_CARDIAC_SERIES_SEGMENTED + MRI_LIVER_SERIES + MRI_LIVER_SERIES_12BIT:
- hd5.create_dataset(position_ds_name, data=[float(x) for x in slicer.ImagePositionPatient])
- except RuntimeError as e:
- logging.warning(f' got error {e} \n orientation : {orientation_ds_name} {slicer.ImageOrientationPatient} and pos: {position_ds_name} {slicer.ImagePositionPatient}')
+ if instance:
+ orientation_ds_name += HD5_GROUP_CHAR + instance
+ position_ds_name += HD5_GROUP_CHAR + instance
+ if orientation_ds_name not in hd5 and series in MRI_BRAIN_SERIES + MRI_CARDIAC_SERIES + MRI_CARDIAC_SERIES_SEGMENTED + MRI_LIVER_SERIES + MRI_LIVER_SERIES_12BIT:
+ hd5.create_dataset(orientation_ds_name, data=[float(x) for x in slicer.ImageOrientationPatient])
+ if position_ds_name not in hd5 and series in MRI_BRAIN_SERIES + MRI_CARDIAC_SERIES + MRI_CARDIAC_SERIES_SEGMENTED + MRI_LIVER_SERIES + MRI_LIVER_SERIES_12BIT:
+ hd5.create_dataset(position_ds_name, data=[float(x) for x in slicer.ImagePositionPatient])
def _has_overlay(d) -> bool:
@@ -695,16 +745,13 @@ def _write_ecg_rest_tensors(ecgs, xml_field, hd5, sample_id, write_pngs, stats,
def create_tensor_in_hd5(
hd5: h5py.File, path_prefix: str, name: str, value, stats: Counter = None, date: datetime.datetime = None,
- instance: str = None, storage_type: StorageType = None, attributes: Dict[str, Any] = None,
+ storage_type: StorageType = None, attributes: Dict[str, Any] = None,
):
hd5_path = tensor_path(path_prefix, name)
- if instance is not None:
- hd5_path = f'{hd5_path}instance_{instance}/'
if hd5_path in hd5:
hd5_path = f'{hd5_path}instance_{len(hd5[hd5_path])}'
- elif instance is None:
+ else:
hd5_path = f'{hd5_path}instance_0'
-
if stats is not None:
stats[hd5_path] += 1
if storage_type == StorageType.STRING:
From 6021abb4b65527349b8042cd52a29c25f3d0532e Mon Sep 17 00:00:00 2001
From: Samwell Freeman
Date: Tue, 29 Sep 2020 17:27:41 -0400
Subject: [PATCH 04/21] paired
---
.../visualization_tools/annotation_storage.py | 36 ++--
ml4h/visualization_tools/annotations.py | 55 +++---
.../dicom_interactive_plots.py | 74 +++----
ml4h/visualization_tools/dicom_plots.py | 122 ++++++------
.../ecg_interactive_plots.py | 22 ++-
ml4h/visualization_tools/ecg_reshape.py | 58 +++---
ml4h/visualization_tools/ecg_static_plots.py | 11 +-
ml4h/visualization_tools/facets.py | 13 +-
ml4h/visualization_tools/hd5_mri_plots.py | 181 +++++++++---------
9 files changed, 311 insertions(+), 261 deletions(-)
diff --git a/ml4h/visualization_tools/annotation_storage.py b/ml4h/visualization_tools/annotation_storage.py
index ac8e89249..d0020b1ec 100644
--- a/ml4h/visualization_tools/annotation_storage.py
+++ b/ml4h/visualization_tools/annotation_storage.py
@@ -2,9 +2,11 @@
import abc
import datetime
-import pandas as pd
+from typing import Optional, Union
+
from google.cloud import bigquery
from google.cloud.bigquery import magics as bqmagics
+import pandas as pd
class AnnotationStorage(abc.ABC):
@@ -14,12 +16,14 @@ class AnnotationStorage(abc.ABC):
"""
@abc.abstractmethod
- def describe(self):
+ def describe(self) -> str:
"""Return a string describing how annotations are stored."""
- pass
@abc.abstractmethod
- def submit_annotation(self, sample_id, annotator, key, value_numeric, value_string, comment):
+ def submit_annotation(
+ self, sample_id: Union[int, str], annotator: str, key: str,
+ value_numeric: Optional[Union[int, float]], value_string: Optional[str], comment: str,
+ ) -> bool:
"""Add an annotation to the collection of annotations.
Args:
@@ -32,10 +36,9 @@ def submit_annotation(self, sample_id, annotator, key, value_numeric, value_stri
Returns:
Whether the submission was successful. Throws an Exception on failure.
"""
- pass
@abc.abstractmethod
- def view_recent_submissions(self, count=10):
+ def view_recent_submissions(self, count: int = 10) -> pd.DataFrame:
"""View a dataframe of up to [count] most recent submissions.
Args:
@@ -44,7 +47,6 @@ def view_recent_submissions(self, count=10):
Returns:
A dataframe of the most recent annotations.
"""
- pass
class TransientAnnotationStorage(AnnotationStorage):
@@ -56,11 +58,14 @@ class TransientAnnotationStorage(AnnotationStorage):
def __init__(self):
self.annotations = []
- def describe(self):
+ def describe(self) -> str:
return '''Annotations will be stored in memory only during the duration of this demo.\n
For durable storage of annotations, use BigQueryAnnotationStorage instead.'''
- def submit_annotation(self, sample_id, annotator, key, value_numeric, value_string, comment):
+ def submit_annotation(
+ self, sample_id: Union[int, str], annotator: str, key: str,
+ value_numeric: Optional[Union[int, float]], value_string: Optional[str], comment: str,
+ ) -> bool:
"""Add this annotation to our in-memory collection of annotations.
Args:
@@ -85,7 +90,7 @@ def submit_annotation(self, sample_id, annotator, key, value_numeric, value_stri
self.annotations.append(annotation)
return True
- def view_recent_submissions(self, count=10):
+ def view_recent_submissions(self, count: int = 10) -> pd.DataFrame:
"""View a dataframe of up to [count] most recent submissions.
Args:
@@ -110,14 +115,17 @@ class BigQueryAnnotationStorage(AnnotationStorage):
annotations_schema.json
"""
- def __init__(self, table):
+ def __init__(self, table: str):
"""This table should already exist."""
self.table = table
- def describe(self):
+ def describe(self) -> str:
return f'''Annotations are stored in BigQuery table {self.table}'''
- def submit_annotation(self, sample_id, annotator, key, value_numeric, value_string, comment):
+ def submit_annotation(
+ self, sample_id: Union[int, str], annotator: str, key: str,
+ value_numeric: Optional[Union[int, float]], value_string: Optional[str], comment: str,
+ ) -> bool:
"""Call a BigQuery INSERT statement to add a row containing annotation information.
Args:
@@ -150,7 +158,7 @@ def submit_annotation(self, sample_id, annotator, key, value_numeric, value_stri
# Return whether the submission completed.
return submission.done()
- def view_recent_submissions(self, count=10):
+ def view_recent_submissions(self, count: int = 10) -> pd.DataFrame:
"""View a dataframe of up to [count] most recent submissions.
This is a convenience method for use within the annotation flow. For full access to the underlying annotations,
diff --git a/ml4h/visualization_tools/annotations.py b/ml4h/visualization_tools/annotations.py
index 2400a07d8..9ca9c1b44 100644
--- a/ml4h/visualization_tools/annotations.py
+++ b/ml4h/visualization_tools/annotations.py
@@ -2,8 +2,11 @@
import os
import socket
+from typing import Any, Dict, Union
+
from IPython.display import display
from IPython.display import HTML
+import pandas as pd
import ipywidgets as widgets
from ml4h.visualization_tools.annotation_storage import AnnotationStorage
from ml4h.visualization_tools.annotation_storage import TransientAnnotationStorage
@@ -11,14 +14,18 @@
DEFAULT_ANNOTATION_STORAGE = TransientAnnotationStorage()
-def _get_df_sample(sample_info, sample_id):
+def _get_df_sample(sample_info: pd.DataFrame, sample_id: Union[int, str]) -> pd.DataFrame:
"""Return a dataframe containing only the row for the indicated sample_id."""
df_sample = sample_info[sample_info['sample_id'] == str(sample_id)]
- if 0 == df_sample.shape[0]: df_sample = sample_info.query('sample_id == ' + str(sample_id))
+ if df_sample.shape[0] == 0: df_sample = sample_info.query('sample_id == ' + str(sample_id))
return df_sample
-def display_annotation_collector(sample_info, sample_id, annotation_storage: AnnotationStorage = DEFAULT_ANNOTATION_STORAGE, custom_annotation_key=None):
+def display_annotation_collector(
+ sample_info: pd.DataFrame, sample_id: Union[int, str],
+ annotation_storage: AnnotationStorage = DEFAULT_ANNOTATION_STORAGE,
+ custom_annotation_key: str = None,
+) -> None:
"""Method to create a gui (set of widgets) through which the user can create an annotation and submit it to storage.
Args:
@@ -26,15 +33,16 @@ def display_annotation_collector(sample_info, sample_id, annotation_storage: Ann
sample_id: The selected sample for which the values will be displayed.
annotation_storage: An instance of AnnotationStorage.
custom_annotation_key: The key for an annotation of data other than the tabular fields.
-
- Returns:
- A notebook-friendly messages indicating the status of the submission.
"""
df_sample = _get_df_sample(sample_info, sample_id)
if df_sample.shape[0] == 0:
- return HTML(f'''
- Warning: Sample {sample_id} not present in sample_info DataFrame.
''')
+ display(
+ HTML(f'''
+ Warning: Sample {sample_id} not present in sample_info DataFrame.
+
'''),
+ Annotation not submitted. Please try again.
+ '''),
)
- submit_button.on_click(on_button_clicked)
+ submit_button.on_click(cb_on_button_clicked)
# Display all the widgets.
display(sample, box1, comment, submit_button, output)
-def _format_annotation(sample_id, key, keyvalue, comment):
+def _format_annotation(
+ sample_id: Union[int, str], key: str, keyvalue: Union[int, float, str], comment: str,
+) -> Dict[str, Any]:
"""Helper method to clean and reshape info from the widgets and the environment into a dictionary representing the annotation."""
# Programmatically get the identity of the person running this Terra notebook.
current_user = os.getenv('OWNER_EMAIL')
@@ -128,11 +140,10 @@ def _format_annotation(sample_id, key, keyvalue, comment):
if current_user is None:
current_user = socket.gethostname() # By convention, we prefix the hostname with our username.
+ value_numeric = None
+ value_string = None
# Check whether the value is string or numeric.
- if keyvalue is None:
- value_numeric = None
- value_string = None
- else:
+ if keyvalue is not None:
try:
value_numeric = float(keyvalue) # this will fail if the value is text
value_string = None
diff --git a/ml4h/visualization_tools/dicom_interactive_plots.py b/ml4h/visualization_tools/dicom_interactive_plots.py
index d9850e841..ec9d63834 100644
--- a/ml4h/visualization_tools/dicom_interactive_plots.py
+++ b/ml4h/visualization_tools/dicom_interactive_plots.py
@@ -1,4 +1,4 @@
-"""Methods for integration of interactive dicom plots within notebooks.
+"""Methods for integration of interactive DICOM plots within notebooks.
TODO:
* Continue to *pragmatically* improve this to make the visualization controls
@@ -8,14 +8,15 @@
import collections
import os
import tempfile
+from typing import Any, DefaultDict, Dict, Optional, Tuple
import zipfile
from IPython.display import display
from IPython.display import HTML
+import numpy as np
import ipywidgets as widgets
import matplotlib.pyplot as plt
from ml4h.runtime_data_defines import get_mri_folders
-import numpy as np
import pydicom
import tensorflow as tf
@@ -27,15 +28,12 @@
MAX_COLOR_RANGE = 6000
-def choose_mri(sample_id, folder=None):
+def choose_mri(sample_id, folder: Optional[str] = None) -> None:
"""Render widget to choose the MRI to plot.
Args:
sample_id: The id of the sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
-
- Returns:
- ipywidget or HTML upon error.
"""
if folder is None:
folders = get_mri_folders(sample_id)
@@ -45,22 +43,26 @@ def choose_mri(sample_id, folder=None):
sample_mris = []
sample_mri_glob = str(sample_id) + '_*.zip'
try:
- for folder in folders:
- sample_mris.extend(tf.io.gfile.glob(pattern=os.path.join(folder, sample_mri_glob)))
+ for f in folders:
+ sample_mris.extend(tf.io.gfile.glob(pattern=os.path.join(f, sample_mri_glob)))
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- return HTML(f'''
-
+ display(
+ HTML(f'''
Warning: MRI not available for sample {sample_id} in {folders}:
{e.message}
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
''')
+
'''),
+ )
+ return
if not sample_mris:
- return HTML(f'''
-
+ display(
+ HTML(f'''
Warning: MRI DICOMs not available for sample {sample_id} in {folders}.
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
''')
+
'''),
+ )
+ return
mri_chooser = widgets.Dropdown(
options=sample_mris,
@@ -77,14 +79,11 @@ def choose_mri(sample_id, folder=None):
display(file_controls_ui, file_controls_output)
-def choose_mri_series(sample_mri):
+def choose_mri_series(sample_mri: str) -> None:
"""Render widgets and interactive plots for MRIs.
Args:
sample_mri: The local or Cloud Storage path to the MRI file.
-
- Returns:
- ipywidget or HTML upon error.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
local_path = os.path.join(tmpdirname, os.path.basename(sample_mri))
@@ -93,13 +92,15 @@ def choose_mri_series(sample_mri):
with zipfile.ZipFile(local_path, 'r') as zip_ref:
zip_ref.extractall(tmpdirname)
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- return HTML(f'''
-
+ display(
+ HTML(f'''
Warning: Cardiac MRI not available for sample {os.path.basename(sample_mri)}:
{e.message}
-
''')
+
'''),
+ )
+ return
- unordered_dicoms = collections.defaultdict(dict)
+ unordered_dicoms: DefaultDict[Any, Any] = collections.defaultdict(dict)
for dcm_file in os.listdir(tmpdirname):
if not dcm_file.endswith('.dcm'):
continue
@@ -112,8 +113,13 @@ def choose_mri_series(sample_mri):
unordered_dicoms[key1][key2] = dcm
if not unordered_dicoms:
- print(f'\n\nNo series available in MRI for sample {os.path.basename(sample_mri)}\n\nTry a different MRI.')
- return None
+ display(
+ HTML(f'''
+ No series available in MRI for sample {os.path.basename(sample_mri)}.
+ Try a different MRI.
+
'''),
+ )
+ return
# Convert from dict of dicts to dict of ordered lists.
dicoms = {}
@@ -134,7 +140,7 @@ def choose_mri_series(sample_mri):
style={'description_width': 'initial'},
layout=widgets.Layout(width='800px'),
)
- # Slide through dicom image instances using a slide bar.
+ # Slide through DICOM image instances using a slide bar.
instance_chooser = widgets.IntSlider(
continuous_update=True,
value=default_instance_value,
@@ -212,25 +218,25 @@ def on_value_change(change):
display(viz_controls_ui, viz_controls_output)
-def compute_color_range(dicoms, series_name):
+def compute_color_range(dicoms: Dict[str, Any], series_name: str) -> Tuple[int, int]:
"""Compute the mean values for the color ranges of instances in the series."""
vmin = np.mean([np.min(d.pixel_array) for d in dicoms[series_name]])
vmax = np.mean([np.max(d.pixel_array) for d in dicoms[series_name]])
- return(vmin, vmax)
+ return (vmin, vmax)
-def compute_instance_range(dicoms, series_name):
+def compute_instance_range(dicoms: Dict[str, Any], series_name: str) -> Tuple[int, int]:
"""Compute middle and max instances."""
middle_instance = int(len(dicoms[series_name]) / 2)
max_instance = len(dicoms[series_name])
- return(middle_instance, max_instance)
+ return (middle_instance, max_instance)
def dicom_animation(
- dicoms, series_name, instance, vmin, vmax, transpose,
- fig_width, title_prefix='',
-):
- """Render one frame of a dicom animation.
+ dicoms: Dict[str, Any], series_name: str, instance: int, vmin: int, vmax: int, transpose: bool,
+ fig_width: int, title_prefix: str = '',
+) -> None:
+ """Render one frame of a DICOM animation.
Args:
dicoms: the dictionary DICOM series and instances lists
@@ -250,7 +256,7 @@ def dicom_animation(
dcm = dicoms[series_name][instance - 1]
if instance != dcm.InstanceNumber:
# Notice invalid input, but don't throw an error.
- print(f'WARNING: Instance parameter {str(instance)} and dicom instance number {str(dcm.InstanceNumber)} do not match.')
+ print(f'WARNING: Instance parameter {str(instance)} and instance number {str(dcm.InstanceNumber)} do not match.')
if transpose:
height = dcm.pixel_array.T.shape[0]
diff --git a/ml4h/visualization_tools/dicom_plots.py b/ml4h/visualization_tools/dicom_plots.py
index ce2b3e083..093691382 100644
--- a/ml4h/visualization_tools/dicom_plots.py
+++ b/ml4h/visualization_tools/dicom_plots.py
@@ -1,16 +1,17 @@
-"""Methods for integration of dicom plots within notebooks."""
+"""Methods for integration of DICOM plots within notebooks."""
import collections
import os
import tempfile
+from typing import Dict, List, Optional, Tuple, Union
import zipfile
from IPython.display import display
from IPython.display import HTML
+import numpy as np
import ipywidgets as widgets
import matplotlib.pyplot as plt
from ml4h.runtime_data_defines import get_cardiac_mri_folder
-import numpy as np
import pydicom
from scipy.ndimage.morphology import binary_closing
from scipy.ndimage.morphology import binary_erosion
@@ -27,21 +28,21 @@
MRI_SEGMENTED_CHANNEL_MAP = {'background': 0, 'ventricle': 1, 'myocardium': 2}
-def _is_mitral_valve_segmentation(d): # -> bool:
- """Determine whether a dicom has mitral valve segmentation.
+def _is_mitral_valve_segmentation(d: pydicom.FileDataset) -> bool:
+ """Determine whether a DICOM has mitral valve segmentation.
This is used for visualization of CINE_segmented_SAX_InlineVF.
Args:
- d: the dicom file
+ d: the DICOM file
Returns:
- Whether or not the dicom has mitral valve segmentation
+ Whether or not the DICOM has mitral valve segmentation
"""
return d.SliceThickness == 6
-def _get_overlay_from_dicom(d):
+def _get_overlay_from_dicom(d: pydicom.FileDataset) -> Tuple[int, int, int]:
"""Get an overlay from a DICOM file.
Morphological operators are used to transform the pixel outline of the
@@ -49,7 +50,7 @@ def _get_overlay_from_dicom(d):
is used for visualization of CINE_segmented_SAX_InlineVF.
Args:
- d: the dicom file
+ d: the DICOM file
Returns:
Raw overlay array with myocardium outline, anatomical mask (a pixel
@@ -77,29 +78,30 @@ def _get_overlay_from_dicom(d):
byte >>= 1
bit += 1
overlay = overlay[:expected_bit_length]
- if overlay_frames == 1:
- overlay = overlay.reshape(rows, cols)
- idx = np.where(overlay == 1)
- min_pos = (np.min(idx[0]), np.min(idx[1]))
- max_pos = (np.max(idx[0]), np.max(idx[1]))
- short_side = min((max_pos[0] - min_pos[0]), (max_pos[1] - min_pos[1]))
- small_radius = max(MRI_MIN_RADIUS, short_side * MRI_SMALL_RADIUS_FACTOR)
- big_radius = max(MRI_MIN_RADIUS+1, short_side * MRI_BIG_RADIUS_FACTOR)
- small_structure = _unit_disk(small_radius)
- m1 = binary_closing(overlay, small_structure).astype(np.int)
- big_structure = _unit_disk(big_radius)
- m2 = binary_closing(overlay, big_structure).astype(np.int)
- anatomical_mask = m1 + m2
+ if overlay_frames != 1:
+ raise ValueError(f'DICOM has {overlay_frames} overlay frames, but only one expected.')
+ overlay = overlay.reshape(rows, cols)
+ idx = np.where(overlay == 1)
+ min_pos = (np.min(idx[0]), np.min(idx[1]))
+ max_pos = (np.max(idx[0]), np.max(idx[1]))
+ short_side = min((max_pos[0] - min_pos[0]), (max_pos[1] - min_pos[1]))
+ small_radius = max(MRI_MIN_RADIUS, short_side * MRI_SMALL_RADIUS_FACTOR)
+ big_radius = max(MRI_MIN_RADIUS+1, short_side * MRI_BIG_RADIUS_FACTOR)
+ small_structure = _unit_disk(small_radius)
+ m1 = binary_closing(overlay, small_structure).astype(np.int)
+ big_structure = _unit_disk(big_radius)
+ m2 = binary_closing(overlay, big_structure).astype(np.int)
+ anatomical_mask = m1 + m2
+ ventricle_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['ventricle'])
+ myocardium_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['myocardium'])
+ if ventricle_pixels == 0 and myocardium_pixels > MRI_MAX_MYOCARDIUM:
+ erode_structure = _unit_disk(small_radius*1.5)
+ anatomical_mask = anatomical_mask - binary_erosion(m1, erode_structure).astype(np.int)
ventricle_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['ventricle'])
- myocardium_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['myocardium'])
- if ventricle_pixels == 0 and myocardium_pixels > MRI_MAX_MYOCARDIUM:
- erode_structure = _unit_disk(small_radius*1.5)
- anatomical_mask = anatomical_mask - binary_erosion(m1, erode_structure).astype(np.int)
- ventricle_pixels = np.count_nonzero(anatomical_mask == MRI_SEGMENTED_CHANNEL_MAP['ventricle'])
- return overlay, anatomical_mask, ventricle_pixels
+ return overlay, anatomical_mask, ventricle_pixels
-def _unit_disk(r): # -> np.ndarray:
+def _unit_disk(r: int) -> np.ndarray:
"""Get the unit disk for a radius.
This is used for visualization of CINE_segmented_SAX_InlineVF.
@@ -114,7 +116,9 @@ def _unit_disk(r): # -> np.ndarray:
return (x ** 2 + y ** 2 <= r ** 2).astype(np.int)
-def plot_cardiac_long_axis(b_series, sides=7, fig_width=18, title_prefix=''):
+def plot_cardiac_long_axis(
+ b_series: List[pydicom.FileDataset], sides: int = 7, fig_width: int = 18, title_prefix: str = '',
+) -> None:
"""Visualize CINE_segmented_SAX_InlineVF series.
Args:
@@ -168,9 +172,9 @@ def plot_cardiac_long_axis(b_series, sides=7, fig_width=18, title_prefix=''):
def plot_cardiac_short_axis(
- series, transpose=False, fig_width=18,
- title_prefix='',
-):
+ series: List[pydicom.FileDataset], transpose: bool = False, fig_width: int = 18,
+ title_prefix: str = '',
+) -> None:
"""Visualize CINE_segmented_LAX series.
Args:
@@ -225,14 +229,14 @@ def plot_cardiac_short_axis(
def plot_mri_series(
- sample_mri, dicoms, series_name, sax_sides,
- lax_transpose, fig_width,
-):
+ sample_mri: str, dicoms: Dict[str, pydicom.FileDataset], series_name: str, sax_sides: int,
+ lax_transpose: bool, fig_width: int,
+) -> None:
"""Visualize the applicable series within this DICOM.
Args:
sample_mri: The local or Cloud Storage path to the MRI file.
- dicoms: A dictionary of dicoms.
+ dicoms: A dictionary of DICOMs.
series_name: The name of the chosen series.
sax_sides: How many sides to display for CINE_segmented_SAX_InlineVF.
lax_transpose: Whether to transpose when plotting CINE_segmented_LAX.
@@ -258,10 +262,9 @@ def plot_mri_series(
)
else:
print(f'Visualization not currently implemented for {series_name}.')
- return None
-def choose_mri_series(sample_mri):
+def choose_mri_series(sample_mri: str) -> None:
"""Render widgets and plots for cardiac MRIs.
Visualization is supported for CINE_segmented_SAX_InlineVF series and
@@ -269,9 +272,6 @@ def choose_mri_series(sample_mri):
Args:
sample_mri: The local or Cloud Storage path to the MRI file.
-
- Returns:
- ipywidget or HTML upon error.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
local_path = os.path.join(tmpdirname, os.path.basename(sample_mri))
@@ -280,11 +280,13 @@ def choose_mri_series(sample_mri):
with zipfile.ZipFile(local_path, 'r') as zip_ref:
zip_ref.extractall(tmpdirname)
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- return HTML(f'''
-
+ display(
+ HTML(f'''
Warning: Cardiac MRI not available for sample {os.path.basename(sample_mri)}:
{e.message}
-
''')
+
'''),
+ )
+ return
filtered_dicoms = collections.defaultdict(list)
series_descriptions = []
@@ -295,7 +297,7 @@ def choose_mri_series(sample_mri):
series_descriptions.append(dcm.SeriesDescription)
if 'cine_segmented_lax' in dcm.SeriesDescription.lower():
filtered_dicoms[dcm.SeriesDescription.lower()].append(dcm)
- if 'cine_segmented_sax_inlinevf' == dcm.SeriesDescription.lower():
+ if dcm.SeriesDescription.lower() == 'cine_segmented_sax_inlinevf':
cur_angle = (dcm.InstanceNumber - 1) // MRI_FRAMES
filtered_dicoms[f'{dcm.SeriesDescription.lower()}_angle_{str(cur_angle)}'].append(dcm)
@@ -350,22 +352,20 @@ def choose_mri_series(sample_mri):
)
display(viz_controls_ui, viz_controls_output)
else:
- print(
- f'\n\nNeither CINE_segmented_SAX_InlineVF nor CINE_segmented_LAX available in MRI for sample {os.path.basename(sample_mri)}.',
- '\n\nTry a different MRI.',
+ display(
+ HTML(f'''
+ Neither CINE_segmented_SAX_InlineVF nor CINE_segmented_LAX available in MRI for sample {os.path.basename(sample_mri)}.
+ Try a different MRI.
+
'''),
)
- return None
-def choose_cardiac_mri(sample_id, folder=None):
+def choose_cardiac_mri(sample_id: Union[int, str], folder: Optional[str] = None) -> None:
"""Render widget to choose the cardiac MRI to plot.
Args:
sample_id: The id of the ECG sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
-
- Returns:
- ipywidget or HTML upon error.
"""
if folder is None:
folder = get_cardiac_mri_folder(sample_id)
@@ -374,19 +374,23 @@ def choose_cardiac_mri(sample_id, folder=None):
try:
sample_mris = tf.io.gfile.glob(pattern=os.path.join(folder, sample_mri_glob))
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- return HTML(f'''
-
+ display(
+ HTML(f'''
Warning: Cardiac MRI not available for sample {sample_id} in {folder}:
{e.message}
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
''')
+
'''),
+ )
+ return
if not sample_mris:
- return HTML(f'''
-
+ display(
+ HTML(f'''
Warning: Cardiac MRI DICOM not available for sample {sample_id} in {folder}.
Use the folder parameter to read DICOMs from a different local directory or Cloud Storage bucket.
-
''')
+
'''),
+ )
+ return
mri_chooser = widgets.Dropdown(
options=[(os.path.basename(mri), mri) for mri in sample_mris],
diff --git a/ml4h/visualization_tools/ecg_interactive_plots.py b/ml4h/visualization_tools/ecg_interactive_plots.py
index 97a4e1547..18ed39a9b 100644
--- a/ml4h/visualization_tools/ecg_interactive_plots.py
+++ b/ml4h/visualization_tools/ecg_interactive_plots.py
@@ -2,10 +2,12 @@
import os
import tempfile
+from typing import Optional, Union
-import altair as alt # Interactive data visualization for plots.
from IPython.display import HTML
-from ml4h.visualization_tools.ecg_reshape import DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME
+import altair as alt # Interactive data visualization for plots.
+from ml4h.TensorMap import TensorMap
+from ml4h.visualization_tools.ecg_reshape import DEFAULT_RESTING_ECG_SIGNAL_TMAP
from ml4h.visualization_tools.ecg_reshape import reshape_exercise_ecg_to_tidy
from ml4h.visualization_tools.ecg_reshape import reshape_resting_ecg_to_tidy
@@ -31,18 +33,21 @@
)
-def resting_ecg_interactive_plot(sample_id, folder=None, tmap_name=DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME):
+def resting_ecg_interactive_plot(
+ sample_id: Union[int, str], folder: Optional[str] = None,
+ tmap: TensorMap = DEFAULT_RESTING_ECG_SIGNAL_TMAP,
+) -> Union[HTML, alt.Chart]:
"""Wrangle resting ECG data to tidy and present it as an interactive plot.
Args:
sample_id: The id of the ECG sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
- tmap_name: The name of the TMAP to use for ecg input.
+ tmap: The TensorMap to use for ECG input.
Returns:
An Altair plot or a notebook-friendly error.
"""
- tidy_resting_ecg_signal = reshape_resting_ecg_to_tidy(sample_id, folder, tmap_name)
+ tidy_resting_ecg_signal = reshape_resting_ecg_to_tidy(sample_id, folder, tmap)
if tidy_resting_ecg_signal.shape[0] == 0:
return HTML(f'''
@@ -85,7 +90,9 @@ def resting_ecg_interactive_plot(sample_id, folder=None, tmap_name=DEFAULT_RESTI
return upper & lower
-def exercise_ecg_interactive_plot(sample_id, folder=None, time_interval_seconds=10):
+def exercise_ecg_interactive_plot(
+ sample_id: Union[int, str], folder: Optional[str] = None, time_interval_seconds: int = 10,
+) -> Union[HTML, alt.Chart]:
"""Wrangle exercise ECG data to tidy and present it as an interactive plot.
Args:
@@ -140,7 +147,8 @@ def exercise_ecg_interactive_plot(sample_id, folder=None, time_interval_seconds=
lead_select,
).transform_filter(
# https://github.com/altair-viz/altair/issues/1960
- f'((toNumber({brush.name}.time) - {time_interval_seconds/2.0}) < datum.time) && (datum.time < toNumber({brush.name}.time) + {time_interval_seconds/2.0})',
+ f'''((toNumber({brush.name}.time) - {time_interval_seconds/2.0}) < datum.time)
+ && (datum.time < toNumber({brush.name}.time) + {time_interval_seconds/2.0})''',
)
return trend.encode(y='heartrate:Q') & trend.encode(y='load:Q') & signal
diff --git a/ml4h/visualization_tools/ecg_reshape.py b/ml4h/visualization_tools/ecg_reshape.py
index b3213d359..167eb5012 100644
--- a/ml4h/visualization_tools/ecg_reshape.py
+++ b/ml4h/visualization_tools/ecg_reshape.py
@@ -1,53 +1,57 @@
"""Methods for reshaping raw ECG signal data for use in the pandas ecosystem."""
import os
import tempfile
+from typing import Any, Dict, Optional, Tuple, Union
+import numpy as np
+import pandas as pd
from biosppy.signals.tools import filter_signal
import h5py
from ml4h.defines import ECG_BIKE_LEADS
from ml4h.defines import ECG_REST_LEADS
from ml4h.runtime_data_defines import get_exercise_ecg_hd5_folder
from ml4h.runtime_data_defines import get_resting_ecg_hd5_folder
-from ml4h.tensor_maps_by_hand import TMAPS
-import numpy as np
-import pandas as pd
+from ml4h.TensorMap import TensorMap
+import ml4h.tensormap.ukb.ecg as ecg_tmaps
import tensorflow as tf
RAW_SCALE = 0.005 # Convert to mV.
SAMPLING_RATE = 500.0
-DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME = 'ecg_rest'
+DEFAULT_RESTING_ECG_SIGNAL_TMAP = ecg_tmaps.ecg_rest
# TODO(deflaux): parameterize exercise ECG by TMAP name if there is similar ECG data from other studies.
-EXERCISE_ECG_SIGNAL_TMAP = TMAPS['ecg-bike-raw-full']
+EXERCISE_ECG_SIGNAL_TMAP = ecg_tmaps.ecg_bike_raw_full
EXERCISE_ECG_TREND_TMAPS = [
- TMAPS['ecg-bike-raw-trend-hr'],
- TMAPS['ecg-bike-raw-trend-load'],
- TMAPS['ecg-bike-raw-trend-grade'],
- TMAPS['ecg-bike-raw-trend-artifact'],
- TMAPS['ecg-bike-raw-trend-mets'],
- TMAPS['ecg-bike-raw-trend-pacecount'],
- TMAPS['ecg-bike-raw-trend-phasename'],
- TMAPS['ecg-bike-raw-trend-phasetime'],
- TMAPS['ecg-bike-raw-trend-time'],
- TMAPS['ecg-bike-raw-trend-vecount'],
+ ecg_tmaps.ecg_bike_raw_trend_hr,
+ ecg_tmaps.ecg_bike_raw_trend_load,
+ ecg_tmaps.ecg_bike_raw_trend_grade,
+ ecg_tmaps.ecg_bike_raw_trend_artifact,
+ ecg_tmaps.ecg_bike_raw_trend_mets,
+ ecg_tmaps.ecg_bike_raw_trend_pacecount,
+ ecg_tmaps.ecg_bike_raw_trend_phasename,
+ ecg_tmaps.ecg_bike_raw_trend_phasetime,
+ ecg_tmaps.ecg_bike_raw_trend_time,
+ ecg_tmaps.ecg_bike_raw_trend_vecount,
]
EXERCISE_PHASES = {0.0: 'Pretest', 1.0: 'Exercise', 2.0: 'Recovery'}
-def _examine_available_keys(hd5):
+def _examine_available_keys(hd5: Dict[str, Any]) -> None:
print(f'hd5 ECG keys {[k for k in hd5.keys() if "ecg" in k]}')
for key in [k for k in hd5.keys() if 'ecg' in k]:
- print(f'hd5 {key} keys {[k for k in hd5[key].keys()]}')
+ print(f'hd5 {key} keys {k for k in hd5[key]}')
-def reshape_resting_ecg_to_tidy(sample_id, folder=None, tmap_name=DEFAULT_RESTING_ECG_SIGNAL_TMAP_NAME):
+def reshape_resting_ecg_to_tidy(
+ sample_id: Union[int, str], folder: Optional[str] = None, tmap: TensorMap = DEFAULT_RESTING_ECG_SIGNAL_TMAP,
+) -> pd.DataFrame:
"""Wrangle resting ECG data to tidy.
Args:
sample_id: The id of the ECG sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
- tmap_name: The name of the TMAP to use for ecg input.
+ tmap: The TensorMap to use for ECG input.
Returns:
A pandas dataframe in tidy format or print a notebook-friendly error and return an empty dataframe.
@@ -55,7 +59,7 @@ def reshape_resting_ecg_to_tidy(sample_id, folder=None, tmap_name=DEFAULT_RESTIN
if folder is None:
folder = get_resting_ecg_hd5_folder(sample_id)
- data = {'lead': [], 'raw': [], 'ts_reference': [], 'filtered': [], 'filtered_1': [], 'filtered_2': []}
+ data: Dict[str, Any] = {'lead': [], 'raw': [], 'ts_reference': [], 'filtered': [], 'filtered_1': [], 'filtered_2': []}
with tempfile.TemporaryDirectory() as tmpdirname:
sample_hd5 = str(sample_id) + '.hd5'
@@ -69,10 +73,10 @@ def reshape_resting_ecg_to_tidy(sample_id, folder=None, tmap_name=DEFAULT_RESTIN
with h5py.File(local_path, mode='r') as hd5:
try:
- signals = TMAPS[tmap_name].tensor_from_file(TMAPS[tmap_name], hd5)
+ signals = tmap.tensor_from_file(tmap, hd5)
except (KeyError, ValueError) as e:
- print(f'''Warning: Resting ECG TMAP {tmap_name} not available for sample {sample_id}.
- Use the tmap_name parameter to choose a different TMAP.\n\n{e}''')
+ print(f'''Warning: Resting ECG TMAP {tmap.name} not available for sample {sample_id}.
+ Use the tmap parameter to choose a different TMAP.\n\n{e}''')
_examine_available_keys(hd5)
return pd.DataFrame(data)
for (lead, channel) in ECG_REST_LEADS.items():
@@ -136,7 +140,9 @@ def reshape_resting_ecg_to_tidy(sample_id, folder=None, tmap_name=DEFAULT_RESTIN
return tidy_signal_df
-def reshape_exercise_ecg_to_tidy(sample_id, folder=None):
+def reshape_exercise_ecg_to_tidy(
+ sample_id: Union[int, str], folder: Optional[str] = None,
+) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""Wrangle exercise ECG signal data to tidy format.
Args:
@@ -208,7 +214,9 @@ def reshape_exercise_ecg_to_tidy(sample_id, folder=None):
return (trend_df, tidy_signal_df)
-def reshape_exercise_ecg_and_trend_to_tidy(sample_id, folder=None):
+def reshape_exercise_ecg_and_trend_to_tidy(
+ sample_id: Union[int, str], folder: Optional[str] = None,
+) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""Wrangle exercise ECG signal and trend data to tidy format.
Args:
diff --git a/ml4h/visualization_tools/ecg_static_plots.py b/ml4h/visualization_tools/ecg_static_plots.py
index 2ebcfc3e1..ac7283237 100644
--- a/ml4h/visualization_tools/ecg_static_plots.py
+++ b/ml4h/visualization_tools/ecg_static_plots.py
@@ -1,17 +1,18 @@
"""Methods for integration of static plots within notebooks."""
import os
import tempfile
+from typing import List, Optional, Union
from IPython.display import HTML
from IPython.display import SVG
+import numpy as np
from ml4h.plots import plot_ecg_rest
from ml4h.runtime_data_defines import get_resting_ecg_hd5_folder
from ml4h.runtime_data_defines import get_resting_ecg_svg_folder
-import numpy as np
import tensorflow as tf
-def display_resting_ecg(sample_id, folder=None):
+def display_resting_ecg(sample_id: Union[int, str], folder: Optional[str] = None) -> Union[HTML, SVG]:
"""Retrieve (or render) and display the SVG of the resting ECG.
Args:
@@ -53,8 +54,8 @@ def display_resting_ecg(sample_id, folder=None):
try:
# We don't need the resulting SVG, so send it to a temporary directory.
with tempfile.TemporaryDirectory() as tmpdirname:
- plot_ecg_rest(tensor_paths = [local_path], rows=[0], out_folder=tmpdirname, is_blind=False)
- except Exception as e:
+ return plot_ecg_rest(tensor_paths=[local_path], rows=[0], out_folder=tmpdirname, is_blind=False)
+ except Exception as e: # pylint: disable=broad-except
return HTML(f'''
Warning: Unable to render static plot of resting ECG for sample {sample_id} from {hd5_folder}:
@@ -62,7 +63,7 @@ def display_resting_ecg(sample_id, folder=None):
''')
-def major_breaks_x_resting_ecg(limits):
+def major_breaks_x_resting_ecg(limits: List[float]) -> np.array:
"""Method to compute breaks for plotnine plots of ECG resting data.
Args:
diff --git a/ml4h/visualization_tools/facets.py b/ml4h/visualization_tools/facets.py
index a45ea88da..18f96327d 100644
--- a/ml4h/visualization_tools/facets.py
+++ b/ml4h/visualization_tools/facets.py
@@ -2,6 +2,7 @@
import base64
import os
+import pandas as pd
from facets_overview.generic_feature_statistics_generator import GenericFeatureStatisticsGenerator
FACETS_DEPENDENCIES = {
@@ -25,10 +26,10 @@
FACETS_DEPENDENCIES[dep] = os.path.basename(url)
-class FacetsOverview(object):
+class FacetsOverview():
"""Methods for Facets Overview notebook integration."""
- def __init__(self, data):
+ def __init__(self, data: pd.DataFrame):
# This takes the dataframe and computes all the inputs to the Facets
# Overview plots such as:
# - numeric variables: histogram bins, mean, min, median, max, etc..
@@ -39,7 +40,7 @@ def __init__(self, data):
[{'name': 'data', 'table': data}],
)
- def _repr_html_(self):
+ def _repr_html_(self) -> str:
"""Html representation of Facets Overview for use in a Jupyter notebook."""
protostr = base64.b64encode(self._proto.SerializeToString()).decode('utf-8')
html_template = '''
@@ -57,14 +58,14 @@ def _repr_html_(self):
return html
-class FacetsDive(object):
+class FacetsDive():
"""Methods for Facets Dive notebook integration."""
- def __init__(self, data, height=1000):
+ def __init__(self, data: pd.DataFrame, height: int = 1000):
self._data = data
self.height = height
- def _repr_html_(self):
+ def _repr_html_(self) -> str:
"""Html representation of Facets Dive for use in a Jupyter notebook."""
html_template = """
diff --git a/ml4h/visualization_tools/hd5_mri_plots.py b/ml4h/visualization_tools/hd5_mri_plots.py
index 20b3305b1..d3894b39d 100644
--- a/ml4h/visualization_tools/hd5_mri_plots.py
+++ b/ml4h/visualization_tools/hd5_mri_plots.py
@@ -1,29 +1,34 @@
"""Methods for integration of plots of mri data processed to 3D tensors from within notebooks."""
+from collections import OrderedDict
from enum import Enum, auto
import os
import tempfile
+from typing import Any, Dict, List, Optional, Tuple, Union
-import h5py
from IPython.display import display
from IPython.display import HTML
+import numpy as np
+import h5py
import ipywidgets as widgets
import matplotlib.pyplot as plt
from ml4h.runtime_data_defines import get_mri_hd5_folder
-from ml4h.tensor_maps_by_hand import TMAPS
-from ml4h.TensorMap import Interpretation
-import numpy as np
+import ml4h.tensormap.ukb.mri as ukb_mri
+import ml4h.tensormap.ukb.mri_vtk as ukb_mri_vtk
+from ml4h.TensorMap import Interpretation, TensorMap
import tensorflow as tf
-# Discover applicable TMAPS.
-CARDIAC_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if ('_lax_' in k or '_sax_' in k) and TMAPS[k].axes() == 3]
-CARDIAC_MRI_TMAP_NAMES.extend(
- [k for k in TMAPS.keys() if TMAPS[k].path_prefix == 'ukb_cardiac_mri' and TMAPS[k].axes() == 3],
+# Discover applicable TensorMaps.
+MRI_TMAPS = {
+ key: value for key, value in ukb_mri.__dict__.items() if isinstance(value, TensorMap)
+ and value.interpretation == Interpretation.CONTINUOUS and value.axes() == 3
+}
+MRI_TMAPS.update(
+ {
+ key: value for key, value in ukb_mri_vtk.__dict__.items()
+ if isinstance(value, TensorMap) and value.interpretation == Interpretation.CONTINUOUS and value.axes() == 3
+ },
)
-LIVER_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if TMAPS[k].path_prefix == 'ukb_liver_mri' and TMAPS[k].axes() == 3]
-BRAIN_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if TMAPS[k].path_prefix == 'ukb_brain_mri' and TMAPS[k].axes() == 3]
-# This includes more than just MRI TMAPS, it is a best effort.
-BEST_EFFORT_MRI_TMAP_NAMES = [k for k in TMAPS.keys() if TMAPS[k].interpretation == Interpretation.CONTINUOUS and TMAPS[k].axes() == 3]
MIN_IMAGE_WIDTH = 8
DEFAULT_IMAGE_WIDTH = 12
@@ -41,42 +46,30 @@ class PlotType(Enum):
class TensorMapCache:
"""Cache the tensor to display for reuse when re-plotting the same TMAP with different plot parameters."""
- def __init__(self, hd5, tmap_name):
+ def __init__(self, hd5: Dict[str, Any], tmap: TensorMap):
self.hd5 = hd5
- self.tmap_name = None
+ self.tmap: Optional[TensorMap] = None
self.tensor = None
- _ = self.get(tmap_name)
+ _ = self.get(tmap)
- def get(self, tmap_name):
- if self.tmap_name != tmap_name:
- self.tensor = TMAPS[tmap_name].tensor_from_file(TMAPS[tmap_name], self.hd5)
- self.tmap_name = tmap_name
+ def get(self, tmap: TensorMap) -> np.array:
+ if self.tmap != tmap:
+ self.tensor = tmap.tensor_from_file(tmap, self.hd5)
+ self.tmap = tmap
return self.tensor
-def choose_cardiac_mri_tmap(sample_id, folder=None, tmap_name='cine_lax_4ch_192', default_tmap_names=CARDIAC_MRI_TMAP_NAMES):
- choose_mri_tmap(sample_id, folder, tmap_name, default_tmap_names)
-
-
-def choose_brain_mri_tmap(sample_id, folder=None, tmap_name='t2_flair_sag_p2_1mm_fs_ellip_pf78_1', default_tmap_names=BRAIN_MRI_TMAP_NAMES):
- choose_mri_tmap(sample_id, folder, tmap_name, default_tmap_names)
-
-
-def choose_liver_mri_tmap(sample_id, folder=None, tmap_name='liver_shmolli_segmented', default_tmap_names=LIVER_MRI_TMAP_NAMES):
- choose_mri_tmap(sample_id, folder, tmap_name, default_tmap_names)
-
-
-def choose_mri_tmap(sample_id, folder=None, tmap_name=None, default_tmap_names=BEST_EFFORT_MRI_TMAP_NAMES):
+def choose_mri_tmap(
+ sample_id: Union[int, str], folder: Optional[str] = None, tmap: Optional[TensorMap] = None,
+ default_tmaps: Dict[str, TensorMap] = MRI_TMAPS,
+) -> None:
"""Render widgets and plots for MRI tensors.
Args:
sample_id: The id of the sample to retrieve.
folder: The local or Cloud Storage folder under which the files reside.
- tmap_name: The TMAP name for the 3D MRI tensor to visualize.
- default_tmap_names: Other TMAP names to offer for visualization, if present in the hd5.
-
- Returns:
- ipywidget or HTML upon error.
+ tmap: The TensorMap for the 3D MRI tensor to visualize.
+ default_tmaps: Other TensorMaps to offer for visualization, if present in the hd5.
"""
if folder is None:
folder = get_mri_hd5_folder(sample_id)
@@ -88,42 +81,45 @@ def choose_mri_tmap(sample_id, folder=None, tmap_name=None, default_tmap_names=B
tf.io.gfile.copy(src=os.path.join(folder, sample_hd5), dst=local_path)
hd5 = h5py.File(local_path, mode='r')
except (tf.errors.NotFoundError, tf.errors.PermissionDeniedError) as e:
- return HTML(f'''
-
+ display(
+ HTML(f'''
Warning: MRI HD5 file not available for sample {sample_id} in folder {folder}:
{e.message}
Use the folder parameter to read HD5s from a different local directory or Cloud Storage bucket.
-
''')
-
- sample_tmap_names = []
- # Add the passed tmap_name parameter, if it is present in this hd5.
- if tmap_name:
- if TMAPS[tmap_name].hd5_key_guess() in hd5:
- if len(TMAPS[tmap_name].shape) == 3:
- sample_tmap_names.append(tmap_name)
+
'''),
+ )
+ return
+
+ sample_tmaps = OrderedDict()
+ # Add the passed tmap parameter, if it is present in this hd5.
+ if tmap:
+ if tmap.hd5_key_guess() in hd5:
+ if len(tmap.shape) == 3:
+ sample_tmaps[tmap.name] = tmap
else:
- print(f'{tmap_name} is not a 3D tensor, skipping it')
+ print(f'{tmap} is not a 3D tensor, skipping it')
else:
- print(f'{tmap_name} is not available in {sample_id}')
- # Also discover applicable TMAPS for this particular sample's HD5 file.
- sample_tmap_names.extend(
- sorted(set([k for k in default_tmap_names if TMAPS[k].hd5_key_guess() in hd5])),
- )
-
- if not sample_tmap_names:
- return HTML(f'''
- Neither {tmap_name} nor any of {default_tmap_names} are present in this HD5 for sample {sample_id} in {folder}.
- Use the tmap_name parameter to try a different TMAP or the folder parameter to try a different hd5 for the sample.
-
''')
-
- default_tmap_name_value = sample_tmap_names[0]
+ print(f'{tmap} is not available in {sample_id}')
+ # Also discover applicable TensorMaps for this particular sample's HD5 file.
+ sample_tmaps.update({n: t for n, t in sorted(default_tmaps.items(), key=lambda t: t[0]) if t.hd5_key_guess() in hd5})
+
+ if not sample_tmaps:
+ display(
+ HTML(f'''
+ Neither {tmap.name} nor any of {default_tmaps.keys()} are present in this HD5 for sample {sample_id} in {folder}.
+ Use the tmap parameter to try a different TensorMap or the folder parameter to try a different hd5 for the sample.
+
'),
- tmap_name_chooser,
+ tmap_chooser,
widgets.HBox([transpose_chooser, fig_width_chooser]),
widgets.HBox([flip_chooser, color_range_chooser]),
widgets.HBox([plot_type_chooser, instance_chooser]),
],
layout=widgets.Layout(width='auto', border='solid 1px grey'),
)
- tmap_cache = TensorMapCache(hd5=hd5, tmap_name=tmap_name_chooser.value)
+ tmap_cache = TensorMapCache(hd5=hd5, tmap=tmap_chooser.value)
viz_controls_output = widgets.interactive_output(
plot_mri_tmap,
{
'sample_id': widgets.fixed(sample_id),
'tmap_cache': widgets.fixed(tmap_cache),
- 'tmap_name': tmap_name_chooser,
+ 'tmap': tmap_chooser,
'plot_type': plot_type_chooser,
'instance': instance_chooser,
'color_range': color_range_chooser,
@@ -209,33 +205,36 @@ def on_plot_type_change(change):
else:
instance_chooser.layout.visibility = 'hidden'
- tmap_name_chooser.observe(on_tmap_value_change, names='value')
+ tmap_chooser.observe(on_tmap_value_change, names='value')
plot_type_chooser.observe(on_plot_type_change, names='value')
display(viz_controls_ui, viz_controls_output)
-def compute_color_range(hd5, tmap_name):
+def compute_color_range(hd5: Dict[str, Any], tmap: TensorMap) -> List[int]:
"""Compute the mean values for the color ranges of instances in the MRI series."""
- mri_tensor = TMAPS[tmap_name].tensor_from_file(TMAPS[tmap_name], hd5)
+ mri_tensor = tmap.tensor_from_file(tmap, hd5)
vmin = np.mean([np.min(mri_tensor[:, :, i]) for i in range(0, mri_tensor.shape[2])])
vmax = np.mean([np.max(mri_tensor[:, :, i]) for i in range(0, mri_tensor.shape[2])])
- return[vmin, vmax]
+ return [vmin, vmax]
-def compute_instance_range(tmap_name):
+def compute_instance_range(tmap: TensorMap) -> Tuple[int, int]:
"""Compute middle and max instances."""
- middle_instance = int(TMAPS[tmap_name].shape[2] / 2)
- max_instance = TMAPS[tmap_name].shape[2]
- return(middle_instance, max_instance)
+ middle_instance = int(tmap.shape[2] / 2)
+ max_instance = tmap.shape[2]
+ return (middle_instance, max_instance)
-def plot_mri_tmap(sample_id, tmap_cache, tmap_name, plot_type, instance, color_range, transpose, flip, fig_width):
+def plot_mri_tmap(
+ sample_id: Union[int, str], tmap_cache: TensorMapCache, tmap: TensorMap, plot_type: PlotType,
+ instance: int, color_range: Tuple[int, int], transpose: bool, flip: bool, fig_width: int,
+) -> None:
"""Visualize the applicable MRI series within this HD5 file.
Args:
sample_id: The local or Cloud Storage path to the MRI file.
tmap_cache: The cache from which to retrieve the tensor to be plotted.
- tmap_name: The name of the chosen TMAP for the MRI series.
+ tmap: The chosen TensorMap for the MRI series.
plot_type: Whether to display instances interactively or in a panel view.
instance: The particular instance to display, if interactive.
color_range: Array of minimum and maximum value for the color range.
@@ -243,12 +242,9 @@ def plot_mri_tmap(sample_id, tmap_cache, tmap_name, plot_type, instance, color_r
flip: Whether to flip the image on its vertical axis
fig_width: The desired width of the figure. Note that height computed as
the proportion of the width based on the data to be plotted.
-
- Returns:
- The plot or a notebook-friendly error message.
"""
- title_prefix = f'{tmap_name} from MRI {sample_id}'
- mri_tensor = tmap_cache.get(tmap_name)
+ title_prefix = f'{tmap.name} from MRI {sample_id}'
+ mri_tensor = tmap_cache.get(tmap)
if plot_type == PlotType.INTERACTIVE:
plot_mri_tensor_as_animation(
mri_tensor=mri_tensor,
@@ -275,10 +271,13 @@ def plot_mri_tmap(sample_id, tmap_cache, tmap_name, plot_type, instance, color_r
title_prefix=title_prefix,
)
else:
- return HTML(f'''