AGC1 add clipping predictor evaluator

Observes clipping predictions and detections and computes evaluation
metrics for the predictor.

Bug: webrtc:12774
Change-Id: I83f5942a3b6491de288510f2200f2f5c0e099bf2
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/221619
Reviewed-by: Hanna Silen <silen@webrtc.org>
Commit-Queue: Alessio Bazzica <alessiob@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#34305}
This commit is contained in:
Alessio Bazzica 2021-06-16 14:55:40 +02:00 committed by WebRTC LUCI CQ
parent 808f49428f
commit 5b9d0c70c2
4 changed files with 863 additions and 0 deletions

View File

@ -58,6 +58,18 @@ rtc_library("clipping_predictor") {
absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
}
rtc_library("clipping_predictor_evaluator") {
sources = [
"clipping_predictor_evaluator.cc",
"clipping_predictor_evaluator.h",
]
deps = [
"../../../rtc_base:checks",
"../../../rtc_base:logging",
]
absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
}
rtc_library("clipping_predictor_level_buffer") {
sources = [
"clipping_predictor_level_buffer.cc",
@ -66,6 +78,7 @@ rtc_library("clipping_predictor_level_buffer") {
deps = [
"../../../rtc_base:checks",
"../../../rtc_base:logging",
"../../../rtc_base:rtc_base_approved",
]
absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
}
@ -128,6 +141,7 @@ if (rtc_include_tests) {
testonly = true
sources = [
"agc_manager_direct_unittest.cc",
"clipping_predictor_evaluator_unittest.cc",
"clipping_predictor_level_buffer_unittest.cc",
"clipping_predictor_unittest.cc",
"loudness_histogram_unittest.cc",
@ -138,15 +152,19 @@ if (rtc_include_tests) {
deps = [
":agc",
":clipping_predictor",
":clipping_predictor_evaluator",
":clipping_predictor_level_buffer",
":gain_control_interface",
":level_estimation",
"..:mocks",
"../../../rtc_base:checks",
"../../../rtc_base:rtc_base_approved",
"../../../rtc_base:safe_conversions",
"../../../test:field_trial",
"../../../test:fileutils",
"../../../test:test_support",
"//testing/gtest",
]
absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
}
}

View File

@ -0,0 +1,175 @@
/*
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "modules/audio_processing/agc/clipping_predictor_evaluator.h"
#include <algorithm>
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
namespace webrtc {
namespace {
// Returns the index of the oldest item in the ring buffer for a non-empty
// ring buffer with give `size`, `tail` index and `capacity`.
int OldestExpectedDetectionIndex(int size, int tail, int capacity) {
RTC_DCHECK_GT(size, 0);
return tail - size + (tail < size ? capacity : 0);
}
} // namespace
ClippingPredictorEvaluator::ClippingPredictorEvaluator(int history_size)
: history_size_(history_size),
ring_buffer_capacity_(history_size + 1),
ring_buffer_(ring_buffer_capacity_),
true_positives_(0),
true_negatives_(0),
false_positives_(0),
false_negatives_(0) {
RTC_DCHECK_GT(history_size_, 0);
Reset();
}
ClippingPredictorEvaluator::~ClippingPredictorEvaluator() = default;
absl::optional<int> ClippingPredictorEvaluator::Observe(
bool clipping_detected,
bool clipping_predicted) {
RTC_DCHECK_GE(ring_buffer_size_, 0);
RTC_DCHECK_LE(ring_buffer_size_, ring_buffer_capacity_);
RTC_DCHECK_GE(ring_buffer_tail_, 0);
RTC_DCHECK_LT(ring_buffer_tail_, ring_buffer_capacity_);
DecreaseTimesToLive();
if (clipping_predicted) {
// TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
Push(/*expected_detection=*/{/*ttl=*/history_size_, /*detected=*/false});
}
// Clipping is expected if there are expected detections regardless of
// whether all the expected detections have been previously matched - i.e.,
// `ExpectedDetection::detected` is true.
const bool clipping_expected = ring_buffer_size_ > 0;
absl::optional<int> prediction_interval;
if (clipping_expected && clipping_detected) {
prediction_interval = FindEarliestPredictionInterval();
// Add a true positive for each unexpired expected detection.
const int num_modified_items = MarkExpectedDetectionAsDetected();
true_positives_ += num_modified_items;
RTC_DCHECK(prediction_interval.has_value() || num_modified_items == 0);
RTC_DCHECK(!prediction_interval.has_value() || num_modified_items > 0);
} else if (clipping_expected && !clipping_detected) {
// Add a false positive if there is one expected detection that has expired
// and that has never been matched before. Note that there is at most one
// unmatched expired detection.
if (HasExpiredUnmatchedExpectedDetection()) {
false_positives_++;
}
} else if (!clipping_expected && clipping_detected) {
false_negatives_++;
} else {
RTC_DCHECK(!clipping_expected && !clipping_detected);
true_negatives_++;
}
return prediction_interval;
}
void ClippingPredictorEvaluator::Reset() {
// Empty the ring buffer of expected detections.
ring_buffer_tail_ = 0;
ring_buffer_size_ = 0;
}
// Cost: O(1).
void ClippingPredictorEvaluator::Push(ExpectedDetection value) {
ring_buffer_[ring_buffer_tail_] = value;
ring_buffer_tail_++;
if (ring_buffer_tail_ == ring_buffer_capacity_) {
ring_buffer_tail_ = 0;
}
ring_buffer_size_ = std::min(ring_buffer_capacity_, ring_buffer_size_ + 1);
}
// Cost: O(N).
void ClippingPredictorEvaluator::DecreaseTimesToLive() {
bool expired_found = false;
for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_;
++i) {
int index = i >= 0 ? i : ring_buffer_capacity_ + i;
RTC_DCHECK_GE(index, 0);
RTC_DCHECK_LT(index, ring_buffer_.size());
RTC_DCHECK_GE(ring_buffer_[index].ttl, 0);
if (ring_buffer_[index].ttl == 0) {
RTC_DCHECK(!expired_found)
<< "There must be at most one expired item in the ring buffer.";
expired_found = true;
RTC_DCHECK_EQ(index, OldestExpectedDetectionIndex(ring_buffer_size_,
ring_buffer_tail_,
ring_buffer_capacity_))
<< "The expired item must be the oldest in the ring buffer.";
}
ring_buffer_[index].ttl--;
}
if (expired_found) {
ring_buffer_size_--;
}
}
// Cost: O(N).
absl::optional<int> ClippingPredictorEvaluator::FindEarliestPredictionInterval()
const {
absl::optional<int> prediction_interval;
for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_;
++i) {
int index = i >= 0 ? i : ring_buffer_capacity_ + i;
RTC_DCHECK_GE(index, 0);
RTC_DCHECK_LT(index, ring_buffer_.size());
if (!ring_buffer_[index].detected) {
prediction_interval = std::max(prediction_interval.value_or(0),
history_size_ - ring_buffer_[index].ttl);
}
}
return prediction_interval;
}
// Cost: O(N).
int ClippingPredictorEvaluator::MarkExpectedDetectionAsDetected() {
int num_modified_items = 0;
for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_;
++i) {
int index = i >= 0 ? i : ring_buffer_capacity_ + i;
RTC_DCHECK_GE(index, 0);
RTC_DCHECK_LT(index, ring_buffer_.size());
if (!ring_buffer_[index].detected) {
num_modified_items++;
}
ring_buffer_[index].detected = true;
}
return num_modified_items;
}
// Cost: O(1).
bool ClippingPredictorEvaluator::HasExpiredUnmatchedExpectedDetection() const {
if (ring_buffer_size_ == 0) {
return false;
}
// If an expired item, that is `ttl` equal to 0, exists, it must be the
// oldest.
const int oldest_index = OldestExpectedDetectionIndex(
ring_buffer_size_, ring_buffer_tail_, ring_buffer_capacity_);
RTC_DCHECK_GE(oldest_index, 0);
RTC_DCHECK_LT(oldest_index, ring_buffer_.size());
return ring_buffer_[oldest_index].ttl == 0 &&
!ring_buffer_[oldest_index].detected;
}
} // namespace webrtc

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#ifndef MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_
#define MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_
#include <vector>
#include "absl/types/optional.h"
namespace webrtc {
// Counts true/false positives/negatives while observing sequences of flag pairs
// that indicate whether clipping has been detected and/or if clipping is
// predicted. When a true positive is found measures the time interval between
// prediction and detection events.
// From the time a prediction is observed and for a period equal to
// `history_size` calls to `Observe()`, one or more detections are expected. If
// the expectation is met, a true positives is added and the time interval
// between the earliest prediction and the detection is recorded; otherwise,
// when the deadline is reached, a false positive is added. Note that one
// detection matches all the expected detections that have not expired - i.e.,
// one detection counts as multiple true positives.
// If a detection is observed, but no prediction has been observed over the past
// `history_size` calls to `Observe()`, then a false negative is added;
// otherwise, a true negative is added.
class ClippingPredictorEvaluator {
public:
// Ctor. `history_size` indicates how long to wait for a call to `Observe()`
// having `clipping_detected` set to true from the time clipping is predicted.
explicit ClippingPredictorEvaluator(int history_size);
ClippingPredictorEvaluator(const ClippingPredictorEvaluator&) = delete;
ClippingPredictorEvaluator& operator=(const ClippingPredictorEvaluator&) =
delete;
~ClippingPredictorEvaluator();
// Observes whether clipping has been detected and/or if clipping is
// predicted. When predicted one or more detections are expected in the next
// `history_size_` calls of `Observe()`. When true positives are found returns
// the prediction interval between the earliest prediction and the detection.
absl::optional<int> Observe(bool clipping_detected, bool clipping_predicted);
// Removes any expectation recently set after a call to `Observe()` having
// `clipping_predicted` set to true.
void Reset();
// Metrics getters.
int true_positives() const { return true_positives_; }
int true_negatives() const { return true_negatives_; }
int false_positives() const { return false_positives_; }
int false_negatives() const { return false_negatives_; }
private:
const int history_size_;
// State of a detection expected to be observed after a prediction.
struct ExpectedDetection {
// Time to live (TTL); remaining number of `Observe()` calls to match a call
// having `clipping_detected` set to true.
int ttl;
// True if an `Observe()` call having `clipping_detected` set to true has
// been observed.
bool detected;
};
// Ring buffer of expected detections.
const int ring_buffer_capacity_;
std::vector<ExpectedDetection> ring_buffer_;
int ring_buffer_tail_;
int ring_buffer_size_;
// Pushes `expected_detection` into `expected_matches_ring_buffer_`.
void Push(ExpectedDetection expected_detection);
// Decreased the TTLs in `expected_matches_ring_buffer_` and removes expired
// items.
void DecreaseTimesToLive();
// Returns the prediction interval for the earliest unexpired expected
// detection if any.
absl::optional<int> FindEarliestPredictionInterval() const;
// Marks all the items in `expected_matches_ring_buffer_` as `detected` and
// returns the number of updated items.
int MarkExpectedDetectionAsDetected();
// Returns true if `expected_matches_ring_buffer_` has an item having `ttl`
// equal to 0 (expired) and `detected` equal to false (unmatched).
bool HasExpiredUnmatchedExpectedDetection() const;
// Metrics.
int true_positives_;
int true_negatives_;
int false_positives_;
int false_negatives_;
};
} // namespace webrtc
#endif // MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_

View File

@ -0,0 +1,568 @@
/*
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "modules/audio_processing/agc/clipping_predictor_evaluator.h"
#include <cstdint>
#include <memory>
#include <tuple>
#include <vector>
#include "absl/types/optional.h"
#include "rtc_base/numerics/safe_conversions.h"
#include "rtc_base/random.h"
#include "test/gmock.h"
#include "test/gtest.h"
namespace webrtc {
namespace {
using testing::Eq;
using testing::Optional;
constexpr bool kDetected = true;
constexpr bool kNotDetected = false;
constexpr bool kPredicted = true;
constexpr bool kNotPredicted = false;
int SumTrueFalsePositivesNegatives(
const ClippingPredictorEvaluator& evaluator) {
return evaluator.true_positives() + evaluator.true_negatives() +
evaluator.false_positives() + evaluator.false_negatives();
}
// Checks the metrics after init - i.e., no call to `Observe()`.
TEST(ClippingPredictorEvaluatorTest, Init) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
class ClippingPredictorEvaluatorParameterization
: public ::testing::TestWithParam<std::tuple<int, int>> {
protected:
uint64_t seed() const {
return rtc::checked_cast<uint64_t>(std::get<0>(GetParam()));
}
int history_size() const { return std::get<1>(GetParam()); }
};
// Checks that after each call to `Observe()` at most one metric changes.
TEST_P(ClippingPredictorEvaluatorParameterization, AtMostOneMetricChanges) {
constexpr int kNumCalls = 123;
Random random_generator(seed());
ClippingPredictorEvaluator evaluator(history_size());
for (int i = 0; i < kNumCalls; ++i) {
SCOPED_TRACE(i);
// Read metrics before `Observe()` is called.
const int last_tp = evaluator.true_positives();
const int last_tn = evaluator.true_negatives();
const int last_fp = evaluator.false_positives();
const int last_fn = evaluator.false_negatives();
// `Observe()` a random observation.
bool clipping_detected = random_generator.Rand<bool>();
bool clipping_predicted = random_generator.Rand<bool>();
evaluator.Observe(clipping_detected, clipping_predicted);
// Check that at most one metric has changed.
int num_changes = 0;
num_changes += last_tp == evaluator.true_positives() ? 0 : 1;
num_changes += last_tn == evaluator.true_negatives() ? 0 : 1;
num_changes += last_fp == evaluator.false_positives() ? 0 : 1;
num_changes += last_fn == evaluator.false_negatives() ? 0 : 1;
EXPECT_GE(num_changes, 0);
EXPECT_LE(num_changes, 1);
}
}
// Checks that after each call to `Observe()` each metric either remains
// unchanged or grows.
TEST_P(ClippingPredictorEvaluatorParameterization, MetricsAreWeaklyMonotonic) {
constexpr int kNumCalls = 123;
Random random_generator(seed());
ClippingPredictorEvaluator evaluator(history_size());
for (int i = 0; i < kNumCalls; ++i) {
SCOPED_TRACE(i);
// Read metrics before `Observe()` is called.
const int last_tp = evaluator.true_positives();
const int last_tn = evaluator.true_negatives();
const int last_fp = evaluator.false_positives();
const int last_fn = evaluator.false_negatives();
// `Observe()` a random observation.
bool clipping_detected = random_generator.Rand<bool>();
bool clipping_predicted = random_generator.Rand<bool>();
evaluator.Observe(clipping_detected, clipping_predicted);
// Check that metrics are weakly monotonic.
EXPECT_GE(evaluator.true_positives(), last_tp);
EXPECT_GE(evaluator.true_negatives(), last_tn);
EXPECT_GE(evaluator.false_positives(), last_fp);
EXPECT_GE(evaluator.false_negatives(), last_fn);
}
}
// Checks that after each call to `Observe()` the growth speed of each metrics
// is bounded.
TEST_P(ClippingPredictorEvaluatorParameterization, BoundedMetricsGrowth) {
constexpr int kNumCalls = 123;
Random random_generator(seed());
ClippingPredictorEvaluator evaluator(history_size());
for (int i = 0; i < kNumCalls; ++i) {
SCOPED_TRACE(i);
// Read metrics before `Observe()` is called.
const int last_tp = evaluator.true_positives();
const int last_tn = evaluator.true_negatives();
const int last_fp = evaluator.false_positives();
const int last_fn = evaluator.false_negatives();
// `Observe()` a random observation.
bool clipping_detected = random_generator.Rand<bool>();
bool clipping_predicted = random_generator.Rand<bool>();
evaluator.Observe(clipping_detected, clipping_predicted);
// Check that TPs grow by at most `history_size() + 1`. Such an upper bound
// is reached when multiple predictions are matched by a single detection.
EXPECT_LE(evaluator.true_positives() - last_tp, history_size() + 1);
// Check that TNs, FPs and FNs grow by at most one. `max_growth`.
EXPECT_LE(evaluator.true_negatives() - last_tn, 1);
EXPECT_LE(evaluator.false_positives() - last_fp, 1);
EXPECT_LE(evaluator.false_negatives() - last_fn, 1);
}
}
// Checks that `Observe()` returns a prediction interval if and only if one or
// more true positives are found.
TEST_P(ClippingPredictorEvaluatorParameterization,
PredictionIntervalIfAndOnlyIfTruePositives) {
constexpr int kNumCalls = 123;
Random random_generator(seed());
ClippingPredictorEvaluator evaluator(history_size());
for (int i = 0; i < kNumCalls; ++i) {
SCOPED_TRACE(i);
// Read true positives before `Observe()` is called.
const int last_tp = evaluator.true_positives();
// `Observe()` a random observation.
bool clipping_detected = random_generator.Rand<bool>();
bool clipping_predicted = random_generator.Rand<bool>();
absl::optional<int> prediction_interval =
evaluator.Observe(clipping_detected, clipping_predicted);
// Check that the prediction interval is returned when a true positive is
// found.
if (evaluator.true_positives() == last_tp) {
EXPECT_FALSE(prediction_interval.has_value());
} else {
EXPECT_TRUE(prediction_interval.has_value());
}
}
}
INSTANTIATE_TEST_SUITE_P(
ClippingPredictorEvaluatorTest,
ClippingPredictorEvaluatorParameterization,
::testing::Combine(::testing::Values(4, 8, 15, 16, 23, 42),
::testing::Values(1, 10, 21)));
// Checks that, observing a detection and a prediction after init, produces a
// true positive.
TEST(ClippingPredictorEvaluatorTest, OneTruePositiveAfterInit) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kDetected, kPredicted);
EXPECT_EQ(evaluator.true_positives(), 1);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that, observing a detection but no prediction after init, produces a
// false negative.
TEST(ClippingPredictorEvaluatorTest, OneFalseNegativeAfterInit) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_negatives(), 1);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
}
// Checks that, observing no detection but a prediction after init, produces a
// false positive after expiration.
TEST(ClippingPredictorEvaluatorTest, OneFalsePositiveAfterInit) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 1);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that, observing no detection and no prediction after init, produces a
// true negative.
TEST(ClippingPredictorEvaluatorTest, OneTrueNegativeAfterInit) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_negatives(), 1);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that the evaluator detects true negatives when clipping is neither
// predicted nor detected.
TEST(ClippingPredictorEvaluatorTest, NeverDetectedAndNotPredicted) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_negatives(), 4);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that the evaluator detects a false negative when clipping is detected
// but not predicted.
TEST(ClippingPredictorEvaluatorTest, DetectedButNotPredicted) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_negatives(), 1);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.true_negatives(), 3);
EXPECT_EQ(evaluator.false_positives(), 0);
}
// Checks that the evaluator does not detect a false positive when clipping is
// predicted but not detected until the observation period expires.
TEST(ClippingPredictorEvaluatorTest,
PredictedOnceAndNeverDetectedBeforeDeadline) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 1);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that the evaluator detects a false positive when clipping is predicted
// but detected after the observation period expires.
TEST(ClippingPredictorEvaluatorTest, PredictedOnceButDetectedAfterDeadline) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 1);
EXPECT_EQ(evaluator.true_positives(), 0);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 1);
}
// Checks that a prediction followed by a detection counts as true positive.
TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndThenImmediatelyDetected) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_positives(), 1);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that a prediction followed by a delayed detection counts as true
// positive if the delay is within the observation period.
TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedBeforeDeadline) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_positives(), 1);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that a prediction followed by a delayed detection counts as true
// positive if the delay equals the observation period.
TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedAtDeadline) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_positives(), 0);
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_positives(), 1);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that a prediction followed by a multiple adjacent detections within
// the deadline counts as a single true positive and that, after the deadline,
// a detection counts as a false negative.
TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedMultipleTimes) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
// Multiple detections.
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_positives(), 1);
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_positives(), 1);
// A detection outside of the observation period counts as false negative.
evaluator.Observe(kDetected, kNotPredicted);
EXPECT_EQ(evaluator.false_negatives(), 1);
EXPECT_EQ(SumTrueFalsePositivesNegatives(evaluator), 2);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
}
// Checks that a false positive is added when clipping is detected after a too
// early prediction.
TEST(ClippingPredictorEvaluatorTest,
PredictedMultipleTimesAndDetectedOnceAfterDeadline) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted); // ---+
evaluator.Observe(kNotDetected, kPredicted); // |
evaluator.Observe(kNotDetected, kPredicted); // |
evaluator.Observe(kNotDetected, kPredicted); // <--+ Not matched.
// The time to match a detection after the first prediction expired.
EXPECT_EQ(evaluator.false_positives(), 1);
evaluator.Observe(kDetected, kNotPredicted);
// The detection above does not match the first prediction because it happened
// after the deadline of the 1st prediction.
EXPECT_EQ(evaluator.false_positives(), 1);
EXPECT_EQ(evaluator.true_positives(), 3);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that multiple consecutive predictions match the first detection
// observed before the expected detection deadline expires.
TEST(ClippingPredictorEvaluatorTest, PredictedMultipleTimesAndDetectedOnce) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted); // --+
evaluator.Observe(kNotDetected, kPredicted); // | --+
evaluator.Observe(kNotDetected, kPredicted); // | | --+
evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+
EXPECT_EQ(evaluator.true_positives(), 3);
// The following observations do not generate any true negatives as they
// belong to the observation period of the last prediction - for which a
// detection has already been matched.
const int true_negatives = evaluator.true_negatives();
evaluator.Observe(kNotDetected, kNotPredicted);
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_negatives(), true_negatives);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that multiple consecutive predictions match the multiple detections
// observed before the expected detection deadline expires.
TEST(ClippingPredictorEvaluatorTest,
PredictedMultipleTimesAndDetectedMultipleTimes) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted); // --+
evaluator.Observe(kNotDetected, kPredicted); // | --+
evaluator.Observe(kNotDetected, kPredicted); // | | --+
evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+
evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+
EXPECT_EQ(evaluator.true_positives(), 3);
// The following observation does not generate a true negative as it belongs
// to the observation period of the last prediction - for which two detections
// have already been matched.
const int true_negatives = evaluator.true_negatives();
evaluator.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(evaluator.true_negatives(), true_negatives);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that multiple consecutive predictions match all the detections
// observed before the expected detection deadline expires.
TEST(ClippingPredictorEvaluatorTest, PredictedMultipleTimesAndAllDetected) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted); // --+
evaluator.Observe(kNotDetected, kPredicted); // | --+
evaluator.Observe(kNotDetected, kPredicted); // | | --+
evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+
evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+
evaluator.Observe(kDetected, kNotPredicted); // <-+
EXPECT_EQ(evaluator.true_positives(), 3);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
// Checks that multiple non-consecutive predictions match all the detections
// observed before the expected detection deadline expires.
TEST(ClippingPredictorEvaluatorTest,
PredictedMultipleTimesWithGapAndAllDetected) {
ClippingPredictorEvaluator evaluator(/*history_size=*/3);
evaluator.Observe(kNotDetected, kPredicted); // --+
evaluator.Observe(kNotDetected, kNotPredicted); // |
evaluator.Observe(kNotDetected, kPredicted); // | --+
evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+
evaluator.Observe(kDetected, kNotPredicted); // <-+
evaluator.Observe(kDetected, kNotPredicted); // <-+
EXPECT_EQ(evaluator.true_positives(), 2);
EXPECT_EQ(evaluator.true_negatives(), 0);
EXPECT_EQ(evaluator.false_positives(), 0);
EXPECT_EQ(evaluator.false_negatives(), 0);
}
class ClippingPredictorEvaluatorPredictionIntervalParameterization
: public ::testing::TestWithParam<std::tuple<int, int>> {
protected:
int num_extra_observe_calls() const { return std::get<0>(GetParam()); }
int history_size() const { return std::get<1>(GetParam()); }
};
// Checks that the minimum prediction interval is returned if clipping is
// correctly predicted as soon as detected - i.e., no anticipation.
TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
MinimumPredictionInterval) {
ClippingPredictorEvaluator evaluator(history_size());
for (int i = 0; i < num_extra_observe_calls(); ++i) {
EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt);
}
absl::optional<int> prediction_interval =
evaluator.Observe(kDetected, kPredicted);
EXPECT_THAT(prediction_interval, Optional(Eq(0)));
}
// Checks that a prediction interval between the minimum and the maximum is
// returned if clipping is correctly predicted before it is detected but not as
// early as possible.
TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
IntermediatePredictionInterval) {
ClippingPredictorEvaluator evaluator(history_size());
for (int i = 0; i < num_extra_observe_calls(); ++i) {
EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt);
}
EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
absl::optional<int> prediction_interval =
evaluator.Observe(kDetected, kPredicted);
EXPECT_THAT(prediction_interval, Optional(Eq(3)));
}
// Checks that the maximum prediction interval is returned if clipping is
// correctly predicted as early as possible.
TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
MaximumPredictionInterval) {
ClippingPredictorEvaluator evaluator(history_size());
for (int i = 0; i < num_extra_observe_calls(); ++i) {
EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt);
}
for (int i = 0; i < history_size(); ++i) {
EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
}
absl::optional<int> prediction_interval =
evaluator.Observe(kDetected, kPredicted);
EXPECT_THAT(prediction_interval, Optional(Eq(history_size())));
}
// Checks that `Observe()` returns the prediction interval as soon as a true
// positive is found and never again while ongoing detections are matched to a
// previously observed prediction.
TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
PredictionIntervalReturnedOnce) {
ASSERT_LT(num_extra_observe_calls(), history_size());
ClippingPredictorEvaluator evaluator(history_size());
// Observe predictions before detection.
for (int i = 0; i < num_extra_observe_calls(); ++i) {
EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
}
// Observe a detection.
absl::optional<int> prediction_interval =
evaluator.Observe(kDetected, kPredicted);
EXPECT_TRUE(prediction_interval.has_value());
// `Observe()` does not return a prediction interval anymore during ongoing
// detections observed while a detection is still expected.
for (int i = 0; i < history_size(); ++i) {
EXPECT_EQ(evaluator.Observe(kDetected, kNotPredicted), absl::nullopt);
}
}
INSTANTIATE_TEST_SUITE_P(
ClippingPredictorEvaluatorTest,
ClippingPredictorEvaluatorPredictionIntervalParameterization,
::testing::Combine(::testing::Values(0, 3, 5), ::testing::Values(7, 11)));
// Checks that, when a detection is expected, the expectation is removed if and
// only if `Reset()` is called after a prediction is observed.
TEST(ClippingPredictorEvaluatorTest, NoFalsePositivesAfterReset) {
constexpr int kHistorySize = 2;
ClippingPredictorEvaluator with_reset(kHistorySize);
with_reset.Observe(kNotDetected, kPredicted);
with_reset.Reset();
with_reset.Observe(kNotDetected, kNotPredicted);
with_reset.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(with_reset.true_positives(), 0);
EXPECT_EQ(with_reset.true_negatives(), 2);
EXPECT_EQ(with_reset.false_positives(), 0);
EXPECT_EQ(with_reset.false_negatives(), 0);
ClippingPredictorEvaluator no_reset(kHistorySize);
no_reset.Observe(kNotDetected, kPredicted);
no_reset.Observe(kNotDetected, kNotPredicted);
no_reset.Observe(kNotDetected, kNotPredicted);
EXPECT_EQ(no_reset.true_positives(), 0);
EXPECT_EQ(no_reset.true_negatives(), 0);
EXPECT_EQ(no_reset.false_positives(), 1);
EXPECT_EQ(no_reset.false_negatives(), 0);
}
} // namespace
} // namespace webrtc