mirror of
https://github.com/macaodha/batdetect2.git
synced 2026-01-11 17:29:34 +01:00
Adding plotting functions
This commit is contained in:
parent
e1908c35ca
commit
87ce2acd6f
@ -0,0 +1,21 @@
|
|||||||
|
from batdetect2.plotting.clip_annotations import plot_clip_annotation
|
||||||
|
from batdetect2.plotting.clip_predictions import plot_clip_prediction
|
||||||
|
from batdetect2.plotting.clips import plot_clip
|
||||||
|
from batdetect2.plotting.matches import (
|
||||||
|
plot_cross_trigger_match,
|
||||||
|
plot_false_negative_match,
|
||||||
|
plot_false_positive_match,
|
||||||
|
plot_matches,
|
||||||
|
plot_true_positive_match,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"plot_clip",
|
||||||
|
"plot_clip_annotation",
|
||||||
|
"plot_clip_prediction",
|
||||||
|
"plot_matches",
|
||||||
|
"plot_false_positive_match",
|
||||||
|
"plot_true_positive_match",
|
||||||
|
"plot_false_negative_match",
|
||||||
|
"plot_cross_trigger_match",
|
||||||
|
]
|
||||||
49
src/batdetect2/plotting/clip_annotations.py
Normal file
49
src/batdetect2/plotting/clip_annotations.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from matplotlib.axes import Axes
|
||||||
|
from soundevent import data, plot
|
||||||
|
|
||||||
|
from batdetect2.plotting.clips import plot_clip
|
||||||
|
from batdetect2.preprocess import PreprocessorProtocol
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"plot_clip_annotation",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def plot_clip_annotation(
|
||||||
|
clip_annotation: data.ClipAnnotation,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
add_points: bool = False,
|
||||||
|
cmap: str = "gray",
|
||||||
|
alpha: float = 1,
|
||||||
|
linewidth: float = 1,
|
||||||
|
fill: bool = False,
|
||||||
|
) -> Axes:
|
||||||
|
ax = plot_clip(
|
||||||
|
clip_annotation.clip,
|
||||||
|
preprocessor=preprocessor,
|
||||||
|
figsize=figsize,
|
||||||
|
ax=ax,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
add_labels=add_labels,
|
||||||
|
spec_cmap=cmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot.plot_annotations(
|
||||||
|
clip_annotation.sound_events,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.004,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
alpha=alpha,
|
||||||
|
linewidth=linewidth,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
)
|
||||||
|
return ax
|
||||||
141
src/batdetect2/plotting/clip_predictions.py
Normal file
141
src/batdetect2/plotting/clip_predictions.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
from typing import Iterable, Optional, Tuple
|
||||||
|
|
||||||
|
from matplotlib.axes import Axes
|
||||||
|
from soundevent import data
|
||||||
|
from soundevent.geometry.operations import Positions, get_geometry_point
|
||||||
|
from soundevent.plot.common import create_axes
|
||||||
|
from soundevent.plot.geometries import plot_geometry
|
||||||
|
from soundevent.plot.tags import TagColorMapper, add_tags_legend, plot_tag
|
||||||
|
|
||||||
|
from batdetect2.plotting.clips import plot_clip
|
||||||
|
from batdetect2.preprocess import PreprocessorProtocol
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"plot_clip_prediction",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def plot_clip_prediction(
|
||||||
|
clip_prediction: data.ClipPrediction,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
add_legend: bool = False,
|
||||||
|
spec_cmap: str = "gray",
|
||||||
|
linewidth: float = 1,
|
||||||
|
fill: bool = False,
|
||||||
|
) -> Axes:
|
||||||
|
ax = plot_clip(
|
||||||
|
clip_prediction.clip,
|
||||||
|
preprocessor=preprocessor,
|
||||||
|
figsize=figsize,
|
||||||
|
ax=ax,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
add_labels=add_labels,
|
||||||
|
spec_cmap=spec_cmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot_predictions(
|
||||||
|
clip_prediction.sound_events,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.004,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=False,
|
||||||
|
linewidth=linewidth,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
legend=add_legend,
|
||||||
|
)
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_predictions(
|
||||||
|
predictions: Iterable[data.SoundEventPrediction],
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
position: Positions = "top-right",
|
||||||
|
color_mapper: Optional[TagColorMapper] = None,
|
||||||
|
time_offset: float = 0.001,
|
||||||
|
freq_offset: float = 1000,
|
||||||
|
legend: bool = True,
|
||||||
|
max_alpha: float = 0.5,
|
||||||
|
color: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Plot an prediction."""
|
||||||
|
if ax is None:
|
||||||
|
ax = create_axes(**kwargs)
|
||||||
|
|
||||||
|
if color_mapper is None:
|
||||||
|
color_mapper = TagColorMapper()
|
||||||
|
|
||||||
|
for prediction in predictions:
|
||||||
|
ax = plot_prediction(
|
||||||
|
prediction,
|
||||||
|
ax=ax,
|
||||||
|
position=position,
|
||||||
|
color_mapper=color_mapper,
|
||||||
|
time_offset=time_offset,
|
||||||
|
freq_offset=freq_offset,
|
||||||
|
max_alpha=max_alpha,
|
||||||
|
color=color,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if legend:
|
||||||
|
ax = add_tags_legend(ax, color_mapper)
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_prediction(
|
||||||
|
prediction: data.SoundEventPrediction,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
position: Positions = "top-right",
|
||||||
|
color_mapper: Optional[TagColorMapper] = None,
|
||||||
|
time_offset: float = 0.001,
|
||||||
|
freq_offset: float = 1000,
|
||||||
|
max_alpha: float = 0.5,
|
||||||
|
alpha: Optional[float] = None,
|
||||||
|
color: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Axes:
|
||||||
|
"""Plot an annotation."""
|
||||||
|
geometry = prediction.sound_event.geometry
|
||||||
|
|
||||||
|
if geometry is None:
|
||||||
|
raise ValueError("Annotation does not have a geometry.")
|
||||||
|
|
||||||
|
if ax is None:
|
||||||
|
ax = create_axes(**kwargs)
|
||||||
|
|
||||||
|
if color_mapper is None:
|
||||||
|
color_mapper = TagColorMapper()
|
||||||
|
|
||||||
|
if alpha is None:
|
||||||
|
alpha = min(prediction.score * max_alpha, 1)
|
||||||
|
|
||||||
|
ax = plot_geometry(
|
||||||
|
geometry,
|
||||||
|
ax=ax,
|
||||||
|
color=color,
|
||||||
|
alpha=alpha,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
x, y = get_geometry_point(geometry, position=position)
|
||||||
|
|
||||||
|
for index, tag in enumerate(prediction.tags):
|
||||||
|
color = color_mapper.get_color(tag.tag)
|
||||||
|
ax = plot_tag(
|
||||||
|
time=x + time_offset,
|
||||||
|
frequency=y - index * freq_offset,
|
||||||
|
color=color,
|
||||||
|
ax=ax,
|
||||||
|
alpha=min(tag.score, prediction.score),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
44
src/batdetect2/plotting/clips.py
Normal file
44
src/batdetect2/plotting/clips.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.axes import Axes
|
||||||
|
from soundevent import data
|
||||||
|
|
||||||
|
from batdetect2.preprocess import (
|
||||||
|
PreprocessorProtocol,
|
||||||
|
get_default_preprocessor,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"plot_clip",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def plot_clip(
|
||||||
|
clip: data.Clip,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
spec_cmap: str = "gray",
|
||||||
|
) -> Axes:
|
||||||
|
if ax is None:
|
||||||
|
_, ax = plt.subplots(figsize=figsize)
|
||||||
|
|
||||||
|
if preprocessor is None:
|
||||||
|
preprocessor = get_default_preprocessor()
|
||||||
|
|
||||||
|
spec = preprocessor.preprocess_clip(clip, audio_dir=audio_dir)
|
||||||
|
|
||||||
|
spec.plot( # type: ignore
|
||||||
|
ax=ax,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
cmap=spec_cmap,
|
||||||
|
add_labels=add_labels,
|
||||||
|
vmin=spec.min().item(),
|
||||||
|
vmax=spec.max().item(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
317
src/batdetect2/plotting/legacy/plot.py
Normal file
317
src/batdetect2/plotting/legacy/plot.py
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
"""Plot functions to visualize detections and spectrograms."""
|
||||||
|
|
||||||
|
from typing import List, Optional, Tuple, Union, cast
|
||||||
|
|
||||||
|
import matplotlib.ticker as tick
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from matplotlib import axes, patches
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from batdetect2.detector.parameters import DEFAULT_PROCESSING_CONFIGURATIONS
|
||||||
|
from batdetect2.types import (
|
||||||
|
Annotation,
|
||||||
|
ProcessingConfiguration,
|
||||||
|
SpectrogramParameters,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"spectrogram_with_detections",
|
||||||
|
"detection",
|
||||||
|
"detections",
|
||||||
|
"spectrogram",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def spectrogram(
|
||||||
|
spec: Union[torch.Tensor, np.ndarray],
|
||||||
|
config: Optional[ProcessingConfiguration] = None,
|
||||||
|
ax: Optional[axes.Axes] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
cmap: str = "plasma",
|
||||||
|
start_time: float = 0,
|
||||||
|
) -> axes.Axes:
|
||||||
|
"""Plot a spectrogram.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
spec (Union[torch.Tensor, np.ndarray]): Spectrogram to plot.
|
||||||
|
config (Optional[ProcessingConfiguration], optional): Configuration
|
||||||
|
used to compute the spectrogram. Defaults to None. If None,
|
||||||
|
the default configuration will be used.
|
||||||
|
ax (Optional[axes.Axes], optional): Matplotlib axes object.
|
||||||
|
Defaults to None. if provided, the spectrogram will be plotted
|
||||||
|
on this axes.
|
||||||
|
figsize (Optional[Tuple[int, int]], optional): Figure size.
|
||||||
|
Defaults to None. If `ax` is None, this will be used to create
|
||||||
|
a new figure of the given size.
|
||||||
|
cmap (str, optional): Colormap to use. Defaults to "plasma".
|
||||||
|
start_time (float, optional): Start time of the spectrogram.
|
||||||
|
Defaults to 0. This is useful if plotting a spectrogram
|
||||||
|
of a segment of a longer audio file.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
axes.Axes: Matplotlib axes object.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError: If the spectrogram is not of
|
||||||
|
shape (1, T, F), (1, 1, T, F) or (T, F)
|
||||||
|
"""
|
||||||
|
# Convert to numpy array if needed
|
||||||
|
if isinstance(spec, torch.Tensor):
|
||||||
|
spec = spec.detach().cpu().numpy()
|
||||||
|
|
||||||
|
# Remove batch and channel dimensions if present
|
||||||
|
spec = spec.squeeze()
|
||||||
|
|
||||||
|
if spec.ndim != 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"Expected a 2D tensor, got {spec.ndim}D tensor instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get config
|
||||||
|
if config is None:
|
||||||
|
config = DEFAULT_PROCESSING_CONFIGURATIONS.copy()
|
||||||
|
|
||||||
|
# Frequency axis is reversed
|
||||||
|
spec = spec[::-1, :]
|
||||||
|
|
||||||
|
if ax is None:
|
||||||
|
# Using cast to fix typing. pyplot subplots is not
|
||||||
|
# correctly typed.
|
||||||
|
ax = cast(axes.Axes, plt.subplots(figsize=figsize)[1])
|
||||||
|
|
||||||
|
# compute extent
|
||||||
|
extent = _compute_spec_extent(spec.shape, config)
|
||||||
|
|
||||||
|
# add start time
|
||||||
|
extent = (extent[0] + start_time, extent[1] + start_time, *extent[2:])
|
||||||
|
|
||||||
|
ax.imshow(spec, aspect="auto", origin="lower", cmap=cmap, extent=extent)
|
||||||
|
|
||||||
|
ax.set_xlabel("Time (s)")
|
||||||
|
ax.set_ylabel("Frequency (kHz)")
|
||||||
|
|
||||||
|
def y_fmt(x, _):
|
||||||
|
return f"{x / 1000:.0f}"
|
||||||
|
|
||||||
|
ax.yaxis.set_major_formatter(tick.FuncFormatter(y_fmt))
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def spectrogram_with_detections(
|
||||||
|
spec: Union[torch.Tensor, np.ndarray],
|
||||||
|
dets: List[Annotation],
|
||||||
|
config: Optional[ProcessingConfiguration] = None,
|
||||||
|
ax: Optional[axes.Axes] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
cmap: str = "plasma",
|
||||||
|
with_names: bool = True,
|
||||||
|
start_time: float = 0,
|
||||||
|
**kwargs,
|
||||||
|
) -> axes.Axes:
|
||||||
|
"""Plot a spectrogram with detections.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
spec (Union[torch.Tensor, np.ndarray]): Spectrogram to plot.
|
||||||
|
detections (List[Annotation]): List of detections.
|
||||||
|
config (Optional[ProcessingConfiguration], optional): Configuration
|
||||||
|
used to compute the spectrogram. Defaults to None. If None,
|
||||||
|
the default configuration will be used.
|
||||||
|
ax (Optional[axes.Axes], optional): Matplotlib axes object.
|
||||||
|
Defaults to None. if provided, the spectrogram will be plotted
|
||||||
|
on this axes.
|
||||||
|
figsize (Optional[Tuple[int, int]], optional): Figure size.
|
||||||
|
Defaults to None. If `ax` is None, this will be used to create
|
||||||
|
a new figure of the given size.
|
||||||
|
cmap (str, optional): Colormap to use. Defaults to "plasma".
|
||||||
|
with_names (bool, optional): Whether to plot the name of the
|
||||||
|
predicted class next to the detection. Defaults to True.
|
||||||
|
start_time (float, optional): Start time of the spectrogram.
|
||||||
|
Defaults to 0. This is useful if plotting a spectrogram
|
||||||
|
of a segment of a longer audio file.
|
||||||
|
**kwargs: Additional keyword arguments to pass to the
|
||||||
|
`plot.detections` function.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
axes.Axes: Matplotlib axes object.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError: If the spectrogram is not of shape (1, F, T),
|
||||||
|
(1, 1, F, T) or (F, T).
|
||||||
|
"""
|
||||||
|
ax = spectrogram(
|
||||||
|
spec,
|
||||||
|
start_time=start_time,
|
||||||
|
config=config,
|
||||||
|
cmap=cmap,
|
||||||
|
ax=ax,
|
||||||
|
figsize=figsize,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax = detections(
|
||||||
|
dets,
|
||||||
|
ax=ax,
|
||||||
|
figsize=figsize,
|
||||||
|
with_names=with_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def detections(
|
||||||
|
dets: List[Annotation],
|
||||||
|
ax: Optional[axes.Axes] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
with_names: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
) -> axes.Axes:
|
||||||
|
"""Plot a list of detections.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dets (List[Annotation]): List of detections.
|
||||||
|
ax (Optional[axes.Axes], optional): Matplotlib axes object.
|
||||||
|
Defaults to None. if provided, the spectrogram will be plotted
|
||||||
|
on this axes.
|
||||||
|
figsize (Optional[Tuple[int, int]], optional): Figure size.
|
||||||
|
Defaults to None. If `ax` is None, this will be used to create
|
||||||
|
a new figure of the given size.
|
||||||
|
with_names (bool, optional): Whether to plot the name of the
|
||||||
|
predicted class next to the detection. Defaults to True.
|
||||||
|
**kwargs: Additional keyword arguments to pass to the
|
||||||
|
`plot.detection` function.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
axes.Axes: Matplotlib axes object on which the detections
|
||||||
|
were plotted.
|
||||||
|
"""
|
||||||
|
if ax is None:
|
||||||
|
# Using cast to fix typing. pyplot subplots is not
|
||||||
|
# correctly typed.
|
||||||
|
ax = cast(axes.Axes, plt.subplots(figsize=figsize)[1])
|
||||||
|
|
||||||
|
for det in dets:
|
||||||
|
ax = detection(
|
||||||
|
det,
|
||||||
|
ax=ax,
|
||||||
|
figsize=figsize,
|
||||||
|
with_name=with_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def detection(
|
||||||
|
det: Annotation,
|
||||||
|
ax: Optional[axes.Axes] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
linewidth: float = 1,
|
||||||
|
edgecolor: str = "w",
|
||||||
|
facecolor: str = "none",
|
||||||
|
with_name: bool = True,
|
||||||
|
) -> axes.Axes:
|
||||||
|
"""Plot a single detection.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
det (Annotation): Detection to plot.
|
||||||
|
ax (Optional[axes.Axes], optional): Matplotlib axes object. Defaults
|
||||||
|
to None. If provided, the spectrogram will be plotted on this axes.
|
||||||
|
figsize (Optional[Tuple[int, int]], optional): Figure size. Defaults
|
||||||
|
to None. If `ax` is None, this will be used to create a new figure
|
||||||
|
of the given size.
|
||||||
|
linewidth (float, optional): Line width of the detection.
|
||||||
|
Defaults to 1.
|
||||||
|
edgecolor (str, optional): Edge color of the detection.
|
||||||
|
Defaults to "w", i.e. white.
|
||||||
|
facecolor (str, optional): Face color of the detection.
|
||||||
|
Defaults to "none", i.e. transparent.
|
||||||
|
with_name (bool, optional): Whether to plot the name of the
|
||||||
|
predicted class next to the detection. Defaults to True.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
axes.Axes: Matplotlib axes object on which the detection
|
||||||
|
was plotted.
|
||||||
|
"""
|
||||||
|
if ax is None:
|
||||||
|
# Using cast to fix typing. pyplot subplots is not
|
||||||
|
# correctly typed.
|
||||||
|
ax = cast(axes.Axes, plt.subplots(figsize=figsize)[1])
|
||||||
|
|
||||||
|
# Plot detection
|
||||||
|
rect = patches.Rectangle(
|
||||||
|
(det["start_time"], det["low_freq"]),
|
||||||
|
det["end_time"] - det["start_time"],
|
||||||
|
det["high_freq"] - det["low_freq"],
|
||||||
|
linewidth=linewidth,
|
||||||
|
edgecolor=edgecolor,
|
||||||
|
facecolor=facecolor,
|
||||||
|
alpha=det.get("det_prob", 1),
|
||||||
|
)
|
||||||
|
ax.add_patch(rect)
|
||||||
|
|
||||||
|
if with_name:
|
||||||
|
# Add class label
|
||||||
|
txt = " ".join([sp[:3] for sp in det["class"].split(" ")])
|
||||||
|
font_info = {
|
||||||
|
"color": edgecolor,
|
||||||
|
"size": 10,
|
||||||
|
"weight": "bold",
|
||||||
|
"alpha": rect.get_alpha(),
|
||||||
|
}
|
||||||
|
y_pos = rect.get_xy()[1] + rect.get_height()
|
||||||
|
ax.text(rect.get_xy()[0], y_pos, txt, fontdict=font_info)
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_spec_extent(
|
||||||
|
shape: Tuple[int, int],
|
||||||
|
params: SpectrogramParameters,
|
||||||
|
) -> Tuple[float, float, float, float]:
|
||||||
|
"""Compute the extent of a spectrogram.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
shape (Tuple[int, int]): Shape of the spectrogram.
|
||||||
|
The first dimension is the frequency axis and the second
|
||||||
|
dimension is the time axis.
|
||||||
|
params (SpectrogramParameters): Spectrogram parameters.
|
||||||
|
Should be the same as the ones used to compute the spectrogram.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[float, float, float, float]: Extent of the spectrogram.
|
||||||
|
The first two values are the minimum and maximum time values,
|
||||||
|
the last two values are the minimum and maximum frequency values.
|
||||||
|
"""
|
||||||
|
fft_win_length = params["fft_win_length"]
|
||||||
|
fft_overlap = params["fft_overlap"]
|
||||||
|
max_freq = params["max_freq"]
|
||||||
|
min_freq = params["min_freq"]
|
||||||
|
|
||||||
|
# compute duration based on spectrogram parameters
|
||||||
|
duration = (shape[1] + 1) * (fft_win_length * (1 - fft_overlap))
|
||||||
|
|
||||||
|
# If the spectrogram is not resized, the duration is correct
|
||||||
|
# but if it is resized, the duration needs to be adjusted
|
||||||
|
# NOTE: For now we can only detect if the spectrogram is resized
|
||||||
|
# by checking if the height is equal to the specified height,
|
||||||
|
# but this could fail.
|
||||||
|
resize_factor = params["resize_factor"]
|
||||||
|
spec_height = params["spec_height"]
|
||||||
|
if spec_height * resize_factor == shape[0]:
|
||||||
|
duration = duration / resize_factor
|
||||||
|
|
||||||
|
return 0, duration, min_freq, max_freq
|
||||||
417
src/batdetect2/plotting/matches.py
Normal file
417
src/batdetect2/plotting/matches.py
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.axes import Axes
|
||||||
|
from soundevent import data, plot
|
||||||
|
from soundevent.geometry import compute_bounds
|
||||||
|
from soundevent.plot.tags import TagColorMapper
|
||||||
|
|
||||||
|
from batdetect2.evaluate.types import MatchEvaluation
|
||||||
|
from batdetect2.plotting.clip_predictions import plot_prediction
|
||||||
|
from batdetect2.plotting.clips import plot_clip
|
||||||
|
from batdetect2.preprocess import (
|
||||||
|
PreprocessorProtocol,
|
||||||
|
get_default_preprocessor,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"plot_matches",
|
||||||
|
"plot_false_positive_match",
|
||||||
|
"plot_true_positive_match",
|
||||||
|
"plot_false_negative_match",
|
||||||
|
"plot_cross_trigger_match",
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_FALSE_POSITIVE_COLOR = "orange"
|
||||||
|
DEFAULT_FALSE_NEGATIVE_COLOR = "red"
|
||||||
|
DEFAULT_TRUE_POSITIVE_COLOR = "green"
|
||||||
|
DEFAULT_CROSS_TRIGGER_COLOR = "orange"
|
||||||
|
DEFAULT_ANNOTATION_LINE_STYLE = "-"
|
||||||
|
DEFAULT_PREDICTION_LINE_STYLE = "--"
|
||||||
|
|
||||||
|
|
||||||
|
def plot_matches(
|
||||||
|
matches: List[data.Match],
|
||||||
|
clip: data.Clip,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
color_mapper: Optional[TagColorMapper] = None,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
add_points: bool = False,
|
||||||
|
fill: bool = False,
|
||||||
|
spec_cmap: str = "gray",
|
||||||
|
false_positive_color: str = DEFAULT_FALSE_POSITIVE_COLOR,
|
||||||
|
false_negative_color: str = DEFAULT_FALSE_NEGATIVE_COLOR,
|
||||||
|
true_positive_color: str = DEFAULT_TRUE_POSITIVE_COLOR,
|
||||||
|
annotation_linestyle: str = DEFAULT_ANNOTATION_LINE_STYLE,
|
||||||
|
prediction_linestyle: str = DEFAULT_PREDICTION_LINE_STYLE,
|
||||||
|
) -> Axes:
|
||||||
|
if preprocessor is None:
|
||||||
|
preprocessor = get_default_preprocessor()
|
||||||
|
|
||||||
|
ax = plot_clip(
|
||||||
|
clip,
|
||||||
|
ax=ax,
|
||||||
|
figsize=figsize,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
add_labels=add_labels,
|
||||||
|
spec_cmap=spec_cmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
if color_mapper is None:
|
||||||
|
color_mapper = TagColorMapper()
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
if match.source is None and match.target is not None:
|
||||||
|
plot.plot_annotation(
|
||||||
|
annotation=match.target,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.004,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
color=false_negative_color,
|
||||||
|
color_mapper=color_mapper,
|
||||||
|
linestyle=annotation_linestyle,
|
||||||
|
)
|
||||||
|
elif match.target is None and match.source is not None:
|
||||||
|
plot_prediction(
|
||||||
|
prediction=match.source,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.004,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
color=false_positive_color,
|
||||||
|
color_mapper=color_mapper,
|
||||||
|
linestyle=prediction_linestyle,
|
||||||
|
)
|
||||||
|
elif match.target is not None and match.source is not None:
|
||||||
|
plot.plot_annotation(
|
||||||
|
annotation=match.target,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.004,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
color=true_positive_color,
|
||||||
|
color_mapper=color_mapper,
|
||||||
|
linestyle=annotation_linestyle,
|
||||||
|
)
|
||||||
|
plot_prediction(
|
||||||
|
prediction=match.source,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.004,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
color=true_positive_color,
|
||||||
|
color_mapper=color_mapper,
|
||||||
|
linestyle=prediction_linestyle,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_DURATION = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
def plot_false_positive_match(
|
||||||
|
match: MatchEvaluation,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
duration: float = DEFAULT_DURATION,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
add_points: bool = False,
|
||||||
|
fill: bool = False,
|
||||||
|
spec_cmap: str = "gray",
|
||||||
|
time_offset: float = 0,
|
||||||
|
color: str = DEFAULT_FALSE_POSITIVE_COLOR,
|
||||||
|
fontsize: Union[float, str] = "small",
|
||||||
|
) -> Axes:
|
||||||
|
assert match.match.source is not None
|
||||||
|
assert match.match.target is None
|
||||||
|
sound_event = match.match.source.sound_event
|
||||||
|
geometry = sound_event.geometry
|
||||||
|
assert geometry is not None
|
||||||
|
|
||||||
|
start_time, _, _, high_freq = compute_bounds(geometry)
|
||||||
|
|
||||||
|
clip = data.Clip(
|
||||||
|
start_time=max(start_time - duration / 2, 0),
|
||||||
|
end_time=min(
|
||||||
|
start_time + duration / 2, sound_event.recording.duration
|
||||||
|
),
|
||||||
|
recording=sound_event.recording,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax = plot_clip(
|
||||||
|
clip,
|
||||||
|
preprocessor=preprocessor,
|
||||||
|
figsize=figsize,
|
||||||
|
ax=ax,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
add_labels=add_labels,
|
||||||
|
spec_cmap=spec_cmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot_prediction(
|
||||||
|
match.match.source,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=time_offset,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
alpha=1,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.text(
|
||||||
|
start_time,
|
||||||
|
high_freq,
|
||||||
|
f"False Positive \nScore: {match.pred_score} \nTop Class: {match.pred_class} \nTop Class Score: {match.pred_class_score} ",
|
||||||
|
va="top",
|
||||||
|
ha="right",
|
||||||
|
color=color,
|
||||||
|
fontsize=fontsize,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_false_negative_match(
|
||||||
|
match: MatchEvaluation,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
duration: float = DEFAULT_DURATION,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
add_points: bool = False,
|
||||||
|
fill: bool = False,
|
||||||
|
spec_cmap: str = "gray",
|
||||||
|
color: str = DEFAULT_FALSE_NEGATIVE_COLOR,
|
||||||
|
fontsize: Union[float, str] = "small",
|
||||||
|
) -> Axes:
|
||||||
|
assert match.match.source is None
|
||||||
|
assert match.match.target is not None
|
||||||
|
sound_event = match.match.target.sound_event
|
||||||
|
geometry = sound_event.geometry
|
||||||
|
assert geometry is not None
|
||||||
|
|
||||||
|
start_time, _, _, high_freq = compute_bounds(geometry)
|
||||||
|
|
||||||
|
clip = data.Clip(
|
||||||
|
start_time=max(start_time - duration / 2, 0),
|
||||||
|
end_time=min(
|
||||||
|
start_time + duration / 2, sound_event.recording.duration
|
||||||
|
),
|
||||||
|
recording=sound_event.recording,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax = plot_clip(
|
||||||
|
clip,
|
||||||
|
preprocessor=preprocessor,
|
||||||
|
figsize=figsize,
|
||||||
|
ax=ax,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
add_labels=add_labels,
|
||||||
|
spec_cmap=spec_cmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot.plot_annotation(
|
||||||
|
match.match.target,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.001,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
alpha=1,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.text(
|
||||||
|
start_time,
|
||||||
|
high_freq,
|
||||||
|
f"False Negative \nClass: {match.gt_class} ",
|
||||||
|
va="top",
|
||||||
|
ha="right",
|
||||||
|
color=color,
|
||||||
|
fontsize=fontsize,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_true_positive_match(
|
||||||
|
match: MatchEvaluation,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
duration: float = DEFAULT_DURATION,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
add_points: bool = False,
|
||||||
|
fill: bool = False,
|
||||||
|
spec_cmap: str = "gray",
|
||||||
|
color: str = DEFAULT_TRUE_POSITIVE_COLOR,
|
||||||
|
fontsize: Union[float, str] = "small",
|
||||||
|
annotation_linestyle: str = DEFAULT_ANNOTATION_LINE_STYLE,
|
||||||
|
prediction_linestyle: str = DEFAULT_PREDICTION_LINE_STYLE,
|
||||||
|
) -> Axes:
|
||||||
|
assert match.match.source is not None
|
||||||
|
assert match.match.target is not None
|
||||||
|
sound_event = match.match.target.sound_event
|
||||||
|
geometry = sound_event.geometry
|
||||||
|
assert geometry is not None
|
||||||
|
|
||||||
|
start_time, _, _, high_freq = compute_bounds(geometry)
|
||||||
|
|
||||||
|
clip = data.Clip(
|
||||||
|
start_time=max(start_time - duration / 2, 0),
|
||||||
|
end_time=min(
|
||||||
|
start_time + duration / 2, sound_event.recording.duration
|
||||||
|
),
|
||||||
|
recording=sound_event.recording,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax = plot_clip(
|
||||||
|
clip,
|
||||||
|
preprocessor=preprocessor,
|
||||||
|
figsize=figsize,
|
||||||
|
ax=ax,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
add_labels=add_labels,
|
||||||
|
spec_cmap=spec_cmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot.plot_annotation(
|
||||||
|
match.match.target,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.001,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
alpha=1,
|
||||||
|
color=color,
|
||||||
|
linestyle=annotation_linestyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot_prediction(
|
||||||
|
match.match.source,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.001,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
alpha=1,
|
||||||
|
color=color,
|
||||||
|
linestyle=prediction_linestyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.text(
|
||||||
|
start_time,
|
||||||
|
high_freq,
|
||||||
|
f"True Positive \nClass: {match.gt_class} \nDet Score: {match.pred_score} \nTop Class Score: {match.pred_class_score} ",
|
||||||
|
va="top",
|
||||||
|
ha="right",
|
||||||
|
color=color,
|
||||||
|
fontsize=fontsize,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_cross_trigger_match(
|
||||||
|
match: MatchEvaluation,
|
||||||
|
preprocessor: Optional[PreprocessorProtocol] = None,
|
||||||
|
figsize: Optional[Tuple[int, int]] = None,
|
||||||
|
ax: Optional[Axes] = None,
|
||||||
|
audio_dir: Optional[data.PathLike] = None,
|
||||||
|
duration: float = DEFAULT_DURATION,
|
||||||
|
add_colorbar: bool = False,
|
||||||
|
add_labels: bool = False,
|
||||||
|
add_points: bool = False,
|
||||||
|
fill: bool = False,
|
||||||
|
spec_cmap: str = "gray",
|
||||||
|
color: str = DEFAULT_CROSS_TRIGGER_COLOR,
|
||||||
|
fontsize: Union[float, str] = "small",
|
||||||
|
annotation_linestyle: str = DEFAULT_ANNOTATION_LINE_STYLE,
|
||||||
|
prediction_linestyle: str = DEFAULT_PREDICTION_LINE_STYLE,
|
||||||
|
) -> Axes:
|
||||||
|
assert match.match.source is not None
|
||||||
|
assert match.match.target is not None
|
||||||
|
sound_event = match.match.source.sound_event
|
||||||
|
geometry = sound_event.geometry
|
||||||
|
assert geometry is not None
|
||||||
|
|
||||||
|
start_time, _, _, high_freq = compute_bounds(geometry)
|
||||||
|
|
||||||
|
clip = data.Clip(
|
||||||
|
start_time=max(start_time - duration / 2, 0),
|
||||||
|
end_time=min(
|
||||||
|
start_time + duration / 2, sound_event.recording.duration
|
||||||
|
),
|
||||||
|
recording=sound_event.recording,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax = plot_clip(
|
||||||
|
clip,
|
||||||
|
preprocessor=preprocessor,
|
||||||
|
figsize=figsize,
|
||||||
|
ax=ax,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
add_colorbar=add_colorbar,
|
||||||
|
add_labels=add_labels,
|
||||||
|
spec_cmap=spec_cmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot.plot_annotation(
|
||||||
|
match.match.target,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.001,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
alpha=1,
|
||||||
|
color=color,
|
||||||
|
linestyle=annotation_linestyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
plot_prediction(
|
||||||
|
match.match.source,
|
||||||
|
ax=ax,
|
||||||
|
time_offset=0.001,
|
||||||
|
freq_offset=2_000,
|
||||||
|
add_points=add_points,
|
||||||
|
facecolor="none" if not fill else None,
|
||||||
|
alpha=1,
|
||||||
|
color=color,
|
||||||
|
linestyle=prediction_linestyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.text(
|
||||||
|
start_time,
|
||||||
|
high_freq,
|
||||||
|
f"Cross Trigger \nTrue Class: {match.gt_class} \nPred Class: {match.pred_class} \nDet Score: {match.pred_score} \nTop Class Score: {match.pred_class_score} ",
|
||||||
|
va="top",
|
||||||
|
ha="right",
|
||||||
|
color=color,
|
||||||
|
fontsize=fontsize,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ax
|
||||||
Loading…
Reference in New Issue
Block a user