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