Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve ingesting geojson annotations #1522

Merged
merged 1 commit into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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