# -*- coding: UTF-8 -*-
"""
:filename: sppas.src.anndata.ann.annotation.py
:author: Brigitte Bigi
:contact: develop@sppas.org
:summary: Representation of an annotation.
.. _This file is part of SPPAS: http://www.sppas.org/
..
---------------------------------------------------------------------
___ __ __ __ ___
/ | \ | \ | \ / the automatic
\__ |__/ |__/ |___| \__ annotation and
\ | | | | \ analysis
___/ | | | | ___/ of speech
Copyright (C) 2011-2021 Brigitte Bigi
Laboratoire Parole et Langage, Aix-en-Provence, France
Use of this software is governed by the GNU Public License, version 3.
SPPAS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
SPPAS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SPPAS. If not, see <http://www.gnu.org/licenses/>.
This banner notice must not be removed.
---------------------------------------------------------------------
"""
import warnings
from sppas.src.anndata.anndataexc import AnnDataTypeError
from sppas.src.anndata.metadata import sppasMetaData
from .annlabel import sppasTag
from .annlabel import sppasLabel
from .annlocation import sppasLocation
from .annlabel import sppasTagCompare
# ----------------------------------------------------------------------------
[docs]class sppasAnnotation(sppasMetaData):
"""Represents an annotation.
A sppasAnnotation() is a container for:
- a sppasLocation()
- a list of sppasLabel()
:Example:
>>> location = sppasLocation(sppasPoint(1.5, radius=0.01))
>>> labels = sppasLabel(sppasTag("foo"))
>>> ann = sppasAnnotation(location, labels)
"""
[docs] def __init__(self, location, labels=list()):
"""Create a new sppasAnnotation instance.
:param location: (sppasLocation) the location(s) where the annotation\
happens
:param labels: (sppasLabel, list) the label(s) to stamp this\
annotation, or a list of them.
"""
super(sppasAnnotation, self).__init__()
# Check location instance.
if isinstance(location, sppasLocation) is False:
raise AnnDataTypeError(location, "sppasLocation")
self.__parent = None
self.__location = location
self.__labels = list()
self.__score = None
self.set_labels(labels)
# -----------------------------------------------------------------------
# Member getters
# -----------------------------------------------------------------------
[docs] def get_parent(self):
"""Return the parent tier or None."""
return self.__parent
# -----------------------------------------------------------------------
[docs] def get_score(self):
"""Return the score of this annotation or None if no score is set."""
return self.__score
# -----------------------------------------------------------------------
[docs] def get_location(self):
"""Return the sppasLocation() of this annotation."""
return self.__location
# -----------------------------------------------------------------------
[docs] def get_labels(self):
"""Return the list of sppasLabel() of this annotation."""
return self.__labels
# -----------------------------------------------------------------------
[docs] def copy(self):
"""Return a full copy of the annotation.
The location, the labels and the metadata are all copied. The 'id'
of the returned annotation is then the same.
:returns: sppasAnnotation()
"""
# Create a copy of the location/labels
location = self.__location.copy()
labels = list()
for l in self.__labels:
labels.append(l.copy())
# Create the new Annotation
other = sppasAnnotation(location, labels)
other.set_parent(self.__parent)
other.set_score(self.__score)
# Copy all metadata, including the 'id'.
for key in self.get_meta_keys():
other.set_meta(key, self.get_meta(key))
return other
# -----------------------------------------------------------------------
# Setters
# -----------------------------------------------------------------------
[docs] def set_parent(self, parent=None):
"""Set a parent tier.
:param parent: (sppasTier) The parent tier of this annotation.
:raises: CtrlVocabContainsError, HierarchyContainsError, \
HierarchyTypeError
"""
if parent is not None:
parent.validate_annotation_location(self.__location)
for label in self.__labels:
parent.validate_annotation_label(label)
self.__parent = parent
# -----------------------------------------------------------------------
[docs] def set_score(self, score=None):
"""Set or reset the score to this annotation.
:param score: (float)
"""
if score is not None:
try:
self.__score = float(score)
except ValueError:
raise AnnDataTypeError(score, "float")
else:
self.__score = None
# -----------------------------------------------------------------------
[docs] def set_labels(self, labels=[]):
"""Fix/reset the list of labels of this annotation.
:param labels: (sppasLabel, list) the label(s) to stamp this \
annotation, or a list of them.
:raises: AnnDataTypeError, TypeError, CtrlVocabContainsError, \
HierarchyContainsError, HierarchyTypeError
"""
self.__labels = list()
if labels is None:
return
if isinstance(labels, sppasLabel) is True:
self.validate_label(labels)
self.__labels.append(labels)
elif isinstance(labels, list) is True:
for label in labels:
if label is not None:
self.validate_label(label)
self.__labels.append(label)
else:
raise AnnDataTypeError(labels, "sppasLabel/list")
# -----------------------------------------------------------------------
[docs] def validate(self):
"""Validate the annotation.
Check if the labels and location match the requirements.
:raises: TypeError, CtrlVocabContainsError, HierarchyContainsError, \
HierarchyTypeError
"""
for label in self.__labels:
self.validate_label(label)
if self.__parent is not None:
self.__parent.validate_annotation_location(self.__location)
# -----------------------------------------------------------------------
[docs] def validate_label(self, label):
"""Validate the label.
Check if the label matches the requirements of this annotation.
:raises: CtrlVocabContainsError, TypeError
"""
if label is None:
return
if isinstance(label, sppasLabel) is False:
raise AnnDataTypeError(label, "sppasLabel")
if len(self.__labels) > 0:
current_type = set(l.get_type()
for l in self.__labels if l is not None)
if len(current_type) > 0 and label.get_type() not in current_type:
raise TypeError()
if self.__parent is not None:
self.__parent.validate_annotation_label(label)
# -----------------------------------------------------------------------
# Labels
# -----------------------------------------------------------------------
[docs] def is_labelled(self):
"""Return True if at least a sppasTag exists and is not None."""
if len(self.__labels) == 0:
return False
for label in self.__labels:
if label is not None:
if label.is_tagged() is True:
return True
return False
# -----------------------------------------------------------------------
[docs] def get_label_type(self):
"""Return the current type of tags, or an empty string."""
if len(self.__labels) == 0:
return ""
for label in self.__labels:
if label is not None:
if label.is_tagged() is True:
return label.get_type()
return ""
# -----------------------------------------------------------------------
[docs] def append_label(self, label):
"""Append a label into the list of labels.
:param label: (sppasLabel)
"""
self.validate_label(label)
self.__labels.append(label)
# -----------------------------------------------------------------------
[docs] def get_labels_best_tag(self):
"""Return a list with the best tag of each label."""
tags = list()
for label in self.__labels:
best = label.get_best()
if best is not None:
tags.append(best)
else:
tags.append(sppasTag(''))
return tags
# -----------------------------------------------------------------------
[docs] def get_best_tag(self, label_idx=0):
"""Return the tag with the highest score of a label or an empty str.
:param label_idx: (int)
"""
# No label defined
if len(self.__labels) == 0:
return sppasTag("")
try:
label = self.__labels[label_idx]
except IndexError:
raise IndexError('Invalid label index')
if label is None:
return sppasTag('')
best = label.get_best()
if best is not None:
return best
return sppasTag("")
# -----------------------------------------------------------------------
[docs] def add_tag(self, tag, score=None, label_idx=0):
"""Append an alternative tag in a label.
:param tag: (sppasTag)
:param score: (float)
:param label_idx: (int)
:raises: AnnDataTypeError, IndexError
"""
# No label previously defined
if len(self.__labels) == 0:
label = sppasLabel(tag, score)
self.__labels.append(label)
else:
try:
label = self.__labels[label_idx]
if label is None:
label = sppasLabel(tag, score)
else:
label.append(tag, score)
except IndexError:
raise IndexError('Invalid label index')
# tag, score were added. Now, validate.
try:
self.validate_label(label)
except:
# restore
label.remove(tag)
raise
# -----------------------------------------------------------------------
[docs] def remove_tag(self, tag, label_idx=0):
"""Remove an alternative tag of the label.
:param tag: (sppasTag) the tag to be removed of the list.
:param label_idx: (int)
"""
try:
label = self.__labels[label_idx]
except IndexError:
raise IndexError('Invalid label index')
label.remove(tag)
# -----------------------------------------------------------------------
[docs] def contains_tag(self, tag, function='exact', reverse=False, label_idx=0):
"""Return True if the given tag is in the label.
:param tag: (sppasTag)
:param function: Search function
:param reverse: Reverse the function.
:param label_idx: (int)
"""
# No label defined
if len(self.__labels) == 0:
return False
try:
label = self.__labels[label_idx]
except IndexError:
raise IndexError('Invalid label index')
if label is None:
return False
if label.is_tagged() is False:
return False
t = sppasTagCompare()
tag_functions = list()
tag_functions.append((t.get(function), tag.get_typed_content(), reverse))
r = label.match(tag_functions)
if reverse is False:
return r
return not r
# -----------------------------------------------------------------------
[docs] def label_is_filled(self):
"""Return True if at least one BEST tag is filled."""
for label in self.__labels:
if label is not None:
if label.is_tagged() is True:
if label.get_best().get_content() != "":
return True
return False
# -----------------------------------------------------------------------
[docs] def label_is_string(self):
"""Return True if the type of the labels is 'str'."""
if len(self.__labels) == 0:
return False
for label in self.__labels:
if label.is_tagged() is True:
return label.is_string()
return False
# -----------------------------------------------------------------------
[docs] def label_is_float(self):
"""Return True if the type of the labels is 'float'."""
if len(self.__labels) == 0:
return False
for label in self.__labels:
if label.is_tagged() is True:
return label.is_float()
return False
# -----------------------------------------------------------------------
[docs] def label_is_int(self):
"""Return True if the type of the labels is 'int'."""
if len(self.__labels) == 0:
return False
for label in self.__labels:
if label.is_tagged() is True:
return label.is_int()
return False
# -----------------------------------------------------------------------
[docs] def label_is_bool(self):
"""Return True if the type of the labels is 'bool'."""
if len(self.__labels) == 0:
return False
for label in self.__labels:
if label.is_tagged() is True:
return label.is_bool()
return False
# -----------------------------------------------------------------------
[docs] def serialize_labels(self, separator="\n", empty="", alt=True):
"""DEPRECATED. Return labels serialized into a string.
TODO: REMOVE THIS METHOD. Use aioutils.serialize_labels() instead.
:param separator: (str) String to separate labels.
:param empty: (str) The text to return if a tag is empty or not set.
:param alt: (bool) Include alternative tags
:returns: (str)
"""
warnings.warn("Use serialize_labels(ann.get_labels()) of aioutils package instead",
DeprecationWarning)
if len(self.__labels) == 0:
return empty
if len(self.__labels) == 1:
return self.__labels[0].serialize(empty, alt)
c = list()
for label in self.__labels:
c.append(label.serialize(empty, alt))
return separator.join(c)
# -----------------------------------------------------------------------
# Localization
# -----------------------------------------------------------------------
[docs] def set_best_localization(self, localization):
"""Set the best localization of the location.
:param localization: (sppasBaseLocalization)
"""
old_loc = self.__location.get_best().copy()
self.__location.get_best().set(localization)
if self.__parent is not None:
try:
self.__parent.validate_annotation_location(self.__location)
except Exception:
self.__location.get_best().set(old_loc)
raise
# -----------------------------------------------------------------------
[docs] def location_is_point(self):
"""Return True if the location is made of sppasPoint locs."""
return self.__location.is_point()
# -----------------------------------------------------------------------
[docs] def location_is_interval(self):
"""Return True if the location is made of sppasInterval locs."""
return self.__location.is_interval()
# -----------------------------------------------------------------------
[docs] def location_is_disjoint(self):
"""Return True if the location is made of sppasDisjoint locs."""
return self.__location.is_disjoint()
# -----------------------------------------------------------------------
[docs] def get_highest_localization(self):
"""Return a copy of the sppasPoint with the highest loc."""
return self.__location.get_highest_localization()
# -----------------------------------------------------------------------
[docs] def get_lowest_localization(self):
"""Return a copy of the sppasPoint with the lowest localization."""
return self.__location.get_lowest_localization()
# -----------------------------------------------------------------------
[docs] def get_all_points(self):
"""Return the list of a copy of all points of this annotation."""
points = list()
if self.__location.is_point():
for localization, score in self.__location:
points.append(localization.copy())
elif self.__location.is_interval():
for localization, score in self.__location:
points.append(localization.get_begin().copy())
points.append(localization.get_end().copy())
elif self.__location.is_disjoint():
for localization, score in self.__location:
for interval in localization.get_intervals():
points.append(interval.get_begin().copy())
points.append(interval.get_end().copy())
return points
# -----------------------------------------------------------------------
[docs] def contains_localization(self, localization):
"""Return True if the given localization is in the location."""
return self.__location.contains(localization)
# -----------------------------------------------------------------------
[docs] def validate_location(self):
"""Validate the location of the annotation.
:raises:
"""
if self.__parent is not None:
self.__parent.validate_annotation_location(self.__location)
# -----------------------------------------------------------------------
# Overloads
# -----------------------------------------------------------------------
def __format__(self, fmt):
return str(self).__format__(fmt)
# -----------------------------------------------------------------------
def __repr__(self):
return "Annotation {:s}: {:s} {:s}".format(self.get_meta('id'),
str(self.__location),
str(self.__labels))
# -----------------------------------------------------------------------
def __str__(self):
return "{:s} {:s} {:s}".format(self.get_meta('id'),
str(self.__location),
str(self.__labels))
# -----------------------------------------------------------------------
def __hash__(self):
# use the hashcode of self identifier since that is used
# for equality checks as well, like "ann in my_dict".
# not required by Python 2.7 but necessary for Python 3.4+
return hash(self.get_id())
# -----------------------------------------------------------------------
def __eq__(self, other):
if other is None:
return False
if isinstance(other, sppasAnnotation) is False:
return False
if self.__score != other.get_score():
return False
if self.__location != other.get_location():
return False
if len(self.__labels) != len(other.get_labels()):
return False
for label1, label2 in zip(self.__labels, other.get_labels()):
if label1 != label2:
return False
return True
# -----------------------------------------------------------------------
def __ne__(self, other):
return not self == other