M
madrigal
8a66860d33
All checks were successful
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 15m51s
Build Project / Build Project (3.10) (pull_request) Successful in 16m14s
Build Project / Build Project (3.11) (pull_request) Successful in 17m9s
Build Project / Build Project (3.12) (pull_request) Successful in 2m29s
Test with tox / Test with tox (3.12) (pull_request) Successful in 21m28s
Test with tox / Test with tox (3.10) (pull_request) Successful in 22m50s
Test with tox / Test with tox (3.11) (pull_request) Successful in 23m18s
130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Optional
|
|
|
|
from sigmf import SigMFFile
|
|
|
|
|
|
class Annotation:
|
|
"""Signal annotations are labels or additional information associated with specific data points or segments within
|
|
a signal. These annotations could be used for tasks like supervised learning, where the goal is to train a model
|
|
to recognize patterns or characteristics in the signal associated with these annotations.
|
|
|
|
Annotations can be used to label interesting points in your recording.
|
|
|
|
:param sample_start: The index of the starting sample of the annotation.
|
|
:type sample_start: int
|
|
:param sample_count: The index of the ending sample of the annotation, inclusive.
|
|
:type sample_count: int
|
|
:param freq_lower_edge: The lower frequency of the annotation.
|
|
:type freq_lower_edge: float
|
|
:param freq_upper_edge: The upper frequency of the annotation.
|
|
:type freq_upper_edge: float
|
|
:param label: The label that will be displayed with the bounding box in compatible viewers including IQEngine.
|
|
Defaults to an emtpy string.
|
|
:type label: str, optional
|
|
:param comment: A human-readable comment. Defaults to an empty string.
|
|
:type comment: str, optional
|
|
:param detail: A dictionary of user defined annotation-specific metadata. Defaults to None.
|
|
:type detail: dict, optional
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
sample_start: int,
|
|
sample_count: int,
|
|
freq_lower_edge: float,
|
|
freq_upper_edge: float,
|
|
label: Optional[str] = "",
|
|
comment: Optional[str] = "",
|
|
detail: Optional[dict] = None,
|
|
):
|
|
"""Initialize a new Annotation instance."""
|
|
self.sample_start = int(sample_start)
|
|
self.sample_count = int(sample_count)
|
|
self.freq_lower_edge = float(freq_lower_edge)
|
|
self.freq_upper_edge = float(freq_upper_edge)
|
|
self.label = str(label)
|
|
self.comment = str(comment)
|
|
|
|
if detail is None:
|
|
self.detail = {}
|
|
elif not _is_jsonable(detail):
|
|
raise ValueError(f"Detail object is not json serializable: {detail}")
|
|
else:
|
|
self.detail = detail
|
|
|
|
def is_valid(self) -> bool:
|
|
"""
|
|
Verify ``sample_count > 0`` and the ``freq_lower_edge < freq_upper_edge``.
|
|
|
|
:returns: True if valid, False if not.
|
|
"""
|
|
|
|
return self.sample_count > 0 and self.freq_lower_edge < self.freq_upper_edge
|
|
|
|
def overlap(self, other):
|
|
"""
|
|
Quantify how much the bounding box in this annotation overlaps with another annotation.
|
|
|
|
:param other: The other annotation.
|
|
:type other: Annotation
|
|
|
|
:returns: The area of the overlap in samples*frequency, or 0 if they do not overlap."""
|
|
|
|
sample_overlap_start = max(self.sample_start, other.sample_start)
|
|
sample_overlap_end = min(self.sample_start + self.sample_count, other.sample_start + other.sample_count)
|
|
|
|
freq_overlap_start = max(self.freq_lower_edge, other.freq_lower_edge)
|
|
freq_overlap_end = min(self.freq_upper_edge, other.freq_upper_edge)
|
|
|
|
if freq_overlap_start >= freq_overlap_end or sample_overlap_start >= sample_overlap_end:
|
|
return 0
|
|
else:
|
|
return (sample_overlap_end - sample_overlap_start) * (freq_overlap_end - freq_overlap_start)
|
|
|
|
def area(self):
|
|
"""
|
|
The 'area' of the bounding box, samples*frequency.
|
|
Useful to quantify annotation size.
|
|
|
|
:returns: sample length multiplied by bandwidth."""
|
|
|
|
return self.sample_count * (self.freq_upper_edge - self.freq_lower_edge)
|
|
|
|
def __eq__(self, other: Annotation) -> bool:
|
|
return self.__dict__ == other.__dict__
|
|
|
|
def to_sigmf_format(self) -> dict:
|
|
"""
|
|
Returns a JSON dictionary representation, formatted for saving in a ``.sigmf-meta`` file.
|
|
"""
|
|
|
|
annotation_dict = {SigMFFile.START_INDEX_KEY: self.sample_start, SigMFFile.LENGTH_INDEX_KEY: self.sample_count}
|
|
|
|
annotation_dict["metadata"] = {
|
|
SigMFFile.LABEL_KEY: self.label,
|
|
SigMFFile.COMMENT_KEY: self.comment,
|
|
SigMFFile.FHI_KEY: self.freq_upper_edge,
|
|
SigMFFile.FLO_KEY: self.freq_lower_edge,
|
|
"ria:detail": self.detail,
|
|
}
|
|
|
|
if _is_jsonable(annotation_dict):
|
|
return annotation_dict
|
|
else:
|
|
raise ValueError("Annotation dictionary was not json serializable.")
|
|
|
|
|
|
def _is_jsonable(x: Any) -> bool:
|
|
"""
|
|
:return: True if ``x`` is JSON serializable, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
try:
|
|
json.dumps(x)
|
|
return True
|
|
except (TypeError, OverflowError):
|
|
return False
|