diff --git a/resources/video_quality_analysis_frame.txt.sha1 b/resources/video_quality_analysis_frame.txt.sha1 deleted file mode 100644 index e4a7c73280..0000000000 --- a/resources/video_quality_analysis_frame.txt.sha1 +++ /dev/null @@ -1 +0,0 @@ -4d1ac894f1743af8059e8d8ae2465f6eaa1790b0 \ No newline at end of file diff --git a/rtc_tools/BUILD.gn b/rtc_tools/BUILD.gn index 99f1ae6b4f..f6b03daed5 100644 --- a/rtc_tools/BUILD.gn +++ b/rtc_tools/BUILD.gn @@ -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", diff --git a/rtc_tools/frame_analyzer/frame_analyzer.cc b/rtc_tools/frame_analyzer/frame_analyzer.cc index b3d9b94b0a..f5a9c2b827 100644 --- a/rtc_tools/frame_analyzer/frame_analyzer.cc +++ b/rtc_tools/frame_analyzer/frame_analyzer.cc @@ -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 reference_video = + webrtc::test::Y4mFile::Open(parser.GetFlag("reference_file")); + rtc::scoped_refptr 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); diff --git a/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.cc b/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.cc index 9a7535b213..96e0ef4e30 100644 --- a/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.cc +++ b/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.cc @@ -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 psnr_per_frame, std::vector ssim_per_frame, size_t frame) { @@ -135,60 +105,29 @@ void print_freezing_metrics(const std::vector& psnr_per_frame, printf("\n"); } -void compute_metrics(const std::string& video_file_name, +void compute_metrics(const rtc::scoped_refptr& video, std::vector* psnr_per_frame, std::vector* 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 current_frame = + video->GetFrame(i); + const rtc::scoped_refptr 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 psnr_per_frame; std::vector ssim_per_frame; - if (check_file_extension(video_file)) { - compute_metrics(video_file, &psnr_per_frame, &ssim_per_frame); + rtc::scoped_refptr video = + webrtc::test::Y4mFile::Open(video_file); + if (video) { + compute_metrics(video, &psnr_per_frame, &ssim_per_frame); } else { return -1; } diff --git a/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.h b/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.h index a1de03baf6..6da9f4ee07 100644 --- a/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.h +++ b/rtc_tools/frame_analyzer/reference_less_video_analysis_lib.h @@ -14,12 +14,7 @@ #include #include -// 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& 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& video, std::vector* psnr_per_frame, std::vector* 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); diff --git a/rtc_tools/frame_analyzer/reference_less_video_analysis_unittest.cc b/rtc_tools/frame_analyzer/reference_less_video_analysis_unittest.cc index 4e20532000..2c3cf3af5b 100644 --- a/rtc_tools/frame_analyzer/reference_less_video_analysis_unittest.cc +++ b/rtc_tools/frame_analyzer/reference_less_video_analysis_unittest.cc @@ -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 video; std::vector psnr_per_frame; std::vector 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 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)); -} diff --git a/rtc_tools/frame_analyzer/video_quality_analysis.cc b/rtc_tools/frame_analyzer/video_quality_analysis.cc index 60a4a016cc..dda55b8030 100644 --- a/rtc_tools/frame_analyzer/video_quality_analysis.cc +++ b/rtc_tools/frame_analyzer/video_quality_analysis.cc @@ -19,11 +19,10 @@ #include #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(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 +static double CalculateMetric( + const FrameMetricFunction& frame_metric_function, + const rtc::scoped_refptr& ref_buffer, + const rtc::scoped_refptr& 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(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(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(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 Psnr(const rtc::scoped_refptr& ref_buffer, + const rtc::scoped_refptr& 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. + return std::min(48.0, + CalculateMetric(&libyuv::I420Psnr, ref_buffer, test_buffer)); } -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); - // 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; +double Ssim(const rtc::scoped_refptr& ref_buffer, + const rtc::scoped_refptr& test_buffer) { + return CalculateMetric(&libyuv::I420Ssim, ref_buffer, test_buffer); } -void RunAnalysis(const char* reference_file_name, - const char* test_file_name, +void RunAnalysis(const rtc::scoped_refptr& reference_video, + const rtc::scoped_refptr& 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 test_frame = + test_video->GetFrame(extracted_test_frame); + const rtc::scoped_refptr 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 > CalculateFrameClusters( diff --git a/rtc_tools/frame_analyzer/video_quality_analysis.h b/rtc_tools/frame_analyzer/video_quality_analysis.h index dca719d295..0ebd0c70d9 100644 --- a/rtc_tools/frame_analyzer/video_quality_analysis.h +++ b/rtc_tools/frame_analyzer/video_quality_analysis.h @@ -15,8 +15,8 @@ #include #include -#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& reference_video, + const rtc::scoped_refptr& 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& ref_buffer, + const rtc::scoped_refptr& 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& ref_buffer, + const rtc::scoped_refptr& 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 diff --git a/rtc_tools/frame_analyzer/video_quality_analysis_unittest.cc b/rtc_tools/frame_analyzer/video_quality_analysis_unittest.cc index d1edb30a19..d9565b24a7 100644 --- a/rtc_tools/frame_analyzer/video_quality_analysis_unittest.cc +++ b/rtc_tools/frame_analyzer/video_quality_analysis_unittest.cc @@ -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); diff --git a/rtc_tools/psnr_ssim_analyzer/psnr_ssim_analyzer.cc b/rtc_tools/psnr_ssim_analyzer/psnr_ssim_analyzer.cc index dc63aca9d7..b995f543ea 100644 --- a/rtc_tools/psnr_ssim_analyzer/psnr_ssim_analyzer.cc +++ b/rtc_tools/psnr_ssim_analyzer/psnr_ssim_analyzer.cc @@ -8,70 +8,42 @@ * be found in the AUTHORS file in the root of the source tree. */ -#include #include #include -#include #include -#include #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& reference_video, + const rtc::scoped_refptr& 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 ref_buffer = + reference_video->GetFrame(i); + const rtc::scoped_refptr 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= --test_file= - * --results_file= --width= - * --height= + * --results_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 reference_video = + webrtc::test::Y4mFile::Open(parser.GetFlag("reference_file")); + rtc::scoped_refptr 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; } diff --git a/rtc_tools/y4m_file_reader.cc b/rtc_tools/y4m_file_reader.cc new file mode 100644 index 0000000000..e2bc399fa8 --- /dev/null +++ b/rtc_tools/y4m_file_reader.cc @@ -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 +#include +#include +#include +#include + +#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& 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 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::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(c)); + } + + absl::optional width; + absl::optional height; + absl::optional fps; + + std::vector 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(suffix); + break; + case 'H': + height = rtc::StringToNumber(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 fraction; + rtc::tokenize(suffix, ':', &fraction); + if (fraction.size() == 2) { + const absl::optional numerator = + rtc::StringToNumber(fraction[0]); + const absl::optional denominator = + rtc::StringToNumber(fraction[1]); + if (numerator && denominator && *denominator != 0) + fps = *numerator / static_cast(*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 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(*width, *height, *fps, + frame_positions, file); +} + +size_t Y4mFile::number_of_frames() const { + return frame_positions_.size(); +} + +rtc::scoped_refptr 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 buffer = I420Buffer::Create(width_, height_); + fread(reinterpret_cast(buffer->MutableDataY()), /* size= */ 1, + width_ * height_, file_); + fread(reinterpret_cast(buffer->MutableDataU()), /* size= */ 1, + buffer->ChromaWidth() * buffer->ChromaHeight(), file_); + fread(reinterpret_cast(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& frame_positions, + FILE* file) + : width_(width), + height_(height), + fps_(fps), + frame_positions_(frame_positions), + file_(file) {} + +Y4mFile::~Y4mFile() { + fclose(file_); +} + +} // namespace test +} // namespace webrtc diff --git a/rtc_tools/y4m_file_reader.h b/rtc_tools/y4m_file_reader.h new file mode 100644 index 0000000000..b11beef28c --- /dev/null +++ b/rtc_tools/y4m_file_reader.h @@ -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 +#include +#include +#include + +#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& video, size_t index); + Iterator(const Iterator& other); + Iterator(Iterator&& other); + Iterator& operator=(Iterator&&); + Iterator& operator=(const Iterator&); + ~Iterator(); + + rtc::scoped_refptr operator*() const; + bool operator==(const Iterator& other) const; + bool operator!=(const Iterator& other) const; + + Iterator operator++(int); + Iterator& operator++(); + + private: + rtc::scoped_refptr video_; + size_t index_; + }; + + Iterator begin() const; + Iterator end() const; + + virtual size_t number_of_frames() const = 0; + virtual rtc::scoped_refptr 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 Open(const std::string& file_name); + + size_t number_of_frames() const override; + + rtc::scoped_refptr 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& frame_positions, + FILE* file); + ~Y4mFile() override; + + private: + const int width_; + const int height_; + const float fps_; + const std::vector frame_positions_; + const rtc::SequencedTaskChecker thread_checker_; + FILE* const file_; +}; + +} // namespace test +} // namespace webrtc + +#endif // RTC_TOOLS_Y4M_FILE_READER_H_ diff --git a/rtc_tools/y4m_file_reader_unittest.cc b/rtc_tools/y4m_file_reader_unittest.cc new file mode 100644 index 0000000000..3c6c92f08b --- /dev/null +++ b/rtc_tools/y4m_file_reader_unittest.cc @@ -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(i), file); + fprintf(file, "FRAME\n"); + for (int i = 0; i < i40_size; ++i) + fputc(static_cast(i + i40_size), file); + fclose(file); + + // Open the newly created file. + video = webrtc::test::Y4mFile::Open(filename); + ASSERT_TRUE(video); + } + + rtc::scoped_refptr 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 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