1 /**
2 az @brief Read RAW Yuv files, commonly used for video compression
3  *
4  * This file is a part of PFSTOOLS package.
5  * ----------------------------------------------------------------------
6  * Copyright (C) 2017 Rafal Mantiuk
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU General Public License as published by
10  *  the Free Software Foundation; either version 2 of the License, or
11  *  (at your option) any later version.
12  *
13  *  This program is distributed in the hope that it will be useful,
14  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  *  GNU General Public License for more details.
17  *
18  *  You should have received a copy of the GNU General Public License
19  *  along with this program; if not, write to the Free Software
20  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  * ----------------------------------------------------------------------
22  *
23  * @author Rafal Mantiuk, <mantiuk@mpi-sb.mpg.de>
24  *
25  * $Id: pfsinrgbe.cpp,v 1.5 2014/06/17 21:57:08 rafm Exp $
26  */
27 
28 #include <config.h>
29 #include <string.h>
30 
31 #include <cstdlib>
32 
33 #include <algorithm>
34 #include <iostream>
35 #include <vector>
36 
37 #include <getopt.h>
38 #include <pfs.h>
39 #include <pfsutils.cpp>
40 #include <climits>
41 
42 #define PROG_NAME "pfsinyuv"
43 
44 char charsToRemove[7] = {'b', 'i', 't', 's', 'f', 'p', 's'};
45 
46 template<typename T>
clamp(T val,T min,T max)47 inline T clamp( T val, T min, T max )
48 {
49   if( val < min )
50     return min;
51   if( val > max )
52     return max;
53   return val;
54 }
55 
56 
57 template<typename T>
read_yuv_channel(FILE * fh,int width,int height,int stride,pfs::Array2D * dest,float gain,float offset,float min,float max)58 bool read_yuv_channel( FILE *fh, int width, int height, int stride, pfs::Array2D *dest, float gain, float offset, float min, float max )
59 {
60   T *line_buf = new T[width];
61 
62   for( int y = 0; y < height; y++ ) {      size_t read = fread( line_buf, sizeof( T ), width, fh );
63   if( y == 0 && read == 0 ) { // End of file reached
64     delete [] line_buf;
65     return false;
66   }
67 
68     if( read != width ) {
69       delete [] line_buf;
70       throw pfs::Exception( "Error when reading Yuv file" );
71     }
72     for( int x = 0; x < width; x++ ) {
73       float v_float = (float)line_buf[x]*gain - offset;
74       (*dest)(x<<stride, y<<stride) = clamp( v_float, min, max );
75     }
76 
77   }
78   delete [] line_buf;
79   return true;
80 }
81 
removeCharsFromString(std::string & str,char * charsToRemove)82 void removeCharsFromString(std::string &str, char* charsToRemove ) {
83   for ( unsigned int i = 0; i < strlen(charsToRemove); ++i ) {
84         str.erase( remove(str.begin(), str.end(), charsToRemove[i]), str.end() );
85    }
86 }
87 
parseIntField(std::string i)88 int parseIntField(std::string i){
89   //parses an integer field. Removes pesky characters like the b in 10b or the p in 1080p
90   removeCharsFromString(i, charsToRemove);
91   return std::atoi(i.data());
92 }
93 
94 //more readable helper method for strings, case insensitive
contains(std::string s1,std::string s2)95 bool contains(std::string s1, std::string s2){
96 
97   std::transform(s1.begin(), s1.end(), s1.begin(), ::tolower);
98   std::transform(s2.begin(), s2.end(), s2.begin(), ::tolower);
99 
100   return s1.find(s2) != std::string::npos || s1 == s2;
101 }
102 
103 
104 //returns a vector of strings tokenized
split(std::string s,std::string delim)105 std::vector<std::string> split(std::string s, std::string delim){
106   return std::vector<std::string>();
107 }
108 
parseFileName(const char * fileNameIn,int * width,int * height,int * bitdepth,pfs::ColorSpace * colorspace,bool * upscale_420,int * fps)109 bool parseFileName(const char * fileNameIn, int * width,  int * height, int * bitdepth, pfs::ColorSpace * colorspace, bool * upscale_420, int * fps){
110   try{
111     /* Parses the filename, puts the result into fields pointed to by the arguments
112      returns true if all went well, false if there's a malformed filename
113      string should be <prefix>_<width>x<height(p)>_<fps>_<bitdepth(b)>_<colorspace>_<chroma_format>
114      Regex would be nice*/
115     std::string fileName(fileNameIn);
116     const std::string delimiter = "_";
117     size_t pos = 0;
118     std::string token;
119     std::vector<std::string> tokens;
120 
121     while ((pos = fileName.find(delimiter)) != std::string::npos) {
122         token = fileName.substr(0, pos);
123         tokens.push_back(token);
124         fileName.erase(0, pos + delimiter.length());
125     }
126     tokens.push_back(fileName);
127 
128     for (std::vector<string>::iterator tok = tokens.begin() + 1; tok != tokens.end(); ++tok) {
129 
130       if (contains(*tok, "x")){
131         sscanf(tok->c_str(), "%dx%d", width, height);
132       }
133       else if(*tok == "10" || *tok == "8" || *tok == "12" || tok->at(tok->length() - 1) == 'b' || contains(*tok, "bit")){ //last character is a b
134         *bitdepth = parseIntField(*tok);
135       }
136       else if(*tok == "24" || *tok == "25" || *tok == "50" || *tok == "60" || contains(*tok, "fps")){
137         *fps = parseIntField(*tok);
138       }
139       else if (contains(*tok, "pq") || (contains(*tok, "2020") && !contains(*tok, "hlg"))){
140         *colorspace = pfs::CS_PQYCbCr2020;
141       }
142       else if (contains(*tok, "bt") || contains(*tok, "709")){
143         *colorspace = pfs::CS_YCbCr709;
144       }
145       else if (contains(*tok, "hlg")){
146         *colorspace = pfs::CS_HLGYCbCr2020;
147       }
148       else if(contains(*tok, "444") || contains(*tok, "420")){
149         *upscale_420 = contains(*tok, "420");
150       }
151     }
152     return true;
153   }
154   catch(...){
155     return false;
156   }
157 }
158 
upscale_420to444(pfs::Array2D * ch)159 void upscale_420to444( pfs::Array2D *ch )
160 {
161   const int height = ch->getRows();
162   const int width = ch->getCols();
163 
164   // For even rows
165     for( int y = 0; y < height; y += 2 ) {
166     float v_l = (*ch)(0,y);
167     float v_r;
168     int x;
169     for( x = 1; x < (width-1); x += 2 ) {
170       v_r = (*ch)(x+1,y);
171       // Interpolate between pixels on the left and right
172       (*ch)(x,y) = (v_l + v_r)/2.f;
173       v_l = v_r;
174     }
175     if( x < width )
176       (*ch)(x,y) = v_r;
177   }
178 
179   // For odd rows
180   int y;
181     for( y = 1; y < (height-1); y += 2 ) {
182     for( int x = 0; x < width; x++ ) {
183       (*ch)(x,y) = ((*ch)(x,y-1) + (*ch)(x,y+1))/2.f;
184     }
185   }
186   // Last row
187   if( y < height ) {
188     for( int x = 0; x < width; x++ ) {
189       (*ch)(x,y) = (*ch)(x,y-1);
190     }
191   }
192 }
193 
194 
195 class YUVReader
196 {
197   FILE *fh;
198   int width, height;
199   int bit_depth;
200   bool subsampling_420;
201   unsigned long int fileSize;
202 
203   enum ColorSpace { REC709, REC2020 };
204   ColorSpace color_space;
205 
206 public:
YUVReader(FILE * fh,int width,int height,int bit_depth,bool subsampling_420)207   YUVReader( FILE *fh, int width, int height, int bit_depth, bool subsampling_420 ) : fh(fh), width( width ), height( height ), bit_depth( bit_depth ), subsampling_420( subsampling_420 )
208   {
209     //initialize filesize
210     fseek(fh, 0L, SEEK_END);
211     fileSize = ftell(fh);
212     //go back from where we came
213     fseek(fh, 0L, SEEK_SET);
214   }
215 
getWidth() const216   int getWidth() const
217     {
218       return width;
219     }
220 
getHeight() const221   int getHeight() const
222     {
223       return height;
224     }
225 
getFileSize() const226   unsigned long int getFileSize() const
227     {
228       return fileSize;
229     }
230 
231   /*
232   Advances a number of frames through the file without reading them */
233 //  bool advanceFrames(int frameNo){
234 //    int divisor = subsampling_420 ? 4 : 1; //this is the integer divisor of the green and blue channels (they are 4 times smaller if 4:2:0 subsampling is enabled)
235 //    long int offset = frameNo * (/*Red:*/ width * height * sizeof(STORE_FORMAT) + /*Green, Blue:*/ 2*width*height*sizeof(STORE_FORMAT)/divisor);
236 //    return fseek(fh, offset, SEEK_CUR);
237 //  }
238 
239   /* See to a given frame */
seekToFrame(int frameNo)240   bool seekToFrame(int frameNo){
241     unsigned long int offset = getFileOffsetFromFrame(frameNo);
242     if(offset > fileSize){
243       throw pfs::Exception("Seeking past EOF, is your given frame range within the range of the input?");
244     }
245     else{
246       return fseek(fh, offset, SEEK_SET);
247     }
248   }
249 
getFileOffsetFromFrame(int frameNo)250   unsigned long int getFileOffsetFromFrame(int frameNo){
251     unsigned long int divisor = subsampling_420 ? 4 : 1; //this is the integer divisor of the green and blue channels (they are 4 times smaller if 4:2:0 subsampling is enabled)
252 
253     if( frameNo <= 0 )
254       throw pfs::Exception( "Invalid frame index. Frame are indexed from 1" );
255 
256     unsigned long int storeFormatSize = bit_depth <= 8 ? sizeof(unsigned char) : sizeof(unsigned short);
257     unsigned long int offset = (frameNo-1) * (/*Luma:*/ width * height * storeFormatSize + /*CrCb:*/ 2*width*height*storeFormatSize/divisor);
258     return offset;
259   }
260 
261   /**
262    * Read a single frame from Yuv video file and store in 3 RGB channels of type pfs::Array.
263    * Integer values are converted into floating point values.
264    *
265    * @return TRUE if the frame has been loaded successfully, FALSE if end-of-file.
266    * Exception is thrown if there is an issue reading a full frame.
267    */
268 
readImage(pfs::Array2D * R,pfs::Array2D * G,pfs::Array2D * B)269   bool readImage( pfs::Array2D *R, pfs::Array2D *G, pfs::Array2D *B ){
270     if(bit_depth <= 8){
271       return readImageTyped<unsigned char>(R, G, B);
272     }
273     else{
274       return readImageTyped<unsigned short>(R, G, B);
275     }
276   }
277 
278   private:
279     template<typename T>
readImageTyped(pfs::Array2D * R,pfs::Array2D * G,pfs::Array2D * B)280     bool readImageTyped( pfs::Array2D *R, pfs::Array2D *G, pfs::Array2D *B)
281     {
282 
283       { // Read Y
284       const float offset = 16.f/219.f;
285       const float gain = 1.f/( (float)(1<<(bit_depth-8))*219.f);
286       if( !read_yuv_channel<T>( fh, width, height, 0, R, gain, offset, 0.f, 1.f ) )  {
287         return false;
288       }
289       }
290 
291       { // Read Cb, Cr
292         const float offset = 128.f/224.f;
293         const float gain = 1.f/( (float)(1<<(bit_depth-8))*224.f);
294         unsigned int chroma_width = width;
295         unsigned int chroma_height = height;
296         unsigned int chroma_stride = 0;
297         if (subsampling_420){
298           chroma_width = width/2;
299           chroma_height = height/2;
300           chroma_stride = 1;
301         }
302 
303         if( !read_yuv_channel<T>( fh, chroma_width, chroma_height, chroma_stride, G, gain, offset, -0.5f, 0.5f ) )
304         throw pfs::Exception( "EOF reached when reading the Cb portion of a frame" );
305 
306         if( !read_yuv_channel<T>( fh, chroma_width, chroma_height, chroma_stride, B, gain, offset, -0.5f, 0.5f ) )
307         throw pfs::Exception( "EOF reached when reading the Cr portion of a frame" );
308         if(subsampling_420){
309           upscale_420to444( B );
310           upscale_420to444( G );
311         }
312       }
313     return true;
314     }
315 };
316 
317 
318 class QuietException
319 {
320 };
321 
printHelp()322 void printHelp()
323 {
324   std::cerr << PROG_NAME " [--verbose] [--quiet] [--width] [--height] [--colorspace] [--no-guess] [--chroma-format] [--bit-depth] [--frames] [--help]" << std::endl
325             << "See man page for more information." << std::endl;
326 }
327 
328 
readFrames(int argc,char * argv[])329 void readFrames( int argc, char* argv[] )
330 {
331   pfs::DOMIO pfsio;
332 
333   bool verbose = false;
334   bool quiet = false;
335   bool opt_noguess = false;
336   int opt_width = -1;
337   int opt_height = -1;
338   int opt_bitdepth = -1;
339   const char * opt_chroma_subsampling = NULL;
340   int opt_frame_min = -1;
341   int opt_frame_max = -1;
342   int opt_frame_stride = 0;
343   int opt_fps = -1;
344   char * filename;
345   pfs::ColorSpace opt_colorspace = pfs::CS_INVALID;
346 
347   // Parse command line parameters
348   static struct option cmdLineOptions[] = {
349     { "help", no_argument, NULL, 'p' },
350     { "verbose", no_argument, NULL, 'v' },
351     { "width", required_argument, NULL, 'w' },
352     { "height", required_argument, NULL, 'h' },
353     { "bit-depth", required_argument, NULL, 'b' },
354     { "colorspace", required_argument, NULL, 'c' },
355     { "quiet", no_argument, NULL, 'q' },
356     { "no-guess", no_argument, NULL, 'n'},
357     { "fps", required_argument, NULL, 'f'},
358     { "chroma-format", required_argument, NULL, 's'},
359     { "frames", required_argument, NULL, 'r'},
360     { NULL, 0, NULL, 0 }
361   };
362   static const char optstring[] = "-pw:h:vqb:c:f:nr:s:";
363 
364   int optionIndex = 0;
365   while( 1 ) {
366     int c = getopt_long (argc, argv, optstring, cmdLineOptions, &optionIndex);
367     if(c == -1){
368       break;
369     }
370     switch( c ) {
371     case 'p':
372       printHelp();
373       throw QuietException();
374     case 'v':
375       verbose = true;
376       break;
377     case 'w':
378       opt_width = strtol( optarg, NULL, 10 );
379       break;
380     case 'h':
381       opt_height = strtol( optarg, NULL, 10 );
382       break;
383     case 'b':
384       opt_bitdepth = strtol( optarg, NULL, 10 );
385       break;
386     case 's':
387       opt_chroma_subsampling = optarg;
388       break;
389     case 'n':
390       opt_noguess = true;
391       break;
392     case 'f':
393       opt_fps = strtol(optarg, NULL, 10);
394       break;
395     case 'r': {
396       pfs::parseFrameRange(optarg, opt_frame_min, opt_frame_max, opt_frame_stride);
397       VERBOSE_STR << "Reading frames " << opt_frame_min << ":" << opt_frame_stride << ":"
398                   << opt_frame_max << std::endl;
399       break;
400     }
401     case 'c':
402       if( !strcasecmp( optarg, "pq2020" ) ) {
403         opt_colorspace = pfs::CS_PQYCbCr2020;
404       }
405       else if( !strcasecmp( optarg, "bt709" )) {
406       opt_colorspace = pfs::CS_YCbCr709;
407       }
408       else if( !strcasecmp( optarg, "hlg2020")){
409         opt_colorspace = pfs::CS_HLGYCbCr2020;
410       }
411       else {
412         throw pfs::Exception( "Unrecognized colorspace name" );
413       }
414       break;
415     case 'q':
416       quiet = true;
417       break;
418     case '?':
419       throw QuietException();
420     case ':':
421       throw QuietException();
422     case '\1':
423       filename = optarg;
424     }
425   }
426 
427   FILE * fh;
428   if(filename != NULL){
429     fh = fopen (filename, "rb");
430   }
431 
432   if( fh == NULL ) {
433     throw pfs::Exception("Couldn't find a filename to open, did you provide one");
434   }; // No more frames
435 
436   int width = -1, height = -1, bitdepth = -1, frame_min = 1, frame_max = INT_MAX, frame_stride = 1, fps = -1;
437   bool upscale_420 = true;
438   pfs::ColorSpace colorspace = pfs::CS_INVALID;
439 
440   VERBOSE_STR << "reading file '" << filename << "'" << std::endl;
441   //we infer the metadata from the filename unless noguess is specified
442   if(!opt_noguess){
443     std::string rawName(filename); //these three lines extract the filename proper, without the containing directory prefix
444     long unsigned int substring = rawName.find_last_of("\\/");
445     rawName = rawName.substr(substring == std::string::npos ? 0 : substring);
446     bool goodFileName = parseFileName(rawName.c_str(), &width, &height, &bitdepth, &colorspace, &upscale_420, &fps);
447     if(!goodFileName){
448       VERBOSE_STR << "Unable to parse filename: " << filename << "\n";
449     }
450   }
451   // Over-ride the auto-recognized params with the ones specified as arguments
452   if( opt_width > -1 )
453     width = opt_width;
454   if( opt_height > -1 )
455     height = opt_height;
456   if( opt_bitdepth > -1 )
457     bitdepth = opt_bitdepth;
458   if( opt_frame_min > -1 )
459     frame_min = opt_frame_min;
460   if( opt_frame_max > -1 )
461     frame_max = opt_frame_max;
462   if( opt_frame_stride != 0)
463     frame_stride = opt_frame_stride;
464   if( opt_colorspace != pfs::CS_INVALID )
465     colorspace = opt_colorspace;
466   if( opt_chroma_subsampling != NULL ){
467     // bad reading, essentially just force upscaling if they input the subsampling as 420
468     upscale_420 = strcmp(opt_chroma_subsampling, "420") == 0;
469   }
470   if( opt_fps > 0 ){
471     fps = opt_fps;
472   }
473 
474   VERBOSE_STR << "Yuv file " << width << "x" << height << " " << bitdepth << "bits" << std::endl;
475   switch( colorspace ) {
476     case pfs::CS_PQYCbCr2020:
477       VERBOSE_STR << "colorspace: HDR PQ BT2020" << std::endl;
478       break;
479     case pfs::CS_YCbCr709:
480       VERBOSE_STR << "colorspace: SDR BT709" << std::endl;
481       break;
482     case pfs::CS_HLGYCbCr2020:
483       VERBOSE_STR << "colorspace: HDR HLG BT2020" << std::endl;
484       break;
485   }
486 
487   if( width <= 0 || height <= 0 )
488     throw pfs::Exception( "Unspecified or incorrect resolution of the Yuv file" );
489 
490   if( bitdepth < 8 || bitdepth > 16 )
491     throw pfs::Exception( "Unspecified or incorrect bit-depth of the Yuv file" );
492 
493   if( frame_min < 0 || frame_max < 0){
494     throw pfs::Exception( "Frame range is invalid ");
495   }
496   if( colorspace == pfs::CS_INVALID ){
497     throw pfs::Exception( "Unspecified colorspace of the Yuv file" );
498   }
499   if( fps > 0 ){
500     VERBOSE_STR << "FPS: " << fps << std::endl;
501   }
502 
503   YUVReader reader( fh, width, height, bitdepth, upscale_420);
504 
505 
506   int currentFrame = frame_min;
507 
508   while( (currentFrame <= frame_max && frame_stride > 0)
509       || (currentFrame >= frame_max && frame_stride < 0) ) { //inclusive
510 
511     pfs::Frame *frame = pfsio.createFrame( reader.getWidth(),
512                                            reader.getHeight() );
513     pfs::Channel *X, *Y, *Z;
514     frame->createXYZChannels( X, Y, Z );
515 
516     if(reader.seekToFrame(currentFrame)) {
517       //some error occured in reading :(
518       pfsio.freeFrame( frame );
519       break;
520     }
521 
522     //Store RGB data temporarily in XYZ channels
523     bool success = reader.readImage( X, Y, Z );
524     if( !success ) { // EOF reached
525       pfsio.freeFrame( frame );
526       break;
527     }
528 
529     VERBOSE_STR << "Reading frame " << currentFrame << std::endl;
530 
531     if( colorspace == pfs::CS_YCbCr709 ) {
532       // The trick to get LDR data in the pfs stream
533       pfs::transformColorSpace( colorspace, X, Y, Z, pfs::CS_SRGB, X, Y, Z );
534       pfs::transformColorSpace( pfs::CS_RGB, X, Y, Z, pfs::CS_XYZ, X, Y, Z );
535     } else {
536       pfs::transformColorSpace( colorspace, X, Y, Z, pfs::CS_XYZ, X, Y, Z );
537     }
538 
539 
540     switch( colorspace ) {
541       case pfs::CS_PQYCbCr2020:
542         frame->getTags()->setString("LUMINANCE", "ABSOLUTE");
543         break;
544       case pfs::CS_YCbCr709:
545         frame->getTags()->setString("LUMINANCE", "DISPLAY");
546         break;
547       case pfs::CS_HLGYCbCr2020:
548         frame->getTags()->setString("LUMINANCE", "ABSOLUTE");
549         break;
550     }
551 
552     if( fps > 0 ){
553       frame->getTags()->setString("FPS", pfs::intToString(fps).c_str());
554     }
555 
556     const char *fileNameTag = strcmp( "-", filename )==0 ? "stdin" : filename;
557     frame->getTags()->setString( "FILE_NAME", fileNameTag );
558 
559     pfsio.writeFrame( frame, stdout );
560     pfsio.freeFrame( frame );
561 
562     currentFrame += frame_stride;
563   }
564   fclose(fh);
565 }
566 
567 
main(int argc,char * argv[])568 int main( int argc, char* argv[] )
569 {
570   try {
571     readFrames( argc, argv );
572   }
573   catch( pfs::Exception ex ) {
574     fprintf( stderr, PROG_NAME " error: %s\n", ex.getMessage() );
575     return EXIT_FAILURE;
576   }
577   catch( QuietException ) {
578     return EXIT_FAILURE;
579   }
580   return EXIT_SUCCESS;
581 }
582