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