From 299fb55951d37568d4bcd266c2fda7145dd08c30 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Wed, 1 Jun 2022 16:09:30 -0400 Subject: [PATCH 1/5] refactor --- apps/dash-image-segmentation/README.md | 2 +- apps/dash-image-segmentation/app.py | 609 ++---------------- .../assets/css/app.css | 61 ++ .../assets/{ => css}/segmentation-style.css | 0 .../assets/{ => github}/screenshot.png | Bin .../assets/{ => images}/dash-logo-new.png | Bin .../assets/images/plotly-logo-dark-theme.png | Bin 0 -> 23021 bytes .../assets/{ => images}/segmentation_img.jpg | Bin .../segmentation_img_example_marks.jpg | Bin apps/dash-image-segmentation/constants.py | 28 + apps/dash-image-segmentation/gitignore | 191 ++++++ apps/dash-image-segmentation/requirements.txt | 18 +- apps/dash-image-segmentation/runtime.txt | 1 + .../utils/components.py | 348 ++++++++++ apps/dash-image-segmentation/utils/figures.py | 128 ++++ .../utils/helper_functions.py | 85 +++ .../{ => utils}/plot_common.py | 0 .../{ => utils}/shape_utils.py | 0 .../{ => utils}/shapes_to_segmentations.py | 4 +- .../{ => utils}/trainable_segmentation.py | 0 .../use_ml_image_segmentation_classifier.py | 6 +- 21 files changed, 912 insertions(+), 569 deletions(-) create mode 100644 apps/dash-image-segmentation/assets/css/app.css rename apps/dash-image-segmentation/assets/{ => css}/segmentation-style.css (100%) rename apps/dash-image-segmentation/assets/{ => github}/screenshot.png (100%) rename apps/dash-image-segmentation/assets/{ => images}/dash-logo-new.png (100%) create mode 100644 apps/dash-image-segmentation/assets/images/plotly-logo-dark-theme.png rename apps/dash-image-segmentation/assets/{ => images}/segmentation_img.jpg (100%) rename apps/dash-image-segmentation/assets/{ => images}/segmentation_img_example_marks.jpg (100%) create mode 100644 apps/dash-image-segmentation/constants.py create mode 100644 apps/dash-image-segmentation/gitignore create mode 100644 apps/dash-image-segmentation/runtime.txt create mode 100644 apps/dash-image-segmentation/utils/components.py create mode 100644 apps/dash-image-segmentation/utils/figures.py create mode 100644 apps/dash-image-segmentation/utils/helper_functions.py rename apps/dash-image-segmentation/{ => utils}/plot_common.py (100%) rename apps/dash-image-segmentation/{ => utils}/shape_utils.py (100%) rename apps/dash-image-segmentation/{ => utils}/shapes_to_segmentations.py (97%) rename apps/dash-image-segmentation/{ => utils}/trainable_segmentation.py (100%) rename apps/dash-image-segmentation/{ => utils}/use_ml_image_segmentation_classifier.py (92%) diff --git a/apps/dash-image-segmentation/README.md b/apps/dash-image-segmentation/README.md index e7bf7001b..313051f79 100644 --- a/apps/dash-image-segmentation/README.md +++ b/apps/dash-image-segmentation/README.md @@ -37,7 +37,7 @@ python app.py ## Screenshot -![Screenshot of app](assets/screenshot.png) +![Screenshot of app](assets/github/screenshot.png) ## Acknowledgements diff --git a/apps/dash-image-segmentation/app.py b/apps/dash-image-segmentation/app.py index 155f95fb1..9cd08dcf1 100644 --- a/apps/dash-image-segmentation/app.py +++ b/apps/dash-image-segmentation/app.py @@ -1,424 +1,37 @@ import plotly.express as px import dash -from dash.dependencies import Input, Output, State -import dash_html_components as html -import dash_core_components as dcc +from dash import Dash, html, dcc, Input, Output, State, callback, callback_context import dash_bootstrap_components as dbc -import plot_common -import json -from shapes_to_segmentations import ( - compute_segmentations, - blend_image_and_classified_regions_pil, -) -from skimage import io as skio -from trainable_segmentation import multiscale_basic_features -import io -import base64 -import PIL.Image -import pickle -from time import time -from joblib import Memory - -memory = Memory("./joblib_cache", bytes_limit=3000000000, verbose=3) - -compute_features = memory.cache(multiscale_basic_features) - -DEFAULT_STROKE_WIDTH = 3 # gives line width of 2^3 = 8 - -DEFAULT_IMAGE_PATH = "assets/segmentation_img.jpg" - -SEG_FEATURE_TYPES = ["intensity", "edges", "texture"] - -# the number of different classes for labels -NUM_LABEL_CLASSES = 5 -DEFAULT_LABEL_CLASS = 0 -class_label_colormap = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2"] -class_labels = list(range(NUM_LABEL_CLASSES)) -# we can't have less colors than classes -assert NUM_LABEL_CLASSES <= len(class_label_colormap) - -# Font and background colors associated with each theme -text_color = {"dark": "#95969A", "light": "#595959"} -card_color = {"dark": "#2D3038", "light": "#FFFFFF"} - - -def class_to_color(n): - return class_label_colormap[n] - - -def color_to_class(c): - return class_label_colormap.index(c) +from time import time -img = skio.imread(DEFAULT_IMAGE_PATH) -features_dict = {} +from utils.helper_functions import ( + class_to_color, + color_to_class, + shapes_to_key, + store_shapes_seg_pair, + look_up_seg, + save_img_classifier, + show_segmentation, +) +from utils.figures import make_default_figure, annotation_react +from utils.components import ( + dbc_header, + description, + segmentation, + sidebar, + meta, +) -external_stylesheets = [dbc.themes.BOOTSTRAP, "assets/segmentation-style.css"] +external_stylesheets = [dbc.themes.BOOTSTRAP, "assets/css/segmentation-style.css"] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) server = app.server app.title = "Interactive image segmentation based on machine learning" - -def make_default_figure( - images=[DEFAULT_IMAGE_PATH], - stroke_color=class_to_color(DEFAULT_LABEL_CLASS), - stroke_width=DEFAULT_STROKE_WIDTH, - shapes=[], -): - fig = plot_common.dummy_fig() - plot_common.add_layout_images_to_fig(fig, images) - fig.update_layout( - { - "dragmode": "drawopenpath", - "shapes": shapes, - "newshape.line.color": stroke_color, - "newshape.line.width": stroke_width, - "margin": dict(l=0, r=0, b=0, t=0, pad=4), - } - ) - return fig - - -def shapes_to_key(shapes): - return json.dumps(shapes) - - -def store_shapes_seg_pair(d, key, seg, remove_old=True): - """ - Stores shapes and segmentation pair in dict d - seg is a PIL.Image object - if remove_old True, deletes all the old keys and values. - """ - bytes_to_encode = io.BytesIO() - seg.save(bytes_to_encode, format="png") - bytes_to_encode.seek(0) - data = base64.b64encode(bytes_to_encode.read()).decode() - if remove_old: - return {key: data} - d[key] = data - return d - - -def look_up_seg(d, key): - """ Returns a PIL.Image object """ - data = d[key] - img_bytes = base64.b64decode(data) - img = PIL.Image.open(io.BytesIO(img_bytes)) - return img - - -# Modal -with open("explanations.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", -) - -button_howto = dbc.Button( - "Learn more", - id="howto-open", - outline=True, - color="info", - # Turn off lowercase transformation for class .button in stylesheet - style={"textTransform": "none"}, -) - -button_github = dbc.Button( - "View Code on github", - outline=True, - color="primary", - href="https://github.com/plotly/dash-sample-apps/tree/master/apps/dash-image-segmentation", - id="gh-link", - style={"text-transform": "none"}, -) - -# Header -header = dbc.Navbar( - dbc.Container( - [ - dbc.Row( - [ - dbc.Col( - html.Img( - id="logo", - src=app.get_asset_url("dash-logo-new.png"), - height="30px", - ), - md="auto", - ), - dbc.Col( - [ - html.Div( - [ - html.H3("Interactive Machine Learning"), - html.P("Image segmentation"), - ], - id="app-title", - ) - ], - md=True, - align="center", - ), - ], - align="center", - ), - dbc.Row( - [ - dbc.Col( - [ - dbc.NavbarToggler(id="navbar-toggler"), - dbc.Collapse( - dbc.Nav( - [ - dbc.NavItem(button_howto), - dbc.NavItem(button_github), - ], - navbar=True, - ), - id="navbar-collapse", - navbar=True, - ), - modal_overlay, - ], - md=2, - ), - ], - align="center", - ), - ], - fluid=True, - ), - dark=True, - color="dark", - sticky="top", -) - -# Description -description = dbc.Col( - [ - dbc.Card( - id="description-card", - children=[ - dbc.CardHeader("Explanation"), - dbc.CardBody( - [ - dbc.Row( - [ - dbc.Col( - [ - html.Img( - src="assets/segmentation_img_example_marks.jpg", - width="200px", - ) - ], - md="auto", - ), - dbc.Col( - html.P( - "This is an example of interactive machine learning for image classification. " - "To train the classifier, draw some marks on the picture using different colors for " - 'different parts, like in the example image. Then enable "Show segmentation" to see the ' - "classes a Random Forest Classifier gave to regions of the image, based on the marks you " - "used as a guide. You may add more marks to clarify parts of the image where the " - "classifier was not successful and the classification will update." - ), - md=True, - ), - ] - ), - ] - ), - ], - ) - ], - md=12, -) - -# Image Segmentation -segmentation = [ - dbc.Card( - id="segmentation-card", - children=[ - dbc.CardHeader("Viewer"), - dbc.CardBody( - [ - # Wrap dcc.Loading in a div to force transparency when loading - html.Div( - id="transparent-loader-wrapper", - children=[ - dcc.Loading( - id="segmentations-loading", - type="circle", - children=[ - # Graph - dcc.Graph( - id="graph", - figure=make_default_figure(), - config={ - "modeBarButtonsToAdd": [ - "drawrect", - "drawopenpath", - "eraseshape", - ] - }, - ), - ], - ) - ], - ), - ] - ), - dbc.CardFooter( - [ - # Download links - html.A(id="download", download="classifier.json",), - html.Div( - children=[ - dbc.ButtonGroup( - [ - dbc.Button( - "Download classified image", - id="download-image-button", - outline=True, - ), - dbc.Button( - "Download classifier", - id="download-button", - outline=True, - ), - ], - size="lg", - style={"width": "100%"}, - ), - ], - ), - html.A(id="download-image", download="classified-image.png",), - ] - ), - ], - ) -] - -# sidebar -sidebar = [ - dbc.Card( - id="sidebar-card", - children=[ - dbc.CardHeader("Tools"), - dbc.CardBody( - [ - html.H6("Label class", className="card-title"), - # Label class chosen with buttons - html.Div( - id="label-class-buttons", - children=[ - dbc.Button( - "%2d" % (n,), - id={"type": "label-class-button", "index": n}, - style={"background-color": class_to_color(c)}, - ) - for n, c in enumerate(class_labels) - ], - ), - html.Hr(), - dbc.Form( - [ - dbc.FormGroup( - [ - dbc.Label( - "Width of annotation paintbrush", - html_for="stroke-width", - ), - # Slider for specifying stroke width - dcc.Slider( - id="stroke-width", - min=0, - max=6, - step=0.1, - value=DEFAULT_STROKE_WIDTH, - ), - ] - ), - dbc.FormGroup( - [ - html.H6( - id="stroke-width-display", - className="card-title", - ), - dbc.Label( - "Blurring parameter", - html_for="sigma-range-slider", - ), - dcc.RangeSlider( - id="sigma-range-slider", - min=0.01, - max=20, - step=0.01, - value=[0.5, 16], - ), - ] - ), - dbc.FormGroup( - [ - dbc.Label( - "Select features", - html_for="segmentation-features", - ), - dcc.Checklist( - id="segmentation-features", - options=[ - {"label": l.capitalize(), "value": l} - for l in SEG_FEATURE_TYPES - ], - value=["intensity", "edges"], - ), - ] - ), - # Indicate showing most recently computed segmentation - dcc.Checklist( - id="show-segmentation", - options=[ - { - "label": "Show segmentation", - "value": "Show segmentation", - } - ], - value=[], - ), - ] - ), - ] - ), - ], - ), -] - -meta = [ - html.Div( - id="no-display", - children=[ - # Store for user created masks - # data is a list of dicts describing shapes - dcc.Store(id="masks", data={"shapes": []}), - dcc.Store(id="classifier-store", data={}), - dcc.Store(id="classified-image-store", data=""), - dcc.Store(id="features_hash", data=""), - ], - ), - html.Div(id="download-dummy"), - html.Div(id="download-image-dummy"), -] - app.layout = html.Div( [ - header, + dbc_header, dbc.Container( [ dbc.Row(description), @@ -434,65 +47,26 @@ def look_up_seg(d, key): ) -# Converts image classifier to a JSON compatible encoding and creates a -# dictionary that can be downloaded -# see use_ml_image_segmentation_classifier.py -def save_img_classifier(clf, label_to_colors_args, segmenter_args): - clfbytes = io.BytesIO() - pickle.dump(clf, clfbytes) - clfb64 = base64.b64encode(clfbytes.getvalue()).decode() - return { - "classifier": clfb64, - "segmenter_args": segmenter_args, - "label_to_colors_args": label_to_colors_args, - } - - -def show_segmentation(image_path, mask_shapes, features, segmenter_args): - """ adds an image showing segmentations to a figure's layout """ - # add 1 because classifier takes 0 to mean no mask - shape_layers = [color_to_class(shape["line"]["color"]) + 1 for shape in mask_shapes] - label_to_colors_args = { - "colormap": class_label_colormap, - "color_class_offset": -1, - } - segimg, _, clf = compute_segmentations( - mask_shapes, - img_path=image_path, - shape_layers=shape_layers, - label_to_colors_args=label_to_colors_args, - features=features, - ) - # get the classifier that we can later store in the Store - classifier = save_img_classifier(clf, label_to_colors_args, segmenter_args) - segimgpng = plot_common.img_array_to_pil_image(segimg) - return (segimgpng, classifier) - - -@app.callback( - [ - Output("graph", "figure"), - Output("masks", "data"), - Output("stroke-width-display", "children"), - Output("classifier-store", "data"), - Output("classified-image-store", "data"), - ], - [ - Input("graph", "relayoutData"), - Input( - {"type": "label-class-button", "index": dash.dependencies.ALL}, - "n_clicks_timestamp", - ), - Input("stroke-width", "value"), - Input("show-segmentation", "value"), - Input("download-button", "n_clicks"), - Input("download-image-button", "n_clicks"), - Input("segmentation-features", "value"), - Input("sigma-range-slider", "value"), - ], - [State("masks", "data"),], +@callback( + Output("graph", "figure"), + Output("masks", "data"), + Output("stroke-width-display", "children"), + Output("classifier-store", "data"), + Output("classified-image-store", "data"), + Input("graph", "relayoutData"), + Input( + {"type": "label-class-button", "index": dash.dependencies.ALL}, + "n_clicks_timestamp", + ), + Input("stroke-width", "value"), + Input("show-segmentation", "value"), + Input("download-button", "n_clicks"), + Input("download-image-button", "n_clicks"), + Input("segmentation-features", "value"), + Input("sigma-range-slider", "value"), + State("masks", "data"), ) -def annotation_react( +def return_annotation_react( graph_relayoutData, any_label_class_button_value, stroke_width_value, @@ -503,83 +77,16 @@ def annotation_react( sigma_range_slider_value, masks_data, ): - classified_image_store_data = dash.no_update - classifier_store_data = dash.no_update - cbcontext = [p["prop_id"] for p in dash.callback_context.triggered][0] - if cbcontext in ["segmentation-features.value", "sigma-range-slider.value"] or ( - ("Show segmentation" in show_segmentation_value) - and (len(masks_data["shapes"]) > 0) - ): - segmentation_features_dict = { - "intensity": False, - "edges": False, - "texture": False, - } - for feat in segmentation_features_value: - segmentation_features_dict[feat] = True - t1 = time() - features = compute_features( - img, - **segmentation_features_dict, - sigma_min=sigma_range_slider_value[0], - sigma_max=sigma_range_slider_value[1], - ) - t2 = time() - print(t2 - t1) - if cbcontext == "graph.relayoutData": - if "shapes" in graph_relayoutData.keys(): - masks_data["shapes"] = graph_relayoutData["shapes"] - else: - return dash.no_update - stroke_width = int(round(2 ** (stroke_width_value))) - # find label class value by finding button with the most recent click - if any_label_class_button_value is None: - label_class_value = DEFAULT_LABEL_CLASS - else: - label_class_value = max( - enumerate(any_label_class_button_value), - key=lambda t: 0 if t[1] is None else t[1], - )[0] - - fig = make_default_figure( - stroke_color=class_to_color(label_class_value), - stroke_width=stroke_width, - shapes=masks_data["shapes"], - ) - # We want the segmentation to be computed - if ("Show segmentation" in show_segmentation_value) and ( - len(masks_data["shapes"]) > 0 - ): - segimgpng = None - try: - feature_opts = dict(segmentation_features_dict=segmentation_features_dict) - feature_opts["sigma_min"] = sigma_range_slider_value[0] - feature_opts["sigma_max"] = sigma_range_slider_value[1] - segimgpng, clf = show_segmentation( - DEFAULT_IMAGE_PATH, masks_data["shapes"], features, feature_opts - ) - if cbcontext == "download-button.n_clicks": - classifier_store_data = clf - if cbcontext == "download-image-button.n_clicks": - classified_image_store_data = plot_common.pil_image_to_uri( - blend_image_and_classified_regions_pil( - PIL.Image.open(DEFAULT_IMAGE_PATH), segimgpng - ) - ) - except ValueError: - # if segmentation fails, draw nothing - pass - images_to_draw = [] - if segimgpng is not None: - images_to_draw = [segimgpng] - fig = plot_common.add_layout_images_to_fig(fig, images_to_draw) - fig.update_layout(uirevision="segmentation") - return ( - fig, + return annotation_react( + graph_relayoutData, + any_label_class_button_value, + stroke_width_value, + show_segmentation_value, + download_button_n_clicks, + download_image_button_n_clicks, + segmentation_features_value, + sigma_range_slider_value, masks_data, - "Current paintbrush width: %d" % (stroke_width,), - classifier_store_data, - classified_image_store_data, ) @@ -608,7 +115,7 @@ def annotation_react( } """, Output("download-image", "href"), - [Input("classified-image-store", "data")], + Input("classified-image-store", "data"), ) # simulate a click on the element when download.href is updated @@ -634,15 +141,15 @@ def annotation_react( } """, Output("download-image-dummy", "children"), - [Input("download-image", "href")], + Input("download-image", "href"), ) - # Callback for modal popup -@app.callback( +@callback( Output("modal", "is_open"), - [Input("howto-open", "n_clicks"), Input("howto-close", "n_clicks")], - [State("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: @@ -651,10 +158,10 @@ def toggle_modal(n1, n2, is_open): # we use a callback to toggle the collapse on small screens -@app.callback( +@callback( Output("navbar-collapse", "is_open"), - [Input("navbar-toggler", "n_clicks")], - [State("navbar-collapse", "is_open")], + Input("navbar-toggler", "n_clicks"), + State("navbar-collapse", "is_open"), ) def toggle_navbar_collapse(n, is_open): if n: diff --git a/apps/dash-image-segmentation/assets/css/app.css b/apps/dash-image-segmentation/assets/css/app.css new file mode 100644 index 000000000..8b3802ab0 --- /dev/null +++ b/apps/dash-image-segmentation/assets/css/app.css @@ -0,0 +1,61 @@ +/* Header */ +.header { + height: 10vh; + display: flex; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display, sans-serif; + font-weight: bold; +} + +.header .header-title { + font-size: 5vh; +} +.subheader-title { + font-size: 1.5vh; +} + +.header-logos { + margin-left: auto; +} +.header-logos img { + margin-left: 3vh !important; + max-height: 5vh; +} + + +/* Demo button css */ +.demo-button { + font-size: 1.5vh; + font-family: Open Sans, sans-serif; + text-decoration: none; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 1.5px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 1vh; + padding-bottom: 1vh; + vertical-align: super; +} + +.demo-button:hover { + color: #7A76FF; + background-position: 0%; +} \ No newline at end of file diff --git a/apps/dash-image-segmentation/assets/segmentation-style.css b/apps/dash-image-segmentation/assets/css/segmentation-style.css similarity index 100% rename from apps/dash-image-segmentation/assets/segmentation-style.css rename to apps/dash-image-segmentation/assets/css/segmentation-style.css diff --git a/apps/dash-image-segmentation/assets/screenshot.png b/apps/dash-image-segmentation/assets/github/screenshot.png similarity index 100% rename from apps/dash-image-segmentation/assets/screenshot.png rename to apps/dash-image-segmentation/assets/github/screenshot.png diff --git a/apps/dash-image-segmentation/assets/dash-logo-new.png b/apps/dash-image-segmentation/assets/images/dash-logo-new.png similarity index 100% rename from apps/dash-image-segmentation/assets/dash-logo-new.png rename to apps/dash-image-segmentation/assets/images/dash-logo-new.png diff --git a/apps/dash-image-segmentation/assets/images/plotly-logo-dark-theme.png b/apps/dash-image-segmentation/assets/images/plotly-logo-dark-theme.png new file mode 100644 index 0000000000000000000000000000000000000000..984dd57ab53b7f9fa47df219e25df91a0885613d GIT binary patch literal 23021 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 0) + ): + segmentation_features_dict = { + "intensity": False, + "edges": False, + "texture": False, + } + for feat in segmentation_features_value: + segmentation_features_dict[feat] = True + t1 = time() + features = compute_features( + img, + **segmentation_features_dict, + sigma_min=sigma_range_slider_value[0], + sigma_max=sigma_range_slider_value[1], + ) + t2 = time() + print(t2 - t1) + if cbcontext == "graph.relayoutData": + if "shapes" in graph_relayoutData.keys(): + masks_data["shapes"] = graph_relayoutData["shapes"] + else: + return dash.no_update + stroke_width = int(round(2 ** (stroke_width_value))) + # find label class value by finding button with the most recent click + if any_label_class_button_value is None: + label_class_value = DEFAULT_LABEL_CLASS + else: + label_class_value = max( + enumerate(any_label_class_button_value), + key=lambda t: 0 if t[1] is None else t[1], + )[0] + + fig = make_default_figure( + stroke_color=class_to_color(label_class_value), + stroke_width=stroke_width, + shapes=masks_data["shapes"], + ) + # We want the segmentation to be computed + if ("Show segmentation" in show_segmentation_value) and ( + len(masks_data["shapes"]) > 0 + ): + segimgpng = None + try: + feature_opts = dict(segmentation_features_dict=segmentation_features_dict) + feature_opts["sigma_min"] = sigma_range_slider_value[0] + feature_opts["sigma_max"] = sigma_range_slider_value[1] + segimgpng, clf = show_segmentation( + DEFAULT_IMAGE_PATH, masks_data["shapes"], features, feature_opts + ) + if cbcontext == "download-button.n_clicks": + classifier_store_data = clf + if cbcontext == "download-image-button.n_clicks": + classified_image_store_data = plot_common.pil_image_to_uri( + blend_image_and_classified_regions_pil( + PIL.Image.open(DEFAULT_IMAGE_PATH), segimgpng + ) + ) + except ValueError: + # if segmentation fails, draw nothing + pass + images_to_draw = [] + if segimgpng is not None: + images_to_draw = [segimgpng] + fig = plot_common.add_layout_images_to_fig(fig, images_to_draw) + fig.update_layout(uirevision="segmentation") + return ( + fig, + masks_data, + "Current paintbrush width: %d" % (stroke_width,), + classifier_store_data, + classified_image_store_data, + ) diff --git a/apps/dash-image-segmentation/utils/helper_functions.py b/apps/dash-image-segmentation/utils/helper_functions.py new file mode 100644 index 000000000..167c93d3b --- /dev/null +++ b/apps/dash-image-segmentation/utils/helper_functions.py @@ -0,0 +1,85 @@ +import base64 +import PIL.Image +import json +import utils.plot_common as plot_common +import io +import pickle + + +from constants import class_label_colormap + +from utils.shapes_to_segmentations import ( + compute_segmentations, + blend_image_and_classified_regions_pil, +) + + +def class_to_color(n): + return class_label_colormap[n] + + +def color_to_class(c): + return class_label_colormap.index(c) + + +def shapes_to_key(shapes): + return json.dumps(shapes) + + +def store_shapes_seg_pair(d, key, seg, remove_old=True): + """ + Stores shapes and segmentation pair in dict d + seg is a PIL.Image object + if remove_old True, deletes all the old keys and values. + """ + bytes_to_encode = io.BytesIO() + seg.save(bytes_to_encode, format="png") + bytes_to_encode.seek(0) + data = base64.b64encode(bytes_to_encode.read()).decode() + if remove_old: + return {key: data} + d[key] = data + return d + + +def look_up_seg(d, key): + """Returns a PIL.Image object""" + data = d[key] + img_bytes = base64.b64decode(data) + img = PIL.Image.open(io.BytesIO(img_bytes)) + return img + + +# Converts image classifier to a JSON compatible encoding and creates a +# dictionary that can be downloaded +# see use_ml_image_segmentation_classifier.py +def save_img_classifier(clf, label_to_colors_args, segmenter_args): + clfbytes = io.BytesIO() + pickle.dump(clf, clfbytes) + clfb64 = base64.b64encode(clfbytes.getvalue()).decode() + return { + "classifier": clfb64, + "segmenter_args": segmenter_args, + "label_to_colors_args": label_to_colors_args, + } + + +def show_segmentation(image_path, mask_shapes, features, segmenter_args): + """adds an image showing segmentations to a figure's layout""" + # add 1 because classifier takes 0 to mean no mask + shape_layers = [color_to_class(shape["line"]["color"]) + 1 for shape in mask_shapes] + label_to_colors_args = { + "colormap": class_label_colormap, + "color_class_offset": -1, + } + segimg, _, clf = compute_segmentations( + mask_shapes, + img_path=image_path, + shape_layers=shape_layers, + label_to_colors_args=label_to_colors_args, + features=features, + ) + # get the classifier that we can later store in the Store + classifier = save_img_classifier(clf, label_to_colors_args, segmenter_args) + segimgpng = plot_common.img_array_to_pil_image(segimg) + return (segimgpng, classifier) diff --git a/apps/dash-image-segmentation/plot_common.py b/apps/dash-image-segmentation/utils/plot_common.py similarity index 100% rename from apps/dash-image-segmentation/plot_common.py rename to apps/dash-image-segmentation/utils/plot_common.py diff --git a/apps/dash-image-segmentation/shape_utils.py b/apps/dash-image-segmentation/utils/shape_utils.py similarity index 100% rename from apps/dash-image-segmentation/shape_utils.py rename to apps/dash-image-segmentation/utils/shape_utils.py diff --git a/apps/dash-image-segmentation/shapes_to_segmentations.py b/apps/dash-image-segmentation/utils/shapes_to_segmentations.py similarity index 97% rename from apps/dash-image-segmentation/shapes_to_segmentations.py rename to apps/dash-image-segmentation/utils/shapes_to_segmentations.py index a0165a5b6..5cd708841 100644 --- a/apps/dash-image-segmentation/shapes_to_segmentations.py +++ b/apps/dash-image-segmentation/utils/shapes_to_segmentations.py @@ -4,8 +4,8 @@ import skimage.util import skimage.io import skimage.color -import shape_utils -from trainable_segmentation import fit_segmenter +import utils.shape_utils as shape_utils +from utils.trainable_segmentation import fit_segmenter import plotly.express as px from sklearn.ensemble import RandomForestClassifier from time import time diff --git a/apps/dash-image-segmentation/trainable_segmentation.py b/apps/dash-image-segmentation/utils/trainable_segmentation.py similarity index 100% rename from apps/dash-image-segmentation/trainable_segmentation.py rename to apps/dash-image-segmentation/utils/trainable_segmentation.py diff --git a/apps/dash-image-segmentation/use_ml_image_segmentation_classifier.py b/apps/dash-image-segmentation/utils/use_ml_image_segmentation_classifier.py similarity index 92% rename from apps/dash-image-segmentation/use_ml_image_segmentation_classifier.py rename to apps/dash-image-segmentation/utils/use_ml_image_segmentation_classifier.py index b023e113f..f3e4faf5b 100644 --- a/apps/dash-image-segmentation/use_ml_image_segmentation_classifier.py +++ b/apps/dash-image-segmentation/utils/use_ml_image_segmentation_classifier.py @@ -14,9 +14,9 @@ """ import os -import plot_common -import shapes_to_segmentations -from trainable_segmentation import multiscale_basic_features, predict_segmenter +import utils.plot_common as plot_common +import utils.shapes_to_segmentations as shapes_to_segmentations +from utils.trainable_segmentation import multiscale_basic_features, predict_segmenter import pickle import base64 import io From dc30d71370c492f3a3590968735592def6bee411 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 30 Jun 2022 14:24:22 -0400 Subject: [PATCH 2/5] updated dbc, replaced FormGroup with Row --- apps/dash-image-segmentation/app.py | 4 +++- apps/dash-image-segmentation/requirements.txt | 4 +++- apps/dash-image-segmentation/utils/components.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/dash-image-segmentation/app.py b/apps/dash-image-segmentation/app.py index 9cd08dcf1..1cbc4ddb5 100644 --- a/apps/dash-image-segmentation/app.py +++ b/apps/dash-image-segmentation/app.py @@ -2,6 +2,7 @@ import dash from dash import Dash, html, dcc, Input, Output, State, callback, callback_context import dash_bootstrap_components as dbc +import dash_mantine_components as dmc from time import time @@ -23,7 +24,8 @@ meta, ) -external_stylesheets = [dbc.themes.BOOTSTRAP, "assets/css/segmentation-style.css"] +external_stylesheets = [dbc.themes.FLATLY, "assets/css/app.css"] + app = dash.Dash(__name__, external_stylesheets=external_stylesheets) server = app.server diff --git a/apps/dash-image-segmentation/requirements.txt b/apps/dash-image-segmentation/requirements.txt index dc8ea49a2..052ef620a 100644 --- a/apps/dash-image-segmentation/requirements.txt +++ b/apps/dash-image-segmentation/requirements.txt @@ -6,7 +6,9 @@ Pillow==9.1.1 scikit_learn==0.23.1 scikit-image==0.16.2 joblib==0.15.1 -dash_bootstrap_components==0.10.7 +dash-bootstrap-components==1.0.3 +dash-mantine-components==0.6.0 +# dash_bootstrap_components==0.10.7 dash==2.4.1 pandas==1.4.2 gunicorn==20.1.0 diff --git a/apps/dash-image-segmentation/utils/components.py b/apps/dash-image-segmentation/utils/components.py index a962d4680..508afa653 100644 --- a/apps/dash-image-segmentation/utils/components.py +++ b/apps/dash-image-segmentation/utils/components.py @@ -261,7 +261,7 @@ def header( html.Hr(), dbc.Form( [ - dbc.FormGroup( + dbc.Row( [ dbc.Label( "Width of annotation paintbrush", @@ -277,7 +277,7 @@ def header( ), ] ), - dbc.FormGroup( + dbc.Row( [ html.H6( id="stroke-width-display", @@ -296,7 +296,7 @@ def header( ), ] ), - dbc.FormGroup( + dbc.Row( [ dbc.Label( "Select features", From 9d431a0c89f8d9f366eb925d42d54fc2ceec2dc6 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 30 Jun 2022 14:56:17 -0400 Subject: [PATCH 3/5] replaced header --- apps/dash-image-segmentation/app.py | 4 +- .../assets/images/plotly-logo-light-theme.png | Bin 0 -> 30763 bytes .../utils/components.py | 198 ++++++++++-------- 3 files changed, 117 insertions(+), 85 deletions(-) create mode 100644 apps/dash-image-segmentation/assets/images/plotly-logo-light-theme.png diff --git a/apps/dash-image-segmentation/app.py b/apps/dash-image-segmentation/app.py index 1cbc4ddb5..a28113adf 100644 --- a/apps/dash-image-segmentation/app.py +++ b/apps/dash-image-segmentation/app.py @@ -17,11 +17,11 @@ ) from utils.figures import make_default_figure, annotation_react from utils.components import ( - dbc_header, description, segmentation, sidebar, meta, + header_items, ) external_stylesheets = [dbc.themes.FLATLY, "assets/css/app.css"] @@ -33,7 +33,7 @@ app.layout = html.Div( [ - dbc_header, + dmc.Header(height=70, padding="md", children=header_items), dbc.Container( [ dbc.Row(description), diff --git a/apps/dash-image-segmentation/assets/images/plotly-logo-light-theme.png b/apps/dash-image-segmentation/assets/images/plotly-logo-light-theme.png new file mode 100644 index 0000000000000000000000000000000000000000..4920c6e340b86d3b6ca4b5603e899020768ad352 GIT binary patch literal 30763 zcmZs?1yr2Pnl%cI2Z!M9?gR@O+}(m(aCZo9!CeyE-QC^Y8+W(h4!1dTzM22b+_kEq zo7L1?^2o0J>Tv?6+4j_sfg@T*pq$+J-9&zJf!jZAzqR%5A9dhKah#CXsZemF$~i0K2)ne;$reH z+I5_tUG6JQufQeei0y1dKzWG~dJj3qM-jy}(~49AUW0+=Up zpL=Dc*I85CTs@NvY8ERIv0;}-l`$v*kv_e)R(GkD#Xov@@2+^`kY!;v7tglgHY@dK zQ#+#K^4h4X+TYcmTow>ea3VX!lf=YC7mC#n9!xGH1&-}r_vHHr7B+~OgRI$qo{HF~ z%HLt}-P~TrWelDywle3)Ik68^A$jL8%a1baN#mvQzABGCp=LT+x^!DI{8@OP6Jsm; z1|S#w&$h`5C_unB1U}e|=q0hQBzb=2=w3Ja`>WQ6iPKKxm!Gh=9dDGtIOPSj~W{W-J4^Z<;3lT+T?iNzx7--D)%y!Kbu{N;>I zouctiM6Jy}bCLhCxgUF-2N`m0Slh3XqVwQ`c?X`MBB-T~-TmFxV^?;NiV zr@Q&IyN`4bsqcr*w&%dlUi&ieJ|;M~CxkpsPIEAs-fViYyq*0ouh(SP0s;gq1GYlJ zzGN(sHXdi!J+j-kIZ~8+F<@~2t8ocuEk|m1>+ut={OoNIDp74RUQ~De`FQpIDp{{x zdL{SrGiPbCvh5xVn-f>7TuaLxn|s+N^ZNc)wZ^r`swaVdDeL{>>Z~xFn}=K zWv7Kt)?&ZHYxzAM4%IjGN*lH6rQf%MI?Ds?b3>5`jx)L3�vNUu%(;ewduU+-%ln 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 literal 0 HcmV?d00001 diff --git a/apps/dash-image-segmentation/utils/components.py b/apps/dash-image-segmentation/utils/components.py index 508afa653..cbf15bda8 100644 --- a/apps/dash-image-segmentation/utils/components.py +++ b/apps/dash-image-segmentation/utils/components.py @@ -1,4 +1,5 @@ import dash_bootstrap_components as dbc +import dash_mantine_components as dmc from dash import html, dcc from utils.figures import make_default_figure from constants import class_labels, DEFAULT_STROKE_WIDTH, SEG_FEATURE_TYPES @@ -36,95 +37,126 @@ ) -def header( - app, header_color, header, subheader=None, header_background_color="transparent" -): - left_headers = html.Div( - [ - html.Div(header, className="header-title"), - html.Div(subheader, className="subheader-title"), - ], - style={"color": header_color}, - ) +# def header( +# app, header_color, header, subheader=None, header_background_color="transparent" +# ): +# left_headers = html.Div( +# [ +# html.Div(header, className="header-title"), +# html.Div(subheader, className="subheader-title"), +# ], +# style={"color": header_color}, +# ) - logo = html.Img(src=app.get_asset_url("images/plotly-logo-light-theme.png")) - logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank") - demo_link = html.A( - "LEARN MORE", - href="https://plotly.com/dash/", - target="_blank", - className="demo-button", - ) - right_logos = html.Div([demo_link, logo_link], className="header-logos") +# logo = html.Img(src=app.get_asset_url("images/plotly-logo-light-theme.png")) +# logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank") +# demo_link = html.A( +# "LEARN MORE", +# href="https://plotly.com/dash/", +# target="_blank", +# className="demo-button", +# ) +# right_logos = html.Div([demo_link, logo_link], className="header-logos") - return html.Div( - [left_headers, right_logos], - className="header", - style={"background-color": header_background_color}, - ) +# return html.Div( +# [left_headers, right_logos], +# className="header", +# style={"background-color": header_background_color}, +# ) -# DBC Header -dbc_header = dbc.Navbar( - dbc.Container( - [ - dbc.Row( - [ - dbc.Col( - html.Img( - id="logo", - src=("assets/images/plotly-logo-dark-theme.png"), - height="30px", - ), - md="auto", - ), - dbc.Col( - [ - html.Div( - [ - html.H3("Interactive Machine Learning"), - html.P("Image segmentation"), - ], - id="app-title", - ) - ], - md=True, - align="center", - ), - ], - align="center", - ), - dbc.Row( - [ - dbc.Col( - [ - dbc.NavbarToggler(id="navbar-toggler"), - dbc.Collapse( - dbc.Nav( - [ - dbc.NavItem(button_howto), - dbc.NavItem(button_github), - ], - navbar=True, - ), - id="navbar-collapse", - navbar=True, - ), - modal_overlay, - ], - md=2, - ), - ], - align="center", - ), - ], - fluid=True, - ), - dark=True, - color="dark", - sticky="top", +# # DBC Header +# dbc_header = dbc.Navbar( +# dbc.Container( +# [ +# dbc.Row( +# [ +# dbc.Col( +# html.Img( +# id="logo", +# src=("assets/images/plotly-logo-dark-theme.png"), +# height="30px", +# ), +# md="auto", +# ), +# dbc.Col( +# [ +# html.Div( +# [ +# html.H3("Interactive Machine Learning"), +# html.P("Image segmentation"), +# ], +# id="app-title", +# ) +# ], +# md=True, +# align="center", +# ), +# ], +# align="center", +# ), +# dbc.Row( +# [ +# dbc.Col( +# [ +# dbc.NavbarToggler(id="navbar-toggler"), +# dbc.Collapse( +# dbc.Nav( +# [ +# dbc.NavItem(button_howto), +# dbc.NavItem(button_github), +# ], +# navbar=True, +# ), +# id="navbar-collapse", +# navbar=True, +# ), +# modal_overlay, +# ], +# md=2, +# ), +# ], +# align="center", +# ), +# ], +# fluid=True, +# ), +# dark=True, +# color="dark", +# sticky="top", +# ) + +header_items = dmc.Group( + position="apart", + children=[ + dmc.Image( + src="assets/images/plotly-logo-light-theme.png", width=200, height=40 + ), + dmc.Text( + "Dash Image Segmentation", + color="gray", + size="xl", + weight=600, + transform="capitalize", + ), + dbc.DropdownMenu( + children=[ + dbc.DropdownMenuItem( + "Behind the App", href="https://plotly.com/dash/design-kit/" + ), + dbc.DropdownMenuItem( + "View Code on Github", + href="https://github.com/plotly/dash-sample-apps/blob/main/apps/dash-image-segmentation/app.py", + ), + ], + nav=True, + in_navbar=True, + label="Learn More", + ), + ], ) + # Description description = dbc.Col( [ From 590c53d66b94d89bc3dfbd6ca282a1be3628eabd Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 30 Jun 2022 16:34:22 -0400 Subject: [PATCH 4/5] added modal, fixed header --- apps/dash-image-segmentation/app.py | 5 + .../utils/components.py | 122 ++---------------- 2 files changed, 19 insertions(+), 108 deletions(-) diff --git a/apps/dash-image-segmentation/app.py b/apps/dash-image-segmentation/app.py index a28113adf..a7c538c50 100644 --- a/apps/dash-image-segmentation/app.py +++ b/apps/dash-image-segmentation/app.py @@ -22,6 +22,7 @@ sidebar, meta, header_items, + modal_overlay, ) external_stylesheets = [dbc.themes.FLATLY, "assets/css/app.css"] @@ -31,6 +32,9 @@ server = app.server app.title = "Interactive image segmentation based on machine learning" +with open("explanations.md", "r") as f: + howto_md = f.read() + app.layout = html.Div( [ dmc.Header(height=70, padding="md", children=header_items), @@ -42,6 +46,7 @@ children=[dbc.Col(segmentation, md=8), dbc.Col(sidebar, md=4)], ), dbc.Row(dbc.Col(meta)), + modal_overlay, ], fluid=True, ), diff --git a/apps/dash-image-segmentation/utils/components.py b/apps/dash-image-segmentation/utils/components.py index cbf15bda8..9f4ed7576 100644 --- a/apps/dash-image-segmentation/utils/components.py +++ b/apps/dash-image-segmentation/utils/components.py @@ -21,111 +21,23 @@ button_howto = dbc.Button( "Learn more", id="howto-open", - outline=True, + outline=False, color="info", # Turn off lowercase transformation for class .button in stylesheet style={"textTransform": "none"}, + size="md", ) button_github = dbc.Button( - "View Code on github", - outline=True, + "Github Code", + outline=False, color="primary", href="https://github.com/plotly/dash-sample-apps/tree/master/apps/dash-image-segmentation", id="gh-link", style={"text-transform": "none"}, + size="md", ) - -# def header( -# app, header_color, header, subheader=None, header_background_color="transparent" -# ): -# left_headers = html.Div( -# [ -# html.Div(header, className="header-title"), -# html.Div(subheader, className="subheader-title"), -# ], -# style={"color": header_color}, -# ) - -# logo = html.Img(src=app.get_asset_url("images/plotly-logo-light-theme.png")) -# logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank") -# demo_link = html.A( -# "LEARN MORE", -# href="https://plotly.com/dash/", -# target="_blank", -# className="demo-button", -# ) -# right_logos = html.Div([demo_link, logo_link], className="header-logos") - -# return html.Div( -# [left_headers, right_logos], -# className="header", -# style={"background-color": header_background_color}, -# ) - - -# # DBC Header -# dbc_header = dbc.Navbar( -# dbc.Container( -# [ -# dbc.Row( -# [ -# dbc.Col( -# html.Img( -# id="logo", -# src=("assets/images/plotly-logo-dark-theme.png"), -# height="30px", -# ), -# md="auto", -# ), -# dbc.Col( -# [ -# html.Div( -# [ -# html.H3("Interactive Machine Learning"), -# html.P("Image segmentation"), -# ], -# id="app-title", -# ) -# ], -# md=True, -# align="center", -# ), -# ], -# align="center", -# ), -# dbc.Row( -# [ -# dbc.Col( -# [ -# dbc.NavbarToggler(id="navbar-toggler"), -# dbc.Collapse( -# dbc.Nav( -# [ -# dbc.NavItem(button_howto), -# dbc.NavItem(button_github), -# ], -# navbar=True, -# ), -# id="navbar-collapse", -# navbar=True, -# ), -# modal_overlay, -# ], -# md=2, -# ), -# ], -# align="center", -# ), -# ], -# fluid=True, -# ), -# dark=True, -# color="dark", -# sticky="top", -# ) - header_items = dmc.Group( position="apart", children=[ @@ -139,24 +51,11 @@ weight=600, transform="capitalize", ), - dbc.DropdownMenu( - children=[ - dbc.DropdownMenuItem( - "Behind the App", href="https://plotly.com/dash/design-kit/" - ), - dbc.DropdownMenuItem( - "View Code on Github", - href="https://github.com/plotly/dash-sample-apps/blob/main/apps/dash-image-segmentation/app.py", - ), - ], - nav=True, - in_navbar=True, - label="Learn More", - ), + dbc.NavItem(button_howto), + dbc.NavItem(button_github), ], ) - # Description description = dbc.Col( [ @@ -345,6 +244,13 @@ ] ), # Indicate showing most recently computed segmentation + # dbc.Button( + # name="Show Segmentation", + # size="lg", + # id="show-segmentation", + # value="Show Segmentation", + # type="submit", + # ), dcc.Checklist( id="show-segmentation", options=[ From 3d1b81e97a488d1292050f2c52c37c58c51975e3 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 30 Jun 2022 16:37:13 -0400 Subject: [PATCH 5/5] update --- apps/dash-image-segmentation/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/dash-image-segmentation/app.py b/apps/dash-image-segmentation/app.py index a7c538c50..367a0ed29 100644 --- a/apps/dash-image-segmentation/app.py +++ b/apps/dash-image-segmentation/app.py @@ -32,8 +32,6 @@ server = app.server app.title = "Interactive image segmentation based on machine learning" -with open("explanations.md", "r") as f: - howto_md = f.read() app.layout = html.Div( [ @@ -46,7 +44,6 @@ children=[dbc.Col(segmentation, md=8), dbc.Col(sidebar, md=4)], ), dbc.Row(dbc.Col(meta)), - modal_overlay, ], fluid=True, ),