-
Notifications
You must be signed in to change notification settings - Fork 6
/
cli.py
402 lines (323 loc) · 15.8 KB
/
cli.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
'''The morph-tool command line launcher'''
import json
import shutil
import logging
import os
from pathlib import Path
from pprint import pprint
import click
from morph_tool.utils import iter_morphology_files
from morphio.mut import Morphology # pylint: disable=import-error
from neurom import load_morphology
from neurom.utils import NeuromJSON
from neuror.cut_plane.detection import CutPlane
from neuror.exceptions import NeuroRError
from neuror.unravel import DEFAULT_WINDOW_HALF_LENGTH
logging.basicConfig()
L = logging.getLogger('neuror')
@click.group()
@click.option('-v', '--verbose', count=True, default=0,
help='-v for INFO, -vv for DEBUG')
def cli(verbose):
'''The CLI entry point.'''
level = (logging.WARNING, logging.INFO, logging.DEBUG)[min(verbose, 2)]
L.setLevel(level)
@cli.group()
def unravel():
'''CLI utilities related to unravelling.'''
@cli.group()
def cut_plane():
'''CLI utilities related to cut-plane repair.'''
@cli.group()
def sanitize():
'''CLI utilities related to sanitizing raw morphologies.
It currently only deals with removing duplicate points but it may do
more in the future.
'''
@cut_plane.group()
def compute():
'''CLI utilities to detect cut planes.'''
@cut_plane.group()
def repair():
'''CLI utilities to repair cut planes.'''
@cli.group()
def error_annotation():
'''CLI utilities related to error annotations.'''
@error_annotation.command(short_help='Annotate errors on a morphology')
@click.argument('input_file', type=click.Path(exists=True, file_okay=True))
@click.argument('output_file')
@click.option('--error_summary_file', type=click.Path(file_okay=True), default='error_summary.json',
help='Path to json file to save error summary')
@click.option('--marker_file', type=click.Path(file_okay=True), default='markers.json',
help='Path to json file to save markers')
def file(input_file, output_file, error_summary_file, marker_file):
'''Annotate errors on a morphology.'''
from neuror.sanitize import annotate_neurolucida
if Path(input_file).suffix not in ['.asc', '.ASC']:
raise NeuroRError('Only .asc/.ASC files are allowed, please convert with morph-tool.')
annotations, summary, markers = annotate_neurolucida(input_file)
shutil.copy(input_file, output_file)
with open(output_file, 'a') as morph_file:
morph_file.write(annotations)
with open(error_summary_file, 'w') as summary_file:
json.dump(summary, summary_file, cls=NeuromJSON)
with open(marker_file, 'w') as m_file:
json.dump(markers, m_file, cls=NeuromJSON)
@error_annotation.command(short_help='Annotate errors on morphologies')
@click.argument('input_dir')
@click.argument('output_dir', type=click.Path(exists=True, file_okay=False, writable=True))
@click.option('--error_summary_file', type=click.Path(file_okay=True), default='error_summary.json',
help='Path to json file to save error summary')
@click.option('--marker_file', type=click.Path(file_okay=True), default='markers.json',
help='Path to json file to save markers')
def folder(input_dir, output_dir, error_summary_file, marker_file):
'''Annotate errors on a morphologies in a folder.'''
from neuror.sanitize import annotate_neurolucida_all
output_dir = Path(output_dir)
morph_paths = list(iter_morphology_files(input_dir))
annotations, summaries, markers = annotate_neurolucida_all(morph_paths)
for morph_path, annotation in annotations.items():
output_file = output_dir / Path(morph_path).name
shutil.copy(morph_path, output_file)
with open(output_file, 'a') as morph_file:
morph_file.write(annotation)
with open(error_summary_file, 'w') as summary_file:
json.dump(summaries, summary_file, indent=4, cls=NeuromJSON)
with open(marker_file, 'w') as m_file:
json.dump(markers, m_file, cls=NeuromJSON)
# pylint: disable=function-redefined
@repair.command(short_help='Repair one morphology')
@click.argument('input_file', type=click.Path(exists=True, file_okay=True))
@click.argument('output_file')
@click.option('--plot_file', type=click.Path(file_okay=True), default=None,
help='Where to save the plot')
@click.option('-a', '--axon-donor', multiple=True,
help='A morphology that provides a reference axon')
@click.option('--cut-file',
type=click.Path(exists=True, file_okay=True), default=None,
help=('Path to a CSV whose columns represents the X, Y and Z '
'coordinates of points from which to start the repair'))
def file(input_file, output_file, plot_file, axon_donor, cut_file):
'''Repair dendrites of a cut neuron.'''
import pandas
from neuror.main import repair # pylint: disable=redefined-outer-name
if cut_file:
cut_points = pandas.read_csv(Path(cut_file).with_suffix('.csv')).values
else:
cut_points = None
repair(input_file, output_file, axons=axon_donor, cut_leaves_coordinates=cut_points,
plot_file=plot_file)
# pylint: disable=function-redefined
@repair.command(short_help='Repair all morphologies in a folder')
@click.argument('input_dir')
@click.argument('output_dir', type=click.Path(exists=True, file_okay=False, writable=True))
@click.option('--plot_dir', default=None, type=click.Path(exists=True, file_okay=False,
writable=True))
@click.option('-a', '--axon-donor', multiple=True,
help='A morphology that provides a reference axon')
@click.option('--cut-file-dir',
type=click.Path(exists=True, file_okay=True), default=None,
help=('A dir with the cut points CSV file for each morphology. '
'See also "neuror cut-plane repair file --help".'))
def folder(input_dir, output_dir, plot_dir, axon_donor, cut_file_dir):
'''Repair dendrites of all neurons in a directory.'''
from neuror.full import repair_all
repair_all(input_dir, output_dir, axons=axon_donor, cut_points_dir=cut_file_dir,
plots_dir=plot_dir)
@unravel.command(short_help='Unravel one morphology')
@click.argument('input_file', type=click.Path(exists=True, file_okay=True))
@click.argument('output_file')
@click.option('--mapping-file', type=click.Path(file_okay=True), default=None,
help=('Path to the file that contains the coordinate mapping before '
'and after unravelling'))
@click.option('--window-half-length', default=DEFAULT_WINDOW_HALF_LENGTH)
def file(input_file, output_file, mapping_file, window_half_length):
'''Unravel a cell.'''
from neuror.unravel import unravel # pylint: disable=redefined-outer-name
neuron, mapping = unravel(input_file, window_half_length=window_half_length)
neuron.write(output_file)
if mapping_file is not None:
if not mapping_file.lower().endswith('csv'):
raise NeuroRError('the mapping file must end with .csv')
mapping.to_csv(mapping_file)
@unravel.command(short_help='Unravel all morphologies in a folder')
@click.argument('input_dir')
@click.argument('output_dir', type=click.Path(exists=True, file_okay=False, writable=True))
@click.option('--raw-plane-dir', type=click.Path(exists=True, file_okay=False), default=None,
help='The path to raw cut planes (if None, defaults to INPUT_DIR/planes)')
@click.option('--unravelled-plane-dir', type=click.Path(exists=True, file_okay=False), default=None,
help='The path to unravelled cut planes (if None, defaults to OUTPUT_DIR/planes)')
@click.option('--window-half-length', default=DEFAULT_WINDOW_HALF_LENGTH)
def folder(input_dir, output_dir, raw_planes_dir, unravelled_planes_dir, window_half_length):
'''Unravel all cells in a folder'''
from neuror.unravel import unravel_all
unravel_all(input_dir, output_dir, raw_planes_dir, unravelled_planes_dir,
window_half_length=window_half_length)
@cli.command(short_help='Generate PDF with morphology plots')
@click.argument('folders', nargs=-1)
@click.option('--title', '-t', multiple=True)
def report(folders, title):
'''Generate a PDF with plots of pre and post repair neurons.'''
from neuror.view import view_all
if not folders:
print('Need to pass at least one folder')
return
if title:
assert len(title) == len(folders)
else:
title = [f'Plot {i}' for i in range(1, len(folders) + 1)]
view_all(folders, title)
@cli.command(short_help='Fix zero diameters')
@click.argument('input_file')
@click.argument('output_file')
def zero_diameters(input_file, output_file):
'''Output a morphology where the zero diameters have been removed.'''
from neuror.zero_diameter_fixer import fix_zero_diameters
neuron = Morphology(input_file)
fix_zero_diameters(neuron)
neuron.write(output_file)
# pylint: disable=function-redefined
@sanitize.command(short_help='Sanitize a morphology')
@click.argument('input_file')
@click.argument('output_file')
def file(input_file, output_file):
'''Sanitize a raw morphology.'''
from neuror.sanitize import sanitize # pylint: disable=redefined-outer-name
sanitize(input_file, output_file)
# pylint: disable=function-redefined
@sanitize.command(short_help='Sanitize all morphologies in a folder')
@click.argument('input_folder')
@click.argument('output_folder')
@click.option('--nprocesses', default=1, help='The number of processes to spawn')
def folder(input_folder, output_folder, nprocesses):
'''Sanitize all morphologies in the folder.'''
from neuror.sanitize import sanitize_all
sanitize_all(input_folder, output_folder, nprocesses=nprocesses)
@cut_plane.group()
def compute():
'''CLI utilities to compute cut planes.'''
def _check_results(result):
'''Check the result status.'''
if not result:
L.error('Empty results')
return -1
status = result.get('status')
if status.lower() != 'ok':
L.warning('Incorrect status: %s', status)
return 1
return 0
@compute.command(short_help='Find a 3D cut plane by providing a manual hint')
@click.argument('filename', type=str, required=True)
def hint(filename):
"""Launch the app to manually search for the cut plane. After running the command,
either click the link in the console or open your browser and go to the address
shown in the console.
Example::
neuror cut-plane compute hint ./tests/data/Neuron_slice.h5
"""
from neuror.cut_plane.viewer import app, set_neuron
set_neuron(filename)
app.run_server(debug=True)
def _export_cut_plane(filename, output, width, display, searched_axes, fix_position):
'''Find the position of the cut plane (it assumes the plane is aligned along X, Y or Z) for
morphology FILENAME.
It returns the cut plane and the positions of all cut terminations.
'''
if os.path.isdir(filename):
raise NeuroRError(f'filename ({filename}) should not be a directory')
result = CutPlane.find(filename,
width,
searched_axes=searched_axes,
fix_position=fix_position).to_json()
if not output:
pprint(result)
else:
with open(output, 'w') as output_file:
json.dump(result, output_file, cls=NeuromJSON)
_check_results(result)
if display:
from neuror.cut_plane.detection import plot
plot(load_morphology(filename), result)
@compute.command(short_help='Compute a cut plane for morphology FILENAME')
@click.argument('filename', type=str, required=True)
@click.option('-o', '--output',
help='Output name for the JSON file (default=STDOUT)')
@click.option('-w', '--width', type=float, default=3,
help='The bin width (in um) of the 1D distributions')
@click.option('-d', '--display', is_flag=True, default=False,
help='Flag to enable the display control plots')
@click.option('-p', '--plane', type=click.Choice(['x', 'y', 'z']), default=None,
help='Force the detection along the given plane')
@click.option('--position', type=float, default=None,
help='Force the position. Requires --plane to be set as well')
def file(filename, output, width, display, plane, position):
'''Find the position of the cut plane (it assumes the plane is aligned along X, Y or Z) for
morphology FILENAME.
It returns the cut plane and the positions of all cut terminations.
Compute a cut plane and outputs it either as a STDOUT stream or in a file
if ``-o`` option is passed.
The control plots can be displayed by passing the ``-d`` option.
The bin width can be changed with the ``-w`` option (see below)
Description of the algorithm:
#. The distribution of all points along X, Y and Z is computed
and put into 3 histograms.
#. For each histogram we look at the first and last empty bins
(that is, the last bin before the histogram starts rising,
and the first after it reaches zero again). Under the assumption
that there is no cut plane, the posteriori probability
of observing this empty bin given the value of the non-empty
neighbour bin is then computed.
#. The lowest probability of the 6 probabilities (2 for each axes)
corresponds to the cut plane.
.. image:: /_images/distrib_1d.png
:align: center
Returns:
A dictionary with the following items:
:status: 'ok' if everything went right, else an informative string
:cut_plane: a tuple (plane, position) where 'plane' is 'X', 'Y' or 'Z'
and 'position' is the position
:cut_leaves: an np.array of all termination points in the cut plane
:figures: if 'display' option was used, a dict where values are tuples (fig, ax)
for each figure
:details: A dict currently only containing -LogP of the bin where the cut plane was found
Example::
neuror cut-plane compute file -d tests/data/Neuron_slice.h5 -o my-plane.json -w 10
'''
_export_cut_plane(filename, output, width, display, plane or ('x', 'y', 'z'), position)
@compute.command(short_help='Compute cut planes for morphologies located in INPUT_DIR')
@click.argument('input_dir')
@click.argument('output_dir', type=click.Path(exists=True, file_okay=False, writable=True))
@click.option('-w', '--width', type=float, default=3,
help='The bin width (in um) of the 1D distributions')
@click.option('-d', '--display', is_flag=True, default=False,
help='Flag to enable the display control plots')
@click.option('-p', '--plane', type=click.Choice(['x', 'y', 'z']), default=None,
help='Force the detection along the given plane')
def folder(input_dir, output_dir, width, display, plane):
'''Compute cut planes for all morphology in INPUT_DIR and save them into OUTPUT_DIR
See "cut-plane compute --help" for more information'''
for inputfilename in iter_morphology_files(input_dir):
L.info('Seaching cut plane for file: %s', inputfilename)
outfilename = os.path.join(output_dir, inputfilename.with_suffix('.json'))
try:
_export_cut_plane(inputfilename, outfilename, width, display=display,
searched_axes=(plane or ('X', 'Y', 'Z')),
fix_position=None)
except Exception as e: # noqa, pylint: disable=broad-except
L.warning('Cut plane computation for %s failed', inputfilename)
L.warning(e, exc_info=True)
@cut_plane.command()
@click.argument('out_filename', nargs=1)
@click.argument('plane_paths', nargs=-1)
def join(out_filename, plane_paths):
'''Merge cut-planes from json files located at PLANE_PATHS into one.
The output is writen at OUT_FILENAME
Example::
neuror cut-plane join result.json plane1.json plane2.json plane3.json
'''
data = []
for plane in plane_paths:
with open(plane) as in_f:
data += json.load(in_f)
with open(out_filename, 'w') as out_f:
json.dump(data, out_f)