From 4c4bd422dce100986a68887ffd737642942f6434 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Wed, 3 Jun 2020 23:27:21 +0200 Subject: [PATCH 01/14] Select a random subset of keypoints --- app.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 686b4a0..14f151a 100644 --- a/app.py +++ b/app.py @@ -2,12 +2,17 @@ import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output, State - -import pandas +import random import plotly.graph_objects as go import plotly.express as px from skimage import data, transform + +KEYPOINTS = ['Nose', 'L_Eye', 'R_Eye', 'L_Ear', 'R_Ear', 'Throat', + 'Withers', 'TailSet', 'L_F_Paw', 'R_F_Paw', 'L_F_Wrist', + 'R_F_Wrist', 'L_F_Elbow', 'R_F_Elbow', 'L_B_Paw', 'R_B_Paw', + 'L_B_Hock', 'R_B_Hock', 'L_B_Stiffle', 'R_B_Stiffle'] + img = data.chelsea() img = img[::2, ::2] images = [img, img[::-1], transform.rotate(img, 30)] @@ -20,6 +25,7 @@ def make_figure_image(i): marker_cmin=0, marker_cmax=3, marker_size=18, mode='markers')) return fig + fig = make_figure_image(0) external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] @@ -27,7 +33,8 @@ def make_figure_image(i): app = dash.Dash(__name__, external_stylesheets=external_stylesheets) server = app.server -options = ['left eye', 'right eye', 'nose'] +options = random.sample(KEYPOINTS, 3) + app.layout = html.Div([ html.Div([ From a8cd80cd977970a69b714fbf9bfa89cf784f0581 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Wed, 3 Jun 2020 23:41:42 +0200 Subject: [PATCH 02/14] Avoid multiple keypoints per label --- app.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index 14f151a..6f5298c 100644 --- a/app.py +++ b/app.py @@ -22,11 +22,12 @@ def make_figure_image(i): fig = px.imshow(images[i % len(images)]) fig.update_traces(hoverinfo='none') fig.add_trace(go.Scatter(x=[], y=[], marker_color=[], - marker_cmin=0, marker_cmax=3, marker_size=18, mode='markers')) + marker_cmin=0, marker_cmax=3, marker_size=18, mode='markers')) return fig fig = make_figure_image(0) +fig.layout.hovermode = 'closest' external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] @@ -34,6 +35,7 @@ def make_figure_image(i): server = app.server options = random.sample(KEYPOINTS, 3) +already_labeled = set() app.layout = html.Div([ @@ -90,12 +92,13 @@ def display_click_data(clickData, option): return dash.no_update, dash.no_update if clickData is None or fig is None: return dash.no_update + n_bpt = options.index(option) + if n_bpt in already_labeled: + return dash.no_update + already_labeled.add(n_bpt) x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] - for i, el in enumerate(options): - if el == option: - new_option = options[(i+1)%(len(options))] - color=i - return [{'x':[[x]], 'y':[[y]], "marker.color":[[color]]}, [1]], new_option + new_option = options[min(len(options) - 1, n_bpt + 1)] + return [{'x':[[x]], 'y':[[y]], "marker.color":[[n_bpt]]}, [1]], new_option if __name__ == '__main__': From 8338340f0334f899f4622ef9996133ca4943719d Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 00:48:21 +0200 Subject: [PATCH 03/14] Disable tooltip --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 6f5298c..0e75dcc 100644 --- a/app.py +++ b/app.py @@ -20,7 +20,7 @@ def make_figure_image(i): fig = px.imshow(images[i % len(images)]) - fig.update_traces(hoverinfo='none') + fig.update_traces(hoverinfo='none', hovertemplate='') fig.add_trace(go.Scatter(x=[], y=[], marker_color=[], marker_cmin=0, marker_cmax=3, marker_size=18, mode='markers')) return fig From 2e9dfbbd230255fd90ba530722ea7e21194744e9 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 00:49:15 +0200 Subject: [PATCH 04/14] Add Clear button --- app.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 0e75dcc..f3472df 100644 --- a/app.py +++ b/app.py @@ -27,7 +27,6 @@ def make_figure_image(i): fig = make_figure_image(0) -fig.layout.hovermode = 'closest' external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] @@ -55,6 +54,7 @@ def make_figure_image(i): ), html.Button('Previous', id='previous'), html.Button('Next', id='next'), + html.Button('Clear', id='clear'), dcc.Store(id='store', data=0) ], className="six columns" @@ -66,16 +66,23 @@ def make_figure_image(i): [Output('basic-interactions', 'figure'), Output('store', 'data')], [Input('next', 'n_clicks'), - Input('previous', 'n_clicks')], + Input('previous', 'n_clicks'), + Input('clear', 'n_clicks')], [State('store', 'data')] ) -def display_click_data(n_clicks_n, n_clicks_p, val): - if n_clicks_n is None and n_clicks_p is None: +def display_click_data(n_clicks_n, n_clicks_p, n_clicks_c, val): + if not any(click for click in (n_clicks_n, n_clicks_p, n_clicks_c)): return dash.no_update, dash.no_update if val is None: val = 0 ctx = dash.callback_context button_id = ctx.triggered[0]['prop_id'].split('.')[0] + if button_id == 'clear': + fig = make_figure_image(val) + global already_labeled + already_labeled = set() + # app.layout.children[1].children[1].value = options[0] + return fig, val index = val + 1 if button_id == 'next' else val - 1 fig = make_figure_image(index) return fig, index @@ -90,11 +97,9 @@ def display_click_data(n_clicks_n, n_clicks_p, val): def display_click_data(clickData, option): if clickData is None or fig is None: return dash.no_update, dash.no_update - if clickData is None or fig is None: - return dash.no_update n_bpt = options.index(option) if n_bpt in already_labeled: - return dash.no_update + return dash.no_update, dash.no_update already_labeled.add(n_bpt) x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] new_option = options[min(len(options) - 1, n_bpt + 1)] From c3dfca9c531b634824ff8642c7ad6d6498e5e152 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 11:58:45 +0200 Subject: [PATCH 05/14] A couple of figure enhancements --- app.py | 53 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/app.py b/app.py index f3472df..15c3120 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,8 @@ import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output, State +import base64 +import os import random import plotly.graph_objects as go import plotly.express as px @@ -12,16 +14,21 @@ 'Withers', 'TailSet', 'L_F_Paw', 'R_F_Paw', 'L_F_Wrist', 'R_F_Wrist', 'L_F_Elbow', 'R_F_Elbow', 'L_B_Paw', 'R_B_Paw', 'L_B_Hock', 'R_B_Hock', 'L_B_Stiffle', 'R_B_Stiffle'] +IMAGE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'full_dog.png') + img = data.chelsea() img = img[::2, ::2] images = [img, img[::-1], transform.rotate(img, 30)] +encoded_image = base64.b64encode(open(IMAGE_PATH, 'rb').read()) def make_figure_image(i): fig = px.imshow(images[i % len(images)]) + fig.layout.xaxis.showticklabels = False + fig.layout.yaxis.showticklabels = False fig.update_traces(hoverinfo='none', hovertemplate='') - fig.add_trace(go.Scatter(x=[], y=[], marker_color=[], + fig.add_trace(go.Scatter(x=[], y=[], marker_color=[], text=[], hoverinfo=[], marker_cmin=0, marker_cmax=3, marker_size=18, mode='markers')) return fig @@ -36,30 +43,46 @@ def make_figure_image(i): options = random.sample(KEYPOINTS, 3) already_labeled = set() +styles = { + 'pre': { + 'border': 'thin lightgrey solid', + 'overflowX': 'scroll' + } +} app.layout = html.Div([ html.Div([ dcc.Graph( id='basic-interactions', - config={'editable':True}, - figure=fig, - )], - className="six columns" + config={'editable': False}, + figure=fig) + ], + className="six columns" ), html.Div([ html.H2("Controls"), dcc.RadioItems(id='radio', - options=[{'label':opt, 'value':opt} for opt in options], - value=options[0] - ), + options=[{'label':opt, 'value':opt} for opt in options], + value=options[0] + ), html.Button('Previous', id='previous'), html.Button('Next', id='next'), html.Button('Clear', id='clear'), dcc.Store(id='store', data=0) - ], - className="six columns" + ], + className="six columns" ), - ]) + html.Div([ + dcc.Markdown(""" + **Instructions**\n + Click on the image to add a keypoint. + """), + html.Pre(id='click-data', style=styles['pre']), + html.Img(src='data:image/png;charset=utf-8;base64,{}'.format(encoded_image)) + ], + className='three columns'), +] +) @app.callback( @@ -77,11 +100,11 @@ def display_click_data(n_clicks_n, n_clicks_p, n_clicks_c, val): val = 0 ctx = dash.callback_context button_id = ctx.triggered[0]['prop_id'].split('.')[0] + global already_labeled + already_labeled = set() + app.layout['radio'].value = options[0] if button_id == 'clear': fig = make_figure_image(val) - global already_labeled - already_labeled = set() - # app.layout.children[1].children[1].value = options[0] return fig, val index = val + 1 if button_id == 'next' else val - 1 fig = make_figure_image(index) @@ -103,7 +126,7 @@ def display_click_data(clickData, option): already_labeled.add(n_bpt) x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] new_option = options[min(len(options) - 1, n_bpt + 1)] - return [{'x':[[x]], 'y':[[y]], "marker.color":[[n_bpt]]}, [1]], new_option + return [{'x':[[x]], 'y':[[y]], "marker.color":[[n_bpt]], 'text':[[option]]}, [1]], new_option if __name__ == '__main__': From 7dac6372e9323b4dee6507b2ffb15a02663ee29f Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 14:02:34 +0200 Subject: [PATCH 06/14] Refactor to allow graph updates --- app.py | 71 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/app.py b/app.py index 15c3120..7d2934a 100644 --- a/app.py +++ b/app.py @@ -28,7 +28,7 @@ def make_figure_image(i): fig.layout.xaxis.showticklabels = False fig.layout.yaxis.showticklabels = False fig.update_traces(hoverinfo='none', hovertemplate='') - fig.add_trace(go.Scatter(x=[], y=[], marker_color=[], text=[], hoverinfo=[], + fig.add_trace(go.Scatter(x=[], y=[], marker_color=[], text=[], marker_cmin=0, marker_cmax=3, marker_size=18, mode='markers')) return fig @@ -41,7 +41,6 @@ def make_figure_image(i): server = app.server options = random.sample(KEYPOINTS, 3) -already_labeled = set() styles = { 'pre': { @@ -78,55 +77,61 @@ def make_figure_image(i): Click on the image to add a keypoint. """), html.Pre(id='click-data', style=styles['pre']), - html.Img(src='data:image/png;charset=utf-8;base64,{}'.format(encoded_image)) + # html.Img(src='data:image/png;charset=utf-8;base64,{}'.format(encoded_image)) ], - className='three columns'), + className='six columns' + ), + html.Div(id='already-labeled', style={'display': 'none'}) ] ) @app.callback( [Output('basic-interactions', 'figure'), + Output('radio', 'value'), Output('store', 'data')], - [Input('next', 'n_clicks'), + [Input('basic-interactions', 'clickData'), + Input('next', 'n_clicks'), Input('previous', 'n_clicks'), Input('clear', 'n_clicks')], - [State('store', 'data')] + [State('basic-interactions', 'figure'), + State('radio', 'value'), + State('store', 'data')] ) -def display_click_data(n_clicks_n, n_clicks_p, n_clicks_c, val): - if not any(click for click in (n_clicks_n, n_clicks_p, n_clicks_c)): - return dash.no_update, dash.no_update - if val is None: - val = 0 +def update_image(clickData, click_n, click_p, click_c, figure, option, ind_image): + if not any(event for event in (clickData, click_n, click_p, click_c)): + return dash.no_update, dash.no_update, dash.no_update + + if ind_image is None: + ind_image = 0 + ctx = dash.callback_context button_id = ctx.triggered[0]['prop_id'].split('.')[0] - global already_labeled - already_labeled = set() - app.layout['radio'].value = options[0] if button_id == 'clear': - fig = make_figure_image(val) - return fig, val - index = val + 1 if button_id == 'next' else val - 1 - fig = make_figure_image(index) - return fig, index + fig = make_figure_image(ind_image) + return fig, options[0], ind_image + elif button_id == 'next': + ind_image += 1 + fig = make_figure_image(ind_image) + return fig, options[0], ind_image + elif button_id == 'previous': + ind_image -= 1 + fig = make_figure_image(ind_image) + return fig, options[0], ind_image - -@app.callback( - [Output('basic-interactions', 'extendData'), - Output('radio', 'value')], - [Input('basic-interactions', 'clickData')], - [State('radio', 'value')] - ) -def display_click_data(clickData, option): - if clickData is None or fig is None: - return dash.no_update, dash.no_update n_bpt = options.index(option) - if n_bpt in already_labeled: - return dash.no_update, dash.no_update - already_labeled.add(n_bpt) x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] + data = figure['data'][1] + if n_bpt >= len(data['x']): + data['x'].append(x) + data['y'].append(y) + data['text'].append(option) + data['marker']['color'].append(n_bpt) + else: + data['x'][n_bpt] = x + data['y'][n_bpt] = y new_option = options[min(len(options) - 1, n_bpt + 1)] - return [{'x':[[x]], 'y':[[y]], "marker.color":[[n_bpt]], 'text':[[option]]}, [1]], new_option + return figure, new_option, ind_image if __name__ == '__main__': From fd924c41dde1d338147c2f947bd5c8c9935fe8bf Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 14:13:21 +0200 Subject: [PATCH 07/14] Add save button --- app.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 7d2934a..a3fdaf6 100644 --- a/app.py +++ b/app.py @@ -52,7 +52,7 @@ def make_figure_image(i): app.layout = html.Div([ html.Div([ dcc.Graph( - id='basic-interactions', + id='canvas', config={'editable': False}, figure=fig) ], @@ -67,6 +67,7 @@ def make_figure_image(i): html.Button('Previous', id='previous'), html.Button('Next', id='next'), html.Button('Clear', id='clear'), + html.Button('Save', id='save'), dcc.Store(id='store', data=0) ], className="six columns" @@ -81,20 +82,30 @@ def make_figure_image(i): ], className='six columns' ), - html.Div(id='already-labeled', style={'display': 'none'}) + html.Div(id='placeholder', style={'display': 'none'}) ] ) +@app.callback(Output('placeholder', 'children'), + [Input('save', 'n_clicks')], + [State('canvas', 'figure'), + State('store', 'data')]) +def save_data(click_s, figure, ind_image): + data = figure['data'][1] + xy = [(x, y) for x, y in zip(data['x'], data['y'])] + print(xy, ind_image) + + @app.callback( - [Output('basic-interactions', 'figure'), + [Output('canvas', 'figure'), Output('radio', 'value'), Output('store', 'data')], - [Input('basic-interactions', 'clickData'), + [Input('canvas', 'clickData'), Input('next', 'n_clicks'), Input('previous', 'n_clicks'), Input('clear', 'n_clicks')], - [State('basic-interactions', 'figure'), + [State('canvas', 'figure'), State('radio', 'value'), State('store', 'data')] ) From 884af9173d76819d0b9e528624118e43f58db211 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 14:28:07 +0200 Subject: [PATCH 08/14] Wrap image indices --- app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index a3fdaf6..a82606a 100644 --- a/app.py +++ b/app.py @@ -122,11 +122,11 @@ def update_image(clickData, click_n, click_p, click_c, figure, option, ind_image fig = make_figure_image(ind_image) return fig, options[0], ind_image elif button_id == 'next': - ind_image += 1 + ind_image = (ind_image + 1) % len(images) fig = make_figure_image(ind_image) return fig, options[0], ind_image elif button_id == 'previous': - ind_image -= 1 + ind_image = (ind_image - 1) % len(images) fig = make_figure_image(ind_image) return fig, options[0], ind_image From 50d73a6e856cf1db32f30bef74f3495655717fc4 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 16:59:16 +0200 Subject: [PATCH 09/14] Draggable keypoints --- app.py | 109 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 23 deletions(-) diff --git a/app.py b/app.py index a82606a..bc03695 100644 --- a/app.py +++ b/app.py @@ -3,24 +3,31 @@ import dash_html_components as html from dash.dependencies import Input, Output, State import base64 +import json import os +import matplotlib.cm +import matplotlib.colors as mcolors +import numpy as np import random import plotly.graph_objects as go import plotly.express as px from skimage import data, transform +COLORMAP = 'plasma' KEYPOINTS = ['Nose', 'L_Eye', 'R_Eye', 'L_Ear', 'R_Ear', 'Throat', 'Withers', 'TailSet', 'L_F_Paw', 'R_F_Paw', 'L_F_Wrist', 'R_F_Wrist', 'L_F_Elbow', 'R_F_Elbow', 'L_B_Paw', 'R_B_Paw', 'L_B_Hock', 'R_B_Hock', 'L_B_Stiffle', 'R_B_Stiffle'] +N_SUBSET = 3 IMAGE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'full_dog.png') +encoded_image = base64.b64encode(open(IMAGE_PATH, 'rb').read()) img = data.chelsea() img = img[::2, ::2] images = [img, img[::-1], transform.rotate(img, 30)] -encoded_image = base64.b64encode(open(IMAGE_PATH, 'rb').read()) +cmap = matplotlib.cm.get_cmap(COLORMAP, N_SUBSET) def make_figure_image(i): @@ -33,6 +40,51 @@ def make_figure_image(i): return fig +def draw_circle(center, radius, n_points=50): + pts = np.linspace(0, 2 * np.pi, n_points) + x = center[0] + radius * np.cos(pts) + y = center[1] + radius * np.sin(pts) + path = 'M ' + str(x[0]) + ',' + str(y[1]) + for k in range(1, x.shape[0]): + path += ' L ' + str(x[k]) + ',' + str(y[k]) + path += ' Z' + return path + + +def compute_circle_center(path): + """ + See Eqn 1 & 2 pp.12-13 in REGRESSIONS CONIQUES, QUADRIQUES + Régressions linéaires et apparentées, circulaire, sphérique + Jacquelin J., 2009. + """ + coords = [list(map(float, coords.split(','))) for coords in path.split(' ')[1::2]] + x, y = np.array(coords).T + n = len(x) + sum_x = np.sum(x) + sum_y = np.sum(y) + sum_x2 = np.sum(x * x) + sum_y2 = np.sum(y * y) + delta11 = n * np.dot(x, y) - sum_x * sum_y + delta20 = n * sum_x2 - sum_x ** 2 + delta02 = n * sum_y2 - sum_y ** 2 + delta30 = n * np.sum(x ** 3) - sum_x2 * sum_x + delta03 = n * np.sum(y ** 3) - sum_y * sum_y2 + delta21 = n * np.sum(x * x * y) - sum_x2 * sum_y + delta12 = n * np.sum(x * y * y) - sum_x * sum_y2 + + # Eqn 2, p.13 + num_a = (delta30 + delta12) * delta02 - (delta03 + delta21) * delta11 + num_b = (delta03 + delta21) * delta20 - (delta30 + delta12) * delta11 + den = 2 * (delta20 * delta02 - delta11 * delta11) + a = num_a / den + b = num_b / den + return int(a), int(b) + + +def get_plotly_color(n): + return mcolors.to_hex(cmap(n)) + + fig = make_figure_image(0) external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] @@ -40,7 +92,7 @@ def make_figure_image(i): app = dash.Dash(__name__, external_stylesheets=external_stylesheets) server = app.server -options = random.sample(KEYPOINTS, 3) +options = random.sample(KEYPOINTS, N_SUBSET) styles = { 'pre': { @@ -53,7 +105,7 @@ def make_figure_image(i): html.Div([ dcc.Graph( id='canvas', - config={'editable': False}, + config={'editable': True}, figure=fig) ], className="six columns" @@ -82,7 +134,8 @@ def make_figure_image(i): ], className='six columns' ), - html.Div(id='placeholder', style={'display': 'none'}) + html.Div(id='placeholder', style={'display': 'none'}), + html.Div(id='shapes', style={'display': 'none'}) ] ) @@ -100,49 +153,59 @@ def save_data(click_s, figure, ind_image): @app.callback( [Output('canvas', 'figure'), Output('radio', 'value'), - Output('store', 'data')], + Output('store', 'data'), + Output('shapes', 'children')], [Input('canvas', 'clickData'), Input('next', 'n_clicks'), Input('previous', 'n_clicks'), Input('clear', 'n_clicks')], [State('canvas', 'figure'), State('radio', 'value'), - State('store', 'data')] + State('store', 'data'), + State('shapes', 'children')] ) -def update_image(clickData, click_n, click_p, click_c, figure, option, ind_image): +def update_image(clickData, click_n, click_p, click_c, figure, option, ind_image, shapes): if not any(event for event in (clickData, click_n, click_p, click_c)): - return dash.no_update, dash.no_update, dash.no_update + return dash.no_update, dash.no_update, dash.no_update, dash.no_update if ind_image is None: ind_image = 0 + if shapes is None: + shapes = [] + else: + shapes = json.loads(shapes) + ctx = dash.callback_context button_id = ctx.triggered[0]['prop_id'].split('.')[0] if button_id == 'clear': - fig = make_figure_image(ind_image) - return fig, options[0], ind_image + return make_figure_image(ind_image), options[0], ind_image, '[]' elif button_id == 'next': ind_image = (ind_image + 1) % len(images) - fig = make_figure_image(ind_image) - return fig, options[0], ind_image + return make_figure_image(ind_image), options[0], ind_image, '[]' elif button_id == 'previous': ind_image = (ind_image - 1) % len(images) - fig = make_figure_image(ind_image) - return fig, options[0], ind_image + return make_figure_image(ind_image), options[0], ind_image, '[]' n_bpt = options.index(option) x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] - data = figure['data'][1] - if n_bpt >= len(data['x']): - data['x'].append(x) - data['y'].append(y) - data['text'].append(option) - data['marker']['color'].append(n_bpt) + circle = draw_circle((x, y), 10) + color = get_plotly_color(n_bpt) + shape = dict(type='path', + path=circle, + line_color=color, + fillcolor=color, + layer='above') + if n_bpt >= len(shapes): + shapes.append(shape) else: - data['x'][n_bpt] = x - data['y'][n_bpt] = y + shapes[n_bpt] = shape + fig.update_layout(shapes=shapes) new_option = options[min(len(options) - 1, n_bpt + 1)] - return figure, new_option, ind_image + return ({'data': figure['data'], 'layout': fig['layout']}, + new_option, + ind_image, + json.dumps(shapes)) if __name__ == '__main__': From c18e03354ab2ac602256d6788f78e035c42d9a38 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 18:46:40 +0200 Subject: [PATCH 10/14] Update keypoint centers after dragging --- app.py | 55 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/app.py b/app.py index bc03695..c95c6ca 100644 --- a/app.py +++ b/app.py @@ -78,7 +78,7 @@ def compute_circle_center(path): den = 2 * (delta20 * delta02 - delta11 * delta11) a = num_a / den b = num_b / den - return int(a), int(b) + return a, b def get_plotly_color(n): @@ -142,12 +142,11 @@ def get_plotly_color(n): @app.callback(Output('placeholder', 'children'), [Input('save', 'n_clicks')], - [State('canvas', 'figure'), - State('store', 'data')]) -def save_data(click_s, figure, ind_image): - data = figure['data'][1] - xy = [(x, y) for x, y in zip(data['x'], data['y'])] - print(xy, ind_image) + [State('store', 'data')]) +def save_data(click_s, ind_image): + if click_s: + xy = {shape.name: compute_circle_center(shape.path) for shape in fig.layout.shapes} + print(xy, ind_image) @app.callback( @@ -156,6 +155,7 @@ def save_data(click_s, figure, ind_image): Output('store', 'data'), Output('shapes', 'children')], [Input('canvas', 'clickData'), + Input('canvas', 'relayoutData'), Input('next', 'n_clicks'), Input('previous', 'n_clicks'), Input('clear', 'n_clicks')], @@ -164,18 +164,13 @@ def save_data(click_s, figure, ind_image): State('store', 'data'), State('shapes', 'children')] ) -def update_image(clickData, click_n, click_p, click_c, figure, option, ind_image, shapes): +def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, option, ind_image, shapes): if not any(event for event in (clickData, click_n, click_p, click_c)): return dash.no_update, dash.no_update, dash.no_update, dash.no_update if ind_image is None: ind_image = 0 - if shapes is None: - shapes = [] - else: - shapes = json.loads(shapes) - ctx = dash.callback_context button_id = ctx.triggered[0]['prop_id'].split('.')[0] if button_id == 'clear': @@ -187,19 +182,31 @@ def update_image(clickData, click_n, click_p, click_c, figure, option, ind_image ind_image = (ind_image - 1) % len(images) return make_figure_image(ind_image), options[0], ind_image, '[]' + if shapes is None: + shapes = [] + else: + shapes = json.loads(shapes) + n_bpt = options.index(option) - x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] - circle = draw_circle((x, y), 10) - color = get_plotly_color(n_bpt) - shape = dict(type='path', - path=circle, - line_color=color, - fillcolor=color, - layer='above') - if n_bpt >= len(shapes): - shapes.append(shape) + already_labeled = [shape['name'] for shape in shapes] + if option not in already_labeled: + if clickData: + x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] + circle = draw_circle((x, y), 10) + color = get_plotly_color(n_bpt) + shape = dict(type='path', + path=circle, + line_color=color, + fillcolor=color, + layer='above', + name=option) + shapes.append(shape) else: - shapes[n_bpt] = shape + key = list(relayoutData)[0] + if 'path' in key: + ind_moving = int(key.split('[')[1].split(']')[0]) + path = relayoutData.pop(key) + shapes[ind_moving]['path'] = path fig.update_layout(shapes=shapes) new_option = options[min(len(options) - 1, n_bpt + 1)] return ({'data': figure['data'], 'layout': fig['layout']}, From 1f5262c052d48d874c9ab9e3a28eb7717bb1f252 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 21:37:58 +0200 Subject: [PATCH 11/14] Minor fixes --- app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index c95c6ca..66d2ee2 100644 --- a/app.py +++ b/app.py @@ -35,8 +35,6 @@ def make_figure_image(i): fig.layout.xaxis.showticklabels = False fig.layout.yaxis.showticklabels = False fig.update_traces(hoverinfo='none', hovertemplate='') - fig.add_trace(go.Scatter(x=[], y=[], marker_color=[], text=[], - marker_cmin=0, marker_cmax=3, marker_size=18, mode='markers')) return fig @@ -113,7 +111,7 @@ def get_plotly_color(n): html.Div([ html.H2("Controls"), dcc.RadioItems(id='radio', - options=[{'label':opt, 'value':opt} for opt in options], + options=[{'label': opt, 'value': opt} for opt in options], value=options[0] ), html.Button('Previous', id='previous'), @@ -174,6 +172,7 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, opt ctx = dash.callback_context button_id = ctx.triggered[0]['prop_id'].split('.')[0] if button_id == 'clear': + fig.layout.shapes = [] return make_figure_image(ind_image), options[0], ind_image, '[]' elif button_id == 'next': ind_image = (ind_image + 1) % len(images) @@ -189,6 +188,7 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, opt n_bpt = options.index(option) already_labeled = [shape['name'] for shape in shapes] + key = list(relayoutData)[0] if option not in already_labeled: if clickData: x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] @@ -199,10 +199,10 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, opt line_color=color, fillcolor=color, layer='above', + opacity=0.8, name=option) shapes.append(shape) else: - key = list(relayoutData)[0] if 'path' in key: ind_moving = int(key.split('[')[1].split(']')[0]) path = relayoutData.pop(key) From 36558950dd6d29e727ba94ce0a3729a199a1ebf2 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 21:38:30 +0200 Subject: [PATCH 12/14] Fix zoom functionalities --- app.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app.py b/app.py index 66d2ee2..9eaab58 100644 --- a/app.py +++ b/app.py @@ -208,6 +208,14 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, opt path = relayoutData.pop(key) shapes[ind_moving]['path'] = path fig.update_layout(shapes=shapes) + if 'range[' in key: + xrange = relayoutData['xaxis.range[0]'], relayoutData['xaxis.range[1]'] + yrange = relayoutData['yaxis.range[0]'], relayoutData['yaxis.range[1]'] + fig.update_xaxes(range=xrange, autorange=False) + fig.update_yaxes(range=yrange, autorange=False) + elif 'autorange' in key: + fig.update_xaxes(autorange=True) + fig.update_yaxes(autorange=True) new_option = options[min(len(options) - 1, n_bpt + 1)] return ({'data': figure['data'], 'layout': fig['layout']}, new_option, From 023e7b048ee1e7e93f07dd5ea009689d8767594d Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 22:56:34 +0200 Subject: [PATCH 13/14] Add slider to adjust keypoint size --- app.py | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/app.py b/app.py index 9eaab58..93e400b 100644 --- a/app.py +++ b/app.py @@ -118,7 +118,16 @@ def get_plotly_color(n): html.Button('Next', id='next'), html.Button('Clear', id='clear'), html.Button('Save', id='save'), - dcc.Store(id='store', data=0) + dcc.Store(id='store', data=0), + html.P([ + html.Label('Keypoint size'), + dcc.Slider(id='slider', + min=3, + max=36, + step=1, + value=12) + ], style={'width': '80%', + 'display': 'inline-block'}) ], className="six columns" ), @@ -128,7 +137,7 @@ def get_plotly_color(n): Click on the image to add a keypoint. """), html.Pre(id='click-data', style=styles['pre']), - # html.Img(src='data:image/png;charset=utf-8;base64,{}'.format(encoded_image)) + html.Img(src='data:image/png;charset=utf-8;base64,{}'.format(encoded_image)) ], className='six columns' ), @@ -156,19 +165,27 @@ def save_data(click_s, ind_image): Input('canvas', 'relayoutData'), Input('next', 'n_clicks'), Input('previous', 'n_clicks'), - Input('clear', 'n_clicks')], + Input('clear', 'n_clicks'), + Input('slider', 'value')], [State('canvas', 'figure'), State('radio', 'value'), State('store', 'data'), State('shapes', 'children')] ) -def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, option, ind_image, shapes): +def update_image(clickData, relayoutData, click_n, click_p, click_c, slider_val, + figure, option, ind_image, shapes): if not any(event for event in (clickData, click_n, click_p, click_c)): return dash.no_update, dash.no_update, dash.no_update, dash.no_update if ind_image is None: ind_image = 0 + if shapes is None: + shapes = [] + else: + shapes = json.loads(shapes) + n_bpt = options.index(option) + ctx = dash.callback_context button_id = ctx.triggered[0]['prop_id'].split('.')[0] if button_id == 'clear': @@ -180,19 +197,18 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, opt elif button_id == 'previous': ind_image = (ind_image - 1) % len(images) return make_figure_image(ind_image), options[0], ind_image, '[]' + elif button_id == 'slider': + for i in range(len(shapes)): + center = compute_circle_center(shapes[i]['path']) + new_path = draw_circle(center, slider_val) + shapes[i]['path'] = new_path - if shapes is None: - shapes = [] - else: - shapes = json.loads(shapes) - - n_bpt = options.index(option) already_labeled = [shape['name'] for shape in shapes] key = list(relayoutData)[0] - if option not in already_labeled: + if option not in already_labeled and button_id != 'slider': if clickData: x, y = clickData['points'][0]['x'], clickData['points'][0]['y'] - circle = draw_circle((x, y), 10) + circle = draw_circle((x, y), slider_val) color = get_plotly_color(n_bpt) shape = dict(type='path', path=circle, @@ -203,7 +219,7 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, opt name=option) shapes.append(shape) else: - if 'path' in key: + if 'path' in key and button_id != 'slider': ind_moving = int(key.split('[')[1].split(']')[0]) path = relayoutData.pop(key) shapes[ind_moving]['path'] = path @@ -216,6 +232,8 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, figure, opt elif 'autorange' in key: fig.update_xaxes(autorange=True) fig.update_yaxes(autorange=True) + if button_id != 'slider': + n_bpt += 1 new_option = options[min(len(options) - 1, n_bpt + 1)] return ({'data': figure['data'], 'layout': fig['layout']}, new_option, From fb7b4f76134c4942eaec84d25988046ad6e20630 Mon Sep 17 00:00:00 2001 From: jeylau <30733203+jeylau@users.noreply.github.com> Date: Thu, 4 Jun 2020 23:00:51 +0200 Subject: [PATCH 14/14] Minor fix --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 93e400b..0c89058 100644 --- a/app.py +++ b/app.py @@ -234,7 +234,7 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, slider_val, fig.update_yaxes(autorange=True) if button_id != 'slider': n_bpt += 1 - new_option = options[min(len(options) - 1, n_bpt + 1)] + new_option = options[min(len(options) - 1, n_bpt)] return ({'data': figure['data'], 'layout': fig['layout']}, new_option, ind_image,