8 |
9 |
10 | dash-canvas is a package for image processing with
11 | [Dash](https://dash.plotly.com/). It provides a Dash component for
12 | annotating images, as well as utility functions for using such
13 | annotations for various image processing tasks.
14 |
15 | Try out the
16 | [gallery of examples](https://dash-canvas.plotly.host/Portal/) and [read
17 | the docs](https://dash.plotly.com/canvas) to learn how to use dash-canvas.
18 |
19 |
20 |
21 |
22 | Get started with:
23 | 1. Install `dash_canvas`: `pip install dash-canvas` (you will also need
24 | `dash-core-components` to run the apps).
25 | 2. Run `python app_seg.py` (for interactive segmentation) or
26 | `python correct_segmentation.py` (for correcting a pre-existing
27 | segmentation)
28 | 3. Visit http://localhost:8050 in your web browser
29 |
30 |
31 |
--------------------------------------------------------------------------------
/dash_canvas/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .image_processing_utils import (watershed_segmentation,
2 | random_walker_segmentation,
3 | random_forest_segmentation,
4 | segmentation_generic,
5 | superpixel_color_segmentation,
6 | modify_segmentation)
7 | from .registration import register_tiles, autocrop
8 | from .parse_json import (parse_jsonstring, parse_jsonstring_line,
9 | parse_jsonstring_rectangle, parse_jsonfile)
10 | from .io_utils import array_to_data_url, image_string_to_PILImage
11 | from .plot_utils import image_with_contour
12 | from .exposure import brightness_adjust, contrast_adjust
13 |
14 | __all__ = ['array_to_data_url',
15 | 'autocrop',
16 | 'brightness_adjust',
17 | 'contrast_adjust',
18 | 'image_string_to_PILImage',
19 | 'image_with_contour',
20 | 'modify_segmentation',
21 | 'parse_jsonfile',
22 | 'parse_jsonstring',
23 | 'parse_jsonstring_line',
24 | 'parse_jsonstring_rectangle',
25 | 'random_forest_segmentation',
26 | 'random_walker_segmentation',
27 | 'register_tiles',
28 | 'segmentation_generic',
29 | 'superpixel_color_segmentation',
30 | 'watershed_segmentation']
31 |
32 |
--------------------------------------------------------------------------------
/dash_canvas/test/test_image_processing_utils.py:
--------------------------------------------------------------------------------
1 | from dash_canvas.utils import watershed_segmentation, modify_segmentation
2 | from skimage import data, segmentation, morphology, measure
3 | import numpy as np
4 | from scipy import ndimage
5 |
6 |
7 | def test_watershed_segmentation():
8 | img = np.zeros((20, 20))
9 | img[2:6, 2:6] = 1
10 | img[10:15, 10:15] = 1
11 | mask = np.zeros_like(img, dtype=np.uint8)
12 | mask[4, 4] = 1
13 | mask[12, 12] = 2
14 | res = watershed_segmentation(img, mask, sigma=0.1)
15 | assert np.all(res[2:6, 2:6] == 1)
16 | assert np.all(res[10:15, 10:15] == 2)
17 |
18 |
19 | def test_split_segmentation():
20 | img = np.zeros((100, 100), dtype=np.uint8)
21 | img[:40, 55:] = 1
22 | img[40:, :30] = 2
23 | img[40:, 30:65] = 3
24 | img[40:, 65:] = 4
25 | img = ndimage.rotate(img, 20)
26 | img = measure.label(img)
27 | img = morphology.remove_small_objects(img, 20)
28 | img = segmentation.relabel_sequential(img)[0]
29 |
30 | mask = np.zeros_like(img)
31 | mask[2:53, 75] = 1
32 | mask[100, 17:60] = 1
33 |
34 | # Labels start at 1
35 | seg = modify_segmentation(img, measure.label(mask), mode='split')
36 | assert len(np.unique(seg)) == len(np.unique(img)) + 2
37 |
38 | # Labels start at 0
39 | seg = modify_segmentation(img + 1, measure.label(mask))
40 | assert len(np.unique(seg)) == len(np.unique(img)) + 2
41 |
42 |
43 | def test_merge_segmentation():
44 | img = np.zeros((20, 20), dtype=np.uint8)
45 | img[:10, :10] = 1
46 | img[10:, :10] = 2
47 | mask = np.zeros_like(img)
48 | mask[:, 5] = 1
49 | seg = modify_segmentation(img, mask, mode='merge')
50 | assert np.all(np.unique(seg) == np.array([0, 1]))
51 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 |
2 | from dash_canvas import DashCanvas
3 | import dash
4 | from dash.dependencies import Input, Output, State
5 | import dash_html_components as html
6 | import dash_core_components as dcc
7 | import plotly.graph_objs as go
8 | import dash_daq as daq
9 |
10 |
11 | filename = 'https://www.publicdomainpictures.net/pictures/60000/nahled/flower-outline-coloring-page.jpg'
12 | canvas_width = 300
13 |
14 | app = dash.Dash(__name__)
15 |
16 | app.layout = html.Div([
17 | html.Div([
18 | DashCanvas(
19 | id='canvas-color',
20 | width=canvas_width,
21 | filename=filename,
22 | hide_buttons=['line', 'zoom', 'pan'],
23 | )
24 | ], className="six columns"),
25 | html.Div([
26 | html.H6(children=['Brush width']),
27 | dcc.Slider(
28 | id='bg-width-slider',
29 | min=2,
30 | max=40,
31 | step=1,
32 | value=[5]
33 | ),
34 | daq.ColorPicker(
35 | id='color-picker',
36 | label='Brush color',
37 | value='#119DFF'
38 | ),
39 | ], className="three columns"),
40 | ])
41 |
42 |
43 | @app.callback(Output('canvas-color', 'lineColor'),
44 | [Input('color-picker', 'value')])
45 | def update_canvas_linewidth(value):
46 | if isinstance(value, dict):
47 | return value['hex']
48 | else:
49 | return value
50 |
51 |
52 | @app.callback(Output('canvas-color', 'lineWidth'),
53 | [Input('bg-width-slider', 'value')])
54 | def update_canvas_linewidth(value):
55 | return value
56 |
57 | if __name__ == '__main__':
58 | app.run_server(debug=True)
59 |
60 |
61 |
--------------------------------------------------------------------------------
/man/dashCanvas.Rd:
--------------------------------------------------------------------------------
1 | % Auto-generated: do not edit by hand
2 | \name{dashCanvas}
3 |
4 | \alias{dashCanvas}
5 |
6 | \title{DashCanvas component}
7 |
8 | \description{
9 | Canvas component for drawing on a background image and selecting regions.
10 | }
11 |
12 | \usage{
13 | dashCanvas(id=NULL, image_content=NULL, zoom=NULL, width=NULL, height=NULL, scale=NULL, tool=NULL, lineWidth=NULL, lineColor=NULL, goButtonTitle=NULL, filename=NULL, trigger=NULL, json_data=NULL, hide_buttons=NULL)
14 | }
15 |
16 | \arguments{
17 | \item{id}{The ID used to identify this component in Dash callbacks}
18 |
19 | \item{image_content}{Image data string, formatted as png or jpg data string. Can be
20 | generated by utils.io_utils.array_to_data_string.}
21 |
22 | \item{zoom}{Zoom factor}
23 |
24 | \item{width}{Width of the canvas}
25 |
26 | \item{height}{Height of the canvas}
27 |
28 | \item{scale}{Scaling ratio between canvas width and image width}
29 |
30 | \item{tool}{Selection of drawing tool, among ["pencil", "pan", "circle",
31 | "rectangle", "select", "line"].}
32 |
33 | \item{lineWidth}{Width of drawing line (in pencil mode)}
34 |
35 | \item{lineColor}{Color of drawing line (in pencil mode). Can be a text string,
36 | like 'yellow', 'red', or a color triplet like 'rgb(255, 0, 0)'.
37 | Alpha is possible with 'rgba(255, 0, 0, 0.5)'.}
38 |
39 | \item{goButtonTitle}{Title of button}
40 |
41 | \item{filename}{Name of image file to load (URL string)}
42 |
43 | \item{trigger}{Counter of how many times the save button was pressed
44 | (to be used mostly as input)}
45 |
46 | \item{json_data}{Sketch content as JSON string, containing background image and
47 | annotations. Use utils.parse_json.parse_jsonstring to parse
48 | this string.}
49 |
50 | \item{hide_buttons}{Names of buttons to hide. Names are "zoom", "pan", "line", "pencil",
51 | "rectangle", "undo", "select".}
52 | }
53 |
--------------------------------------------------------------------------------
/dash_canvas/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function as _
2 |
3 | import os as _os
4 | import sys as _sys
5 | import json
6 |
7 | import dash as _dash
8 |
9 | # noinspection PyUnresolvedReferences
10 | from ._imports_ import *
11 | from ._imports_ import __all__
12 |
13 | if not hasattr(_dash, 'development'):
14 | print('Dash was not successfully imported. '
15 | 'Make sure you don\'t have a file '
16 | 'named \n"dash.py" in your current directory.', file=_sys.stderr)
17 | _sys.exit(1)
18 |
19 | _basepath = _os.path.dirname(__file__)
20 | _filepath = _os.path.abspath(_os.path.join(_basepath, 'package.json'))
21 | with open(_filepath) as f:
22 | package = json.load(f)
23 |
24 | package_name = package['name'].replace(' ', '_').replace('-', '_')
25 | __version__ = package['version']
26 |
27 | _current_path = _os.path.dirname(_os.path.abspath(__file__))
28 |
29 | _this_module = _sys.modules[__name__]
30 |
31 | async_resources = [
32 | 'canvas'
33 | ]
34 |
35 | _js_dist = []
36 |
37 | _js_dist.extend([{
38 | 'relative_package_path': 'async-{}.js'.format(async_resource),
39 | 'dev_package_path': 'async-{}.dev.js'.format(async_resource),
40 | 'external_url': (
41 | 'https://unpkg.com/dash-canvas@{}'
42 | '/dash_canvas/async-{}.js'
43 | ).format(__version__, async_resource),
44 | 'namespace': 'dash_canvas',
45 | 'async': True
46 | } for async_resource in async_resources])
47 |
48 | _js_dist.extend([
49 | {
50 | 'relative_package_path': 'dash_canvas.min.js',
51 | 'dev_package_path': 'dash_canvas.dev.js',
52 | 'namespace': package_name
53 | }
54 | ])
55 |
56 | _css_dist = []
57 |
58 |
59 | for _component in __all__:
60 | setattr(locals()[_component], '_js_dist', _js_dist)
61 | setattr(locals()[_component], '_css_dist', _css_dist)
62 |
--------------------------------------------------------------------------------
/_validate_init.py:
--------------------------------------------------------------------------------
1 | """
2 | DO NOT MODIFY
3 | This file is used to validate your publish settings.
4 | """
5 | from __future__ import print_function
6 |
7 | import os
8 | import sys
9 | import importlib
10 |
11 |
12 | components_package = 'dash_canvas'
13 |
14 | components_lib = importlib.import_module(components_package)
15 |
16 | missing_dist_msg = 'Warning {} was not found in `{}.__init__.{}`!!!'
17 | missing_manifest_msg = '''
18 | Warning {} was not found in `MANIFEST.in`!
19 | It will not be included in the build!
20 | '''
21 |
22 | with open('MANIFEST.in', 'r') as f:
23 | manifest = f.read()
24 |
25 |
26 | def check_dist(dist, filename):
27 | # Support the dev bundle.
28 | if filename.endswith('dev.js'):
29 | return True
30 |
31 | return any(
32 | filename in x
33 | for d in dist
34 | for x in (
35 | [d.get('relative_package_path')]
36 | if not isinstance(d.get('relative_package_path'), list)
37 | else d.get('relative_package_path')
38 | )
39 | )
40 |
41 |
42 | def check_manifest(filename):
43 | return filename in manifest
44 |
45 |
46 | def check_file(dist, filename):
47 | if not check_dist(dist, filename):
48 | print(
49 | missing_dist_msg.format(filename, components_package, '_js_dist'),
50 | file=sys.stderr
51 | )
52 | if not check_manifest(filename):
53 | print(missing_manifest_msg.format(filename),
54 | file=sys.stderr)
55 |
56 |
57 | for cur, _, files in os.walk(components_package):
58 | for f in files:
59 |
60 | if f.endswith('js'):
61 | # noinspection PyProtectedMember
62 | check_file(components_lib._js_dist, f)
63 | elif f.endswith('css'):
64 | # noinspection PyProtectedMember
65 | check_file(components_lib._css_dist, f)
66 | elif not f.endswith('py'):
67 | check_manifest(f)
68 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | 'python-3.6':
5 | docker:
6 | - image: circleci/python:3.6-stretch-node-browsers
7 | environment:
8 | PYTHON_VERSION: py36
9 | PERCY_ENABLE: 0
10 |
11 | steps:
12 | - checkout
13 |
14 | - run:
15 | name: Create virtual env
16 | command: python -m venv || virtualenv venv
17 |
18 | - run:
19 | name: Write job name
20 | command: echo $CIRCLE_JOB > circlejob.txt
21 |
22 | - restore_cache:
23 | key: deps1-{{ .Branch }}-{{ checksum "requirements/package.txt" }}-{{ checksum "package-lock.json" }}-{{ checksum ".circleci/config.yml" }}-{{ checksum "circlejob.txt" }}
24 |
25 | - run:
26 | name: Install dependencies
27 | command: |
28 | . venv/bin/activate
29 | pip install --progress-bar off -r requirements/package.txt
30 | npm ci
31 |
32 | - save_cache:
33 | key: deps1-{{ .Branch }}-{{ checksum "requirements/package.txt" }}-{{ checksum "package-lock.json" }}-{{ checksum ".circleci/config.yml" }}-{{ checksum "circlejob.txt" }}
34 | paths:
35 | - 'venv'
36 | - 'node_modules'
37 |
38 | - run:
39 | name: Build
40 | command: |
41 | . venv/bin/activate
42 | npm run build
43 | python setup.py develop
44 |
45 | - run:
46 | name: Run tests
47 | command: |
48 | . venv/bin/activate
49 | pytest dash_canvas
50 |
51 | - run:
52 | name: Run usage tests
53 | command: |
54 | . venv/bin/activate
55 | pytest tests
56 |
57 |
58 |
59 |
60 | workflows:
61 | version: 2
62 | build:
63 | jobs:
64 | - 'python-3.6'
65 |
--------------------------------------------------------------------------------
/app4_measure_length.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | from skimage import io
4 |
5 | import dash
6 | from dash.dependencies import Input, Output, State
7 | import dash_html_components as html
8 | import dash_core_components as dcc
9 | import dash_table
10 |
11 | import dash_canvas
12 | from dash_canvas.utils.io_utils import (image_string_to_PILImage,
13 | array_to_data_url)
14 | from dash_canvas.utils.parse_json import parse_jsonstring_line
15 |
16 |
17 | def title():
18 | return "Measure lengths"
19 |
20 | def description():
21 | return "Draw lines on objects to measure their lengths."
22 |
23 |
24 | filename = 'https://upload.wikimedia.org/wikipedia/commons/a/a4/MRI_T2_Brain_axial_image.jpg'
25 | img = io.imread(filename)[..., 0].T
26 | height, width = img.shape
27 | canvas_width = 600
28 | canvas_height = round(height * canvas_width / width)
29 | scale = canvas_width / width
30 |
31 | list_columns = ['length', 'width', 'height']
32 | columns = [{"name": i, "id": i} for i in list_columns]
33 |
34 | layout = html.Div([
35 | html.Div([
36 | dash_canvas.DashCanvas(
37 | id='canvas-line',
38 | width=canvas_width,
39 | height=canvas_height,
40 | scale=scale,
41 | lineWidth=2,
42 | lineColor='red',
43 | tool='line',
44 | image_content=array_to_data_url(img),
45 | goButtonTitle='Measure',
46 | ),
47 | ], className="seven columns"),
48 | html.Div([
49 | html.H2('Draw lines and measure object lengths'),
50 | html.H4(children="Objects properties"),
51 | html.Div(id='sh_x', hidden=True),
52 | dash_table.DataTable(
53 | id='table-line',
54 | columns=columns,
55 | editable=True,
56 | )
57 | ], className="four columns"),
58 | ])
59 |
60 |
61 | def callbacks(app):
62 |
63 | @app.callback(Output('table-line', 'data'),
64 | [Input('canvas-line', 'json_data')])
65 | def show_string(string):
66 | props = parse_jsonstring_line(string)
67 | df = pd.DataFrame(props, columns=list_columns)
68 | return df.to_dict("records")
69 |
--------------------------------------------------------------------------------
/assets/gallery-style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans');
2 | @import url('https://fonts.googleapis.com/css?family=Dosis');
3 |
4 | #index-waitfor {
5 | background-color: #DFE8F3;
6 | position: absolute;
7 | width: 100%;
8 | left: 0px;
9 | top: 0px;
10 | }
11 |
12 | #gallery-title {
13 | text-align: center;
14 | font-size: 36pt;
15 | font-family: 'Dosis';
16 | }
17 | #gallery-subtitle {
18 | text-align: center;
19 | font-size: 18pt;
20 | font-family: 'Open Sans'
21 | }
22 |
23 | #gallery-apps{
24 | position: static;
25 | margin: 0 auto;
26 | padding: 20px;
27 | text-align: center;
28 | width:calc(100% - 40px); ;
29 | height: auto;
30 | min-height: 100vh;
31 | }
32 |
33 | .gallery-app {
34 | position: relative;
35 | display: inline-block;
36 | height: 300px;
37 | width: 25%;
38 | min-width: 275px;
39 | padding: 5px;
40 | margin-right: 20px;
41 | margin-bottom:10px;
42 | vertical-align: top;
43 | text-align: left;
44 | overflow: hidden;
45 | border-radius: 15px;
46 | }
47 | .gallery-app-img {
48 | transition-duration:500ms;
49 | object-fit: cover;
50 | width: 100%;
51 | height: 100%;
52 | transform: scale(1.1);
53 | border-radius: 15px;
54 | }
55 | .gallery-app-info {
56 | height: calc(100% - 20px);
57 | border-radius: 10px;
58 | padding: 10px;
59 | position: absolute;
60 | top: 10px;
61 | left: 10px;
62 | opacity: 0;
63 | }
64 | .gallery-app-name {
65 | color: white;
66 | font-family: 'Dosis';
67 | font-size: 24pt;
68 | line-height: 28pt;
69 | font-weight: 100 !important;
70 | }
71 | .gallery-app-desc {
72 | max-height: 160px;
73 | width: calc(100% - 30px);
74 | overflow-y: auto;
75 | color: white;
76 | font-family: 'Open Sans';
77 | font-size: 11pt;
78 | margin: 15px;
79 | margin-top: 25px;
80 | }
81 | .gallery-app ::-webkit-scrollbar {
82 | display: none;
83 | }
84 |
85 | .gallery-app:hover .gallery-app-info, .gallery-app:hover .gallery-app-link {
86 | opacity: 1;
87 | }
88 | .gallery-app:hover .gallery-app-img {
89 | -webkit-filter: blur(5px) grayscale(0.5) brightness(30%);
90 | }}
91 |
92 |
--------------------------------------------------------------------------------
/usage.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import json
3 | from skimage import io
4 |
5 | import dash_canvas
6 | import dash
7 | from dash.dependencies import Input, Output
8 | import dash_html_components as html
9 | import dash_core_components as dcc
10 | import plotly.graph_objs as go
11 |
12 | from parse_json import parse_jsonstring
13 | from image_processing_utils import watershed_segmentation
14 | from plot_utils import image_with_contour
15 |
16 | # Image to segment and shape parameters
17 | filename = 'https://upload.wikimedia.org/wikipedia/commons/e/e4/Mitochondria%2C_mammalian_lung_-_TEM_%282%29.jpg'
18 | img = io.imread(filename, as_gray=True)
19 | print(img.dtype)
20 | height, width = img.shape
21 | canvas_width = 400
22 | canvas_height = int(height * canvas_width / width)
23 | scale = canvas_width / width
24 |
25 | # ------------------ App definition ---------------------
26 |
27 | app = dash.Dash(__name__)
28 |
29 | app.css.append_css({
30 | 'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css'
31 | })
32 |
33 |
34 | app.scripts.config.serve_locally = True
35 | app.css.config.serve_locally = True
36 |
37 |
38 | app.layout = html.Div([
39 | html.Div([
40 | html.Div([
41 | html.H2(children='Segmentation tool'),
42 | dcc.Markdown('''
43 | Paint on each object you want to segment
44 | then press the Save button to trigger the segmentation.
45 | '''),
46 |
47 | dash_canvas.DashCanvas(
48 | id='canvas',
49 | label='my-label',
50 | width=canvas_width,
51 | height=canvas_height,
52 | scale=scale,
53 | filename=filename,
54 | ),
55 | ], className="six columns"),
56 | html.Div([
57 | html.H2(children='Segmentation result'),
58 | dcc.Graph(
59 | id='segmentation',
60 | figure=image_with_contour(img, img>0)
61 | )
62 | ], className="six columns")],# Div
63 | className="row")
64 | ])
65 |
66 | # ----------------------- Callbacks -----------------------------
67 |
68 | @app.callback(Output('segmentation', 'figure'),
69 | [Input('canvas', 'json_data')])
70 | def update_figure(string):
71 | mask = parse_jsonstring(string, shape=(height, width))
72 | seg = watershed_segmentation(img, mask)
73 | return image_with_contour(img, seg)
74 |
75 |
76 | if __name__ == '__main__':
77 | app.run_server(debug=True)
78 |
--------------------------------------------------------------------------------
/extract-meta.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const reactDocs = require('react-docgen');
6 |
7 | const componentPaths = process.argv.slice(2);
8 | if (!componentPaths.length) {
9 | help();
10 | process.exit(1);
11 | }
12 |
13 | const metadata = Object.create(null);
14 | componentPaths.forEach(componentPath =>
15 | collectMetadataRecursively(componentPath)
16 | );
17 | writeOut(metadata);
18 |
19 | function help() {
20 | console.error('usage: ');
21 | console.error(
22 | 'extract-meta path/to/component(s) ' +
23 | ' [path/to/more/component(s), ...] > metadata.json'
24 | );
25 | }
26 |
27 | function writeError(msg, filePath) {
28 | if (filePath) {
29 | process.stderr.write(`Error with path ${filePath}`);
30 | }
31 |
32 | process.stderr.write(msg + '\n');
33 | if (msg instanceof Error) {
34 | process.stderr.write(msg.stack + '\n');
35 | }
36 | }
37 |
38 | function checkWarn(name, value) {
39 | if (value.length < 1) {
40 | process.stderr.write(`\nDescription for ${name} is missing!\n`)
41 | }
42 | }
43 |
44 | function docstringWarning(doc) {
45 | checkWarn(doc.displayName, doc.description);
46 |
47 | Object.entries(doc.props).forEach(
48 | ([name, p]) => checkWarn(`${doc.displayName}.${name}`, p.description)
49 | );
50 | }
51 |
52 |
53 | function parseFile(filepath) {
54 | const urlpath = filepath.split(path.sep).join('/');
55 | let src;
56 |
57 | if (!['.jsx', '.js'].includes(path.extname(filepath))) {
58 | return;
59 | }
60 |
61 | try {
62 | src = fs.readFileSync(filepath);
63 | const doc = metadata[urlpath] = reactDocs.parse(src);
64 | docstringWarning(doc);
65 | } catch (error) {
66 | writeError(error, filepath);
67 | }
68 | }
69 |
70 | function collectMetadataRecursively(componentPath) {
71 | if (fs.lstatSync(componentPath).isDirectory()) {
72 | let dirs;
73 | try {
74 | dirs = fs.readdirSync(componentPath);
75 | } catch (error) {
76 | writeError(error, componentPath);
77 | }
78 | dirs.forEach(filename => {
79 | const filepath = path.join(componentPath, filename);
80 | if (fs.lstatSync(filepath).isDirectory()) {
81 | collectMetadataRecursively(filepath);
82 | } else {
83 | parseFile(filepath);
84 | }
85 | });
86 | } else {
87 | parseFile(componentPath);
88 | }
89 | }
90 |
91 | function writeOut(result) {
92 | console.log(JSON.stringify(result, '\t', 2));
93 | }
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dash_canvas",
3 | "version": "0.1.0",
4 | "description": "Sketching pad for dash based on react-sketch",
5 | "repository": {
6 | "type": "git",
7 | "url": "git@github.com:plotly/dash-canvas.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/plotly/dash-canvas/issues"
11 | },
12 | "homepage": "https://github.com/plotly/dash-canvas",
13 | "main": "build/index.js",
14 | "scripts": {
15 | "start": "webpack-serve ./webpack.serve.config.js --open",
16 | "validate-init": "python _validate_init.py",
17 | "build:js-dev": "webpack --mode development",
18 | "build:js": "webpack --mode production",
19 | "build:py": "node ./extract-meta.js src/lib/components > dash_canvas/metadata.json && copyfiles package.json dash_canvas && python -c \"import dash; dash.development.component_loader.generate_classes('dash_canvas', 'dash_canvas/metadata.json')\"",
20 | "build": "npm run build:js && npm run build:js-dev && npm run build:py && npm run validate-init",
21 | "postbuild": "es-check es5 dash_canvas/*.js"
22 | },
23 | "author": "Emmanuelle Gouillart ",
24 | "license": "MIT",
25 | "dependencies": {
26 | "material-ui": "^0.20.0",
27 | "plotly-icons": ">=1.0",
28 | "react": "16.13.0",
29 | "react-color": "^2.18.0",
30 | "react-dom": "16.13.0",
31 | "react-sketch": ">=0.4.4"
32 | },
33 | "devDependencies": {
34 | "@babel/cli": "^7.6.2",
35 | "@babel/core": "^7.6.2",
36 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
37 | "@babel/preset-env": "^7.6.2",
38 | "@babel/preset-react": "^7.0.0",
39 | "@plotly/webpack-dash-dynamic-import": "^1.1.4",
40 | "babel-eslint": "^8.2.3",
41 | "babel-loader": "^8.0.6",
42 | "babel-preset-env": "^1.7.0",
43 | "babel-preset-react": "^6.24.1",
44 | "copyfiles": "^2.0.0",
45 | "css-loader": "^0.28.11",
46 | "es-check": "^5.0.0",
47 | "eslint": "^4.19.1",
48 | "eslint-config-prettier": "^2.9.0",
49 | "eslint-plugin-import": "^2.12.0",
50 | "eslint-plugin-react": "^7.9.1",
51 | "flexboxgrid": "^6.3.1",
52 | "npm": "^6.1.0",
53 | "react-docgen": "^4.1.1",
54 | "react-dropzone": "4.2.7",
55 | "style-loader": "^0.21.0",
56 | "webpack": "^4.41.0",
57 | "webpack-cli": "^3.3.9",
58 | "webpack-serve": "^1.0.2"
59 | },
60 | "peerDependencies": {
61 | "react": ">=0.14",
62 | "react-dom": ">=0.14"
63 | },
64 | "engines": {
65 | "node": ">=8.11.0",
66 | "npm": ">=6.1.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/dash_canvas/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dash_canvas",
3 | "version": "0.1.0",
4 | "description": "Sketching pad for dash based on react-sketch",
5 | "repository": {
6 | "type": "git",
7 | "url": "git@github.com:plotly/dash-canvas.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/plotly/dash-canvas/issues"
11 | },
12 | "homepage": "https://github.com/plotly/dash-canvas",
13 | "main": "build/index.js",
14 | "scripts": {
15 | "start": "webpack-serve ./webpack.serve.config.js --open",
16 | "validate-init": "python _validate_init.py",
17 | "build:js-dev": "webpack --mode development",
18 | "build:js": "webpack --mode production",
19 | "build:py": "node ./extract-meta.js src/lib/components > dash_canvas/metadata.json && copyfiles package.json dash_canvas && python -c \"import dash; dash.development.component_loader.generate_classes('dash_canvas', 'dash_canvas/metadata.json')\"",
20 | "build": "npm run build:js && npm run build:js-dev && npm run build:py && npm run validate-init",
21 | "postbuild": "es-check es5 dash_canvas/*.js"
22 | },
23 | "author": "Emmanuelle Gouillart ",
24 | "license": "MIT",
25 | "dependencies": {
26 | "material-ui": "^0.20.0",
27 | "plotly-icons": ">=1.0",
28 | "react": "16.13.0",
29 | "react-color": "^2.18.0",
30 | "react-dom": "16.13.0",
31 | "react-sketch": ">=0.4.4"
32 | },
33 | "devDependencies": {
34 | "@babel/cli": "^7.6.2",
35 | "@babel/core": "^7.6.2",
36 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
37 | "@babel/preset-env": "^7.6.2",
38 | "@babel/preset-react": "^7.0.0",
39 | "@plotly/webpack-dash-dynamic-import": "^1.1.4",
40 | "babel-eslint": "^8.2.3",
41 | "babel-loader": "^8.0.6",
42 | "babel-preset-env": "^1.7.0",
43 | "babel-preset-react": "^6.24.1",
44 | "copyfiles": "^2.0.0",
45 | "css-loader": "^0.28.11",
46 | "es-check": "^5.0.0",
47 | "eslint": "^4.19.1",
48 | "eslint-config-prettier": "^2.9.0",
49 | "eslint-plugin-import": "^2.12.0",
50 | "eslint-plugin-react": "^7.9.1",
51 | "flexboxgrid": "^6.3.1",
52 | "npm": "^6.1.0",
53 | "react-docgen": "^4.1.1",
54 | "react-dropzone": "4.2.7",
55 | "style-loader": "^0.21.0",
56 | "webpack": "^4.41.0",
57 | "webpack-cli": "^3.3.9",
58 | "webpack-serve": "^1.0.2"
59 | },
60 | "peerDependencies": {
61 | "react": ">=0.14",
62 | "react-dom": ">=0.14"
63 | },
64 | "engines": {
65 | "node": ">=8.11.0",
66 | "npm": ">=6.1.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/dash_canvas/utils/plot_utils.py:
--------------------------------------------------------------------------------
1 | import plotly.graph_objs as go
2 | import PIL
3 | import numpy as np
4 | from skimage import color, img_as_ubyte
5 | from plotly import colors
6 |
7 | def image_with_contour(img, labels, mode='lines', shape=None):
8 | """
9 | Figure with contour plot of labels superimposed on background image.
10 |
11 | Parameters
12 | ----------
13 |
14 | img : URL, dataURI or ndarray
15 | Background image. If a numpy array, it is transformed into a PIL
16 | Image object.
17 | labels : 2D ndarray
18 | Contours are the isolines of labels.
19 | shape: tuple, optional
20 | Shape of the arrays, to be provided if ``img`` is not a numpy array.
21 | """
22 | try:
23 | sh_y, sh_x = shape if shape is not None else img.shape
24 | except AttributeError:
25 | print('''the shape of the image must be provided with the
26 | ``shape`` parameter if ``img`` is not a numpy array''')
27 | if type(img) == np.ndarray:
28 | img = img_as_ubyte(color.gray2rgb(img))
29 | img = PIL.Image.fromarray(img)
30 | labels = labels.astype(np.float)
31 | custom_viridis = colors.PLOTLY_SCALES['Viridis']
32 | custom_viridis.insert(0, [0, '#FFFFFF'])
33 | custom_viridis[1][0] = 1.e-4
34 | # Contour plot of segmentation
35 | print('mode is', mode)
36 | opacity = 0.4 if mode is None else 1
37 | cont = go.Contour(z=labels[::-1],
38 | contours=dict(start=0, end=labels.max() + 1, size=1,
39 | coloring=mode),
40 | line=dict(width=1),
41 | showscale=False,
42 | colorscale=custom_viridis,
43 | opacity=opacity,
44 | )
45 | # Layout
46 | layout= go.Layout(
47 | images = [dict(
48 | source=img,
49 | xref="x",
50 | yref="y",
51 | x=0,
52 | y=sh_y,
53 | sizex=sh_x,
54 | sizey=sh_y,
55 | sizing="contain",
56 | layer="below")],
57 | xaxis=dict(
58 | showgrid=False,
59 | zeroline=False,
60 | showline=False,
61 | ticks='',
62 | showticklabels=False,
63 | ),
64 | yaxis=dict(
65 | showgrid=False,
66 | zeroline=False,
67 | showline=False,
68 | scaleanchor="x",
69 | ticks='',
70 | showticklabels=False,),
71 | margin=dict(b=5, t=20))
72 | fig = go.Figure(data=[cont], layout=layout)
73 | return fig
74 |
75 |
76 | if __name__ == '__main__':
77 | from skimage import data
78 | import plotly.plotly as py
79 | camera = data.camera()
80 | fig = image_with_contour(camera, camera > 150)
81 | py.iplot(fig)
82 |
--------------------------------------------------------------------------------
/dash_canvas/AlternativeCanvas.py:
--------------------------------------------------------------------------------
1 | # AUTO GENERATED FILE - DO NOT EDIT
2 |
3 | from dash.development.base_component import Component, _explicitize_args
4 |
5 |
6 | class AlternativeCanvas(Component):
7 | """A AlternativeCanvas component.
8 | AlternativeCanvas is an example component.
9 | It takes a property, `label`, and
10 | displays it.
11 | It renders an input with the property `value`
12 | which is editable by the user.
13 |
14 | Keyword arguments:
15 | - id (string; optional): The ID used to identify this component in Dash callbacks
16 | - label (string; required): A label that will be printed when this component is rendered.
17 | - value (string; optional): The value displayed in the input
18 | - lineColor (string; optional): the color of the line
19 |
20 | Available events: """
21 | @_explicitize_args
22 | def __init__(self, id=Component.UNDEFINED, label=Component.REQUIRED, value=Component.UNDEFINED, lineColor=Component.UNDEFINED, **kwargs):
23 | self._prop_names = ['id', 'label', 'value', 'lineColor']
24 | self._type = 'AlternativeCanvas'
25 | self._namespace = 'dash_canvas'
26 | self._valid_wildcard_attributes = []
27 | self.available_events = []
28 | self.available_properties = ['id', 'label', 'value', 'lineColor']
29 | self.available_wildcard_properties = []
30 |
31 | _explicit_args = kwargs.pop('_explicit_args')
32 | _locals = locals()
33 | _locals.update(kwargs) # For wildcard attrs
34 | args = {k: _locals[k] for k in _explicit_args if k != 'children'}
35 |
36 | for k in ['label']:
37 | if k not in args:
38 | raise TypeError(
39 | 'Required argument `' + k + '` was not specified.')
40 | super(AlternativeCanvas, self).__init__(**args)
41 |
42 | def __repr__(self):
43 | if(any(getattr(self, c, None) is not None
44 | for c in self._prop_names
45 | if c is not self._prop_names[0])
46 | or any(getattr(self, c, None) is not None
47 | for c in self.__dict__.keys()
48 | if any(c.startswith(wc_attr)
49 | for wc_attr in self._valid_wildcard_attributes))):
50 | props_string = ', '.join([c+'='+repr(getattr(self, c, None))
51 | for c in self._prop_names
52 | if getattr(self, c, None) is not None])
53 | wilds_string = ', '.join([c+'='+repr(getattr(self, c, None))
54 | for c in self.__dict__.keys()
55 | if any([c.startswith(wc_attr)
56 | for wc_attr in
57 | self._valid_wildcard_attributes])])
58 | return ('AlternativeCanvas(' + props_string +
59 | (', ' + wilds_string if wilds_string != '' else '') + ')')
60 | else:
61 | return (
62 | 'AlternativeCanvas(' +
63 | repr(getattr(self, self._prop_names[0], None)) + ')')
64 |
--------------------------------------------------------------------------------
/tests/test_dash_canvas.py:
--------------------------------------------------------------------------------
1 | import json
2 | from functools import partial
3 |
4 | from skimage import img_as_ubyte
5 | import numpy as np
6 |
7 | import dash
8 | from dash.dependencies import Input, Output, State
9 | from dash.exceptions import PreventUpdate
10 | import dash_html_components as html
11 | import dash_core_components as dcc
12 |
13 | import dash_canvas
14 | from dash_canvas.utils import array_to_data_url
15 |
16 | from selenium.webdriver.support.ui import WebDriverWait
17 |
18 | def _get_button_by_title(dash_duo, title):
19 | return dash_duo.wait_for_element(
20 | 'button[title="{}"]'.format(title)
21 | )
22 |
23 |
24 | TIMEOUT = 10
25 |
26 | def test_canvas_undo_redo(dash_duo):
27 | h, w = 10, 10
28 | overlay = np.zeros((h, w), dtype=np.uint8)
29 | overlay = img_as_ubyte(overlay)
30 |
31 | calls = 0
32 | data_saved = []
33 |
34 | # Set up a small app. This could probably be made into a fixture.
35 | app = dash.Dash(__name__)
36 | app.layout = html.Div([
37 | dcc.Store(id='cache', data=''),
38 | dash_canvas.DashCanvas(
39 | id="canvas",
40 | width=w,
41 | height=h,
42 | image_content=array_to_data_url(overlay),
43 | goButtonTitle="save"
44 | )
45 | ])
46 |
47 | @app.callback(
48 | Output('cache', 'data'),
49 | [Input("canvas", "trigger")],
50 | [State("canvas", "json_data")]
51 | )
52 | def update_overlay(flag, data):
53 | if flag is None or data is None:
54 | raise PreventUpdate
55 |
56 | data_saved.append(data)
57 |
58 | nonlocal calls
59 | calls = calls + 1
60 |
61 | def calls_equals(count, driver):
62 | nonlocal calls
63 | return calls == count
64 |
65 | dash_duo.start_server(app)
66 |
67 | # At application startup, a black 10x10 image is shown. When we click
68 | # save, we expect a non-trivial JSON object representing this image. We
69 | # assert that we get this object, but we don't dig into it.
70 | btn = _get_button_by_title(dash_duo, "Save")
71 | btn.click()
72 |
73 | WebDriverWait(dash_duo.driver, TIMEOUT).until(partial(calls_equals, 1))
74 | objs_1 = json.loads(data_saved[-1])['objects']
75 | assert len(objs_1) > 0
76 |
77 | # When we click "undo", the image disappears. We check that we get an
78 | # empty JSON representation back.
79 | btn = _get_button_by_title(dash_duo, "Undo")
80 | btn.click()
81 | btn = _get_button_by_title(dash_duo, "Save")
82 | btn.click()
83 | WebDriverWait(dash_duo.driver, TIMEOUT).until(partial(calls_equals, 2))
84 |
85 | objs_2 = json.loads(data_saved[-1])['objects']
86 | assert objs_2 == []
87 |
88 | # When we click "redo", the original 10x10 black image is restored.
89 | btn = _get_button_by_title(dash_duo, "Redo")
90 | btn.click()
91 | btn = _get_button_by_title(dash_duo, "Save")
92 | btn.click()
93 | WebDriverWait(dash_duo.driver, TIMEOUT).until(partial(calls_equals, 3))
94 |
95 | objs_3 = json.loads(data_saved[-1])['objects']
96 | assert objs_1 == objs_3
97 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const WebpackDashDynamicImport = require('@plotly/webpack-dash-dynamic-import');
3 |
4 | const packagejson = require('./package.json');
5 |
6 | const dashLibraryName = packagejson.name.replace(/-/g, '_');
7 |
8 | module.exports = (env, argv) => {
9 |
10 | let mode;
11 |
12 | const overrides = module.exports || {};
13 |
14 | // if user specified mode flag take that value
15 | if (argv && argv.mode) {
16 | mode = argv.mode;
17 | }
18 |
19 | // else if configuration object is already set (module.exports) use that value
20 | else if (overrides.mode) {
21 | mode = overrides.mode;
22 | }
23 |
24 | // else take webpack default (production)
25 | else {
26 | mode = 'production';
27 | }
28 |
29 | let filename = (overrides.output || {}).filename;
30 | if (!filename) {
31 | const modeSuffix = mode === 'development' ? 'dev' : 'min';
32 | filename = `${dashLibraryName}.${modeSuffix}.js`;
33 | }
34 |
35 | const entry = overrides.entry || { main: './src/lib/index.js' };
36 |
37 | const devtool = overrides.devtool || (
38 | mode === 'development' ? "eval-source-map" : 'none'
39 | );
40 |
41 | const externals = ('externals' in overrides) ? overrides.externals : ({
42 | react: 'React',
43 | 'react-dom': 'ReactDOM',
44 | 'plotly.js': 'Plotly',
45 | });
46 |
47 | return {
48 | mode,
49 | entry,
50 | output: {
51 | path: path.resolve(__dirname, dashLibraryName),
52 | chunkFilename: mode === 'development' ? '[name].dev.js' : '[name].js',
53 | filename,
54 | library: dashLibraryName,
55 | libraryTarget: 'window',
56 | },
57 | externals,
58 | module: {
59 | rules: [
60 | {
61 | test: /\.js$/,
62 | exclude: /node_modules/,
63 | use: {
64 | loader: 'babel-loader',
65 | },
66 | },
67 | {
68 | test: /\.css$/,
69 | use: [
70 | {
71 | loader: 'style-loader',
72 | },
73 | {
74 | loader: 'css-loader',
75 | },
76 | ],
77 | },
78 | ],
79 | },
80 | devtool,
81 | optimization: {
82 | splitChunks: {
83 | name: true,
84 | cacheGroups: {
85 | async: {
86 | chunks: 'async',
87 | minSize: 0,
88 | name(module, chunks, cacheGroupKey) {
89 | return `${cacheGroupKey}-${chunks[0].name}`;
90 | }
91 | }
92 | }
93 | }
94 | },
95 | plugins: [
96 | new WebpackDashDynamicImport()
97 | ]
98 | }
99 | };
100 |
--------------------------------------------------------------------------------
/dash_canvas/DashCanvas.py:
--------------------------------------------------------------------------------
1 | # AUTO GENERATED FILE - DO NOT EDIT
2 |
3 | from dash.development.base_component import Component, _explicitize_args
4 |
5 |
6 | class DashCanvas(Component):
7 | """A DashCanvas component.
8 | Canvas component for drawing on a background image and selecting
9 | regions.
10 |
11 | Keyword arguments:
12 | - id (string; optional): The ID used to identify this component in Dash callbacks
13 | - image_content (string; default ''): Image data string, formatted as png or jpg data string. Can be
14 | generated by utils.io_utils.array_to_data_string.
15 | - zoom (number; default 1): Zoom factor
16 | - width (number; default 500): Width of the canvas
17 | - height (number; default 500): Height of the canvas
18 | - scale (number; default 1): Scaling ratio between canvas width and image width
19 | - tool (string; default "pencil"): Selection of drawing tool, among ["pencil", "pan", "circle",
20 | "rectangle", "select", "line"].
21 | - lineWidth (number; default 10): Width of drawing line (in pencil mode)
22 | - lineColor (string; default 'red'): Color of drawing line (in pencil mode). Can be a text string,
23 | like 'yellow', 'red', or a color triplet like 'rgb(255, 0, 0)'.
24 | Alpha is possible with 'rgba(255, 0, 0, 0.5)'.
25 | - goButtonTitle (string; default 'Save'): Title of button
26 | - filename (string; default ''): Name of image file to load (URL string)
27 | - trigger (number; default 0): Counter of how many times the save button was pressed
28 | (to be used mostly as input)
29 | - json_data (string; default ''): Sketch content as JSON string, containing background image and
30 | annotations. Use utils.parse_json.parse_jsonstring to parse
31 | this string.
32 | - hide_buttons (list of strings; optional): Names of buttons to hide. Names are "zoom", "pan", "line", "pencil",
33 | "rectangle", "undo", "select"."""
34 | @_explicitize_args
35 | def __init__(self, id=Component.UNDEFINED, image_content=Component.UNDEFINED, zoom=Component.UNDEFINED, width=Component.UNDEFINED, height=Component.UNDEFINED, scale=Component.UNDEFINED, tool=Component.UNDEFINED, lineWidth=Component.UNDEFINED, lineColor=Component.UNDEFINED, goButtonTitle=Component.UNDEFINED, filename=Component.UNDEFINED, trigger=Component.UNDEFINED, json_data=Component.UNDEFINED, hide_buttons=Component.UNDEFINED, **kwargs):
36 | self._prop_names = ['id', 'image_content', 'zoom', 'width', 'height', 'scale', 'tool', 'lineWidth', 'lineColor', 'goButtonTitle', 'filename', 'trigger', 'json_data', 'hide_buttons']
37 | self._type = 'DashCanvas'
38 | self._namespace = 'dash_canvas'
39 | self._valid_wildcard_attributes = []
40 | self.available_properties = ['id', 'image_content', 'zoom', 'width', 'height', 'scale', 'tool', 'lineWidth', 'lineColor', 'goButtonTitle', 'filename', 'trigger', 'json_data', 'hide_buttons']
41 | self.available_wildcard_properties = []
42 |
43 | _explicit_args = kwargs.pop('_explicit_args')
44 | _locals = locals()
45 | _locals.update(kwargs) # For wildcard attrs
46 | args = {k: _locals[k] for k in _explicit_args if k != 'children'}
47 |
48 | for k in []:
49 | if k not in args:
50 | raise TypeError(
51 | 'Required argument `' + k + '` was not specified.')
52 | super(DashCanvas, self).__init__(**args)
53 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at accounts@plot.ly. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4/), and may also be found online at .
44 |
--------------------------------------------------------------------------------
/tests/data_test.json:
--------------------------------------------------------------------------------
1 | "{\"objects\":[{\"type\":\"image\",\"originX\":\"left\",\"originY\":\"top\",\"left\":0,\"top\":0,\"width\":640,\"height\":433,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeLineJoin\":\"miter\",\"strokeMiterLimit\":10,\"scaleX\":0.78,\"scaleY\":0.78,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"crossOrigin\":\"\",\"alignX\":\"none\",\"alignY\":\"none\",\"meetOrSlice\":\"meet\",\"src\":\"https://upload.wikimedia.org/wikipedia/commons/e/e4/Mitochondria%2C_mammalian_lung_-_TEM_%282%29.jpg\",\"filters\":[],\"resizeFilters\":[]},{\"type\":\"path\",\"originX\":\"left\",\"originY\":\"top\",\"left\":67,\"top\":186.97,\"width\":39.03,\"height\":39.06,\"fill\":null,\"stroke\":\"black\",\"strokeWidth\":30,\"strokeDashArray\":null,\"strokeLineCap\":\"round\",\"strokeLineJoin\":\"round\",\"strokeMiterLimit\":10,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"pathOffset\":{\"x\":101.515,\"y\":221.5},\"path\":[[\"M\",82,201.97],[\"Q\",82,202,82,202.5],[\"Q\",82,203,82.5,203.5],[\"Q\",83,204,83.5,205],[\"Q\",84,206,85.5,208.5],[\"Q\",87,211,95,218],[\"Q\",103,225,104.5,227],[\"Q\",106,229,106.5,229],[\"Q\",107,229,107.5,229.5],[\"Q\",108,230,109.5,231.5],[\"Q\",111,233,113,234.5],[\"Q\",115,236,117,237.5],[\"Q\",119,239,120,240],[\"L\",121.03,241.03]]},{\"type\":\"path\",\"originX\":\"left\",\"originY\":\"top\",\"left\":208.97,\"top\":117.97,\"width\":10.06,\"height\":41.06,\"fill\":null,\"stroke\":\"black\",\"strokeWidth\":30,\"strokeDashArray\":null,\"strokeLineCap\":\"round\",\"strokeLineJoin\":\"round\",\"strokeMiterLimit\":10,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"pathOffset\":{\"x\":229,\"y\":153.5},\"path\":[[\"M\",223.97,132.97],[\"Q\",224,133,224.5,133.5],[\"Q\",225,134,225,135],[\"Q\",225,136,225.5,138],[\"Q\",226,140,227,143.5],[\"Q\",228,147,228.5,149.5],[\"Q\",229,152,229,154.5],[\"Q\",229,157,229.5,158.5],[\"Q\",230,160,231.5,165],[\"Q\",233,170,233.5,172],[\"L\",234.03,174.03]]},{\"type\":\"path\",\"originX\":\"left\",\"originY\":\"top\",\"left\":323.97,\"top\":105,\"width\":48.06,\"height\":4,\"fill\":null,\"stroke\":\"black\",\"strokeWidth\":30,\"strokeDashArray\":null,\"strokeLineCap\":\"round\",\"strokeLineJoin\":\"round\",\"strokeMiterLimit\":10,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"pathOffset\":{\"x\":363,\"y\":122},\"path\":[[\"M\",338.97,124],[\"Q\",339,124,340,124],[\"Q\",341,124,342,124],[\"Q\",343,124,346,123.5],[\"Q\",349,123,353,122],[\"Q\",357,121,360.5,120.5],[\"Q\",364,120,368.5,120],[\"Q\",373,120,376,120],[\"Q\",379,120,383,120],[\"L\",387.03,120]]}]}"
2 |
--------------------------------------------------------------------------------
/review_checklist.md:
--------------------------------------------------------------------------------
1 | # Code Review Checklist
2 |
3 | ## Code quality & design
4 |
5 | - Is your code clear? If you had to go back to it in a month, would you be happy to? If someone else had to contribute to it, would they be able to?
6 |
7 | A few suggestions:
8 |
9 | - Make your variable names descriptive and use the same naming conventions throughout the code.
10 |
11 | - For more complex pieces of logic, consider putting a comment, and maybe an example.
12 |
13 | - In the comments, focus on describing _why_ the code does what it does, rather than describing _what_ it does. The reader can most likely read the code, but not necessarily understand why it was necessary.
14 |
15 | - Don't overdo it in the comments. The code should be clear enough to speak for itself. Stale comments that no longer reflect the intent of the code can hurt code comprehension.
16 |
17 | * Don't repeat yourself. Any time you see that the same piece of logic can be applied in multiple places, factor it out into a function, or variable, and reuse that code.
18 | * Scan your code for expensive operations (large computations, DOM queries, React re-renders). Have you done your possible to limit their impact? If not, it is going to slow your app down.
19 | * Can you think of cases where your current code will break? How are you handling errors? Should the user see them as notifications? Should your app try to auto-correct them for them?
20 |
21 | ## Component API
22 |
23 | - Have you tested your component on the Python side by creating an app in `usage.py` ?
24 |
25 | Do all of your component's props work when set from the back-end?
26 |
27 | Should all of them be settable from the back-end or are some only relevant to user interactions in the front-end?
28 |
29 | - Have you provided some basic documentation about your component? The Dash community uses [react docstrings](https://github.com/plotly/dash-docs/blob/master/tutorial/plugins.py#L45) to provide basic information about dash components. Take a look at this [Checklist component example](https://github.com/plotly/dash-core-components/blob/master/src/components/Checklist.react.js) and others from the dash-core-components repository.
30 |
31 | At a minimum, you should describe what your component does, and describe its props and the features they enable.
32 |
33 | Be careful to use the correct formatting for your docstrings for them to be properly recognized.
34 |
35 | ## Tests
36 |
37 | - The Dash team uses integration tests extensively, and we highly encourage you to write tests for the main functionality of your component. In the `tests` folder of the boilerplate, you can see a sample integration test. By launching it, you will run a sample Dash app in a browser. You can run the test with:
38 | ```
39 | python -m tests.test_render
40 | ```
41 | [Browse the Dash component code on GitHub for more examples of testing.](https://github.com/plotly/dash-core-components)
42 |
43 | ## Ready to publish? Final scan
44 |
45 | - Take a last look at the external resources that your component is using. Are all the external resources used [referenced in `MANIFEST.in`](https://github.com/plotly/dash-docs/blob/0b2fd8f892db720a7f3dc1c404b4cff464b5f8d4/tutorial/plugins.py#L55)?
46 |
47 | - [You're ready to publish!](https://github.com/plotly/dash-component-boilerplate/blob/master/%7B%7Bcookiecutter.project_shortname%7D%7D/README.md#create-a-production-build-and-publish)
48 |
--------------------------------------------------------------------------------
/src/lib/components/DashCanvas.react.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component, lazy, Suspense } from 'react';
3 |
4 | // eslint-disable-next-line no-inline-comments
5 | const RealDashCanvas = lazy(() => import(/* webpackChunkName: "canvas" */ '../fragments/DashCanvas.react'));
6 |
7 | /**
8 | * Canvas component for drawing on a background image and selecting
9 | * regions.
10 | */
11 | export default class DashCanvas extends Component {
12 | render() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | DashCanvas.defaultProps = {
22 | filename: '',
23 | json_data: '', image_content: '', trigger: 0,
24 | width: 500, height: 500, scale: 1, lineWidth: 10,
25 | lineColor: 'red', tool: "pencil", zoom: 1,
26 | goButtonTitle: 'Save', hide_buttons: []
27 | };
28 |
29 | DashCanvas.propTypes = {
30 | /**
31 | * The ID used to identify this component in Dash callbacks
32 | */
33 | id: PropTypes.string,
34 |
35 | /**
36 | * Image data string, formatted as png or jpg data string. Can be
37 | * generated by utils.io_utils.array_to_data_string.
38 | */
39 | image_content: PropTypes.string,
40 |
41 | /**
42 | * Zoom factor
43 | */
44 | zoom: PropTypes.number,
45 |
46 |
47 | /**
48 | * Width of the canvas
49 | */
50 | width: PropTypes.number,
51 |
52 | /**
53 | * Height of the canvas
54 | */
55 | height: PropTypes.number,
56 |
57 | /**
58 | * Scaling ratio between canvas width and image width
59 | */
60 | scale: PropTypes.number,
61 |
62 | /**
63 | * Selection of drawing tool, among ["pencil", "pan", "circle",
64 | * "rectangle", "select", "line"].
65 | */
66 | tool: PropTypes.string,
67 |
68 | /**
69 | * Width of drawing line (in pencil mode)
70 | */
71 | lineWidth: PropTypes.number,
72 |
73 | /**
74 | * Color of drawing line (in pencil mode). Can be a text string,
75 | * like 'yellow', 'red', or a color triplet like 'rgb(255, 0, 0)'.
76 | * Alpha is possible with 'rgba(255, 0, 0, 0.5)'.
77 | */
78 | lineColor: PropTypes.string,
79 |
80 | /**
81 | * Title of button
82 | */
83 | goButtonTitle: PropTypes.string,
84 |
85 |
86 | /**
87 | * Name of image file to load (URL string)
88 | */
89 | filename: PropTypes.string,
90 |
91 |
92 | /**
93 | * Counter of how many times the save button was pressed
94 | * (to be used mostly as input)
95 | */
96 | trigger: PropTypes.number,
97 |
98 | /**
99 | * Sketch content as JSON string, containing background image and
100 | * annotations. Use utils.parse_json.parse_jsonstring to parse
101 | * this string.
102 | */
103 | json_data: PropTypes.string,
104 |
105 | /**
106 | * Names of buttons to hide. Names are "zoom", "pan", "line", "pencil",
107 | * "rectangle", "undo", "select".
108 | */
109 | hide_buttons: PropTypes.arrayOf(PropTypes.string),
110 |
111 | /**
112 | * Dash-assigned callback that should be called whenever any of the
113 | * properties change
114 | */
115 | setProps: PropTypes.func
116 | };
117 |
118 | export const propTypes = DashCanvas.propTypes;
119 | export const defaultProps = DashCanvas.defaultProps;
120 |
--------------------------------------------------------------------------------
/dash_canvas/test/test_registration.py:
--------------------------------------------------------------------------------
1 | from dash_canvas.utils import register_tiles
2 | import numpy as np
3 | from skimage import data, color
4 | import matplotlib.pyplot as plt
5 |
6 | def test_stitching_one_row():
7 | im = data.moon()
8 | l = 256
9 |
10 | n_rows = 1
11 | n_cols = im.shape[1] // l
12 | top, left = 50, 0
13 | init_i, init_j = top, left
14 |
15 | imgs = np.empty((n_rows, n_cols, l, l))
16 |
17 | overlap_h = [30, 50]
18 |
19 | i = 0
20 | for j in range(n_cols):
21 | sub_im = im[init_i:init_i + l, init_j:init_j + l]
22 | imgs[i, j] = sub_im
23 | init_j += l - overlap_h[1]
24 | init_i += - overlap_h[0]
25 |
26 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2)
27 | real_top = min(top, top - overlap_h[0])
28 | delta = im[real_top:real_top+stitch.shape[0], :stitch.shape[1]].astype(np.float) - stitch.astype(np.float)
29 | assert np.all(delta[50:-50] == 0)
30 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2,
31 | blending=False)
32 | delta = im[real_top:real_top+stitch.shape[0], :stitch.shape[1]].astype(np.float) - stitch.astype(np.float)
33 | assert np.all(delta[50:-50] == 0)
34 |
35 | # local_overlap
36 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.5, pad=l//2,
37 | overlap_local={(0, 1):[20, 45]})
38 | delta = im[real_top:stitch.shape[0]+real_top, :stitch.shape[1]].astype(np.float) - stitch.astype(np.float)
39 | assert np.all(delta[50:-50] == 0)
40 |
41 | imgs = np.empty((n_rows, n_cols, l, l))
42 |
43 | overlap_h = [-30, 50]
44 | real_top = min(top, top - overlap_h[0])
45 | init_i, init_j = top, left
46 |
47 | i = 0
48 | for j in range(n_cols):
49 | sub_im = im[init_i:init_i + l, init_j:init_j + l]
50 | imgs[i, j] = sub_im
51 | init_j += l - overlap_h[1]
52 | init_i += -overlap_h[0]
53 |
54 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.5, pad=l//2,
55 | overlap_local={(0, 1):[-20, 45]})
56 | delta = im[real_top:stitch.shape[0]+real_top, :stitch.shape[1]].astype(np.float) - stitch.astype(np.float)
57 | assert np.all(delta[50:-50] == 0)
58 |
59 |
60 | def test_stitching_two_rows():
61 | im = data.moon()
62 | l = 256
63 | # two rows
64 | n_rows = 2
65 | n_cols = im.shape[1] // l
66 | init_i, init_j = 0, 0
67 | overlap_h = [5, 50]
68 | overlap_v = 40
69 |
70 | imgs = np.empty((n_rows, n_cols, l, l))
71 | for i in range(n_rows):
72 | for j in range(n_cols):
73 | sub_im = im[init_i:init_i + l, init_j:init_j + l]
74 | imgs[i, j] = sub_im
75 | init_j += l - overlap_h[1]
76 | init_j = 0
77 | init_i += l - overlap_v
78 |
79 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2)
80 | delta = im[:stitch.shape[0], :stitch.shape[1]].astype(np.float) - stitch.astype(np.float)
81 | print(delta.mean())
82 | assert np.all(delta == 0)
83 |
84 |
85 | def test_stitching_color():
86 | im = color.gray2rgb(data.moon())
87 | l = 256
88 |
89 | n_rows = 1
90 | n_cols = im.shape[1] // l
91 |
92 | init_i, init_j = 0, 0
93 |
94 | imgs = np.empty((n_rows, n_cols, l, l, 3))
95 |
96 | overlap_h = [5, 50]
97 |
98 | i = 0
99 | for j in range(n_cols):
100 | sub_im = im[init_i:init_i + l, init_j:init_j + l]
101 | imgs[i, j] = sub_im
102 | init_j += l - overlap_h[1]
103 |
104 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2)
105 | delta = im[:l, :stitch.shape[1]].astype(np.float) - stitch.astype(np.float)
106 | assert np.all(delta == 0)
107 |
108 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["eslint:recommended", "prettier"],
3 | "parser": "babel-eslint",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module",
7 | "ecmaFeatures": {
8 | "arrowFunctions": true,
9 | "blockBindings": true,
10 | "classes": true,
11 | "defaultParams": true,
12 | "destructuring": true,
13 | "forOf": true,
14 | "generators": true,
15 | "modules": true,
16 | "templateStrings": true,
17 | "jsx": true
18 | }
19 | },
20 | "env": {
21 | "browser": true,
22 | "es6": true,
23 | "jasmine": true,
24 | "jest": true,
25 | "node": true
26 | },
27 | "globals": {
28 | "jest": true
29 | },
30 | "plugins": [
31 | "react",
32 | "import"
33 | ],
34 | "rules": {
35 | "accessor-pairs": ["error"],
36 | "block-scoped-var": ["error"],
37 | "consistent-return": ["error"],
38 | "curly": ["error", "all"],
39 | "default-case": ["error"],
40 | "dot-location": ["off"],
41 | "dot-notation": ["error"],
42 | "eqeqeq": ["error"],
43 | "guard-for-in": ["off"],
44 | "import/named": ["off"],
45 | "import/no-duplicates": ["error"],
46 | "import/no-named-as-default": ["error"],
47 | "new-cap": ["error"],
48 | "no-alert": [1],
49 | "no-caller": ["error"],
50 | "no-case-declarations": ["error"],
51 | "no-console": ["off"],
52 | "no-div-regex": ["error"],
53 | "no-dupe-keys": ["error"],
54 | "no-else-return": ["error"],
55 | "no-empty-pattern": ["error"],
56 | "no-eq-null": ["error"],
57 | "no-eval": ["error"],
58 | "no-extend-native": ["error"],
59 | "no-extra-bind": ["error"],
60 | "no-extra-boolean-cast": ["error"],
61 | "no-inline-comments": ["error"],
62 | "no-implicit-coercion": ["error"],
63 | "no-implied-eval": ["error"],
64 | "no-inner-declarations": ["off"],
65 | "no-invalid-this": ["error"],
66 | "no-iterator": ["error"],
67 | "no-labels": ["error"],
68 | "no-lone-blocks": ["error"],
69 | "no-loop-func": ["error"],
70 | "no-multi-str": ["error"],
71 | "no-native-reassign": ["error"],
72 | "no-new": ["error"],
73 | "no-new-func": ["error"],
74 | "no-new-wrappers": ["error"],
75 | "no-param-reassign": ["error"],
76 | "no-process-env": ["warn"],
77 | "no-proto": ["error"],
78 | "no-redeclare": ["error"],
79 | "no-return-assign": ["error"],
80 | "no-script-url": ["error"],
81 | "no-self-compare": ["error"],
82 | "no-sequences": ["error"],
83 | "no-shadow": ["off"],
84 | "no-throw-literal": ["error"],
85 | "no-undefined": ["error"],
86 | "no-unused-expressions": ["error"],
87 | "no-use-before-define": ["error", "nofunc"],
88 | "no-useless-call": ["error"],
89 | "no-useless-concat": ["error"],
90 | "no-with": ["error"],
91 | "prefer-const": ["error"],
92 | "radix": ["error"],
93 | "react/jsx-no-duplicate-props": ["error"],
94 | "react/jsx-no-undef": ["error"],
95 | "react/jsx-uses-react": ["error"],
96 | "react/jsx-uses-vars": ["error"],
97 | "react/no-did-update-set-state": ["error"],
98 | "react/no-direct-mutation-state": ["error"],
99 | "react/no-is-mounted": ["error"],
100 | "react/no-unknown-property": ["error"],
101 | "react/prefer-es6-class": ["error", "always"],
102 | "react/prop-types": "error",
103 | "valid-jsdoc": ["off"],
104 | "yoda": ["error"],
105 | "spaced-comment": ["error", "always", {
106 | "block": {
107 | exceptions: ["*"]
108 | }
109 | }],
110 | "no-unused-vars": ["error", {
111 | "args": "after-used",
112 | "argsIgnorePattern": "^_",
113 | "caughtErrorsIgnorePattern": "^e$"
114 | }],
115 | "no-magic-numbers": ["error", {
116 | "ignoreArrayIndexes": true,
117 | "ignore": [-1, 0, 1, 2, 3, 100, 10, 0.5]
118 | }],
119 | "no-underscore-dangle": ["off"]
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/index.py:
--------------------------------------------------------------------------------
1 | import dash_core_components as dcc
2 | import dash_html_components as html
3 | from dash.dependencies import Input, Output
4 | from glob import glob
5 | import base64
6 | import dash
7 |
8 | import app1_seg as app1
9 | import app2_correct_segmentation as app2
10 | import app3_background_removal as app3
11 | import app4_measure_length as app4
12 | import app5_stitching as app5
13 |
14 | app = dash.Dash(__name__)
15 | server = app.server
16 | app.config.suppress_callback_exceptions = True
17 |
18 |
19 | app.layout = html.Div([
20 | dcc.Location(id='url', refresh=False),
21 | html.Div(id='page-content')
22 | ])
23 |
24 | apps = {'app1': app1, 'app2': app2, 'app3': app3, 'app4': app4,
25 | 'app5': app5}
26 |
27 | for key in apps:
28 | try:
29 | apps[key].callbacks(app)
30 | except AttributeError:
31 | continue
32 |
33 |
34 | def demo_app_desc(name):
35 | """ Returns the content of the description specified in the app. """
36 | desc = ''
37 | try:
38 | desc = apps[name].description()
39 | except AttributeError:
40 | pass
41 | return desc
42 |
43 |
44 | def demo_app_name(name):
45 | """ Returns a capitalized title for the app, with "Dash"
46 | in front."""
47 | desc = ''
48 | try:
49 | desc = apps[name].title()
50 | except AttributeError:
51 | pass
52 | return desc
53 |
54 |
55 | def demo_app_link_id(name):
56 | """Returns the value of the id of the dcc.Link related to the demo app. """
57 | return 'app-link-id-{}'.format(name)
58 |
59 |
60 | def demo_app_img_src(name):
61 | """ Returns the base-64 encoded image corresponding
62 | to the specified app."""
63 | pic_fname = './app_pics/{}.png'.format(
64 | name
65 | )
66 | try:
67 | return 'data:image/png;base64,{}'.format(
68 | base64.b64encode(
69 | open(pic_fname, 'rb').read()).decode())
70 | except Exception:
71 | return 'data:image/png;base64,{}'.format(
72 | base64.b64encode(
73 | open('./assets/dashbio_logo.png', 'rb').read()).decode())
74 |
75 |
76 |
77 | @app.callback(Output('page-content', 'children'),
78 | [Input('url', 'pathname')])
79 | def display_page(pathname):
80 | if pathname is not None and len(pathname) > 1 and pathname[1:] in apps.keys():
81 | app_name = pathname[1:]
82 | return html.Div(id="waitfor",
83 | children=apps[app_name].layout,
84 | )
85 | else:
86 | return html.Div(
87 | id='gallery-apps',
88 | children=[
89 | html.Div(className='gallery-app', children=[
90 | dcc.Link(
91 | children=[
92 | html.Img(className='gallery-app-img',
93 | src=demo_app_img_src(name)),
94 | html.Div(className='gallery-app-info',
95 | children=[
96 | html.Div(className='gallery-app-name',
97 | children=[
98 | demo_app_name(name)
99 | ]),
100 | html.Div(className='gallery-app-desc',
101 | children=[demo_app_desc(name)
102 | ]),
103 |
104 | ])
105 | ],
106 | id=demo_app_link_id(name),
107 | href="/{}".format(
108 | name.replace("app_", "").replace("_", "-")
109 | )
110 | )
111 | ]) for name in apps
112 | ])
113 |
114 |
115 |
116 | server = app.server
117 |
118 | if __name__ == '__main__':
119 | app.run_server(debug=True)
120 |
--------------------------------------------------------------------------------
/dash_canvas/utils/parse_json.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import json
3 | from skimage import draw, morphology
4 | from scipy import ndimage
5 |
6 |
7 | def _indices_of_path(path, scale=1):
8 | """
9 | Retrieve pixel indices (integer values).
10 |
11 | Parameters
12 | ----------
13 |
14 | path: SVG-like path formatted as JSON string
15 | The path is formatted like
16 | ['M', x0, y0],
17 | ['Q', xc1, yc1, xe1, ye1],
18 | ['Q', xc2, yc2, xe2, ye2],
19 | ...
20 | ['L', xn, yn]
21 | where (xc, yc) are for control points and (xe, ye) for end points.
22 |
23 | Notes
24 | -----
25 |
26 | I took a weight of 1 and it seems fine from visual inspection.
27 | """
28 | rr, cc = [], []
29 | for (Q1, Q2) in zip(path[:-2], path[1:-1]):
30 | # int(round()) is for Python 2 compatibility
31 | inds = draw.bezier_curve(int(round(Q1[-1] / scale)),
32 | int(round(Q1[-2] / scale)),
33 | int(round(Q2[2] / scale)),
34 | int(round(Q2[1] / scale)),
35 | int(round(Q2[4] / scale)),
36 | int(round(Q2[3] / scale)), 1)
37 | rr += list(inds[0])
38 | cc += list(inds[1])
39 | return rr, cc
40 |
41 |
42 | def parse_jsonstring(string, shape=None, scale=1):
43 | """
44 | Parse JSON string to draw the path saved by react-sketch.
45 |
46 | Up to now only path objects are processed (created with Pencil tool).
47 |
48 | Parameters
49 | ----------
50 |
51 | data : str
52 | JSON string of data
53 | shape: tuple, optional
54 | shape of returned image.
55 |
56 | Returns
57 | -------
58 |
59 | mask: ndarray of bools
60 | binary array where the painted regions are one.
61 | """
62 | if shape is None:
63 | shape = (500, 500)
64 | mask = np.zeros(shape, dtype=np.bool)
65 | try:
66 | data = json.loads(string)
67 | except:
68 | return mask
69 | scale = 1
70 | for obj in data['objects']:
71 | if obj['type'] == 'image':
72 | scale = obj['scaleX']
73 | elif obj['type'] == 'path':
74 | scale_obj = obj['scaleX']
75 | inds = _indices_of_path(obj['path'], scale=scale / scale_obj)
76 | radius = round(obj['strokeWidth'] / 2. / scale)
77 | mask_tmp = np.zeros(shape, dtype=np.bool)
78 | mask_tmp[inds[0], inds[1]] = 1
79 | mask_tmp = ndimage.binary_dilation(mask_tmp,
80 | morphology.disk(radius))
81 | mask += mask_tmp
82 | return mask
83 |
84 |
85 | def parse_jsonstring_line(string):
86 | """
87 | Return geometry of line objects.
88 |
89 | Parameters
90 | ----------
91 |
92 | data : str
93 | JSON string of data
94 |
95 | """
96 | try:
97 | data = json.loads(string)
98 | except:
99 | return None
100 | scale = 1
101 | props = []
102 | for obj in data['objects']:
103 | if obj['type'] == 'image':
104 | scale = obj['scaleX']
105 | elif obj['type'] == 'line':
106 | length = np.sqrt(obj['width']**2 + obj['height']**2)
107 | scale_factor = obj['scaleX'] / scale
108 | props.append([scale_factor * length,
109 | scale_factor * obj['width'],
110 | scale_factor * obj['height'],
111 | scale_factor * obj['left'],
112 | scale_factor * obj['top']])
113 | return (np.array(props)).astype(np.int)
114 |
115 |
116 | def parse_jsonstring_rectangle(string):
117 | """
118 | Return geometry of rectangle objects.
119 |
120 | Parameters
121 | ----------
122 |
123 | data : str
124 | JSON string of data
125 |
126 | """
127 | try:
128 | data = json.loads(string)
129 | except:
130 | return None
131 | scale = 1
132 | props = []
133 | for obj in data['objects']:
134 | if obj['type'] == 'image':
135 | scale = obj['scaleX']
136 | elif obj['type'] == 'rect':
137 | scale_factor = obj['scaleX'] / scale
138 | props.append([scale_factor * obj['width'],
139 | scale_factor * obj['height'],
140 | scale_factor * obj['left'],
141 | scale_factor * obj['top']])
142 | return (np.array(props)).astype(np.int)
143 |
144 |
145 | def parse_jsonfile(filename, shape=None):
146 | with open(filename, 'r') as fp:
147 | string = json.load(fp)
148 | return parse_jsonstring(string, shape=shape)
149 |
150 |
151 |
--------------------------------------------------------------------------------
/app2_correct_segmentation.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import json
3 | from skimage import io, color, segmentation, img_as_ubyte, filters, measure
4 | from PIL import Image
5 |
6 |
7 | import dash_canvas
8 | import dash
9 | from dash.dependencies import Input, Output, State
10 | import dash_html_components as html
11 | import dash_core_components as dcc
12 | import plotly.graph_objs as go
13 |
14 | from dash_canvas.utils.parse_json import parse_jsonstring
15 | from dash_canvas.utils.io_utils import image_string_to_PILImage, array_to_data_url
16 | from dash_canvas.utils.image_processing_utils import modify_segmentation
17 |
18 |
19 | # Image to segment and shape parameters
20 | filename = 'https://upload.wikimedia.org/wikipedia/commons/1/1b/HumanChromosomesChromomycinA3.jpg'
21 | img = io.imread(filename, as_gray=True)
22 | mask = img > 1.2 * filters.threshold_otsu(img)
23 | labels = measure.label(mask)
24 |
25 |
26 | overlay = segmentation.mark_boundaries(img, labels)
27 | overlay = img_as_ubyte(overlay)
28 |
29 | height, width = img.shape[:2]
30 | canvas_width = 500
31 | canvas_height = round(height * canvas_width / width)
32 | scale = canvas_width / width
33 |
34 | # ------------------ App definition ---------------------
35 |
36 |
37 | def title():
38 | return "Segmentation post-processing"
39 |
40 |
41 | def description():
42 | return "Merge or separate labeled regions in order to improve an automatic segmentation"
43 |
44 |
45 | layout = html.Div([
46 | html.Div([
47 | html.H3(children='Manual correction of automatic segmentation'),
48 | dcc.Markdown('''
49 | Annotate the picture to delineate boundaries
50 | between objects (in split mode) or to join objects
51 | together (in merge mode), then press the
52 | "Update segmentation" button to correct
53 | the segmentation.
54 | '''),
55 | html.H5(children='Annotations'),
56 | dcc.RadioItems(id='mode',
57 | options=[
58 | {'label': 'Merge objects', 'value': 'merge'},
59 | {'label': 'Split objects', 'value': 'split'},
60 | ],
61 | value='split',
62 | # labelStyle={'display': 'inline-block'}
63 | ),
64 | html.H5(children='Save segmentation'),
65 | dcc.RadioItems(id='save-mode',
66 | options=[
67 | {'label': 'png', 'value': 'png'},
68 | #{'label': 'raw', 'value': 'raw'},
69 | ],
70 | value='png',
71 | labelStyle={'display': 'inline-block'}
72 | ),
73 | html.A(
74 | 'Download Data',
75 | id='download-link',
76 | download="correct_segmentation.png",
77 | href="",
78 | target="_blank"
79 | ),
80 | dcc.Store(id='cache', data=''),
81 |
82 | ], className="four columns"),
83 | html.Div([
84 | dash_canvas.DashCanvas(
85 | id='canvas_',
86 | width=canvas_width,
87 | height=canvas_height,
88 | scale=scale,
89 | lineWidth=2,
90 | lineColor='red',
91 | image_content=array_to_data_url(overlay),
92 | goButtonTitle='Update segmentation',
93 | ),
94 | ], className="six columns"),
95 | ])
96 |
97 | # ----------------------- Callbacks -----------------------------
98 |
99 |
100 | def callbacks(app):
101 | @app.callback(Output('cache', 'data'),
102 | [Input('canvas_', 'trigger'),],
103 | [State('canvas_', 'json_data'),
104 | State('canvas_', 'scale'),
105 | State('canvas_', 'height'),
106 | State('canvas_', 'width'),
107 | State('cache', 'data'),
108 | State('mode', 'value')])
109 | def update_segmentation(toggle, string, s, h, w, children, mode):
110 | print("updating")
111 | if len(children) == 0:
112 | labs = labels
113 | else:
114 | labs = np.asarray(children)
115 | with open('data.json', 'w') as fp:
116 | json.dump(string, fp)
117 | mask = parse_jsonstring(string, shape=(height, width))
118 | new_labels = modify_segmentation(labs, mask, img=img, mode=mode)
119 | return new_labels
120 |
121 |
122 | @app.callback(Output('canvas_', 'image_content'),
123 | [Input('cache', 'data')])
124 | def update_figure(labs):
125 | new_labels = np.array(labs)
126 | overlay = segmentation.mark_boundaries(img, new_labels)
127 | overlay = img_as_ubyte(overlay)
128 | return array_to_data_url(overlay)
129 |
130 |
131 | @app.callback(Output('download-link', 'download'),
132 | [Input('save-mode', 'value')])
133 | def download_name(save_mode):
134 | if save_mode == 'png':
135 | return 'correct_segmentation.png'
136 | else:
137 | return 'correct_segmentation.raw'
138 |
139 |
140 | @app.callback(Output('download-link', 'href'),
141 | [Input('cache', 'data')],
142 | [State('save-mode', 'value')])
143 | def save_segmentation(labs, save_mode):
144 | new_labels = np.array(labs)
145 | np.save('labels.npy', new_labels)
146 | if save_mode == 'png':
147 | color_labels = color.label2rgb(new_labels)
148 | uri = array_to_data_url(new_labels, dtype=np.uint8)
149 | return uri
150 |
151 |
--------------------------------------------------------------------------------
/dash_canvas/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "src/lib/components/DashCanvas.react.js": {
3 | "description": "Canvas component for drawing on a background image and selecting\nregions.",
4 | "displayName": "DashCanvas",
5 | "methods": [],
6 | "props": {
7 | "id": {
8 | "type": {
9 | "name": "string"
10 | },
11 | "required": false,
12 | "description": "The ID used to identify this component in Dash callbacks"
13 | },
14 | "image_content": {
15 | "type": {
16 | "name": "string"
17 | },
18 | "required": false,
19 | "description": "Image data string, formatted as png or jpg data string. Can be\ngenerated by utils.io_utils.array_to_data_string.",
20 | "defaultValue": {
21 | "value": "''",
22 | "computed": false
23 | }
24 | },
25 | "zoom": {
26 | "type": {
27 | "name": "number"
28 | },
29 | "required": false,
30 | "description": "Zoom factor",
31 | "defaultValue": {
32 | "value": "1",
33 | "computed": false
34 | }
35 | },
36 | "width": {
37 | "type": {
38 | "name": "number"
39 | },
40 | "required": false,
41 | "description": "Width of the canvas",
42 | "defaultValue": {
43 | "value": "500",
44 | "computed": false
45 | }
46 | },
47 | "height": {
48 | "type": {
49 | "name": "number"
50 | },
51 | "required": false,
52 | "description": "Height of the canvas",
53 | "defaultValue": {
54 | "value": "500",
55 | "computed": false
56 | }
57 | },
58 | "scale": {
59 | "type": {
60 | "name": "number"
61 | },
62 | "required": false,
63 | "description": "Scaling ratio between canvas width and image width",
64 | "defaultValue": {
65 | "value": "1",
66 | "computed": false
67 | }
68 | },
69 | "tool": {
70 | "type": {
71 | "name": "string"
72 | },
73 | "required": false,
74 | "description": "Selection of drawing tool, among [\"pencil\", \"pan\", \"circle\",\n\"rectangle\", \"select\", \"line\"].",
75 | "defaultValue": {
76 | "value": "\"pencil\"",
77 | "computed": false
78 | }
79 | },
80 | "lineWidth": {
81 | "type": {
82 | "name": "number"
83 | },
84 | "required": false,
85 | "description": "Width of drawing line (in pencil mode)",
86 | "defaultValue": {
87 | "value": "10",
88 | "computed": false
89 | }
90 | },
91 | "lineColor": {
92 | "type": {
93 | "name": "string"
94 | },
95 | "required": false,
96 | "description": "Color of drawing line (in pencil mode). Can be a text string,\nlike 'yellow', 'red', or a color triplet like 'rgb(255, 0, 0)'.\nAlpha is possible with 'rgba(255, 0, 0, 0.5)'.",
97 | "defaultValue": {
98 | "value": "'red'",
99 | "computed": false
100 | }
101 | },
102 | "goButtonTitle": {
103 | "type": {
104 | "name": "string"
105 | },
106 | "required": false,
107 | "description": "Title of button",
108 | "defaultValue": {
109 | "value": "'Save'",
110 | "computed": false
111 | }
112 | },
113 | "filename": {
114 | "type": {
115 | "name": "string"
116 | },
117 | "required": false,
118 | "description": "Name of image file to load (URL string)",
119 | "defaultValue": {
120 | "value": "''",
121 | "computed": false
122 | }
123 | },
124 | "trigger": {
125 | "type": {
126 | "name": "number"
127 | },
128 | "required": false,
129 | "description": "Counter of how many times the save button was pressed\n(to be used mostly as input)",
130 | "defaultValue": {
131 | "value": "0",
132 | "computed": false
133 | }
134 | },
135 | "json_data": {
136 | "type": {
137 | "name": "string"
138 | },
139 | "required": false,
140 | "description": "Sketch content as JSON string, containing background image and\nannotations. Use utils.parse_json.parse_jsonstring to parse\nthis string.",
141 | "defaultValue": {
142 | "value": "''",
143 | "computed": false
144 | }
145 | },
146 | "hide_buttons": {
147 | "type": {
148 | "name": "arrayOf",
149 | "value": {
150 | "name": "string"
151 | }
152 | },
153 | "required": false,
154 | "description": "Names of buttons to hide. Names are \"zoom\", \"pan\", \"line\", \"pencil\",\n\"rectangle\", \"undo\", \"select\".",
155 | "defaultValue": {
156 | "value": "[]",
157 | "computed": false
158 | }
159 | },
160 | "setProps": {
161 | "type": {
162 | "name": "func"
163 | },
164 | "required": false,
165 | "description": "Dash-assigned callback that should be called whenever any of the\nproperties change"
166 | }
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/dash_canvas/dash_canvas.min.js:
--------------------------------------------------------------------------------
1 | window.dash_canvas=function(e){function t(t){for(var n,o,i=t[0],a=t[1],u=0,c=[];u 0:
86 | if image is None:
87 | im = img
88 | image = img
89 | else:
90 | im = image_string_to_PILImage(image)
91 | im = np.asarray(im)
92 | seg = superpixel_color_segmentation(im, mask)
93 | else:
94 | if image is None:
95 | image = img
96 | seg = np.ones((h, w))
97 | fill_value = 255 * np.ones(3, dtype=np.uint8)
98 | dat = np.copy(im)
99 | dat[np.logical_not(seg)] = fill_value
100 | return array_to_data_url(dat)
101 |
102 |
103 |
104 | @app.callback(Output('canvas-bg', 'image_content'),
105 | [Input('upload-image-bg', 'contents')])
106 | def update_canvas_upload(image_string):
107 | if image_string is None:
108 | raise ValueError
109 | if image_string is not None:
110 | return image_string
111 | else:
112 | return None
113 |
114 |
115 | @app.callback(Output('canvas-bg', 'height'),
116 | [Input('upload-image-bg', 'contents')],
117 | [State('canvas-bg', 'width'),
118 | State('canvas-bg', 'height')])
119 | def update_canvas_upload_shape(image_string, w, h):
120 | if image_string is None:
121 | raise ValueError
122 | if image_string is not None:
123 | # very dirty hack, this should be made more robust using regexp
124 | im = image_string_to_PILImage(image_string)
125 | im_h, im_w = im.height, im.width
126 | return round(w / im_w * im_h)
127 | else:
128 | return canvas_height
129 |
130 |
131 | @app.callback(Output('canvas-bg', 'scale'),
132 | [Input('upload-image-bg', 'contents')])
133 | def update_canvas_upload_scale(image_string):
134 | if image_string is None:
135 | raise ValueError
136 | if image_string is not None:
137 | # very dirty hack, this should be made more robust using regexp
138 | im = image_string_to_PILImage(image_string)
139 | im_h, im_w = im.height, im.width
140 | return canvas_width / im_w
141 | else:
142 | return scale
143 |
144 |
145 | @app.callback(Output('canvas-bg', 'lineWidth'),
146 | [Input('bg-width-slider', 'value')])
147 | def update_canvas_linewidth(value):
148 | return value
149 |
150 |
--------------------------------------------------------------------------------
/dash_canvas/utils/registration.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from skimage import io, measure, feature
3 | from scipy import ndimage
4 |
5 |
6 | def autocrop(img):
7 | """
8 | Remove zero-valued rectangles at the border of the image.
9 |
10 | Parameters
11 | ----------
12 |
13 | img: ndarray
14 | Image to be cropped
15 | """
16 | slices = ndimage.find_objects(img > 0)[0]
17 | return img[slices]
18 |
19 |
20 | def _blending_mask(shape):
21 | mask = np.zeros(shape, dtype=np.int)
22 | mask[1:-1, 1:-1] = 1
23 | return ndimage.distance_transform_cdt(mask) + 1
24 |
25 |
26 | def register_tiles(imgs, n_rows, n_cols, overlap_global=None,
27 | overlap_local=None, pad=None, blending=True):
28 | """
29 | Stitch together overlapping tiles of a mosaic, using Fourier-based
30 | registration to estimate the shifts between neighboring tiles.
31 |
32 | Parameters
33 | ----------
34 |
35 | imgs: array of tiles, of shape (n_rows, n_cols, l_r, l_r) with (l_c, l_r)
36 | the shape of individual tiles.
37 | n_rows: int
38 | number of rows of the mosaic.
39 | n_cols : int
40 | number of columns of the mosaic.
41 | overlap_global : float
42 | Fraction of overlap between tiles.
43 | overlap_local : dictionary
44 | Local overlaps between pairs of tiles. overlap_local[(i, j)] is a pair
45 | of (x, y) shifts giving the 2D shift vector between tiles i and j.
46 | Indices (i, j) are the raveled indices of the tile numbers.
47 | pad : int
48 | Value of the padding used at the border of the stitched image. An
49 | autocrop is performed at the end to remove the unnecessary padding.
50 |
51 | Notes
52 | -----
53 |
54 | Fourier-based registration is used in this function
55 | (skimage.feature.register_translation).
56 | """
57 | if pad is None:
58 | pad = 200
59 | l_r, l_c = imgs.shape[2:4]
60 | if overlap_global is None:
61 | overlap_global = 0.15
62 | overlap_value = int(float(overlap_global) * l_r)
63 | imgs = imgs.astype(np.float)
64 | if blending:
65 | blending_mask = _blending_mask((l_r, l_c))
66 | else:
67 | blending_mask = np.ones((l_r, l_c))
68 |
69 | if imgs.ndim == 4:
70 | canvas = np.zeros((2 * pad + n_rows * l_r, 2 * pad + n_cols * l_c),
71 | dtype=imgs.dtype)
72 | else:
73 | canvas = np.zeros((2 * pad + n_rows * l_r, 2 * pad + n_cols * l_c, 3),
74 | dtype=imgs.dtype)
75 | blending_mask = np.dstack((blending_mask, )*3)
76 | weights = np.zeros_like(canvas)
77 | init_r, init_c = pad, pad
78 | weighted_img = imgs[0, 0] * blending_mask
79 | canvas[init_r:init_r + l_r, init_c:init_c + l_c] = weighted_img
80 | weights[init_r:init_r + l_r, init_c:init_c + l_c] = blending_mask
81 | shifts = np.empty((n_rows, n_cols, 2), dtype=np.int)
82 | shifts[0, 0] = init_r, init_c
83 |
84 | for i_rows in range(n_rows):
85 | # Shifts between rows
86 | if i_rows >= 1:
87 | index_target = np.ravel_multi_index((i_rows, 0), (n_rows, n_cols))
88 | index_orig = index_target - n_cols
89 | try:
90 | overlap = overlap_local[(index_orig, index_target)]
91 | except (KeyError, TypeError):
92 | overlap = np.array([overlap_value, 0])
93 | init_r, init_c = shifts[i_rows - 1, 0]
94 | init_r += l_r
95 | shift_vert = feature.register_translation(
96 | imgs[i_rows - 1, 0, -overlap[0]:, :(l_c - overlap[1])],
97 | imgs[i_rows, 0, :overlap[0], -(l_c - overlap[1]):])[0]
98 | init_r += int(shift_vert[0]) - overlap[0]
99 | init_c += int(shift_vert[1]) - overlap[1]
100 | shifts[i_rows, 0] = init_r, init_c
101 | # Fill canvas and weights
102 | weighted_img = imgs[i_rows, 0] * blending_mask
103 | canvas[init_r:init_r + l_r, init_c:init_c + l_c] += weighted_img
104 | weights[init_r:init_r + l_r, init_c:init_c + l_c] += blending_mask
105 | # Shifts between columns
106 | for j_cols in range(n_cols - 1):
107 | index_orig = np.ravel_multi_index((i_rows, j_cols),
108 | (n_rows, n_cols))
109 | index_target = index_orig + 1
110 | try:
111 | overlap = overlap_local[(index_orig, index_target)]
112 | except (KeyError, TypeError):
113 | overlap = np.array([0, overlap_value])
114 | init_c += l_c
115 | if overlap[0] < 0:
116 | print("up")
117 | row_start_1 = -(l_r + overlap[0])
118 | row_end_1 = None
119 | row_start_2 = None
120 | row_end_2 = l_r + overlap[0]
121 | else:
122 | print("down")
123 | row_start_1 = None
124 | row_end_1= (l_r - overlap[0])
125 | row_start_2 = -(l_r - overlap[0])
126 | row_end_2 = None
127 | shift_horiz = feature.register_translation(
128 | imgs[i_rows, j_cols, row_start_1:row_end_1, -overlap[1]:],
129 | imgs[i_rows, j_cols + 1, row_start_2:row_end_2, :overlap[1]])[0]
130 | init_r += int(shift_horiz[0]) - (overlap[0])
131 | init_c += int(shift_horiz[1]) - overlap[1]
132 | shifts[i_rows, j_cols + 1] = init_r, init_c
133 | # Fill canvas and weights
134 | weighted_img = imgs[i_rows, j_cols + 1] * blending_mask
135 | canvas[init_r:init_r + l_r, init_c:init_c + l_c] += weighted_img
136 | weights[init_r:init_r + l_r, init_c:init_c + l_c] += blending_mask
137 |
138 | canvas /= (weights + 1.e-5)
139 | return autocrop(np.rint(canvas).astype(np.uint8))
140 |
--------------------------------------------------------------------------------
/src/lib/fragments/DashCanvas.react.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { SketchField, Tools } from 'react-sketch';
3 | import {
4 | ZoomMinusIcon, ZoomPlusIcon, EditIcon, PanIcon,
5 | ArrowLeftIcon, ArrowRightIcon, PlotLineIcon, SquareIcon, TagOutlineIcon
6 | }
7 | from 'plotly-icons';
8 |
9 | import { propTypes, defaultProps } from '../components/DashCanvas.react';
10 |
11 | const styles = {
12 | button: {
13 | margin: '3px',
14 | padding: '0px',
15 | width: '50px',
16 | height: '50px',
17 | verticalAlign: 'middle',
18 | },
19 |
20 | textbutton: {
21 | verticalAlign: 'top',
22 | height: '50px',
23 | color: 'blue',
24 | verticalAlign: 'middle',
25 | }
26 | };
27 |
28 | /**
29 | * Canvas component for drawing on a background image and selecting
30 | * regions.
31 | */
32 | export default class DashCanvas extends Component {
33 | constructor(props) {
34 | super(props);
35 | this.state = {
36 | height: 200
37 | };
38 | this._save = this._save.bind(this);
39 | this._undo = this._undo.bind(this);
40 | this._zoom = this._zoom.bind(this);
41 | this._zoom_factor = this._zoom_factor.bind(this);
42 | this._unzoom = this._unzoom.bind(this);
43 | this._pantool = this._pantool.bind(this);
44 | this._penciltool = this._penciltool.bind(this);
45 | this._linetool = this._linetool.bind(this);
46 | this._selecttool = this._selecttool.bind(this);
47 | }
48 |
49 |
50 | componentDidMount() {
51 | let sketch = this._sketch;
52 | if (this.props.filename.length > 0 ||
53 | this.props.image_content.length > 0) {
54 | var content = (this.props.filename.length > 0) ? this.props.filename :
55 | this.props.image_content;
56 | var img = new Image();
57 | img.onload = () => {
58 | var new_height = this.state.height;
59 | var new_scale = 1;
60 | var height = img.height;
61 | var width = img.width;
62 | new_height = Math.round(height * sketch.props.width / width);
63 | new_scale = new_height / height;
64 | this.setState({ height: new_height });
65 | sketch.clear();
66 | let opts = {
67 | left: 0,
68 | top: 0,
69 | scale: new_scale
70 | }
71 | sketch.addImg(content, opts);
72 | }
73 | img.src = content;
74 | } else {
75 | sketch._fc.setBackgroundColor(sketch.props.backgroundColor);
76 | }
77 | }
78 |
79 |
80 | componentDidUpdate(prevProps) {
81 | let sketch = this._sketch;
82 | // Typical usage (don't forget to compare props):
83 | if (
84 | (this.props.image_content !== prevProps.image_content)) {
85 | var img = new Image();
86 | var new_height = this.state.height;
87 | var new_scale = 1;
88 | img.onload = () => {
89 | var height = img.height;
90 | var width = img.width;
91 | new_height = Math.round(height * sketch.props.width / width);
92 | new_scale = new_height / height;
93 | this.setState({ height: new_height });
94 | sketch.clear();
95 | let opts = {
96 | left: 0,
97 | top: 0,
98 | scale: new_scale
99 | }
100 | sketch.addImg(this.props.image_content, opts);
101 | }
102 | img.src = this.props.image_content;
103 | if (this.props.setProps) {
104 | let JSON_string = JSON.stringify(this._sketch.toJSON());
105 | this.props.setProps({ json_data: JSON_string });
106 | }
107 |
108 | sketch._fc.setZoom(this.props.zoom);
109 | };
110 | };
111 |
112 |
113 | _save() {
114 | let JSON_string = JSON.stringify(this._sketch.toJSON());
115 | let toggle_value = this.props.trigger + 1
116 | if (this.props.setProps) {
117 | this.props.setProps({ json_data: JSON_string, trigger: toggle_value });
118 | }
119 | };
120 |
121 |
122 | _undo() {
123 | this._sketch.undo();
124 | this.setState({
125 | canUndo: this._sketch.canUndo(),
126 | canRedo: this._sketch.canRedo()
127 | })
128 | };
129 | _redo() {
130 | this._sketch.redo();
131 | console.log(this._sketch);
132 | this.setState({
133 | canUndo: this._sketch.canUndo(),
134 | canRedo: this._sketch.canRedo()
135 | })
136 | };
137 |
138 | _zoom_factor(factor) {
139 | this._sketch.zoom(factor);
140 | let zoom_factor = this.props.zoom;
141 | this.props.setProps({ zoom: factor * zoom_factor })
142 | };
143 |
144 |
145 | _zoom() {
146 | this._sketch.zoom(1.25);
147 | let zoom_factor = this.props.zoom;
148 | this.props.setProps({ zoom: 1.25 * zoom_factor })
149 | };
150 |
151 |
152 | _unzoom() {
153 | this._sketch.zoom(0.8);
154 | let zoom_factor = this.props.zoom;
155 | this.props.setProps({ zoom: 0.8 * zoom_factor });
156 | };
157 |
158 |
159 | _pantool() {
160 | this.props.setProps({ tool: "pan" });
161 | };
162 |
163 |
164 | _penciltool() {
165 | this.props.setProps({ tool: "pencil" });
166 | };
167 |
168 |
169 | _linetool() {
170 | this.props.setProps({ tool: "line" });
171 | };
172 |
173 |
174 | _rectangletool() {
175 | this.props.setProps({ tool: "rectangle" });
176 | };
177 |
178 |
179 |
180 | _selecttool() {
181 | this.props.setProps({ tool: "select" });
182 | };
183 |
184 |
185 |
186 |
187 | render() {
188 | var toolsArray = {};
189 | toolsArray["pencil"] = Tools.Pencil;
190 | toolsArray["pan"] = Tools.Pan;
191 | toolsArray["line"] = Tools.Line;
192 | toolsArray["circle"] = Tools.Circle;
193 | toolsArray["select"] = Tools.Select;
194 | toolsArray["rectangle"] = Tools.Rectangle;
195 | const hide_buttons = this.props.hide_buttons;
196 | const show_line = !(hide_buttons.includes("line"));
197 | const show_pan = !(hide_buttons.includes("pan"));
198 | const show_zoom = !(hide_buttons.includes("zoom"));
199 | const show_pencil = !(hide_buttons.includes("pencil"));
200 | const show_undo = !(hide_buttons.includes("undo"));
201 | const show_select = !(hide_buttons.includes("select"));
202 | const show_rectangle = !(hide_buttons.includes("rectangle"));
203 | var width_defined = this.props.width > 0;
204 | var width = width_defined ? this.props.width : null;
205 | return (
206 |