On-fly calculation of quality metrics.

Calculation of quality metrics required writing of decoded video
to file. There were two drawbacks with that approach. First, frame
drops significantly affected metrics because comparison was done
against the last decoded frame. Second, simulcast/SVC required
writing of multiple files. This might be too much data to dump.

On-fly metrics calculation is done in frame decoded callback.
Calculation time is excluded from encoding/decoding time. If CPU
usage measurement is enabled metrics calculation is disabled since
it affects CPU usage. The results are reported in Stats::PrintSummary.

Bug: webrtc:8524
Change-Id: Id54fb21f2f95deeb93757afaf46bde7d7ae18dac
Reviewed-on: https://webrtc-review.googlesource.com/22560
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Reviewed-by: Patrik Höglund <phoglund@webrtc.org>
Reviewed-by: Rasmus Brandt <brandtr@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#20798}
This commit is contained in:
Sergey Silkin
2017-11-17 14:47:32 +01:00
committed by Commit Bot
parent c5975bf25d
commit 64eaa99cfc
9 changed files with 150 additions and 128 deletions

View File

@ -42,6 +42,16 @@ bool LessForBitRate(const FrameStatistic& s1, const FrameStatistic& s2) {
return s1.bitrate_kbps < s2.bitrate_kbps;
}
bool LessForPsnr(const FrameStatistic& s1, const FrameStatistic& s2) {
RTC_DCHECK_NE(s1.frame_number, s2.frame_number);
return s1.psnr < s2.psnr;
}
bool LessForSsim(const FrameStatistic& s1, const FrameStatistic& s2) {
RTC_DCHECK_NE(s1.frame_number, s2.frame_number);
return s1.ssim < s2.ssim;
}
} // namespace
FrameStatistic* Stats::AddFrame() {
@ -77,6 +87,8 @@ void Stats::PrintSummary() const {
size_t num_key_frames = 0;
size_t num_delta_frames = 0;
int num_encode_failures = 0;
double total_psnr = 0.0;
double total_ssim = 0.0;
for (const FrameStatistic& stat : stats_) {
total_encoding_time_us += stat.encode_time_us;
@ -92,6 +104,10 @@ void Stats::PrintSummary() const {
if (stat.encode_return_code != 0) {
++num_encode_failures;
}
if (stat.decoding_successful) {
total_psnr += stat.psnr;
total_ssim += stat.ssim;
}
}
// Encoding stats.
@ -164,6 +180,24 @@ void Stats::PrintSummary() const {
printf(" Max bitrate: %7d kbps (frame %d)\n", frame_it->bitrate_kbps,
frame_it->frame_number);
// Quality.
printf("Quality:\n");
if (decoded_frames.empty()) {
printf("No successfully decoded frames exist in this statistics.\n");
} else {
frame_it = std::min_element(decoded_frames.begin(), decoded_frames.end(),
LessForPsnr);
printf(" PSNR min: %f (frame %d)\n", frame_it->psnr,
frame_it->frame_number);
printf(" PSNR avg: %f\n", total_psnr / decoded_frames.size());
frame_it = std::min_element(decoded_frames.begin(), decoded_frames.end(),
LessForSsim);
printf(" SSIM min: %f (frame %d)\n", frame_it->ssim,
frame_it->frame_number);
printf(" SSIM avg: %f\n", total_ssim / decoded_frames.size());
}
printf("\n");
printf("Total encoding time : %7d ms.\n", total_encoding_time_us / 1000);
printf("Total decoding time : %7d ms.\n", total_decoding_time_us / 1000);

View File

@ -50,6 +50,10 @@ struct FrameStatistic {
int packets_dropped = 0;
size_t total_packets = 0;
size_t manipulated_length = 0;
// Quality.
float psnr = 0.0;
float ssim = 0.0;
};
// Statistics for a sequence of processed frames. This class is not thread safe.

View File

@ -99,7 +99,6 @@ void ExtractBufferWithSize(const VideoFrame& image,
VideoProcessor::VideoProcessor(webrtc::VideoEncoder* encoder,
webrtc::VideoDecoder* decoder,
FrameReader* analysis_frame_reader,
FrameWriter* analysis_frame_writer,
PacketManipulator* packet_manipulator,
const TestConfig& config,
Stats* stats,
@ -113,7 +112,6 @@ VideoProcessor::VideoProcessor(webrtc::VideoEncoder* encoder,
decode_callback_(this),
packet_manipulator_(packet_manipulator),
analysis_frame_reader_(analysis_frame_reader),
analysis_frame_writer_(analysis_frame_writer),
encoded_frame_writer_(encoded_frame_writer),
decoded_frame_writer_(decoded_frame_writer),
last_inputed_frame_num_(-1),
@ -127,7 +125,6 @@ VideoProcessor::VideoProcessor(webrtc::VideoEncoder* encoder,
RTC_DCHECK(decoder);
RTC_DCHECK(packet_manipulator);
RTC_DCHECK(analysis_frame_reader);
RTC_DCHECK(analysis_frame_writer);
RTC_DCHECK(stats);
// Setup required callbacks for the encoder and decoder.
@ -158,7 +155,7 @@ VideoProcessor::~VideoProcessor() {
void VideoProcessor::ProcessFrame() {
RTC_DCHECK_CALLED_SEQUENTIALLY(&sequence_checker_);
++last_inputed_frame_num_;
const int frame_number = ++last_inputed_frame_num_;
// Get frame from file.
rtc::scoped_refptr<I420BufferInterface> buffer(
@ -167,15 +164,13 @@ void VideoProcessor::ProcessFrame() {
// Use the frame number as the basis for timestamp to identify frames. Let the
// first timestamp be non-zero, to not make the IvfFileWriter believe that we
// want to use capture timestamps in the IVF files.
const uint32_t rtp_timestamp = (last_inputed_frame_num_ + 1) *
kRtpClockRateHz /
const uint32_t rtp_timestamp = (frame_number + 1) * kRtpClockRateHz /
config_.codec_settings.maxFramerate;
rtp_timestamp_to_frame_num_[rtp_timestamp] = last_inputed_frame_num_;
VideoFrame source_frame(buffer, rtp_timestamp, kNoRenderTime,
webrtc::kVideoRotation_0);
rtp_timestamp_to_frame_num_[rtp_timestamp] = frame_number;
input_frames_[frame_number] = rtc::MakeUnique<VideoFrame>(
buffer, rtp_timestamp, kNoRenderTime, webrtc::kVideoRotation_0);
std::vector<FrameType> frame_types =
config_.FrameTypeForFrame(last_inputed_frame_num_);
std::vector<FrameType> frame_types = config_.FrameTypeForFrame(frame_number);
// Create frame statistics object used for aggregation at end of test run.
FrameStatistic* frame_stat = stats_->AddFrame();
@ -184,7 +179,7 @@ void VideoProcessor::ProcessFrame() {
// time recordings should wrap the Encode call as tightly as possible.
frame_stat->encode_start_ns = rtc::TimeNanos();
frame_stat->encode_return_code =
encoder_->Encode(source_frame, nullptr, &frame_types);
encoder_->Encode(*input_frames_[frame_number], nullptr, &frame_types);
}
void VideoProcessor::SetRates(int bitrate_kbps, int framerate_fps) {
@ -222,30 +217,24 @@ void VideoProcessor::FrameEncoded(webrtc::VideoCodecType codec,
config_.encoded_frame_checker->CheckEncodedFrame(codec, encoded_image);
}
// Check for dropped frames.
const int frame_number =
rtp_timestamp_to_frame_num_[encoded_image._timeStamp];
// Ensure strict monotonicity.
RTC_CHECK_GT(frame_number, last_encoded_frame_num_);
// Check for dropped frames.
bool last_frame_missing = false;
if (frame_number > 0) {
RTC_DCHECK_GE(last_encoded_frame_num_, 0);
int num_dropped_from_last_encode =
frame_number - last_encoded_frame_num_ - 1;
RTC_DCHECK_GE(num_dropped_from_last_encode, 0);
RTC_CHECK_GE(rate_update_index_, 0);
num_dropped_frames_[rate_update_index_] += num_dropped_from_last_encode;
if (num_dropped_from_last_encode > 0) {
// For dropped frames, we write out the last decoded frame to avoid
// getting out of sync for the computation of PSNR and SSIM.
for (int i = 0; i < num_dropped_from_last_encode; i++) {
WriteDecodedFrameToFile(&last_decoded_frame_buffer_);
}
}
const FrameStatistic* last_encoded_frame_stat =
stats_->GetFrame(last_encoded_frame_num_);
last_frame_missing = (last_encoded_frame_stat->manipulated_length == 0);
}
// Ensure strict monotonicity.
RTC_CHECK_GT(frame_number, last_encoded_frame_num_);
last_encoded_frame_num_ = frame_number;
// Update frame statistics.
@ -285,18 +274,12 @@ void VideoProcessor::FrameEncoded(webrtc::VideoCodecType codec,
frame_stat->decode_return_code =
decoder_->Decode(copied_image, last_frame_missing, nullptr);
if (frame_stat->decode_return_code != WEBRTC_VIDEO_CODEC_OK) {
// Write the last successful frame the output file to avoid getting it out
// of sync with the source file for SSIM and PSNR comparisons.
WriteDecodedFrameToFile(&last_decoded_frame_buffer_);
}
if (encoded_frame_writer_) {
RTC_CHECK(encoded_frame_writer_->WriteFrame(encoded_image, codec));
}
}
void VideoProcessor::FrameDecoded(const VideoFrame& image) {
void VideoProcessor::FrameDecoded(const VideoFrame& decoded_frame) {
RTC_DCHECK_CALLED_SEQUENTIALLY(&sequence_checker_);
// For the highest measurement accuracy of the decode time, the start/stop
@ -304,46 +287,67 @@ void VideoProcessor::FrameDecoded(const VideoFrame& image) {
int64_t decode_stop_ns = rtc::TimeNanos();
// Update frame statistics.
const int frame_number = rtp_timestamp_to_frame_num_[image.timestamp()];
const int frame_number =
rtp_timestamp_to_frame_num_[decoded_frame.timestamp()];
FrameStatistic* frame_stat = stats_->GetFrame(frame_number);
frame_stat->decoded_width = image.width();
frame_stat->decoded_height = image.height();
frame_stat->decoded_width = decoded_frame.width();
frame_stat->decoded_height = decoded_frame.height();
frame_stat->decode_time_us =
GetElapsedTimeMicroseconds(frame_stat->decode_start_ns, decode_stop_ns);
frame_stat->decoding_successful = true;
// Ensure strict monotonicity.
RTC_CHECK_GT(frame_number, last_decoded_frame_num_);
// Check if the codecs have resized the frame since previously decoded frame.
if (frame_number > 0) {
RTC_CHECK_GE(last_decoded_frame_num_, 0);
if (decoded_frame_writer_ && last_decoded_frame_num_ >= 0) {
// For dropped/lost frames, write out the last decoded frame to make it
// look like a freeze at playback.
const int num_dropped_frames = frame_number - last_decoded_frame_num_;
for (int i = 0; i < num_dropped_frames; i++) {
WriteDecodedFrameToFile(&last_decoded_frame_buffer_);
}
}
// TODO(ssilkin): move to FrameEncoded when webm:1474 is implemented.
const FrameStatistic* last_decoded_frame_stat =
stats_->GetFrame(last_decoded_frame_num_);
if (image.width() != last_decoded_frame_stat->decoded_width ||
image.height() != last_decoded_frame_stat->decoded_height) {
if (decoded_frame.width() != last_decoded_frame_stat->decoded_width ||
decoded_frame.height() != last_decoded_frame_stat->decoded_height) {
RTC_CHECK_GE(rate_update_index_, 0);
++num_spatial_resizes_[rate_update_index_];
}
}
// Ensure strict monotonicity.
RTC_CHECK_GT(frame_number, last_decoded_frame_num_);
last_decoded_frame_num_ = frame_number;
// If the frame size is different from the original size, scale back to the
// original size. This is needed for the PSNR and SSIM calculations.
rtc::Buffer buffer;
ExtractBufferWithSize(image, config_.codec_settings.width,
config_.codec_settings.height, &buffer);
WriteDecodedFrameToFile(&buffer);
// Skip quality metrics calculation to not affect CPU usage.
if (!config_.measure_cpu) {
frame_stat->psnr =
I420PSNR(input_frames_[frame_number].get(), &decoded_frame);
frame_stat->ssim =
I420SSIM(input_frames_[frame_number].get(), &decoded_frame);
}
last_decoded_frame_buffer_ = std::move(buffer);
// Delay erasing of input frames by one frame. The current frame might
// still be needed for other simulcast stream or spatial layer.
const int frame_number_to_erase = frame_number - 1;
if (frame_number_to_erase >= 0) {
auto input_frame_erase_to =
input_frames_.lower_bound(frame_number_to_erase);
input_frames_.erase(input_frames_.begin(), input_frame_erase_to);
}
if (decoded_frame_writer_) {
ExtractBufferWithSize(decoded_frame, config_.codec_settings.width,
config_.codec_settings.height,
&last_decoded_frame_buffer_);
WriteDecodedFrameToFile(&last_decoded_frame_buffer_);
}
}
void VideoProcessor::WriteDecodedFrameToFile(rtc::Buffer* buffer) {
RTC_DCHECK_EQ(buffer->size(), analysis_frame_writer_->FrameLength());
RTC_CHECK(analysis_frame_writer_->WriteFrame(buffer->data()));
if (decoded_frame_writer_) {
RTC_DCHECK_EQ(buffer->size(), decoded_frame_writer_->FrameLength());
RTC_CHECK(decoded_frame_writer_->WriteFrame(buffer->data()));
}
RTC_DCHECK_EQ(buffer->size(), decoded_frame_writer_->FrameLength());
RTC_CHECK(decoded_frame_writer_->WriteFrame(buffer->data()));
}
bool VideoProcessor::ExcludeFrame(const EncodedImage& encoded_image) {

View File

@ -62,7 +62,6 @@ class VideoProcessor {
VideoProcessor(webrtc::VideoEncoder* encoder,
webrtc::VideoDecoder* decoder,
FrameReader* analysis_frame_reader,
FrameWriter* analysis_frame_writer,
PacketManipulator* packet_manipulator,
const TestConfig& config,
Stats* stats,
@ -199,10 +198,16 @@ class VideoProcessor {
// Fake network.
PacketManipulator* const packet_manipulator_;
// Input frames. Used as reference at frame quality evaluation.
// Async codecs might queue frames. To handle that we keep input frame
// and release it after corresponding coded frame is decoded and quality
// measurement is done.
std::map<int, std::unique_ptr<VideoFrame>> input_frames_
RTC_GUARDED_BY(sequence_checker_);
// These (mandatory) file manipulators are used for, e.g., objective PSNR and
// SSIM calculations at the end of a test run.
FrameReader* const analysis_frame_reader_;
FrameWriter* const analysis_frame_writer_;
// These (optional) file writers are used to persistently store the encoded
// and decoded bitstreams. The purpose is to give the experimenter an option

View File

@ -51,23 +51,6 @@ const float kInitialBufferSize = 0.5f;
const float kOptimalBufferSize = 0.6f;
const float kScaleKeyFrameSize = 0.5f;
void VerifyQuality(const QualityMetricsResult& psnr_result,
const QualityMetricsResult& ssim_result,
const QualityThresholds& quality_thresholds) {
EXPECT_GT(psnr_result.average, quality_thresholds.min_avg_psnr);
EXPECT_GT(psnr_result.min, quality_thresholds.min_min_psnr);
EXPECT_GT(ssim_result.average, quality_thresholds.min_avg_ssim);
EXPECT_GT(ssim_result.min, quality_thresholds.min_min_ssim);
}
void PrintQualityMetrics(const QualityMetricsResult& psnr_result,
const QualityMetricsResult& ssim_result) {
printf("Quality statistics\n==\n");
printf("PSNR avg: %f, min: %f\n", psnr_result.average, psnr_result.min);
printf("SSIM avg: %f, min: %f\n", ssim_result.average, ssim_result.min);
printf("\n");
}
bool RunEncodeInRealTime(const TestConfig& config) {
if (config.measure_cpu) {
return true;
@ -284,10 +267,15 @@ void VideoProcessorIntegrationTest::ProcessFramesAndMaybeVerify(
// Calculate and print rate control statistics.
rate_update_index = 0;
frame_number = 0;
quality_ = QualityMetrics();
ResetRateControlMetrics(rate_update_index, rate_profiles);
while (frame_number < num_frames) {
UpdateRateControlMetrics(frame_number);
if (quality_thresholds) {
UpdateQualityMetrics(frame_number);
}
if (bs_thresholds) {
VerifyBitstream(frame_number, *bs_thresholds);
}
@ -310,28 +298,14 @@ void VideoProcessorIntegrationTest::ProcessFramesAndMaybeVerify(
VerifyRateControlMetrics(rate_update_index, rc_thresholds, num_dropped_frames,
num_spatial_resizes);
if (quality_thresholds) {
VerifyQualityMetrics(*quality_thresholds);
}
// Calculate and print other statistics.
EXPECT_EQ(num_frames, static_cast<int>(stats_.size()));
stats_.PrintSummary();
cpu_process_time_->Print();
// Calculate and print image quality statistics.
// TODO(marpan): Should compute these quality metrics per SetRates update.
QualityMetricsResult psnr_result, ssim_result;
EXPECT_EQ(0, I420MetricsFromFiles(config_.input_filename.c_str(),
config_.output_filename.c_str(),
config_.codec_settings.width,
config_.codec_settings.height, &psnr_result,
&ssim_result));
if (quality_thresholds) {
VerifyQuality(psnr_result, ssim_result, *quality_thresholds);
}
PrintQualityMetrics(psnr_result, ssim_result);
// Remove analysis file.
if (remove(config_.output_filename.c_str()) < 0) {
fprintf(stderr, "Failed to remove temporary file!\n");
}
}
void VideoProcessorIntegrationTest::CreateEncoderAndDecoder() {
@ -482,8 +456,8 @@ void VideoProcessorIntegrationTest::SetUpAndInitObjects(
task_queue->PostTask([this, &sync_event]() {
processor_ = rtc::MakeUnique<VideoProcessor>(
encoder_.get(), decoder_.get(), analysis_frame_reader_.get(),
analysis_frame_writer_.get(), packet_manipulator_.get(), config_,
&stats_, encoded_frame_writer_.get(), decoded_frame_writer_.get());
packet_manipulator_.get(), config_, &stats_,
encoded_frame_writer_.get(), decoded_frame_writer_.get());
sync_event.Set();
});
sync_event.Wait(rtc::Event::kForever);
@ -501,9 +475,7 @@ void VideoProcessorIntegrationTest::ReleaseAndCloseObjects(
// The VideoProcessor must be destroyed before the codecs.
DestroyEncoderAndDecoder();
// Close the analysis files before we use them for SSIM/PSNR calculations.
analysis_frame_reader_->Close();
analysis_frame_writer_->Close();
// Close visualization files.
if (encoded_frame_writer_) {
@ -591,6 +563,19 @@ void VideoProcessorIntegrationTest::VerifyRateControlMetrics(
}
}
void VideoProcessorIntegrationTest::UpdateQualityMetrics(int frame_number) {
FrameStatistic* frame_stat = stats_.GetFrame(frame_number);
if (frame_stat->decoding_successful) {
++quality_.num_decoded_frames;
quality_.total_psnr += frame_stat->psnr;
quality_.total_ssim += frame_stat->ssim;
if (frame_stat->psnr < quality_.min_psnr)
quality_.min_psnr = frame_stat->psnr;
if (frame_stat->ssim < quality_.min_ssim)
quality_.min_ssim = frame_stat->ssim;
}
}
void VideoProcessorIntegrationTest::PrintRateControlMetrics(
int rate_update_index,
const std::vector<int>& num_dropped_frames,
@ -656,6 +641,17 @@ void VideoProcessorIntegrationTest::VerifyBitstream(
EXPECT_LE(*(frame_stat->max_nalu_length), bs_thresholds.max_nalu_length);
}
void VideoProcessorIntegrationTest::VerifyQualityMetrics(
const QualityThresholds& quality_thresholds) {
EXPECT_GT(quality_.num_decoded_frames, 0);
EXPECT_GT(quality_.total_psnr / quality_.num_decoded_frames,
quality_thresholds.min_avg_psnr);
EXPECT_GT(quality_.min_psnr, quality_thresholds.min_min_psnr);
EXPECT_GT(quality_.total_ssim / quality_.num_decoded_frames,
quality_thresholds.min_avg_ssim);
EXPECT_GT(quality_.min_ssim, quality_thresholds.min_min_ssim);
}
// Reset quantities before each encoder rate update.
void VideoProcessorIntegrationTest::ResetRateControlMetrics(
int rate_update_index,

View File

@ -12,6 +12,7 @@
#define MODULES_VIDEO_CODING_CODECS_TEST_VIDEOPROCESSOR_INTEGRATIONTEST_H_
#include <cmath>
#include <limits>
#include <memory>
#include <string>
#include <vector>
@ -159,6 +160,14 @@ class VideoProcessorIntegrationTest : public testing::Test {
float key_framesize_kbits;
};
struct QualityMetrics {
int num_decoded_frames = 0;
double total_psnr = 0.0;
double total_ssim = 0.0;
double min_psnr = std::numeric_limits<double>::max();
double min_ssim = std::numeric_limits<double>::max();
};
void CreateEncoderAndDecoder();
void DestroyEncoderAndDecoder();
void SetUpAndInitObjects(rtc::TaskQueue* task_queue,
@ -185,6 +194,9 @@ class VideoProcessorIntegrationTest : public testing::Test {
void VerifyBitstream(int frame_number,
const BitstreamThresholds& bs_thresholds);
void UpdateQualityMetrics(int frame_number);
void VerifyQualityMetrics(const QualityThresholds& quality_thresholds);
void PrintSettings() const;
// Codecs.
@ -207,6 +219,8 @@ class VideoProcessorIntegrationTest : public testing::Test {
// Rates set for every encoder rate update.
TargetRates target_;
QualityMetrics quality_;
};
} // namespace test

View File

@ -20,7 +20,6 @@
#include "test/gmock.h"
#include "test/gtest.h"
#include "test/testsupport/mock/mock_frame_reader.h"
#include "test/testsupport/mock/mock_frame_writer.h"
#include "test/testsupport/packet_reader.h"
#include "test/testsupport/unittest_utils.h"
#include "test/video_codec_settings.h"
@ -55,7 +54,7 @@ class VideoProcessorTest : public testing::Test {
.WillRepeatedly(Return(kFrameSize));
video_processor_ = rtc::MakeUnique<VideoProcessor>(
&encoder_mock_, &decoder_mock_, &frame_reader_mock_,
&frame_writer_mock_, &packet_manipulator_mock_, config_, &stats_,
&packet_manipulator_mock_, config_, &stats_,
nullptr /* encoded_frame_writer */, nullptr /* decoded_frame_writer */);
}
@ -78,7 +77,6 @@ class VideoProcessorTest : public testing::Test {
MockVideoEncoder encoder_mock_;
MockVideoDecoder decoder_mock_;
MockFrameReader frame_reader_mock_;
MockFrameWriter frame_writer_mock_;
MockPacketManipulator packet_manipulator_mock_;
Stats stats_;
std::unique_ptr<VideoProcessor> video_processor_;