Skip to content

Commit 1bdcd05

Browse files
authored
Merge pull request #122 from DigitalSlideArchive/girder-ids-in-annotations
Handle annotations with girderId references.
2 parents d2ee5f8 + 8188a0c commit 1bdcd05

File tree

5 files changed

+164
-2
lines changed

5 files changed

+164
-2
lines changed

README.rst

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,29 @@ Development
4848

4949
The most convenient way to develop on HistomicsUI is to use the `devops scripts from the Digital Slide Archive <https://github.com/DigitalSlideArchive/digital_slide_archive/tree/master/devops>`_.
5050

51+
Annotations and Metadata from Jobs
52+
----------------------------------
53+
54+
This handles ingesting annotations and metadata that are uploaded and associating them with existing large image items in the Girder database. These annotations and metadata re commonly generated through jobs, such as HistomicTK tasks, but can also be added manually.
55+
56+
If a file is uploaded to the Girder system that includes a ``reference`` record, and that ``reference`` record contains an ``identifier`` field and a ``fileId`` field, specific identifiers can be used to ingest the results. If a ``userId`` is specified in the ``reference`` record, permissions for adding the annotation or metadata are associated with that user.
57+
58+
Metadata
59+
========
60+
61+
Identifiers ending in ``ItemMetadata`` are loaded and then set as metadata on the associated item that contains the specified file. Conceptually, this is the same as calling the ``PUT`` ``item/{id}/metadata`` endpoint.
62+
63+
Annotations
64+
===========
65+
66+
Identifiers ending in ``AnnotationFile`` are loaded as annotations, associated with the item that contains the specified file. Conceptually, this is the same as uploaded the file via the annotation endpoints for the item associated with the specified ``fileId``.
67+
68+
If the annotation file contains any annotations with elements that contain ``girderId`` values, the ``girderId`` values can be ``identifier`` values from files that were uploaded with a ``reference`` record that contains a matching ``uuid`` field. The ``uuid`` field is required for this, but is treated as an arbitrary string.
69+
70+
5171
Funding
5272
-------
53-
This work is funded in part by the NIH grant U24-CA194362-01_.
73+
This work was funded in part by the NIH grant U24-CA194362-01_.
5474

5575
.. _HistomicsUI: https://github.com/DigitalSlideArchive/HistomicsUI
5676
.. _Docker: https://www.docker.com/

histomicsui/handlers.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import json
33

4+
import cachetools
45
from girder import logger
56
from girder.constants import AccessType
67
from girder.exceptions import RestException
@@ -13,6 +14,8 @@
1314

1415
from .constants import PluginSettings
1516

17+
_recentIdentifiers = cachetools.TTLCache(maxsize=100, ttl=86400)
18+
1619

1720
def _itemFromEvent(event, identifierEnding, itemAccessLevel=AccessType.READ):
1821
"""
@@ -34,6 +37,13 @@ def _itemFromEvent(event, identifierEnding, itemAccessLevel=AccessType.READ):
3437
identifier = reference['identifier']
3538
except (ValueError, TypeError):
3639
logger.debug('Failed to parse data.process reference: %r', reference)
40+
if identifier and 'uuid' in reference:
41+
if reference['uuid'] not in _recentIdentifiers:
42+
_recentIdentifiers[reference['uuid']] = {}
43+
_recentIdentifiers[reference['uuid']][identifier] = info
44+
reprocessFunc = _recentIdentifiers[reference['uuid']].pop('_reprocess', None)
45+
if reprocessFunc:
46+
reprocessFunc()
3747
if identifier is not None and identifier.endswith(identifierEnding):
3848
if 'userId' not in reference or 'itemId' not in reference or 'fileId' not in reference:
3949
logger.error('Reference does not contain required information.')
@@ -46,7 +56,50 @@ def _itemFromEvent(event, identifierEnding, itemAccessLevel=AccessType.READ):
4656
user = User().load(userId, force=True)
4757
image = File().load(imageId, level=AccessType.READ, user=user)
4858
item = Item().load(image['itemId'], level=itemAccessLevel, user=user)
49-
return {'item': item, 'user': user, 'file': image}
59+
return {'item': item, 'user': user, 'file': image, 'uuid': reference.get('uuid')}
60+
61+
62+
def resolveAnnotationGirderIds(event, results, data, possibleGirderIds):
63+
"""
64+
If an annotation has references to girderIds, resolve them to actual ids.
65+
66+
:param event: a data.process event.
67+
:param results: the results from _itemFromEvent,
68+
:param data: annotation data.
69+
:param possibleGirderIds: a list of annotation elements with girderIds
70+
needing resolution.
71+
:returns: True if all ids were processed.
72+
"""
73+
# Exclude actual girderIds from resolution
74+
girderIds = []
75+
for element in possibleGirderIds:
76+
# This will throw an exception if the girderId isn't well-formed as an
77+
# actual id.
78+
try:
79+
if Item().load(element['girderId'], level=AccessType.READ, force=True) is None:
80+
girderIds.append(element)
81+
except Exception:
82+
girderIds.append(element)
83+
if not len(girderIds):
84+
return True
85+
idRecord = _recentIdentifiers.get(results.get('uuid'))
86+
if idRecord and not all(element['girderId'] in idRecord for element in girderIds):
87+
idRecord['_reprocess'] = lambda: process_annotations(event)
88+
return False
89+
for element in girderIds:
90+
element['girderId'] = str(idRecord[element['girderId']]['file']['itemId'])
91+
# Currently, all girderIds inside annotations are expected to be
92+
# large images. In this case, load them and ask if they can be so,
93+
# in case they are small images
94+
from girder_large_image.models.image_item import ImageItem
95+
96+
try:
97+
item = ImageItem().load(element['girderId'], force=True)
98+
ImageItem().createImageItem(
99+
item, list(ImageItem().childFiles(item=item, limit=1))[0], createJob=False)
100+
except Exception:
101+
pass
102+
return True
50103

51104

52105
def process_annotations(event):
@@ -73,6 +126,16 @@ def process_annotations(event):
73126

74127
if not isinstance(data, list):
75128
data = [data]
129+
# Check some of the early elements to see if there are any girderIds
130+
# that need resolution.
131+
if 'uuid' in results:
132+
girderIds = [
133+
element for annotation in data
134+
for element in annotation.get('elements', [])[:100]
135+
if 'girderId' in element]
136+
if len(girderIds):
137+
if not resolveAnnotationGirderIds(event, results, data, girderIds):
138+
return
76139
for annotation in data:
77140
try:
78141
Annotation().createAnnotation(item, user, annotation)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def prerelease_local_scheme(version):
4444
install_requires=[
4545
'girder-large-image-annotation',
4646
'girder-slicer-cli-web>=1.2.3',
47+
'cachetools',
4748
],
4849
extras_require={
4950
'analysis': [
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "Sample",
3+
"elements": [
4+
{
5+
"type": "image",
6+
"girderId": "ImageRecord1"
7+
},
8+
{
9+
"type": "point",
10+
"center": [1033,363,0],
11+
"lineColor": "rgb(0,0,0)",
12+
"lineWidth": 2,
13+
"fillColor": "rgba(0,0,0,0)"
14+
},
15+
{
16+
"type": "polyline",
17+
"points": [[1029,408,0], [1012,449,0], [1055,458,0], [1060,414,0]],
18+
"fillColor": "rgba(0,0,0,0)",
19+
"lineColor": "rgb(0,0,0)",
20+
"lineWidth": 2,
21+
"closed": true
22+
}
23+
]
24+
}

tests/test_handlers.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,57 @@ def testMetadataHandler(self, server, fsAssetstore, admin):
6565
item = Item().load(file['itemId'], user=admin)
6666
assert item['meta']['sample'] == 'value'
6767
assert item['meta']['complex']['key1'] == 'value1'
68+
69+
def testAnnotationWithGirderIdHandler(self, server, fsAssetstore, admin):
70+
file = utilities.uploadExternalFile('Easy1.png', admin, fsAssetstore)
71+
item = Item().load(file['itemId'], user=admin)
72+
utilities.uploadExternalFile(
73+
'Easy1.png', admin, fsAssetstore, reference=json.dumps({
74+
'identifier': 'ImageRecord1',
75+
'uuid': '12345',
76+
'userId': str(admin['_id']),
77+
'itemId': str(item['_id']),
78+
'fileId': str(file['_id']),
79+
}))
80+
assert Annotation().findOne({'itemId': item['_id']}) is None
81+
utilities.uploadTestFile(
82+
'sample_girder_id.anot', admin, fsAssetstore, reference=json.dumps({
83+
'identifier': 'IsAnAnnotationFile',
84+
'uuid': '12345',
85+
'userId': str(admin['_id']),
86+
'itemId': str(item['_id']),
87+
'fileId': str(file['_id']),
88+
}))
89+
starttime = time.time()
90+
while time.time() < starttime + 10:
91+
if Annotation().findOne({'itemId': item['_id']}) is not None:
92+
break
93+
time.sleep(0.1)
94+
assert Annotation().findOne({'itemId': item['_id']}) is not None
95+
96+
def testAnnotationWithGirderIdHandlerAltOrder(self, server, fsAssetstore, admin):
97+
file = utilities.uploadExternalFile('Easy1.png', admin, fsAssetstore)
98+
item = Item().load(file['itemId'], user=admin)
99+
utilities.uploadTestFile(
100+
'sample_girder_id.anot', admin, fsAssetstore, reference=json.dumps({
101+
'identifier': 'IsAnAnnotationFile',
102+
'uuid': '12346',
103+
'userId': str(admin['_id']),
104+
'itemId': str(item['_id']),
105+
'fileId': str(file['_id']),
106+
}))
107+
assert Annotation().findOne({'itemId': item['_id']}) is None
108+
utilities.uploadExternalFile(
109+
'Easy1.png', admin, fsAssetstore, reference=json.dumps({
110+
'identifier': 'ImageRecord1',
111+
'uuid': '12346',
112+
'userId': str(admin['_id']),
113+
'itemId': str(item['_id']),
114+
'fileId': str(file['_id']),
115+
}))
116+
starttime = time.time()
117+
while time.time() < starttime + 10:
118+
if Annotation().findOne({'itemId': item['_id']}) is not None:
119+
break
120+
time.sleep(0.1)
121+
assert Annotation().findOne({'itemId': item['_id']}) is not None

0 commit comments

Comments
 (0)