MythTV  0.26-pre
TemplateFinder.cpp
Go to the documentation of this file.
00001 // POSIX headers
00002 #include <sys/time.h>      /* gettimeofday */
00003 
00004 // ANSI C headers
00005 #include <cmath>
00006 #include <cstdlib>
00007 
00008 // Qt headers
00009 #include <QFile>
00010 #include <QFileInfo>
00011 #include <QTextStream>
00012 
00013 // MythTV headers
00014 #include "mythplayer.h"
00015 #include "mythcorecontext.h"    /* gContext */
00016 #include "frame.h"          /* VideoFrame */
00017 #include "mythmiscutil.h"
00018 #include "mythsystem.h"
00019 #include "exitcodes.h"
00020 
00021 // Commercial Flagging headers
00022 #include "CommDetector2.h"
00023 #include "pgm.h"
00024 #include "PGMConverter.h"
00025 #include "BorderDetector.h"
00026 #include "EdgeDetector.h"
00027 #include "TemplateFinder.h"
00028 
00029 using namespace commDetector2;
00030 
00031 namespace {
00032 
00033 int writeJPG(QString prefix, const AVPicture *img, int imgheight)
00034 {
00035     const int imgwidth = img->linesize[0];
00036     QFileInfo jpgfi(prefix + ".jpg");
00037     if (!jpgfi.exists())
00038     {
00039         QFile pgmfile(prefix + ".pgm");
00040         if (!pgmfile.exists())
00041         {
00042             QByteArray pfname = pgmfile.fileName().toLocal8Bit();
00043             if (pgm_write(img->data[0], imgwidth, imgheight,
00044                           pfname.constData()))
00045             {
00046                 return -1;
00047             }
00048         }
00049 
00050         QString cmd = QString("convert -quality 50 -resize 192x144 %1 %2")
00051                       .arg(pgmfile.fileName()).arg(jpgfi.filePath());
00052         if (myth_system(cmd) != GENERIC_EXIT_OK)
00053             return -1;
00054 
00055         if (!pgmfile.remove())
00056         {
00057             LOG(VB_COMMFLAG, LOG_ERR, 
00058                 QString("TemplateFinder.writeJPG error removing %1 (%2)")
00059                     .arg(pgmfile.fileName()).arg(strerror(errno)));
00060             return -1;
00061         }
00062     }
00063     return 0;
00064 }
00065 
00066 int
00067 pgm_scorepixels(unsigned int *scores, int width, int row, int col,
00068         const AVPicture *src, int srcheight)
00069 {
00070     /* Every time a pixel is an edge, give it a point. */
00071     const int   srcwidth = src->linesize[0];
00072     int         rr, cc;
00073 
00074     for (rr = 0; rr < srcheight; rr++)
00075     {
00076         for (cc = 0; cc < srcwidth; cc++)
00077         {
00078             if (src->data[0][rr * srcwidth + cc])
00079                 scores[(row + rr) * width + col + cc]++;
00080         }
00081     }
00082 
00083     return 0;
00084 }
00085 
00086 int
00087 sort_ascending(const void *aa, const void *bb)
00088 {
00089     return *(unsigned int*)aa - *(unsigned int*)bb;
00090 }
00091 
00092 float
00093 bounding_score(const AVPicture *img, int row, int col, int width, int height)
00094 {
00095     /* Return a value between [0..1] */
00096     const int       imgwidth = img->linesize[0];
00097     unsigned int    score;
00098     int             rr, cc, rr2, cc2;
00099 
00100     score = 0;
00101     rr2 = row + height;
00102     cc2 = col + width;
00103     for (rr = row; rr < rr2; rr++)
00104     {
00105         for (cc = col; cc < cc2; cc++)
00106         {
00107             if (img->data[0][rr * imgwidth + cc])
00108                 score++;
00109         }
00110     }
00111     return (float)score / (width * height);
00112 }
00113 
00114 bool
00115 rowisempty(const AVPicture *img, int row, int col, int width)
00116 {
00117     const int   imgwidth = img->linesize[0];
00118     for (int cc = col; cc < col + width; cc++)
00119         if (img->data[0][row * imgwidth + cc])
00120             return false;
00121     return true;
00122 }
00123 
00124 bool
00125 colisempty(const AVPicture *img, int col, int row, int height)
00126 {
00127     const int   imgwidth = img->linesize[0];
00128     for (int rr = row; rr < row + height; rr++)
00129         if (img->data[0][rr * imgwidth + col])
00130             return false;
00131     return true;
00132 }
00133 
00134 int
00135 bounding_box(const AVPicture *img, int imgheight,
00136         int minrow, int mincol, int maxrow1, int maxcol1,
00137         int *prow, int *pcol, int *pwidth, int *pheight)
00138 {
00139     const int           imgwidth = img->linesize[0];
00140     /*
00141      * TUNABLE:
00142      *
00143      * Maximum logo size, expressed as a percentage of the content area
00144      * (adjusting for letterboxing and pillarboxing).
00145      */
00146     static const int    MAXWIDTHPCT = 20;
00147     static const int    MAXHEIGHTPCT = 20;
00148 
00149     /*
00150      * TUNABLE:
00151      *
00152      * Safety margin to avoid cutting too much of the logo.
00153      * Higher values cut more, but avoid noise as part of the template..
00154      * Lower values cut less, but can include noise as part of the template.
00155      */
00156     const int           VERTSLOP = max(4, imgheight * 1 / 15);
00157     const int           HORIZSLOP = max(4, imgwidth * 1 / 20);
00158 
00159     int                 maxwidth, maxheight;
00160     int                 width, height, row, col;
00161     int                 newrow, newcol, newright, newbottom;
00162 
00163     maxwidth = (maxcol1 - mincol) * MAXWIDTHPCT / 100;
00164     maxheight = (maxrow1 - minrow) * MAXHEIGHTPCT / 100;
00165 
00166     row = minrow;
00167     col = mincol;
00168     width = maxcol1 - mincol;
00169     height = maxrow1 - minrow;
00170 
00171     for (;;)
00172     {
00173         float           score, newscore;
00174         int             ii;
00175         bool            improved = false;
00176 
00177         LOG(VB_COMMFLAG, LOG_INFO, QString("bounding_box %1x%2@(%3,%4)")
00178                 .arg(width).arg(height).arg(col).arg(row));
00179 
00180         /* Chop top. */
00181         score = bounding_score(img, row, col, width, height);
00182         newrow = row;
00183         for (ii = 1; ii < height; ii++)
00184         {
00185             if ((newscore = bounding_score(img, row + ii, col,
00186                             width, height - ii)) < score)
00187                 break;
00188             score = newscore;
00189             newrow = row + ii;
00190             improved = true;
00191         }
00192 
00193         /* Chop left. */
00194         score = bounding_score(img, row, col, width, height);
00195         newcol = col;
00196         for (ii = 1; ii < width; ii++)
00197         {
00198             if ((newscore = bounding_score(img, row, col + ii,
00199                             width - ii, height)) < score)
00200                 break;
00201             score = newscore;
00202             newcol = col + ii;
00203             improved = true;
00204         }
00205 
00206         /* Chop bottom. */
00207         score = bounding_score(img, row, col, width, height);
00208         newbottom = row + height;
00209         for (ii = 1; ii < height; ii++)
00210         {
00211             if ((newscore = bounding_score(img, row, col,
00212                             width, height - ii)) < score)
00213                 break;
00214             score = newscore;
00215             newbottom = row + height - ii;
00216             improved = true;
00217         }
00218 
00219         /* Chop right. */
00220         score = bounding_score(img, row, col, width, height);
00221         newright = col + width;
00222         for (ii = 1; ii < width; ii++)
00223         {
00224             if ((newscore = bounding_score(img, row, col,
00225                             width - ii, height)) < score)
00226                 break;
00227             score = newscore;
00228             newright = col + width - ii;
00229             improved = true;
00230         }
00231 
00232         if (!improved)
00233             break;
00234 
00235         row = newrow;
00236         col = newcol;
00237         width = newright - newcol;
00238         height = newbottom - newrow;
00239 
00240         /*
00241          * Noise edge pixels in the frequency template can sometimes stretch
00242          * the template area to be larger than it should be.
00243          *
00244          * However, noise needs to be distinguished from a uniform distribution
00245          * of noise pixels (e.g., no real statically-located template). So if
00246          * the template area is too "large", then some quadrant must have a
00247          * clear majority of the edge pixels; otherwise we declare failure (no
00248          * template found).
00249          *
00250          * Intuitively, we should simply repeat until a single bounding box is
00251          * converged upon. However, this requires a more sophisticated
00252          * bounding_score function that I don't feel like figuring out.
00253          * Indefinitely repeating with the present bounding_score function will
00254          * tend to chop off too much. Instead, simply do some sanity checks on
00255          * the candidate template's size, and prune the template area and
00256          * repeat if it is too "large".
00257          */
00258 
00259         if (width > maxwidth)
00260         {
00261             /* Too wide; test left and right portions. */
00262             int             chop = width / 3;
00263             int             chopwidth = width - chop;
00264             float           left, right, minscore, maxscore;
00265 
00266             left = bounding_score(img, row, col, chopwidth, height);
00267             right = bounding_score(img, row, col + chop, chopwidth, height);
00268             LOG(VB_COMMFLAG, LOG_INFO, 
00269                 QString("bounding_box too wide (%1 > %2); left=%3, right=%4")
00270                     .arg(width).arg(maxwidth)
00271                     .arg(left, 0, 'f', 3).arg(right, 0, 'f', 3));
00272             minscore = min(left, right);
00273             maxscore = max(left, right);
00274             if (maxscore < 3 * minscore / 2)
00275             {
00276                 /*
00277                  * Edge pixel distribution too uniform; give up.
00278                  *
00279                  * XXX: also fails for horizontally-centered templates ...
00280                  */
00281                 LOG(VB_COMMFLAG, LOG_ERR, "bounding_box giving up (edge "
00282                                           "pixels distributed too uniformly)");
00283                 return -1;
00284             }
00285 
00286             if (left < right)
00287                 col += chop;
00288             width -= chop;
00289             continue;
00290         }
00291 
00292         if (height > maxheight)
00293         {
00294             /* Too tall; test upper and lower portions. */
00295             int             chop = height / 3;
00296             int             chopheight = height - chop;
00297             float           upper, lower, minscore, maxscore;
00298 
00299             upper = bounding_score(img, row, col, width, chopheight);
00300             lower = bounding_score(img, row + chop, col, width, chopheight);
00301             LOG(VB_COMMFLAG, LOG_INFO,
00302                 QString("bounding_box too tall (%1 > %2); upper=%3, lower=%4")
00303                     .arg(height).arg(maxheight)
00304                     .arg(upper, 0, 'f', 3).arg(lower, 0, 'f', 3));
00305             minscore = min(upper, lower);
00306             maxscore = max(upper, lower);
00307             if (maxscore < 3 * minscore / 2)
00308             {
00309                 /*
00310                  * Edge pixel distribution too uniform; give up.
00311                  *
00312                  * XXX: also fails for vertically-centered templates ...
00313                  */
00314                 LOG(VB_COMMFLAG, LOG_ERR, "bounding_box giving up (edge "
00315                                           "pixel distribution too uniform)");
00316                 return -1;
00317             }
00318 
00319             if (upper < lower)
00320                 row += chop;
00321             height -= chop;
00322             continue;
00323         }
00324 
00325         break;
00326     }
00327 
00328     /*
00329      * The above "chop" algorithm often cuts off the outside edges of the
00330      * logos because the outside edges don't contribute enough to the score. So
00331      * compensate by now expanding the bounding box (up to a *SLOP pixels in
00332      * each direction) to include all edge pixels.
00333      */
00334 
00335     LOG(VB_COMMFLAG, LOG_INFO,
00336         QString("bounding_box %1x%2@(%3,%4); horizslop=%5,vertslop=%6")
00337             .arg(width).arg(height).arg(col).arg(row)
00338             .arg(HORIZSLOP).arg(VERTSLOP));
00339 
00340     /* Expand upwards. */
00341     newrow = row - 1;
00342     for (;;)
00343     {
00344         if (newrow <= minrow)
00345         {
00346             newrow = minrow;
00347             break;
00348         }
00349         if (row - newrow >= VERTSLOP)
00350         {
00351             newrow = row - VERTSLOP;
00352             break;
00353         }
00354         if (rowisempty(img, newrow, col, width))
00355         {
00356             newrow++;
00357             break;
00358         }
00359         newrow--;
00360     }
00361     newrow = max(minrow, newrow - 1);   /* Empty row on top. */
00362 
00363     /* Expand leftwards. */
00364     newcol = col - 1;
00365     for (;;)
00366     {
00367         if (newcol <= mincol)
00368         {
00369             newcol = mincol;
00370             break;
00371         }
00372         if (col - newcol >= HORIZSLOP)
00373         {
00374             newcol = col - HORIZSLOP;
00375             break;
00376         }
00377         if (colisempty(img, newcol, row, height))
00378         {
00379             newcol++;
00380             break;
00381         }
00382         newcol--;
00383     }
00384     newcol = max(mincol, newcol - 1);   /* Empty column to left. */
00385 
00386     /* Expand rightwards. */
00387     newright = col + width;
00388     for (;;)
00389     {
00390         if (newright >= maxcol1)
00391         {
00392             newright = maxcol1;
00393             break;
00394         }
00395         if (newright - (col + width) >= HORIZSLOP)
00396         {
00397             newright = col + width + HORIZSLOP;
00398             break;
00399         }
00400         if (colisempty(img, newright, row, height))
00401             break;
00402         newright++;
00403     }
00404     newright = min(maxcol1, newright + 1);  /* Empty column to right. */
00405 
00406     /* Expand downwards. */
00407     newbottom = row + height;
00408     for (;;)
00409     {
00410         if (newbottom >= maxrow1)
00411         {
00412             newbottom = maxrow1;
00413             break;
00414         }
00415         if (newbottom - (row + height) >= VERTSLOP)
00416         {
00417             newbottom = row + height + VERTSLOP;
00418             break;
00419         }
00420         if (rowisempty(img, newbottom, col, width))
00421             break;
00422         newbottom++;
00423     }
00424     newbottom = min(maxrow1, newbottom + 1);    /* Empty row on bottom. */
00425 
00426     row = newrow;
00427     col = newcol;
00428     width = newright - newcol;
00429     height = newbottom - newrow;
00430 
00431     LOG(VB_COMMFLAG, LOG_INFO, QString("bounding_box %1x%2@(%3,%4)")
00432             .arg(width).arg(height).arg(col).arg(row));
00433 
00434     *prow = row;
00435     *pcol = col;
00436     *pwidth = width;
00437     *pheight = height;
00438     return 0;
00439 }
00440 
00441 int
00442 template_alloc(const unsigned int *scores, int width, int height,
00443         int minrow, int mincol, int maxrow1, int maxcol1, AVPicture *tmpl,
00444         int *ptmplrow, int *ptmplcol, int *ptmplwidth, int *ptmplheight,
00445         bool debug_edgecounts, QString debugdir)
00446 {
00447     /*
00448      * TUNABLE:
00449      *
00450      * Higher values select for "stronger" pixels to be in the template, but
00451      * weak pixels might be missed.
00452      *
00453      * Lower values allow more pixels to be included as part of the template,
00454      * but strong non-template pixels might be included.
00455      */
00456     static const float      MINSCOREPCTILE = 0.998;
00457 
00458     const int               nn = width * height;
00459     int                     ii, first, last;
00460     unsigned int            *sortedscores, threshscore;
00461     AVPicture               thresh;
00462 
00463     if (avpicture_alloc(&thresh, PIX_FMT_GRAY8, width, height))
00464     {
00465         LOG(VB_COMMFLAG, LOG_ERR,
00466             QString("template_alloc avpicture_alloc thresh (%1x%2) failed")
00467                 .arg(width).arg(height));
00468         return -1;
00469     }
00470 
00471     sortedscores = new unsigned int[nn];
00472     memcpy(sortedscores, scores, nn * sizeof(*sortedscores));
00473     qsort(sortedscores, nn, sizeof(*sortedscores), sort_ascending);
00474 
00475     if (sortedscores[0] == sortedscores[nn - 1])
00476     {
00477         /* All pixels in the template area look the same; no template. */
00478         LOG(VB_COMMFLAG, LOG_ERR,
00479             QString("template_alloc: %1x%2 pixels all identical!")
00480                 .arg(width).arg(height));
00481         goto free_thresh;
00482     }
00483 
00484     /* Threshold the edge frequences. */
00485 
00486     ii = (int)roundf(nn * MINSCOREPCTILE);
00487     threshscore = sortedscores[ii];
00488     for (first = ii; first > 0 && sortedscores[first] == threshscore; first--)
00489         ;
00490     if (sortedscores[first] != threshscore)
00491         first++;
00492     for (last = ii; last < nn - 1 && sortedscores[last] == threshscore; last++)
00493         ;
00494     if (sortedscores[last] != threshscore)
00495         last--;
00496 
00497     LOG(VB_COMMFLAG, LOG_INFO, QString("template_alloc wanted %1, got %2-%3")
00498             .arg(MINSCOREPCTILE, 0, 'f', 6)
00499             .arg((float)first / nn, 0, 'f', 6)
00500             .arg((float)last / nn, 0, 'f', 6));
00501 
00502     for (ii = 0; ii < nn; ii++)
00503         thresh.data[0][ii] = scores[ii] >= threshscore ? UCHAR_MAX : 0;
00504 
00505     if (debug_edgecounts)
00506     {
00507         /* Scores, rescaled to [0..UCHAR_MAX]. */
00508         AVPicture scored;
00509         if (avpicture_alloc(&scored, PIX_FMT_GRAY8, width, height))
00510         {
00511             LOG(VB_COMMFLAG, LOG_ERR,
00512                 QString("template_alloc avpicture_alloc scored (%1x%2) failed")
00513                     .arg(width).arg(height));
00514             goto free_thresh;
00515         }
00516         unsigned int maxscore = sortedscores[nn - 1];
00517         for (ii = 0; ii < nn; ii++)
00518             scored.data[0][ii] = scores[ii] * UCHAR_MAX / maxscore;
00519         int error = writeJPG(debugdir + "/TemplateFinder-scores", &scored,
00520                 height);
00521         avpicture_free(&scored);
00522         if (error)
00523             goto free_thresh;
00524 
00525         /* Thresholded scores. */
00526         if (writeJPG(debugdir + "/TemplateFinder-edgecounts", &thresh, height))
00527             goto free_thresh;
00528     }
00529 
00530     /* Crop to a minimal bounding box. */
00531 
00532     if (bounding_box(&thresh, height, minrow, mincol, maxrow1, maxcol1,
00533                 ptmplrow, ptmplcol, ptmplwidth, ptmplheight))
00534         goto free_thresh;
00535 
00536     if ((uint)(*ptmplwidth * *ptmplheight) > USHRT_MAX)
00537     {
00538         /* Max value of data type of TemplateMatcher::edgematch */
00539         LOG(VB_COMMFLAG, LOG_ERR,
00540             QString("template_alloc bounding_box too big (%1x%2)")
00541                 .arg(*ptmplwidth).arg(*ptmplheight));
00542         goto free_thresh;
00543     }
00544 
00545     if (avpicture_alloc(tmpl, PIX_FMT_GRAY8, *ptmplwidth, *ptmplheight))
00546     {
00547         LOG(VB_COMMFLAG, LOG_ERR,
00548             QString("template_alloc avpicture_alloc tmpl (%1x%2) failed")
00549                 .arg(*ptmplwidth).arg(*ptmplheight));
00550         goto free_thresh;
00551     }
00552 
00553     if (pgm_crop(tmpl, &thresh, height, *ptmplrow, *ptmplcol,
00554                 *ptmplwidth, *ptmplheight))
00555         goto free_thresh;
00556 
00557     delete []sortedscores;
00558     avpicture_free(&thresh);
00559     return 0;
00560 
00561 free_thresh:
00562     delete []sortedscores;
00563     avpicture_free(&thresh);
00564     return -1;
00565 }
00566 
00567 int
00568 analyzeFrameDebug(long long frameno, const AVPicture *pgm, int pgmheight,
00569         const AVPicture *cropped, const AVPicture *edges, int cropheight,
00570         int croprow, int cropcol, bool debug_frames, QString debugdir)
00571 {
00572     static const int    delta = 24;
00573     static int          lastrow, lastcol, lastwidth, lastheight;
00574     const int           cropwidth = cropped->linesize[0];
00575     int                 rowsame, colsame, widthsame, heightsame;
00576 
00577     rowsame = abs(lastrow - croprow) <= delta ? 1 : 0;
00578     colsame = abs(lastcol - cropcol) <= delta ? 1 : 0;
00579     widthsame = abs(lastwidth - cropwidth) <= delta ? 1 : 0;
00580     heightsame = abs(lastheight - cropheight) <= delta ? 1 : 0;
00581 
00582     if (frameno > 0 && rowsame + colsame + widthsame + heightsame >= 3)
00583         return 0;
00584 
00585     LOG(VB_COMMFLAG, LOG_INFO,
00586         QString("TemplateFinder Frame %1: %2x%3@(%4,%5)")
00587             .arg(frameno, 5)
00588             .arg(cropwidth).arg(cropheight)
00589             .arg(cropcol).arg(croprow));
00590 
00591     lastrow = croprow;
00592     lastcol = cropcol;
00593     lastwidth = cropwidth;
00594     lastheight = cropheight;
00595 
00596     if (debug_frames)
00597     {
00598         QString base = QString("%1/TemplateFinder-%2")
00599             .arg(debugdir).arg(frameno, 5, 10, QChar('0'));
00600 
00601         /* PGM greyscale image of frame. */
00602         if (writeJPG(base, pgm, pgmheight))
00603             goto error;
00604 
00605         /* Cropped template area of frame. */
00606         if (writeJPG(base + "-cropped", cropped, cropheight))
00607             goto error;
00608 
00609         /* Edges of cropped template area of frame. */
00610         if (writeJPG(base + "-edges", edges, cropheight))
00611             goto error;
00612     }
00613 
00614     return 0;
00615 
00616 error:
00617     return -1;
00618 }
00619 
00620 bool
00621 readTemplate(QString datafile, int *prow, int *pcol, int *pwidth, int *pheight,
00622         QString tmplfile, AVPicture *tmpl, bool *pvalid)
00623 {
00624     QFile dfile(datafile);
00625     QFileInfo dfileinfo(dfile);
00626 
00627     if (!dfile.open(QIODevice::ReadOnly))
00628         return false;
00629 
00630     if (!dfileinfo.size())
00631     {
00632         /* Dummy file: no template. */
00633         *pvalid = false;
00634         return true;
00635     }
00636 
00637     QTextStream stream(&dfile);
00638     stream >> *prow >> *pcol >> *pwidth >> *pheight;
00639     dfile.close();
00640 
00641     if (avpicture_alloc(tmpl, PIX_FMT_GRAY8, *pwidth, *pheight))
00642     {
00643         LOG(VB_COMMFLAG, LOG_ERR,
00644             QString("readTemplate avpicture_alloc %1 (%2x%3) failed")
00645                 .arg(tmplfile).arg(*pwidth).arg(*pheight));
00646         return false;
00647     }
00648 
00649     QByteArray tmfile = tmplfile.toAscii();
00650     if (pgm_read(tmpl->data[0], *pwidth, *pheight, tmfile.constData()))
00651     {
00652         avpicture_free(tmpl);
00653         return false;
00654     }
00655 
00656     *pvalid = true;
00657     return true;
00658 }
00659 
00660 void
00661 writeDummyTemplate(QString datafile)
00662 {
00663     /* Leave a 0-byte file. */
00664     QFile dfile(datafile);
00665 
00666     if (!dfile.open(QIODevice::WriteOnly | QIODevice::Truncate) &&
00667         dfile.exists())
00668         (void)dfile.remove();
00669 }
00670 
00671 bool
00672 writeTemplate(QString tmplfile, const AVPicture *tmpl, QString datafile,
00673         int row, int col, int width, int height)
00674 {
00675     QFile tfile(tmplfile);
00676 
00677     QByteArray tmfile = tmplfile.toAscii();
00678     if (pgm_write(tmpl->data[0], width, height, tmfile.constData()))
00679         return false;
00680 
00681     QFile dfile(datafile);
00682     if (!dfile.open(QIODevice::WriteOnly))
00683         return false;
00684 
00685     QTextStream stream(&dfile);
00686     stream << row << " " << col << "\n" << width << " " << height << "\n";
00687     dfile.close();
00688     return true;
00689 }
00690 
00691 };  /* namespace */
00692 
00693 TemplateFinder::TemplateFinder(PGMConverter *pgmc, BorderDetector *bd,
00694         EdgeDetector *ed, MythPlayer *player, int proglen,
00695         QString debugdir)
00696     : FrameAnalyzer()
00697     , pgmConverter(pgmc)
00698     , borderDetector(bd)
00699     , edgeDetector(ed)
00700     , nextFrame(0)
00701     , width(-1)
00702     , height(-1)
00703     , scores(NULL)
00704     , mincontentrow(INT_MAX)
00705     , mincontentcol(INT_MAX)
00706     , maxcontentrow1(INT_MIN)
00707     , maxcontentcol1(INT_MIN)
00708     , tmplrow(-1)
00709     , tmplcol(-1)
00710     , tmplwidth(-1)
00711     , tmplheight(-1)
00712     , cwidth(-1)
00713     , cheight(-1)
00714     , debugLevel(0)
00715     , debugdir(debugdir)
00716     , debugdata(debugdir + "/TemplateFinder.txt")
00717     , debugtmpl(debugdir + "/TemplateFinder.pgm")
00718     , debug_template(false)
00719     , debug_edgecounts(false)
00720     , debug_frames(false)
00721     , tmpl_valid(false)
00722     , tmpl_done(false)
00723 {
00724     /*
00725      * TUNABLE:
00726      *
00727      * The number of frames desired for sampling to build the template.
00728      *
00729      * Higher values should yield a more accurate template, but requires more
00730      * time.
00731      */
00732     unsigned int        samplesNeeded = 300;
00733 
00734     /*
00735      * TUNABLE:
00736      *
00737      * The leading amount of time (in seconds) to sample frames for building up
00738      * the possible template, and the interval between frames for analysis.
00739      * This affects how soon flagging can start after a recording has begun
00740      * (a.k.a. "real-time flagging").
00741      *
00742      * Sample half of the program length or 20 minutes, whichever is less.
00743      */
00744     sampleTime = min(proglen / 2, 20 * 60);
00745 
00746     const float fps = player->GetFrameRate();
00747 
00748     if (samplesNeeded > UINT_MAX)
00749     {
00750         /* Max value of "scores" data type */
00751         samplesNeeded = UINT_MAX;
00752     }
00753 
00754     frameInterval = (int)roundf(sampleTime * fps / samplesNeeded);
00755     endFrame = 0 + frameInterval * samplesNeeded - 1;
00756 
00757     LOG(VB_COMMFLAG, LOG_INFO,
00758         QString("TemplateFinder: sampleTime=%1s, samplesNeeded=%2, endFrame=%3")
00759             .arg(sampleTime).arg(samplesNeeded).arg(endFrame));
00760 
00761     memset(&cropped, 0, sizeof(cropped));
00762     memset(&tmpl, 0, sizeof(tmpl));
00763     memset(&analyze_time, 0, sizeof(analyze_time));
00764 
00765     /*
00766      * debugLevel:
00767      *      0: no extra debugging
00768      *      1: cache computations into debugdir [O(1) files]
00769      *      2: extra verbosity [O(nframes)]
00770      *      3: dump frames into debugdir [O(nframes) files]
00771      */
00772     debugLevel = gCoreContext->GetNumSetting("TemplateFinderDebugLevel", 0);
00773 
00774     if (debugLevel >= 1)
00775     {
00776         createDebugDirectory(debugdir,
00777             QString("TemplateFinder debugLevel %1").arg(debugLevel));
00778 
00779         debug_template = true;
00780         debug_edgecounts = true;
00781 
00782         if (debugLevel >= 3)
00783             debug_frames = true;
00784     }
00785 }
00786 
00787 TemplateFinder::~TemplateFinder(void)
00788 {
00789     if (scores)
00790         delete []scores;
00791     avpicture_free(&tmpl);
00792     avpicture_free(&cropped);
00793 }
00794 
00795 enum FrameAnalyzer::analyzeFrameResult
00796 TemplateFinder::MythPlayerInited(MythPlayer *player, long long nframes)
00797 {
00798     /*
00799      * Only detect edges in portions of the frame where we expect to find
00800      * a template. This serves two purposes:
00801      *
00802      *  - Speed: reduce search space.
00803      *  - Correctness (insofar as the assumption of template location is
00804      *    correct): don't "pollute" the set of candidate template edges with
00805      *    the "content" edges in the non-template portions of the frame.
00806      */
00807     QString tmpldims, playerdims;
00808 
00809     (void)nframes; /* gcc */
00810     QSize buf_dim = player->GetVideoBufferSize();
00811     width  = buf_dim.width();
00812     height = buf_dim.height();
00813     playerdims = QString("%1x%2").arg(width).arg(height);
00814 
00815     if (debug_template)
00816     {
00817         if ((tmpl_done = readTemplate(debugdata, &tmplrow, &tmplcol,
00818                         &tmplwidth, &tmplheight, debugtmpl, &tmpl,
00819                         &tmpl_valid)))
00820         {
00821             tmpldims = tmpl_valid ? QString("%1x%2@(%3,%4)")
00822                 .arg(tmplwidth).arg(tmplheight).arg(tmplcol).arg(tmplrow) :
00823                     "no template";
00824 
00825             LOG(VB_COMMFLAG, LOG_INFO,
00826                 QString("TemplateFinder::MythPlayerInited read %1: %2")
00827                     .arg(debugtmpl)
00828                     .arg(tmpldims));
00829         }
00830     }
00831 
00832     if (pgmConverter->MythPlayerInited(player))
00833         goto free_tmpl;
00834 
00835     if (borderDetector->MythPlayerInited(player))
00836         goto free_tmpl;
00837 
00838     if (tmpl_done)
00839     {
00840         if (tmpl_valid)
00841         {
00842             LOG(VB_COMMFLAG, LOG_INFO,
00843                 QString("TemplateFinder::MythPlayerInited %1 of %2 (%3)")
00844                     .arg(tmpldims).arg(playerdims).arg(debugtmpl));
00845         }
00846         return ANALYZE_FINISHED;
00847     }
00848 
00849     LOG(VB_COMMFLAG, LOG_INFO,
00850         QString("TemplateFinder::MythPlayerInited framesize %1")
00851             .arg(playerdims));
00852     scores = new unsigned int[width * height];
00853 
00854     return ANALYZE_OK;
00855 
00856 free_tmpl:
00857     avpicture_free(&tmpl);
00858     return ANALYZE_FATAL;
00859 }
00860 
00861 int
00862 TemplateFinder::resetBuffers(int newwidth, int newheight)
00863 {
00864     if (cwidth == newwidth && cheight == newheight)
00865         return 0;
00866 
00867     avpicture_free(&cropped);
00868 
00869     if (avpicture_alloc(&cropped, PIX_FMT_GRAY8, newwidth, newheight))
00870     {
00871         LOG(VB_COMMFLAG, LOG_ERR,
00872             QString("TemplateFinder::resetBuffers "
00873                     "avpicture_alloc cropped (%1x%2) failed")
00874                 .arg(newwidth).arg(newheight));
00875         return -1;
00876     }
00877 
00878     cwidth = newwidth;
00879     cheight = newheight;
00880     return 0;
00881 }
00882 
00883 enum FrameAnalyzer::analyzeFrameResult
00884 TemplateFinder::analyzeFrame(const VideoFrame *frame, long long frameno,
00885         long long *pNextFrame)
00886 {
00887     /*
00888      * TUNABLE:
00889      *
00890      * When looking for edges in frames, select some percentile of
00891      * squared-gradient magnitudes (intensities) as candidate edges. (This
00892      * number conventionally should not go any lower than the 95th percentile;
00893      * see edge_mark.)
00894      *
00895      * Higher values result in fewer edges; faint logos might not be picked up.
00896      * Lower values result in more edges; non-logo edges might be picked up.
00897      *
00898      * The TemplateFinder accumulates all its state in the "scores" array to
00899      * be processed later by TemplateFinder::finished.
00900      */
00901     const int           FRAMESGMPCTILE = 90;
00902 
00903     /*
00904      * TUNABLE:
00905      *
00906      * Exclude some portion of the center of the frame from edge analysis.
00907      * Elminate false edge-detection logo positives from talking-host types of
00908      * shows where the high-contrast host and clothes (e.g., tie against white
00909      * shirt against dark jacket) dominates the edges.
00910      *
00911      * This has a nice side-effect of reducing the area to be examined (speed
00912      * optimization).
00913      */
00914     static const float  EXCLUDEWIDTH = 0.5;
00915     static const float  EXCLUDEHEIGHT = 0.5;
00916 
00917     const AVPicture     *pgm, *edges;
00918     int                 pgmwidth, pgmheight;
00919     int                 croprow, cropcol, cropwidth, cropheight;
00920     int                 excluderow, excludecol, excludewidth, excludeheight;
00921     struct timeval      start, end, elapsed;
00922 
00923     if (frameno < nextFrame)
00924     {
00925         *pNextFrame = nextFrame;
00926         return ANALYZE_OK;
00927     }
00928 
00929     nextFrame = frameno + frameInterval;
00930     *pNextFrame = min(endFrame, nextFrame);
00931 
00932     if (!(pgm = pgmConverter->getImage(frame, frameno, &pgmwidth, &pgmheight)))
00933         goto error;
00934 
00935     if (!borderDetector->getDimensions(pgm, pgmheight, frameno,
00936                 &croprow, &cropcol, &cropwidth, &cropheight))
00937     {
00938         /* Not a blank frame. */
00939 
00940         (void)gettimeofday(&start, NULL);
00941 
00942         if (croprow < mincontentrow)
00943             mincontentrow = croprow;
00944         if (cropcol < mincontentcol)
00945             mincontentcol = cropcol;
00946         if (cropcol + cropwidth > maxcontentcol1)
00947             maxcontentcol1 = cropcol + cropwidth;
00948         if (croprow + cropheight > maxcontentrow1)
00949             maxcontentrow1 = croprow + cropheight;
00950 
00951         if (resetBuffers(cropwidth, cropheight))
00952             goto error;
00953 
00954         if (pgm_crop(&cropped, pgm, pgmheight, croprow, cropcol,
00955                     cropwidth, cropheight))
00956             goto error;
00957 
00958         /*
00959          * Translate the excluded area of the screen into "cropped"
00960          * coordinates.
00961          */
00962         excludewidth = (int)(pgmwidth * EXCLUDEWIDTH);
00963         excludeheight = (int)(pgmheight * EXCLUDEHEIGHT);
00964         excluderow = (pgmheight - excludeheight) / 2 - croprow;
00965         excludecol = (pgmwidth - excludewidth) / 2 - cropcol;
00966         (void)edgeDetector->setExcludeArea(excluderow, excludecol,
00967                 excludewidth, excludeheight);
00968 
00969         if (!(edges = edgeDetector->detectEdges(&cropped, cropheight,
00970                         FRAMESGMPCTILE)))
00971             goto error;
00972 
00973         if (pgm_scorepixels(scores, pgmwidth, croprow, cropcol,
00974                     edges, cropheight))
00975             goto error;
00976 
00977         if (debugLevel >= 2)
00978         {
00979             if (analyzeFrameDebug(frameno, pgm, pgmheight, &cropped, edges,
00980                         cropheight, croprow, cropcol, debug_frames, debugdir))
00981                 goto error;
00982         }
00983 
00984         (void)gettimeofday(&end, NULL);
00985         timersub(&end, &start, &elapsed);
00986         timeradd(&analyze_time, &elapsed, &analyze_time);
00987     }
00988 
00989     if (nextFrame > endFrame)
00990         return ANALYZE_FINISHED;
00991 
00992     return ANALYZE_OK;
00993 
00994 error:
00995     LOG(VB_COMMFLAG, LOG_ERR,
00996         QString("TemplateFinder::analyzeFrame error at frame %1")
00997             .arg(frameno));
00998 
00999     if (nextFrame > endFrame)
01000         return ANALYZE_FINISHED;
01001 
01002     return ANALYZE_ERROR;
01003 }
01004 
01005 int
01006 TemplateFinder::finished(long long nframes, bool final)
01007 {
01008     (void)nframes;  /* gcc */
01009     if (!tmpl_done)
01010     {
01011         if (template_alloc(scores, width, height,
01012                     mincontentrow, mincontentcol,
01013                     maxcontentrow1, maxcontentcol1,
01014                     &tmpl, &tmplrow, &tmplcol, &tmplwidth, &tmplheight,
01015                     debug_edgecounts, debugdir))
01016         {
01017             if (final)
01018                 writeDummyTemplate(debugdata);
01019         }
01020         else
01021         {
01022             if (final && debug_template)
01023             {
01024                 if (!(tmpl_valid = writeTemplate(debugtmpl, &tmpl, debugdata,
01025                                 tmplrow, tmplcol, tmplwidth, tmplheight)))
01026                     goto free_tmpl;
01027 
01028                 LOG(VB_COMMFLAG, LOG_INFO,
01029                     QString("TemplateFinder::finished wrote %1"
01030                             " and %2 [%3x%4@(%5,%6)]")
01031                         .arg(debugtmpl).arg(debugdata)
01032                         .arg(tmplwidth).arg(tmplheight)
01033                         .arg(tmplcol).arg(tmplrow));
01034             }
01035         }
01036 
01037         if (final)
01038             tmpl_done = true;
01039     }
01040 
01041     borderDetector->setLogoState(this);
01042 
01043     return 0;
01044 
01045 free_tmpl:
01046     avpicture_free(&tmpl);
01047     return -1;
01048 }
01049 
01050 int
01051 TemplateFinder::reportTime(void) const
01052 {
01053     if (pgmConverter->reportTime())
01054         return -1;
01055 
01056     if (borderDetector->reportTime())
01057         return -1;
01058 
01059     LOG(VB_COMMFLAG, LOG_INFO, QString("TF Time: analyze=%1s")
01060             .arg(strftimeval(&analyze_time)));
01061     return 0;
01062 }
01063 
01064 const struct AVPicture *
01065 TemplateFinder::getTemplate(int *prow, int *pcol, int *pwidth, int *pheight)
01066     const
01067 {
01068     if (tmpl_valid)
01069     {
01070         *prow = tmplrow;
01071         *pcol = tmplcol;
01072         *pwidth = tmplwidth;
01073         *pheight = tmplheight;
01074         return &tmpl;
01075     }
01076     return NULL;
01077 }
01078 
01079 /* vim: set expandtab tabstop=4 shiftwidth=4: */
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends