-
Notifications
You must be signed in to change notification settings - Fork 13
/
CATMAIDImport.py
2753 lines (2252 loc) · 106 KB
/
CATMAIDImport.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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""CATMAID to Blender Import Script.
Connects to CATMAID servers and retrieves skeleton data.
Copyright (C) Philipp Schlegel, 2014.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import bmesh
import bpy
import colorsys
import json
import math
import re
import requests
import time
import urllib
import numpy as np
from bpy.types import Panel, Operator, AddonPreferences
from bpy.props import (FloatVectorProperty, FloatProperty, StringProperty,
BoolProperty, EnumProperty, IntProperty,)
from bpy_extras.io_utils import orientation_helper, axis_conversion
from mathutils import Matrix
from collections import defaultdict
from collections.abc import Iterable
from concurrent.futures import ThreadPoolExecutor
from requests.exceptions import HTTPError
########################################
# Settings
########################################
bl_info = {
"name": "CATMAID Import",
"author": "Philipp Schlegel",
"version": (7, 1, 0),
"for_catmaid_version": '2020.02.15-684-gcbe37bd',
"blender": (2, 80, 0), # this MUST be 2.80.0 (i.e. not 2.9x)
"location": "View3D > Sidebar (N) > CATMAID",
"description": "Imports Neuron from CATMAID server, Analysis tools",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Object"}
client = None
# Will populate this later
catmaid_volumes = [('None', 'None', 'Do not import volume from this list')]
DEFAULTS = {
"connectors": {
0: {'color': (0, 0.8, 0.8, 1), # postsynapses
'name': 'Presynapses'},
1: {'color': (1, 0, 0, 1), # presynapses
'name': 'Postsynapses'},
2: {'color': (0, 1, 0, 1), # gap junctions
'name': 'GapJunctions'},
3: {'color': (0.5, 0, 0.5, 1), # abutting
'name': 'Abutting'},
}
}
########################################
# UI Elements
########################################
class CATMAID_PT_import_panel(Panel):
"""Creates import menu in viewport side menu."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_label = "Import"
bl_category = "CATMAID"
def draw(self, context):
layout = self.layout
ver_str = '.'.join([str(i) for i in bl_info['version']])
layout.label(text=f'CATMAID plugin v{ver_str}')
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("catmaid.connect", text="Connect to CATMAID", icon='PLUGIN')
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("fetch.neuron", text="Import Neuron(s)", icon='ARMATURE_DATA')
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("fetch.connectors", text="Import Connectors", icon='PMARKER_SEL')
row.operator("display.help", text="", icon='QUESTION').entry = 'retrieve.connectors'
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("import.volume", text='Import Volume', icon='IMPORT')
class CATMAID_PT_export_panel(Panel):
"""Creates export menu in viewport side menu."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_label = "Export"
bl_category = "CATMAID"
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("export.volume", text='Export Volume', icon='EXPORT')
class CATMAID_PT_properties_panel(Panel):
"""Creates properties menu in viewport side menu."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_label = "Neuron Properties"
bl_category = "CATMAID"
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("curve.change", text="Curve properties",
icon='COLOR_BLUE')
row.operator("display.help", text="", icon='QUESTION').entry = 'curve.change'
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("material.change", text="Set colors",
icon='COLOR_BLUE')
row.operator("display.help", text="", icon='QUESTION').entry = 'change.material'
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("material.randomize", text="Randomize colors", icon='COLOR')
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("material.kmeans", text="Color by clusters", icon='LIGHT_SUN')
row.operator("display.help", text="", icon='QUESTION').entry = 'material.kmeans'
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("color.by_annotation", text="Color by Annotation", icon='SORTALPHA')
row = layout.row(align=True)
row.alignment = 'EXPAND'
row.operator("color.by_strahler", text="Color by Strahler Index", icon='MOD_ARRAY')
row.operator("display.help", text="", icon='QUESTION').entry = 'color.by_strahler'
########################################
# Operators
########################################
class CATMAID_OP_connect(Operator):
bl_idname = "catmaid.connect"
bl_label = 'Connect CATMAID'
bl_description = "Connect to given CATMAID server"
local_http_user: StringProperty(name="HTTP User")
local_http_pw: StringProperty(name="HTTP Password",
subtype='PASSWORD')
local_token: StringProperty(name="API token",
description='How to retrieve Token: '
'http://catmaid.github.io/dev/api.html#api-token')
local_server_url: StringProperty(name="Server Url")
local_project_id: IntProperty(name='Project ID', default=0)
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.prop(self, "local_server_url")
row = layout.row(align=True)
row.prop(self, "local_token")
row = layout.row(align=True)
row.prop(self, "local_project_id")
row = layout.row(align=True)
row.prop(self, "local_http_user")
row = layout.row(align=True)
row.prop(self, "local_http_pw")
layout.label(text="Use Addon preferences to set peristent server url, credentials, etc.")
def invoke(self, context, event):
self.local_http_user = get_pref('http_user', '')
self.local_token = get_pref('api_token', '')
self.local_http_pw = get_pref('http_pw', '')
self.local_project_id = get_pref('project_id', 0)
self.local_server_url = get_pref('server_url', '')
self.max_threads = get_pref('max_threads', 20)
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
print('Connecting to CATMAID server')
print('URL: %s' % self.local_server_url)
print('HTTP user: %s' % self.local_http_user)
print('Token: %s' % self.local_token)
global client
client = CatmaidClient(server=self.local_server_url,
api_token=self.local_token,
http_user=self.local_http_user,
http_password=self.local_http_pw,
project_id=self.local_project_id,
max_threads=self.max_threads)
# Retrieve volumes, to test if connection is good:
try:
volumes = client.get_volume_list()
self.report({'INFO'}, 'Connection successful')
print('Test call successful')
except BaseException:
self.report({'ERROR'}, 'Connection failed: see console')
raise
global catmaid_volumes
catmaid_volumes = [('None', 'None', 'Do not import volume from list')]
catmaid_volumes += [(str(e[0]), e[1], str(e[2])) for e in volumes]
return {'FINISHED'}
class CATMAID_OP_fetch_neuron(Operator):
"""Fetch neurons."""
bl_idname = "fetch.neuron"
bl_label = 'Fetch neurons'
bl_description = "Fetch given neurons from global server"
names: StringProperty(name="Name(s)",
description="Search by neuron names. Separate "
"multiple names by commas. For example: "
"'neuronA,neuronB,neuronC'")
partial_match: BoolProperty(name="Allow partial matches?", default=False,
description="Allow partial matches for neuron "
"names and annotations! Will also "
"become case-insensitive")
annotations: StringProperty(name="Annotations(s)",
description="Search by skeleton IDs. Separate "
"multiple annotations by commas. For "
"example: 'annotation 1,annotation2"
",annotation3'")
intersect: BoolProperty(name="Intersect", default=False,
description="If true, all identifiers (e.g. two "
"annotations or name + annotation) "
"have to be true for a neuron to be "
"loaded")
skeleton_ids: StringProperty(name="Skeleton ID(s)",
description="Search by skeleton IDs. Separate "
"multiple IDs by commas. "
"Does not accept more than 400 "
"characters in total")
minimum_nodes: IntProperty(name="Minimum node count", default=0, min=0,
description="Neurons matching above criteria but with "
"fewer nodes than this will be ignored. "
"If > 0 AND all other filters are "
"left empty will fetch all skeletons "
" with this many nodes or more.")
import_synapses: BoolProperty(name="Synapses", default=True,
description="Import chemical synapses (pre- "
"and postsynapses), similarly to "
"3D Viewer in CATMAID")
import_gap_junctions: BoolProperty(name="Gap Junctions", default=False,
description="Import gap junctions, "
"similarly to 3D Viewer in "
"CATMAID")
import_abutting: BoolProperty(name="Abutting Connectors", default=False,
description="Import abutting connectors")
cn_spheres: BoolProperty(name="Connectors as spheres", default=False,
description="Import connectors as spheres instead "
"of curves")
downsampling: IntProperty(name="Downsampling Factor", default=2, min=1, max=20,
description="Will reduce number of nodes by given "
"factor. Root, ends and forks are "
"preserved")
use_radius: BoolProperty(name="Use node radii", default=False,
description="If true, neuron will use node radii "
"for thickness. If false, radius is "
"assumed to be 70nm (for visibility)")
neuron_mat_for_connectors: BoolProperty(name="Connectors same color as neuron",
default=False,
description="If true, connectors "
"will have the same "
"color as the neuron")
skip_existing: BoolProperty(name="Skip existing", default=True,
description="If True, will not add neurons that "
"are already in the scene")
# ATTENTION:
# using check() in an operator that uses threads, will lead to segmentation faults!
def check(self, context):
return True
@classmethod
def poll(cls, context):
if client:
return True
else:
return False
def draw(self, context):
layout = self.layout
box = layout.box()
row = box.row(align=False)
row.prop(self, "names")
row = box.row(align=False)
row.prop(self, "annotations")
row = box.row(align=False)
row.prop(self, "skeleton_ids")
row = box.row(align=False)
row.prop(self, "partial_match")
row.enabled = True if self.names or self.annotations else False
row.prop(self, "intersect")
row.enabled = True if self.names or self.annotations else False
row = box.row(align=False)
row.prop(self, "minimum_nodes")
layout.label(text="Import Options")
box = layout.box()
row = box.row(align=False)
row.prop(self, "import_synapses")
row.prop(self, "import_gap_junctions")
row.prop(self, "import_abutting")
row = box.row(align=False)
row.prop(self, "cn_spheres")
row.enabled = True if self.import_synapses or self.import_gap_junctions or self.import_abutting else False
row = box.row(align=False)
row.prop(self, "neuron_mat_for_connectors")
row.enabled = True if self.import_synapses or self.import_gap_junctions or self.import_abutting else False
row = box.row(align=False)
row.prop(self, "downsampling")
row = box.row(align=False)
row.prop(self, "use_radius")
row.prop(self, "skip_existing")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
retrieve_by_names = []
if self.names:
retrieve_by_names = client.search_names(self.names, self.partial_match)
if not len(retrieve_by_names):
print('ERROR: Search name(s) not found! Import stopped')
self.report({'ERROR'}, 'Search tag(s) not found! Import stopped')
return{'FINISHED'}
retrieve_by_skids = []
if self.skeleton_ids:
retrieve_by_skids = np.array([x.strip() for x in self.skeleton_ids.split(',')])
retrieve_by_annotations = []
if self.annotations:
# In case there are commas in the annotation, we use quotation marks
if self.annotations.startswith('"') and self.annotations.endswith('"'):
annotations = [self.annotations[1:-1]]
else:
annotations = [x.strip() for x in self.annotations.split(',')]
retrieve_by_annotations = client.search_annotations(annotations,
allow_partial=self.partial_match,
intersect=self.intersect)
if not len(retrieve_by_annotations):
print('ERROR: No matching annotation(s) found! Import stopped')
self.report({'ERROR'}, 'No matching annotation(s) found! Import stopped')
return{'FINISHED'}
if self.intersect:
skeletons_to_retrieve = set.intersection(set(retrieve_by_annotations),
set(retrieve_by_names))
if not skeletons_to_retrieve:
print('WARNING: No neurons left after intersection! Import stopped')
self.report({'ERROR'}, 'Intersection empty! Import stopped')
return{'FINISHED'}
else:
skeletons_to_retrieve = set.union(set(retrieve_by_annotations),
set(retrieve_by_names),
set(retrieve_by_skids))
if self.minimum_nodes > 1 and skeletons_to_retrieve:
print(f'Filtering {len(skeletons_to_retrieve)} neurons for size')
counts = client.get_node_counts(skeletons_to_retrieve)
skeletons_to_retrieve = [e for e in skeletons_to_retrieve if counts.get(str(e), 0) >= self.minimum_nodes]
if self.skip_existing:
existing_skids = {ob['skeleton_id'] for ob in bpy.data.objects if 'skeleton_id' in ob}
skeletons_to_retrieve = skeletons_to_retrieve - existing_skids
# Only if all other filters are empty AND a minimum node count has been
# provided, we will find skeletons by size
if not self.names and not self.annotations and not self.skeleton_ids and (self.minimum_nodes > 0):
skeletons_to_retrieve = set(list(client.search_size(self.minimum_nodes)))
if self.skip_existing:
existing_skids = {ob['skeleton_id'] for ob in bpy.data.objects if 'skeleton_id' in ob}
skeletons_to_retrieve = skeletons_to_retrieve - existing_skids
if not len(skeletons_to_retrieve):
raise ValueError('No skeletons matching the given criteria found!')
print(f'{len(skeletons_to_retrieve)} neurons found')
print('Resolving names...')
neuron_names = client.get_names(skeletons_to_retrieve)
print("Collecting skeleton data...")
start = time.time()
skdata = client.get_skeletons(list(skeletons_to_retrieve),
with_history=False,
with_abutting=self.import_abutting)
print(f"Importing {len(skdata)} skeletons into Blender...")
for skid in skdata:
# Create an object name
object_name = f'#{skid} - {neuron_names[str(skid)]}'
import_skeleton(skdata[skid],
skeleton_id=str(skid),
object_name=object_name,
downsampling=self.downsampling,
import_synapses=self.import_synapses,
import_gap_junctions=self.import_gap_junctions,
import_abutting=self.import_abutting,
use_radii=self.use_radius,
cn_as_curves=not self.cn_spheres,
neuron_mat_for_connectors=self.neuron_mat_for_connectors)
print(f'Finished Import in {time.time()-start:.1f}s')
return {'FINISHED'}
def _get_available_volumes(self, context):
"""Simply returns parsed list of available volumes."""
# Must be defined before CATMAID_OP_fetch_volume
return catmaid_volumes
class CATMAID_OP_fetch_volume(Operator):
"""Imports a volume as mesh from CATMAID."""
bl_idname = "import.volume"
bl_label = "Import volumes from CATMAID"
bl_description = "Fetch volume from server"
volume: EnumProperty(name='Import from List',
items=_get_available_volumes,
description='Select volume to be imported. List will '
'refresh on (re-)connecting to CATMAID '
'server.')
by_name: StringProperty(name='Import by Name', default='',
description='Name of volume to import.')
allow_partial: BoolProperty(name='Allow partial match', default=True,
description='If True, name can be a partial match.')
@classmethod
def poll(cls, context):
if client:
return True
return False
def draw(self, context):
layout = self.layout
row = layout.row()
row.alignment = 'CENTER'
row.label(text="Reconnect to CATMAID server to refresh list")
row = layout.row()
row.prop(self, "volume")
row = layout.row()
row.prop(self, "by_name")
row = layout.row()
row.prop(self, "allow_partial")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
volumes_to_retrieve = []
if self.volume != 'None':
volumes_to_retrieve.append(self.volume)
if self.by_name:
if self.allow_partial:
volumes_to_retrieve += [v[0] for v in catmaid_volumes if self.by_name.lower() in v[1].lower()]
else:
volumes_to_retrieve += [v[0] for v in catmaid_volumes if self.by_name.lower() == v[1].lower()]
for k, vol in enumerate(volumes_to_retrieve):
vertices, faces, name = client.get_volume(vol)
print(f'Importing volume {k} of {len(volumes_to_retrieve)}: '
f'{name} (ID {vol}) - {len(vertices)} vertices/'
f'{len(faces)} faces after clean-up')
import_mesh(vertices, faces, name=name)
return{'FINISHED'}
class CATMAID_OP_upload_volume(Operator):
"""Export a mesh as volume to CATMAID."""
bl_idname = "export.volume"
bl_label = "Export mesh to CATMAID"
bl_description = "Export mesh to CATMAID as volume"
volume_name: StringProperty(name='Name', default='',
description='If not explicitly provided, will '
'use the Blender object name.')
comment: StringProperty(name='Comment', default='',
description='Add comment to volume.')
@classmethod
def poll(cls, context):
if client:
return True
return False
def draw(self, context):
layout = self.layout
if not len(self.selected):
layout.label(text='Must have one or more meshes selected!')
return
elif len(self.selected) == 1:
layout.label(text='Single mesh selected for export:')
layout.prop(self, "volume_name")
else:
layout.label(text=f"{len(self.selected)} meshes selected: using objects' names for export:")
layout.prop(self, "comment")
layout.label(text="Meshes will show up in CATMAID 3D viewer and volume manager.")
layout.label(text="Requires CATMAID version 2016.04.18 or higher.")
layout.label(text="Polygon faces will be converted into triangles - please save Blender file before clicking OK!")
def invoke(self, context, event):
# Set selected objects at draw time
self.selected = [o for o in bpy.context.selected_objects if o.type == 'MESH']
if len(self.selected) == 1:
self.volume_name = self.selected[0].name
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
if not self.selected:
print("No meshes selected!")
return{'FINISHED'}
for i, obj in enumerate(self.selected):
if self.volume_name == '':
vol_name = obj.name
else:
vol_name = self.volume_name
# Make this object the active one
bpy.context.view_layer.objects.active = obj
# Check if mesh is trimesh:
is_trimesh = np.all([len(f.vertices) == 3 for f in obj.data.polygons])
if not is_trimesh:
print('Mesh not a trimesh - trying to convert')
# First go out of edit mode and select all vertices while in object mode
if obj.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Select all vertices
for v in obj.data.vertices:
v.select = True
# Now go to edit mode and convert to trimesh
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.quads_convert_to_tris()
bpy.ops.object.mode_set(mode='OBJECT')
# Check if again mesh is trimesh:
is_trimesh = np.all([len(f.vertices) == 3 for f in obj.data.polygons])
if all(is_trimesh):
print(f"{i} of {len(self.selected)}: Mesh '{vol_name}"
'successfully converted to trimesh!')
else:
print(f"{i} of {len(self.selected)}: Error during "
f"conversion of '{vol_name}' to trimesh "
"- try manually!")
continue
# Now create postdata
verts = np.empty(len(obj.data.vertices) * 3)
obj.data.vertices.foreach_get('co', verts) # this fills above array
verts = verts.reshape(len(obj.data.vertices), 3)
faces = np.empty(len(obj.data.polygons) * 3)
obj.data.polygons.foreach_get('vertices', faces)
faces = faces.reshape(len(obj.data.polygons), 3)
resp = client.upload_volume(verts, faces,
name=vol_name, comment=self.comment)
if 'success' in resp and resp['success'] is True:
print(f"{i} of {len(self.selected)}: Export of mesh '{vol_name}' "
"successful")
self.report({'INFO'}, 'Success!')
else:
print(f"{i} of {len(self.selected)}: Export of mesh '{vol_name}' "
"failed!")
self.report({'ERROR'}, 'See console for details!')
print(resp)
return{'FINISHED'}
class CATMAID_OP_display_help(Operator):
"""Displays popup with additional help."""
bl_idname = "display.help"
bl_label = "Advanced Tooltip"
bl_description = "Display help tooltip"
entry: StringProperty(name="which entry to show",
default='', options={'HIDDEN'})
def execute (self, context):
return {'FINISHED'}
def draw(self, context):
layout = self.layout
if self.entry == 'color.by_similarity':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Color Neurons by Similarity - Tooltip')
box = layout.box()
box.label(text='This function colors neurons based on how similar they are with respect to either: morphology, synapse placement, connectivity or paired connectivity.')
box.label(text='It is highly recommended to have SciPy installed - this will increase speed of calculation a lot!')
box.label(text='See https://github.com/schlegelp/CATMAID-to-Blender on how to install SciPy in Blender.')
box.label(text='Use <Settings> button to set parameters, then <Start Calculation>.')
layout.label(text='Morphology:')
box = layout.box()
box.label(text='Neurons that have close-by projections with similar orientation are')
box.label(text='similar. See Kohl et al. (2013, Cell).')
layout.label(text='Synapses:')
box = layout.box()
box.label(text='Neurons that have similar numbers of synapses in the same area')
box.label(text='are similar. See Schlegel et al. (2016, eLife).')
layout.label(text='Connectivity:')
box = layout.box()
box.label(text='Neurons that connects with similar number of synapses to the same')
box.label(text='partners are similar. See Schlegel et al. (2016, eLife).')
layout.label(text='Paired Connectivity:')
box = layout.box()
box.label(text='Neurons that mirror (left/right comparison) each others connectivity')
box.label(text='are similar. This requires synaptic partners to be paired with a')
box.label(text='<paired with #skeleton_id> annotation.')
elif self.entry == 'color.by_pairs':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Color Neurons by Pairs - Tooltip')
box = layout.box()
box.label(text='Gives paired neurons the same color. Pairing is based on annotations:')
box.label(text='Neuron needs to have a <paired with #skeleton_id> annotation.')
box.label(text='For example <paired with #1874652>.')
elif self.entry == 'material.kmeans':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Color by Spatial Distribution - Tooltip')
box = layout.box()
box.label(text='This function colors neurons based on spatial clustering of their somas.')
box.label(text='Uses the k-Mean algorithm. You need to set the number of clusters you')
box.label(text='expect and the algorithm finds clusters with smallest variance.')
elif self.entry == 'retrieve.by_pairs':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Retrieve Pairs of Neurons - Tooltip')
box = layout.box()
box.label(text='Retrieves neurons paired with already the loaded neurons. Pairing is')
box.label(text='based on annotations: neuron needs to have a <paired with #skeleton_id>')
box.label(text='annotation. For example neuron #1234 has annotation')
box.label(text='<paired with #5678> and neuron 5678 has annotation')
box.label(text='<paired with #1234>.')
elif self.entry == 'retrieve.connectors':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Retrieve Connectors - Tooltip')
box = layout.box()
box.label(text='Retrieves connectors as spheres. Outgoing (presynaptic) connectors can be')
box.label(text='scaled (weighted) based on the number of postsynaptically connected')
box.label(text='neurons. Incoming (postsynaptic) connectors always have base radius.')
elif self.entry == 'change.material':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Change Materials - Tooltip')
box = layout.box()
box.label(text='By default, all imported neurons have a standard material with random')
box.label(text='color. You can change the material of individual neurons in the ')
box.label(text='material tab or in bulk using this function. For more options')
box.label(text='see material tab.')
elif self.entry == 'color.by_strahler':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Color by Strahler - Tooltip')
box = layout.box()
box.label(text='Colors neurons by Strahler index. Result may will look odd')
box.label(text='in the viewport unless viewport shading is set to <render> or')
box.label(text='<material>. In any case, if you render it will look awesome!')
elif self.entry == 'animate.history':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Animate history - Tooltip')
box = layout.box()
box.label(text='This works essentially like the corresponding function of CATMAIDs 3D viewer: nodes and connectors pop into existence ')
box.label(text='as they were traced originally. Attention: using the <Skip idle phases> option will compromise relation between neurons')
box.label(text='because idle phases are currently calculated for each neuron individually. The <Show timer> will also be affected!')
elif self.entry == 'curve.change':
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Change Curve Properties - Tooltip')
box = layout.box()
box.label(text='Skeletons are created using curves. Bevel depth determines the ')
box.label(text='thickness of the curves. Radial (bevel) and curve resolution ')
box.label(text='determine how detailed the curves are - lower to improve ')
box.label(text='render performance.')
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=400)
class CATMAID_OP_material_change(Operator):
"""Change colors."""
bl_idname = "material.change"
bl_label = "Change colors"
bl_options = {'UNDO'}
bl_description = "Change color"
which_neurons: EnumProperty(name="Which Objects?",
items=[('Selected', 'Selected', 'Selected'),
('All', 'All', 'All')],
default='Selected',
description="Assign common material to which neurons")
to_neurons: BoolProperty(name='Neurons',
default=True,
description='Include neurons?')
to_outputs: BoolProperty(name='Presynapses',
default=False,
description='Include presynaptic sites (outgoing)?')
to_inputs: BoolProperty(name='Postsynapses',
default=False,
description='Include postsynaptic sites (incoming)?')
change_color: BoolProperty(name='Color', default=True, description='Change color?')
new_color: FloatVectorProperty(name="", description="Set new color",
default=(0.0, 1, 0.0), min=0.0, max=1.0,
subtype='COLOR')
def check(self, context):
return True
def draw(self, context):
layout = self.layout
layout.label(text="Apply to")
box = layout.box()
row = box.row(align=False)
row.prop(self, "which_neurons")
row = box.row(align=False)
row.prop(self, "to_neurons")
row.prop(self, "to_outputs")
row.prop(self, "to_inputs")
layout.label(text="Change")
box = layout.box()
row = box.row(align=False)
col = row.column()
col.prop(self, "change_color")
if self.change_color:
col = row.column()
col.prop(self, "new_color")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
if self.which_neurons == 'Selected':
ob_list = bpy.context.selected_objects
elif self.which_neurons == 'All':
ob_list = bpy.data.objects
filtered_ob_list = []
# First find the objects
for ob in ob_list:
if 'subtype' not in ob:
continue
if ob['subtype'] in ('NEURITES', 'SOMA') and not self.to_neurons:
continue
if ob['subtype'] in ('CONNECTORS'):
if ob['cn_type'] == 'Presynapses' and not self.to_outputs:
continue
if ob['cn_type'] == 'Postsynapses' and not self.to_inputs:
continue
filtered_ob_list.append(ob)
# Now apply cahnges
for ob in filtered_ob_list:
if self.change_color:
ob.active_material.diffuse_color = (self.new_color[0],
self.new_color[1],
self.new_color[2],
1)
self.report({'INFO'}, f'{len(filtered_ob_list)} materials changed')
return {'FINISHED'}
class CATMAID_OP_curve_change(Operator):
"""Change curve settings."""
bl_idname = "curve.change"
bl_label = "Change curve properties"
bl_options = {'UNDO'}
bl_description = "Change curve properties"
which_neurons: EnumProperty(name="Which Objects?",
items=[('Selected', 'Selected', 'Selected'),
('All', 'All', 'All')],
default='Selected',
description="Assign common material to which neurons")
to_neurons: BoolProperty(name='Neurons',
default=True,
description='Include neurons?')
to_outputs: BoolProperty(name='Presynapses',
default=False,
description='Include presynaptic sites (outgoing)?')
to_inputs: BoolProperty(name='Postsynapses',
default=False,
description='Include postsynaptic sites (incoming)?')
change_bevel: BoolProperty(name='Thickness', default=False,
description='Change neuron thickness (bevel)?')
new_bevel: FloatProperty(name="", description="Set new thickness.",
default=0.015, min=0)
change_bevel_res: BoolProperty(name='Radial resolution', default=False,
description='Change radial (bevel) resolution?')
new_bevel_res: IntProperty(name="", description="Set new resolution.",
default=5, min=0)
change_curve_res: BoolProperty(name='Curve resolution', default=False,
description='Change curve resolution?')
new_curve_res: IntProperty(name="", description="Set new resolution.",
default=10, min=0)
def check(self, context):
return True
def draw(self, context):
layout = self.layout
layout.label(text="Apply to")
box = layout.box()
row = box.row(align=False)
row.prop(self, "which_neurons")
row = box.row(align=False)
row.prop(self, "to_neurons")
row.prop(self, "to_outputs")
row.prop(self, "to_inputs")
layout.label(text="Change")
box = layout.box()
row = box.row(align=False)
col = row.column()
col.prop(self, "change_bevel")
if self.change_bevel:
col = row.column()
col.prop(self, "new_bevel")
row = box.row(align=False)
col = row.column()
col.prop(self, "change_bevel_res")
if self.change_bevel_res:
col = row.column()
col.prop(self, "new_bevel_res")
row = box.row(align=False)
col = row.column()
col.prop(self, "change_curve_res")
if self.change_curve_res:
col = row.column()
col.prop(self, "new_curve_res")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
if self.which_neurons == 'Selected':
ob_list = bpy.context.selected_objects
elif self.which_neurons == 'All':
ob_list = bpy.data.objects
filtered_ob_list = []
# First find the objects
for ob in ob_list:
if 'subtype' not in ob:
continue
if ob['subtype'] in ('NEURITES', 'SOMA') and not self.to_neurons:
continue
if ob['subtype'] in ('CONNECTORS'):
if ob['cn_type'] == 'Presynapses' and not self.to_outputs:
continue
if ob['cn_type'] == 'Postsynapses' and not self.to_inputs:
continue
filtered_ob_list.append(ob)
# Now apply cahnges
for ob in filtered_ob_list:
if self.change_bevel and ob.type == 'CURVE':
ob.data.bevel_depth = self.new_bevel
if self.change_bevel_res and ob.type == 'CURVE':
ob.data.bevel_resolution = self.new_bevel_res
if self.change_curve_res and ob.type == 'CURVE':
ob.data.resolution_u = self.new_curve_res
self.report({'INFO'}, f'{len(filtered_ob_list)} curves changed')
return {'FINISHED'}
class CATMAID_OP_material_randomize(Operator):
"""Assigns new semi-random colors to neurons"""
bl_idname = "material.randomize"
bl_label = "Assign (semi-) random colors"
bl_description = "Assign (semi-) random colors"
bl_options = {'UNDO'}
which_neurons: EnumProperty(name="Which Neurons?",
items=[('Selected', 'Selected', 'Selected'),
('All', 'All', 'All')],
description="Choose which neurons to give random color.",
default='All')
color_range: EnumProperty(name="Range",
items=(('RGB', 'RGB', 'RGB'),
("Grayscale", "Grayscale", "Grayscale"),),
default="RGB",
description="Choose mode of randomizing colors")
start_color: FloatVectorProperty(name="Color range start",
description="Set start of color range (for RGB). Keep start and end the same to use full range.",
default=(1, 0.0, 0.0), min=0.0, max=1.0,
subtype='COLOR')
end_color: FloatVectorProperty(name="Color range end",
description="Set end of color range (for RGB). Keep start and end the same to use full range.",
default=(1, 0.0, 0.0), min=0.0, max=1.0,
subtype='COLOR')
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
if self.which_neurons == 'All':
to_process = bpy.data.objects
elif self.which_neurons == 'Selected':
to_process = bpy.context.selected_objects
to_process = [o for o in to_process if 'type' in o]
to_process = [o for o in to_process if o['type'] == 'NEURON']
neurons = set([o['id'] for o in to_process])
colors = random_colors(len(neurons),
color_range=self.color_range,
start_rgb=self.start_color,
end_rgb=self.end_color,