Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MVP for the DLC webinar #5

Open
wants to merge 30 commits into
base: webinar
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2bb91c2
add file path
stes Jun 4, 2020
de8dd94
stes/webinar
stes Jun 5, 2020
c11e5a0
refactor
stes Jun 5, 2020
7510314
Add MVC layout; disable dragging
stes Jun 5, 2020
c615677
fix layout
stes Jun 5, 2020
e440231
Fix dockerfile
stes Jun 5, 2020
34e01c9
Update image; add data fetching
stes Jun 5, 2020
ad21ce6
Data preparation script
stes Jun 5, 2020
723d038
automatic app update on server
stes Jun 5, 2020
f1ced91
Fix missing csv column
stes Jun 5, 2020
7930b8f
Update view.py
MMathisLab Jun 5, 2020
744fc72
Merge pull request #6 from DeepLabCut/MMathisLab-patch-1
stes Jun 5, 2020
6c03c4f
Merge branch 'webinar' into stes/webinar
jeylau Jun 5, 2020
5ebf034
Re-enable draggable mode and clean imports
jeylau Jun 5, 2020
6723d02
And this one...
jeylau Jun 5, 2020
ae4856a
Update slider range
jeylau Jun 5, 2020
40c29b5
Retain only a subset of keypoints
jeylau Jun 5, 2020
72da802
Auto-formatting
jeylau Jun 5, 2020
c8fcb76
Minor fix
jeylau Jun 5, 2020
d42402c
Add default random ID
jeylau Jun 5, 2020
95730fc
Shuffle keypoints when changing image
jeylau Jun 5, 2020
528f78e
Putative gain in speed upon fetching images
jeylau Jun 5, 2020
899f650
Store data when changing image
jeylau Jun 5, 2020
d915c2c
Shuffle fetched images
jeylau Jun 5, 2020
4fd4ec3
Quick patch missing bodypart
jeylau Jun 5, 2020
a2b122e
Fix empty ID
jeylau Jun 5, 2020
488d486
rm options
stes Jun 5, 2020
8c63c9f
final fix for webinar
stes Jun 5, 2020
a36bd3e
fix annottion adding
stes Jun 5, 2020
e7f92e0
creating filelist
AlexEMG Jun 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**
!*.py
!static/
!config/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
data/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8

RUN /usr/local/bin/python -m pip install --upgrade pip
RUN pip install --no-cache-dir \
dash \
plotly \
scikit-image \
pandas \
gunicorn
RUN pip install --no-cache-dir requests

RUN mkdir -p /app
RUN mkdir -p /app/static
WORKDIR /app

ADD config/ /app/config
ADD *.py /app/
ADD static/ /app/static/

ENTRYPOINT ["gunicorn", "-w", "1", "-b", "0.0.0.0:8050", "app:server"]
Empty file added __init__.py
Empty file.
261 changes: 80 additions & 181 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import flask
import dash
import dash_core_components as dcc
import dash_html_components as html
Expand All @@ -13,203 +14,91 @@
import plotly.express as px
from skimage import data, transform

import utils, config, model, view

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)]
cmap = matplotlib.cm.get_cmap(COLORMAP, N_SUBSET)


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='')
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 a, 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']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
server = app.server

options = random.sample(KEYPOINTS, N_SUBSET)

styles = {
'pre': {
'border': 'thin lightgrey solid',
'overflowX': 'scroll'
}
}

app.layout = html.Div([
html.Div([
dcc.Graph(
id='canvas',
config={'editable': True},
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]
),
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),
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"
),
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='six columns'
),
html.Div(id='placeholder', style={'display': 'none'}),
html.Div(id='shapes', style={'display': 'none'})
]
)


@app.callback(Output('placeholder', 'children'),
__version__ = "0.1"

print(f"| Starting version {__version__}")

config = config.Config('config/config.json')
db = model.AppModel(config = config)
cmap = matplotlib.cm.get_cmap('plasma', len(config.options))
server = flask.Flask(__name__)
view = view.AppView(__name__, db = db, config = config, server = server)


@server.route('/csv/')
def fetch_csv():
return db.to_csv()

@server.route('/overview/')
def fetch_html():
return db.to_html()

@view.app.callback(Output('placeholder', 'children'),
[Input('save', 'n_clicks')],
[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}
xy = {shape.name: utils.compute_circle_center(shape.path) for shape in view.fig.layout.shapes}
print(xy, ind_image)


@app.callback(
@view.app.callback(
[Output('canvas', 'figure'),
Output('radio', 'value'),
Output('store', 'data'),
Output('shapes', 'children')],
Output('shapes', 'children'),
],
[Input('canvas', 'clickData'),
Input('canvas', 'relayoutData'),
Input('next', 'n_clicks'),
Input('previous', 'n_clicks'),
Input('clear', 'n_clicks'),
Input('slider', 'value')],
Input('slider', 'value'),
Input('input_name', 'value')
],
[State('canvas', 'figure'),
State('radio', 'value'),
State('store', 'data'),
State('shapes', 'children')]
)
def update_image(clickData, relayoutData, click_n, click_p, click_c, slider_val,
def update_image(clickData, relayoutData, click_n, click_p, click_c, slider_val, username,
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
# TODO Refactor: Remove if/else statements and instead write multiple
# callbacks.

if shapes is None:
shapes = []
else:
shapes = json.loads(shapes)
n_bpt = options.index(option)
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
shapes = [] if shapes is None else json.loads(shapes)
n_bpt = view.options.index(option)

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)
return make_figure_image(ind_image), options[0], ind_image, '[]'
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':
if button_id == 'slider':
for i in range(len(shapes)):
center = compute_circle_center(shapes[i]['path'])
new_path = draw_circle(center, slider_val)
center = utils.compute_circle_center(shapes[i]['path'])
new_path = utils.draw_circle(center, slider_val)
shapes[i]['path'] = new_path
elif button_id in ['clear', 'next', 'previous']:
if button_id == 'clear':
view.fig.layout.shapes = []
elif button_id == 'next':
ind_image = (ind_image + 1) % len(db.dataset)
elif button_id == 'previous':
ind_image = (ind_image - 1) % len(db.dataset)
return view.make_figure_image(ind_image), view.options[0], ind_image, '[]'

already_labeled = [shape['name'] for shape in shapes]
key = list(relayoutData)[0]

keys = list(relayoutData)
key = keys[0] if len(keys) > 0 else ""
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), slider_val)
color = get_plotly_color(n_bpt)
circle = utils.draw_circle((x, y), slider_val)
color = utils.get_plotly_color(cmap, n_bpt)
shape = dict(type='path',
path=circle,
line_color=color,
Expand All @@ -218,28 +107,38 @@ def update_image(clickData, relayoutData, click_n, click_p, click_c, slider_val,
opacity=0.8,
name=option)
shapes.append(shape)
else:
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
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)
db.add_annotation(
name = option, username = username,
xy = utils.compute_circle_center(circle)
)

# TODO this produces an error (see below)
#else:
# 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

view.fig.update_layout(shapes=shapes)
print(f"| processing key: {key}")
if False:
if 'range[' in key:
xrange = relayoutData['xaxis.range[0]'], relayoutData['xaxis.range[1]']
yrange = relayoutData['yaxis.range[0]'], relayoutData['yaxis.range[1]']
view.fig.update_xaxes(range=xrange, autorange=False)
view.fig.update_yaxes(range=yrange, autorange=False)
elif 'autorange' in key:
view.fig.update_xaxes(autorange=True)
view.fig.update_yaxes(autorange=True)
if button_id != 'slider':
n_bpt += 1
new_option = options[min(len(options) - 1, n_bpt)]
return ({'data': figure['data'], 'layout': fig['layout']},
# TODO only advance the options if we placed a new point.
new_option = view.options[min(len(view.options) - 1, n_bpt)]
return ({'data': figure['data'], 'layout': view.fig['layout']},
new_option,
ind_image,
json.dumps(shapes))


if __name__ == '__main__':
app.run_server(debug=True, port=8051)
view.app.run_server(debug=True, port=8051)
13 changes: 13 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import json
import os.path as osp
import glob

class Config():

def __init__(self, config):
assert osp.exists(config)

with open(config, "r") as fp:
for key, val in json.load(fp).items():
setattr(self, key, val)
self.fnames = glob.glob('data/*.png')
31 changes: 31 additions & 0 deletions config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"abbrv" : {
"L" : "Left",
"R" : "Right",
"F" : "Front",
"B" : "Back"
},
"options" : [
"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"
],
"urls" : "config/filelist.lst"
}
Loading