Skip to content

Commit 6594af4

Browse files
committed
Cropping tool: add support for Neuroglancer Precomputed data
This update allows users to extract sub-volumes from Neuroglancer Precomputed stacks using the Cropping Tool. Also move the Neuroglancer Precomputed changelog text to the correct position.
1 parent 3ebd855 commit 6594af4

File tree

3 files changed

+123
-37
lines changed

3 files changed

+123
-37
lines changed

CHANGELOG.md

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@
3434

3535
### Features and enhancements
3636

37+
Data sources:
38+
39+
- A new tile source has been added: Neuroglancer Precomputed data, with ID 14.
40+
This image block source works very similar to the N5 tile source. At the
41+
moment only GZip and JPEG compression is supported. [Sharded](Sharded) datasets can be
42+
loaded. In order for the voxel space coordinates to match between CATMAID and
43+
Neuroglancer, if the Neuroglancer dataset defines a voxel offset, the
44+
respective CATMAID stack needs to have its zoom-level zero voxel offset
45+
defined in the stack meta data in the admin view, e.g. `{"voxelOffset":
46+
[-3072, -3072, 0]}`.
47+
48+
- Neuroglancer Precomputed URLs that start with `gs://` are rewritten on the fly
49+
to use Google Cloud storage get API requess, improving CORS support.
50+
51+
Cropping tool:
52+
53+
- Neuroglancer Precomputed datasets (sharded and unsharded) are now supported
54+
and can be used extract sub-volumes.
55+
3756
Connector types:
3857

3958
- A new connector has been added to link mitochondria to cells. It can be used
@@ -49,7 +68,6 @@ Neuron similarity widget:
4968
original NBLAST publication. The UI allows to adjust dot and distance breaks
5069
as well as an easy way to add an optional scale factor.
5170

52-
5371
Tracing data cache:
5472

5573
- The management command `catmaid_update_cache_tables` can now remove existing
@@ -635,19 +653,7 @@ Vagrant:
635653

636654
Data sources:
637655

638-
- A new tile source has been added: Neuroglancer Precomputed data, with ID 14.
639-
This image block source works very similar to the N5 tile source. At the
640-
moment only GZip and JPEG compression is supported. Sharded datasets can be
641-
loaded. In order for the voxel space coordinates to match between CATMAID and
642-
Neuroglancer, if the Neuroglancer dataset defines a voxel offset, the
643-
respective CATMAID stack needs to have its zoom-level zero voxel offset
644-
defined in the stack meta data in the admin view, e.g. `{"voxelOffset":
645-
[-3072, -3072, 0]}`.
646-
647-
- Neuroglancer Precomputed URLs that start with `gs://` are rewritten on the fly
648-
to use Google Cloud storage get API requess, improving CORS support.
649-
650-
- A second new tile source is available: CloudVolume based tiles, generated by
656+
- A new tile source is available: CloudVolume based tiles, generated by
651657
the back-end. While slow for many users, it is a convenient way of make
652658
neuroglancer volumes available to CATMAID if they aren't supported by the
653659
Neuroglancer Precomputed source. It works better for a small set of users and

django/applications/catmaid/control/cropping.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import os.path
99
import math
10+
import numpy as np
1011
from PIL import Image as PILImage, TiffImagePlugin
1112
import requests
1213
from time import time
@@ -84,11 +85,10 @@ def __init__(self, user, project_id, stack_mirror_ids, x_min,
8485
self.single_channel = single_channel
8586
self.output_path = output_path
8687

87-
def get_tile_path(self, stack, mirror, tile_coords) -> str:
88-
""" This method will be used when get_tile_path is called after the
89-
job has been initialized.
90-
"""
88+
def get_source_metadata(self, stack, mirror, tile_coords):
9189
tile_source = self.stack_tile_sources[stack.id]
90+
if tile_source.is_block_source:
91+
return tile_source.get_block_details(mirror, tile_coords, self.zoom_level)
9292
return tile_source.get_tile_url(mirror, tile_coords, self.zoom_level)
9393

9494
def create_tiff_metadata(self, n_images):
@@ -181,8 +181,8 @@ class ImagePart:
181181
""" A part of a 2D image where height and width are not necessarily
182182
of the same size. Provides readout of the defined sub-area of the image.
183183
"""
184-
def __init__( self, path, x_min_src, x_max_src, y_min_src, y_max_src, x_dst, y_dst ):
185-
self.path = path
184+
def __init__( self, meta, x_min_src, x_max_src, y_min_src, y_max_src, x_dst, y_dst ):
185+
self.meta = meta
186186
self.x_min_src = x_min_src
187187
self.x_max_src = x_max_src
188188
self.y_min_src = y_min_src
@@ -208,19 +208,39 @@ def __str__(self):
208208
f'({self.x_max_src}, {self.y_min_src})')
209209

210210
def get_image(self):
211-
# Open the image
212-
try:
213-
r = requests.get(self.path, allow_redirects=True, verify=verify_ssl, timeout=1)
214-
if not r:
215-
raise ValueError(f"Could not get {self.path}")
216-
if r.status_code != 200:
217-
raise ValueError(f"Unexpected status code ({r.status_code}) for {self.path}")
218-
img_data = r.content
219-
bytes_read = len(img_data)
220-
except requests.exceptions.RequestException as e:
221-
raise ImageRetrievalError(self.path, str(e))
222-
223-
image = PILImage.open(BytesIO(img_data))
211+
if type(self.meta) == str:
212+
# Open the image
213+
try:
214+
r = requests.get(self.meta, allow_redirects=True, verify=verify_ssl, timeout=1)
215+
if not r:
216+
raise ValueError(f"Could not get {self.meta}")
217+
if r.status_code != 200:
218+
raise ValueError(f"Unexpected status code ({r.status_code}) for {self.meta}")
219+
img_data = r.content
220+
bytes_read = len(img_data)
221+
except requests.exceptions.RequestException as e:
222+
raise ImageRetrievalError(self.meta, str(e))
223+
224+
image = PILImage.open(BytesIO(img_data))
225+
elif type(self.meta) == dict:
226+
mirror = self.meta['mirror']
227+
tile_coord = self.meta['tile_coord']
228+
x = tile_coord[0] * mirror.tile_width
229+
y = tile_coord[1] * mirror.tile_height
230+
z = self.meta['tile_coord'][2]
231+
232+
cutout = self.meta['dataset'][
233+
x:(x + mirror.tile_width),
234+
y:(y + mirror.tile_height),
235+
z]
236+
237+
# FIXME: Don't just assume first channel (last dimension below)
238+
image_data = np.transpose(cutout[:,:,0,0])
239+
bytes_read = len(image_data)
240+
image = PILImage.frombuffer('RGBA',
241+
(mirror.tile_width, mirror.tile_height), image_data, 'raw', 'L', 0, 1)
242+
else:
243+
raise ValueError(f'Unknown image meta data type: {self.meta}')
224244

225245
src_width, src_height = image.size
226246

@@ -374,6 +394,11 @@ class BB:
374394
width = 0
375395
height = 0
376396

397+
def __str__(self):
398+
return f'Bounding Box: ({self.px_x_min}, {self.px_y_min}, {self.px_z_min}) - ' + \
399+
f'({self.px_x_max}, {self.px_y_max}, {self.px_z_max}) ' + \
400+
f'[width: {self.width}, height: {self.height}]'
401+
377402

378403
def extract_substack_no_rotation(job) -> List:
379404
""" Extracts a sub-stack as specified in the passed job without respecting
@@ -484,9 +509,9 @@ def extract_substack_no_rotation(job) -> List:
484509
cur_px_y_max = bb.px_y_max - y * tile_height
485510
# Create an image part definition
486511
z = bb.px_z_min + nz
487-
path = job.get_tile_path(stack, mirror, (x, y, z))
512+
source_meta = job.get_source_metadata(stack, mirror, (x, y, z))
488513
try:
489-
part = ImagePart(path, cur_px_x_min, cur_px_x_max,
514+
part = ImagePart(source_meta, cur_px_x_min, cur_px_x_max,
490515
cur_px_y_min, cur_px_y_max, x_dst, y_dst)
491516
image_parts.append( part )
492517
except Exception as e:

django/applications/catmaid/control/tile.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ def put_tile(request:HttpRequest, project_id=None, stack_id=None) -> HttpRespons
214214
return HttpResponse("Image pushed to HDF5.", content_type="plain/text")
215215

216216

217-
class TileSource(object):
217+
class TileSource():
218+
219+
is_block_source = False
218220

219221
def get_canary_url(self, mirror) -> str:
220222
"""Get the canary URL for this mirror.
@@ -287,10 +289,63 @@ def get_tile_url(self, mirror, tile_coords, zoom_level=0) -> str:
287289
return path
288290

289291

292+
class NeuroglancerTileSource(TileSource):
293+
""" Uses cloud-volume to access neuroglancer data.
294+
"""
295+
296+
is_block_source = True
297+
description = "Neuroglancer Precomputed image stack"
298+
param_token = '%SCALE_DATASET%'
299+
300+
def __init__(self):
301+
# Init cloud volume
302+
self.dataset = None
303+
304+
super().__init__()
305+
306+
def normalize_url_path(self, url, path):
307+
if not url[-1] == '/':
308+
url = url + '/'
309+
310+
url = url + path
311+
312+
if url.startswith("gs://"):
313+
components = url.split("/");
314+
if len(components) < 4:
315+
raise ValueError(f'Unsupported Google Cloud URL: {path}');
316+
bucketName = components[2];
317+
subPath = "%2F".join(components[3:])
318+
return f'https://www.googleapis.com/storage/v1/b/{bucketName}/o/{subPath}?alt=media'
319+
320+
return url
321+
322+
def get_canary_url(self, mirror) -> str:
323+
# Basic info URL
324+
url = mirror.image_base[:mirror.image_base.index(self.param_token)]
325+
return self.normalize_url_path(url, 'info')
326+
327+
def get_block_details(self, mirror, tile_coords, zoom_level=0) -> dict:
328+
if not cloudvolume_available:
329+
raise ValueError('Cloudvolume not available')
330+
if not self.dataset:
331+
url = mirror.image_base[:mirror.image_base.index(self.param_token)]
332+
self.dataset = cloudvolume.CloudVolume(f"precomputed://{url}",
333+
mip=zoom_level,
334+
use_https=True,
335+
bounded=False)
336+
return {
337+
'dataset': self.dataset,
338+
'tile_coord': tile_coords,
339+
'zoom_level': zoom_level,
340+
'mirror': mirror,
341+
}
342+
343+
290344
tile_source_map = {
291345
TileSourceTypes.FILE_BASED_STACK: DefaultTileSource,
292346
TileSourceTypes.FILE_BASED_ZOOM_STACK: BackslashTileSource,
293-
TileSourceTypes.DIRECTORY_BASED_STACK: LargeDataTileSource
347+
TileSourceTypes.DIRECTORY_BASED_STACK: LargeDataTileSource,
348+
TileSourceTypes.NEUROGLANCER_PRECOMPUTED: NeuroglancerTileSource,
294349
}
295350

296351
def get_tile_source(type_id):

0 commit comments

Comments
 (0)