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/mythconfig.h"
19#include "libmythtv/mythframe.h" /* VideoFrame */
21
22// Commercial Flagging headers
23#include "BorderDetector.h"
24#include "CommDetector2.h"
25#include "EdgeDetector.h"
26#include "PGMConverter.h"
27#include "TemplateFinder.h"
28#include "pgm.h"
29
30extern "C" {
31 #include "libavutil/imgutils.h"
32 }
33
34using namespace commDetector2;
35
36namespace {
37
38//returns true on success, false otherwise
39bool writeJPG(const QString& prefix, const AVFrame *img, int imgheight)
40{
41 const int imgwidth = img->linesize[0];
42 QFileInfo jpgfi(prefix + ".jpg");
43 if (!jpgfi.exists())
44 {
45 QFile pgmfile(prefix + ".pgm");
46 if (!pgmfile.exists())
47 {
48 QByteArray pfname = pgmfile.fileName().toLocal8Bit();
49 if (pgm_write(img->data[0], imgwidth, imgheight,
50 pfname.constData()))
51 {
52 return false;
53 }
54 }
55
56 QString cmd = QString("convert -quality 50 -resize 192x144 %1 %2")
57 .arg(pgmfile.fileName(), jpgfi.filePath());
58 if (myth_system(cmd) != GENERIC_EXIT_OK)
59 return false;
60
61 if (!pgmfile.remove())
62 {
63 LOG(VB_COMMFLAG, LOG_ERR,
64 QString("TemplateFinder.writeJPG error removing %1 (%2)")
65 .arg(pgmfile.fileName(), strerror(errno)));
66 return false;
67 }
68 }
69 return true;
70}
71
72int
73pgm_scorepixels(unsigned int *scores, int width, int row, int col,
74 const AVFrame *src, int srcheight)
75{
76 /* Every time a pixel is an edge, give it a point. */
77 const int srcwidth = src->linesize[0];
78
79 for (int rr = 0; rr < srcheight; rr++)
80 {
81 for (int cc = 0; cc < srcwidth; cc++)
82 {
83 if (src->data[0][(rr * srcwidth) + cc])
84 scores[((row + rr) * width) + col + cc]++;
85 }
86 }
87
88 return 0;
89}
90
91int
92sort_ascending(const void *aa, const void *bb)
93{
94 return *(unsigned int*)aa - *(unsigned int*)bb;
95}
96
97float
98bounding_score(const AVFrame *img, int row, int col, int width, int height)
99{
100 /* Return a value between [0..1] */
101 const int imgwidth = img->linesize[0];
102
103 uint score = 0;
104 int rr2 = row + height;
105 int cc2 = col + width;
106 for (int rr = row; rr < rr2; rr++)
107 {
108 for (int cc = col; cc < cc2; cc++)
109 {
110 if (img->data[0][(rr * imgwidth) + cc])
111 score++;
112 }
113 }
114 return (float)score / (width * height);
115}
116
117bool
118rowisempty(const AVFrame *img, int row, int col, int width)
119{
120 const int imgwidth = img->linesize[0];
121 for (int cc = col; cc < col + width; cc++)
122 if (img->data[0][(row * imgwidth) + cc])
123 return false;
124 return true;
125}
126
127bool
128colisempty(const AVFrame *img, int col, int row, int height)
129{
130 const int imgwidth = img->linesize[0];
131 for (int rr = row; rr < row + height; rr++)
132 if (img->data[0][(rr * imgwidth) + col])
133 return false;
134 return true;
135}
136
138int
139bounding_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 constexpr int kMaxWidthPct = 20;
151 static constexpr int kMaxHeightPct = 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 = std::max(4, imgheight * 1 / 15);
161 const int HORIZSLOP = std::max(4, imgwidth * 1 / 20);
162
163 int maxwidth = (maxcol1 - mincol) * kMaxWidthPct / 100;
164 int maxheight = (maxrow1 - minrow) * kMaxHeightPct / 100;
165
166 int row = minrow;
167 int col = mincol;
168 int width = maxcol1 - mincol;
169 int height = maxrow1 - minrow;
170 int newrow = 0;
171 int newcol = 0;
172 int newright = 0;
173 int newbottom = 0;
174
175 for (;;)
176 {
177 bool improved = false;
178
179 LOG(VB_COMMFLAG, LOG_INFO, QString("bounding_box %1x%2@(%3,%4)")
180 .arg(width).arg(height).arg(col).arg(row));
181
182 /* Chop top. */
183 float score = bounding_score(img, row, col, width, height);
184 newrow = row;
185 for (int ii = 1; ii < height; ii++)
186 {
187 float newscore =
188 bounding_score(img, row + ii, col, width, height - ii);
189 if (newscore < score)
190 break;
191 score = newscore;
192 newrow = row + ii;
193 improved = true;
194 }
195
196 /* Chop left. */
197 score = bounding_score(img, row, col, width, height);
198 newcol = col;
199 for (int ii = 1; ii < width; ii++)
200 {
201 float newscore =
202 bounding_score(img, row, col + ii, width - ii, height);
203 if (newscore < 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 (int ii = 1; ii < height; ii++)
214 {
215 float newscore =
216 bounding_score(img, row, col, width, height - ii);
217 if (newscore < score)
218 break;
219 score = newscore;
220 newbottom = row + height - ii;
221 improved = true;
222 }
223
224 /* Chop right. */
225 score = bounding_score(img, row, col, width, height);
226 newright = col + width;
227 for (int ii = 1; ii < width; ii++)
228 {
229 float newscore =
230 bounding_score(img, row, col, width - ii, height);
231 if (newscore < score)
232 break;
233 score = newscore;
234 newright = col + width - ii;
235 improved = true;
236 }
237
238 if (!improved)
239 break;
240
241 row = newrow;
242 col = newcol;
243 width = newright - newcol;
244 height = newbottom - newrow;
245
246 /*
247 * Noise edge pixels in the frequency template can sometimes stretch
248 * the template area to be larger than it should be.
249 *
250 * However, noise needs to be distinguished from a uniform distribution
251 * of noise pixels (e.g., no real statically-located template). So if
252 * the template area is too "large", then some quadrant must have a
253 * clear majority of the edge pixels; otherwise we declare failure (no
254 * template found).
255 *
256 * Intuitively, we should simply repeat until a single bounding box is
257 * converged upon. However, this requires a more sophisticated
258 * bounding_score function that I don't feel like figuring out.
259 * Indefinitely repeating with the present bounding_score function will
260 * tend to chop off too much. Instead, simply do some sanity checks on
261 * the candidate template's size, and prune the template area and
262 * repeat if it is too "large".
263 */
264
265 if (width > maxwidth)
266 {
267 /* Too wide; test left and right portions. */
268 int chop = width / 3;
269 int chopwidth = width - chop;
270
271 float left = bounding_score(img, row, col, chopwidth, height);
272 float right = bounding_score(img, row, col + chop, chopwidth, height);
273 LOG(VB_COMMFLAG, LOG_INFO,
274 QString("bounding_box too wide (%1 > %2); left=%3, right=%4")
275 .arg(width).arg(maxwidth)
276 .arg(left, 0, 'f', 3).arg(right, 0, 'f', 3));
277 float minscore = std::min(left, right);
278 float maxscore = std::max(left, right);
279 if (maxscore < 3 * minscore / 2)
280 {
281 /*
282 * Edge pixel distribution too uniform; give up.
283 *
284 * XXX: also fails for horizontally-centered templates ...
285 */
286 LOG(VB_COMMFLAG, LOG_ERR, "bounding_box giving up (edge "
287 "pixels distributed too uniformly)");
288 return -1;
289 }
290
291 if (left < right)
292 col += chop;
293 width -= chop;
294 continue;
295 }
296
297 if (height > maxheight)
298 {
299 /* Too tall; test upper and lower portions. */
300 int chop = height / 3;
301 int chopheight = height - chop;
302
303 float upper = bounding_score(img, row, col, width, chopheight);
304 float 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 float minscore = std::min(upper, lower);
310 float maxscore = std::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 = std::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 = std::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 = std::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 = std::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
445bool
446template_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, const 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 constexpr float kMinScorePctile = 0.998;
461
462 const int nn = width * height;
463 int ii = 0;
464 int first = 0;
465 int last = 0;
466 unsigned int threshscore = 0;
467 AVFrame thresh;
468
469 if (av_image_alloc(thresh.data, thresh.linesize,
470 width, height, AV_PIX_FMT_GRAY8, IMAGE_ALIGN) < 0)
471 {
472 LOG(VB_COMMFLAG, LOG_ERR,
473 QString("template_alloc av_image_alloc thresh (%1x%2) failed")
474 .arg(width).arg(height));
475 return false;
476 }
477
478 std::vector<uint> sortedscores;
479 sortedscores.resize(nn);
480 memcpy(sortedscores.data(), scores, nn * sizeof(uint));
481 qsort(sortedscores.data(), nn, sizeof(uint), sort_ascending);
482
483 // Automatically clean up allocation at function exit
484 auto cleanup_fn = [&](int* /*x*/) {
485 av_freep(reinterpret_cast<void*>(&thresh.data[0]));
486 };
487 std::unique_ptr<int, decltype(cleanup_fn)> cleanup { &first, cleanup_fn };
488
489 if (sortedscores[0] == sortedscores[nn - 1])
490 {
491 /* All pixels in the template area look the same; no template. */
492 LOG(VB_COMMFLAG, LOG_ERR,
493 QString("template_alloc: %1x%2 pixels all identical!")
494 .arg(width).arg(height));
495 return false;
496 }
497
498 /* Threshold the edge frequences. */
499
500 ii = (int)roundf(nn * kMinScorePctile);
501 threshscore = sortedscores[ii];
502 for (first = ii; first > 0 && sortedscores[first] == threshscore; first--)
503 ;
504 if (sortedscores[first] != threshscore)
505 first++;
506 for (last = ii; last < nn - 1 && sortedscores[last] == threshscore; last++)
507 ;
508 if (sortedscores[last] != threshscore)
509 last--;
510
511 LOG(VB_COMMFLAG, LOG_INFO, QString("template_alloc wanted %1, got %2-%3")
512 .arg(kMinScorePctile, 0, 'f', 6)
513 .arg((float)first / nn, 0, 'f', 6)
514 .arg((float)last / nn, 0, 'f', 6));
515
516 for (ii = 0; ii < nn; ii++)
517 thresh.data[0][ii] = scores[ii] >= threshscore ? UCHAR_MAX : 0;
518
519 if (debug_edgecounts)
520 {
521 /* Scores, rescaled to [0..UCHAR_MAX]. */
522 AVFrame scored;
523 if (av_image_alloc(scored.data, scored.linesize,
524 width, height, AV_PIX_FMT_GRAY8, IMAGE_ALIGN) < 0)
525 {
526 LOG(VB_COMMFLAG, LOG_ERR,
527 QString("template_alloc av_image_alloc scored (%1x%2) failed")
528 .arg(width).arg(height));
529 return false;
530 }
531 unsigned int maxscore = sortedscores[nn - 1];
532 for (ii = 0; ii < nn; ii++)
533 scored.data[0][ii] = scores[ii] * UCHAR_MAX / maxscore;
534 bool success = writeJPG(debugdir + "/TemplateFinder-scores", &scored,
535 height);
536 av_freep(reinterpret_cast<void*>(&scored.data[0]));
537 if (!success)
538 return false;
539
540 /* Thresholded scores. */
541 if (!writeJPG(debugdir + "/TemplateFinder-edgecounts", &thresh, height))
542 return false;
543 }
544
545 /* Crop to a minimal bounding box. */
546
547 if (bounding_box(&thresh, height, minrow, mincol, maxrow1, maxcol1,
548 ptmplrow, ptmplcol, ptmplwidth, ptmplheight))
549 return false;
550
551 if ((uint)(*ptmplwidth * *ptmplheight) > USHRT_MAX)
552 {
553 /* Max value of data type of TemplateMatcher::edgematch */
554 LOG(VB_COMMFLAG, LOG_ERR,
555 QString("template_alloc bounding_box too big (%1x%2)")
556 .arg(*ptmplwidth).arg(*ptmplheight));
557 return false;
558 }
559
560 if (av_image_alloc(tmpl->data, tmpl->linesize,
561 *ptmplwidth, *ptmplheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN) < 0)
562 {
563 LOG(VB_COMMFLAG, LOG_ERR,
564 QString("template_alloc av_image_alloc tmpl (%1x%2) failed")
565 .arg(*ptmplwidth).arg(*ptmplheight));
566 return false;
567 }
568
569 if (pgm_crop(tmpl, &thresh, height, *ptmplrow, *ptmplcol,
570 *ptmplwidth, *ptmplheight))
571 return false;
572
573 return true;
574}
575
576bool
577analyzeFrameDebug(long long frameno, const AVFrame *pgm, int pgmheight,
578 const AVFrame *cropped, const AVFrame *edges, int cropheight,
579 int croprow, int cropcol, bool debug_frames, const QString& debugdir)
580{
581 static constexpr int kDelta = 24;
582 static int s_lastrow;
583 static int s_lastcol;
584 static int s_lastwidth;
585 static int s_lastheight;
586 const int cropwidth = cropped->linesize[0];
587
588 int rowsame = abs(s_lastrow - croprow) <= kDelta ? 1 : 0;
589 int colsame = abs(s_lastcol - cropcol) <= kDelta ? 1 : 0;
590 int widthsame = abs(s_lastwidth - cropwidth) <= kDelta ? 1 : 0;
591 int heightsame = abs(s_lastheight - cropheight) <= kDelta ? 1 : 0;
592
593 if (frameno > 0 && rowsame + colsame + widthsame + heightsame >= 3)
594 return true;
595
596 LOG(VB_COMMFLAG, LOG_INFO,
597 QString("TemplateFinder Frame %1: %2x%3@(%4,%5)")
598 .arg(frameno, 5)
599 .arg(cropwidth).arg(cropheight)
600 .arg(cropcol).arg(croprow));
601
602 s_lastrow = croprow;
603 s_lastcol = cropcol;
604 s_lastwidth = cropwidth;
605 s_lastheight = cropheight;
606
607 if (debug_frames)
608 {
609 QString base = QString("%1/TemplateFinder-%2")
610 .arg(debugdir).arg(frameno, 5, 10, QChar('0'));
611
612 /* PGM greyscale image of frame. */
613 if (!writeJPG(base, pgm, pgmheight))
614 return false;
615
616 /* Cropped template area of frame. */
617 if (!writeJPG(base + "-cropped", cropped, cropheight))
618 return false;
619
620 /* Edges of cropped template area of frame. */
621 if (!writeJPG(base + "-edges", edges, cropheight))
622 return false;
623 }
624
625 return true;
626}
627
628/* NOLINTNEXTLINE(readability-non-const-parameter) */
629bool readTemplate(const QString& datafile, int *prow, int *pcol, int *pwidth, int *pheight,
630 const 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 // Read template size information from file.
646 QTextStream stream(&dfile);
647 stream >> *prow >> *pcol >> *pwidth >> *pheight;
648 dfile.close();
649
650 if (*pwidth < 0 || *pheight < 0)
651 {
652 LOG(VB_COMMFLAG, LOG_ERR, QString("readTemplate no saved template"));
653 return false;
654 }
655
656 if (av_image_alloc(tmpl->data, tmpl->linesize,
657 *pwidth, *pheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN) < 0)
658 {
659 LOG(VB_COMMFLAG, LOG_ERR,
660 QString("readTemplate av_image_alloc %1 (%2x%3) failed")
661 .arg(tmplfile).arg(*pwidth).arg(*pheight));
662 return false;
663 }
664
665 QByteArray tmfile = tmplfile.toLatin1();
666 if (pgm_read(tmpl->data[0], *pwidth, *pheight, tmfile.constData()))
667 {
668 av_freep(reinterpret_cast<void*>(&tmpl->data[0]));
669 return false;
670 }
671
672 *pvalid = true;
673 return true;
674}
675
676void
677writeDummyTemplate(const QString& datafile)
678{
679 /* Leave a 0-byte file. */
680 QFile dfile(datafile);
681
682 if (!dfile.open(QIODevice::WriteOnly | QIODevice::Truncate) &&
683 dfile.exists())
684 (void)dfile.remove();
685}
686
687bool
688writeTemplate(const QString& tmplfile, const AVFrame *tmpl, const QString& datafile,
689 int row, int col, int width, int height)
690{
691 QFile tfile(tmplfile);
692
693 QByteArray tmfile = tmplfile.toLatin1();
694 if (pgm_write(tmpl->data[0], width, height, tmfile.constData()))
695 return false;
696
697 QFile dfile(datafile);
698 if (!dfile.open(QIODevice::WriteOnly))
699 return false;
700
701 QTextStream stream(&dfile);
702 stream << row << " " << col << "\n" << width << " " << height << "\n";
703 dfile.close();
704 return true;
705}
706
707}; /* namespace */
708
709TemplateFinder::TemplateFinder(std::shared_ptr<PGMConverter> pgmc,
710 std::shared_ptr<BorderDetector> bd,
711 std::shared_ptr<EdgeDetector> ed,
712 MythPlayer *player, std::chrono::seconds proglen,
713 const QString& debugdir)
714 : m_pgmConverter(std::move(pgmc)),
715 m_borderDetector(std::move(bd)),
716 m_edgeDetector(std::move(ed)),
717 m_sampleTime(std::min(proglen / 2, 20 * 60s)),
718 m_debugDir(debugdir),
719 m_debugData(debugdir + "/TemplateFinder.txt"),
720 m_debugTmpl(debugdir + "/TemplateFinder.pgm")
721{
722 /*
723 * TUNABLE:
724 *
725 * The number of frames desired for sampling to build the template.
726 *
727 * Higher values should yield a more accurate template, but requires more
728 * time.
729 */
730 unsigned int samplesNeeded = 300;
731
732 /*
733 * TUNABLE:
734 *
735 * The leading amount of time (in seconds) to sample frames for building up
736 * the possible template, and the interval between frames for analysis.
737 * This affects how soon flagging can start after a recording has begun
738 * (a.k.a. "real-time flagging").
739 *
740 * Sample half of the program length or 20 minutes, whichever is less.
741 */
742 // m_sampleTime
743
744 const float fps = player->GetFrameRate();
745
746 m_frameInterval = (int)roundf(m_sampleTime.count() * fps / samplesNeeded);
747 m_endFrame = 0 + ((long long)m_frameInterval * samplesNeeded) - 1;
748
749 LOG(VB_COMMFLAG, LOG_INFO,
750 QString("TemplateFinder: sampleTime=%1s, samplesNeeded=%2, endFrame=%3")
751 .arg(m_sampleTime.count()).arg(samplesNeeded).arg(m_endFrame));
752
753 /*
754 * debugLevel:
755 * 0: no extra debugging
756 * 1: cache computations into debugdir [O(1) files]
757 * 2: extra verbosity [O(nframes)]
758 * 3: dump frames into debugdir [O(nframes) files]
759 */
760 m_debugLevel = gCoreContext->GetNumSetting("TemplateFinderDebugLevel", 0);
761
762 if (m_debugLevel >= 1)
763 {
765 QString("TemplateFinder debugLevel %1").arg(m_debugLevel));
766
767 m_debugTemplate = true;
768 m_debugEdgeCounts = true;
769
770 if (m_debugLevel >= 3)
771 m_debugFrames = true;
772 }
773}
774
776{
777 delete []m_scores;
778 av_freep(reinterpret_cast<void*>(&m_tmpl.data[0]));
779 av_freep(reinterpret_cast<void*>(&m_cropped.data[0]));
780}
781
784 [[maybe_unused]] long long nframes)
785{
786 /*
787 * Only detect edges in portions of the frame where we expect to find
788 * a template. This serves two purposes:
789 *
790 * - Speed: reduce search space.
791 * - Correctness (insofar as the assumption of template location is
792 * correct): don't "pollute" the set of candidate template edges with
793 * the "content" edges in the non-template portions of the frame.
794 */
795 QString tmpldims;
796 QString playerdims;
797
798 QSize buf_dim = player->GetVideoBufferSize();
799 m_width = buf_dim.width();
800 m_height = buf_dim.height();
801 playerdims = QString("%1x%2").arg(m_width).arg(m_height);
802
803 if (m_debugTemplate)
804 {
807 &m_tmplValid);
808 if (m_tmplDone)
809 {
810 tmpldims = m_tmplValid ? QString("%1x%2@(%3,%4)")
811 .arg(m_tmplWidth).arg(m_tmplHeight).arg(m_tmplCol).arg(m_tmplRow) :
812 "no template";
813
814 LOG(VB_COMMFLAG, LOG_INFO,
815 QString("TemplateFinder::MythPlayerInited read %1: %2")
816 .arg(m_debugTmpl, tmpldims));
817 }
818 }
819
820 if (m_pgmConverter->MythPlayerInited(player) ||
821 m_borderDetector->MythPlayerInited(player)) {
822 av_freep(reinterpret_cast<void*>(&m_tmpl.data[0]));
823 return ANALYZE_FATAL;
824 }
825
826 if (m_tmplDone)
827 {
828 if (m_tmplValid)
829 {
830 LOG(VB_COMMFLAG, LOG_INFO,
831 QString("TemplateFinder::MythPlayerInited %1 of %2 (%3)")
832 .arg(tmpldims, playerdims, m_debugTmpl));
833 }
834 return ANALYZE_FINISHED;
835 }
836
837 LOG(VB_COMMFLAG, LOG_INFO,
838 QString("TemplateFinder::MythPlayerInited framesize %1")
839 .arg(playerdims));
840 m_scores = new unsigned int[m_width * m_height];
841
842 return ANALYZE_OK;
843}
844
845int
846TemplateFinder::resetBuffers(int newwidth, int newheight)
847{
848 if (m_cwidth == newwidth && m_cheight == newheight)
849 return 0;
850
851 av_freep(reinterpret_cast<void*>(&m_cropped.data[0]));
852
853 if (av_image_alloc(m_cropped.data, m_cropped.linesize,
854 newwidth, newheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN) < 0)
855 {
856 LOG(VB_COMMFLAG, LOG_ERR,
857 QString("TemplateFinder::resetBuffers "
858 "av_image_alloc cropped (%1x%2) failed")
859 .arg(newwidth).arg(newheight));
860 return -1;
861 }
862
863 m_cwidth = newwidth;
864 m_cheight = newheight;
865 return 0;
866}
867
869TemplateFinder::analyzeFrame(const MythVideoFrame *frame, long long frameno,
870 long long *pNextFrame)
871{
872 /*
873 * TUNABLE:
874 *
875 * When looking for edges in frames, select some percentile of
876 * squared-gradient magnitudes (intensities) as candidate edges. (This
877 * number conventionally should not go any lower than the 95th percentile;
878 * see edge_mark.)
879 *
880 * Higher values result in fewer edges; faint logos might not be picked up.
881 * Lower values result in more edges; non-logo edges might be picked up.
882 *
883 * The TemplateFinder accumulates all its state in the "scores" array to
884 * be processed later by TemplateFinder::finished.
885 */
886 const int FRAMESGMPCTILE = 90;
887
888 /*
889 * TUNABLE:
890 *
891 * Exclude some portion of the center of the frame from edge analysis.
892 * Elminate false edge-detection logo positives from talking-host types of
893 * shows where the high-contrast host and clothes (e.g., tie against white
894 * shirt against dark jacket) dominates the edges.
895 *
896 * This has a nice side-effect of reducing the area to be examined (speed
897 * optimization).
898 */
899 static constexpr float kExcludeWidth = 0.5;
900 static constexpr float kExcludeHeight = 0.5;
901
902 int pgmwidth= 0;
903 int pgmheight = 0;
904 int croprow= 0;
905 int cropcol = 0;
906 int cropwidth = 0;
907 int cropheight = 0;
908
909 if (frameno < m_nextFrame)
910 {
911 *pNextFrame = m_nextFrame;
912 return ANALYZE_OK;
913 }
914
915 m_nextFrame = frameno + m_frameInterval;
916 *pNextFrame = std::min(m_endFrame, m_nextFrame);
917
918 try
919 {
920 const AVFrame *pgm = m_pgmConverter->getImage(frame, frameno, &pgmwidth, &pgmheight);
921 if (pgm == nullptr)
922 throw 1;
923
924 if (!m_borderDetector->getDimensions(pgm, pgmheight, frameno,
925 &croprow, &cropcol, &cropwidth, &cropheight))
926 {
927 /* Not a blank frame. */
928
929 auto start = nowAsDuration<std::chrono::microseconds>();
930
931 m_minContentRow = std::min(croprow, m_minContentRow);
932 m_minContentCol = std::min(cropcol, m_minContentCol);
933 m_maxContentCol1 = std::max(cropcol + cropwidth, m_maxContentCol1);
934 m_maxContentRow1 = std::max(croprow + cropheight, m_maxContentRow1);
935
936 if (resetBuffers(cropwidth, cropheight))
937 throw 2;
938
939 if (pgm_crop(&m_cropped, pgm, pgmheight, croprow, cropcol,
940 cropwidth, cropheight))
941 throw 3;
942
943 /*
944 * Translate the excluded area of the screen into "cropped"
945 * coordinates.
946 */
947 int excludewidth = (int)(pgmwidth * kExcludeWidth);
948 int excludeheight = (int)(pgmheight * kExcludeHeight);
949 int excluderow = ((pgmheight - excludeheight) / 2) - croprow;
950 int excludecol = ((pgmwidth - excludewidth) / 2) - cropcol;
951 (void)m_edgeDetector->setExcludeArea(excluderow, excludecol,
952 excludewidth, excludeheight);
953
954 const AVFrame *edges =
955 m_edgeDetector->detectEdges(&m_cropped, cropheight, FRAMESGMPCTILE);
956 if (edges == nullptr)
957 throw 4;
958
959 if (pgm_scorepixels(m_scores, pgmwidth, croprow, cropcol,
960 edges, cropheight))
961 throw 5;
962
963 if (m_debugLevel >= 2)
964 {
965 if (!analyzeFrameDebug(frameno, pgm, pgmheight, &m_cropped, edges,
966 cropheight, croprow, cropcol, m_debugFrames, m_debugDir))
967 throw 6;
968 }
969
970 auto end = nowAsDuration<std::chrono::microseconds>();
971 m_analyzeTime += (end - start);
972 }
973
975 return ANALYZE_FINISHED;
976
977 return ANALYZE_OK;
978 }
979 catch (int e)
980 {
981 LOG(VB_COMMFLAG, LOG_ERR,
982 QString("TemplateFinder::analyzeFrame error at frame %1, step %2")
983 .arg(frameno).arg(e));
984
986 return ANALYZE_FINISHED;
987
988 return ANALYZE_ERROR;
989 }
990}
991
992int
993TemplateFinder::finished([[maybe_unused]] long long nframes, bool final)
994{
995 if (!m_tmplDone)
996 {
1002 {
1003 if (final)
1005 }
1006 else
1007 {
1008 if (final && m_debugTemplate)
1009 {
1012 if (!m_tmplValid)
1013 {
1014 av_freep(reinterpret_cast<void*>(&m_tmpl.data[0]));
1015 return -1;
1016 }
1017
1018 LOG(VB_COMMFLAG, LOG_INFO,
1019 QString("TemplateFinder::finished wrote %1"
1020 " and %2 [%3x%4@(%5,%6)]")
1022 .arg(m_tmplWidth).arg(m_tmplHeight)
1023 .arg(m_tmplCol).arg(m_tmplRow));
1024 }
1025 }
1026
1027 if (final)
1028 m_tmplDone = true;
1029 }
1030
1031 m_borderDetector->setLogoState(this);
1032
1033 return 0;
1034}
1035
1036int
1038{
1039 if (m_pgmConverter->reportTime())
1040 return -1;
1041
1042 if (m_borderDetector->reportTime())
1043 return -1;
1044
1045 LOG(VB_COMMFLAG, LOG_INFO, QString("TF Time: analyze=%1s")
1046 .arg(strftimeval(m_analyzeTime)));
1047 return 0;
1048}
1049
1050const struct AVFrame *
1051TemplateFinder::getTemplate(int *prow, int *pcol, int *pwidth, int *pheight)
1052 const
1053{
1054 if (m_tmplValid)
1055 {
1056 *prow = m_tmplRow;
1057 *pcol = m_tmplCol;
1058 *pwidth = m_tmplWidth;
1059 *pheight = m_tmplHeight;
1060 return &m_tmpl;
1061 }
1062 return nullptr;
1063}
1064
1065/* vim: set expandtab tabstop=4 shiftwidth=4: */
AVFrame AVFrame
int GetNumSetting(const QString &key, int defaultval=0)
QSize GetVideoBufferSize(void) const
Definition: mythplayer.h:129
float GetFrameRate(void) const
Definition: mythplayer.h:132
std::shared_ptr< BorderDetector > m_borderDetector
unsigned int * m_scores
QString m_debugTmpl
std::chrono::microseconds m_analyzeTime
enum analyzeFrameResult analyzeFrame(const MythVideoFrame *frame, long long frameno, long long *pNextFrame) override
QString m_debugData
enum analyzeFrameResult MythPlayerInited(MythPlayer *player, long long nframes) override
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)
int reportTime(void) const override
~TemplateFinder(void) override
std::shared_ptr< PGMConverter > m_pgmConverter
const struct AVFrame * getTemplate(int *prow, int *pcol, int *pwidth, int *pheight) const
std::shared_ptr< EdgeDetector > m_edgeDetector
std::chrono::seconds m_sampleTime
int resetBuffers(int newwidth, int newheight)
long long m_endFrame
QString m_debugDir
long long m_nextFrame
int finished(long long nframes, bool final) override
unsigned int uint
Definition: compat.h:60
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
uint myth_system(const QString &command, uint flags, std::chrono::seconds timeout)
bool writeJPG(const QString &prefix, const AVFrame *img, int imgheight)
int pgm_scorepixels(unsigned int *scores, int width, int row, int col, const AVFrame *src, int srcheight)
int bounding_box(const AVFrame *img, int imgheight, int minrow, int mincol, int maxrow1, int maxcol1, int *prow, int *pcol, int *pwidth, int *pheight)
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)
bool colisempty(const AVFrame *img, int col, int row, int height)
void writeDummyTemplate(const QString &datafile)
float bounding_score(const AVFrame *img, int row, int col, int width, int height)
bool writeTemplate(const QString &tmplfile, const AVFrame *tmpl, const QString &datafile, int row, int col, int width, int height)
bool rowisempty(const AVFrame *img, int row, int col, int width)
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)
int sort_ascending(const void *aa, const void *bb)
bool readTemplate(const QString &datafile, int *prow, int *pcol, int *pwidth, int *pheight, const QString &tmplfile, AVFrame *tmpl, bool *pvalid)
void createDebugDirectory(const QString &dirname, const QString &comment)
QString strftimeval(std::chrono::microseconds usecs)
int pgm_write(const unsigned char *buf, int width, int height, const char *filename)
Definition: pgm.cpp:79
int pgm_crop(AVFrame *dst, const AVFrame *src, int srcheight, int srcrow, int srccol, int cropwidth, int cropheight)
Definition: pgm.cpp:161
int pgm_read(unsigned char *buf, int width, int height, const char *filename)
Definition: pgm.cpp:34
static QString cleanup(const QString &str)