Source code for neuror.cut_plane.viewer

'''App to find cut planes with arbitrary cut planes orientations
with the help of a manual hint.

Related to https://bbpteam.epfl.ch/project/issues/browse/NGV-85
'''
import json

import neurom as nm
import numpy as np
from neurom import load_morphology
from neurom.geom import bounding_box
from pyquaternion import Quaternion

from neuror.cut_plane.detection import CutPlane, _minimize
from neuror.cut_plane.planes import _get_displaced_pos

try:
    import dash
    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output, State
    from plotly_helper.neuron_viewer import NeuronBuilder
except ImportError as e:
    raise ImportError(
        'neuror[plotly] is not installed.'
        ' Please install it by doing: pip install neuror[plotly]') from e


# Copy of: https://codepen.io/chriddyp/pen/bWLwgP.css
external_stylesheets = ['dash.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

BIN_WIDTH = 10


[docs]class NumpyEncoder(json.JSONEncoder): '''JSON encoder that handles numpy types. In python3, `numpy types <https://numpy.org/doc/stable/user/basics.types.html>`_ don't serialize to correctly, so a custom converter is needed. '''
[docs] def default(self, o): # pylint: disable=method-hidden if isinstance(o, np.floating): return float(o) if isinstance(o, np.integer): return int(o) if isinstance(o, np.ndarray): return o.tolist() return json.JSONEncoder.default(self, o)
[docs]def create_plane(pos, quat): """Create a 3d plane using a center position and a quaternion for orientation. Args : pos: x,y,z position of the plane's center (array([x,y,z])) quat: quaternion representing the orientations (Quaternion) Returns : dict: A square surface to the plotly format """ length = np.linalg.norm(BBOX[1] - BBOX[0]) / 2. positif_x = _get_displaced_pos(pos, quat, length, (1, 0, 0)) positif_y = _get_displaced_pos(pos, quat, length, (0, 1, 0)) negatif_x = _get_displaced_pos(pos, quat, length, (-1, 0, 0)) negatif_y = _get_displaced_pos(pos, quat, length, (0, -1, 0)) x = [[positif_x[0], positif_y[0]], [negatif_y[0], negatif_x[0]]] y = [[positif_x[1], positif_y[1]], [negatif_y[1], negatif_x[1]]] z = [[positif_x[2], positif_y[2]], [negatif_y[2], negatif_x[2]]] return { "z": z, "x": x, "y": y, "showscale": False, "type": "surface", "surfacecolor": ["green", "green"], "opacity": 1, }
ROT_X, ROT_Y, ROT_Z = 4, 45, -21 TRANSL_X, TRANSL_Y, TRANSL_Z = 0, 0, 61 app.layout = html.Div(children=[ html.Pre(id='click-data'), html.Pre(id='neuron'), html.Pre(id='optimized'), html.Div( [ "Rotate the plane until it is aligned with the cut plane, then click optimize, " "then click export. " 'On the histogram, all points should fall on only one side of the red line', html.Div( [ dcc.Graph(id='graph'), ], style={'width': '70%', 'display': 'inline-block'}, ), html.Div( [ dcc.Graph(id='bar'), html.Div([ html.Button('Optimize', id='button'), dcc.Checklist(id='hide-plane', options=[ {'label': 'Hide plane', 'value': 'hidden'} ], value=[]), "Rotations are with respect to the frame attached to the plane.", html.Div( [html.Div(['Rotate X:'], id='output-x-rotate'), dcc.Input(id='rotate-x-slider', type='number', value=ROT_X), ] ), html.Div( [html.Div(['Rotate Y:'], id='output-y-rotate'), dcc.Input(id='rotate-y-slider', type='number', value=ROT_Y), ], ), html.Div( [html.Div(['Rotate Z:'], id='output-z-rotate'), dcc.Input(id='rotate-z-slider', type='number', value=ROT_Z), ] ), html.Div( [html.Div(['Translate X:'], id='output-x-translate'), dcc.Input(id='translate-x-slider', type='number', value=TRANSL_X)] ), html.Div( [html.Div(['Translate Y:'], id='output-y-translate'), dcc.Input(id='translate-y-slider', type='number', value=TRANSL_Y), ], ), html.Div( [html.Div(['Translate Z:'], id='output-z-translate'), dcc.Input(id='translate-z-slider', type='number', value=TRANSL_Z, min=-10000), ] ), dcc.Input(id='export-path-input', type='text', value='/tmp/cut-plane.json'), html.Button('export', id='export')], style={'margin-left': '100px'}) ], style={'width': '30%', 'display': 'inline-block', 'vertical-align': 'top'} ), ], style={'width': '100%', 'display': 'inline'}) ] ) NEURON = FIGURE = BBOX = None
[docs]def set_neuron(filename): '''Globally loads the neuron.''' global NEURON, FIGURE, BBOX # pylint: disable=global-statement NEURON = load_morphology(filename) FIGURE = NeuronBuilder(NEURON, '3d').get_figure() BBOX = bounding_box(NEURON)
[docs]@app.callback( Output('graph', 'figure'), [ Input('rotate-x-slider', 'value'), Input('rotate-y-slider', 'value'), Input('rotate-z-slider', 'value'), Input('translate-x-slider', 'value'), Input('translate-y-slider', 'value'), Input('translate-z-slider', 'value'), Input('hide-plane', 'value'), ], [State('graph', 'relayoutData')] ) def display_click_data(rot_x, rot_y, rot_z, transl_x, transl_y, transl_z, hide, layout): '''Callback that redraw everything when sliders are changed.''' qx = Quaternion(axis=[1, 0, 0], angle=rot_x / 180. * np.pi) qy = Quaternion(axis=[0, 1, 0], angle=rot_y / 180. * np.pi) qz = Quaternion(axis=[0, 0, 1], angle=rot_z / 180. * np.pi) plane = create_plane([transl_x, transl_y, transl_z], qx * qy * qz) FIGURE['data'] = [x for x in FIGURE['data'] if not isinstance(x, dict)] if not hide: FIGURE['data'].append(plane) FIGURE['layout']['autosize'] = False FIGURE['layout']['height'] = 1500 if layout and layout.get('scene.camera'): FIGURE['layout']['scene']['camera'] = layout['scene.camera'] return FIGURE
[docs]@app.callback( dash.dependencies.Output('bar', 'figure'), [ Input('rotate-x-slider', 'value'), Input('rotate-y-slider', 'value'), Input('rotate-z-slider', 'value'), Input('translate-x-slider', 'value'), Input('translate-y-slider', 'value'), Input('translate-z-slider', 'value'), ]) def update_output(rot_x, rot_y, rot_z, transl_x, transl_y, transl_z): '''Update histo when sliders are changed.''' transformations = [rot_x, rot_y, rot_z, transl_x, transl_y, transl_z] bin_width = 10 cut_plane = CutPlane.from_rotations_translations(transformations, NEURON, bin_width) hist, binning = cut_plane.histogram() binning += bin_width / 2. return { 'data': [ {'x': binning, 'y': hist, 'type': 'bar', 'name': 'SF'}, ], 'layout': { 'title': 'Distance to plane distribution', 'xaxis': { 'title': 'Distance to plane (um)' }, 'yaxis': { 'title': 'Counts' }, 'shapes': [ # Line Vertical { 'type': 'line', 'x0': 0, 'y0': 0, 'x1': 0, 'y1': np.max(hist), 'line': { 'color': 'red', 'width': 3, }, } ] } }
[docs]@app.callback( dash.dependencies.Output('optimized', 'data-*'), [Input('button', 'n_clicks')], [ State('rotate-x-slider', 'value'), State('rotate-y-slider', 'value'), State('rotate-z-slider', 'value'), State('translate-x-slider', 'value'), State('translate-y-slider', 'value'), State('translate-z-slider', 'value'), ] ) def optimize(n_clicks, rot_x, rot_y, rot_z, transl_x, transl_y, transl_z): '''Optimize cut plane parameters.''' if not n_clicks: return rot_x, rot_y, rot_z, transl_x, transl_y, transl_z points = np.array([point for neurite in (NEURON.neurites or []) for section in nm.iter_sections(neurite) for point in section.points]) params = rot_x, rot_y, rot_z, transl_x, transl_y, transl_z result = _minimize(params, points, bin_width=BIN_WIDTH) return result
[docs]@app.callback( Output('rotate-x-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_x_rotate(params): '''Dash callback.''' return params[0]
[docs]@app.callback( Output('rotate-y-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_y_rotate(params): '''Dash callback.''' return params[1]
[docs]@app.callback( Output('rotate-z-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_z_rotate(params): '''Dash callback.''' return params[2]
[docs]@app.callback( Output('translate-x-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_x_translate(params): '''Dash callback.''' return params[3]
[docs]@app.callback( Output('translate-y-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_y_translate(params): '''Dash callback.''' return params[4]
[docs]@app.callback( Output('translate-z-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_z_translate(params): '''Dash callback.''' return params[5]
[docs]@app.callback( dash.dependencies.Output('click-data', 'children'), [Input('export', 'n_clicks')], [State('rotate-x-slider', 'value'), State('rotate-y-slider', 'value'), State('rotate-z-slider', 'value'), State('translate-x-slider', 'value'), State('translate-y-slider', 'value'), State('translate-z-slider', 'value'), State('export-path-input', 'value')]) def export(n_clicks, rot_x, rot_y, rot_z, transl_x, transl_y, transl_z, output_path): '''Write the final file cut-plane.json to disk.''' if not n_clicks: return plane = CutPlane.from_rotations_translations( [rot_x, rot_y, rot_z, transl_x, transl_y, transl_z], NEURON, BIN_WIDTH) payload = [plane.to_json()] with open(output_path, 'w') as f: json.dump(payload, f, cls=NumpyEncoder)