Skip to content

Commit

Permalink
Merge pull request #1522 from girder/ingest-geojson
Browse files Browse the repository at this point in the history
Improve ingesting geojson annotations
  • Loading branch information
manthey authored May 10, 2024
2 parents d8adabb + 6624b31 commit a81fa94
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
- Speed up multi source compositing in tiled cases ([#1513](../../pull/1513))
- Speed up some tifffile and multi source access cases ([#1515](../../pull/1515))
- Allow specifying a minimum number of annotations elements when maxDetails is used ([#1521](../../pull/1521))
- Improved import of GeoJSON annotations ([#1522](../../pull/1522))

### Changes
- Limit internal metadata on multi-source files with huge numbers of sources ([#1514](../../pull/1514))
- Make DICOMweb assetstore imports compatible with Girder generics ([#1504](../../pull/1504))

### Bug Fixes
- Fix touch actions in the image viewer in some instances ([#1516](../../pull/1516))
- Fix multisource dtype issues that resulted in float32 results ([#1520](../../pull/1520))

## 1.28.1

Expand Down
3 changes: 2 additions & 1 deletion girder_annotation/girder_large_image_annotation/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from girder.models.user import User

from .models.annotation import Annotation
from .utils import isGeoJSON

_recentIdentifiers = cachetools.TTLCache(maxsize=100, ttl=86400)

Expand Down Expand Up @@ -142,7 +143,7 @@ def process_annotations(event): # noqa: C901
if time.time() - startTime > 10:
logger.info('Decoded json in %5.3fs', time.time() - startTime)

if not isinstance(data, list):
if not isinstance(data, list) or isGeoJSON(data):
data = [data]
data = [entry['annotation'] if 'annotation' in entry else entry for entry in data]
# Check some of the early elements to see if there are any girderIds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from girder.models.setting import Setting
from girder.models.user import User

from ..utils import AnnotationGeoJSON
from ..utils import AnnotationGeoJSON, GeoJSONAnnotation, isGeoJSON
from .annotationelement import Annotationelement

# Some arrays longer than this are validated using numpy rather than jsonschema
Expand Down Expand Up @@ -788,6 +788,10 @@ def _migrateACL(self, annotation):
return annotation

def createAnnotation(self, item, creator, annotation, public=None):
if isGeoJSON(annotation):
geojson = GeoJSONAnnotation(annotation)
if geojson.elementCount:
annotation = geojson.annotation
now = datetime.datetime.now(datetime.timezone.utc)
doc = {
'itemId': item['_id'],
Expand Down
177 changes: 177 additions & 0 deletions girder_annotation/girder_large_image_annotation/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,180 @@ def elementToGeoJSON(self, element):
if result['geometry']['type'].lower() != element['type']:
result['properties']['type'] = element['type']
return result

@property
def geojson(self):
return ''.join(self)


class GeoJSONAnnotation:
def __init__(self, geojson):
if not isinstance(geojson, (dict, list, tuple)):
geojson = json.loads(geojson)
self._elements = []
self._annotation = {'elements': self._elements}
self._parseFeature(geojson)

def _parseFeature(self, geoelem):
if isinstance(geoelem, (list, tuple)):
for entry in geoelem:
self._parseFeature(entry)
if not isinstance(geoelem, dict) or 'type' not in geoelem:
return
if geoelem['type'] == 'FeatureCollection':
return self._parseFeature(geoelem.get('features', []))
if geoelem['type'] == 'GeometryCollection' and isinstance(geoelem.get('geometries'), list):
for entry in geoelem['geometry']:
self._parseFeature({'type': 'Feature', 'geometry': entry})
return
if geoelem['type'] in {'Point', 'LineString', 'Polygon', 'MultiPoint',
'MultiLineString', 'MultiPolygon'}:
geoelem = {'type': 'Feature', 'geometry': geoelem}
element = {k: v for k, v in geoelem.get('properties', {}).items() if k in {
'id', 'label', 'group', 'user', 'lineColor', 'lineWidth',
'fillColor', 'radius', 'width', 'height', 'rotation',
'normal',
}}
if 'annotation' in geoelem.get('properties', {}):
self._annotation.update(geoelem['properties']['annotation'])
self._annotation['elements'] = self._elements
elemtype = geoelem.get('properties', {}).get('type', '') or geoelem['geometry']['type']
func = getattr(self, elemtype.lower() + 'Type', None)
if func is not None:
result = func(geoelem['geometry'], element)
if isinstance(result, list):
self._elements.extend(result)
else:
self._elements.append(result)

def circleType(self, elem, result):
cx = sum(e[0] for e in elem['coordinates'][0][:4]) / 4
cy = sum(e[1] for e in elem['coordinates'][0][:4]) / 4
try:
cz = elem['coordinates'][0][0][2]
except Exception:
cz = 0
radius = (max(e[0] for e in elem['coordinates'][0][:4]) -
min(e[0] for e in elem['coordinates'][0][:4])) / 2
result['type'] = 'circle'
result['radius'] = radius
result['center'] = [cx, cy, cz]
return result

def ellipseType(self, elem, result):
result = self.rectangleType(elem, result)
result['type'] = 'ellipse'
return result

def rectangleType(self, elem, result):
coor = elem['coordinates'][0]
cx = sum(e[0] for e in coor[:4]) / 4
cy = sum(e[1] for e in coor[:4]) / 4
try:
cz = elem['coordinates'][0][0][2]
except Exception:
cz = 0
width = ((coor[0][0] - coor[1][0]) ** 2 + (coor[0][1] - coor[1][1]) ** 2) ** 0.5
height = ((coor[1][0] - coor[2][0]) ** 2 + (coor[1][1] - coor[2][1]) ** 2) ** 0.5
rotation = math.atan2(coor[1][1] - coor[0][1], coor[1][0] - coor[0][0])
result['center'] = [cx, cy, cz]
result['width'] = width
result['height'] = height
result['rotation'] = rotation
result['type'] = 'rectangle'
return result

def pointType(self, elem, result):
result['center'] = (elem['coordinates'] + [0, 0, 0])[:3]
result['type'] = 'point'
return result

def multipointType(self, elem, result):
results = []
result['type'] = 'point'
for entry in elem['coordinates']:
subresult = result.copy()
subresult['center'] = (entry + [0, 0, 0])[:3]
results.append(subresult)
return results

def polylineType(self, elem, result):
if elem.get('type') == 'LineString':
return self.linestringType(elem, result)
return self.polygonType(elem, result)

def polygonType(self, elem, result):
result['points'] = [(pt + [0])[:3] for pt in elem['coordinates'][0][:-1]]
if len(elem['coordinates']) > 1:
result['holes'] = [
[(pt + [0])[:3] for pt in loop[:-1]]
for loop in elem['coordinates'][1:]
]
result['closed'] = True
result['type'] = 'polyline'
return result

def multipolygonType(self, elem, result):
results = []
result['closed'] = True
result['type'] = 'polyline'
for entry in elem['coordinates']:
subresult = result.copy()
subresult['points'] = [(pt + [0])[:3] for pt in entry[0][:-1]]
if len(entry) > 1:
subresult['holes'] = [
[(pt + [0])[:3] for pt in loop[:-1]]
for loop in entry[1:]
]
results.append(subresult)
return results

def linestringType(self, elem, result):
result['points'] = [(pt + [0])[:3] for pt in elem['coordinates']]
result['closed'] = False
result['type'] = 'polyline'
return result

def multilinestringType(self, elem, result):
results = []
result['closed'] = False
result['type'] = 'polyline'
for entry in elem['coordinates']:
subresult = result.copy()
subresult['points'] = [(pt + [0])[:3] for pt in entry]
results.append(subresult)
return results

def annotationToJSON(self):
return json.dumps(self._annotation)

@property
def annotation(self):
return self._annotation

@property
def elements(self):
return self._elements

@property
def elementCount(self):
return len(self._elements)


def isGeoJSON(annotation):
"""
Check if a list or dictionary appears to contain a GeoJSON record.
:param annotation: a list or dictionary.
:returns: True if this appears to be GeoJSON
"""
if isinstance(annotation, list):
if len(annotation) < 1:
return False
annotation = annotation[0]
if not isinstance(annotation, dict) or 'type' not in annotation:
return False
return annotation['type'] in {
'Feature', 'FeatureCollection', 'GeometryCollection', 'Point',
'LineString', 'Polygon', 'MultiPoint', 'MultiLineString',
'MultiPolygon'}
23 changes: 23 additions & 0 deletions girder_annotation/test_annotation/test_annotations_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,15 @@ def testLoadAnnotationGeoJSON(self, server, admin):
assert resp.json['type'] == 'FeatureCollection'
assert len(resp.json['features']) == 3

def testGeoJSONRoundTrip(self, admin):
import girder_large_image_annotation

self.makeAnnot(admin)
geojson = girder_large_image_annotation.utils.AnnotationGeoJSON(
self.hasGroups['_id']).geojson
annot = girder_large_image_annotation.utils.GeoJSONAnnotation(geojson)
assert annot.elementCount == 3

def testLoadAnnotationGeoJSONVariety(self, server, admin):
self.makeAnnot(admin)
annot = Annotation().createAnnotation(
Expand Down Expand Up @@ -828,6 +837,20 @@ def testLoadAnnotationGeoJSONVariety(self, server, admin):
assert resp.json['type'] == 'FeatureCollection'
assert len(resp.json['features']) == 6

import girder_large_image_annotation
annot = girder_large_image_annotation.utils.GeoJSONAnnotation(resp.json)
assert annot.elementCount == 6

resp = server.request(
path='/annotation/item/{}'.format(self.item['_id']),
method='POST',
user=admin,
type='application/json',
body=json.dumps(resp.json),
)
assert utilities.respStatus(resp) == 200
assert resp.json == 1


@pytest.mark.usefixtures('unbindLargeImage', 'unbindAnnotation')
@pytest.mark.plugin('large_image_annotation')
Expand Down

0 comments on commit a81fa94

Please sign in to comment.