Add Y4mFileReader

Encapsulate logic for reading .y4m video files in a single class. We
currently have spread out logic for opening .y4m files with partial
parsing. This CL consolidates this logic into a single class with a well
defined interface.

Change-Id: Id61673b3c95a0053b30e95b4cf382e1c6b05fc30
Bug: webrtc:9642
Reviewed-on: https://webrtc-review.googlesource.com/94772
Reviewed-by: Patrik Höglund <phoglund@webrtc.org>
Reviewed-by: Paulina Hensman <phensman@webrtc.org>
Commit-Queue: Magnus Jedvert <magjed@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#24398}
This commit is contained in:
Magnus Jedvert 2018-08-22 19:23:34 +02:00 committed by Commit Bot
parent 63af828a1b
commit 404be7f302
13 changed files with 543 additions and 382 deletions

View file

@ -1 +0,0 @@
4d1ac894f1743af8059e8d8ae2465f6eaa1790b0

View file

@ -17,6 +17,7 @@ group("rtc_tools") {
":command_line_parser",
":frame_analyzer",
":video_quality_analysis",
":y4m_file_reader",
]
if (!build_with_chromium) {
deps += [
@ -59,12 +60,28 @@ rtc_static_library("command_line_parser") {
]
}
rtc_static_library("y4m_file_reader") {
sources = [
"y4m_file_reader.cc",
"y4m_file_reader.h",
]
deps = [
"../api/video:video_frame",
"../api/video:video_frame_i420",
"../rtc_base:rtc_base_approved",
"../rtc_base:sequenced_task_checker",
"//third_party/abseil-cpp/absl/types:optional",
]
}
rtc_static_library("video_quality_analysis") {
sources = [
"frame_analyzer/video_quality_analysis.cc",
"frame_analyzer/video_quality_analysis.h",
]
deps = [
":y4m_file_reader",
"../api/video:video_frame_i420",
"../common_video",
"../test:perf_test",
"//third_party/libyuv",
@ -80,6 +97,7 @@ rtc_executable("frame_analyzer") {
deps = [
":command_line_parser",
":video_quality_analysis",
":y4m_file_reader",
"../test:perf_test",
"//build/win:default_exe_manifest",
]
@ -96,6 +114,7 @@ if (!build_with_chromium) {
deps = [
":command_line_parser",
":video_quality_analysis",
":y4m_file_reader",
"//build/win:default_exe_manifest",
]
}
@ -108,6 +127,7 @@ if (!build_with_chromium) {
deps = [
":video_quality_analysis",
":y4m_file_reader",
]
}
@ -288,7 +308,6 @@ if (rtc_include_tests) {
tools_unittests_resources = [
"../resources/foreman_cif.yuv",
"../resources/reference_less_video_test_file.y4m",
"../resources/video_quality_analysis_frame.txt",
]
if (is_ios) {
@ -310,6 +329,7 @@ if (rtc_include_tests) {
"frame_editing/frame_editing_unittest.cc",
"sanitizers_unittest.cc",
"simple_command_line_parser_unittest.cc",
"y4m_file_reader_unittest.cc",
]
if (!build_with_chromium && is_clang) {
@ -322,6 +342,7 @@ if (rtc_include_tests) {
":frame_editing_lib",
":reference_less_video_analysis_lib",
":video_quality_analysis",
":y4m_file_reader",
"../common_video:common_video",
"../rtc_base",
"../rtc_base:checks",

View file

@ -17,6 +17,7 @@
#include "rtc_tools/frame_analyzer/video_quality_analysis.h"
#include "rtc_tools/simple_command_line_parser.h"
#include "rtc_tools/y4m_file_reader.h"
#include "test/testsupport/perf_test.h"
/*
@ -101,11 +102,19 @@ int main(int argc, char* argv[]) {
webrtc::test::ResultsContainer results;
webrtc::test::RunAnalysis(parser.GetFlag("reference_file").c_str(),
parser.GetFlag("test_file").c_str(),
parser.GetFlag("stats_file_ref").c_str(),
parser.GetFlag("stats_file_test").c_str(), width,
height, &results);
rtc::scoped_refptr<webrtc::test::Y4mFile> reference_video =
webrtc::test::Y4mFile::Open(parser.GetFlag("reference_file"));
rtc::scoped_refptr<webrtc::test::Y4mFile> test_video =
webrtc::test::Y4mFile::Open(parser.GetFlag("test_file"));
if (!reference_video || !test_video) {
fprintf(stderr, "Error opening video files\n");
return 0;
}
webrtc::test::RunAnalysis(
reference_video, test_video, parser.GetFlag("stats_file_ref").c_str(),
parser.GetFlag("stats_file_test").c_str(), width, height, &results);
webrtc::test::GetMaxRepeatedAndSkippedFrames(
parser.GetFlag("stats_file_ref"), parser.GetFlag("stats_file_test"),
&results);

View file

@ -25,36 +25,6 @@
#define strtok_r strtok_s
#endif
void get_height_width_fps(int* height,
int* width,
int* fps,
const std::string& video_file) {
// File header looks like :
// YUV4MPEG2 W1280 H720 F25:1 Ip A0:0 C420mpeg2 XYSCSS=420MPEG2.
char frame_header[STATS_LINE_LENGTH];
FILE* input_file = fopen(video_file.c_str(), "rb");
size_t bytes_read = fread(frame_header, 1, STATS_LINE_LENGTH - 1, input_file);
frame_header[bytes_read] = '\0';
std::string file_header_stats[5];
int no_of_stats = 0;
char* save_ptr;
char* token = strtok_r(frame_header, " ", &save_ptr);
while (token != NULL) {
file_header_stats[no_of_stats++] = token;
token = strtok_r(NULL, " ", &save_ptr);
}
*width = std::stoi(file_header_stats[1].erase(0, 1));
*height = std::stoi(file_header_stats[2].erase(0, 1));
*fps = std::stoi(file_header_stats[3].erase(0, 1));
printf("Height: %d Width: %d fps:%d \n", *height, *width, *fps);
fclose(input_file);
}
bool frozen_frame(std::vector<double> psnr_per_frame,
std::vector<double> ssim_per_frame,
size_t frame) {
@ -135,60 +105,29 @@ void print_freezing_metrics(const std::vector<double>& psnr_per_frame,
printf("\n");
}
void compute_metrics(const std::string& video_file_name,
void compute_metrics(const rtc::scoped_refptr<webrtc::test::Y4mFile>& video,
std::vector<double>* psnr_per_frame,
std::vector<double>* ssim_per_frame) {
int height = 0, width = 0, fps = 0;
get_height_width_fps(&height, &width, &fps, video_file_name);
int no_of_frames = 0;
int size = webrtc::test::GetI420FrameSize(width, height);
// Allocate buffers for test and reference frames.
uint8_t* current_frame = new uint8_t[size];
uint8_t* next_frame = new uint8_t[size];
while (true) {
if (!(webrtc::test::ExtractFrameFromY4mFile(video_file_name.c_str(), width,
height, no_of_frames,
current_frame))) {
break;
}
if (!(webrtc::test::ExtractFrameFromY4mFile(video_file_name.c_str(), width,
height, no_of_frames + 1,
next_frame))) {
break;
}
double result_psnr = webrtc::test::CalculateMetrics(
webrtc::test::kPSNR, current_frame, next_frame, width, height);
double result_ssim = webrtc::test::CalculateMetrics(
webrtc::test::kSSIM, current_frame, next_frame, width, height);
for (size_t i = 0; i < video->number_of_frames() - 1; ++i) {
const rtc::scoped_refptr<webrtc::I420BufferInterface> current_frame =
video->GetFrame(i);
const rtc::scoped_refptr<webrtc::I420BufferInterface> next_frame =
video->GetFrame(i + 1);
double result_psnr = webrtc::test::Psnr(current_frame, next_frame);
double result_ssim = webrtc::test::Ssim(current_frame, next_frame);
psnr_per_frame->push_back(result_psnr);
ssim_per_frame->push_back(result_ssim);
no_of_frames++;
}
// Cleanup.
delete[] current_frame;
delete[] next_frame;
}
bool check_file_extension(const std::string& video_file_name) {
if (video_file_name.substr(video_file_name.length() - 3, 3) != "y4m") {
printf("Only y4m video file format is supported. Given: %s\n",
video_file_name.c_str());
return false;
}
return true;
}
int run_analysis(const std::string& video_file) {
std::vector<double> psnr_per_frame;
std::vector<double> ssim_per_frame;
if (check_file_extension(video_file)) {
compute_metrics(video_file, &psnr_per_frame, &ssim_per_frame);
rtc::scoped_refptr<webrtc::test::Y4mFile> video =
webrtc::test::Y4mFile::Open(video_file);
if (video) {
compute_metrics(video, &psnr_per_frame, &ssim_per_frame);
} else {
return -1;
}

View file

@ -14,12 +14,7 @@
#include <string>
#include <vector>
// Parse the file header to extract height, width and fps
// for a given video file.
void get_height_width_fps(int* height,
int* width,
int* fps,
const std::string& video_file);
#include "rtc_tools/y4m_file_reader.h"
// Returns true if the frame is frozen based on psnr and ssim freezing
// threshold values.
@ -39,13 +34,10 @@ void print_freezing_metrics(const std::vector<double>& psnr_per_frame,
// Compute the metrics like freezing score based on PSNR and SSIM values for a
// given video file.
void compute_metrics(const std::string& video_file_name,
void compute_metrics(const rtc::scoped_refptr<webrtc::test::Y4mFile>& video,
std::vector<double>* psnr_per_frame,
std::vector<double>* ssim_per_frame);
// Checks the file extension and return true if it is y4m.
bool check_file_extension(const std::string& video_file_name);
// Compute freezing score metrics and prints the metrics
// for a list of video files.
int run_analysis(const std::string& video_file);

View file

@ -20,16 +20,18 @@
class ReferenceLessVideoAnalysisTest : public ::testing::Test {
public:
void SetUp() override {
video_file =
webrtc::test::ResourcePath("reference_less_video_test_file", "y4m");
video = webrtc::test::Y4mFile::Open(
webrtc::test::ResourcePath("reference_less_video_test_file", "y4m"));
ASSERT_TRUE(video);
}
std::string video_file;
rtc::scoped_refptr<webrtc::test::Y4mFile> video;
std::vector<double> psnr_per_frame;
std::vector<double> ssim_per_frame;
};
TEST_F(ReferenceLessVideoAnalysisTest, MatchComputedMetrics) {
compute_metrics(video_file, &psnr_per_frame, &ssim_per_frame);
compute_metrics(video, &psnr_per_frame, &ssim_per_frame);
EXPECT_EQ(74, (int)psnr_per_frame.size());
ASSERT_NEAR(27.2f, psnr_per_frame[1], 0.1f);
@ -39,26 +41,11 @@ TEST_F(ReferenceLessVideoAnalysisTest, MatchComputedMetrics) {
ASSERT_NEAR(0.9f, ssim_per_frame[5], 0.1f);
}
TEST_F(ReferenceLessVideoAnalysisTest, MatchHeightWidthFps) {
int height = 0, width = 0, fps = 0;
get_height_width_fps(&height, &width, &fps, video_file.c_str());
EXPECT_EQ(height, 720);
EXPECT_EQ(width, 1280);
EXPECT_EQ(fps, 25);
}
TEST_F(ReferenceLessVideoAnalysisTest, MatchIdenticalFrameClusters) {
compute_metrics(video_file, &psnr_per_frame, &ssim_per_frame);
compute_metrics(video, &psnr_per_frame, &ssim_per_frame);
std::vector<int> identical_frame_clusters =
find_frame_clusters(psnr_per_frame, ssim_per_frame);
EXPECT_EQ(5, (int)identical_frame_clusters.size());
EXPECT_EQ(1, identical_frame_clusters[0]);
EXPECT_EQ(1, identical_frame_clusters[4]);
}
TEST_F(ReferenceLessVideoAnalysisTest, CheckFileExtension) {
EXPECT_TRUE(check_file_extension(video_file));
std::string txt_file =
webrtc::test::ResourcePath("video_quality_analysis_frame", "txt");
EXPECT_FALSE(check_file_extension(txt_file));
}

View file

@ -19,11 +19,10 @@
#include <utility>
#include "test/testsupport/perf_test.h"
#include "third_party/libyuv/include/libyuv/compare.h"
#include "third_party/libyuv/include/libyuv/convert.h"
#define STATS_LINE_LENGTH 32
#define Y4M_FILE_HEADER_MAX_SIZE 200
#define Y4M_FRAME_DELIMITER "FRAME"
#define Y4M_FRAME_HEADER_SIZE 6
namespace webrtc {
namespace test {
@ -92,160 +91,47 @@ bool GetNextStatsLine(FILE* stats_file, char* line) {
return true;
}
bool ExtractFrameFromYuvFile(const char* i420_file_name,
int width,
int height,
int frame_number,
uint8_t* result_frame) {
int frame_size = GetI420FrameSize(width, height);
int offset = frame_number * frame_size; // Calculate offset for the frame.
bool errors = false;
FILE* input_file = fopen(i420_file_name, "rb");
if (input_file == NULL) {
fprintf(stderr, "Couldn't open input file for reading: %s\n",
i420_file_name);
return false;
}
// Change stream pointer to new offset.
fseek(input_file, offset, SEEK_SET);
size_t bytes_read = fread(result_frame, 1, frame_size, input_file);
if (bytes_read != static_cast<size_t>(frame_size) && ferror(input_file)) {
fprintf(stdout, "Error while reading frame no %d from file %s\n",
frame_number, i420_file_name);
errors = true;
}
fclose(input_file);
return !errors;
template <typename FrameMetricFunction>
static double CalculateMetric(
const FrameMetricFunction& frame_metric_function,
const rtc::scoped_refptr<I420BufferInterface>& ref_buffer,
const rtc::scoped_refptr<I420BufferInterface>& test_buffer) {
RTC_CHECK_EQ(ref_buffer->width(), test_buffer->width());
RTC_CHECK_EQ(ref_buffer->height(), test_buffer->height());
return frame_metric_function(
ref_buffer->DataY(), ref_buffer->StrideY(), ref_buffer->DataU(),
ref_buffer->StrideU(), ref_buffer->DataV(), ref_buffer->StrideV(),
test_buffer->DataY(), test_buffer->StrideY(), test_buffer->DataU(),
test_buffer->StrideU(), test_buffer->DataV(), test_buffer->StrideV(),
test_buffer->width(), test_buffer->height());
}
bool ExtractFrameFromY4mFile(const char* y4m_file_name,
int width,
int height,
int frame_number,
uint8_t* result_frame) {
int frame_size = GetI420FrameSize(width, height);
int inital_offset = frame_number * (frame_size + Y4M_FRAME_HEADER_SIZE);
int frame_offset = 0;
FILE* input_file = fopen(y4m_file_name, "rb");
if (input_file == NULL) {
fprintf(stderr, "Couldn't open input file for reading: %s\n",
y4m_file_name);
return false;
}
// YUV4MPEG2, a.k.a. Y4M File format has a file header and a frame header. The
// file header has the aspect: "YUV4MPEG2 C420 W640 H360 Ip F30:1 A1:1".
char frame_header[Y4M_FILE_HEADER_MAX_SIZE];
size_t bytes_read =
fread(frame_header, 1, Y4M_FILE_HEADER_MAX_SIZE - 1, input_file);
if (bytes_read != static_cast<size_t>(frame_size) && ferror(input_file)) {
fprintf(stdout, "Error while reading frame from file %s\n", y4m_file_name);
fclose(input_file);
return false;
}
frame_header[bytes_read] = '\0';
std::string header_contents(frame_header);
std::size_t found = header_contents.find(Y4M_FRAME_DELIMITER);
if (found == std::string::npos) {
fprintf(stdout, "Corrupted Y4M header, could not find \"FRAME\" in %s\n",
header_contents.c_str());
fclose(input_file);
return false;
}
frame_offset = static_cast<int>(found);
// Change stream pointer to new offset, skipping the frame header as well.
fseek(input_file, inital_offset + frame_offset + Y4M_FRAME_HEADER_SIZE,
SEEK_SET);
bytes_read = fread(result_frame, 1, frame_size, input_file);
if (feof(input_file)) {
fclose(input_file);
return false;
}
if (bytes_read != static_cast<size_t>(frame_size) && ferror(input_file)) {
fprintf(stdout, "Error while reading frame no %d from file %s\n",
frame_number, y4m_file_name);
fclose(input_file);
return false;
}
fclose(input_file);
return true;
}
double CalculateMetrics(VideoAnalysisMetricsType video_metrics_type,
const uint8_t* ref_frame,
const uint8_t* test_frame,
int width,
int height) {
if (!ref_frame || !test_frame)
return -1;
else if (height < 0 || width < 0)
return -1;
int half_width = (width + 1) >> 1;
int half_height = (height + 1) >> 1;
const uint8_t* src_y_a = ref_frame;
const uint8_t* src_u_a = src_y_a + width * height;
const uint8_t* src_v_a = src_u_a + half_width * half_height;
const uint8_t* src_y_b = test_frame;
const uint8_t* src_u_b = src_y_b + width * height;
const uint8_t* src_v_b = src_u_b + half_width * half_height;
int stride_y = width;
int stride_uv = half_width;
double result = 0.0;
switch (video_metrics_type) {
case kPSNR:
// In the following: stride is determined by width.
result = libyuv::I420Psnr(src_y_a, width, src_u_a, half_width, src_v_a,
half_width, src_y_b, width, src_u_b, half_width,
src_v_b, half_width, width, height);
double Psnr(const rtc::scoped_refptr<I420BufferInterface>& ref_buffer,
const rtc::scoped_refptr<I420BufferInterface>& test_buffer) {
// LibYuv sets the max psnr value to 128, we restrict it to 48.
// In case of 0 mse in one frame, 128 can skew the results significantly.
result = (result > 48.0) ? 48.0 : result;
break;
case kSSIM:
result = libyuv::I420Ssim(src_y_a, stride_y, src_u_a, stride_uv, src_v_a,
stride_uv, src_y_b, stride_y, src_u_b,
stride_uv, src_v_b, stride_uv, width, height);
break;
default:
assert(false);
}
return result;
return std::min(48.0,
CalculateMetric(&libyuv::I420Psnr, ref_buffer, test_buffer));
}
void RunAnalysis(const char* reference_file_name,
const char* test_file_name,
double Ssim(const rtc::scoped_refptr<I420BufferInterface>& ref_buffer,
const rtc::scoped_refptr<I420BufferInterface>& test_buffer) {
return CalculateMetric(&libyuv::I420Ssim, ref_buffer, test_buffer);
}
void RunAnalysis(const rtc::scoped_refptr<webrtc::test::Video>& reference_video,
const rtc::scoped_refptr<webrtc::test::Video>& test_video,
const char* stats_file_reference_name,
const char* stats_file_test_name,
int width,
int height,
ResultsContainer* results) {
// Check if the reference_file_name ends with "y4m".
bool y4m_mode = false;
if (std::string(reference_file_name).find("y4m") != std::string::npos) {
y4m_mode = true;
}
int size = GetI420FrameSize(width, height);
FILE* stats_file_ref = fopen(stats_file_reference_name, "r");
FILE* stats_file_test = fopen(stats_file_test_name, "r");
// String buffer for the lines in the stats file.
char line[STATS_LINE_LENGTH];
// Allocate buffers for test and reference frames.
uint8_t* test_frame = new uint8_t[size];
uint8_t* reference_frame = new uint8_t[size];
int previous_frame_number = -1;
// Maps barcode id to the frame id for the reference video.
@ -282,21 +168,14 @@ void RunAnalysis(const char* reference_file_name,
assert(extracted_test_frame != -1);
assert(decoded_frame_number != -1);
ExtractFrameFromYuvFile(test_file_name, width, height, extracted_test_frame,
test_frame);
if (y4m_mode) {
ExtractFrameFromY4mFile(reference_file_name, width, height,
extracted_ref_frame, reference_frame);
} else {
ExtractFrameFromYuvFile(reference_file_name, width, height,
extracted_ref_frame, reference_frame);
}
const rtc::scoped_refptr<webrtc::I420BufferInterface> test_frame =
test_video->GetFrame(extracted_test_frame);
const rtc::scoped_refptr<webrtc::I420BufferInterface> reference_frame =
reference_video->GetFrame(extracted_ref_frame);
// Calculate the PSNR and SSIM.
double result_psnr =
CalculateMetrics(kPSNR, reference_frame, test_frame, width, height);
double result_ssim =
CalculateMetrics(kSSIM, reference_frame, test_frame, width, height);
double result_psnr = Psnr(reference_frame, test_frame);
double result_ssim = Ssim(reference_frame, test_frame);
previous_frame_number = decoded_frame_number;
@ -312,8 +191,6 @@ void RunAnalysis(const char* reference_file_name,
// Cleanup.
fclose(stats_file_ref);
fclose(stats_file_test);
delete[] test_frame;
delete[] reference_frame;
}
std::vector<std::pair<int, int> > CalculateFrameClusters(

View file

@ -15,8 +15,8 @@
#include <utility>
#include <vector>
#include "third_party/libyuv/include/libyuv/compare.h"
#include "third_party/libyuv/include/libyuv/convert.h"
#include "api/video/i420_buffer.h"
#include "rtc_tools/y4m_file_reader.h"
namespace webrtc {
namespace test {
@ -44,8 +44,6 @@ struct ResultsContainer {
int decode_errors_test;
};
enum VideoAnalysisMetricsType { kPSNR, kSSIM };
// A function to run the PSNR and SSIM analysis on the test file. The test file
// comprises the frames that were captured during the quality measurement test.
// There may be missing or duplicate frames. Also the frames start at a random
@ -61,23 +59,23 @@ enum VideoAnalysisMetricsType { kPSNR, kSSIM };
// problem with the decoding there would be 'Barcode error' instead of yyyy.
// The stat files are used to compare the right frames with each other and
// to calculate statistics.
void RunAnalysis(const char* reference_file_name,
const char* test_file_name,
void RunAnalysis(const rtc::scoped_refptr<webrtc::test::Video>& reference_video,
const rtc::scoped_refptr<webrtc::test::Video>& test_video,
const char* stats_file_reference_name,
const char* stats_file_test_name,
int width,
int height,
ResultsContainer* results);
// Compute PSNR or SSIM for an I420 frame (all planes). When we are calculating
// PSNR values, the max return value (in the case where the test and reference
// frames are exactly the same) will be 48. In the case of SSIM the max return
// value will be 1.
double CalculateMetrics(VideoAnalysisMetricsType video_metrics_type,
const uint8_t* ref_frame,
const uint8_t* test_frame,
int width,
int height);
// Compute PSNR for an I420 buffer (all planes). The max return value (in the
// case where the test and reference frames are exactly the same) will be 48.
double Psnr(const rtc::scoped_refptr<I420BufferInterface>& ref_buffer,
const rtc::scoped_refptr<I420BufferInterface>& test_buffer);
// Compute SSIM for an I420 buffer (all planes). The max return value (in the
// case where the test and reference frames are exactly the same) will be 1.
double Ssim(const rtc::scoped_refptr<I420BufferInterface>& ref_buffer,
const rtc::scoped_refptr<I420BufferInterface>& test_buffer);
// Prints the result from the analysis in Chromium performance
// numbers compatible format to stdout. If the results object contains no frames
@ -129,21 +127,6 @@ bool IsThereBarcodeError(std::string line);
// frame_0023 0284, we will get 284.
int ExtractDecodedFrameNumber(std::string line);
// Extracts an I420 frame at position frame_number from the raw YUV file.
bool ExtractFrameFromYuvFile(const char* i420_file_name,
int width,
int height,
int frame_number,
uint8_t* result_frame);
// Extracts an I420 frame at position frame_number from the Y4M file. The first
// frame has corresponded |frame_number| 0.
bool ExtractFrameFromY4mFile(const char* i420_file_name,
int width,
int height,
int frame_number,
uint8_t* result_frame);
} // namespace test
} // namespace webrtc

View file

@ -41,31 +41,6 @@ class VideoQualityAnalysisTest : public ::testing::Test {
std::string stats_filename_;
};
TEST_F(VideoQualityAnalysisTest, MatchExtractedY4mFrame) {
std::string video_file =
webrtc::test::ResourcePath("reference_less_video_test_file", "y4m");
std::string extracted_frame_from_video_file =
webrtc::test::ResourcePath("video_quality_analysis_frame", "txt");
int frame_height = 720, frame_width = 1280;
int frame_number = 2;
int size = GetI420FrameSize(frame_width, frame_height);
uint8_t* result_frame = new uint8_t[size];
uint8_t* expected_frame = new uint8_t[size];
FILE* input_file = fopen(extracted_frame_from_video_file.c_str(), "rb");
fread(expected_frame, 1, size, input_file);
ExtractFrameFromY4mFile(video_file.c_str(), frame_width, frame_height,
frame_number, result_frame);
EXPECT_EQ(*expected_frame, *result_frame);
fclose(input_file);
delete[] result_frame;
delete[] expected_frame;
}
TEST_F(VideoQualityAnalysisTest, PrintAnalysisResultsEmpty) {
ResultsContainer result;
PrintAnalysisResults(logfile_, "Empty", &result);

View file

@ -8,70 +8,42 @@
* be found in the AUTHORS file in the root of the source tree.
*/
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <map>
#include <string>
#include <vector>
#include "rtc_tools/frame_analyzer/video_quality_analysis.h"
#include "rtc_tools/simple_command_line_parser.h"
#include "rtc_tools/y4m_file_reader.h"
#define MAX_NUM_FRAMES_PER_FILE INT_MAX
void CompareFiles(const char* reference_file_name,
const char* test_file_name,
const char* results_file_name,
int width,
int height) {
// Check if the reference_file_name ends with "y4m".
bool y4m_mode = false;
if (std::string(reference_file_name).find("y4m") != std::string::npos) {
y4m_mode = true;
}
void CompareFiles(
const rtc::scoped_refptr<webrtc::test::Video>& reference_video,
const rtc::scoped_refptr<webrtc::test::Video>& test_video,
const char* results_file_name) {
FILE* results_file = fopen(results_file_name, "w");
int size = webrtc::test::GetI420FrameSize(width, height);
// Allocate buffers for test and reference frames.
uint8_t* test_frame = new uint8_t[size];
uint8_t* ref_frame = new uint8_t[size];
bool read_result = true;
for (int frame_counter = 0; frame_counter < MAX_NUM_FRAMES_PER_FILE;
++frame_counter) {
read_result &=
(y4m_mode)
? webrtc::test::ExtractFrameFromY4mFile(
reference_file_name, width, height, frame_counter, ref_frame)
: webrtc::test::ExtractFrameFromYuvFile(
reference_file_name, width, height, frame_counter, ref_frame);
read_result &= webrtc::test::ExtractFrameFromYuvFile(
test_file_name, width, height, frame_counter, test_frame);
if (!read_result)
break;
const size_t num_frames = std::min(reference_video->number_of_frames(),
test_video->number_of_frames());
for (size_t i = 0; i < num_frames; ++i) {
const rtc::scoped_refptr<webrtc::I420BufferInterface> ref_buffer =
reference_video->GetFrame(i);
const rtc::scoped_refptr<webrtc::I420BufferInterface> test_buffer =
test_video->GetFrame(i);
// Calculate the PSNR and SSIM.
double result_psnr = webrtc::test::CalculateMetrics(
webrtc::test::kPSNR, ref_frame, test_frame, width, height);
double result_ssim = webrtc::test::CalculateMetrics(
webrtc::test::kSSIM, ref_frame, test_frame, width, height);
fprintf(results_file, "Frame: %d, PSNR: %f, SSIM: %f\n", frame_counter,
result_psnr, result_ssim);
double result_psnr = webrtc::test::Psnr(ref_buffer, test_buffer);
double result_ssim = webrtc::test::Ssim(ref_buffer, test_buffer);
fprintf(results_file, "Frame: %zu, PSNR: %f, SSIM: %f\n", i, result_psnr,
result_ssim);
}
delete[] test_frame;
delete[] ref_frame;
fclose(results_file);
}
/*
* A tool running PSNR and SSIM analysis on two videos - a reference video and a
* test video. The two videos should be I420 YUV videos.
* test video. The two videos should be I420 Y4M videos.
* The tool just runs PSNR and SSIM on the corresponding frames in the test and
* the reference videos until either the first or the second video runs out of
* frames. The result is written in a results text file in the format:
@ -82,8 +54,7 @@ void CompareFiles(const char* reference_file_name,
*
* Usage:
* psnr_ssim_analyzer --reference_file=<name_of_file> --test_file=<name_of_file>
* --results_file=<name_of_file> --width=<width_of_frames>
* --height=<height_of_frames>
* --results_file=<name_of_file>
*/
int main(int argc, char* argv[]) {
std::string program_name = argv[0];
@ -93,12 +64,8 @@ int main(int argc, char* argv[]) {
"Example usage:\n" +
program_name +
" --reference_file=ref.yuv "
"--test_file=test.yuv --results_file=results.txt --width=320 "
"--height=240\n"
"--test_file=test.yuv --results_file=results.txt\n"
"Command line flags:\n"
" - width(int): The width of the reference and test files. Default: -1\n"
" - height(int): The height of the reference and test files. "
" Default: -1\n"
" - reference_file(string): The reference YUV file to compare against."
" Default: ref.yuv\n"
" - test_file(string): The test YUV file to run the analysis for."
@ -112,8 +79,6 @@ int main(int argc, char* argv[]) {
parser.Init(argc, argv);
parser.SetUsageMessage(usage);
parser.SetFlag("width", "-1");
parser.SetFlag("height", "-1");
parser.SetFlag("results_file", "results.txt");
parser.SetFlag("reference_file", "ref.yuv");
parser.SetFlag("test_file", "test.yuv");
@ -127,16 +92,26 @@ int main(int argc, char* argv[]) {
}
parser.PrintEnteredFlags();
int width = strtol((parser.GetFlag("width")).c_str(), NULL, 10);
int height = strtol((parser.GetFlag("height")).c_str(), NULL, 10);
rtc::scoped_refptr<webrtc::test::Y4mFile> reference_video =
webrtc::test::Y4mFile::Open(parser.GetFlag("reference_file"));
rtc::scoped_refptr<webrtc::test::Y4mFile> test_video =
webrtc::test::Y4mFile::Open(parser.GetFlag("test_file"));
if (width <= 0 || height <= 0) {
fprintf(stderr, "Error: width or height cannot be <= 0!\n");
return -1;
if (!reference_video || !test_video) {
fprintf(stderr, "Error opening video files\n");
return 0;
}
if (reference_video->width() != test_video->width() ||
reference_video->height() != test_video->height()) {
fprintf(stderr,
"Reference and test video files do not have same size: %dx%d "
"versus %dx%d\n",
reference_video->width(), reference_video->height(),
test_video->width(), test_video->height());
return 0;
}
CompareFiles(parser.GetFlag("reference_file").c_str(),
parser.GetFlag("test_file").c_str(),
parser.GetFlag("results_file").c_str(), width, height);
CompareFiles(reference_video, test_video,
parser.GetFlag("results_file").c_str());
return 0;
}

View file

@ -0,0 +1,234 @@
/*
* Copyright (c) 2018 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 "rtc_tools/y4m_file_reader.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <utility>
#include "absl/types/optional.h"
#include "api/video/i420_buffer.h"
#include "rtc_base/logging.h"
#include "rtc_base/refcountedobject.h"
#include "rtc_base/sequenced_task_checker.h"
#include "rtc_base/string_to_number.h"
#include "rtc_base/stringencode.h"
#include "rtc_base/stringutils.h"
namespace webrtc {
namespace test {
Video::Iterator::Iterator(const rtc::scoped_refptr<const Video>& video,
size_t index)
: video_(video), index_(index) {}
Video::Iterator::Iterator(const Video::Iterator& other) = default;
Video::Iterator::Iterator(Video::Iterator&& other) = default;
Video::Iterator& Video::Iterator::operator=(Video::Iterator&&) = default;
Video::Iterator& Video::Iterator::operator=(const Video::Iterator&) = default;
Video::Iterator::~Iterator() = default;
rtc::scoped_refptr<I420BufferInterface> Video::Iterator::operator*() const {
return video_->GetFrame(index_);
}
bool Video::Iterator::operator==(const Video::Iterator& other) const {
return index_ == other.index_;
}
bool Video::Iterator::operator!=(const Video::Iterator& other) const {
return !(*this == other);
}
Video::Iterator Video::Iterator::operator++(int) {
const Iterator copy = *this;
++*this;
return copy;
}
Video::Iterator& Video::Iterator::operator++() {
++index_;
return *this;
}
Video::Iterator Video::begin() const {
return Iterator(this, 0);
}
Video::Iterator Video::end() const {
return Iterator(this, number_of_frames());
}
rtc::scoped_refptr<Y4mFile> Y4mFile::Open(const std::string& file_name) {
FILE* file = fopen(file_name.c_str(), "rb");
if (file == nullptr) {
RTC_LOG(LS_ERROR) << "Could not open input file for reading: " << file_name;
return nullptr;
}
int parse_file_header_result = -1;
fscanf(file, "YUV4MPEG2 %n", &parse_file_header_result);
if (parse_file_header_result == -1) {
RTC_LOG(LS_ERROR) << "File " << file_name
<< " does not start with YUV4MPEG2 header";
return nullptr;
}
std::string header_line;
while (true) {
const int c = fgetc(file);
if (c == EOF) {
RTC_LOG(LS_ERROR) << "Could not read header line";
return nullptr;
}
if (c == '\n')
break;
header_line.push_back(static_cast<char>(c));
}
absl::optional<int> width;
absl::optional<int> height;
absl::optional<float> fps;
std::vector<std::string> fields;
rtc::tokenize(header_line, ' ', &fields);
for (const std::string& field : fields) {
const char prefix = field.front();
const std::string suffix = field.substr(1);
switch (prefix) {
case 'W':
width = rtc::StringToNumber<int>(suffix);
break;
case 'H':
height = rtc::StringToNumber<int>(suffix);
break;
case 'C':
if (suffix != "420" && suffix != "420mpeg2") {
RTC_LOG(LS_ERROR)
<< "Does not support any other color space than I420 or "
"420mpeg2, but was: "
<< suffix;
return nullptr;
}
break;
case 'F': {
std::vector<std::string> fraction;
rtc::tokenize(suffix, ':', &fraction);
if (fraction.size() == 2) {
const absl::optional<int> numerator =
rtc::StringToNumber<int>(fraction[0]);
const absl::optional<int> denominator =
rtc::StringToNumber<int>(fraction[1]);
if (numerator && denominator && *denominator != 0)
fps = *numerator / static_cast<float>(*denominator);
break;
}
}
}
}
if (!width || !height) {
RTC_LOG(LS_ERROR) << "Could not find width and height in file header";
return nullptr;
}
if (!fps) {
RTC_LOG(LS_ERROR) << "Could not find fps in file header";
return nullptr;
}
RTC_LOG(LS_INFO) << "Video has resolution: " << *width << "x" << *height
<< " " << *fps << " fps";
if (*width % 2 != 0 || *height % 2 != 0) {
RTC_LOG(LS_ERROR)
<< "Only supports even width/height so that chroma size is a "
"whole number.";
return nullptr;
}
const int i420_frame_size = 3 * *width * *height / 2;
std::vector<fpos_t> frame_positions;
while (true) {
int parse_frame_header_result = -1;
fscanf(file, "FRAME\n%n", &parse_frame_header_result);
if (parse_frame_header_result == -1) {
if (!feof(file)) {
RTC_LOG(LS_ERROR) << "Did not find FRAME header, ignoring rest of file";
}
break;
}
fpos_t pos;
fgetpos(file, &pos);
frame_positions.push_back(pos);
// Skip over YUV pixel data.
fseek(file, i420_frame_size, SEEK_CUR);
}
if (frame_positions.empty()) {
RTC_LOG(LS_ERROR) << "Could not find any frames in the file";
return nullptr;
}
RTC_LOG(LS_INFO) << "Video has " << frame_positions.size() << " frames";
return new rtc::RefCountedObject<Y4mFile>(*width, *height, *fps,
frame_positions, file);
}
size_t Y4mFile::number_of_frames() const {
return frame_positions_.size();
}
rtc::scoped_refptr<I420BufferInterface> Y4mFile::GetFrame(
size_t frame_index) const {
RTC_DCHECK_CALLED_SEQUENTIALLY(&thread_checker_);
RTC_CHECK_LT(frame_index, frame_positions_.size());
fsetpos(file_, &frame_positions_[frame_index]);
rtc::scoped_refptr<I420Buffer> buffer = I420Buffer::Create(width_, height_);
fread(reinterpret_cast<char*>(buffer->MutableDataY()), /* size= */ 1,
width_ * height_, file_);
fread(reinterpret_cast<char*>(buffer->MutableDataU()), /* size= */ 1,
buffer->ChromaWidth() * buffer->ChromaHeight(), file_);
fread(reinterpret_cast<char*>(buffer->MutableDataV()), /* size= */ 1,
buffer->ChromaWidth() * buffer->ChromaHeight(), file_);
if (ferror(file_) != 0) {
RTC_LOG(LS_ERROR) << "Could not read YUV data for frame " << frame_index;
return nullptr;
}
return buffer;
}
int Y4mFile::width() const {
return width_;
}
int Y4mFile::height() const {
return height_;
}
float Y4mFile::fps() const {
return fps_;
}
Y4mFile::Y4mFile(int width,
int height,
float fps,
const std::vector<fpos_t>& frame_positions,
FILE* file)
: width_(width),
height_(height),
fps_(fps),
frame_positions_(frame_positions),
file_(file) {}
Y4mFile::~Y4mFile() {
fclose(file_);
}
} // namespace test
} // namespace webrtc

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2018 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 RTC_TOOLS_Y4M_FILE_READER_H_
#define RTC_TOOLS_Y4M_FILE_READER_H_
#include <cstdio>
#include <iterator>
#include <string>
#include <vector>
#include "api/video/video_frame.h"
#include "rtc_base/refcount.h"
#include "rtc_base/sequenced_task_checker.h"
namespace webrtc {
namespace test {
// Iterable class representing a sequence of I420 buffers. This class is not
// thread safe because it is expected to be backed by a file.
class Video : public rtc::RefCountInterface {
public:
class Iterator {
public:
typedef int value_type;
typedef std::ptrdiff_t difference_type;
typedef int* pointer;
typedef int& reference;
typedef std::input_iterator_tag iterator_category;
Iterator(const rtc::scoped_refptr<const Video>& video, size_t index);
Iterator(const Iterator& other);
Iterator(Iterator&& other);
Iterator& operator=(Iterator&&);
Iterator& operator=(const Iterator&);
~Iterator();
rtc::scoped_refptr<I420BufferInterface> operator*() const;
bool operator==(const Iterator& other) const;
bool operator!=(const Iterator& other) const;
Iterator operator++(int);
Iterator& operator++();
private:
rtc::scoped_refptr<const Video> video_;
size_t index_;
};
Iterator begin() const;
Iterator end() const;
virtual size_t number_of_frames() const = 0;
virtual rtc::scoped_refptr<I420BufferInterface> GetFrame(
size_t index) const = 0;
};
class Y4mFile : public Video {
public:
// This function opens the file and reads it as an .y4m file. It returns null
// on failure. The file will be closed when the returned object is destroyed.
static rtc::scoped_refptr<Y4mFile> Open(const std::string& file_name);
size_t number_of_frames() const override;
rtc::scoped_refptr<I420BufferInterface> GetFrame(
size_t frame_index) const override;
int width() const;
int height() const;
float fps() const;
protected:
Y4mFile(int width,
int height,
float fps,
const std::vector<fpos_t>& frame_positions,
FILE* file);
~Y4mFile() override;
private:
const int width_;
const int height_;
const float fps_;
const std::vector<fpos_t> frame_positions_;
const rtc::SequencedTaskChecker thread_checker_;
FILE* const file_;
};
} // namespace test
} // namespace webrtc
#endif // RTC_TOOLS_Y4M_FILE_READER_H_

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2018 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 "rtc_tools/y4m_file_reader.h"
#include "test/gtest.h"
#include "test/testsupport/fileutils.h"
namespace webrtc {
namespace test {
class Y4mFileReaderTest : public ::testing::Test {
public:
void SetUp() override {
const std::string filename =
TempFilename(webrtc::test::OutputPath(), "test_video_file.y4m");
// Create simple test video of size 6x4.
FILE* file = fopen(filename.c_str(), "wb");
ASSERT_TRUE(file != nullptr);
fprintf(file, "YUV4MPEG2 W6 H4 F57:10 C420 dummyParam\n");
fprintf(file, "FRAME\n");
const int width = 6;
const int height = 4;
const int i40_size = width * height * 3 / 2;
for (int i = 0; i < i40_size; ++i)
fputc(static_cast<char>(i), file);
fprintf(file, "FRAME\n");
for (int i = 0; i < i40_size; ++i)
fputc(static_cast<char>(i + i40_size), file);
fclose(file);
// Open the newly created file.
video = webrtc::test::Y4mFile::Open(filename);
ASSERT_TRUE(video);
}
rtc::scoped_refptr<webrtc::test::Y4mFile> video;
};
TEST_F(Y4mFileReaderTest, TestParsingFileHeader) {
EXPECT_EQ(6, video->width());
EXPECT_EQ(4, video->height());
EXPECT_EQ(5.7f, video->fps());
}
TEST_F(Y4mFileReaderTest, TestParsingNumberOfFrames) {
EXPECT_EQ(2u, video->number_of_frames());
}
TEST_F(Y4mFileReaderTest, TestPixelContent) {
int cnt = 0;
for (const rtc::scoped_refptr<I420BufferInterface> frame : *video) {
for (int i = 0; i < 6 * 4; ++i, ++cnt)
EXPECT_EQ(cnt, frame->DataY()[i]);
for (int i = 0; i < 3 * 2; ++i, ++cnt)
EXPECT_EQ(cnt, frame->DataU()[i]);
for (int i = 0; i < 3 * 2; ++i, ++cnt)
EXPECT_EQ(cnt, frame->DataV()[i]);
}
}
} // namespace test
} // namespace webrtc