1/*
2 *  Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#include "webrtc/tools/frame_analyzer/video_quality_analysis.h"
12
13#include <assert.h>
14#include <stdio.h>
15#include <stdlib.h>
16
17#include <string>
18
19#define STATS_LINE_LENGTH 32
20#define Y4M_FILE_HEADER_MAX_SIZE 200
21#define Y4M_FRAME_DELIMITER "FRAME"
22#define Y4M_FRAME_HEADER_SIZE 6
23
24namespace webrtc {
25namespace test {
26
27using std::string;
28
29int GetI420FrameSize(int width, int height) {
30  int half_width = (width + 1) >> 1;
31  int half_height = (height + 1) >> 1;
32
33  int y_plane = width * height;  // I420 Y plane.
34  int u_plane = half_width * half_height;  // I420 U plane.
35  int v_plane = half_width * half_height;  // I420 V plane.
36
37  return y_plane + u_plane + v_plane;
38}
39
40int ExtractFrameSequenceNumber(std::string line) {
41  size_t space_position = line.find(' ');
42  if (space_position == string::npos) {
43    return -1;
44  }
45  std::string frame = line.substr(0, space_position);
46
47  size_t underscore_position = frame.find('_');
48  if (underscore_position == string::npos) {
49    return -1;
50  }
51  std::string frame_number = frame.substr(underscore_position + 1);
52
53  return strtol(frame_number.c_str(), NULL, 10);
54}
55
56int ExtractDecodedFrameNumber(std::string line) {
57  size_t space_position = line.find(' ');
58  if (space_position == string::npos) {
59    return -1;
60  }
61  std::string decoded_number = line.substr(space_position + 1);
62
63  return strtol(decoded_number.c_str(), NULL, 10);
64}
65
66bool IsThereBarcodeError(std::string line) {
67  size_t barcode_error_position = line.find("Barcode error");
68  if (barcode_error_position != string::npos) {
69    return true;
70  }
71  return false;
72}
73
74bool GetNextStatsLine(FILE* stats_file, char* line) {
75  int chars = 0;
76  char buf = 0;
77
78  while (buf != '\n') {
79    size_t chars_read = fread(&buf, 1, 1, stats_file);
80    if (chars_read != 1 || feof(stats_file)) {
81      return false;
82    }
83    line[chars] = buf;
84    ++chars;
85  }
86  line[chars-1] = '\0';  // Strip the trailing \n and put end of string.
87  return true;
88}
89
90bool ExtractFrameFromYuvFile(const char* i420_file_name, int width, int height,
91                             int frame_number, uint8* result_frame) {
92  int frame_size = GetI420FrameSize(width, height);
93  int offset = frame_number * frame_size;  // Calculate offset for the frame.
94  bool errors = false;
95
96  FILE* input_file = fopen(i420_file_name, "rb");
97  if (input_file == NULL) {
98    fprintf(stderr, "Couldn't open input file for reading: %s\n",
99            i420_file_name);
100    return false;
101  }
102
103  // Change stream pointer to new offset.
104  fseek(input_file, offset, SEEK_SET);
105
106  size_t bytes_read = fread(result_frame, 1, frame_size, input_file);
107  if (bytes_read != static_cast<size_t>(frame_size) &&
108      ferror(input_file)) {
109    fprintf(stdout, "Error while reading frame no %d from file %s\n",
110            frame_number, i420_file_name);
111    errors = true;
112  }
113  fclose(input_file);
114  return !errors;
115}
116
117bool ExtractFrameFromY4mFile(const char* y4m_file_name, int width, int height,
118                             int frame_number, uint8* result_frame) {
119  int frame_size = GetI420FrameSize(width, height);
120  int frame_offset = frame_number * frame_size;
121  bool errors = false;
122
123  FILE* input_file = fopen(y4m_file_name, "rb");
124  if (input_file == NULL) {
125    fprintf(stderr, "Couldn't open input file for reading: %s\n",
126            y4m_file_name);
127    return false;
128  }
129
130  // YUV4MPEG2, a.k.a. Y4M File format has a file header and a frame header. The
131  // file header has the aspect: "YUV4MPEG2 C420 W640 H360 Ip F30:1 A1:1".
132  // Skip the header if this is the first frame of the file.
133  if (frame_number == 0) {
134    char frame_header[Y4M_FILE_HEADER_MAX_SIZE];
135    size_t bytes_read =
136        fread(frame_header, 1, Y4M_FILE_HEADER_MAX_SIZE, input_file);
137    if (bytes_read != static_cast<size_t>(frame_size) && ferror(input_file)) {
138      fprintf(stdout, "Error while reading first frame from file %s\n",
139          y4m_file_name);
140      fclose(input_file);
141      return false;
142    }
143    std::string header_contents(frame_header);
144    std::size_t found = header_contents.find(Y4M_FRAME_DELIMITER);
145    if (found == std::string::npos) {
146      fprintf(stdout, "Corrupted Y4M header, could not find \"FRAME\" in %s\n",
147          header_contents.c_str());
148      fclose(input_file);
149      return false;
150    }
151    frame_offset = static_cast<int>(found);
152  }
153
154  // Change stream pointer to new offset, skipping the frame header as well.
155  fseek(input_file, frame_offset + Y4M_FRAME_HEADER_SIZE, SEEK_SET);
156
157  size_t bytes_read = fread(result_frame, 1, frame_size, input_file);
158  if (bytes_read != static_cast<size_t>(frame_size) &&
159      ferror(input_file)) {
160    fprintf(stdout, "Error while reading frame no %d from file %s\n",
161            frame_number, y4m_file_name);
162    errors = true;
163  }
164
165  fclose(input_file);
166  return !errors;
167}
168
169double CalculateMetrics(VideoAnalysisMetricsType video_metrics_type,
170                        const uint8* ref_frame,  const uint8* test_frame,
171                        int width, int height) {
172  if (!ref_frame || !test_frame)
173    return -1;
174  else if (height < 0 || width < 0)
175    return -1;
176  int half_width = (width + 1) >> 1;
177  int half_height = (height + 1) >> 1;
178  const uint8* src_y_a = ref_frame;
179  const uint8* src_u_a = src_y_a + width * height;
180  const uint8* src_v_a = src_u_a + half_width * half_height;
181  const uint8* src_y_b = test_frame;
182  const uint8* src_u_b = src_y_b + width * height;
183  const uint8* src_v_b = src_u_b + half_width * half_height;
184
185  int stride_y = width;
186  int stride_uv = half_width;
187
188  double result = 0.0;
189
190  switch (video_metrics_type) {
191    case kPSNR:
192      // In the following: stride is determined by width.
193      result = libyuv::I420Psnr(src_y_a, width, src_u_a, half_width,
194                                src_v_a, half_width, src_y_b, width,
195                                src_u_b, half_width, src_v_b, half_width,
196                                width, height);
197      // LibYuv sets the max psnr value to 128, we restrict it to 48.
198      // In case of 0 mse in one frame, 128 can skew the results significantly.
199      result = (result > 48.0) ? 48.0 : result;
200      break;
201    case kSSIM:
202      result = libyuv::I420Ssim(src_y_a, stride_y, src_u_a, stride_uv,
203                                src_v_a, stride_uv, src_y_b, stride_y,
204                                src_u_b, stride_uv, src_v_b, stride_uv,
205                                width, height);
206      break;
207    default:
208      assert(false);
209  }
210
211  return result;
212}
213
214void RunAnalysis(const char* reference_file_name, const char* test_file_name,
215                 const char* stats_file_name, int width, int height,
216                 ResultsContainer* results) {
217  // Check if the reference_file_name ends with "y4m".
218  bool y4m_mode = false;
219  if (std::string(reference_file_name).find("y4m") != std::string::npos){
220    y4m_mode = true;
221  }
222
223  int size = GetI420FrameSize(width, height);
224  FILE* stats_file = fopen(stats_file_name, "r");
225
226  // String buffer for the lines in the stats file.
227  char line[STATS_LINE_LENGTH];
228
229  // Allocate buffers for test and reference frames.
230  uint8* test_frame = new uint8[size];
231  uint8* reference_frame = new uint8[size];
232  int previous_frame_number = -1;
233
234  // While there are entries in the stats file.
235  while (GetNextStatsLine(stats_file, line)) {
236    int extracted_test_frame = ExtractFrameSequenceNumber(line);
237    int decoded_frame_number = ExtractDecodedFrameNumber(line);
238
239    // If there was problem decoding the barcode in this frame or the frame has
240    // been duplicated, continue.
241    if (IsThereBarcodeError(line) ||
242        decoded_frame_number == previous_frame_number) {
243      continue;
244    }
245
246    assert(extracted_test_frame != -1);
247    assert(decoded_frame_number != -1);
248
249    ExtractFrameFromYuvFile(test_file_name, width, height, extracted_test_frame,
250                            test_frame);
251    if (y4m_mode) {
252      ExtractFrameFromY4mFile(reference_file_name, width, height,
253                              decoded_frame_number, reference_frame);
254    } else {
255      ExtractFrameFromYuvFile(reference_file_name, width, height,
256                              decoded_frame_number, reference_frame);
257    }
258
259    // Calculate the PSNR and SSIM.
260    double result_psnr = CalculateMetrics(kPSNR, reference_frame, test_frame,
261                                          width, height);
262    double result_ssim = CalculateMetrics(kSSIM, reference_frame, test_frame,
263                                          width, height);
264
265    previous_frame_number = decoded_frame_number;
266
267    // Fill in the result struct.
268    AnalysisResult result;
269    result.frame_number = decoded_frame_number;
270    result.psnr_value = result_psnr;
271    result.ssim_value = result_ssim;
272
273    results->frames.push_back(result);
274  }
275
276  // Cleanup.
277  fclose(stats_file);
278  delete[] test_frame;
279  delete[] reference_frame;
280}
281
282void PrintMaxRepeatedAndSkippedFrames(const std::string& label,
283                                      const std::string& stats_file_name) {
284  PrintMaxRepeatedAndSkippedFrames(stdout, label, stats_file_name);
285}
286
287void PrintMaxRepeatedAndSkippedFrames(FILE* output, const std::string& label,
288                                      const std::string& stats_file_name) {
289  FILE* stats_file = fopen(stats_file_name.c_str(), "r");
290  if (stats_file == NULL) {
291    fprintf(stderr, "Couldn't open stats file for reading: %s\n",
292            stats_file_name.c_str());
293    return;
294  }
295  char line[STATS_LINE_LENGTH];
296
297  int repeated_frames = 1;
298  int max_repeated_frames = 1;
299  int max_skipped_frames = 1;
300  int previous_frame_number = -1;
301
302  while (GetNextStatsLine(stats_file, line)) {
303    int decoded_frame_number = ExtractDecodedFrameNumber(line);
304
305    if (decoded_frame_number == -1) {
306      continue;
307    }
308
309    // Calculate how many frames a cluster of repeated frames contains.
310    if (decoded_frame_number == previous_frame_number) {
311      ++repeated_frames;
312      if (repeated_frames > max_repeated_frames) {
313        max_repeated_frames = repeated_frames;
314      }
315    } else {
316      repeated_frames = 1;
317    }
318
319    // Calculate how much frames have been skipped.
320    if (decoded_frame_number != 0 && previous_frame_number != -1) {
321      int skipped_frames = decoded_frame_number - previous_frame_number - 1;
322      if (skipped_frames > max_skipped_frames) {
323        max_skipped_frames = skipped_frames;
324      }
325    }
326    previous_frame_number = decoded_frame_number;
327  }
328  fprintf(output, "RESULT Max_repeated: %s= %d\n", label.c_str(),
329          max_repeated_frames);
330  fprintf(output, "RESULT Max_skipped: %s= %d\n", label.c_str(),
331          max_skipped_frames);
332  fclose(stats_file);
333}
334
335void PrintAnalysisResults(const std::string& label, ResultsContainer* results) {
336  PrintAnalysisResults(stdout, label, results);
337}
338
339void PrintAnalysisResults(FILE* output, const std::string& label,
340                          ResultsContainer* results) {
341  std::vector<AnalysisResult>::iterator iter;
342
343  fprintf(output, "RESULT Unique_frames_count: %s= %u\n", label.c_str(),
344          static_cast<unsigned int>(results->frames.size()));
345
346  if (results->frames.size() > 0u) {
347    fprintf(output, "RESULT PSNR: %s= [", label.c_str());
348    for (iter = results->frames.begin(); iter != results->frames.end() - 1;
349         ++iter) {
350      fprintf(output, "%f,", iter->psnr_value);
351    }
352    fprintf(output, "%f] dB\n", iter->psnr_value);
353
354    fprintf(output, "RESULT SSIM: %s= [", label.c_str());
355    for (iter = results->frames.begin(); iter != results->frames.end() - 1;
356         ++iter) {
357      fprintf(output, "%f,", iter->ssim_value);
358    }
359    fprintf(output, "%f]\n", iter->ssim_value);
360  }
361}
362
363}  // namespace test
364}  // namespace webrtc
365