Skip to content

Commit

Permalink
Merge pull request #371 from girder/centroids-master
Browse files Browse the repository at this point in the history
Add an option to get centroids
  • Loading branch information
manthey authored Sep 19, 2019
2 parents 0f6e4fa + ba91786 commit 6522964
Show file tree
Hide file tree
Showing 11 changed files with 1,232 additions and 727 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ def getElements(self, annotation, region=None):
The sum of the details values of the elements may exceed maxDetails
slightly (the sum of all but the last element will be less than
maxDetails, but the last element may exceed the value).
centroids: if specified and true, only return the id, center of the
bounding box, and bounding box size for each element.
:param annotation: the annotation to get elements for. Modified.
:param region: if present, a dictionary restricting which annotations
Expand All @@ -128,8 +130,7 @@ def getElements(self, annotation, region=None):

def yieldElements(self, annotation, region=None, info=None):
"""
Given an annotation, fetch the elements from the database and add them
to it.
Given an annotation, fetch the elements from the database.
When a region is used to request specific element, the following
keys can be specified:
left, right, top, bottom, low, high: the spatial area where
Expand All @@ -149,10 +150,21 @@ def yieldElements(self, annotation, region=None, info=None):
The sum of the details values of the elements may exceed maxDetails
slightly (the sum of all but the last element will be less than
maxDetails, but the last element may exceed the value).
centroids: if specified and true, only return the id, center of the
bounding box, and bounding box size for each element.
:param annotation: the annotation to get elements for. Modified.
:param region: if present, a dictionary restricting which annotations
are returned.
:param info: an optional dictionary that will be modified with
additional query information, including count (total number of
available elements), returned (number of elements in response),
maxDetails (as specified by the region dictionary), details (sum of
details returned), limit (as specified by region), centroids (a
boolean based on the region specification).
:returns: a list of elements. If centroids were requested, each entry
is a list with str(id), x, y, size. Otherwise, each entry is the
element record.
"""
info = info if info is not None else {}
region = region or {}
Expand All @@ -176,9 +188,25 @@ def yieldElements(self, annotation, region=None, info=None):
queryLimit = maxDetails if maxDetails and (not limit or maxDetails < limit) else limit
offset = int(region['offset']) if region.get('offset') else 0
logger.debug('element query %r for %r', query, region)
fields = {'_id': True, 'element': True, 'bbox.details': True}
centroids = str(region.get('centroids')).lower() == 'true'
if centroids:
# fields = {'_id': True, 'element': True, 'bbox': True}
fields = {
'_id': True,
'element.id': True,
'bbox': True}
proplist = []
propskeys = ['type', 'fillColor', 'lineColor', 'lineWidth', 'closed']
for key in propskeys:
fields['element.%s' % key] = True
props = {}
info['centroids'] = True
info['props'] = proplist
info['propskeys'] = propskeys
elementCursor = self.find(
query=query, sort=[(sortkey, sortdir)], limit=queryLimit, offset=offset,
fields={'_id': True, 'element': True, 'bbox.details': True})
query=query, sort=[(sortkey, sortdir)], limit=queryLimit,
offset=offset, fields=fields)

info.update({
'count': elementCursor.count(),
Expand All @@ -194,9 +222,26 @@ def yieldElements(self, annotation, region=None, info=None):
for entry in elementCursor:
element = entry['element']
element.setdefault('id', entry['_id'])
yield element
if centroids:
bbox = entry.get('bbox')
if not bbox or 'lowx' not in bbox or 'size' not in bbox:
continue
prop = tuple(element.get(key) for key in propskeys)
if prop not in props:
props[prop] = len(props)
proplist.append(list(prop))
yield [
str(element['id']),
(bbox['lowx'] + bbox['highx']) / 2,
(bbox['lowy'] + bbox['highy']) / 2,
bbox['size'] if entry.get('type') != 'point' else 0,
props[prop]
]
details += 1
else:
yield element
details += entry.get('bbox', {}).get('details', 1)
count += 1
details += entry.get('bbox', {}).get('details', 1)
if maxDetails and details >= maxDetails:
break
info['returned'] = count
Expand Down Expand Up @@ -299,6 +344,8 @@ def _boundingBox(self, element):
bbox['size'] = (
(bbox['highy'] - bbox['lowy'])**2 +
(bbox['highx'] - bbox['lowx'])**2) ** 0.5
# we may want to store perimeter or area as that could help when we
# simplify to points
return bbox

def updateElements(self, annotation):
Expand Down
33 changes: 29 additions & 4 deletions girder_annotation/girder_large_image_annotation/rest/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
##############################################################################

import json
import struct
import ujson

import cherrypy
Expand Down Expand Up @@ -138,6 +139,9 @@ def getAnnotationSchema(self, params):
'points are used to defined it. This is applied in addition '
'to the limit. Using maxDetails helps ensure results will be '
'able to be rendered.', required=False, dataType='int')
.param('centroids', 'If true, only return the centroids of each '
'element. The results are returned as a packed binary array '
'with a json wrapper.', dataType='boolean', required=False)
.pagingParams(defaultSort='_id', defaultLimit=None,
defaultSortDir=SortDir.ASCENDING)
.errorResponse('ID was invalid.')
Expand Down Expand Up @@ -174,19 +178,28 @@ def _getAnnotation(self, user, id, params):
breakStr = b'"elements": ['
base = json.dumps(annotation, sort_keys=True, allow_nan=False,
cls=JsonEncoder).encode('utf8').split(breakStr)
centroids = str(params.get('centroids')).lower() == 'true'

def generateResult():
info = {}
idx = 0
yield base[0]
yield breakStr
collect = []
if centroids:
# Add a null byte to indicate the start of the binary data
yield b'\x00'
for element in Annotationelement().yieldElements(annotation, params, info):
# The json conversion is fastest if we use defaults as much as
# possible. The only value in an annotation element that needs
# special handling is the id, so cast that ourselves and then
# use a json encoder in the most compact form.
element['id'] = str(element['id'])
if isinstance(element, dict):
element['id'] = str(element['id'])
else:
element = struct.pack(
'>QL', int(element[0][:16], 16), int(element[0][16:24], 16)
) + struct.pack('<fffl', *element[1:])
# Use ujson; it is much faster. The standard json library
# could be used in its most default mode instead like so:
# result = json.dumps(element, separators=(',', ':'))
Expand All @@ -196,18 +209,30 @@ def generateResult():
# significantly faster than 10 and not much slower than 1000.
collect.append(element)
if len(collect) >= 100:
yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1]
if isinstance(collect[0], dict):
yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1]
else:
yield b''.join(collect)
idx += 1
collect = []
if len(collect):
yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1]
if isinstance(collect[0], dict):
yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1]
else:
yield b''.join(collect)
if centroids:
# Add a final null byte to indicate the end of the binary data
yield b'\x00'
yield base[1].rstrip().rstrip(b'}')
yield b', "_elementQuery": '
yield json.dumps(
info, sort_keys=True, allow_nan=False, cls=JsonEncoder).encode('utf8')
yield b'}'

setResponseHeader('Content-Type', 'application/json')
if centroids:
setResponseHeader('Content-Type', 'application/octet-stream')
else:
setResponseHeader('Content-Type', 'application/json')
return generateResult

@describeRoute(
Expand Down
Loading

0 comments on commit 6522964

Please sign in to comment.