Introduction to dash-canvas

pip install dash-canvas==0.1.0

dash-canvas is a module for image annotation and image processing
using Dash. It provides both the DashCanvas object for drawing
and annotations on images, and a set of utility functions to process
images using the annotations.

dash-canvas can be used in various fields in which user
interaction with images is required, such as quality control in
industry, identification and segmentation of cells or organs in life
and medical sciences, quantification of phases in materials and
geosciences, construction of training sets for machine learning, etc.

Install dash-canvas with

pip install -U dash-canvas

The source is on GitHub at plotly/dash-canvas.

A gallery of examples is hosted at https://dash-canvas.plotly.host/.

DashCanvas: a canvas object for annotations

Let’s get started with a simple canvas object.

import dash
import dash_html_components as html
from dash_canvas import DashCanvas

app = dash.Dash(__name__)
app.config.suppress_callback_exceptions = True

app.layout = html.Div([
    html.H5('Press down the left mouse button and draw inside the canvas'),
    DashCanvas(id='canvas_101')
    ])


if __name__ == '__main__':
    app.run_server(debug=True)
Press down the left mouse button and draw inside the canvas

You can draw inside the object with the freehand tool, and use the tool
buttons to draw lines, zoom in and out, pan, select objects and move them
inside the canvas.

DashCanvas comes with a set of properties which can be adjusted to
control the geometry of the canvas, the default tool and its properties.
You can pass a background image either as a filename (filename
property) or as a data string (image_content property); more examples
below).

import dash
import dash_html_components as html
from dash_canvas import DashCanvas
import numpy as np

app = dash.Dash(__name__)

filename = 'https://raw.githubusercontent.com/plotly/datasets/master/mitochondria.jpg'
canvas_width = 500


app.layout = html.Div([
    DashCanvas(id='canvas_image',
               tool='line',
               lineWidth=5,
               lineColor='red',
               filename=filename,
               width=canvas_width)
    ])


if __name__ == '__main__':
    app.run_server(debug=True)

The height of the canvas is adjusted automatically by keeping the aspect
ratio of the background image.

Basic callbacks to modify DashCanvas properties

Like any Dash component, the properties of a DashCanvas can be
modified by other components, via callbacks. Please be sure to have
read first through the Dash tutorial to
know how to write callbacks.


from dash_canvas import DashCanvas
import dash
from dash.dependencies import Input, Output, State
import dash_html_components as html
import dash_core_components as dcc
import dash_daq as daq

filename = 'https://www.publicdomainpictures.net/pictures/60000/nahled/flower-outline-coloring-page.jpg'
canvas_width = 300

app = dash.Dash(__name__)

app.layout = html.Div([
        html.Div([
            DashCanvas(
                id='canvas-color',
                width=canvas_width,
                filename=filename,
                hide_buttons=['line', 'zoom', 'pan'],
                )
                ], className="six columns"),
        html.Div([
            html.H6(children=['Brush width']),
            dcc.Slider(
                id='bg-width-slider',
                min=2,
                max=40,
                step=1,
                value=5
            ),
            daq.ColorPicker(
                id='color-picker',
                label='Brush color',
                value=dict(hex='#119DFF')
        ),
        ], className="three columns"),
        ])


@app.callback(Output('canvas-color', 'lineColor'),
            [Input('color-picker', 'value')])
def update_canvas_linewidth(value):
    if isinstance(value, dict):
        return value['hex']
    else:
        return value


@app.callback(Output('canvas-color', 'lineWidth'),
            [Input('bg-width-slider', 'value')])
def update_canvas_linewidth(value):
    return value

if __name__ == '__main__':
    app.run_server(debug=True)
Brush width

In the example above, a slider dcc.Slider and a color picker
daq.ColorPicker are used to adjust the width and color of the drawing
brush. We just created an image coloring tool in a few lines of code! You
can learn more about available components in the
component libraries section of the
Dash documentation. Also
note that the set of available buttons has been restricted through the
hide_buttons properties, in order to keep the app design simple.

Retrieving the geometry of annotations and using utility functions

The geometry of annotations can be retrieved by pressing the bottom-right
button of the DashCanvas. This button is called “Save” by default;
the name can be customized through the goButtonTitle property.
This button updates the json_data property of DashCanvas, which
is a JSON string with information about the background image and the
geometry of annotations.

import dash
from dash.exceptions import PreventUpdate
from dash.dependencies import Input, Output, State
import dash_html_components as html
from dash_canvas import DashCanvas
import json
from dash_table import DataTable

app = dash.Dash(__name__)

filename = 'https://raw.githubusercontent.com/plotly/datasets/master/mitochondria.jpg'
canvas_width = 500

columns = ['type', 'width', 'height', 'scaleX', 'strokeWidth', 'path']

app.layout = html.Div([
    html.H6('Draw on image and press Save to show annotations geometry'),
    DashCanvas(id='annot-canvas',
               lineWidth=5,
               filename=filename,
               width=canvas_width,
               ),
    DataTable(id='annot-canvas-table',
              style_cell={'textAlign': 'left'},
              columns=[{"name": i, "id": i} for i in columns]),
    ])


@app.callback(Output('annot-canvas-table', 'data'),
              [Input('annot-canvas', 'json_data')])
def update_data(string):
    if string:
        data = json.loads(string)
    else:
        raise PreventUpdate
    return data['objects'][1:]


if __name__ == '__main__':
    app.run_server(debug=True)
Draw on image and press Save to show annotations geometry

You can either write custom functions to parse the JSON string, or
use the utility functions included in the dash_canvas package. In
particular, dash_canvas.utils.parse_json_string returns a binary
mask with non-zero pixels displaying the annotations:

import numpy as np
from skimage import io
import dash
from dash.exceptions import PreventUpdate
from dash.dependencies import Input, Output, State
import dash_html_components as html
from dash_canvas import DashCanvas
from dash_canvas.utils import array_to_data_url, parse_jsonstring

app = dash.Dash(__name__)

filename = 'https://raw.githubusercontent.com/plotly/datasets/master/mitochondria.jpg'

canvas_width = 300

app.layout = html.Div([
    html.H6('Draw on image and press Save to show annotations geometry'),
    html.Div([
    DashCanvas(id='canvas',
               lineWidth=5,
               filename=filename,
               width=canvas_width,
               ),
    ], className="five columns"),
    html.Div([
    html.Img(id='my-image', width=300),
    ], className="five columns"),
    ])


@app.callback(Output('my-image', 'src'),
              [Input('canvas', 'json_data')])
def update_data(string):
    if string:
        mask = parse_jsonstring(string, io.imread(filename, as_gray=True).shape)
    else:
        raise PreventUpdate
    return array_to_data_url((255 * mask).astype(np.uint8))


if __name__ == '__main__':
    app.run_server(debug=True)
Draw on image and press Save to show annotations geometry

The above example uses the array_to_data_url utility function to
transform a NumPy array into an image data string.

Finally, dash-canvas provides utility functions to process images
given the binary mask derived from annotations:

import dash
from dash.exceptions import PreventUpdate
from dash.dependencies import Input, Output, State
import dash_html_components as html
import dash_canvas
from dash_canvas import DashCanvas
from dash_canvas.utils import (array_to_data_url, parse_jsonstring,
                              watershed_segmentation)
from skimage import io, color, img_as_ubyte
import numpy as np

app = dash.Dash(__name__)
filename = 'https://raw.githubusercontent.com/plotly/datasets/master/mitochondria.jpg'

canvas_width = 300

app.layout = html.Div([
    html.H6('Annotate the two objects and the background'),
    html.Div([
    DashCanvas(id='segmentation-canvas',
               lineWidth=5,
               filename=filename,
               width=canvas_width,
               ),
    ], className="five columns"),
    html.Div([
    html.Img(id='segmentation-img', width=300),
    ], className="five columns"),
    ])


@app.callback(Output('segmentation-img', 'src'),
              [Input('segmentation-canvas', 'json_data')])
def segmentation(string):
    if string:
        mask = parse_jsonstring(string, io.imread(filename, as_gray=True).shape)
        seg = watershed_segmentation(io.imread(filename, as_gray=True), mask)
        src = color.label2rgb(seg, image=io.imread(filename, as_gray=True))
    else:
        raise PreventUpdate
    return array_to_data_url(img_as_ubyte(src))


if __name__ == '__main__':
    app.run_server(debug=True)
Annotate the two objects and the background

These functions rely on scikit-image to
process arrays as images. Here we used the watershed algorithm
from scikit-image.

Updating the background image

The background image can be updated thanks to the image_content
property (a str), for example using the contents property of
dcc.Upload (an “open file” dialog). Updating image_content
triggers the update of the json_data property containing the
annotations.

More examples

A gallery of examples using DashCanvas is deployed on
https://dash-canvas.plotly.host/, its source code is
plotly/canvas-portal.