|  | /* | 
|  | *  Copyright (c) 2012 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/frame_analyzer/video_quality_analysis.h" | 
|  |  | 
|  | #include <assert.h> | 
|  | #include <stdio.h> | 
|  | #include <stdlib.h> | 
|  | #include <algorithm> | 
|  | #include <map> | 
|  | #include <string> | 
|  | #include <utility> | 
|  |  | 
|  | #include "test/testsupport/perf_test.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 { | 
|  |  | 
|  | ResultsContainer::ResultsContainer() {} | 
|  | ResultsContainer::~ResultsContainer() {} | 
|  |  | 
|  | int GetI420FrameSize(int width, int height) { | 
|  | int half_width = (width + 1) >> 1; | 
|  | int half_height = (height + 1) >> 1; | 
|  |  | 
|  | int y_plane = width * height;            // I420 Y plane. | 
|  | int u_plane = half_width * half_height;  // I420 U plane. | 
|  | int v_plane = half_width * half_height;  // I420 V plane. | 
|  |  | 
|  | return y_plane + u_plane + v_plane; | 
|  | } | 
|  |  | 
|  | int ExtractFrameSequenceNumber(std::string line) { | 
|  | size_t space_position = line.find(' '); | 
|  | if (space_position == std::string::npos) { | 
|  | return -1; | 
|  | } | 
|  | std::string frame = line.substr(0, space_position); | 
|  |  | 
|  | size_t underscore_position = frame.find('_'); | 
|  | if (underscore_position == std::string::npos) { | 
|  | return -1; | 
|  | } | 
|  | std::string frame_number = frame.substr(underscore_position + 1); | 
|  |  | 
|  | return strtol(frame_number.c_str(), NULL, 10); | 
|  | } | 
|  |  | 
|  | int ExtractDecodedFrameNumber(std::string line) { | 
|  | size_t space_position = line.find(' '); | 
|  | if (space_position == std::string::npos) { | 
|  | return -1; | 
|  | } | 
|  | std::string decoded_number = line.substr(space_position + 1); | 
|  |  | 
|  | return strtol(decoded_number.c_str(), NULL, 10); | 
|  | } | 
|  |  | 
|  | bool IsThereBarcodeError(std::string line) { | 
|  | size_t barcode_error_position = line.find("Barcode error"); | 
|  | if (barcode_error_position != std::string::npos) { | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool GetNextStatsLine(FILE* stats_file, char* line) { | 
|  | int chars = 0; | 
|  | char buf = 0; | 
|  |  | 
|  | while (buf != '\n') { | 
|  | size_t chars_read = fread(&buf, 1, 1, stats_file); | 
|  | if (chars_read != 1 || feof(stats_file)) { | 
|  | return false; | 
|  | } | 
|  | line[chars] = buf; | 
|  | ++chars; | 
|  | } | 
|  | line[chars - 1] = '\0';  // Strip the trailing \n and put end of string. | 
|  | 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; | 
|  | } | 
|  |  | 
|  | 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); | 
|  | // 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; | 
|  | } | 
|  |  | 
|  | void RunAnalysis(const char* reference_file_name, | 
|  | const char* test_file_name, | 
|  | 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. | 
|  | // In case two frames have same id, then we only save the first one. | 
|  | std::map<int, int> ref_barcode_to_frame; | 
|  | // While there are entries in the stats file. | 
|  | while (GetNextStatsLine(stats_file_ref, line)) { | 
|  | int extracted_ref_frame = ExtractFrameSequenceNumber(line); | 
|  | int decoded_frame_number = ExtractDecodedFrameNumber(line); | 
|  |  | 
|  | // Insert will only add if it is not in map already. | 
|  | ref_barcode_to_frame.insert( | 
|  | std::make_pair(decoded_frame_number, extracted_ref_frame)); | 
|  | } | 
|  |  | 
|  | while (GetNextStatsLine(stats_file_test, line)) { | 
|  | int extracted_test_frame = ExtractFrameSequenceNumber(line); | 
|  | int decoded_frame_number = ExtractDecodedFrameNumber(line); | 
|  | auto it = ref_barcode_to_frame.find(decoded_frame_number); | 
|  | if (it == ref_barcode_to_frame.end()) { | 
|  | // Not found in the reference video. | 
|  | // TODO(mandermo) print | 
|  | continue; | 
|  | } | 
|  | int extracted_ref_frame = it->second; | 
|  |  | 
|  | // If there was problem decoding the barcode in this frame or the frame has | 
|  | // been duplicated, continue. | 
|  | if (IsThereBarcodeError(line) || | 
|  | decoded_frame_number == previous_frame_number) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | 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); | 
|  | } | 
|  |  | 
|  | // 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); | 
|  |  | 
|  | previous_frame_number = decoded_frame_number; | 
|  |  | 
|  | // Fill in the result struct. | 
|  | AnalysisResult result; | 
|  | result.frame_number = decoded_frame_number; | 
|  | result.psnr_value = result_psnr; | 
|  | result.ssim_value = result_ssim; | 
|  |  | 
|  | results->frames.push_back(result); | 
|  | } | 
|  |  | 
|  | // Cleanup. | 
|  | fclose(stats_file_ref); | 
|  | fclose(stats_file_test); | 
|  | delete[] test_frame; | 
|  | delete[] reference_frame; | 
|  | } | 
|  |  | 
|  | std::vector<std::pair<int, int> > CalculateFrameClusters( | 
|  | FILE* file, | 
|  | int* num_decode_errors) { | 
|  | if (num_decode_errors) { | 
|  | *num_decode_errors = 0; | 
|  | } | 
|  | std::vector<std::pair<int, int> > frame_cnt; | 
|  | char line[STATS_LINE_LENGTH]; | 
|  | while (GetNextStatsLine(file, line)) { | 
|  | int decoded_frame_number; | 
|  | if (IsThereBarcodeError(line)) { | 
|  | decoded_frame_number = DECODE_ERROR; | 
|  | if (num_decode_errors) { | 
|  | ++*num_decode_errors; | 
|  | } | 
|  | } else { | 
|  | decoded_frame_number = ExtractDecodedFrameNumber(line); | 
|  | } | 
|  | if (frame_cnt.size() >= 2 && decoded_frame_number != DECODE_ERROR && | 
|  | frame_cnt.back().first == DECODE_ERROR && | 
|  | frame_cnt[frame_cnt.size() - 2].first == decoded_frame_number) { | 
|  | // Handle when there is a decoding error inside a cluster of frames. | 
|  | frame_cnt[frame_cnt.size() - 2].second += frame_cnt.back().second + 1; | 
|  | frame_cnt.pop_back(); | 
|  | } else if (frame_cnt.empty() || | 
|  | frame_cnt.back().first != decoded_frame_number) { | 
|  | frame_cnt.push_back(std::make_pair(decoded_frame_number, 1)); | 
|  | } else { | 
|  | ++frame_cnt.back().second; | 
|  | } | 
|  | } | 
|  | return frame_cnt; | 
|  | } | 
|  |  | 
|  | void GetMaxRepeatedAndSkippedFrames(const std::string& stats_file_ref_name, | 
|  | const std::string& stats_file_test_name, | 
|  | ResultsContainer* results) { | 
|  | FILE* stats_file_ref = fopen(stats_file_ref_name.c_str(), "r"); | 
|  | FILE* stats_file_test = fopen(stats_file_test_name.c_str(), "r"); | 
|  | if (stats_file_ref == NULL) { | 
|  | fprintf(stderr, "Couldn't open reference stats file for reading: %s\n", | 
|  | stats_file_ref_name.c_str()); | 
|  | return; | 
|  | } | 
|  | if (stats_file_test == NULL) { | 
|  | fprintf(stderr, "Couldn't open test stats file for reading: %s\n", | 
|  | stats_file_test_name.c_str()); | 
|  | fclose(stats_file_ref); | 
|  | return; | 
|  | } | 
|  |  | 
|  | int max_repeated_frames = 1; | 
|  | int max_skipped_frames = 0; | 
|  |  | 
|  | int decode_errors_ref = 0; | 
|  | int decode_errors_test = 0; | 
|  |  | 
|  | std::vector<std::pair<int, int> > frame_cnt_ref = | 
|  | CalculateFrameClusters(stats_file_ref, &decode_errors_ref); | 
|  |  | 
|  | std::vector<std::pair<int, int> > frame_cnt_test = | 
|  | CalculateFrameClusters(stats_file_test, &decode_errors_test); | 
|  |  | 
|  | fclose(stats_file_ref); | 
|  | fclose(stats_file_test); | 
|  |  | 
|  | auto it_ref = frame_cnt_ref.begin(); | 
|  | auto it_test = frame_cnt_test.begin(); | 
|  | auto end_ref = frame_cnt_ref.end(); | 
|  | auto end_test = frame_cnt_test.end(); | 
|  |  | 
|  | if (it_test == end_test || it_ref == end_ref) { | 
|  | fprintf(stderr, "Either test or ref file is empty, nothing to print\n"); | 
|  | return; | 
|  | } | 
|  |  | 
|  | while (it_test != end_test && it_test->first == DECODE_ERROR) { | 
|  | ++it_test; | 
|  | } | 
|  |  | 
|  | if (it_test == end_test) { | 
|  | fprintf(stderr, "Test video only has barcode decode errors\n"); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Find the first frame in the reference video that match the first frame in | 
|  | // the test video. | 
|  | while (it_ref != end_ref && | 
|  | (it_ref->first == DECODE_ERROR || it_ref->first != it_test->first)) { | 
|  | ++it_ref; | 
|  | } | 
|  | if (it_ref == end_ref) { | 
|  | fprintf(stderr, | 
|  | "The barcode in the test video's first frame is not in the " | 
|  | "reference video.\n"); | 
|  | return; | 
|  | } | 
|  |  | 
|  | int total_skipped_frames = 0; | 
|  | for (;;) { | 
|  | max_repeated_frames = | 
|  | std::max(max_repeated_frames, it_test->second - it_ref->second + 1); | 
|  |  | 
|  | bool passed_error = false; | 
|  |  | 
|  | ++it_test; | 
|  | while (it_test != end_test && it_test->first == DECODE_ERROR) { | 
|  | ++it_test; | 
|  | passed_error = true; | 
|  | } | 
|  | if (it_test == end_test) { | 
|  | break; | 
|  | } | 
|  |  | 
|  | int skipped_frames = 0; | 
|  | ++it_ref; | 
|  | for (; it_ref != end_ref; ++it_ref) { | 
|  | if (it_ref->first != DECODE_ERROR && it_ref->first >= it_test->first) { | 
|  | break; | 
|  | } | 
|  | ++skipped_frames; | 
|  | } | 
|  | if (passed_error) { | 
|  | // If we pass an error in the test video, then we are conservative | 
|  | // and will not calculate skipped frames for that part. | 
|  | skipped_frames = 0; | 
|  | } | 
|  | if (it_ref != end_ref && it_ref->first == it_test->first) { | 
|  | total_skipped_frames += skipped_frames; | 
|  | if (skipped_frames > max_skipped_frames) { | 
|  | max_skipped_frames = skipped_frames; | 
|  | } | 
|  | continue; | 
|  | } | 
|  | fprintf(stdout, | 
|  | "Found barcode %d in test video, which is not in reference video\n", | 
|  | it_test->first); | 
|  | break; | 
|  | } | 
|  |  | 
|  | results->max_repeated_frames = max_repeated_frames; | 
|  | results->max_skipped_frames = max_skipped_frames; | 
|  | results->total_skipped_frames = total_skipped_frames; | 
|  | results->decode_errors_ref = decode_errors_ref; | 
|  | results->decode_errors_test = decode_errors_test; | 
|  | } | 
|  |  | 
|  | void PrintAnalysisResults(const std::string& label, ResultsContainer* results) { | 
|  | PrintAnalysisResults(stdout, label, results); | 
|  | } | 
|  |  | 
|  | void PrintAnalysisResults(FILE* output, | 
|  | const std::string& label, | 
|  | ResultsContainer* results) { | 
|  | SetPerfResultsOutput(output); | 
|  |  | 
|  | if (results->frames.size() > 0u) { | 
|  | PrintResult("Unique_frames_count", "", label, results->frames.size(), | 
|  | "score", false); | 
|  |  | 
|  | std::vector<double> psnr_values; | 
|  | std::vector<double> ssim_values; | 
|  | for (const auto& frame : results->frames) { | 
|  | psnr_values.push_back(frame.psnr_value); | 
|  | ssim_values.push_back(frame.ssim_value); | 
|  | } | 
|  |  | 
|  | PrintResultList("PSNR", "", label, psnr_values, "dB", false); | 
|  | PrintResultList("SSIM", "", label, ssim_values, "score", false); | 
|  | } | 
|  |  | 
|  | PrintResult("Max_repeated", "", label, results->max_repeated_frames, "", | 
|  | false); | 
|  | PrintResult("Max_skipped", "", label, results->max_skipped_frames, "", false); | 
|  | PrintResult("Total_skipped", "", label, results->total_skipped_frames, "", | 
|  | false); | 
|  | PrintResult("Decode_errors_reference", "", label, results->decode_errors_ref, | 
|  | "", false); | 
|  | PrintResult("Decode_errors_test", "", label, results->decode_errors_test, "", | 
|  | false); | 
|  | } | 
|  |  | 
|  | }  // namespace test | 
|  | }  // namespace webrtc |