MythTV  master
TemplateFinder.cpp
Go to the documentation of this file.
1 // ANSI C headers
2 #include <algorithm>
3 #include <cmath>
4 #include <cstdlib>
5 #include <utility>
6 
7 // Qt headers
8 #include <QFile>
9 #include <QFileInfo>
10 #include <QTextStream>
11 
12 // MythTV headers
13 #include "libmythbase/exitcodes.h"
15 #include "libmythbase/mythdate.h"
18 #include "libmythtv/mythframe.h" /* VideoFrame */
19 #include "libmythtv/mythplayer.h"
20 
21 // Commercial Flagging headers
22 #include "BorderDetector.h"
23 #include "CommDetector2.h"
24 #include "EdgeDetector.h"
25 #include "PGMConverter.h"
26 #include "TemplateFinder.h"
27 #include "pgm.h"
28 
29 extern "C" {
30  #include "libavutil/imgutils.h"
31  }
32 
33 using namespace commDetector2;
34 
35 namespace {
36 
37 //returns true on success, false otherwise
38 bool writeJPG(const QString& prefix, const AVFrame *img, int imgheight)
39 {
40  const int imgwidth = img->linesize[0];
41  QFileInfo jpgfi(prefix + ".jpg");
42  if (!jpgfi.exists())
43  {
44  QFile pgmfile(prefix + ".pgm");
45  if (!pgmfile.exists())
46  {
47  QByteArray pfname = pgmfile.fileName().toLocal8Bit();
48  if (pgm_write(img->data[0], imgwidth, imgheight,
49  pfname.constData()))
50  {
51  return false;
52  }
53  }
54 
55  QString cmd = QString("convert -quality 50 -resize 192x144 %1 %2")
56  .arg(pgmfile.fileName(), jpgfi.filePath());
57  if (myth_system(cmd) != GENERIC_EXIT_OK)
58  return false;
59 
60  if (!pgmfile.remove())
61  {
62  LOG(VB_COMMFLAG, LOG_ERR,
63  QString("TemplateFinder.writeJPG error removing %1 (%2)")
64  .arg(pgmfile.fileName(), strerror(errno)));
65  return false;
66  }
67  }
68  return true;
69 }
70 
71 int
72 pgm_scorepixels(unsigned int *scores, int width, int row, int col,
73  const AVFrame *src, int srcheight)
74 {
75  /* Every time a pixel is an edge, give it a point. */
76  const int srcwidth = src->linesize[0];
77 
78  for (int rr = 0; rr < srcheight; rr++)
79  {
80  for (int 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 
102  uint score = 0;
103  int rr2 = row + height;
104  int cc2 = col + width;
105  for (int rr = row; rr < rr2; rr++)
106  {
107  for (int cc = col; cc < cc2; cc++)
108  {
109  if (img->data[0][(rr * imgwidth) + cc])
110  score++;
111  }
112  }
113  return (float)score / (width * height);
114 }
115 
116 bool
117 rowisempty(const AVFrame *img, int row, int col, int width)
118 {
119  const int imgwidth = img->linesize[0];
120  for (int cc = col; cc < col + width; cc++)
121  if (img->data[0][(row * imgwidth) + cc])
122  return false;
123  return true;
124 }
125 
126 bool
127 colisempty(const AVFrame *img, int col, int row, int height)
128 {
129  const int imgwidth = img->linesize[0];
130  for (int rr = row; rr < row + height; rr++)
131  if (img->data[0][(rr * imgwidth) + col])
132  return false;
133  return true;
134 }
135 
136 int
137 bounding_box(const AVFrame *img, int imgheight,
138  int minrow, int mincol, int maxrow1, int maxcol1,
139  int *prow, int *pcol, int *pwidth, int *pheight)
140 {
141  const int imgwidth = img->linesize[0];
142  /*
143  * TUNABLE:
144  *
145  * Maximum logo size, expressed as a percentage of the content area
146  * (adjusting for letterboxing and pillarboxing).
147  */
148  static constexpr int kMaxWidthPct = 20;
149  static constexpr int kMaxHeightPct = 20;
150 
151  /*
152  * TUNABLE:
153  *
154  * Safety margin to avoid cutting too much of the logo.
155  * Higher values cut more, but avoid noise as part of the template..
156  * Lower values cut less, but can include noise as part of the template.
157  */
158  const int VERTSLOP = std::max(4, imgheight * 1 / 15);
159  const int HORIZSLOP = std::max(4, imgwidth * 1 / 20);
160 
161  int maxwidth = (maxcol1 - mincol) * kMaxWidthPct / 100;
162  int maxheight = (maxrow1 - minrow) * kMaxHeightPct / 100;
163 
164  int row = minrow;
165  int col = mincol;
166  int width = maxcol1 - mincol;
167  int height = maxrow1 - minrow;
168  int newrow = 0;
169  int newcol = 0;
170  int newright = 0;
171  int newbottom = 0;
172 
173  for (;;)
174  {
175  bool improved = false;
176 
177  LOG(VB_COMMFLAG, LOG_INFO, QString("bounding_box %1x%2@(%3,%4)")
178  .arg(width).arg(height).arg(col).arg(row));
179 
180  /* Chop top. */
181  float score = bounding_score(img, row, col, width, height);
182  newrow = row;
183  for (int ii = 1; ii < height; ii++)
184  {
185  float newscore =
186  bounding_score(img, row + ii, col, width, height - ii);
187  if (newscore < score)
188  break;
189  score = newscore;
190  newrow = row + ii;
191  improved = true;
192  }
193 
194  /* Chop left. */
195  score = bounding_score(img, row, col, width, height);
196  newcol = col;
197  for (int ii = 1; ii < width; ii++)
198  {
199  float newscore =
200  bounding_score(img, row, col + ii, width - ii, height);
201  if (newscore < score)
202  break;
203  score = newscore;
204  newcol = col + ii;
205  improved = true;
206  }
207 
208  /* Chop bottom. */
209  score = bounding_score(img, row, col, width, height);
210  newbottom = row + height;
211  for (int ii = 1; ii < height; ii++)
212  {
213  float newscore =
214  bounding_score(img, row, col, width, height - ii);
215  if (newscore < score)
216  break;
217  score = newscore;
218  newbottom = row + height - ii;
219  improved = true;
220  }
221 
222  /* Chop right. */
223  score = bounding_score(img, row, col, width, height);
224  newright = col + width;
225  for (int ii = 1; ii < width; ii++)
226  {
227  float newscore =
228  bounding_score(img, row, col, width - ii, height);
229  if (newscore < 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 
269  float left = bounding_score(img, row, col, chopwidth, height);
270  float right = bounding_score(img, row, col + chop, chopwidth, height);
271  LOG(VB_COMMFLAG, LOG_INFO,
272  QString("bounding_box too wide (%1 > %2); left=%3, right=%4")
273  .arg(width).arg(maxwidth)
274  .arg(left, 0, 'f', 3).arg(right, 0, 'f', 3));
275  float minscore = std::min(left, right);
276  float maxscore = std::max(left, right);
277  if (maxscore < 3 * minscore / 2)
278  {
279  /*
280  * Edge pixel distribution too uniform; give up.
281  *
282  * XXX: also fails for horizontally-centered templates ...
283  */
284  LOG(VB_COMMFLAG, LOG_ERR, "bounding_box giving up (edge "
285  "pixels distributed too uniformly)");
286  return -1;
287  }
288 
289  if (left < right)
290  col += chop;
291  width -= chop;
292  continue;
293  }
294 
295  if (height > maxheight)
296  {
297  /* Too tall; test upper and lower portions. */
298  int chop = height / 3;
299  int chopheight = height - chop;
300 
301  float upper = bounding_score(img, row, col, width, chopheight);
302  float lower = bounding_score(img, row + chop, col, width, chopheight);
303  LOG(VB_COMMFLAG, LOG_INFO,
304  QString("bounding_box too tall (%1 > %2); upper=%3, lower=%4")
305  .arg(height).arg(maxheight)
306  .arg(upper, 0, 'f', 3).arg(lower, 0, 'f', 3));
307  float minscore = std::min(upper, lower);
308  float maxscore = std::max(upper, lower);
309  if (maxscore < 3 * minscore / 2)
310  {
311  /*
312  * Edge pixel distribution too uniform; give up.
313  *
314  * XXX: also fails for vertically-centered templates ...
315  */
316  LOG(VB_COMMFLAG, LOG_ERR, "bounding_box giving up (edge "
317  "pixel distribution too uniform)");
318  return -1;
319  }
320 
321  if (upper < lower)
322  row += chop;
323  height -= chop;
324  continue;
325  }
326 
327  break;
328  }
329 
330  /*
331  * The above "chop" algorithm often cuts off the outside edges of the
332  * logos because the outside edges don't contribute enough to the score. So
333  * compensate by now expanding the bounding box (up to a *SLOP pixels in
334  * each direction) to include all edge pixels.
335  */
336 
337  LOG(VB_COMMFLAG, LOG_INFO,
338  QString("bounding_box %1x%2@(%3,%4); horizslop=%5,vertslop=%6")
339  .arg(width).arg(height).arg(col).arg(row)
340  .arg(HORIZSLOP).arg(VERTSLOP));
341 
342  /* Expand upwards. */
343  newrow = row - 1;
344  for (;;)
345  {
346  if (newrow <= minrow)
347  {
348  newrow = minrow;
349  break;
350  }
351  if (row - newrow >= VERTSLOP)
352  {
353  newrow = row - VERTSLOP;
354  break;
355  }
356  if (rowisempty(img, newrow, col, width))
357  {
358  newrow++;
359  break;
360  }
361  newrow--;
362  }
363  newrow = std::max(minrow, newrow - 1); /* Empty row on top. */
364 
365  /* Expand leftwards. */
366  newcol = col - 1;
367  for (;;)
368  {
369  if (newcol <= mincol)
370  {
371  newcol = mincol;
372  break;
373  }
374  if (col - newcol >= HORIZSLOP)
375  {
376  newcol = col - HORIZSLOP;
377  break;
378  }
379  if (colisempty(img, newcol, row, height))
380  {
381  newcol++;
382  break;
383  }
384  newcol--;
385  }
386  newcol = std::max(mincol, newcol - 1); /* Empty column to left. */
387 
388  /* Expand rightwards. */
389  newright = col + width;
390  for (;;)
391  {
392  if (newright >= maxcol1)
393  {
394  newright = maxcol1;
395  break;
396  }
397  if (newright - (col + width) >= HORIZSLOP)
398  {
399  newright = col + width + HORIZSLOP;
400  break;
401  }
402  if (colisempty(img, newright, row, height))
403  break;
404  newright++;
405  }
406  newright = std::min(maxcol1, newright + 1); /* Empty column to right. */
407 
408  /* Expand downwards. */
409  newbottom = row + height;
410  for (;;)
411  {
412  if (newbottom >= maxrow1)
413  {
414  newbottom = maxrow1;
415  break;
416  }
417  if (newbottom - (row + height) >= VERTSLOP)
418  {
419  newbottom = row + height + VERTSLOP;
420  break;
421  }
422  if (rowisempty(img, newbottom, col, width))
423  break;
424  newbottom++;
425  }
426  newbottom = std::min(maxrow1, newbottom + 1); /* Empty row on bottom. */
427 
428  row = newrow;
429  col = newcol;
430  width = newright - newcol;
431  height = newbottom - newrow;
432 
433  LOG(VB_COMMFLAG, LOG_INFO, QString("bounding_box %1x%2@(%3,%4)")
434  .arg(width).arg(height).arg(col).arg(row));
435 
436  *prow = row;
437  *pcol = col;
438  *pwidth = width;
439  *pheight = height;
440  return 0;
441 }
442 
443 bool
444 template_alloc(const unsigned int *scores, int width, int height,
445  int minrow, int mincol, int maxrow1, int maxcol1, AVFrame *tmpl,
446  int *ptmplrow, int *ptmplcol, int *ptmplwidth, int *ptmplheight,
447  bool debug_edgecounts, const QString& debugdir)
448 {
449  /*
450  * TUNABLE:
451  *
452  * Higher values select for "stronger" pixels to be in the template, but
453  * weak pixels might be missed.
454  *
455  * Lower values allow more pixels to be included as part of the template,
456  * but strong non-template pixels might be included.
457  */
458  static constexpr float kMinScorePctile = 0.998;
459 
460  const int nn = width * height;
461  int ii = 0;
462  int first = 0;
463  int last = 0;
464  unsigned int threshscore = 0;
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 false;
474  }
475 
476  uint *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 * kMinScorePctile);
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(kMinScorePctile, 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  bool success = writeJPG(debugdir + "/TemplateFinder-scores", &scored,
526  height);
527  av_freep(reinterpret_cast<void*>(&scored.data[0]));
528  if (!success)
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(reinterpret_cast<void*>(&thresh.data[0]));
566 
567  return true;
568 
569 free_thresh:
570  delete []sortedscores;
571  av_freep(reinterpret_cast<void*>(&thresh.data[0]));
572  return false;
573 }
574 
575 bool
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, const QString& debugdir)
579 {
580  static constexpr int kDelta = 24;
581  static int s_lastrow;
582  static int s_lastcol;
583  static int s_lastwidth;
584  static int s_lastheight;
585  const int cropwidth = cropped->linesize[0];
586 
587  int rowsame = abs(s_lastrow - croprow) <= kDelta ? 1 : 0;
588  int colsame = abs(s_lastcol - cropcol) <= kDelta ? 1 : 0;
589  int widthsame = abs(s_lastwidth - cropwidth) <= kDelta ? 1 : 0;
590  int heightsame = abs(s_lastheight - cropheight) <= kDelta ? 1 : 0;
591 
592  if (frameno > 0 && rowsame + colsame + widthsame + heightsame >= 3)
593  return true;
594 
595  LOG(VB_COMMFLAG, LOG_INFO,
596  QString("TemplateFinder Frame %1: %2x%3@(%4,%5)")
597  .arg(frameno, 5)
598  .arg(cropwidth).arg(cropheight)
599  .arg(cropcol).arg(croprow));
600 
601  s_lastrow = croprow;
602  s_lastcol = cropcol;
603  s_lastwidth = cropwidth;
604  s_lastheight = cropheight;
605 
606  if (debug_frames)
607  {
608  QString base = QString("%1/TemplateFinder-%2")
609  .arg(debugdir).arg(frameno, 5, 10, QChar('0'));
610 
611  /* PGM greyscale image of frame. */
612  if (!writeJPG(base, pgm, pgmheight))
613  return false;
614 
615  /* Cropped template area of frame. */
616  if (!writeJPG(base + "-cropped", cropped, cropheight))
617  return false;
618 
619  /* Edges of cropped template area of frame. */
620  if (!writeJPG(base + "-edges", edges, cropheight))
621  return false;
622  }
623 
624  return true;
625 }
626 
627 /* NOLINTNEXTLINE(readability-non-const-parameter) */
628 bool readTemplate(const QString& datafile, int *prow, int *pcol, int *pwidth, int *pheight,
629  const QString& tmplfile, AVFrame *tmpl, bool *pvalid)
630 {
631  QFile dfile(datafile);
632  QFileInfo dfileinfo(dfile);
633 
634  if (!dfile.open(QIODevice::ReadOnly))
635  return false;
636 
637  if (!dfileinfo.size())
638  {
639  /* Dummy file: no template. */
640  *pvalid = false;
641  return true;
642  }
643 
644  QTextStream stream(&dfile);
645  stream >> *prow >> *pcol >> *pwidth >> *pheight;
646  dfile.close();
647 
648  if (av_image_alloc(tmpl->data, tmpl->linesize,
649  *pwidth, *pheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
650  {
651  LOG(VB_COMMFLAG, LOG_ERR,
652  QString("readTemplate av_image_alloc %1 (%2x%3) failed")
653  .arg(tmplfile).arg(*pwidth).arg(*pheight));
654  return false;
655  }
656 
657  QByteArray tmfile = tmplfile.toLatin1();
658  if (pgm_read(tmpl->data[0], *pwidth, *pheight, tmfile.constData()))
659  {
660  av_freep(reinterpret_cast<void*>(&tmpl->data[0]));
661  return false;
662  }
663 
664  *pvalid = true;
665  return true;
666 }
667 
668 void
669 writeDummyTemplate(const QString& datafile)
670 {
671  /* Leave a 0-byte file. */
672  QFile dfile(datafile);
673 
674  if (!dfile.open(QIODevice::WriteOnly | QIODevice::Truncate) &&
675  dfile.exists())
676  (void)dfile.remove();
677 }
678 
679 bool
680 writeTemplate(const QString& tmplfile, const AVFrame *tmpl, const QString& datafile,
681  int row, int col, int width, int height)
682 {
683  QFile tfile(tmplfile);
684 
685  QByteArray tmfile = tmplfile.toLatin1();
686  if (pgm_write(tmpl->data[0], width, height, tmfile.constData()))
687  return false;
688 
689  QFile dfile(datafile);
690  if (!dfile.open(QIODevice::WriteOnly))
691  return false;
692 
693  QTextStream stream(&dfile);
694  stream << row << " " << col << "\n" << width << " " << height << "\n";
695  dfile.close();
696  return true;
697 }
698 
699 }; /* namespace */
700 
701 TemplateFinder::TemplateFinder(std::shared_ptr<PGMConverter> pgmc,
702  std::shared_ptr<BorderDetector> bd,
703  std::shared_ptr<EdgeDetector> ed,
704  MythPlayer *player, std::chrono::seconds proglen,
705  const QString& debugdir)
706  : m_pgmConverter(std::move(pgmc)),
707  m_borderDetector(std::move(bd)),
708  m_edgeDetector(std::move(ed)),
709  m_sampleTime(std::min(proglen / 2, 20 * 60s)),
710  m_debugDir(debugdir),
711  m_debugData(debugdir + "/TemplateFinder.txt"),
712  m_debugTmpl(debugdir + "/TemplateFinder.pgm")
713 {
714  /*
715  * TUNABLE:
716  *
717  * The number of frames desired for sampling to build the template.
718  *
719  * Higher values should yield a more accurate template, but requires more
720  * time.
721  */
722  unsigned int samplesNeeded = 300;
723 
724  /*
725  * TUNABLE:
726  *
727  * The leading amount of time (in seconds) to sample frames for building up
728  * the possible template, and the interval between frames for analysis.
729  * This affects how soon flagging can start after a recording has begun
730  * (a.k.a. "real-time flagging").
731  *
732  * Sample half of the program length or 20 minutes, whichever is less.
733  */
734  // m_sampleTime
735 
736  const float fps = player->GetFrameRate();
737 
738  m_frameInterval = (int)roundf(m_sampleTime.count() * fps / samplesNeeded);
739  m_endFrame = 0 + (long long)m_frameInterval * samplesNeeded - 1;
740 
741  LOG(VB_COMMFLAG, LOG_INFO,
742  QString("TemplateFinder: sampleTime=%1s, samplesNeeded=%2, endFrame=%3")
743  .arg(m_sampleTime.count()).arg(samplesNeeded).arg(m_endFrame));
744 
745  /*
746  * debugLevel:
747  * 0: no extra debugging
748  * 1: cache computations into debugdir [O(1) files]
749  * 2: extra verbosity [O(nframes)]
750  * 3: dump frames into debugdir [O(nframes) files]
751  */
752  m_debugLevel = gCoreContext->GetNumSetting("TemplateFinderDebugLevel", 0);
753 
754  if (m_debugLevel >= 1)
755  {
757  QString("TemplateFinder debugLevel %1").arg(m_debugLevel));
758 
759  m_debugTemplate = true;
760  m_debugEdgeCounts = true;
761 
762  if (m_debugLevel >= 3)
763  m_debugFrames = true;
764  }
765 }
766 
768 {
769  delete []m_scores;
770  av_freep(reinterpret_cast<void*>(&m_tmpl.data[0]));
771  av_freep(reinterpret_cast<void*>(&m_cropped.data[0]));
772 }
773 
776  [[maybe_unused]] long long nframes)
777 {
778  /*
779  * Only detect edges in portions of the frame where we expect to find
780  * a template. This serves two purposes:
781  *
782  * - Speed: reduce search space.
783  * - Correctness (insofar as the assumption of template location is
784  * correct): don't "pollute" the set of candidate template edges with
785  * the "content" edges in the non-template portions of the frame.
786  */
787  QString tmpldims;
788  QString playerdims;
789 
790  QSize buf_dim = player->GetVideoBufferSize();
791  m_width = buf_dim.width();
792  m_height = buf_dim.height();
793  playerdims = QString("%1x%2").arg(m_width).arg(m_height);
794 
795  if (m_debugTemplate)
796  {
799  &m_tmplValid);
800  if (m_tmplDone)
801  {
802  tmpldims = m_tmplValid ? QString("%1x%2@(%3,%4)")
803  .arg(m_tmplWidth).arg(m_tmplHeight).arg(m_tmplCol).arg(m_tmplRow) :
804  "no template";
805 
806  LOG(VB_COMMFLAG, LOG_INFO,
807  QString("TemplateFinder::MythPlayerInited read %1: %2")
808  .arg(m_debugTmpl, tmpldims));
809  }
810  }
811 
812  if (m_pgmConverter->MythPlayerInited(player))
813  goto free_tmpl;
814 
815  if (m_borderDetector->MythPlayerInited(player))
816  goto free_tmpl;
817 
818  if (m_tmplDone)
819  {
820  if (m_tmplValid)
821  {
822  LOG(VB_COMMFLAG, LOG_INFO,
823  QString("TemplateFinder::MythPlayerInited %1 of %2 (%3)")
824  .arg(tmpldims, playerdims, m_debugTmpl));
825  }
826  return ANALYZE_FINISHED;
827  }
828 
829  LOG(VB_COMMFLAG, LOG_INFO,
830  QString("TemplateFinder::MythPlayerInited framesize %1")
831  .arg(playerdims));
832  m_scores = new unsigned int[m_width * m_height];
833 
834  return ANALYZE_OK;
835 
836 free_tmpl:
837  av_freep(reinterpret_cast<void*>(&m_tmpl.data[0]));
838  return ANALYZE_FATAL;
839 }
840 
841 int
842 TemplateFinder::resetBuffers(int newwidth, int newheight)
843 {
844  if (m_cwidth == newwidth && m_cheight == newheight)
845  return 0;
846 
847  av_freep(reinterpret_cast<void*>(&m_cropped.data[0]));
848 
849  if (av_image_alloc(m_cropped.data, m_cropped.linesize,
850  newwidth, newheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
851  {
852  LOG(VB_COMMFLAG, LOG_ERR,
853  QString("TemplateFinder::resetBuffers "
854  "av_image_alloc cropped (%1x%2) failed")
855  .arg(newwidth).arg(newheight));
856  return -1;
857  }
858 
859  m_cwidth = newwidth;
860  m_cheight = newheight;
861  return 0;
862 }
863 
865 TemplateFinder::analyzeFrame(const MythVideoFrame *frame, long long frameno,
866  long long *pNextFrame)
867 {
868  /*
869  * TUNABLE:
870  *
871  * When looking for edges in frames, select some percentile of
872  * squared-gradient magnitudes (intensities) as candidate edges. (This
873  * number conventionally should not go any lower than the 95th percentile;
874  * see edge_mark.)
875  *
876  * Higher values result in fewer edges; faint logos might not be picked up.
877  * Lower values result in more edges; non-logo edges might be picked up.
878  *
879  * The TemplateFinder accumulates all its state in the "scores" array to
880  * be processed later by TemplateFinder::finished.
881  */
882  const int FRAMESGMPCTILE = 90;
883 
884  /*
885  * TUNABLE:
886  *
887  * Exclude some portion of the center of the frame from edge analysis.
888  * Elminate false edge-detection logo positives from talking-host types of
889  * shows where the high-contrast host and clothes (e.g., tie against white
890  * shirt against dark jacket) dominates the edges.
891  *
892  * This has a nice side-effect of reducing the area to be examined (speed
893  * optimization).
894  */
895  static constexpr float kExcludeWidth = 0.5;
896  static constexpr float kExcludeHeight = 0.5;
897 
898  int pgmwidth= 0;
899  int pgmheight = 0;
900  int croprow= 0;
901  int cropcol = 0;
902  int cropwidth = 0;
903  int cropheight = 0;
904 
905  if (frameno < m_nextFrame)
906  {
907  *pNextFrame = m_nextFrame;
908  return ANALYZE_OK;
909  }
910 
911  m_nextFrame = frameno + m_frameInterval;
912  *pNextFrame = std::min(m_endFrame, m_nextFrame);
913 
914  const AVFrame *pgm = m_pgmConverter->getImage(frame, frameno, &pgmwidth, &pgmheight);
915  if (pgm == nullptr)
916  goto error;
917 
918  if (!m_borderDetector->getDimensions(pgm, pgmheight, frameno,
919  &croprow, &cropcol, &cropwidth, &cropheight))
920  {
921  /* Not a blank frame. */
922 
923  auto start = nowAsDuration<std::chrono::microseconds>();
924 
925  m_minContentRow = std::min(croprow, m_minContentRow);
926  m_minContentCol = std::min(cropcol, m_minContentCol);
927  m_maxContentCol1 = std::max(cropcol + cropwidth, m_maxContentCol1);
928  m_maxContentRow1 = std::max(croprow + cropheight, m_maxContentRow1);
929 
930  if (resetBuffers(cropwidth, cropheight))
931  goto error;
932 
933  if (pgm_crop(&m_cropped, pgm, pgmheight, croprow, cropcol,
934  cropwidth, cropheight))
935  goto error;
936 
937  /*
938  * Translate the excluded area of the screen into "cropped"
939  * coordinates.
940  */
941  int excludewidth = (int)(pgmwidth * kExcludeWidth);
942  int excludeheight = (int)(pgmheight * kExcludeHeight);
943  int excluderow = ((pgmheight - excludeheight) / 2) - croprow;
944  int excludecol = ((pgmwidth - excludewidth) / 2) - cropcol;
945  (void)m_edgeDetector->setExcludeArea(excluderow, excludecol,
946  excludewidth, excludeheight);
947 
948  const AVFrame *edges =
949  m_edgeDetector->detectEdges(&m_cropped, cropheight, FRAMESGMPCTILE);
950  if (edges == nullptr)
951  goto error;
952 
953  if (pgm_scorepixels(m_scores, pgmwidth, croprow, cropcol,
954  edges, cropheight))
955  goto error;
956 
957  if (m_debugLevel >= 2)
958  {
959  if (!analyzeFrameDebug(frameno, pgm, pgmheight, &m_cropped, edges,
960  cropheight, croprow, cropcol, m_debugFrames, m_debugDir))
961  goto error;
962  }
963 
964  auto end = nowAsDuration<std::chrono::microseconds>();
965  m_analyzeTime += (end - start);
966  }
967 
968  if (m_nextFrame > m_endFrame)
969  return ANALYZE_FINISHED;
970 
971  return ANALYZE_OK;
972 
973 error:
974  LOG(VB_COMMFLAG, LOG_ERR,
975  QString("TemplateFinder::analyzeFrame error at frame %1")
976  .arg(frameno));
977 
978  if (m_nextFrame > m_endFrame)
979  return ANALYZE_FINISHED;
980 
981  return ANALYZE_ERROR;
982 }
983 
984 int
985 TemplateFinder::finished([[maybe_unused]] long long nframes, bool final)
986 {
987  if (!m_tmplDone)
988  {
994  {
995  if (final)
997  }
998  else
999  {
1000  if (final && m_debugTemplate)
1001  {
1004  if (!m_tmplValid)
1005  goto free_tmpl;
1006 
1007  LOG(VB_COMMFLAG, LOG_INFO,
1008  QString("TemplateFinder::finished wrote %1"
1009  " and %2 [%3x%4@(%5,%6)]")
1010  .arg(m_debugTmpl, m_debugData)
1011  .arg(m_tmplWidth).arg(m_tmplHeight)
1012  .arg(m_tmplCol).arg(m_tmplRow));
1013  }
1014  }
1015 
1016  if (final)
1017  m_tmplDone = true;
1018  }
1019 
1020  m_borderDetector->setLogoState(this);
1021 
1022  return 0;
1023 
1024 free_tmpl:
1025  av_freep(reinterpret_cast<void*>(&m_tmpl.data[0]));
1026  return -1;
1027 }
1028 
1029 int
1031 {
1032  if (m_pgmConverter->reportTime())
1033  return -1;
1034 
1035  if (m_borderDetector->reportTime())
1036  return -1;
1037 
1038  LOG(VB_COMMFLAG, LOG_INFO, QString("TF Time: analyze=%1s")
1039  .arg(strftimeval(m_analyzeTime)));
1040  return 0;
1041 }
1042 
1043 const struct AVFrame *
1044 TemplateFinder::getTemplate(int *prow, int *pcol, int *pwidth, int *pheight)
1045  const
1046 {
1047  if (m_tmplValid)
1048  {
1049  *prow = m_tmplRow;
1050  *pcol = m_tmplCol;
1051  *pwidth = m_tmplWidth;
1052  *pheight = m_tmplHeight;
1053  return &m_tmpl;
1054  }
1055  return nullptr;
1056 }
1057 
1058 /* vim: set expandtab tabstop=4 shiftwidth=4: */
TemplateFinder::m_tmplValid
bool m_tmplValid
Definition: TemplateFinder.h:102
TemplateFinder::m_debugTemplate
bool m_debugTemplate
Definition: TemplateFinder.h:99
TemplateFinder::m_debugData
QString m_debugData
Definition: TemplateFinder.h:97
FrameAnalyzer::ANALYZE_ERROR
@ ANALYZE_ERROR
Definition: FrameAnalyzer.h:38
anonymous_namespace{TemplateFinder.cpp}::bounding_box
int bounding_box(const AVFrame *img, int imgheight, int minrow, int mincol, int maxrow1, int maxcol1, int *prow, int *pcol, int *pwidth, int *pheight)
Definition: TemplateFinder.cpp:137
anonymous_namespace{TemplateFinder.cpp}::bounding_score
float bounding_score(const AVFrame *img, int row, int col, int width, int height)
Definition: TemplateFinder.cpp:97
TemplateFinder::m_analyzeTime
std::chrono::microseconds m_analyzeTime
Definition: TemplateFinder.h:104
CommDetector2.h
pgm_read
int pgm_read(unsigned char *buf, int width, int height, const char *filename)
Definition: pgm.cpp:35
anonymous_namespace{TemplateFinder.cpp}::template_alloc
bool template_alloc(const unsigned int *scores, int width, int height, int minrow, int mincol, int maxrow1, int maxcol1, AVFrame *tmpl, int *ptmplrow, int *ptmplcol, int *ptmplwidth, int *ptmplheight, bool debug_edgecounts, const QString &debugdir)
Definition: TemplateFinder.cpp:444
TemplateFinder::m_tmplWidth
int m_tmplWidth
Definition: TemplateFinder.h:87
TemplateFinder::m_minContentCol
int m_minContentCol
Definition: TemplateFinder.h:80
anonymous_namespace{TemplateFinder.cpp}::readTemplate
bool readTemplate(const QString &datafile, int *prow, int *pcol, int *pwidth, int *pheight, const QString &tmplfile, AVFrame *tmpl, bool *pvalid)
Definition: TemplateFinder.cpp:628
TemplateFinder::m_edgeDetector
std::shared_ptr< EdgeDetector > m_edgeDetector
Definition: TemplateFinder.h:68
FrameAnalyzer::ANALYZE_OK
@ ANALYZE_OK
Definition: FrameAnalyzer.h:37
TemplateFinder::m_maxContentCol1
int m_maxContentCol1
Definition: TemplateFinder.h:82
MythPlayer::GetFrameRate
float GetFrameRate(void) const
Definition: mythplayer.h:133
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
mythplayer.h
mythframe.h
TemplateFinder::m_scores
unsigned int * m_scores
Definition: TemplateFinder.h:77
MythPlayer
Definition: mythplayer.h:83
TemplateFinder::m_maxContentRow1
int m_maxContentRow1
Definition: TemplateFinder.h:81
TemplateFinder::m_tmplDone
bool m_tmplDone
Definition: TemplateFinder.h:103
myth_system
uint myth_system(const QString &command, uint flags, std::chrono::seconds timeout)
Definition: mythsystemlegacy.cpp:506
commDetector2::createDebugDirectory
void createDebugDirectory(const QString &dirname, const QString &comment)
Definition: CommDetector2.cpp:225
hardwareprofile.distros.mythtv_data.data_mythtv.prefix
string prefix
Definition: data_mythtv.py:37
TemplateFinder::m_borderDetector
std::shared_ptr< BorderDetector > m_borderDetector
Definition: TemplateFinder.h:67
TemplateFinder::MythPlayerInited
enum analyzeFrameResult MythPlayerInited(MythPlayer *player, long long nframes) override
Definition: TemplateFinder.cpp:775
TemplateFinder::m_debugDir
QString m_debugDir
Definition: TemplateFinder.h:96
mythsystemlegacy.h
anonymous_namespace{TemplateFinder.cpp}::colisempty
bool colisempty(const AVFrame *img, int col, int row, int height)
Definition: TemplateFinder.cpp:127
TemplateFinder::m_tmplRow
int m_tmplRow
Definition: TemplateFinder.h:85
AVFrame
struct AVFrame AVFrame
Definition: BorderDetector.h:15
pgm_write
int pgm_write(const unsigned char *buf, int width, int height, const char *filename)
Definition: pgm.cpp:83
mythdate.h
mythlogging.h
pgm.h
TemplateFinder::m_sampleTime
std::chrono::seconds m_sampleTime
Definition: TemplateFinder.h:70
PGMConverter.h
MythPlayer::GetVideoBufferSize
QSize GetVideoBufferSize(void) const
Definition: mythplayer.h:130
anonymous_namespace{TemplateFinder.cpp}::writeJPG
bool writeJPG(const QString &prefix, const AVFrame *img, int imgheight)
Definition: TemplateFinder.cpp:38
TemplateFinder::m_height
ssize_t m_height
Definition: TemplateFinder.h:76
GENERIC_EXIT_OK
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
TemplateFinder::m_debugLevel
int m_debugLevel
Definition: TemplateFinder.h:95
TemplateFinder::m_tmpl
AVFrame m_tmpl
Definition: TemplateFinder.h:84
TemplateFinder::reportTime
int reportTime(void) const override
Definition: TemplateFinder.cpp:1030
pgm_crop
int pgm_crop(AVFrame *dst, const AVFrame *src, [[maybe_unused]] int srcheight, int srcrow, int srccol, int cropwidth, int cropheight)
Definition: pgm.cpp:168
hardwareprofile.smolt.long
long
Definition: smolt.py:74
FrameAnalyzer::analyzeFrameResult
analyzeFrameResult
Definition: FrameAnalyzer.h:36
TemplateFinder::TemplateFinder
TemplateFinder(std::shared_ptr< PGMConverter > pgmc, std::shared_ptr< BorderDetector > bd, std::shared_ptr< EdgeDetector > ed, MythPlayer *player, std::chrono::seconds proglen, const QString &debugdir)
Definition: TemplateFinder.cpp:701
FrameAnalyzer::ANALYZE_FATAL
@ ANALYZE_FATAL
Definition: FrameAnalyzer.h:40
TemplateFinder::m_cheight
int m_cheight
Definition: TemplateFinder.h:92
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:57
commDetector2::strftimeval
QString strftimeval(std::chrono::microseconds usecs)
Definition: CommDetector2.cpp:263
EdgeDetector.h
TemplateFinder::finished
int finished(long long nframes, bool final) override
Definition: TemplateFinder.cpp:985
MythCoreContext::GetNumSetting
int GetNumSetting(const QString &key, int defaultval=0)
Definition: mythcorecontext.cpp:918
TemplateFinder::resetBuffers
int resetBuffers(int newwidth, int newheight)
Definition: TemplateFinder.cpp:842
hardwareprofile.smolt.error
def error(message)
Definition: smolt.py:409
anonymous_namespace{TemplateFinder.cpp}::analyzeFrameDebug
bool analyzeFrameDebug(long long frameno, const AVFrame *pgm, int pgmheight, const AVFrame *cropped, const AVFrame *edges, int cropheight, int croprow, int cropcol, bool debug_frames, const QString &debugdir)
Definition: TemplateFinder.cpp:576
TemplateFinder::m_debugTmpl
QString m_debugTmpl
Definition: TemplateFinder.h:98
BorderDetector.h
mythcorecontext.h
TemplateFinder::m_cropped
AVFrame m_cropped
Definition: TemplateFinder.h:90
anonymous_namespace{TemplateFinder.cpp}::writeTemplate
bool writeTemplate(const QString &tmplfile, const AVFrame *tmpl, const QString &datafile, int row, int col, int width, int height)
Definition: TemplateFinder.cpp:680
anonymous_namespace{TemplateFinder.cpp}::rowisempty
bool rowisempty(const AVFrame *img, int row, int col, int width)
Definition: TemplateFinder.cpp:117
commDetector2
Definition: CommDetector2.cpp:189
TemplateFinder::m_tmplHeight
int m_tmplHeight
Definition: TemplateFinder.h:88
TemplateFinder::m_debugEdgeCounts
bool m_debugEdgeCounts
Definition: TemplateFinder.h:100
TemplateFinder::m_debugFrames
bool m_debugFrames
Definition: TemplateFinder.h:101
anonymous_namespace{TemplateFinder.cpp}::pgm_scorepixels
int pgm_scorepixels(unsigned int *scores, int width, int row, int col, const AVFrame *src, int srcheight)
Definition: TemplateFinder.cpp:72
TemplateFinder::m_minContentRow
int m_minContentRow
Definition: TemplateFinder.h:79
anonymous_namespace{TemplateFinder.cpp}::sort_ascending
int sort_ascending(const void *aa, const void *bb)
Definition: TemplateFinder.cpp:91
TemplateFinder::analyzeFrame
enum analyzeFrameResult analyzeFrame(const MythVideoFrame *frame, long long frameno, long long *pNextFrame) override
Definition: TemplateFinder.cpp:865
TemplateFinder.h
TemplateFinder::m_frameInterval
int m_frameInterval
Definition: TemplateFinder.h:71
MythVideoFrame
Definition: mythframe.h:87
FrameAnalyzer::ANALYZE_FINISHED
@ ANALYZE_FINISHED
Definition: FrameAnalyzer.h:39
exitcodes.h
TemplateFinder::m_cwidth
int m_cwidth
Definition: TemplateFinder.h:91
anonymous_namespace{TemplateFinder.cpp}::writeDummyTemplate
void writeDummyTemplate(const QString &datafile)
Definition: TemplateFinder.cpp:669
TemplateFinder::m_nextFrame
long long m_nextFrame
Definition: TemplateFinder.h:73
TemplateFinder::getTemplate
const struct AVFrame * getTemplate(int *prow, int *pcol, int *pwidth, int *pheight) const
Definition: TemplateFinder.cpp:1044
TemplateFinder::~TemplateFinder
~TemplateFinder(void) override
Definition: TemplateFinder.cpp:767
TemplateFinder::m_width
ssize_t m_width
Definition: TemplateFinder.h:75
TemplateFinder::m_endFrame
long long m_endFrame
Definition: TemplateFinder.h:72
TemplateFinder::m_tmplCol
int m_tmplCol
Definition: TemplateFinder.h:86
uint
unsigned int uint
Definition: freesurround.h:24
TemplateFinder::m_pgmConverter
std::shared_ptr< PGMConverter > m_pgmConverter
Definition: TemplateFinder.h:66