Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions birdnet_analyzer/analyze/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,9 @@ def _merge_consecutive_segments(
Input predictions containing at least "input", "species_name", "start_time",
"end_time" and "confidence".
merge_consecutive : int
Number of consecutive rows that must be contiguous to merge them.
Maximum number of consecutive contiguous rows to collapse into a single
segment. Runs longer than this are split into chunks of at most
``merge_consecutive`` rows; runs shorter than it are still merged.
hop_size : float, optional
Allowed tolerance (in seconds) between the end of one segment and the start of
the next to consider them consecutive, by default 3.0.
Expand Down Expand Up @@ -445,7 +447,8 @@ def _merge_consecutive_segments(
else:
break

if len(window) == merge_consecutive:
# Merge whatever consecutive rows we collected (1 up to merge_consecutive).
if len(window) > 1:
merged_row = window[0].copy()
merged_row["start_time"] = window[0]["start_time"]
merged_row["end_time"] = window[-1]["end_time"]
Expand All @@ -461,10 +464,10 @@ def _merge_consecutive_segments(
merged_row["confidence"] = avg_confidence

merged_records.append(merged_row.to_dict())
i += merge_consecutive
else:
merged_records.append(window[0].to_dict())
i += 1

i += len(window)

merged_df = pd.DataFrame.from_records(merged_records, columns=df.columns)
return merged_df.sort_values(by=["input", "start_time", "end_time", "species_name"])
Expand Down
2 changes: 1 addition & 1 deletion birdnet_analyzer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ def __call__(self, parser, namespace, values, option_string=None):
"--merge_consecutive",
type=int,
default=1,
help="Maximum number of consecutive detections above MIN_CONF to merge for each detected species. This will result in fewer entires in the result file with segments longer than 3 seconds. Set to 0 or 1 to disable merging. Set to None to include all consecutive detections. We use the mean of the top 3 scores from all consecutive detections for merging.",
help="Maximum number of consecutive detections above the threshold to merge for each detected species. This will result in fewer entries in the result file with segments longer than 3 seconds. Set to 0 or 1 to disable merging. We use the mean of the top 3 scores from all consecutive detections for merging.",
)
Comment on lines 500 to 504
parser.add_argument(
"--use_perch",
Expand Down
63 changes: 62 additions & 1 deletion tests/analyze/test_core_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_merge_consecutive_segments_merges_expected_rows():
assert merged_records[2]["confidence"] == pytest.approx(0.6)


def test_merge_consecutive_segments_requires_full_window():
def test_merge_consecutive_segments_merges_full_window():
df = pd.DataFrame(
[
{
Expand Down Expand Up @@ -103,6 +103,67 @@ def test_merge_consecutive_segments_requires_full_window():
assert record["confidence"] == pytest.approx((0.25 + 0.75 + 0.5) / 3)


def test_merge_consecutive_segments_merges_up_to_the_window_size():
# A run of 5 consecutive segments with merge_consecutive=3 must be split into
# chunks of at most 3: one merged row of 3 followed by one merged row of 2.
df = pd.DataFrame(
[
{
"input": "file.wav",
"species_name": "species",
"start_time": float(i),
"end_time": float(i + 1),
"confidence": 0.1 * (i + 1),
}
for i in range(5)
]
)

merged = _merge_consecutive_segments(df, merge_consecutive=3, hop_size=1.0)

assert len(merged) == 2

records = merged.to_dict("records")

assert records[0]["start_time"] == 0.0
assert records[0]["end_time"] == 3.0
assert records[0]["confidence"] == pytest.approx((0.1 + 0.2 + 0.3) / 3)

assert records[1]["start_time"] == 3.0
assert records[1]["end_time"] == 5.0
assert records[1]["confidence"] == pytest.approx((0.4 + 0.5) / 2)


def test_merge_consecutive_segments_merges_partial_run_shorter_than_window():
# A run shorter than merge_consecutive must still be merged (up to N).
df = pd.DataFrame(
[
{
"input": "file.wav",
"species_name": "species",
"start_time": 0.0,
"end_time": 1.0,
"confidence": 0.25,
},
{
"input": "file.wav",
"species_name": "species",
"start_time": 1.0,
"end_time": 2.0,
"confidence": 0.75,
},
]
)
merged = _merge_consecutive_segments(df, merge_consecutive=3, hop_size=1.0)

assert len(merged) == 1

record = merged.to_dict("records")[0]
assert record["start_time"] == 0.0
assert record["end_time"] == 2.0
assert record["confidence"] == pytest.approx((0.25 + 0.75) / 2)


def test_merge_consecutive_segments_handles_float16_time_columns():
df = pd.DataFrame(
[
Expand Down
Loading