diff --git a/dash-covid-xray/app.py b/dash-covid-xray/app.py deleted file mode 100644 index 83c4e0f..0000000 --- a/dash-covid-xray/app.py +++ /dev/null @@ -1,463 +0,0 @@ -from time import time - -import dash -import dash_bootstrap_components as dbc -import numpy as np -import plotly.express as px -import plotly.graph_objects as go - -from dash import dcc -from dash import html -from dash.dependencies import Input, Output, State -from dash_slicer import VolumeSlicer -from nilearn import image -from scipy import ndimage -from skimage import draw, filters, exposure, measure -from skimage.morphology import ball -from skimage.filters.rank import median -from skimage.util import img_as_ubyte - -external_stylesheets = [dbc.themes.BOOTSTRAP] -app = dash.Dash(__name__, update_title=None, external_stylesheets=external_stylesheets) -server = app.server - - -t1 = time() - -# ------------- I/O and data massaging --------------------------------------------------- - -img = image.load_img("assets/radiopaedia_org_covid-19-pneumonia-7_85703_0-dcm.nii") -mat = img.affine -img = img.get_fdata() -img = np.copy(np.moveaxis(img, -1, 0))[:, ::-1] - -spacing = abs(mat[2, 2]), abs(mat[1, 1]), abs(mat[0, 0]) - -# Create smoothed image and histogram -#ligne coorigée de celle du git -#med_img = filters.median(img, selem=np.ones((1, 3, 3), dtype=np.bool_)) -# Convert the image to 8-bit for rank filters -img = (img - np.min(img)) / (np.max(img) - np.min(img)) * 2 - 1 -img_ubyte = img_as_ubyte(img) -# Use a disk as the structuring element -selem = ball(1) -med_img = median(img_ubyte, selem) -hi = exposure.histogram(med_img) - -# Create mesh -verts, faces, _, _ = measure.marching_cubes(med_img, 200, step_size=5) -x, y, z = verts.T -i, j, k = faces.T -fig_mesh = go.Figure() -fig_mesh.add_trace(go.Mesh3d(x=z, y=y, z=x, opacity=0.2, i=k, j=j, k=i)) - -# Create slicers -slicer1 = VolumeSlicer(app, img, axis=0, spacing=spacing, thumbnail=False) -slicer1.graph.figure.update_layout( - dragmode="drawclosedpath", newshape_line_color="cyan", plot_bgcolor="rgb(0, 0, 0)" -) -slicer1.graph.config.update( - modeBarButtonsToAdd=["drawclosedpath", "eraseshape",] -) - -slicer2 = VolumeSlicer(app, img, axis=1, spacing=spacing, thumbnail=False) -slicer2.graph.figure.update_layout( - dragmode="drawrect", newshape_line_color="cyan", plot_bgcolor="rgb(0, 0, 0)" -) -slicer2.graph.config.update( - modeBarButtonsToAdd=["drawrect", "eraseshape",] -) - - -def path_to_coords(path): - """From SVG path to numpy array of coordinates, each row being a (row, col) point""" - indices_str = [ - el.replace("M", "").replace("Z", "").split(",") for el in path.split("L") - ] - return np.array(indices_str, dtype=float) - - -def largest_connected_component(mask): - labels, _ = ndimage.label(mask) - sizes = np.bincount(labels.ravel())[1:] - return labels == (np.argmax(sizes) + 1) - - -t2 = time() -print("initial calculations", t2 - t1) - -# ------------- Define App Layout --------------------------------------------------- -axial_card = dbc.Card( - [ - dbc.CardHeader("Axial view of the lung"), - dbc.CardBody([slicer1.graph, slicer1.slider, *slicer1.stores], style={'maxHeight': '500px', 'overflowY': 'auto'}), - dbc.CardFooter( - [ - html.H6( - [ - "Step 1: Draw a rough outline that encompasses all ground glass occlusions across ", - html.Span( - "all axial slices", - id="tooltip-target-1", - className="tooltip-target", - ), - ".", - ] - ), - dbc.Tooltip( - "Use the slider to scroll vertically through the image and look for the ground glass occlusions.", - target="tooltip-target-1", - ), - ] - ), - ] -) - -saggital_card = dbc.Card( - [ - dbc.CardHeader("Sagittal view of the lung"), - dbc.CardBody([slicer2.graph, slicer2.slider, *slicer2.stores], style={'maxHeight': '500px', 'overflowY': 'auto'}), - dbc.CardFooter( - [ - html.H6( - [ - "Step 2:\n\nDraw a rectangle to determine the ", - html.Span( - "min and max height ", - id="tooltip-target-2", - className="tooltip-target", - ), - "of the occlusion.", - ] - ), - dbc.Tooltip( - "Only the min and max height of the rectangle are used, the width is ignored", - target="tooltip-target-2", - ), - ] - ), - ] -) - -histogram_card = dbc.Card( - [ - dbc.CardHeader("Histogram of intensity values"), - dbc.CardBody( - [ - dcc.Graph( - id="graph-histogram", - figure=px.bar( - x=hi[1], - y=hi[0], - labels={"x": "intensity", "y": "count"}, - template="plotly_white", - ), - config={ - "modeBarButtonsToAdd": [ - "drawline", - "drawclosedpath", - "drawrect", - "eraseshape", - ] - }, - ), - ] - ), - dbc.CardFooter( - [ - dbc.Toast( - [ - html.P( - "Before you can select value ranges in this histogram, you need to define a region" - " of interest in the slicer views above (step 1 and 2)!", - className="mb-0", - ) - ], - id="roi-warning", - header="Please select a volume of interest first", - icon="danger", - is_open=True, - dismissable=False, - ), - "Step 3: Select a range of values to segment the occlusion. Hover on slices to find the typical " - "values of the occlusion.", - ] - ), - ] -) - -mesh_card = dbc.Card( - [ - dbc.CardHeader("3D mesh representation of the image data and annotation"), - dbc.CardBody([dcc.Graph(id="graph-helper", figure=fig_mesh)]), - ] -) - -# Define Modal -with open("assets/modal.md", "r") as f: - howto_md = f.read() - -modal_overlay = dbc.Modal( - [ - dbc.ModalBody(html.Div([dcc.Markdown(howto_md)], id="howto-md")), - dbc.ModalFooter(dbc.Button("Close", id="howto-close", className="howto-bn")), - ], - id="modal", - size="lg", -) - -# Buttons -button_gh = dbc.Button( - "Learn more", - id="howto-open", - outline=True, - color="secondary", - # Turn off lowercase transformation for class .button in stylesheet - style={"textTransform": "none"}, -) - -button_howto = dbc.Button( - "View Code on github", - outline=True, - color="primary", - href="https://github.com/plotly/dash-sample-apps/tree/master/apps/dash-covid-xray", - id="gh-link", - style={"text-transform": "none"}, -) - -nav_bar = dbc.Navbar( - dbc.Container( - [ - dbc.Row( - [ - dbc.Col( - dbc.Row( - [ - dbc.Col( - html.A( - html.Img( - src=app.get_asset_url("dash-logo-new.png"), - height="30px", - ), - href="https://plotly.com/dash/", - ), - style={"width": "min-content"}, - ), - dbc.Col( - html.Div( - [ - html.H3("Covid X-Ray app"), - html.P( - "Exploration and annotation of CT images" - ), - ], - id="app_title", - ) - ), - ], - align="center", - style={"display": "inline-flex"}, - ) - ), - dbc.Col( - [ - dbc.NavbarToggler(id="navbar-toggler"), - dbc.Collapse( - dbc.Nav( - [dbc.NavItem(button_howto), dbc.NavItem(button_gh)], - className="ml-auto", - navbar=True, - ), - id="navbar-collapse", - navbar=True, - ), - ] - ), - modal_overlay, - ], - align="center", - style={"width": "100%"}, - ), - ], - fluid=True, - ), - color="dark", - dark=True, -) - - -app.layout = html.Div( - [ - nav_bar, - dbc.Container( - [ - dbc.Row([dbc.Col(axial_card), dbc.Col(saggital_card)]), - dbc.Row([dbc.Col(histogram_card), dbc.Col(mesh_card),]), - ], - fluid=True, - ), - dcc.Store(id="annotations", data={}), - dcc.Store(id="occlusion-surface", data={}), - ], -) - -t3 = time() -print("layout definition", t3 - t2) - - -# ------------- Define App Interactivity --------------------------------------------------- -@app.callback( - [Output("graph-histogram", "figure"), Output("roi-warning", "is_open")], - [Input("annotations", "data")], -) -def update_histo(annotations): - if ( - annotations is None - or annotations.get("x") is None - or annotations.get("z") is None - ): - return dash.no_update, dash.no_update - # Horizontal mask for the xy plane (z-axis) - path = path_to_coords(annotations["z"]["path"]) - rr, cc = draw.polygon(path[:, 1] / spacing[1], path[:, 0] / spacing[2]) - if len(rr) == 0 or len(cc) == 0: - return dash.no_update, dash.no_update - mask = np.zeros(img.shape[1:]) - mask[rr, cc] = 1 - mask = ndimage.binary_fill_holes(mask) - # top and bottom, the top is a lower number than the bottom because y values - # increase moving down the figure - top, bottom = sorted([int(annotations["x"][c] / spacing[0]) for c in ["y0", "y1"]]) - intensities = med_img[top:bottom, mask].ravel() - if len(intensities) == 0: - return dash.no_update, dash.no_update - hi = exposure.histogram(intensities) - fig = px.bar( - x=hi[1], - y=hi[0], - # Histogram - labels={"x": "intensity", "y": "count"}, - ) - fig.update_layout(dragmode="select", title_font=dict(size=20, color="blue")) - return fig, False - - -@app.callback( - [ - Output("occlusion-surface", "data"), - Output(slicer1.overlay_data.id, "data"), - Output(slicer2.overlay_data.id, "data"), - ], - [Input("graph-histogram", "selectedData"), Input("annotations", "data")], -) -def update_segmentation_slices(selected, annotations): - ctx = dash.callback_context - # When shape annotations are changed, reset segmentation visualization - if ( - ctx.triggered[0]["prop_id"] == "annotations.data" - or annotations is None - or annotations.get("x") is None - or annotations.get("z") is None - ): - mask = np.zeros_like(med_img) - overlay1 = slicer1.create_overlay_data(mask) - overlay2 = slicer2.create_overlay_data(mask) - return go.Mesh3d(), overlay1, overlay2 - elif selected is not None and "range" in selected: - if len(selected["points"]) == 0: - return dash.no_update - v_min, v_max = selected["range"]["x"] - t_start = time() - # Horizontal mask - path = path_to_coords(annotations["z"]["path"]) - rr, cc = draw.polygon(path[:, 1] / spacing[1], path[:, 0] / spacing[2]) - mask = np.zeros(img.shape[1:]) - mask[rr, cc] = 1 - mask = ndimage.binary_fill_holes(mask) - # top and bottom, the top is a lower number than the bottom because y values - # increase moving down the figure - top, bottom = sorted( - [int(annotations["x"][c] / spacing[0]) for c in ["y0", "y1"]] - ) - img_mask = np.logical_and(med_img > v_min, med_img <= v_max) - img_mask[:top] = False - img_mask[bottom:] = False - img_mask[top:bottom, np.logical_not(mask)] = False - img_mask = largest_connected_component(img_mask) - # img_mask_color = mask_to_color(img_mask) - t_end = time() - print("build the mask", t_end - t_start) - t_start = time() - # Update 3d viz - verts, faces, _, _ = measure.marching_cubes( - filters.median(img_mask, selem=np.ones((1, 7, 7))), 0.5, step_size=3 - ) - t_end = time() - print("marching cubes", t_end - t_start) - x, y, z = verts.T - i, j, k = faces.T - trace = go.Mesh3d(x=z, y=y, z=x, color="red", opacity=0.8, i=k, j=j, k=i) - overlay1 = slicer1.create_overlay_data(img_mask) - overlay2 = slicer2.create_overlay_data(img_mask) - # todo: do we need an output to trigger an update? - return trace, overlay1, overlay2 - else: - return (dash.no_update,) * 3 - - -@app.callback( - Output("annotations", "data"), - [Input(slicer1.graph.id, "relayoutData"), Input(slicer2.graph.id, "relayoutData"),], - [State("annotations", "data")], -) -def update_annotations(relayout1, relayout2, annotations): - if relayout1 is not None and "shapes" in relayout1: - if len(relayout1["shapes"]) >= 1: - shape = relayout1["shapes"][-1] - annotations["z"] = shape - else: - annotations.pop("z", None) - elif relayout1 is not None and "shapes[2].path" in relayout1: - annotations["z"]["path"] = relayout1["shapes[2].path"] - - if relayout2 is not None and "shapes" in relayout2: - if len(relayout2["shapes"]) >= 1: - shape = relayout2["shapes"][-1] - annotations["x"] = shape - else: - annotations.pop("x", None) - elif relayout2 is not None and ( - "shapes[2].y0" in relayout2 or "shapes[2].y1" in relayout2 - ): - annotations["x"]["y0"] = relayout2["shapes[2].y0"] - annotations["x"]["y1"] = relayout2["shapes[2].y1"] - return annotations - - -app.clientside_callback( - """ -function(surf, fig){ - let fig_ = {...fig}; - fig_.data[1] = surf; - return fig_; - } -""", - output=Output("graph-helper", "figure"), - inputs=[Input("occlusion-surface", "data"),], - state=[State("graph-helper", "figure"),], -) - - -@app.callback( - Output("modal", "is_open"), - [Input("howto-open", "n_clicks"), Input("howto-close", "n_clicks")], - [State("modal", "is_open")], -) -def toggle_modal(n1, n2, is_open): - if n1 or n2: - return not is_open - return is_open - - -if __name__ == "__main__": - app.run_server(debug=True, dev_tools_props_check=False) \ No newline at end of file diff --git a/dash-covid-xray/assets/dash-logo-new.png b/dash-covid-xray/assets/dash-logo-new.png deleted file mode 100644 index eb700fc..0000000 Binary files a/dash-covid-xray/assets/dash-logo-new.png and /dev/null differ diff --git a/dash-covid-xray/assets/modal.md b/dash-covid-xray/assets/modal.md deleted file mode 100644 index ac98295..0000000 --- a/dash-covid-xray/assets/modal.md +++ /dev/null @@ -1,35 +0,0 @@ -## What this app can do - -This app displays 3-D chest tomography data of a patient with Covid-19. The imaging data show -[Ground Glass Opacities (GGO) in the lung](https://en.wikipedia.org/wiki/Ground-glass_opacity#COVID-19), a common imaging -finding among Covid-19 patients. The purpose of this app is to extract the spatial extent of the GGOs in the imaging -data for the -[eventual purpose of training machine learning models](https://eoss-image-processing.github.io/2020/12/16/ct-app.html) -to do this automatically. -The imaging data in this app are displayed with [`dash-slicer`](https://dash.plotly.com/slicer), a Dash component that -provides interactive slicing views of volumetric data. The data used in this app come from the open dataset of -the [COVID-19 image data collection](https://github.com/ieee8023/covid-chestxray-dataset). - -## How to use this app -With this app you can: - -- Interactively explore the 3-D chest tomography data with two linked `dash-slicer` viewers. Note that the position of - the left (axial) slice of the lung is displayed with a blue line in the right (sagittal) slice of the lung. Similarly, - the position of the right slice is displayed with an orange line in the left viewer. -- Draw an outline of the GGOs on the axial viewer. Make sure that this outline encompasses all GGO across all of the - axial slices. -- Indicate the vertical span of GGOs in the image by drawing a rectangle on the saggital viewer. -- Selecting a range of image intensity values that reflect GGOs in the outlined region of interest by drawing a - selection in the histogram. Note that you first need to define the region of interest by drawing the outline and - height of GGOs in the axial and sagittal viewers. - -## Where to learn more -If you want to learn how to build apps like this, check out: -- [Dash by plotly](https://plotly.com/dash/) for a python based framework to build powerful dashboard apps -- [Dash Slicer](https://dash.plotly.com/slicer) to learn more on how to make interactive 3D image slicers in Dash -- [scikit-image](https://scikit-image.org/docs/stable/user_guide.html) for a scientific image processing python library -- [this app on github](https://github.com/plotly/dash-sample-apps/tree/master/apps/dash-covid-xray) to check out the code used to make this web app. -- [our blog post on this app](https://eoss-image-processing.github.io/2020/12/16/ct-app.html) - - - diff --git a/dash-covid-xray/assets/radiopaedia_org_covid-19-pneumonia-7_85703_0-dcm.nii b/dash-covid-xray/assets/radiopaedia_org_covid-19-pneumonia-7_85703_0-dcm.nii deleted file mode 100644 index fd351b7..0000000 Binary files a/dash-covid-xray/assets/radiopaedia_org_covid-19-pneumonia-7_85703_0-dcm.nii and /dev/null differ diff --git a/dash-covid-xray/assets/style-covid.css b/dash-covid-xray/assets/style-covid.css deleted file mode 100644 index 2c93a40..0000000 --- a/dash-covid-xray/assets/style-covid.css +++ /dev/null @@ -1,18 +0,0 @@ -.card { - margin-top: 1em; - height: 60vh; -} - -div#app_title { - min-width: max-content; - color: white; -} - -.tooltip-target { - text-decoration: underline; - cursor: pointer; -} - -.dash-graph { - height: 100%; -}