MythTV  master
TemplateFinder.cpp
Go to the documentation of this file.
1 // POSIX headers
2 #include <sys/time.h> /* gettimeofday */
3 
4 // ANSI C headers
5 #include <cmath>
6 #include <cstdlib>
7 
8 // Qt headers
9 #include <QFile>
10 #include <QFileInfo>
11 #include <QTextStream>
12 
13 // MythTV headers
14 #include "mythplayer.h"
15 #include "mythcorecontext.h" /* gContext */
16 #include "mythframe.h" /* VideoFrame */
17 #include "mythdate.h"
18 #include "mythsystemlegacy.h"
19 #include "exitcodes.h"
20 
21 // Commercial Flagging headers
22 #include "CommDetector2.h"
23 #include "pgm.h"
24 #include "PGMConverter.h"
25 #include "BorderDetector.h"
26 #include "EdgeDetector.h"
27 #include "TemplateFinder.h"
28 
29 extern "C" {
30  #include "libavutil/imgutils.h"
31  }
32 
33 using namespace commDetector2;
34 
35 namespace {
36 
37 int writeJPG(QString prefix, const AVFrame *img, int imgheight)
38 {
39  const int imgwidth = img->linesize[0];
40  QFileInfo jpgfi(prefix + ".jpg");
41  if (!jpgfi.exists())
42  {
43  QFile pgmfile(prefix + ".pgm");
44  if (!pgmfile.exists())
45  {
46  QByteArray pfname = pgmfile.fileName().toLocal8Bit();
47  if (pgm_write(img->data[0], imgwidth, imgheight,
48  pfname.constData()))
49  {
50  return -1;
51  }
52  }
53 
54  QString cmd = QString("convert -quality 50 -resize 192x144 %1 %2")
55  .arg(pgmfile.fileName()).arg(jpgfi.filePath());
56  if (myth_system(cmd) != GENERIC_EXIT_OK)
57  return -1;
58 
59  if (!pgmfile.remove())
60  {
61  LOG(VB_COMMFLAG, LOG_ERR,
62  QString("TemplateFinder.writeJPG error removing %1 (%2)")
63  .arg(pgmfile.fileName()).arg(strerror(errno)));
64  return -1;
65  }
66  }
67  return 0;
68 }
69 
70 int
71 pgm_scorepixels(unsigned int *scores, int width, int row, int col,
72  const AVFrame *src, int srcheight)
73 {
74  /* Every time a pixel is an edge, give it a point. */
75  const int srcwidth = src->linesize[0];
76  int rr, cc;
77 
78  for (rr = 0; rr < srcheight; rr++)
79  {
80  for (cc = 0; cc < srcwidth; cc++)
81  {
82  if (src->data[0][rr * srcwidth + cc])
83  scores[(row + rr) * width + col + cc]++;
84  }
85  }
86 
87  return 0;
88 }
89 
90 int
91 sort_ascending(const void *aa, const void *bb)
92 {
93  return *(unsigned int*)aa - *(unsigned int*)bb;
94 }
95 
96 float
97 bounding_score(const AVFrame *img, int row, int col, int width, int height)
98 {
99  /* Return a value between [0..1] */
100  const int imgwidth = img->linesize[0];
101  unsigned int score;
102  int rr, cc, rr2, cc2;
103 
104  score = 0;
105  rr2 = row + height;
106  cc2 = col + width;
107  for (rr = row; rr < rr2; rr++)
108  {
109  for (cc = col; cc < cc2; cc++)
110  {
111  if (img->data[0][rr * imgwidth + cc])
112  score++;
113  }
114  }
115  return (float)score / (width * height);
116 }
117 
118 bool
119 rowisempty(const AVFrame *img, int row, int col, int width)
120 {
121  const int imgwidth = img->linesize[0];
122  for (int cc = col; cc < col + width; cc++)
123  if (img->data[0][row * imgwidth + cc])
124  return false;
125  return true;
126 }
127 
128 bool
129 colisempty(const AVFrame *img, int col, int row, int height)
130 {
131  const int imgwidth = img->linesize[0];
132  for (int rr = row; rr < row + height; rr++)
133  if (img->data[0][rr * imgwidth + col])
134  return false;
135  return true;
136 }
137 
138 int
139 bounding_box(const AVFrame *img, int imgheight,
140  int minrow, int mincol, int maxrow1, int maxcol1,
141  int *prow, int *pcol, int *pwidth, int *pheight)
142 {
143  const int imgwidth = img->linesize[0];
144  /*
145  * TUNABLE:
146  *
147  * Maximum logo size, expressed as a percentage of the content area
148  * (adjusting for letterboxing and pillarboxing).
149  */
150  static const int MAXWIDTHPCT = 20;
151  static const int MAXHEIGHTPCT = 20;
152 
153  /*
154  * TUNABLE:
155  *
156  * Safety margin to avoid cutting too much of the logo.
157  * Higher values cut more, but avoid noise as part of the template..
158  * Lower values cut less, but can include noise as part of the template.
159  */
160  const int VERTSLOP = max(4, imgheight * 1 / 15);
161  const int HORIZSLOP = max(4, imgwidth * 1 / 20);
162 
163  int maxwidth, maxheight;
164  int width, height, row, col;
165  int newrow, newcol, newright, newbottom;
166 
167  maxwidth = (maxcol1 - mincol) * MAXWIDTHPCT / 100;
168  maxheight = (maxrow1 - minrow) * MAXHEIGHTPCT / 100;
169 
170  row = minrow;
171  col = mincol;
172  width = maxcol1 - mincol;
173  height = maxrow1 - minrow;
174 
175  for (;;)
176  {
177  float score, newscore;
178  int ii;
179  bool improved = false;
180 
181  LOG(VB_COMMFLAG, LOG_INFO, QString("bounding_box %1x%2@(%3,%4)")
182  .arg(width).arg(height).arg(col).arg(row));
183 
184  /* Chop top. */
185  score = bounding_score(img, row, col, width, height);
186  newrow = row;
187  for (ii = 1; ii < height; ii++)
188  {
189  if ((newscore = bounding_score(img, row + ii, col,
190  width, height - ii)) < score)
191  break;
192  score = newscore;
193  newrow = row + ii;
194  improved = true;
195  }
196 
197  /* Chop left. */
198  score = bounding_score(img, row, col, width, height);
199  newcol = col;
200  for (ii = 1; ii < width; ii++)
201  {
202  if ((newscore = bounding_score(img, row, col + ii,
203  width - ii, height)) < score)
204  break;
205  score = newscore;
206  newcol = col + ii;
207  improved = true;
208  }
209 
210  /* Chop bottom. */
211  score = bounding_score(img, row, col, width, height);
212  newbottom = row + height;
213  for (ii = 1; ii < height; ii++)
214  {
215  if ((newscore = bounding_score(img, row, col,
216  width, height - ii)) < score)
217  break;
218  score = newscore;
219  newbottom = row + height - ii;
220  improved = true;
221  }
222 
223  /* Chop right. */
224  score = bounding_score(img, row, col, width, height);
225  newright = col + width;
226  for (ii = 1; ii < width; ii++)
227  {
228  if ((newscore = bounding_score(img, row, col,
229  width - ii, height)) < score)
230  break;
231  score = newscore;
232  newright = col + width - ii;
233  improved = true;
234  }
235 
236  if (!improved)
237  break;
238 
239  row = newrow;
240  col = newcol;
241  width = newright - newcol;
242  height = newbottom - newrow;
243 
244  /*
245  * Noise edge pixels in the frequency template can sometimes stretch
246  * the template area to be larger than it should be.
247  *
248  * However, noise needs to be distinguished from a uniform distribution
249  * of noise pixels (e.g., no real statically-located template). So if
250  * the template area is too "large", then some quadrant must have a
251  * clear majority of the edge pixels; otherwise we declare failure (no
252  * template found).
253  *
254  * Intuitively, we should simply repeat until a single bounding box is
255  * converged upon. However, this requires a more sophisticated
256  * bounding_score function that I don't feel like figuring out.
257  * Indefinitely repeating with the present bounding_score function will
258  * tend to chop off too much. Instead, simply do some sanity checks on
259  * the candidate template's size, and prune the template area and
260  * repeat if it is too "large".
261  */
262 
263  if (width > maxwidth)
264  {
265  /* Too wide; test left and right portions. */
266  int chop = width / 3;
267  int chopwidth = width - chop;
268  float left, right, minscore, maxscore;
269 
270  left = bounding_score(img, row, col, chopwidth, height);
271  right = bounding_score(img, row, col + chop, chopwidth, height);
272  LOG(VB_COMMFLAG, LOG_INFO,
273  QString("bounding_box too wide (%1 > %2); left=%3, right=%4")
274  .arg(width).arg(maxwidth)
275  .arg(left, 0, 'f', 3).arg(right, 0, 'f', 3));
276  minscore = min(left, right);
277  maxscore = max(left, right);
278  if (maxscore < 3 * minscore / 2)
279  {
280  /*
281  * Edge pixel distribution too uniform; give up.
282  *
283  * XXX: also fails for horizontally-centered templates ...
284  */
285  LOG(VB_COMMFLAG, LOG_ERR, "bounding_box giving up (edge "
286  "pixels distributed too uniformly)");
287  return -1;
288  }
289 
290  if (left < right)
291  col += chop;
292  width -= chop;
293  continue;
294  }
295 
296  if (height > maxheight)
297  {
298  /* Too tall; test upper and lower portions. */
299  int chop = height / 3;
300  int chopheight = height - chop;
301  float upper, lower, minscore, maxscore;
302 
303  upper = bounding_score(img, row, col, width, chopheight);
304  lower = bounding_score(img, row + chop, col, width, chopheight);
305  LOG(VB_COMMFLAG, LOG_INFO,
306  QString("bounding_box too tall (%1 > %2); upper=%3, lower=%4")
307  .arg(height).arg(maxheight)
308  .arg(upper, 0, 'f', 3).arg(lower, 0, 'f', 3));
309  minscore = min(upper, lower);
310  maxscore = max(upper, lower);
311  if (maxscore < 3 * minscore / 2)
312  {
313  /*
314  * Edge pixel distribution too uniform; give up.
315  *
316  * XXX: also fails for vertically-centered templates ...
317  */
318  LOG(VB_COMMFLAG, LOG_ERR, "bounding_box giving up (edge "
319  "pixel distribution too uniform)");
320  return -1;
321  }
322 
323  if (upper < lower)
324  row += chop;
325  height -= chop;
326  continue;
327  }
328 
329  break;
330  }
331 
332  /*
333  * The above "chop" algorithm often cuts off the outside edges of the
334  * logos because the outside edges don't contribute enough to the score. So
335  * compensate by now expanding the bounding box (up to a *SLOP pixels in
336  * each direction) to include all edge pixels.
337  */
338 
339  LOG(VB_COMMFLAG, LOG_INFO,
340  QString("bounding_box %1x%2@(%3,%4); horizslop=%5,vertslop=%6")
341  .arg(width).arg(height).arg(col).arg(row)
342  .arg(HORIZSLOP).arg(VERTSLOP));
343 
344  /* Expand upwards. */
345  newrow = row - 1;
346  for (;;)
347  {
348  if (newrow <= minrow)
349  {
350  newrow = minrow;
351  break;
352  }
353  if (row - newrow >= VERTSLOP)
354  {
355  newrow = row - VERTSLOP;
356  break;
357  }
358  if (rowisempty(img, newrow, col, width))
359  {
360  newrow++;
361  break;
362  }
363  newrow--;
364  }
365  newrow = max(minrow, newrow - 1); /* Empty row on top. */
366 
367  /* Expand leftwards. */
368  newcol = col - 1;
369  for (;;)
370  {
371  if (newcol <= mincol)
372  {
373  newcol = mincol;
374  break;
375  }
376  if (col - newcol >= HORIZSLOP)
377  {
378  newcol = col - HORIZSLOP;
379  break;
380  }
381  if (colisempty(img, newcol, row, height))
382  {
383  newcol++;
384  break;
385  }
386  newcol--;
387  }
388  newcol = max(mincol, newcol - 1); /* Empty column to left. */
389 
390  /* Expand rightwards. */
391  newright = col + width;
392  for (;;)
393  {
394  if (newright >= maxcol1)
395  {
396  newright = maxcol1;
397  break;
398  }
399  if (newright - (col + width) >= HORIZSLOP)
400  {
401  newright = col + width + HORIZSLOP;
402  break;
403  }
404  if (colisempty(img, newright, row, height))
405  break;
406  newright++;
407  }
408  newright = min(maxcol1, newright + 1); /* Empty column to right. */
409 
410  /* Expand downwards. */
411  newbottom = row + height;
412  for (;;)
413  {
414  if (newbottom >= maxrow1)
415  {
416  newbottom = maxrow1;
417  break;
418  }
419  if (newbottom - (row + height) >= VERTSLOP)
420  {
421  newbottom = row + height + VERTSLOP;
422  break;
423  }
424  if (rowisempty(img, newbottom, col, width))
425  break;
426  newbottom++;
427  }
428  newbottom = min(maxrow1, newbottom + 1); /* Empty row on bottom. */
429 
430  row = newrow;
431  col = newcol;
432  width = newright - newcol;
433  height = newbottom - newrow;
434 
435  LOG(VB_COMMFLAG, LOG_INFO, QString("bounding_box %1x%2@(%3,%4)")
436  .arg(width).arg(height).arg(col).arg(row));
437 
438  *prow = row;
439  *pcol = col;
440  *pwidth = width;
441  *pheight = height;
442  return 0;
443 }
444 
445 int
446 template_alloc(const unsigned int *scores, int width, int height,
447  int minrow, int mincol, int maxrow1, int maxcol1, AVFrame *tmpl,
448  int *ptmplrow, int *ptmplcol, int *ptmplwidth, int *ptmplheight,
449  bool debug_edgecounts, QString debugdir)
450 {
451  /*
452  * TUNABLE:
453  *
454  * Higher values select for "stronger" pixels to be in the template, but
455  * weak pixels might be missed.
456  *
457  * Lower values allow more pixels to be included as part of the template,
458  * but strong non-template pixels might be included.
459  */
460  static const float MINSCOREPCTILE = 0.998;
461 
462  const int nn = width * height;
463  int ii, first, last;
464  unsigned int *sortedscores, threshscore;
465  AVFrame thresh;
466 
467  if (av_image_alloc(thresh.data, thresh.linesize,
468  width, height, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
469  {
470  LOG(VB_COMMFLAG, LOG_ERR,
471  QString("template_alloc av_image_alloc thresh (%1x%2) failed")
472  .arg(width).arg(height));
473  return -1;
474  }
475 
476  sortedscores = new unsigned int[nn];
477  memcpy(sortedscores, scores, nn * sizeof(*sortedscores));
478  qsort(sortedscores, nn, sizeof(*sortedscores), sort_ascending);
479 
480  if (sortedscores[0] == sortedscores[nn - 1])
481  {
482  /* All pixels in the template area look the same; no template. */
483  LOG(VB_COMMFLAG, LOG_ERR,
484  QString("template_alloc: %1x%2 pixels all identical!")
485  .arg(width).arg(height));
486  goto free_thresh;
487  }
488 
489  /* Threshold the edge frequences. */
490 
491  ii = (int)roundf(nn * MINSCOREPCTILE);
492  threshscore = sortedscores[ii];
493  for (first = ii; first > 0 && sortedscores[first] == threshscore; first--)
494  ;
495  if (sortedscores[first] != threshscore)
496  first++;
497  for (last = ii; last < nn - 1 && sortedscores[last] == threshscore; last++)
498  ;
499  if (sortedscores[last] != threshscore)
500  last--;
501 
502  LOG(VB_COMMFLAG, LOG_INFO, QString("template_alloc wanted %1, got %2-%3")
503  .arg(MINSCOREPCTILE, 0, 'f', 6)
504  .arg((float)first / nn, 0, 'f', 6)
505  .arg((float)last / nn, 0, 'f', 6));
506 
507  for (ii = 0; ii < nn; ii++)
508  thresh.data[0][ii] = scores[ii] >= threshscore ? UCHAR_MAX : 0;
509 
510  if (debug_edgecounts)
511  {
512  /* Scores, rescaled to [0..UCHAR_MAX]. */
513  AVFrame scored;
514  if (av_image_alloc(scored.data, scored.linesize,
515  width, height, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
516  {
517  LOG(VB_COMMFLAG, LOG_ERR,
518  QString("template_alloc av_image_alloc scored (%1x%2) failed")
519  .arg(width).arg(height));
520  goto free_thresh;
521  }
522  unsigned int maxscore = sortedscores[nn - 1];
523  for (ii = 0; ii < nn; ii++)
524  scored.data[0][ii] = scores[ii] * UCHAR_MAX / maxscore;
525  int error = writeJPG(debugdir + "/TemplateFinder-scores", &scored,
526  height);
527  av_freep(&scored.data[0]);
528  if (error)
529  goto free_thresh;
530 
531  /* Thresholded scores. */
532  if (writeJPG(debugdir + "/TemplateFinder-edgecounts", &thresh, height))
533  goto free_thresh;
534  }
535 
536  /* Crop to a minimal bounding box. */
537 
538  if (bounding_box(&thresh, height, minrow, mincol, maxrow1, maxcol1,
539  ptmplrow, ptmplcol, ptmplwidth, ptmplheight))
540  goto free_thresh;
541 
542  if ((uint)(*ptmplwidth * *ptmplheight) > USHRT_MAX)
543  {
544  /* Max value of data type of TemplateMatcher::edgematch */
545  LOG(VB_COMMFLAG, LOG_ERR,
546  QString("template_alloc bounding_box too big (%1x%2)")
547  .arg(*ptmplwidth).arg(*ptmplheight));
548  goto free_thresh;
549  }
550 
551  if (av_image_alloc(tmpl->data, tmpl->linesize,
552  *ptmplwidth, *ptmplheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
553  {
554  LOG(VB_COMMFLAG, LOG_ERR,
555  QString("template_alloc av_image_alloc tmpl (%1x%2) failed")
556  .arg(*ptmplwidth).arg(*ptmplheight));
557  goto free_thresh;
558  }
559 
560  if (pgm_crop(tmpl, &thresh, height, *ptmplrow, *ptmplcol,
561  *ptmplwidth, *ptmplheight))
562  goto free_thresh;
563 
564  delete []sortedscores;
565  av_freep(&thresh.data[0]);
566 
567  return 0;
568 
569 free_thresh:
570  delete []sortedscores;
571  av_freep(&thresh.data[0]);
572  return -1;
573 }
574 
575 int
576 analyzeFrameDebug(long long frameno, const AVFrame *pgm, int pgmheight,
577  const AVFrame *cropped, const AVFrame *edges, int cropheight,
578  int croprow, int cropcol, bool debug_frames, QString debugdir)
579 {
580  static const int delta = 24;
581  static int lastrow, lastcol, lastwidth, lastheight;
582  const int cropwidth = cropped->linesize[0];
583  int rowsame, colsame, widthsame, heightsame;
584 
585  rowsame = abs(lastrow - croprow) <= delta ? 1 : 0;
586  colsame = abs(lastcol - cropcol) <= delta ? 1 : 0;
587  widthsame = abs(lastwidth - cropwidth) <= delta ? 1 : 0;
588  heightsame = abs(lastheight - cropheight) <= delta ? 1 : 0;
589 
590  if (frameno > 0 && rowsame + colsame + widthsame + heightsame >= 3)
591  return 0;
592 
593  LOG(VB_COMMFLAG, LOG_INFO,
594  QString("TemplateFinder Frame %1: %2x%3@(%4,%5)")
595  .arg(frameno, 5)
596  .arg(cropwidth).arg(cropheight)
597  .arg(cropcol).arg(croprow));
598 
599  lastrow = croprow;
600  lastcol = cropcol;
601  lastwidth = cropwidth;
602  lastheight = cropheight;
603 
604  if (debug_frames)
605  {
606  QString base = QString("%1/TemplateFinder-%2")
607  .arg(debugdir).arg(frameno, 5, 10, QChar('0'));
608 
609  /* PGM greyscale image of frame. */
610  if (writeJPG(base, pgm, pgmheight))
611  goto error;
612 
613  /* Cropped template area of frame. */
614  if (writeJPG(base + "-cropped", cropped, cropheight))
615  goto error;
616 
617  /* Edges of cropped template area of frame. */
618  if (writeJPG(base + "-edges", edges, cropheight))
619  goto error;
620  }
621 
622  return 0;
623 
624 error:
625  return -1;
626 }
627 
628 bool
629 readTemplate(QString datafile, int *prow, int *pcol, int *pwidth, int *pheight,
630  QString tmplfile, AVFrame *tmpl, bool *pvalid)
631 {
632  QFile dfile(datafile);
633  QFileInfo dfileinfo(dfile);
634 
635  if (!dfile.open(QIODevice::ReadOnly))
636  return false;
637 
638  if (!dfileinfo.size())
639  {
640  /* Dummy file: no template. */
641  *pvalid = false;
642  return true;
643  }
644 
645  QTextStream stream(&dfile);
646  stream >> *prow >> *pcol >> *pwidth >> *pheight;
647  dfile.close();
648 
649  if (av_image_alloc(tmpl->data, tmpl->linesize,
650  *pwidth, *pheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
651  {
652  LOG(VB_COMMFLAG, LOG_ERR,
653  QString("readTemplate av_image_alloc %1 (%2x%3) failed")
654  .arg(tmplfile).arg(*pwidth).arg(*pheight));
655  return false;
656  }
657 
658  QByteArray tmfile = tmplfile.toLatin1();
659  if (pgm_read(tmpl->data[0], *pwidth, *pheight, tmfile.constData()))
660  {
661  av_freep(&tmpl->data[0]);
662  return false;
663  }
664 
665  *pvalid = true;
666  return true;
667 }
668 
669 void
670 writeDummyTemplate(QString datafile)
671 {
672  /* Leave a 0-byte file. */
673  QFile dfile(datafile);
674 
675  if (!dfile.open(QIODevice::WriteOnly | QIODevice::Truncate) &&
676  dfile.exists())
677  (void)dfile.remove();
678 }
679 
680 bool
681 writeTemplate(QString tmplfile, const AVFrame *tmpl, QString datafile,
682  int row, int col, int width, int height)
683 {
684  QFile tfile(tmplfile);
685 
686  QByteArray tmfile = tmplfile.toLatin1();
687  if (pgm_write(tmpl->data[0], width, height, tmfile.constData()))
688  return false;
689 
690  QFile dfile(datafile);
691  if (!dfile.open(QIODevice::WriteOnly))
692  return false;
693 
694  QTextStream stream(&dfile);
695  stream << row << " " << col << "\n" << width << " " << height << "\n";
696  dfile.close();
697  return true;
698 }
699 
700 }; /* namespace */
701 
703  EdgeDetector *ed, MythPlayer *player, int proglen,
704  QString debugdir)
705  : FrameAnalyzer()
706  , pgmConverter(pgmc)
707  , borderDetector(bd)
708  , edgeDetector(ed)
709  , nextFrame(0)
710  , width(-1)
711  , height(-1)
712  , scores(nullptr)
713  , mincontentrow(INT_MAX)
714  , mincontentcol(INT_MAX)
715  , maxcontentrow1(INT_MIN)
716  , maxcontentcol1(INT_MIN)
717  , tmplrow(-1)
718  , tmplcol(-1)
719  , tmplwidth(-1)
720  , tmplheight(-1)
721  , cwidth(-1)
722  , cheight(-1)
723  , debugLevel(0)
724  , debugdir(debugdir)
725  , debugdata(debugdir + "/TemplateFinder.txt")
726  , debugtmpl(debugdir + "/TemplateFinder.pgm")
727  , debug_template(false)
728  , debug_edgecounts(false)
729  , debug_frames(false)
730  , tmpl_valid(false)
731  , tmpl_done(false)
732 {
733  /*
734  * TUNABLE:
735  *
736  * The number of frames desired for sampling to build the template.
737  *
738  * Higher values should yield a more accurate template, but requires more
739  * time.
740  */
741  unsigned int samplesNeeded = 300;
742 
743  /*
744  * TUNABLE:
745  *
746  * The leading amount of time (in seconds) to sample frames for building up
747  * the possible template, and the interval between frames for analysis.
748  * This affects how soon flagging can start after a recording has begun
749  * (a.k.a. "real-time flagging").
750  *
751  * Sample half of the program length or 20 minutes, whichever is less.
752  */
753  sampleTime = min(proglen / 2, 20 * 60);
754 
755  const float fps = player->GetFrameRate();
756 
757  frameInterval = (int)roundf(sampleTime * fps / samplesNeeded);
758  endFrame = 0 + (long long)frameInterval * samplesNeeded - 1;
759 
760  LOG(VB_COMMFLAG, LOG_INFO,
761  QString("TemplateFinder: sampleTime=%1s, samplesNeeded=%2, endFrame=%3")
762  .arg(sampleTime).arg(samplesNeeded).arg(endFrame));
763 
764  memset(&cropped, 0, sizeof(cropped));
765  memset(&tmpl, 0, sizeof(tmpl));
766  memset(&analyze_time, 0, sizeof(analyze_time));
767 
768  /*
769  * debugLevel:
770  * 0: no extra debugging
771  * 1: cache computations into debugdir [O(1) files]
772  * 2: extra verbosity [O(nframes)]
773  * 3: dump frames into debugdir [O(nframes) files]
774  */
775  debugLevel = gCoreContext->GetNumSetting("TemplateFinderDebugLevel", 0);
776 
777  if (debugLevel >= 1)
778  {
780  QString("TemplateFinder debugLevel %1").arg(debugLevel));
781 
782  debug_template = true;
783  debug_edgecounts = true;
784 
785  if (debugLevel >= 3)
786  debug_frames = true;
787  }
788 }
789 
791 {
792  if (scores)
793  delete []scores;
794  av_freep(&tmpl.data[0]);
795  av_freep(&cropped.data[0]);
796 }
797 
799 TemplateFinder::MythPlayerInited(MythPlayer *player, long long nframes)
800 {
801  /*
802  * Only detect edges in portions of the frame where we expect to find
803  * a template. This serves two purposes:
804  *
805  * - Speed: reduce search space.
806  * - Correctness (insofar as the assumption of template location is
807  * correct): don't "pollute" the set of candidate template edges with
808  * the "content" edges in the non-template portions of the frame.
809  */
810  QString tmpldims, playerdims;
811 
812  (void)nframes; /* gcc */
813  QSize buf_dim = player->GetVideoBufferSize();
814  width = buf_dim.width();
815  height = buf_dim.height();
816  playerdims = QString("%1x%2").arg(width).arg(height);
817 
818  if (debug_template)
819  {
820  if ((tmpl_done = readTemplate(debugdata, &tmplrow, &tmplcol,
822  &tmpl_valid)))
823  {
824  tmpldims = tmpl_valid ? QString("%1x%2@(%3,%4)")
825  .arg(tmplwidth).arg(tmplheight).arg(tmplcol).arg(tmplrow) :
826  "no template";
827 
828  LOG(VB_COMMFLAG, LOG_INFO,
829  QString("TemplateFinder::MythPlayerInited read %1: %2")
830  .arg(debugtmpl)
831  .arg(tmpldims));
832  }
833  }
834 
835  if (pgmConverter->MythPlayerInited(player))
836  goto free_tmpl;
837 
838  if (borderDetector->MythPlayerInited(player))
839  goto free_tmpl;
840 
841  if (tmpl_done)
842  {
843  if (tmpl_valid)
844  {
845  LOG(VB_COMMFLAG, LOG_INFO,
846  QString("TemplateFinder::MythPlayerInited %1 of %2 (%3)")
847  .arg(tmpldims).arg(playerdims).arg(debugtmpl));
848  }
849  return ANALYZE_FINISHED;
850  }
851 
852  LOG(VB_COMMFLAG, LOG_INFO,
853  QString("TemplateFinder::MythPlayerInited framesize %1")
854  .arg(playerdims));
855  scores = new unsigned int[width * height];
856 
857  return ANALYZE_OK;
858 
859 free_tmpl:
860  av_freep(&tmpl.data[0]);
861  return ANALYZE_FATAL;
862 }
863 
864 int
865 TemplateFinder::resetBuffers(int newwidth, int newheight)
866 {
867  if (cwidth == newwidth && cheight == newheight)
868  return 0;
869 
870  av_freep(&cropped.data[0]);
871 
872  if (av_image_alloc(cropped.data, cropped.linesize,
873  newwidth, newheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
874  {
875  LOG(VB_COMMFLAG, LOG_ERR,
876  QString("TemplateFinder::resetBuffers "
877  "av_image_alloc cropped (%1x%2) failed")
878  .arg(newwidth).arg(newheight));
879  return -1;
880  }
881 
882  cwidth = newwidth;
883  cheight = newheight;
884  return 0;
885 }
886 
888 TemplateFinder::analyzeFrame(const VideoFrame *frame, long long frameno,
889  long long *pNextFrame)
890 {
891  /*
892  * TUNABLE:
893  *
894  * When looking for edges in frames, select some percentile of
895  * squared-gradient magnitudes (intensities) as candidate edges. (This
896  * number conventionally should not go any lower than the 95th percentile;
897  * see edge_mark.)
898  *
899  * Higher values result in fewer edges; faint logos might not be picked up.
900  * Lower values result in more edges; non-logo edges might be picked up.
901  *
902  * The TemplateFinder accumulates all its state in the "scores" array to
903  * be processed later by TemplateFinder::finished.
904  */
905  const int FRAMESGMPCTILE = 90;
906 
907  /*
908  * TUNABLE:
909  *
910  * Exclude some portion of the center of the frame from edge analysis.
911  * Elminate false edge-detection logo positives from talking-host types of
912  * shows where the high-contrast host and clothes (e.g., tie against white
913  * shirt against dark jacket) dominates the edges.
914  *
915  * This has a nice side-effect of reducing the area to be examined (speed
916  * optimization).
917  */
918  static const float EXCLUDEWIDTH = 0.5;
919  static const float EXCLUDEHEIGHT = 0.5;
920 
921  const AVFrame *pgm, *edges;
922  int pgmwidth, pgmheight;
923  int croprow, cropcol, cropwidth, cropheight;
924  int excluderow, excludecol, excludewidth, excludeheight;
925  struct timeval start, end, elapsed;
926 
927  if (frameno < nextFrame)
928  {
929  *pNextFrame = nextFrame;
930  return ANALYZE_OK;
931  }
932 
933  nextFrame = frameno + frameInterval;
934  *pNextFrame = min(endFrame, nextFrame);
935 
936  if (!(pgm = pgmConverter->getImage(frame, frameno, &pgmwidth, &pgmheight)))
937  goto error;
938 
939  if (!borderDetector->getDimensions(pgm, pgmheight, frameno,
940  &croprow, &cropcol, &cropwidth, &cropheight))
941  {
942  /* Not a blank frame. */
943 
944  (void)gettimeofday(&start, nullptr);
945 
946  if (croprow < mincontentrow)
947  mincontentrow = croprow;
948  if (cropcol < mincontentcol)
949  mincontentcol = cropcol;
950  if (cropcol + cropwidth > maxcontentcol1)
951  maxcontentcol1 = cropcol + cropwidth;
952  if (croprow + cropheight > maxcontentrow1)
953  maxcontentrow1 = croprow + cropheight;
954 
955  if (resetBuffers(cropwidth, cropheight))
956  goto error;
957 
958  if (pgm_crop(&cropped, pgm, pgmheight, croprow, cropcol,
959  cropwidth, cropheight))
960  goto error;
961 
962  /*
963  * Translate the excluded area of the screen into "cropped"
964  * coordinates.
965  */
966  excludewidth = (int)(pgmwidth * EXCLUDEWIDTH);
967  excludeheight = (int)(pgmheight * EXCLUDEHEIGHT);
968  excluderow = (pgmheight - excludeheight) / 2 - croprow;
969  excludecol = (pgmwidth - excludewidth) / 2 - cropcol;
970  (void)edgeDetector->setExcludeArea(excluderow, excludecol,
971  excludewidth, excludeheight);
972 
973  if (!(edges = edgeDetector->detectEdges(&cropped, cropheight,
974  FRAMESGMPCTILE)))
975  goto error;
976 
977  if (pgm_scorepixels(scores, pgmwidth, croprow, cropcol,
978  edges, cropheight))
979  goto error;
980 
981  if (debugLevel >= 2)
982  {
983  if (analyzeFrameDebug(frameno, pgm, pgmheight, &cropped, edges,
984  cropheight, croprow, cropcol, debug_frames, debugdir))
985  goto error;
986  }
987 
988  (void)gettimeofday(&end, nullptr);
989  timersub(&end, &start, &elapsed);
990  timeradd(&analyze_time, &elapsed, &analyze_time);
991  }
992 
993  if (nextFrame > endFrame)
994  return ANALYZE_FINISHED;
995 
996  return ANALYZE_OK;
997 
998 error:
999  LOG(VB_COMMFLAG, LOG_ERR,
1000  QString("TemplateFinder::analyzeFrame error at frame %1")
1001  .arg(frameno));
1002 
1003  if (nextFrame > endFrame)
1004  return ANALYZE_FINISHED;
1005 
1006  return ANALYZE_ERROR;
1007 }
1008 
1009 int
1010 TemplateFinder::finished(long long nframes, bool final)
1011 {
1012  (void)nframes; /* gcc */
1013  if (!tmpl_done)
1014  {
1015  if (template_alloc(scores, width, height,
1020  {
1021  if (final)
1022  writeDummyTemplate(debugdata);
1023  }
1024  else
1025  {
1026  if (final && debug_template)
1027  {
1028  if (!(tmpl_valid = writeTemplate(debugtmpl, &tmpl, debugdata,
1030  goto free_tmpl;
1031 
1032  LOG(VB_COMMFLAG, LOG_INFO,
1033  QString("TemplateFinder::finished wrote %1"
1034  " and %2 [%3x%4@(%5,%6)]")
1035  .arg(debugtmpl).arg(debugdata)
1036  .arg(tmplwidth).arg(tmplheight)
1037  .arg(tmplcol).arg(tmplrow));
1038  }
1039  }
1040 
1041  if (final)
1042  tmpl_done = true;
1043  }
1044 
1046 
1047  return 0;
1048 
1049 free_tmpl:
1050  av_freep(&tmpl.data[0]);
1051  return -1;
1052 }
1053 
1054 int
1056 {
1057  if (pgmConverter->reportTime())
1058  return -1;
1059 
1060  if (borderDetector->reportTime())
1061  return -1;
1062 
1063  LOG(VB_COMMFLAG, LOG_INFO, QString("TF Time: analyze=%1s")
1064  .arg(strftimeval(&analyze_time)));
1065  return 0;
1066 }
1067 
1068 const struct AVFrame *
1069 TemplateFinder::getTemplate(int *prow, int *pcol, int *pwidth, int *pheight)
1070  const
1071 {
1072  if (tmpl_valid)
1073  {
1074  *prow = tmplrow;
1075  *pcol = tmplcol;
1076  *pwidth = tmplwidth;
1077  *pheight = tmplheight;
1078  return &tmpl;
1079  }
1080  return nullptr;
1081 }
1082 
1083 /* vim: set expandtab tabstop=4 shiftwidth=4: */
Definition: cc.h:13
struct exc__state * last
Definition: pxsup2dast.c:98
long long endFrame
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
enum analyzeFrameResult analyzeFrame(const VideoFrame *frame, long long frameno, long long *pNextFrame) override
static void error(const char *str,...)
Definition: vbi.c:41
TemplateFinder(PGMConverter *pgmc, BorderDetector *bd, EdgeDetector *ed, MythPlayer *player, int proglen, QString debugdir)
#define timeradd(a, b, result)
Definition: compat.h:293
struct AVFrame AVFrame
BorderDetector * borderDetector
struct timeval analyze_time
static int sort_ascending(const void *aa, const void *bb)
unsigned int uint
Definition: compat.h:140
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
long long nextFrame
int finished(long long nframes, bool final) override
int pgm_read(unsigned char *buf, int width, int height, const char *filename)
Definition: pgm.cpp:32
#define IMAGE_ALIGN
Definition: mythconfig.h:19
VERBOSE_PREAMBLE false
Definition: verbosedefs.h:85
int MythPlayerInited(const MythPlayer *player)
int reportTime(void) const override
const struct AVFrame * getTemplate(int *prow, int *pcol, int *pwidth, int *pheight) const
float GetFrameRate(void) const
Definition: mythplayer.h:176
void setLogoState(TemplateFinder *finder)
enum analyzeFrameResult MythPlayerInited(MythPlayer *player, long long nframes) override
int resetBuffers(int newcwidth, int newcheight)
uint myth_system(const QString &command, uint flags, uint timeout)
int MythPlayerInited(const MythPlayer *player)
int GetNumSetting(const QString &key, int defaultval=0)
PGMConverter * pgmConverter
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
QSize GetVideoBufferSize(void) const
Definition: mythplayer.h:173
int pgm_crop(AVFrame *dst, const AVFrame *src, int srcheight, int srcrow, int srccol, int cropwidth, int cropheight)
Definition: pgm.cpp:161
int pgm_write(const unsigned char *buf, int width, int height, const char *filename)
Definition: pgm.cpp:79
QString strftimeval(const struct timeval *tv)
int reportTime(void)
#define timersub(a, b, result)
Definition: compat.h:303
unsigned int sampleTime
int getDimensions(const AVFrame *pgm, int pgmheight, long long frameno, int *prow, int *pcol, int *pwidth, int *pheight)
unsigned int * scores
const AVFrame * getImage(const VideoFrame *frame, long long frameno, int *pwidth, int *pheight)
void createDebugDirectory(QString dirname, QString comment)
int reportTime(void)