ngsidekick 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ngsidekick/__init__.py +22 -0
- ngsidekick/annotations/__init__.py +0 -0
- ngsidekick/annotations/local.py +405 -0
- ngsidekick/annotations/precomputed/__init__.py +1 -0
- ngsidekick/annotations/precomputed/_id.py +34 -0
- ngsidekick/annotations/precomputed/_relationships.py +161 -0
- ngsidekick/annotations/precomputed/_spatial.py +594 -0
- ngsidekick/annotations/precomputed/_util.py +102 -0
- ngsidekick/annotations/precomputed/_write_buffers.py +206 -0
- ngsidekick/annotations/precomputed/compressed_morton.py +138 -0
- ngsidekick/annotations/precomputed/precomputed.py +489 -0
- ngsidekick/annotations/util.py +167 -0
- ngsidekick/py.typed +2 -0
- ngsidekick/segmentcolors.py +224 -0
- ngsidekick/segmentprops/__init__.py +1 -0
- ngsidekick/segmentprops/segmentprops.py +704 -0
- ngsidekick/segmentprops/select_segment_properties.py +128 -0
- ngsidekick/storage.py +147 -0
- ngsidekick/util.py +36 -0
- ngsidekick/video_tool_utils.py +367 -0
- ngsidekick-0.0.2.dist-info/METADATA +91 -0
- ngsidekick-0.0.2.dist-info/RECORD +24 -0
- ngsidekick-0.0.2.dist-info/WHEEL +4 -0
- ngsidekick-0.0.2.dist-info/licenses/LICENSE +30 -0
ngsidekick/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
neuroglancer-related utility functions
|
|
3
|
+
"""
|
|
4
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("ngsidekick")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
# Package is not installed
|
|
10
|
+
__version__ = "unknown"
|
|
11
|
+
|
|
12
|
+
from .storage import download_ngstate, upload_ngstate, upload_ngstates, upload_json, upload_to_bucket, make_bucket_public
|
|
13
|
+
from .util import parse_nglink, format_nglink, layer_dict, layer_state
|
|
14
|
+
from .annotations.local import (
|
|
15
|
+
local_annotation_json, extract_local_annotations,
|
|
16
|
+
|
|
17
|
+
# deprecated names
|
|
18
|
+
extract_annotations, annotation_layer_json, point_annotation_layer_json
|
|
19
|
+
)
|
|
20
|
+
from .annotations.precomputed import write_precomputed_annotations
|
|
21
|
+
from .segmentprops import segment_properties_json, segment_properties_to_dataframe
|
|
22
|
+
from .segmentcolors import hex_string_from_segment_id
|
|
File without changes
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from textwrap import indent, dedent
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from ..util import parse_nglink
|
|
8
|
+
from .util import annotation_property_specs
|
|
9
|
+
|
|
10
|
+
def extract_local_annotations(link, *, link_index=None, user=None, visible_only=False):
|
|
11
|
+
"""
|
|
12
|
+
Extract local://annotations data from a neuroglancer link.
|
|
13
|
+
The annotation coordinates (point, pointA, pointB, radii) are extracted
|
|
14
|
+
into separate columns, named x,y,z/xa,ya,za/xb,yb,zb/rx,ry,rz
|
|
15
|
+
(consistent with local_annotation_json() in this file).
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
link:
|
|
19
|
+
Either a neuroglancer JSON state (dict), or a neuroglancer link with embedded state,
|
|
20
|
+
or a neuroglancer link that references a JSON state file, such as
|
|
21
|
+
https://neuroglancer-demo.appspot.com/#!gs://flyem-views/hemibrain/v1.2/base.json
|
|
22
|
+
|
|
23
|
+
link_index:
|
|
24
|
+
Deprecated.
|
|
25
|
+
Adds a column named 'link_index' populated with the provided value in all rows.
|
|
26
|
+
|
|
27
|
+
user:
|
|
28
|
+
Deprecated.
|
|
29
|
+
Adds a column named 'user' populated with the provided value in all rows.
|
|
30
|
+
|
|
31
|
+
visible_only:
|
|
32
|
+
If True, do not extract annotations from layers that are not currently
|
|
33
|
+
visible in the given link state.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
DataFrame
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(link, str):
|
|
39
|
+
link = parse_nglink(link)
|
|
40
|
+
annotation_layers = [layer for layer in link['layers'] if layer['type'] == "annotation"]
|
|
41
|
+
|
|
42
|
+
dfs = []
|
|
43
|
+
for layer in annotation_layers:
|
|
44
|
+
if visible_only and (layer.get('archived', False) or not layer.get('visible', True)):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
_df = pd.DataFrame(layer['annotations'])
|
|
49
|
+
except KeyError as e:
|
|
50
|
+
continue
|
|
51
|
+
_df['layer'] = layer['name']
|
|
52
|
+
dfs.append(_df)
|
|
53
|
+
|
|
54
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
55
|
+
if 'point' in df.columns:
|
|
56
|
+
rows = df['point'].notnull()
|
|
57
|
+
df.loc[rows, [*'xyz']] = df.loc[rows, 'point'].tolist()
|
|
58
|
+
if 'pointA' in df.columns:
|
|
59
|
+
rows = df['pointA'].notnull()
|
|
60
|
+
df.loc[rows, ['xa', 'ya', 'za']] = df.loc[rows, 'pointA'].tolist()
|
|
61
|
+
if 'pointB' in df.columns:
|
|
62
|
+
rows = df['pointB'].notnull()
|
|
63
|
+
df.loc[rows, ['xb', 'yb', 'zb']] = df.loc[rows, 'pointB'].tolist()
|
|
64
|
+
if 'radii' in df.columns:
|
|
65
|
+
rows = df['radii'].notnull()
|
|
66
|
+
df.loc[rows, ['rx', 'ry', 'rz']] = df.loc[rows, 'radii'].tolist()
|
|
67
|
+
|
|
68
|
+
# Convert to int if possible.
|
|
69
|
+
for col in [*'xyz', 'xa', 'ya', 'za', 'xb', 'yb', 'zb', 'rx', 'ry', 'rz', 'radii']:
|
|
70
|
+
if col in df and df[col].notnull().all() and not (df[col] % 1).any():
|
|
71
|
+
df[col] = df[col].astype(np.int64)
|
|
72
|
+
|
|
73
|
+
if link_index is not None:
|
|
74
|
+
df['link_index'] = link_index
|
|
75
|
+
if user is not None:
|
|
76
|
+
df['user'] = user
|
|
77
|
+
|
|
78
|
+
df = df.drop(columns=['point', 'pointA', 'pointB', 'radii'], errors='ignore')
|
|
79
|
+
cols = ['link_index', 'user', 'layer', 'type', *'xyz', 'rx', 'ry', 'rz', 'xa', 'ya', 'za', 'xb', 'yb', 'zb', 'id', 'description']
|
|
80
|
+
df = df[[c for c in cols if c in df.columns]]
|
|
81
|
+
return df
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# legacy name
|
|
85
|
+
extract_annotations = extract_local_annotations
|
|
86
|
+
|
|
87
|
+
# Tip: Here's a nice repo with lots of colormaps implemented in GLSL.
|
|
88
|
+
# https://github.com/kbinani/colormap-shaders
|
|
89
|
+
SHADER_FMT = dedent("""\
|
|
90
|
+
void main() {{
|
|
91
|
+
setColor(defaultColor());
|
|
92
|
+
setPointMarkerSize({size:.1f});
|
|
93
|
+
}}
|
|
94
|
+
""")
|
|
95
|
+
|
|
96
|
+
LOCAL_ANNOTATION_JSON = {
|
|
97
|
+
"name": "annotations",
|
|
98
|
+
"type": "annotation",
|
|
99
|
+
"source": {
|
|
100
|
+
"url": "local://annotations",
|
|
101
|
+
"transform": {
|
|
102
|
+
"outputDimensions": {
|
|
103
|
+
"x": [
|
|
104
|
+
8e-09,
|
|
105
|
+
"m"
|
|
106
|
+
],
|
|
107
|
+
"y": [
|
|
108
|
+
8e-09,
|
|
109
|
+
"m"
|
|
110
|
+
],
|
|
111
|
+
"z": [
|
|
112
|
+
8e-09,
|
|
113
|
+
"m"
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"tool": "annotatePoint",
|
|
119
|
+
"shader": "\nvoid main() {\n setColor(defaultColor());\n setPointMarkerSize(8.0);\n}\n",
|
|
120
|
+
"panels": [
|
|
121
|
+
{
|
|
122
|
+
"row": 1,
|
|
123
|
+
"flex": 1.22,
|
|
124
|
+
"tab": "annotations"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"annotations": [
|
|
128
|
+
# {
|
|
129
|
+
# "point": [23367, 35249, 68171],
|
|
130
|
+
# "type": "point",
|
|
131
|
+
# "id": "149909688276769607",
|
|
132
|
+
# "description": "soma"
|
|
133
|
+
# },
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def local_annotation_json(df, name="annotations", color="#ffff00", size=8.0, linkedSegmentationLayer=None,
|
|
139
|
+
show_panel=False, properties=[], shader=None, res_nm_xyz=(8,8,8)):
|
|
140
|
+
"""
|
|
141
|
+
Construct the JSON data for a neuroglancer local annotations layer.
|
|
142
|
+
This does not result in a complete neuroglancer link; it results in something
|
|
143
|
+
that can be added to the layers list in the neuroglancer viewer JSON state.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
df:
|
|
148
|
+
DataFrame containing the annotation data.
|
|
149
|
+
Which columns you must provide depends on which annotation type(s) you want to display.
|
|
150
|
+
|
|
151
|
+
- For point annotations, provide ['x', 'y', 'z']
|
|
152
|
+
- For line annotations or axis_aligned_bounding_box annotations,
|
|
153
|
+
provide ['xa', 'ya', 'za', 'xb', 'yb', 'zb']
|
|
154
|
+
- For ellipsoid annotations, provide ['x', 'y', 'z', 'rx', 'ry', 'rz']
|
|
155
|
+
for the center point and radii.
|
|
156
|
+
|
|
157
|
+
You may also provide a column 'type' to explicitly set the annotation type.
|
|
158
|
+
In some cases, 'type' isn't needed since annotation type can be inferred from the
|
|
159
|
+
columns you provided. But in the case of line and box annotations, the input
|
|
160
|
+
columns are the same, so you must provide a 'type' column.
|
|
161
|
+
|
|
162
|
+
You may also provide additional columns to use as annotation properties,
|
|
163
|
+
in which case they should be listed in the 'properties' argument. (See below.)
|
|
164
|
+
|
|
165
|
+
name:
|
|
166
|
+
The name of the annotation layer
|
|
167
|
+
|
|
168
|
+
color:
|
|
169
|
+
The default color for annotations, which can be overridden by the annotation shader.
|
|
170
|
+
|
|
171
|
+
size:
|
|
172
|
+
The annotation size to hard-code into the default annotation shader used by this function.
|
|
173
|
+
(Only used for points and line endpoints.)
|
|
174
|
+
|
|
175
|
+
linkedSegmentationLayer:
|
|
176
|
+
If the annotations should be associated with another layer in the view,
|
|
177
|
+
this specifies the name of that layer.
|
|
178
|
+
This function sets the 'filterBySegmentation' key to hide annotations from non-selected segments.
|
|
179
|
+
If you are providing a linkedSegmentationLayer, your dataframe should contain
|
|
180
|
+
a 'segments' column to indicate which segments are associated with each annotation.
|
|
181
|
+
|
|
182
|
+
show_panel:
|
|
183
|
+
If True, the selection panel will be visible in the side bar by default.
|
|
184
|
+
|
|
185
|
+
properties:
|
|
186
|
+
The list column names to use as annotation properties.
|
|
187
|
+
Properties are visible in the selection panel when an annotation is selected,
|
|
188
|
+
and they can also be used in annotation shaders via special functions neuroglancer
|
|
189
|
+
defines for each property. For example, for a property named 'confidence',
|
|
190
|
+
you could write setPointMarkerSize(prop_confidence()) in your annotation shader.
|
|
191
|
+
|
|
192
|
+
This function supports annotation color proprties via strings (e.g. '#ffffff') and
|
|
193
|
+
also annotation 'enum' properties if you pass them via pandas categorical columns.
|
|
194
|
+
|
|
195
|
+
By default, the annotation IDs are the same as the column names and the annotation types are inferred.
|
|
196
|
+
You can override the property 'spec' by supplying a dict-of-dicts here instead of a list of columns:
|
|
197
|
+
|
|
198
|
+
properties={
|
|
199
|
+
"my_column": {
|
|
200
|
+
"id": "my property",
|
|
201
|
+
"description": "This is my annotation property.",
|
|
202
|
+
"type": "float32",
|
|
203
|
+
"enum_values": [0.0, 0.5, 1.0],
|
|
204
|
+
"enum_labels": ["nothing", "something", "everything"],
|
|
205
|
+
},
|
|
206
|
+
"another_column: {...}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
dict (JSON data)
|
|
211
|
+
"""
|
|
212
|
+
df = _standardize_annotation_dataframe(df)
|
|
213
|
+
if isinstance(properties, str):
|
|
214
|
+
properties = [properties]
|
|
215
|
+
|
|
216
|
+
data = copy.deepcopy(LOCAL_ANNOTATION_JSON)
|
|
217
|
+
res_m = (np.array(res_nm_xyz) * 1e-9).tolist()
|
|
218
|
+
output_dim = {k: [r, 'm'] for k,r in zip('xyz', res_m)}
|
|
219
|
+
data['source']['transform']['outputDimensions'] = output_dim
|
|
220
|
+
data['name'] = name
|
|
221
|
+
data['annotationColor'] = color
|
|
222
|
+
|
|
223
|
+
if shader:
|
|
224
|
+
data['shader'] = shader
|
|
225
|
+
else:
|
|
226
|
+
data['shader'] = _default_shader(df['type'].unique(), size)
|
|
227
|
+
|
|
228
|
+
if linkedSegmentationLayer:
|
|
229
|
+
data['linkedSegmentationLayer'] = linkedSegmentationLayer
|
|
230
|
+
data['filterBySegmentation'] = ['segments']
|
|
231
|
+
|
|
232
|
+
if not show_panel:
|
|
233
|
+
del data['panels']
|
|
234
|
+
|
|
235
|
+
prop_specs = annotation_property_specs(df, properties)
|
|
236
|
+
if prop_specs:
|
|
237
|
+
data['annotationProperties'] = prop_specs
|
|
238
|
+
properties = [p['id'] for p in prop_specs]
|
|
239
|
+
|
|
240
|
+
data['annotations'].clear()
|
|
241
|
+
data['annotations'] = _annotation_list_json(
|
|
242
|
+
df, linkedSegmentationLayer, properties
|
|
243
|
+
)
|
|
244
|
+
return data
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Deprecated name (now supports more than just points)
|
|
248
|
+
point_annotation_layer_json = local_annotation_json
|
|
249
|
+
|
|
250
|
+
# Deprecated name
|
|
251
|
+
annotation_layer_json = local_annotation_json
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _annotation_list_json(df, linkedSegmentationLayer, properties):
|
|
255
|
+
"""
|
|
256
|
+
Helper for local_annotation_json().
|
|
257
|
+
|
|
258
|
+
Generate the list of annotations for an annotation layer,
|
|
259
|
+
assuming the input dataframe has already been pre-conditioned.
|
|
260
|
+
"""
|
|
261
|
+
# Replace categoricals with their integer codes.
|
|
262
|
+
# The corresponding enum_labels are already stored in the property specs
|
|
263
|
+
for col in properties:
|
|
264
|
+
if df[col].dtype == "category":
|
|
265
|
+
df[col] = df[col].cat.codes
|
|
266
|
+
|
|
267
|
+
annotations = []
|
|
268
|
+
for row in df.itertuples():
|
|
269
|
+
entry = {}
|
|
270
|
+
entry['type'] = row.type
|
|
271
|
+
entry['id'] = row.id
|
|
272
|
+
if 'description' in df.columns:
|
|
273
|
+
entry['description'] = row.description
|
|
274
|
+
|
|
275
|
+
if row.type == 'point':
|
|
276
|
+
entry['point'] = [row.x, row.y, row.z]
|
|
277
|
+
elif row.type in ('line', 'axis_aligned_bounding_box'):
|
|
278
|
+
entry['pointA'] = [row.xa, row.ya, row.za]
|
|
279
|
+
entry['pointB'] = [row.xb, row.yb, row.zb]
|
|
280
|
+
elif row.type == 'ellipsoid':
|
|
281
|
+
entry['point'] = [row.x, row.y, row.z]
|
|
282
|
+
entry['radii'] = [row.rx, row.ry, row.rz]
|
|
283
|
+
else:
|
|
284
|
+
raise RuntimeError(f'Invalid annotation type: {row.type}')
|
|
285
|
+
|
|
286
|
+
if linkedSegmentationLayer and 'segments' in df.columns:
|
|
287
|
+
segments = row.segments
|
|
288
|
+
if not hasattr(segments, '__len__'):
|
|
289
|
+
segments = [segments]
|
|
290
|
+
segments = [str(int(s)) for s in segments]
|
|
291
|
+
entry['segments'] = segments
|
|
292
|
+
|
|
293
|
+
if properties:
|
|
294
|
+
entry['props'] = [getattr(row, prop) for prop in properties]
|
|
295
|
+
|
|
296
|
+
annotations.append(entry)
|
|
297
|
+
return annotations
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _standardize_annotation_dataframe(df):
|
|
301
|
+
"""
|
|
302
|
+
Helper for local_annotation_json().
|
|
303
|
+
Add empty columns as needed until the dataframe
|
|
304
|
+
has all possible annotation columns.
|
|
305
|
+
|
|
306
|
+
Also populate the 'type' column with inferred
|
|
307
|
+
annotation types based on the columns the user DID provide.
|
|
308
|
+
"""
|
|
309
|
+
df = df.copy()
|
|
310
|
+
id_cols = [*'xyz', 'xa', 'ya', 'za', 'xb', 'yb', 'zb', 'rx', 'ry', 'rz', 'type']
|
|
311
|
+
for col in id_cols:
|
|
312
|
+
if col not in df.columns:
|
|
313
|
+
df[col] = np.nan
|
|
314
|
+
|
|
315
|
+
if 'id' in df.columns:
|
|
316
|
+
df['id'] = df['id'].astype(str)
|
|
317
|
+
else:
|
|
318
|
+
df['id'] = [
|
|
319
|
+
str(hex(abs(hash(tuple(x)))))
|
|
320
|
+
for x in df[id_cols].values.tolist()
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
is_point_or_ellipsoid = df[[*'xyz']].notnull().all(axis=1)
|
|
324
|
+
is_line_or_box = df[['xa', 'ya', 'za', 'xb', 'yb', 'zb']].notnull().all(axis=1)
|
|
325
|
+
assert (is_point_or_ellipsoid ^ is_line_or_box).all(), \
|
|
326
|
+
"You must supply either [x,y,z] or [xa,ya,za,xb,yb,zb] for every row (and not both)."
|
|
327
|
+
|
|
328
|
+
df['type'] = df['type'].fillna(
|
|
329
|
+
df['rx'].isnull().map({
|
|
330
|
+
True: None,
|
|
331
|
+
False: 'ellipsoid'
|
|
332
|
+
})
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# We have no way of choosing between 'line' and 'axis_aligned_bounding_box'
|
|
336
|
+
# unless the user provides the 'type' explicitly.
|
|
337
|
+
# We default to 'axis_aligned_bounding_box' because it's harder to type than 'line' :-)
|
|
338
|
+
df['type'] = df['type'].fillna(
|
|
339
|
+
df['x'].isnull().map({
|
|
340
|
+
True: 'axis_aligned_bounding_box',
|
|
341
|
+
False: 'point'
|
|
342
|
+
})
|
|
343
|
+
)
|
|
344
|
+
return df
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _default_shader(annotation_types, default_size):
|
|
348
|
+
"""
|
|
349
|
+
Create a default annotation shader that is pre-populated with
|
|
350
|
+
the annotation API functions so you don't have to look them up.
|
|
351
|
+
"""
|
|
352
|
+
shader_body = ""
|
|
353
|
+
if 'point' in annotation_types:
|
|
354
|
+
# Note:
|
|
355
|
+
# In older versions of neuroglancer,
|
|
356
|
+
# setPointMarkerColor(vec3) doesn't exist yet,
|
|
357
|
+
# so we must use vec4.
|
|
358
|
+
# https://github.com/google/neuroglancer/pull/475
|
|
359
|
+
shader_body += dedent(f"""\
|
|
360
|
+
//
|
|
361
|
+
// Point Marker API
|
|
362
|
+
//
|
|
363
|
+
setPointMarkerSize({default_size});
|
|
364
|
+
setPointMarkerColor(vec4(defaultColor(), 1.0));
|
|
365
|
+
setPointMarkerBorderWidth(1.0);
|
|
366
|
+
setPointMarkerBorderColor(defaultColor());
|
|
367
|
+
""")
|
|
368
|
+
|
|
369
|
+
if 'line' in annotation_types:
|
|
370
|
+
shader_body += dedent(f"""\
|
|
371
|
+
//
|
|
372
|
+
// Line API
|
|
373
|
+
//
|
|
374
|
+
setLineColor(defaultColor());
|
|
375
|
+
setEndpointMarkerSize({default_size}, {default_size});
|
|
376
|
+
setEndpointMarkerColor(defaultColor(), defaultColor());
|
|
377
|
+
setEndpointMarkerBorderWidth(1.0, 1.0);
|
|
378
|
+
setEndpointMarkerBorderColor(defaultColor(), defaultColor());
|
|
379
|
+
""")
|
|
380
|
+
|
|
381
|
+
if 'axis_aligned_bounding_box' in annotation_types:
|
|
382
|
+
shader_body += dedent("""\
|
|
383
|
+
//
|
|
384
|
+
// Bounding Box API
|
|
385
|
+
//
|
|
386
|
+
setBoundingBoxBorderWidth(1.0);
|
|
387
|
+
setBoundingBoxBorderColor(defaultColor());
|
|
388
|
+
setBoundingBoxFillColor(vec4(defaultColor(), 0.5));
|
|
389
|
+
""")
|
|
390
|
+
|
|
391
|
+
if 'ellipsoid' in annotation_types:
|
|
392
|
+
shader_body += dedent("""\
|
|
393
|
+
//
|
|
394
|
+
// Ellipsoid API
|
|
395
|
+
//
|
|
396
|
+
setEllipsoidFillColor(defaultColor());
|
|
397
|
+
""")
|
|
398
|
+
|
|
399
|
+
shader_main = dedent(f"""\
|
|
400
|
+
void main() {{
|
|
401
|
+
{indent(shader_body, ' '*12)[12:]}\
|
|
402
|
+
}}
|
|
403
|
+
""")
|
|
404
|
+
|
|
405
|
+
return shader_main
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .precomputed import write_precomputed_annotations, TableHandle
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from ._write_buffers import _write_buffers
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _write_annotations_by_id(df, output_dir, write_sharded):
|
|
8
|
+
"""
|
|
9
|
+
Write the annotations to the "Annotation ID Index", a subdirectory of output_dir.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
df:
|
|
13
|
+
DataFrame with columns ['id_buf', 'ann_buf'].
|
|
14
|
+
|
|
15
|
+
output_dir:
|
|
16
|
+
Directory to write the annotations to.
|
|
17
|
+
A single subdirectory named 'by_id' will be created in output_dir.
|
|
18
|
+
|
|
19
|
+
write_sharded:
|
|
20
|
+
Whether to write the annotations in sharded format.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
JSON metadata to be written under the 'by_id' key in the top-level 'info' file.
|
|
24
|
+
Currently, this is always {"key": "by_id"}
|
|
25
|
+
"""
|
|
26
|
+
if 'rel_buf' in df.columns:
|
|
27
|
+
ann_bufs = df['ann_buf'] + df['rel_buf']
|
|
28
|
+
else:
|
|
29
|
+
ann_bufs = df['ann_buf']
|
|
30
|
+
|
|
31
|
+
logger.info("Writing annotations to 'by_id' index")
|
|
32
|
+
metadata = _write_buffers(ann_bufs, output_dir, "by_id", write_sharded)
|
|
33
|
+
return metadata
|
|
34
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from ._util import _encode_uint64_series, TableHandle
|
|
6
|
+
from ._write_buffers import _write_buffers
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _encode_relationships(df, relationships):
|
|
12
|
+
"""
|
|
13
|
+
For each annotation in the given dataframe, encode the related IDs
|
|
14
|
+
for all relationships into a buffer according to the neuroglancer spec.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
pd.Series of dtype=object, containing one buffer for each annotation.
|
|
18
|
+
"""
|
|
19
|
+
if not relationships:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
encoded_relationships = {}
|
|
23
|
+
for rel_col in relationships:
|
|
24
|
+
encoded_relationships[rel_col] = _encode_related_ids(df[rel_col])
|
|
25
|
+
|
|
26
|
+
# Concatenate buffers on each row.
|
|
27
|
+
# Note:
|
|
28
|
+
# Using sum() is O(R^2) in the number of relationships R, but we generally
|
|
29
|
+
# expect few relationships, so this is faster than df.apply(b''.join, axis=1),
|
|
30
|
+
# since .sum() uses a single C call whereas using b''.join() would
|
|
31
|
+
# use many Python calls.
|
|
32
|
+
rel_bufs = pd.DataFrame(encoded_relationships, index=df.index).sum(axis=1)
|
|
33
|
+
return rel_bufs
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _encode_related_ids(related_ids):
|
|
37
|
+
"""
|
|
38
|
+
Given a Series containing lists of IDs, encode each list of IDs
|
|
39
|
+
in the format neuroglancer expects for each relationship in an
|
|
40
|
+
annotation.
|
|
41
|
+
|
|
42
|
+
Each item in related_ids is a list, which gets encoded as
|
|
43
|
+
<count><id_1><id_2><id_3>..., where <count> is uint32 and
|
|
44
|
+
<id_1><id_2><id_3>... are each uint64.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
related_ids:
|
|
48
|
+
A Series of length N and dtype=object, containing lists of IDs.
|
|
49
|
+
As a special convenience in the case where every row contains
|
|
50
|
+
exactly one ID, you may pass a series with dtype=auint64,
|
|
51
|
+
which will be interpreted as if each entry were a list of length 1.
|
|
52
|
+
(In this case, the implementation is slightly faster than in the general case.)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A numpy array with N entries, where each entry is a buffer as shown above.
|
|
56
|
+
"""
|
|
57
|
+
# Special case if the relationship contains only a single ID for each annotation.
|
|
58
|
+
if np.issubdtype(related_ids.dtype, np.integer):
|
|
59
|
+
buf = (
|
|
60
|
+
pd.DataFrame({'count': np.uint32(1), 'id': related_ids})
|
|
61
|
+
.astype({'count': np.uint32, 'id': np.uint64}, copy=False)
|
|
62
|
+
.to_records(index=False)
|
|
63
|
+
.tobytes()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
encoded_ids = [
|
|
67
|
+
buf[i*12:(i+1)*12]
|
|
68
|
+
for i in range(len(related_ids))
|
|
69
|
+
]
|
|
70
|
+
return np.array(encoded_ids, dtype=object)
|
|
71
|
+
|
|
72
|
+
# Otherwise, the relationship contains lists.
|
|
73
|
+
else:
|
|
74
|
+
assert related_ids.dtype == object
|
|
75
|
+
counts = related_ids.map(len).to_numpy(np.uint32)
|
|
76
|
+
offsets = 8 * np.cumulative_sum(counts, include_initial=True)
|
|
77
|
+
|
|
78
|
+
ids_buf = np.concatenate(related_ids, dtype=np.uint64).tobytes()
|
|
79
|
+
counts_buf = counts.tobytes()
|
|
80
|
+
|
|
81
|
+
encoded_ids = [
|
|
82
|
+
counts_buf[i*4:(i+1)*4] + ids_buf[start:end]
|
|
83
|
+
for i, (start, end) in enumerate(zip(offsets[:-1], offsets[1:]))
|
|
84
|
+
]
|
|
85
|
+
return np.array(encoded_ids, dtype=object)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _write_annotations_by_relationships(df_handle: TableHandle, relationships, output_dir, write_sharded):
|
|
89
|
+
"""
|
|
90
|
+
Write the annotations to a "Related Object ID Index" for each relationship.
|
|
91
|
+
Each relationship is written to a separate subdirectory of output_dir.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
df_handle:
|
|
95
|
+
TableHandle holding a DataFrame with columns ['id_buf', 'ann_buf', *relationships].
|
|
96
|
+
The handle's reference will be unset before this function returns.
|
|
97
|
+
|
|
98
|
+
relationships:
|
|
99
|
+
List of relationship column names.
|
|
100
|
+
|
|
101
|
+
output_dir:
|
|
102
|
+
Directory to write the annotations to.
|
|
103
|
+
Each relationship is written to a separate subdirectory of output_dir.
|
|
104
|
+
|
|
105
|
+
write_sharded:
|
|
106
|
+
Whether to write the annotations in sharded format.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
JSON metadata to be written under the 'relationships' key in the top-level 'info' file,
|
|
110
|
+
consisting of a list of JSON objects (one for each relationship).
|
|
111
|
+
"""
|
|
112
|
+
handles = {
|
|
113
|
+
r: TableHandle(df_handle.df) for r in relationships
|
|
114
|
+
}
|
|
115
|
+
df_handle.df = None
|
|
116
|
+
|
|
117
|
+
by_rel_metadata = []
|
|
118
|
+
for relationship, df_handle in handles.items():
|
|
119
|
+
metadata = _write_annotations_by_relationship(
|
|
120
|
+
df_handle,
|
|
121
|
+
relationship,
|
|
122
|
+
output_dir,
|
|
123
|
+
write_sharded
|
|
124
|
+
)
|
|
125
|
+
by_rel_metadata.append(metadata)
|
|
126
|
+
|
|
127
|
+
return by_rel_metadata
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _write_annotations_by_relationship(df_handle: TableHandle, relationship, output_dir, write_sharded):
|
|
131
|
+
"""
|
|
132
|
+
Write the annotations to a "Related Object ID Index" for a single relationship.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
JSON metadata for the relationship, including the key and sharding spec if applicable.
|
|
136
|
+
"""
|
|
137
|
+
logger.info(f"Grouping annotations by relationship {relationship}")
|
|
138
|
+
bufs_by_segment = (
|
|
139
|
+
df_handle.df[['id_buf', 'ann_buf', relationship]]
|
|
140
|
+
.dropna(subset=relationship)
|
|
141
|
+
.explode(relationship)
|
|
142
|
+
.groupby(relationship, sort=False)
|
|
143
|
+
# Use b''.join() instead of 'sum' to avoid O(N^2) performance for large groups.
|
|
144
|
+
.agg({'id_buf': ['count', b''.join], 'ann_buf': b''.join})
|
|
145
|
+
)
|
|
146
|
+
df_handle.df = None
|
|
147
|
+
|
|
148
|
+
logger.info(f"Combining annotation and ID buffers for relationship '{relationship}'")
|
|
149
|
+
bufs_by_segment.columns = ['count', 'id_buf', 'ann_buf']
|
|
150
|
+
bufs_by_segment['count_buf'] = _encode_uint64_series(bufs_by_segment['count'])
|
|
151
|
+
bufs_by_segment['combined_buf'] = bufs_by_segment[['count_buf', 'ann_buf', 'id_buf']].sum(axis=1)
|
|
152
|
+
|
|
153
|
+
logger.info(f"Writing annotations to 'by_rel_{relationship}' index")
|
|
154
|
+
metadata = _write_buffers(
|
|
155
|
+
bufs_by_segment['combined_buf'],
|
|
156
|
+
output_dir,
|
|
157
|
+
f"by_rel_{relationship}",
|
|
158
|
+
write_sharded
|
|
159
|
+
)
|
|
160
|
+
metadata['id'] = relationship
|
|
161
|
+
return metadata
|