mmif-python 1.0.10__tar.gz → 1.0.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mmif-python-1.0.10 → mmif-python-1.0.12}/MANIFEST.in +1 -0
- {mmif-python-1.0.10/mmif_python.egg-info → mmif-python-1.0.12}/PKG-INFO +17 -1
- mmif-python-1.0.12/README.md +18 -0
- mmif-python-1.0.12/VERSION +1 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/res/clams.vocabulary.yaml +5 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/res/mmif.json +3 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/serialize/annotation.py +9 -9
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/serialize/mmif.py +29 -12
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/serialize/model.py +2 -2
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/serialize/view.py +40 -7
- mmif-python-1.0.12/mmif/utils/sequence_helper.py +292 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/utils/timeunit_helper.py +2 -2
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/utils/video_document_helper.py +12 -8
- mmif-python-1.0.12/mmif/ver/__init__.py +2 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/vocabulary/__init__.py +1 -1
- mmif-python-1.0.12/mmif/vocabulary/annotation_types.py +43 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/vocabulary/base_types.py +2 -2
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/vocabulary/document_types.py +3 -3
- {mmif-python-1.0.10 → mmif-python-1.0.12/mmif_python.egg-info}/PKG-INFO +17 -1
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif_python.egg-info/SOURCES.txt +2 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif_python.egg-info/requires.txt +3 -0
- mmif-python-1.0.12/requirements.seq +1 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/setup.py +6 -2
- mmif-python-1.0.10/README.md +0 -3
- mmif-python-1.0.10/VERSION +0 -1
- mmif-python-1.0.10/mmif/ver/__init__.py +0 -2
- mmif-python-1.0.10/mmif/vocabulary/annotation_types.py +0 -33
- {mmif-python-1.0.10 → mmif-python-1.0.12}/LICENSE +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/__init__.py +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/res/__init__.py +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/res/do-not-edit.txt +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/serialize/__init__.py +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/utils/__init__.py +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/ver/do-not-edit.txt +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif/vocabulary/do-not-edit.txt +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif_docloc_http/__init__.py +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/requirements.cv +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/requirements.txt +0 -0
- {mmif-python-1.0.10 → mmif-python-1.0.12}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mmif-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.12
|
|
4
4
|
Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
|
|
5
5
|
Home-page: https://mmif.clams.ai
|
|
6
6
|
Author: Brandeis Lab for Linguistics and Computation
|
|
@@ -10,6 +10,21 @@ Description: ## MultiMedia Interchange Format
|
|
|
10
10
|
[MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
## mmif-python
|
|
14
|
+
`mmif-python` is a Python implementation of the MMIF data format.
|
|
15
|
+
`mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
|
|
16
|
+
including ;
|
|
17
|
+
|
|
18
|
+
1. de-/serialization of MMIF internal data structures to/from JSON
|
|
19
|
+
2. validation of MMIF JSON
|
|
20
|
+
3. handling of CLAMS vocabulary types
|
|
21
|
+
4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
|
|
22
|
+
|
|
23
|
+
## For more ...
|
|
24
|
+
* [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
|
|
25
|
+
* [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
|
|
26
|
+
* [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
|
|
27
|
+
|
|
13
28
|
Platform: UNKNOWN
|
|
14
29
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
15
30
|
Classifier: Intended Audience :: Developers
|
|
@@ -17,5 +32,6 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
17
32
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
33
|
Requires-Python: >=3.8
|
|
19
34
|
Description-Content-Type: text/markdown
|
|
35
|
+
Provides-Extra: seq
|
|
20
36
|
Provides-Extra: cv
|
|
21
37
|
Provides-Extra: dev
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## MultiMedia Interchange Format
|
|
2
|
+
[MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## mmif-python
|
|
6
|
+
`mmif-python` is a Python implementation of the MMIF data format.
|
|
7
|
+
`mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
|
|
8
|
+
including ;
|
|
9
|
+
|
|
10
|
+
1. de-/serialization of MMIF internal data structures to/from JSON
|
|
11
|
+
2. validation of MMIF JSON
|
|
12
|
+
3. handling of CLAMS vocabulary types
|
|
13
|
+
4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
|
|
14
|
+
|
|
15
|
+
## For more ...
|
|
16
|
+
* [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
|
|
17
|
+
* [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
|
|
18
|
+
* [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.12
|
|
@@ -78,6 +78,11 @@ properties:
|
|
|
78
78
|
<br><br>
|
|
79
79
|
[Note] Annotations from a classifier app must have this property.
|
|
80
80
|
classifications:
|
|
81
|
+
type: Map from String to Number
|
|
82
|
+
description: >-
|
|
83
|
+
Alias for the <code>classification</code> metadata. Here for historical
|
|
84
|
+
reasons.
|
|
85
|
+
classification:
|
|
81
86
|
type: Map from String to Number
|
|
82
87
|
description: >-
|
|
83
88
|
A map from label values to their "score" numbers provided by a classifier.
|
|
@@ -18,16 +18,16 @@ from typing import Union, Dict, List, Optional, Iterator, MutableMapping, TypeVa
|
|
|
18
18
|
from urllib.parse import urlparse
|
|
19
19
|
|
|
20
20
|
from mmif.vocabulary import ThingTypesBase, DocumentTypesBase
|
|
21
|
-
from .model import MmifObject,
|
|
21
|
+
from .model import MmifObject, PRMTV_TYPES
|
|
22
22
|
from .. import DocumentTypes, AnnotationTypes
|
|
23
23
|
import mmif_docloc_http
|
|
24
24
|
|
|
25
25
|
__all__ = ['Annotation', 'AnnotationProperties', 'Document', 'DocumentProperties', 'Text']
|
|
26
26
|
|
|
27
27
|
T = TypeVar('T')
|
|
28
|
-
LIST_PRMTV = typing.List[
|
|
28
|
+
LIST_PRMTV = typing.List[PRMTV_TYPES] # list of values (most cases for annotation props)
|
|
29
29
|
LIST_LIST_PRMTV = typing.List[LIST_PRMTV] # list of list of values (e.g. for coordinates)
|
|
30
|
-
DICT_PRMTV = typing.Dict[str,
|
|
30
|
+
DICT_PRMTV = typing.Dict[str, PRMTV_TYPES] # dict of values (`text` prop of `TextDocument` and other complex props)
|
|
31
31
|
DICT_LIST_PRMTV = typing.Dict[str, LIST_PRMTV] # dict of list of values (even more complex props)
|
|
32
32
|
|
|
33
33
|
|
|
@@ -77,7 +77,7 @@ class Annotation(MmifObject):
|
|
|
77
77
|
subtypes, and effectively deprecated `frameType` and `boxType`
|
|
78
78
|
in `TimeFrame` and `BoundingBox` respectively.
|
|
79
79
|
"""
|
|
80
|
-
prop_aliases = AnnotationTypes.
|
|
80
|
+
prop_aliases = AnnotationTypes._prop_aliases.get(self._type.shortname, {})
|
|
81
81
|
for alias_reprep, alias_group in prop_aliases.items():
|
|
82
82
|
if key_to_add in alias_group:
|
|
83
83
|
for alias in alias_group:
|
|
@@ -129,10 +129,10 @@ class Annotation(MmifObject):
|
|
|
129
129
|
|
|
130
130
|
@staticmethod
|
|
131
131
|
def check_prop_value_is_simple_enough(
|
|
132
|
-
value: Union[
|
|
132
|
+
value: Union[PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> bool:
|
|
133
133
|
|
|
134
134
|
def json_primitives(x):
|
|
135
|
-
return isinstance(x, typing.get_args(
|
|
135
|
+
return isinstance(x, typing.get_args(PRMTV_TYPES))
|
|
136
136
|
|
|
137
137
|
def json_primitives_list(x):
|
|
138
138
|
return isinstance(x, list) and all(map(json_primitives, x))
|
|
@@ -146,7 +146,7 @@ class Annotation(MmifObject):
|
|
|
146
146
|
or (isinstance(value, dict) and all(map(lambda x: isinstance(x[0], str) and (json_primitives(x[1]) or json_primitives_list(x[1])), value.items())))
|
|
147
147
|
|
|
148
148
|
def add_property(self, name: str,
|
|
149
|
-
value: Union[
|
|
149
|
+
value: Union[PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> None:
|
|
150
150
|
"""
|
|
151
151
|
Adds a property to the annotation's properties.
|
|
152
152
|
:param name: the name of the property
|
|
@@ -162,7 +162,7 @@ class Annotation(MmifObject):
|
|
|
162
162
|
# f"(\"{name}\": \"{str(value)}\"")
|
|
163
163
|
self._add_prop_aliases(name, value)
|
|
164
164
|
|
|
165
|
-
def get(self, prop_name: str) -> Union['AnnotationProperties',
|
|
165
|
+
def get(self, prop_name: str) -> Union['AnnotationProperties', PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]:
|
|
166
166
|
"""
|
|
167
167
|
A special getter for Annotation properties. This is to allow for
|
|
168
168
|
directly accessing properties without having to go through the
|
|
@@ -242,7 +242,7 @@ class Document(Annotation):
|
|
|
242
242
|
super().__init__(doc_obj)
|
|
243
243
|
|
|
244
244
|
def add_property(self, name: str,
|
|
245
|
-
value: Union[
|
|
245
|
+
value: Union[PRMTV_TYPES, LIST_PRMTV]
|
|
246
246
|
) -> None:
|
|
247
247
|
"""
|
|
248
248
|
Adds a property to the document's properties.
|
|
@@ -6,6 +6,7 @@ See the specification docs and the JSON Schema file for more information.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import math
|
|
9
10
|
import warnings
|
|
10
11
|
from collections import defaultdict
|
|
11
12
|
from datetime import datetime
|
|
@@ -123,12 +124,20 @@ class Mmif(MmifObject):
|
|
|
123
124
|
# then, do the same to associated Document objects. Note that,
|
|
124
125
|
# in a view, it is guaranteed that all Annotation objects are not duplicates
|
|
125
126
|
if ann.at_type == AnnotationTypes.Annotation:
|
|
127
|
+
doc_id = ann.get_property('document')
|
|
126
128
|
try:
|
|
127
|
-
doc_id = ann.get_property('document')
|
|
128
129
|
for prop_key, prop_value in ann.properties.items():
|
|
129
130
|
self.get_document_by_id(doc_id)._props_ephemeral[prop_key] = prop_value
|
|
130
131
|
except KeyError:
|
|
131
|
-
warnings.warn(f"Annotation {ann.id} has a document ID {doc_id} that
|
|
132
|
+
warnings.warn(f"Annotation {ann.id} (in view {view.id}) has a document ID {doc_id} that "
|
|
133
|
+
f"does not exist in the MMIF object. Skipping.", RuntimeWarning)
|
|
134
|
+
# lastly, add quick access to `start` and `end` values if the annotation is using `targets` property
|
|
135
|
+
if 'targets' in ann.properties:
|
|
136
|
+
if 'start' in ann.properties or 'end' in ann.properties:
|
|
137
|
+
raise ValueError(f"Annotation {ann.id} (in view {view.id}) has `targes` and `start`/`end/` "
|
|
138
|
+
f"properties at the same time. Annotation anchors are ambiguous.")
|
|
139
|
+
ann._props_ephemeral['start'] = self._get_linear_anchor_point(ann, start=True)
|
|
140
|
+
ann._props_ephemeral['end'] = self._get_linear_anchor_point(ann, start=False)
|
|
132
141
|
|
|
133
142
|
def generate_capital_annotations(self):
|
|
134
143
|
"""
|
|
@@ -427,7 +436,8 @@ class Mmif(MmifObject):
|
|
|
427
436
|
next(annotations)
|
|
428
437
|
views.append(view)
|
|
429
438
|
except StopIteration:
|
|
430
|
-
# search failed by the full doc_id string,
|
|
439
|
+
# means search failed by the full doc_id string,
|
|
440
|
+
# now try trimming the view_id from the string and re-do the search
|
|
431
441
|
if Mmif.id_delimiter in doc_id:
|
|
432
442
|
vid, did = doc_id.split(Mmif.id_delimiter)
|
|
433
443
|
if view.id == vid:
|
|
@@ -479,7 +489,7 @@ class Mmif(MmifObject):
|
|
|
479
489
|
return view
|
|
480
490
|
return None
|
|
481
491
|
|
|
482
|
-
def _get_linear_anchor_point(self, ann: Annotation, start: bool = True) -> Union[int, float]:
|
|
492
|
+
def _get_linear_anchor_point(self, ann: Annotation, targets_sorted=False, start: bool = True) -> Union[int, float]:
|
|
483
493
|
# TODO (krim @ 2/5/24): Update the return type once timeunits are unified to `ms` as integers (https://github.com/clamsproject/mmif/issues/192)
|
|
484
494
|
"""
|
|
485
495
|
Retrieves the anchor point of the annotation. Currently, this method only supports linear anchors,
|
|
@@ -487,21 +497,28 @@ class Mmif(MmifObject):
|
|
|
487
497
|
|
|
488
498
|
:param ann: An Annotation object that has a linear anchor point. Namely, some subtypes of `Region` vocabulary type.
|
|
489
499
|
:param start: If True, returns the start anchor point. Otherwise, returns the end anchor point. N/A for `timePoint` anchors.
|
|
500
|
+
:param targets_sorted: If True, the method will assume that the targets are sorted in the order of the anchor points.
|
|
490
501
|
:return: the anchor point of the annotation. 1d for linear regions (time, text)
|
|
491
502
|
"""
|
|
492
503
|
props = ann.properties
|
|
493
504
|
if 'timePoint' in props:
|
|
494
505
|
return ann.get_property('timePoint')
|
|
495
506
|
elif 'targets' in props:
|
|
507
|
+
|
|
508
|
+
def get_target_ann(cur_ann, target_id):
|
|
509
|
+
if Mmif.id_delimiter not in target_id:
|
|
510
|
+
target_id = Mmif.id_delimiter.join((cur_ann.parent, target_id))
|
|
511
|
+
return self.__getitem__(target_id)
|
|
512
|
+
|
|
513
|
+
if not targets_sorted:
|
|
514
|
+
point = math.inf if start else -1
|
|
515
|
+
comp = min if start else max
|
|
516
|
+
for target_id in ann.get_property('targets'):
|
|
517
|
+
target = get_target_ann(ann, target_id)
|
|
518
|
+
point = comp(point, self._get_linear_anchor_point(target, start=start))
|
|
519
|
+
return point
|
|
496
520
|
target_id = ann.get_property('targets')[0 if start else -1]
|
|
497
|
-
|
|
498
|
-
# since the targets list is not guaranteed to be sorted.
|
|
499
|
-
# However, due the recursive nature of this method, it is likely impossible
|
|
500
|
-
# to get all `start` (or `end`) values of the targets recursively and then pick the min (or max) value.
|
|
501
|
-
if Mmif.id_delimiter in target_id:
|
|
502
|
-
target = self.__getitem__(target_id)
|
|
503
|
-
else:
|
|
504
|
-
target = self.__getitem__(Mmif.id_delimiter.join((ann.parent, target_id)))
|
|
521
|
+
target = get_target_ann(ann, target_id)
|
|
505
522
|
return self._get_linear_anchor_point(target, start=start)
|
|
506
523
|
elif (start and 'start' in props) or (not start and 'end' in props):
|
|
507
524
|
return ann.get_property('start' if start else 'end')
|
|
@@ -20,14 +20,14 @@ from deepdiff import DeepDiff
|
|
|
20
20
|
|
|
21
21
|
T = TypeVar('T')
|
|
22
22
|
S = TypeVar('S')
|
|
23
|
-
|
|
23
|
+
PRMTV_TYPES: Type = Union[str, int, float, bool, None]
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
26
|
'MmifObject',
|
|
27
27
|
'MmifObjectEncoder',
|
|
28
28
|
'DataList',
|
|
29
29
|
'DataDict',
|
|
30
|
-
'
|
|
30
|
+
'PRMTV_TYPES'
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
|
|
@@ -8,6 +8,7 @@ data that was previously present in the MMIF file.
|
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from typing import Dict, Union, Optional, Generator, List, cast
|
|
10
10
|
|
|
11
|
+
from mmif.serialize.model import PRMTV_TYPES
|
|
11
12
|
from mmif.vocabulary import ThingTypesBase, ClamsTypesBase
|
|
12
13
|
from .annotation import Annotation, Document
|
|
13
14
|
from .model import MmifObject, DataList, DataDict
|
|
@@ -243,7 +244,8 @@ class ViewMetadata(MmifObject):
|
|
|
243
244
|
self.timestamp: Optional[datetime] = None
|
|
244
245
|
self.app: str = ''
|
|
245
246
|
self.contains: ContainsDict = ContainsDict()
|
|
246
|
-
self.parameters:
|
|
247
|
+
self.parameters: Dict[str, str] = {}
|
|
248
|
+
self.appConfiguration: Dict[str, Union[PRMTV_TYPES, List[PRMTV_TYPES]]] = {}
|
|
247
249
|
self.error: Union[dict, ErrorDict] = {}
|
|
248
250
|
self.warnings: List[str] = []
|
|
249
251
|
self._required_attributes = ["app"]
|
|
@@ -282,16 +284,43 @@ class ViewMetadata(MmifObject):
|
|
|
282
284
|
self.add_contain(new_contain, at_type)
|
|
283
285
|
return new_contain
|
|
284
286
|
|
|
285
|
-
def add_contain(self, contain: 'Contain', at_type: Union[str, ThingTypesBase]):
|
|
287
|
+
def add_contain(self, contain: 'Contain', at_type: Union[str, ThingTypesBase]) -> None:
|
|
286
288
|
self.contains[at_type] = contain
|
|
287
289
|
|
|
288
|
-
def
|
|
289
|
-
|
|
290
|
+
def add_app_configuration(self, config_key: str, config_value: Union[PRMTV_TYPES, List[PRMTV_TYPES]]) -> None:
|
|
291
|
+
"""
|
|
292
|
+
Add a configuration key-value pair to the app_configuration dictionary.
|
|
293
|
+
"""
|
|
294
|
+
self.appConfiguration[config_key] = config_value
|
|
295
|
+
|
|
296
|
+
def get_app_configuration(self, config_key: str) -> Union[PRMTV_TYPES, List[PRMTV_TYPES]]:
|
|
297
|
+
"""
|
|
298
|
+
Get a configuration value from the app_configuration dictionary.
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
return self.appConfiguration[config_key]
|
|
302
|
+
except KeyError:
|
|
303
|
+
raise KeyError(f"app is not configured for \"{config_key}\" key in the view: {self.serialize()}")
|
|
290
304
|
|
|
291
|
-
def
|
|
305
|
+
def add_parameters(self, **runtime_params: str):
|
|
306
|
+
"""
|
|
307
|
+
Add runtime parameters as a batch (dict) to the view metadata. Note that parameter values must be strings.
|
|
308
|
+
"""
|
|
309
|
+
for k, v in runtime_params.items():
|
|
310
|
+
self.add_parameter(k, v)
|
|
311
|
+
|
|
312
|
+
def add_parameter(self, param_key: str, param_value: str):
|
|
313
|
+
"""
|
|
314
|
+
Add a single runtime parameter to the view metadata. Note that parameter value must be a string.
|
|
315
|
+
"""
|
|
316
|
+
assert isinstance(param_value, str), \
|
|
317
|
+
f"Parameter value must be a string, \"{param_value}\" ({type(param_value)}) is given for key \"{param_key}\"."
|
|
292
318
|
self.parameters[param_key] = param_value
|
|
293
319
|
|
|
294
|
-
def get_parameter(self, param_key):
|
|
320
|
+
def get_parameter(self, param_key: str) -> str:
|
|
321
|
+
"""
|
|
322
|
+
Get a runtime parameter from the view metadata.
|
|
323
|
+
"""
|
|
295
324
|
try:
|
|
296
325
|
return self.parameters[param_key]
|
|
297
326
|
except KeyError:
|
|
@@ -384,7 +413,11 @@ class ContainsDict(DataDict[ThingTypesBase, Contain]):
|
|
|
384
413
|
return self._items.get(key, default)
|
|
385
414
|
|
|
386
415
|
def __contains__(self, item: Union[str, ThingTypesBase]):
|
|
387
|
-
|
|
416
|
+
if isinstance(item, str):
|
|
417
|
+
string_keys = [str(k) for k in self._items.keys()]
|
|
418
|
+
return item in string_keys
|
|
419
|
+
else:
|
|
420
|
+
return item in self._items
|
|
388
421
|
|
|
389
422
|
def pop(self, key):
|
|
390
423
|
self._items.pop(key)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides helpers for handling sequence labeling. Specifically, it provides
|
|
3
|
+
|
|
4
|
+
* a generalized label re-mapper for "post-binning" of labels
|
|
5
|
+
* conversion from a list of CLAMS annotations (with ``classification`` props) into a list of reals (scores by labels), can be combined with the label re-mapper mentioned above
|
|
6
|
+
* :py:meth:`mmif.utils.sequence_helper.smooth_outlying_short_intervals`: a simple smoothing algorithm by trimming "short" outlier sequences
|
|
7
|
+
|
|
8
|
+
However, it DOES NOT provide
|
|
9
|
+
|
|
10
|
+
* direct conversion between CLAMS annotations. For example, it does not directly handle stitching of ``TimePoint`` into ``TimeFrames``.
|
|
11
|
+
* support for multi-class scenario, such as handling of _competing_ subsequence or overlapping labels.
|
|
12
|
+
|
|
13
|
+
Some functions can use optional external libraries (e.g., ``numpy``) for better performance.
|
|
14
|
+
Hence, if you see a warning about missing optional packages, you might want to install them by running ``pip install mmif-python[seq]``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import importlib
|
|
18
|
+
import itertools
|
|
19
|
+
import warnings
|
|
20
|
+
from typing import List, Tuple, Dict, Union, Iterable, Callable
|
|
21
|
+
|
|
22
|
+
import mmif
|
|
23
|
+
from mmif import Annotation
|
|
24
|
+
from mmif.serialize.model import PRMTV_TYPES
|
|
25
|
+
|
|
26
|
+
for seq_dep in ['numpy']:
|
|
27
|
+
try:
|
|
28
|
+
importlib.__import__(seq_dep)
|
|
29
|
+
except ImportError as e:
|
|
30
|
+
warnings.warn(f"Optional package \"{e.name}\" is not found. "
|
|
31
|
+
f"You might want to install Sequence Helper dependencies "
|
|
32
|
+
f"by running `pip install mmif-python[seq]=={mmif.__version__}`")
|
|
33
|
+
|
|
34
|
+
NEG_LABEL = '-'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def smooth_outlying_short_intervals(scores: List[float],
|
|
38
|
+
min_spseq_size: int,
|
|
39
|
+
min_snseq_size: int,
|
|
40
|
+
min_score: float = 0.5,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Given a list of scores, a score threshold, and smoothing parameters,
|
|
44
|
+
identify the intervals of "positive" scores by "trimming" the short
|
|
45
|
+
positive sequences ("spseq") and short negative sequences ("snseq"). To
|
|
46
|
+
decide the positivity, first step is binarization of the scores by the
|
|
47
|
+
``min_score`` threshold. Given ``Sr`` as "raw" input real-number scores
|
|
48
|
+
list, and ``min_score=0.5``,
|
|
49
|
+
|
|
50
|
+
.. code-block:: javascript
|
|
51
|
+
|
|
52
|
+
Sr: [0.3, 0.6, 0.2, 0.8, 0.2, 0.9, 0.8, 0.5, 0.1, 0.5, 0.8, 0.3, 1.0, 0.7, 0.5, 0.5, 0.5, 0.8, 0.3, 0.6]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
the binarization is done by simply comparing each score to the
|
|
57
|
+
threshold to get ``S`` list of binary scores
|
|
58
|
+
|
|
59
|
+
.. code-block:: javascript
|
|
60
|
+
|
|
61
|
+
1.0 : |
|
|
62
|
+
0.9 : | |
|
|
63
|
+
0.8 : | | | | | |
|
|
64
|
+
0.7 : | | | | | | |
|
|
65
|
+
0.6 : | | | | | | | | |
|
|
66
|
+
0.5 :----+-----+-----+--+--+-----+--+-----+--+--+--+--+--+-----+-
|
|
67
|
+
0.4 : | | | | | | | | | | | | | |
|
|
68
|
+
0.3 : | | | | | | | | | | | | | | | | |
|
|
69
|
+
0.2 : | | | | | | | | | | | | | | | | | | |
|
|
70
|
+
0.1 : | | | | | | | | | | | | | | | | | | | |
|
|
71
|
+
0.0 +------------------------------------------------------------
|
|
72
|
+
raw :.3 .6 .2 .8 .2 .9 .8 .5 .1 .5 .8 .3 1. .7 .5 .5 .5 .8 .3 .6
|
|
73
|
+
S : 0 1 0 1 0 1 1 0 0 0 1 0 1 1 0 1 1 1 0 1
|
|
74
|
+
|
|
75
|
+
Note that the size of a positive or negative sequence can be as small
|
|
76
|
+
as 1.
|
|
77
|
+
|
|
78
|
+
Then, here are examples of smoothing a list of binary scores into
|
|
79
|
+
intervals, by trimming "very short" (under thresholds) sequences of
|
|
80
|
+
positive or negative:
|
|
81
|
+
|
|
82
|
+
.. note::
|
|
83
|
+
legends:
|
|
84
|
+
|
|
85
|
+
* ``t`` is unit index (e.g. time index)
|
|
86
|
+
* ``S`` is the list of binary scores (zeros and ones)
|
|
87
|
+
* ``I`` is the list of intervals after smoothing
|
|
88
|
+
|
|
89
|
+
#. with params ``min_spseq_size==1``, ``min_snseq_size==4``
|
|
90
|
+
|
|
91
|
+
.. code-block:: javascript
|
|
92
|
+
|
|
93
|
+
t: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
94
|
+
S: [0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1]
|
|
95
|
+
I: [0, 1--1--1--1--1--1--1--1--1--1--1--1, 0--0--0--0--0--0, 1]
|
|
96
|
+
|
|
97
|
+
Explanation: ``min_snseq_size`` is used to smooth short sequences
|
|
98
|
+
of negative predictions. In this, zeros from t[7:10] are smoothed
|
|
99
|
+
into "one" I, while zeros from t[13:19] are kept as "zero" I. Note
|
|
100
|
+
that the "short" snseqs at the either ends (t[0:1]) are never
|
|
101
|
+
smoothed.
|
|
102
|
+
|
|
103
|
+
#. with params ``min_spseq_size==4``, ``min_snseq_size==2``
|
|
104
|
+
|
|
105
|
+
.. code-block:: javascript
|
|
106
|
+
|
|
107
|
+
t: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
108
|
+
S: [0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1]
|
|
109
|
+
I: [0, 1--1--1--1--1--1, 0--0--0--0--0--0--0--0--0--0--0--0--0]
|
|
110
|
+
|
|
111
|
+
Explanation: ``min_spseq_size`` is used to smooth short sequences
|
|
112
|
+
of positive predictions. In this example, the spseqs of ones from
|
|
113
|
+
both t[10:13] and t[19:20] are smoothed. Note that the "short"
|
|
114
|
+
spseqs at the either ends (t[19:20]) are always smoothed.
|
|
115
|
+
|
|
116
|
+
#. with params ``min_spseq_size==4``, ``min_snseq_size==4``
|
|
117
|
+
|
|
118
|
+
.. code-block:: javascript
|
|
119
|
+
|
|
120
|
+
t: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
121
|
+
S: [0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1]
|
|
122
|
+
I: [0, 1--1--1--1--1--1--1--1--1--1--1--1--0--0--0--0--0--0--0]
|
|
123
|
+
|
|
124
|
+
Explanation: When two threshold parameters are working together,
|
|
125
|
+
the algorithm will prioritize the smoothing of the snseqs over the
|
|
126
|
+
smoothing of the spseqs. Thus, in this example, the snseq t[7:10]
|
|
127
|
+
gets first smoothed "up" before the spseq t[10:13] is smoothed
|
|
128
|
+
"down", resulting in a long final I.
|
|
129
|
+
|
|
130
|
+
#. with params ``min_spseq_size==4``, ``min_snseq_size==4``
|
|
131
|
+
|
|
132
|
+
.. code-block:: javascript
|
|
133
|
+
|
|
134
|
+
t: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
135
|
+
S: [1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1]
|
|
136
|
+
I: [1--1--1--1--1--1--1, 0--0--0--0, 1--1--1--1--1--1--1--1--1]
|
|
137
|
+
|
|
138
|
+
Explanation: Since smoothing of snseqs is prioritized, short spseqs
|
|
139
|
+
at the beginning or the end can be kept.
|
|
140
|
+
|
|
141
|
+
#. with params ``min_spseq_size==1``, ``min_snseq_size==1``
|
|
142
|
+
|
|
143
|
+
.. code-block:: javascript
|
|
144
|
+
|
|
145
|
+
t: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
146
|
+
S: [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1]
|
|
147
|
+
I: [0--0--0, 1--1--1--1, 0--0--0--0, 1--1--1--1, 0--0--0, 1--1]
|
|
148
|
+
|
|
149
|
+
Explanation: When both width thresholds are set to 1, the algorithm
|
|
150
|
+
works essentially in the "stitching" only mode.
|
|
151
|
+
|
|
152
|
+
:param scores: **SORTED** list of scores to be smoothed. The score list
|
|
153
|
+
is assumed to be "exhaust" the entire time or space of
|
|
154
|
+
the underlying document segment.
|
|
155
|
+
(Sorted by the start, and then by the end of anchors)
|
|
156
|
+
:param min_score: minimum threshold to use to discard
|
|
157
|
+
low-scored units (strictly less than)
|
|
158
|
+
:param min_spseq_size: minimum size of a positive sequence not to be
|
|
159
|
+
smoothed (greater or equal to)
|
|
160
|
+
:param min_snseq_size: minimum size of a negative sequence not to be
|
|
161
|
+
smoothed (greater or equal to)
|
|
162
|
+
:return: list of tuples of start(inclusive)/end(exclusive) indices
|
|
163
|
+
of the "positive" sequences. Negative sequences (regardless of
|
|
164
|
+
their size) are not included in the output.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def trimmer(elems, min_width, target=False, keep_short_ends=False):
|
|
168
|
+
"""
|
|
169
|
+
This will loop through a list of bools and convert short ``target``
|
|
170
|
+
subsequences to ¬target (or not-target). A "short" subsequence is
|
|
171
|
+
defined by its size being strictly smaller than ``min_width``.
|
|
172
|
+
When ``always_trim_ends``, the first and the last subsequences are
|
|
173
|
+
always converted to ¬target, regardless of their size.
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
assert min_width > 0
|
|
177
|
+
except AssertionError:
|
|
178
|
+
raise ValueError(f"minimum width threshold must be a positive number, but got {min_width}")
|
|
179
|
+
group_gen = itertools.groupby(elems)
|
|
180
|
+
new_membership = None
|
|
181
|
+
last_membership = None
|
|
182
|
+
last_membersnum = 0
|
|
183
|
+
added = 0
|
|
184
|
+
for i, (positivity, group) in enumerate(group_gen):
|
|
185
|
+
membersnum = len(list(group))
|
|
186
|
+
for _ in range(last_membersnum): # won't be executed for the first group (last_membersnum=0)
|
|
187
|
+
yield new_membership
|
|
188
|
+
added += 1
|
|
189
|
+
new_membership = not target
|
|
190
|
+
# keep target as target if
|
|
191
|
+
# 1. the sequence is wide enough
|
|
192
|
+
# 2. the sequence is at the beginning of the list and "keep_ends"
|
|
193
|
+
if positivity == target and (membersnum >= min_width or (keep_short_ends and i == 0)):
|
|
194
|
+
new_membership = target
|
|
195
|
+
last_membersnum = membersnum
|
|
196
|
+
last_membership = positivity
|
|
197
|
+
# and finally 3. the sequence is at the final of the list and "keep_ends"
|
|
198
|
+
new_membership = not target
|
|
199
|
+
if last_membership == target and (last_membersnum >= min_width or keep_short_ends):
|
|
200
|
+
new_membership = target
|
|
201
|
+
for _ in range(last_membersnum):
|
|
202
|
+
yield new_membership
|
|
203
|
+
added += 1
|
|
204
|
+
|
|
205
|
+
# first pass to smooth short gaps first
|
|
206
|
+
pass1 = trimmer(
|
|
207
|
+
map(lambda x: x >= min_score, scores),
|
|
208
|
+
min_snseq_size, target=False, keep_short_ends=True)
|
|
209
|
+
pass2 = trimmer(
|
|
210
|
+
pass1, min_spseq_size, target=True, keep_short_ends=False)
|
|
211
|
+
return _sequence_to_intervals(pass2)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _sequence_to_intervals(seq: Iterable[bool]) -> List[Tuple[int, int]]:
|
|
215
|
+
pos_ints = []
|
|
216
|
+
cur = 0
|
|
217
|
+
for positivity, members in itertools.groupby(seq):
|
|
218
|
+
l = len(list(members))
|
|
219
|
+
if positivity:
|
|
220
|
+
pos_ints.append((cur, cur + l))
|
|
221
|
+
cur += l
|
|
222
|
+
return pos_ints
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def validate_labelset(annotations: Iterable[Annotation]) -> List[str]:
|
|
226
|
+
"""
|
|
227
|
+
Simple check for a list of annotations to see if they have the same label set.
|
|
228
|
+
|
|
229
|
+
:raise: AttributeError if an element in the input list doesn't have the ``labelset`` property
|
|
230
|
+
:raise: ValueError if different ``labelset`` values are found
|
|
231
|
+
:return: a list of the common ``labelset`` value (list of label names)
|
|
232
|
+
"""
|
|
233
|
+
# first, grab the label set from the source annotations
|
|
234
|
+
peek, annotations = itertools.tee(annotations)
|
|
235
|
+
try:
|
|
236
|
+
src_labels = set(next(peek).get_property('labelset'))
|
|
237
|
+
except KeyError:
|
|
238
|
+
raise AttributeError("The annotation in the list doesn't have "
|
|
239
|
+
"'labelset' property. Are they annotated by"
|
|
240
|
+
"a classification task?")
|
|
241
|
+
|
|
242
|
+
# and validate that all annotations have the same label set
|
|
243
|
+
for a in annotations:
|
|
244
|
+
if set(a.get_property('labelset')) != src_labels:
|
|
245
|
+
raise ValueError("All annotations must have the same label set, "
|
|
246
|
+
f"but found {a.get_property('labelset')}, "
|
|
247
|
+
f"different from {src_labels}")
|
|
248
|
+
return list(src_labels)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def build_label_remapper(src_labels: List[str], dst_labels: Dict[str, PRMTV_TYPES]) -> Dict[str, PRMTV_TYPES]:
|
|
252
|
+
"""
|
|
253
|
+
Build a label remapper dictionary from source and destination labels.
|
|
254
|
+
|
|
255
|
+
:param src_labels: a list of all labels on the source side
|
|
256
|
+
:param dst_labels: a dict from source labels to destination labels.
|
|
257
|
+
Source labels not in this dict will be remapped to a negative label (``-``).
|
|
258
|
+
:return: a dict that exhaustively maps source labels to destination labels
|
|
259
|
+
"""
|
|
260
|
+
if len(dst_labels) == 0:
|
|
261
|
+
return dict(zip(src_labels, src_labels))
|
|
262
|
+
else:
|
|
263
|
+
return {**dst_labels, **dict((k, NEG_LABEL) for k in src_labels if k not in dst_labels)}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def build_score_lists(classifications: List[Dict], label_remapper: Dict,
|
|
267
|
+
score_remap_op: Callable[..., float] = max) \
|
|
268
|
+
-> Tuple[Dict[str, int], "numpy.ndarray"]: # pytype: disable=name-error
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
Build lists of scores indexed by the label names.
|
|
272
|
+
|
|
273
|
+
:param classifications: list of dictionaries of classification results, taken from input annotation objects
|
|
274
|
+
:param label_remapper: a dictionary that maps source label names to destination label names (formerly "postbin")
|
|
275
|
+
:param score_remap_op: a function to remap the scores from multiple source labels binned to a destination label
|
|
276
|
+
common choices are ``max``, ``min``, or ``sum``
|
|
277
|
+
:return: 1. a dictionary that maps label names to their index in the score list
|
|
278
|
+
2. 2-d numpy array of scores, of which rows are indexed by label map dict (first return value)
|
|
279
|
+
"""
|
|
280
|
+
import numpy
|
|
281
|
+
scores = {lbl: [] for lbl in label_remapper.values()}
|
|
282
|
+
for c_idx, classification in enumerate(classifications):
|
|
283
|
+
for src_label, src_score in classification.items():
|
|
284
|
+
dst_label = label_remapper[src_label]
|
|
285
|
+
if len(scores[dst_label]) == c_idx: # means this is the first score for this label for this loop iter
|
|
286
|
+
scores[dst_label].append(src_score)
|
|
287
|
+
else:
|
|
288
|
+
scores[dst_label][-1] = score_remap_op((scores[dst_label][-1], src_score))
|
|
289
|
+
label_idx = {label: i for i, label in enumerate(scores.keys())}
|
|
290
|
+
score_lists = list(scores.values())
|
|
291
|
+
|
|
292
|
+
return label_idx, numpy.array(score_lists)
|
|
@@ -74,11 +74,11 @@ def convert(t: Union[int, float, str], in_unit: str, out_unit: str, fps: float)
|
|
|
74
74
|
return int(t * fps)
|
|
75
75
|
# s>(ms or i)
|
|
76
76
|
elif in_unit == 'second':
|
|
77
|
-
return t * 1000 if out_unit == 'millisecond' else _second_to_isoformat(t)
|
|
77
|
+
return int(t * 1000) if out_unit == 'millisecond' else _second_to_isoformat(t)
|
|
78
78
|
# ms>(s or i)
|
|
79
79
|
elif in_unit == 'millisecond':
|
|
80
80
|
return t / 1000 if out_unit == 'second' else _millisecond_to_isoformat(t)
|
|
81
81
|
# f>ms, f>s
|
|
82
82
|
else:
|
|
83
|
-
return (t / fps) if out_unit == 'second' else (t / fps * 1000) # pytype: disable=bad-return-type
|
|
83
|
+
return (t / fps) if out_unit == 'second' else (round(t / fps, 3) * 1000) # pytype: disable=bad-return-type
|
|
84
84
|
|
|
@@ -81,15 +81,19 @@ def extract_frames_as_images(video_document: Document, framenums: List[int], as_
|
|
|
81
81
|
from PIL import Image
|
|
82
82
|
frames = []
|
|
83
83
|
video = capture(video_document)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
cur_f = 0
|
|
85
|
+
while True:
|
|
86
|
+
if not framenums or cur_f > video_document.get_property(FRAMECOUNT_DOCPROP_KEY):
|
|
87
|
+
break
|
|
88
88
|
ret, frame = video.read()
|
|
89
|
-
if
|
|
89
|
+
if cur_f == framenums[0]:
|
|
90
|
+
if not ret:
|
|
91
|
+
sec = convert(cur_f, 'f', 's', video_document.get_property(FPS_DOCPROP_KEY))
|
|
92
|
+
warnings.warn(f'Frame #{cur_f} ({sec}s) could not be read from the video {video_document.id}.')
|
|
93
|
+
continue
|
|
90
94
|
frames.append(Image.fromarray(frame[:, :, ::-1]) if as_PIL else frame)
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
framenums.pop(0)
|
|
96
|
+
cur_f += 1
|
|
93
97
|
return frames
|
|
94
98
|
|
|
95
99
|
|
|
@@ -104,7 +108,7 @@ def get_mid_framenum(mmif: Mmif, time_frame: Annotation):
|
|
|
104
108
|
timeunit = time_frame.get_property('timeUnit')
|
|
105
109
|
video_document = mmif[time_frame.get_property('document')]
|
|
106
110
|
fps = get_framerate(video_document)
|
|
107
|
-
return convert(
|
|
111
|
+
return convert(time_frame.get_property('start') + time_frame.get_property('end'), timeunit, 'frame', fps) // 2
|
|
108
112
|
|
|
109
113
|
|
|
110
114
|
def extract_mid_frame(mmif: Mmif, time_frame: Annotation, as_PIL: bool = False):
|
|
@@ -6,4 +6,4 @@ from .base_types import DocumentTypesBase
|
|
|
6
6
|
from .annotation_types import AnnotationTypes
|
|
7
7
|
from .document_types import DocumentTypes
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
_typevers = {**ThingType._typevers, **AnnotationTypes._typevers, **DocumentTypes._typevers}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Spec version 1.0.4
|
|
2
|
+
# This file is auto-generated by setup.py
|
|
3
|
+
|
|
4
|
+
from .base_types import AnnotationTypesBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AnnotationTypes(AnnotationTypesBase):
|
|
8
|
+
"""
|
|
9
|
+
This class contains the CLAMS annotation types
|
|
10
|
+
defined in the spec version 1.0.4 as class variables.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# prpoerty aliases, first added in MMIF 1.0.2
|
|
14
|
+
# map from type short name to a dictionary of property aliases
|
|
15
|
+
# where nested dictionary maps from representative name to its aliases
|
|
16
|
+
_prop_aliases = {
|
|
17
|
+
'TimeFrame': {'label': {'frameType', 'label'},
|
|
18
|
+
'classification': {'classifications', 'classification'}},
|
|
19
|
+
'BoundingBox': {'label': {'boxType', 'label'},
|
|
20
|
+
'classification': {'classifications', 'classification'}},
|
|
21
|
+
'Annotation': {'classification': {'classifications', 'classification'}},
|
|
22
|
+
'Region': {'classification': {'classifications', 'classification'}},
|
|
23
|
+
'Interval': {'classification': {'classifications', 'classification'}},
|
|
24
|
+
'Span': {'classification': {'classifications', 'classification'}},
|
|
25
|
+
'Chapter': {'classification': {'classifications', 'classification'}},
|
|
26
|
+
'Polygon': {'classification': {'classifications', 'classification'}},
|
|
27
|
+
'VideoObject': {'classification': {'classifications', 'classification'}},
|
|
28
|
+
'Relation': {'classification': {'classifications', 'classification'}},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Annotation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Annotation/v5')
|
|
32
|
+
Region = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Region/v4')
|
|
33
|
+
TimePoint = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimePoint/v4')
|
|
34
|
+
Interval = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Interval/v4')
|
|
35
|
+
Span = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Span/v4')
|
|
36
|
+
TimeFrame = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimeFrame/v5')
|
|
37
|
+
Chapter = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Chapter/v5')
|
|
38
|
+
Polygon = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Polygon/v4')
|
|
39
|
+
BoundingBox = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/BoundingBox/v4')
|
|
40
|
+
VideoObject = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/VideoObject/v4')
|
|
41
|
+
Relation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Relation/v4')
|
|
42
|
+
Alignment = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Alignment/v1')
|
|
43
|
+
_typevers = {'Annotation': 'v5', 'Region': 'v4', 'TimePoint': 'v4', 'Interval': 'v4', 'Span': 'v4', 'TimeFrame': 'v5', 'Chapter': 'v5', 'Polygon': 'v4', 'BoundingBox': 'v4', 'VideoObject': 'v4', 'Relation': 'v4', 'Alignment': 'v1'}
|
|
@@ -196,7 +196,7 @@ class DocumentTypesBase(ClamsTypesBase):
|
|
|
196
196
|
class ThingType(ThingTypesBase):
|
|
197
197
|
"""
|
|
198
198
|
This class contains the topmost CLAMS thing type
|
|
199
|
-
defined in the spec version 1.0.
|
|
199
|
+
defined in the spec version 1.0.4 as a class variable.
|
|
200
200
|
"""
|
|
201
201
|
Thing = ClamsTypesBase('http://mmif.clams.ai/vocabulary/Thing/v1')
|
|
202
|
-
|
|
202
|
+
_typevers = {'Thing': 'v1'}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Spec version 1.0.
|
|
1
|
+
# Spec version 1.0.4
|
|
2
2
|
# This file is auto-generated by setup.py
|
|
3
3
|
|
|
4
4
|
from .base_types import DocumentTypesBase
|
|
@@ -7,11 +7,11 @@ from .base_types import DocumentTypesBase
|
|
|
7
7
|
class DocumentTypes(DocumentTypesBase):
|
|
8
8
|
"""
|
|
9
9
|
This class contains the CLAMS document types
|
|
10
|
-
defined in the spec version 1.0.
|
|
10
|
+
defined in the spec version 1.0.4 as class variables.
|
|
11
11
|
"""
|
|
12
12
|
Document = DocumentTypesBase('http://mmif.clams.ai/vocabulary/Document/v1')
|
|
13
13
|
VideoDocument = DocumentTypesBase('http://mmif.clams.ai/vocabulary/VideoDocument/v1')
|
|
14
14
|
AudioDocument = DocumentTypesBase('http://mmif.clams.ai/vocabulary/AudioDocument/v1')
|
|
15
15
|
ImageDocument = DocumentTypesBase('http://mmif.clams.ai/vocabulary/ImageDocument/v1')
|
|
16
16
|
TextDocument = DocumentTypesBase('http://mmif.clams.ai/vocabulary/TextDocument/v1')
|
|
17
|
-
|
|
17
|
+
_typevers = {'Document': 'v1', 'VideoDocument': 'v1', 'AudioDocument': 'v1', 'ImageDocument': 'v1', 'TextDocument': 'v1'}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mmif-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.12
|
|
4
4
|
Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
|
|
5
5
|
Home-page: https://mmif.clams.ai
|
|
6
6
|
Author: Brandeis Lab for Linguistics and Computation
|
|
@@ -10,6 +10,21 @@ Description: ## MultiMedia Interchange Format
|
|
|
10
10
|
[MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
## mmif-python
|
|
14
|
+
`mmif-python` is a Python implementation of the MMIF data format.
|
|
15
|
+
`mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
|
|
16
|
+
including ;
|
|
17
|
+
|
|
18
|
+
1. de-/serialization of MMIF internal data structures to/from JSON
|
|
19
|
+
2. validation of MMIF JSON
|
|
20
|
+
3. handling of CLAMS vocabulary types
|
|
21
|
+
4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
|
|
22
|
+
|
|
23
|
+
## For more ...
|
|
24
|
+
* [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
|
|
25
|
+
* [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
|
|
26
|
+
* [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
|
|
27
|
+
|
|
13
28
|
Platform: UNKNOWN
|
|
14
29
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
15
30
|
Classifier: Intended Audience :: Developers
|
|
@@ -17,5 +32,6 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
17
32
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
33
|
Requires-Python: >=3.8
|
|
19
34
|
Description-Content-Type: text/markdown
|
|
35
|
+
Provides-Extra: seq
|
|
20
36
|
Provides-Extra: cv
|
|
21
37
|
Provides-Extra: dev
|
|
@@ -4,6 +4,7 @@ README.md
|
|
|
4
4
|
setup.py
|
|
5
5
|
./VERSION
|
|
6
6
|
./requirements.cv
|
|
7
|
+
./requirements.seq
|
|
7
8
|
./requirements.txt
|
|
8
9
|
mmif/__init__.py
|
|
9
10
|
mmif/res/__init__.py
|
|
@@ -16,6 +17,7 @@ mmif/serialize/mmif.py
|
|
|
16
17
|
mmif/serialize/model.py
|
|
17
18
|
mmif/serialize/view.py
|
|
18
19
|
mmif/utils/__init__.py
|
|
20
|
+
mmif/utils/sequence_helper.py
|
|
19
21
|
mmif/utils/timeunit_helper.py
|
|
20
22
|
mmif/utils/video_document_helper.py
|
|
21
23
|
mmif/ver/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -69,7 +69,7 @@ def generate_vocab_enum(spec_version, clams_types_vers, mod_name) -> str:
|
|
|
69
69
|
for type_name, type_ver in clams_types_vers:
|
|
70
70
|
vocab_url = f'http://mmif.clams.ai/vocabulary/{type_name}/{type_ver}'
|
|
71
71
|
file_out.write(f" {type_name} = {base_class_name}('{vocab_url}')\n")
|
|
72
|
-
file_out.write(f"
|
|
72
|
+
file_out.write(f" _typevers = {dict(clams_types_vers)}\n")
|
|
73
73
|
|
|
74
74
|
string_out = file_out.getvalue()
|
|
75
75
|
file_out.close()
|
|
@@ -134,7 +134,7 @@ def generate_vocabulary(spec_version, clams_types_vers):
|
|
|
134
134
|
f"from .{mod_name} import {class_name}"
|
|
135
135
|
for mod_name, classes in types.items()
|
|
136
136
|
for class_name in classes
|
|
137
|
-
) + '\n\n' + "
|
|
137
|
+
) + '\n\n' + "_typevers = {**ThingType._typevers, **AnnotationTypes._typevers, **DocumentTypes._typevers}" + '\n'
|
|
138
138
|
)
|
|
139
139
|
document_types = []
|
|
140
140
|
annotation_types = []
|
|
@@ -276,6 +276,9 @@ with open('requirements.txt') as requirements:
|
|
|
276
276
|
with open('requirements.cv') as requirements:
|
|
277
277
|
cv_requires = requirements.readlines()
|
|
278
278
|
|
|
279
|
+
with open('requirements.seq') as requirements:
|
|
280
|
+
seq_requires = requirements.readlines()
|
|
281
|
+
|
|
279
282
|
setuptools.setup(
|
|
280
283
|
name=name,
|
|
281
284
|
version=version,
|
|
@@ -294,6 +297,7 @@ setuptools.setup(
|
|
|
294
297
|
},
|
|
295
298
|
install_requires=requires,
|
|
296
299
|
extras_require={
|
|
300
|
+
'seq': seq_requires,
|
|
297
301
|
'cv': cv_requires,
|
|
298
302
|
'dev': [
|
|
299
303
|
'pytest',
|
mmif-python-1.0.10/README.md
DELETED
mmif-python-1.0.10/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.10
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Spec version 1.0.2
|
|
2
|
-
# This file is auto-generated by setup.py
|
|
3
|
-
|
|
4
|
-
from .base_types import AnnotationTypesBase
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class AnnotationTypes(AnnotationTypesBase):
|
|
8
|
-
"""
|
|
9
|
-
This class contains the CLAMS annotation types
|
|
10
|
-
defined in the spec version 1.0.2 as class variables.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
# prpoerty aliases, first added in MMIF 1.0.2
|
|
14
|
-
# map from type short name to a dictionary of property aliases
|
|
15
|
-
# where nested dictionary maps from representative name to its aliases
|
|
16
|
-
prop_aliases = {
|
|
17
|
-
'TimeFrame': {'label': {'frameType', 'label'}},
|
|
18
|
-
'BoundingBox': {'label': {'boxType', 'label'}},
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
Annotation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Annotation/v3')
|
|
22
|
-
Region = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Region/v2')
|
|
23
|
-
TimePoint = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimePoint/v2')
|
|
24
|
-
Interval = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Interval/v2')
|
|
25
|
-
Span = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Span/v2')
|
|
26
|
-
TimeFrame = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimeFrame/v3')
|
|
27
|
-
Chapter = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Chapter/v3')
|
|
28
|
-
Polygon = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Polygon/v2')
|
|
29
|
-
BoundingBox = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/BoundingBox/v2')
|
|
30
|
-
VideoObject = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/VideoObject/v2')
|
|
31
|
-
Relation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Relation/v2')
|
|
32
|
-
Alignment = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Alignment/v1')
|
|
33
|
-
typevers = {'Annotation': 'v3', 'Region': 'v2', 'TimePoint': 'v2', 'Interval': 'v2', 'Span': 'v2', 'TimeFrame': 'v3', 'Chapter': 'v3', 'Polygon': 'v2', 'BoundingBox': 'v2', 'VideoObject': 'v2', 'Relation': 'v2', 'Alignment': 'v1'}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|