F
11d9532b5c
Annotations package (new): - Add threshold_qualifier with 3-pass hysteresis detector (Pass 1: strong bursts, Pass 2: weak residual bursts, Pass 3: macro-window faint burst detection), auto window_size scaled to 1ms, channel selection, and stable noise_floor baseline throughout - Add energy_detector, cusum_annotator, parallel_signal_separator, qualify_slice, signal_isolation, annotation_transforms - Add __init__.py exporting the four functions used by the CLI - Fix all imports from utils.data → ria_toolkit_oss.datatypes CLI annotate command (new): - Port full annotate CLI from utils including list, add, remove, clear, energy, cusum, threshold, and separate subcommands - Fix imports from utils.* → ria_toolkit_oss.* and utils_cli.* → ria_toolkit_oss_cli.* - Safe overwrite logic: _annotated files always writable, originals protected; --overwrite writes in-place only on _annotated inputs CLI view command: - Add 'annotations' as a valid --type, wiring view_annotations from view_signal view_signal.py: - Add view_annotations function with blue/purple alternating palette and threshold %-sorted drawing order (lower % renders on top) recording.py (datatypes): - Fix lazy imports in to_wav() and to_blue() from utils.io → ria_toolkit_oss.io io/recording.py: - Add compatibility shim in from_npy to remap utils.data.annotation.Annotation to ria_toolkit_oss.datatypes.annotation.Annotation when loading .npy files pickled by the utils package
56 lines
1.7 KiB
Python
56 lines
1.7 KiB
Python
from ria_toolkit_oss.datatypes.annotation import Annotation
|
|
|
|
# TODO figure out how to transfer labels in the merge case
|
|
|
|
|
|
def remove_contained_boxes(annotations: list[Annotation]):
|
|
"""
|
|
Remove all annotations (bounding boxes) that are entirely contained within other boxes in the list.
|
|
|
|
:param annotations: A list of Annotation objects.
|
|
:type annotations: list[Annotation]
|
|
|
|
:returns: A new list of Annotation objects.
|
|
:rtype: list[Annotation]"""
|
|
|
|
output_boxes = []
|
|
|
|
for i in range(len(annotations)):
|
|
contained = False
|
|
for j in range(len(annotations)):
|
|
if i != j and is_annotation_contained(annotations[i], annotations[j]):
|
|
contained = True
|
|
break
|
|
|
|
if not contained:
|
|
output_boxes.append(annotations[i])
|
|
|
|
return output_boxes
|
|
|
|
|
|
def is_annotation_contained(inner: Annotation, outer: Annotation) -> bool:
|
|
"""
|
|
Check if an annotation box is entirely contained within another annotation bounding box.
|
|
|
|
:param inner: The inner box.
|
|
:type inner: Annotation.
|
|
:param outer: The outer box.
|
|
:type outer: Annotation.
|
|
|
|
:returns: True if inner is within outer, false otherwise.
|
|
:rtype: bool
|
|
"""
|
|
|
|
inner_sample_stop = inner.sample_start + inner.sample_count
|
|
outer_sample_stop = outer.sample_start + outer.sample_count
|
|
|
|
if inner.sample_start > outer.sample_start and inner_sample_stop < outer_sample_stop:
|
|
if inner.freq_lower_edge > outer.freq_lower_edge and inner.freq_upper_edge < outer.freq_upper_edge:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def merge_annotations(annotations: list[Annotation], overlap_threshold) -> list[Annotation]:
|
|
raise NotImplementedError
|