28 #include "libavutil/imgutils.h"
36 bool writeJPG(
const QString&
prefix,
const AVFrame *img,
int imgheight)
38 const int imgwidth = img->linesize[0];
39 QFileInfo jpgfi(
prefix +
".jpg");
42 QFile pgmfile(
prefix +
".pgm");
43 if (!pgmfile.exists())
45 QByteArray pfname = pgmfile.fileName().toLocal8Bit();
46 if (
pgm_write(img->data[0], imgwidth, imgheight,
53 QString cmd = QString(
"convert -quality 50 -resize 192x144 %1 %2")
54 .arg(pgmfile.fileName(), jpgfi.filePath());
58 if (!pgmfile.remove())
60 LOG(VB_COMMFLAG, LOG_ERR,
61 QString(
"TemplateFinder.writeJPG error removing %1 (%2)")
62 .arg(pgmfile.fileName(), strerror(errno)));
70 pgm_scorepixels(
unsigned int *scores,
int width,
int row,
int col,
71 const AVFrame *src,
int srcheight)
74 const int srcwidth = src->linesize[0];
76 for (
int rr = 0; rr < srcheight; rr++)
78 for (
int cc = 0;
cc < srcwidth;
cc++)
80 if (src->data[0][rr * srcwidth +
cc])
81 scores[(row + rr) * width + col +
cc]++;
91 return *(
unsigned int*)aa - *(
unsigned int*)bb;
95 bounding_score(
const AVFrame *img,
int row,
int col,
int width,
int height)
98 const int imgwidth = img->linesize[0];
101 int rr2 = row + height;
102 int cc2 = col + width;
103 for (
int rr = row; rr < rr2; rr++)
105 for (
int cc = col;
cc < cc2;
cc++)
107 if (img->data[0][rr * imgwidth +
cc])
111 return (
float)score / (width * height);
115 rowisempty(
const AVFrame *img,
int row,
int col,
int width)
117 const int imgwidth = img->linesize[0];
118 for (
int cc = col;
cc < col + width;
cc++)
119 if (img->data[0][row * imgwidth +
cc])
125 colisempty(
const AVFrame *img,
int col,
int row,
int height)
127 const int imgwidth = img->linesize[0];
128 for (
int rr = row; rr < row + height; rr++)
129 if (img->data[0][rr * imgwidth + col])
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)
139 const int imgwidth = img->linesize[0];
146 static constexpr
int kMaxWidthPct = 20;
147 static constexpr
int kMaxHeightPct = 20;
156 const int VERTSLOP = std::max(4, imgheight * 1 / 15);
157 const int HORIZSLOP = std::max(4, imgwidth * 1 / 20);
159 int maxwidth = (maxcol1 - mincol) * kMaxWidthPct / 100;
160 int maxheight = (maxrow1 - minrow) * kMaxHeightPct / 100;
164 int width = maxcol1 - mincol;
165 int height = maxrow1 - minrow;
173 float newscore = NAN;
174 bool improved =
false;
176 LOG(VB_COMMFLAG, LOG_INFO, QString(
"bounding_box %1x%2@(%3,%4)")
177 .arg(width).arg(height).arg(col).arg(row));
180 float score = bounding_score(img, row, col, width, height);
182 for (
int ii = 1; ii < height; ii++)
184 if ((newscore = bounding_score(img, row + ii, col,
185 width, height - ii)) < score)
193 score = bounding_score(img, row, col, width, height);
195 for (
int ii = 1; ii < width; ii++)
197 if ((newscore = bounding_score(img, row, col + ii,
198 width - ii, height)) < score)
206 score = bounding_score(img, row, col, width, height);
207 newbottom = row + height;
208 for (
int ii = 1; ii < height; ii++)
210 if ((newscore = bounding_score(img, row, col,
211 width, height - ii)) < score)
214 newbottom = row + height - ii;
219 score = bounding_score(img, row, col, width, height);
220 newright = col + width;
221 for (
int ii = 1; ii < width; ii++)
223 if ((newscore = bounding_score(img, row, col,
224 width - ii, height)) < score)
227 newright = col + width - ii;
236 width = newright - newcol;
237 height = newbottom - newrow;
258 if (width > maxwidth)
261 int chop = width / 3;
262 int chopwidth = width - chop;
264 float left = bounding_score(img, row, col, chopwidth, height);
265 float right = bounding_score(img, row, col + chop, chopwidth, height);
266 LOG(VB_COMMFLAG, LOG_INFO,
267 QString(
"bounding_box too wide (%1 > %2); left=%3, right=%4")
268 .arg(width).arg(maxwidth)
269 .arg(left, 0,
'f', 3).arg(right, 0,
'f', 3));
270 float minscore = std::min(left, right);
271 float maxscore = std::max(left, right);
272 if (maxscore < 3 * minscore / 2)
279 LOG(VB_COMMFLAG, LOG_ERR,
"bounding_box giving up (edge "
280 "pixels distributed too uniformly)");
290 if (height > maxheight)
293 int chop = height / 3;
294 int chopheight = height - chop;
296 float upper = bounding_score(img, row, col, width, chopheight);
297 float lower = bounding_score(img, row + chop, col, width, chopheight);
298 LOG(VB_COMMFLAG, LOG_INFO,
299 QString(
"bounding_box too tall (%1 > %2); upper=%3, lower=%4")
300 .arg(height).arg(maxheight)
301 .arg(upper, 0,
'f', 3).arg(lower, 0,
'f', 3));
302 float minscore = std::min(upper, lower);
303 float maxscore = std::max(upper, lower);
304 if (maxscore < 3 * minscore / 2)
311 LOG(VB_COMMFLAG, LOG_ERR,
"bounding_box giving up (edge "
312 "pixel distribution too uniform)");
332 LOG(VB_COMMFLAG, LOG_INFO,
333 QString(
"bounding_box %1x%2@(%3,%4); horizslop=%5,vertslop=%6")
334 .arg(width).arg(height).arg(col).arg(row)
335 .arg(HORIZSLOP).arg(VERTSLOP));
341 if (newrow <= minrow)
346 if (row - newrow >= VERTSLOP)
348 newrow = row - VERTSLOP;
351 if (rowisempty(img, newrow, col, width))
358 newrow = std::max(minrow, newrow - 1);
364 if (newcol <= mincol)
369 if (col - newcol >= HORIZSLOP)
371 newcol = col - HORIZSLOP;
374 if (colisempty(img, newcol, row, height))
381 newcol = std::max(mincol, newcol - 1);
384 newright = col + width;
387 if (newright >= maxcol1)
392 if (newright - (col + width) >= HORIZSLOP)
394 newright = col + width + HORIZSLOP;
397 if (colisempty(img, newright, row, height))
401 newright = std::min(maxcol1, newright + 1);
404 newbottom = row + height;
407 if (newbottom >= maxrow1)
412 if (newbottom - (row + height) >= VERTSLOP)
414 newbottom = row + height + VERTSLOP;
417 if (rowisempty(img, newbottom, col, width))
421 newbottom = std::min(maxrow1, newbottom + 1);
425 width = newright - newcol;
426 height = newbottom - newrow;
428 LOG(VB_COMMFLAG, LOG_INFO, QString(
"bounding_box %1x%2@(%3,%4)")
429 .arg(width).arg(height).arg(col).arg(row));
439 template_alloc(
const unsigned int *scores,
int width,
int height,
440 int minrow,
int mincol,
int maxrow1,
int maxcol1,
AVFrame *tmpl,
441 int *ptmplrow,
int *ptmplcol,
int *ptmplwidth,
int *ptmplheight,
442 bool debug_edgecounts,
const QString& debugdir)
453 static constexpr
float kMinScorePctile = 0.998;
455 const int nn = width * height;
459 unsigned int threshscore = 0;
462 if (av_image_alloc(thresh.data, thresh.linesize,
463 width, height, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
465 LOG(VB_COMMFLAG, LOG_ERR,
466 QString(
"template_alloc av_image_alloc thresh (%1x%2) failed")
467 .arg(width).arg(height));
471 uint *sortedscores =
new unsigned int[nn];
472 memcpy(sortedscores, scores, nn *
sizeof(*sortedscores));
475 if (sortedscores[0] == sortedscores[nn - 1])
478 LOG(VB_COMMFLAG, LOG_ERR,
479 QString(
"template_alloc: %1x%2 pixels all identical!")
480 .arg(width).arg(height));
486 ii = (int)roundf(nn * kMinScorePctile);
487 threshscore = sortedscores[ii];
488 for (first = ii; first > 0 && sortedscores[first] == threshscore; first--)
490 if (sortedscores[first] != threshscore)
492 for (last = ii; last < nn - 1 && sortedscores[last] == threshscore; last++)
494 if (sortedscores[last] != threshscore)
497 LOG(VB_COMMFLAG, LOG_INFO, QString(
"template_alloc wanted %1, got %2-%3")
498 .arg(kMinScorePctile, 0,
'f', 6)
499 .arg((
float)first / nn, 0,
'f', 6)
500 .arg((
float)last / nn, 0,
'f', 6));
502 for (ii = 0; ii < nn; ii++)
503 thresh.data[0][ii] = scores[ii] >= threshscore ? UCHAR_MAX : 0;
505 if (debug_edgecounts)
509 if (av_image_alloc(scored.data, scored.linesize,
510 width, height, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
512 LOG(VB_COMMFLAG, LOG_ERR,
513 QString(
"template_alloc av_image_alloc scored (%1x%2) failed")
514 .arg(width).arg(height));
517 unsigned int maxscore = sortedscores[nn - 1];
518 for (ii = 0; ii < nn; ii++)
519 scored.data[0][ii] = scores[ii] * UCHAR_MAX / maxscore;
520 bool success = writeJPG(debugdir +
"/TemplateFinder-scores", &scored,
522 av_freep(&scored.data[0]);
527 if (!writeJPG(debugdir +
"/TemplateFinder-edgecounts", &thresh, height))
533 if (bounding_box(&thresh, height, minrow, mincol, maxrow1, maxcol1,
534 ptmplrow, ptmplcol, ptmplwidth, ptmplheight))
537 if ((
uint)(*ptmplwidth * *ptmplheight) > USHRT_MAX)
540 LOG(VB_COMMFLAG, LOG_ERR,
541 QString(
"template_alloc bounding_box too big (%1x%2)")
542 .arg(*ptmplwidth).arg(*ptmplheight));
546 if (av_image_alloc(tmpl->data, tmpl->linesize,
547 *ptmplwidth, *ptmplheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
549 LOG(VB_COMMFLAG, LOG_ERR,
550 QString(
"template_alloc av_image_alloc tmpl (%1x%2) failed")
551 .arg(*ptmplwidth).arg(*ptmplheight));
555 if (
pgm_crop(tmpl, &thresh, height, *ptmplrow, *ptmplcol,
556 *ptmplwidth, *ptmplheight))
559 delete []sortedscores;
560 av_freep(&thresh.data[0]);
565 delete []sortedscores;
566 av_freep(&thresh.data[0]);
571 analyzeFrameDebug(
long long frameno,
const AVFrame *pgm,
int pgmheight,
573 int croprow,
int cropcol,
bool debug_frames,
const QString& debugdir)
575 static constexpr
int kDelta = 24;
576 static int s_lastrow;
577 static int s_lastcol;
578 static int s_lastwidth;
579 static int s_lastheight;
580 const int cropwidth = cropped->linesize[0];
582 int rowsame = abs(s_lastrow - croprow) <= kDelta ? 1 : 0;
583 int colsame = abs(s_lastcol - cropcol) <= kDelta ? 1 : 0;
584 int widthsame = abs(s_lastwidth - cropwidth) <= kDelta ? 1 : 0;
585 int heightsame = abs(s_lastheight - cropheight) <= kDelta ? 1 : 0;
587 if (frameno > 0 && rowsame + colsame + widthsame + heightsame >= 3)
590 LOG(VB_COMMFLAG, LOG_INFO,
591 QString(
"TemplateFinder Frame %1: %2x%3@(%4,%5)")
593 .arg(cropwidth).arg(cropheight)
594 .arg(cropcol).arg(croprow));
598 s_lastwidth = cropwidth;
599 s_lastheight = cropheight;
603 QString base = QString(
"%1/TemplateFinder-%2")
604 .arg(debugdir).arg(frameno, 5, 10, QChar(
'0'));
607 if (!writeJPG(base, pgm, pgmheight))
611 if (!writeJPG(base +
"-cropped", cropped, cropheight))
615 if (!writeJPG(base +
"-edges", edges, cropheight))
623 bool readTemplate(
const QString& datafile,
int *prow,
int *pcol,
int *pwidth,
int *pheight,
624 const QString& tmplfile,
AVFrame *tmpl,
bool *pvalid)
626 QFile dfile(datafile);
627 QFileInfo dfileinfo(dfile);
629 if (!dfile.open(QIODevice::ReadOnly))
632 if (!dfileinfo.size())
639 QTextStream stream(&dfile);
640 stream >> *prow >> *pcol >> *pwidth >> *pheight;
643 if (av_image_alloc(tmpl->data, tmpl->linesize,
644 *pwidth, *pheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
646 LOG(VB_COMMFLAG, LOG_ERR,
647 QString(
"readTemplate av_image_alloc %1 (%2x%3) failed")
648 .arg(tmplfile).arg(*pwidth).arg(*pheight));
652 QByteArray tmfile = tmplfile.toLatin1();
653 if (
pgm_read(tmpl->data[0], *pwidth, *pheight, tmfile.constData()))
655 av_freep(&tmpl->data[0]);
664 writeDummyTemplate(
const QString& datafile)
667 QFile dfile(datafile);
669 if (!dfile.open(QIODevice::WriteOnly | QIODevice::Truncate) &&
671 (void)dfile.remove();
675 writeTemplate(
const QString& tmplfile,
const AVFrame *tmpl,
const QString& datafile,
676 int row,
int col,
int width,
int height)
678 QFile tfile(tmplfile);
680 QByteArray tmfile = tmplfile.toLatin1();
681 if (
pgm_write(tmpl->data[0], width, height, tmfile.constData()))
684 QFile dfile(datafile);
685 if (!dfile.open(QIODevice::WriteOnly))
688 QTextStream stream(&dfile);
689 stream << row <<
" " << col <<
"\n" << width <<
" " << height <<
"\n";
697 std::shared_ptr<BorderDetector> bd,
698 std::shared_ptr<EdgeDetector> ed,
699 MythPlayer *player, std::chrono::seconds proglen,
700 const QString& debugdir)
701 : m_pgmConverter(
std::move(pgmc)),
702 m_borderDetector(
std::move(bd)),
703 m_edgeDetector(
std::move(ed)),
704 m_sampleTime(
std::min(proglen / 2, 20 * 60s)),
705 m_debugDir(debugdir),
706 m_debugData(debugdir +
"/TemplateFinder.txt"),
707 m_debugTmpl(debugdir +
"/TemplateFinder.pgm")
717 unsigned int samplesNeeded = 300;
736 LOG(VB_COMMFLAG, LOG_INFO,
737 QString(
"TemplateFinder: sampleTime=%1s, samplesNeeded=%2, endFrame=%3")
752 QString(
"TemplateFinder debugLevel %1").arg(
m_debugLevel));
765 av_freep(&
m_tmpl.data[0]);
800 LOG(VB_COMMFLAG, LOG_INFO,
801 QString(
"TemplateFinder::MythPlayerInited read %1: %2")
816 LOG(VB_COMMFLAG, LOG_INFO,
817 QString(
"TemplateFinder::MythPlayerInited %1 of %2 (%3)")
823 LOG(VB_COMMFLAG, LOG_INFO,
824 QString(
"TemplateFinder::MythPlayerInited framesize %1")
831 av_freep(&
m_tmpl.data[0]);
844 newwidth, newheight, AV_PIX_FMT_GRAY8, IMAGE_ALIGN))
846 LOG(VB_COMMFLAG, LOG_ERR,
847 QString(
"TemplateFinder::resetBuffers "
848 "av_image_alloc cropped (%1x%2) failed")
849 .arg(newwidth).arg(newheight));
860 long long *pNextFrame)
876 const int FRAMESGMPCTILE = 90;
889 static constexpr
float kExcludeWidth = 0.5;
890 static constexpr
float kExcludeHeight = 0.5;
913 &croprow, &cropcol, &cropwidth, &cropheight))
917 auto start = nowAsDuration<std::chrono::microseconds>();
932 cropwidth, cropheight))
939 int excludewidth = (int)(pgmwidth * kExcludeWidth);
940 int excludeheight = (int)(pgmheight * kExcludeHeight);
941 int excluderow = (pgmheight - excludeheight) / 2 - croprow;
942 int excludecol = (pgmwidth - excludewidth) / 2 - cropcol;
944 excludewidth, excludeheight);
948 if (edges ==
nullptr)
951 if (pgm_scorepixels(
m_scores, pgmwidth, croprow, cropcol,
957 if (!analyzeFrameDebug(frameno, pgm, pgmheight, &
m_cropped, edges,
962 auto end = nowAsDuration<std::chrono::microseconds>();
972 LOG(VB_COMMFLAG, LOG_ERR,
973 QString(
"TemplateFinder::analyzeFrame error at frame %1")
1005 LOG(VB_COMMFLAG, LOG_INFO,
1006 QString(
"TemplateFinder::finished wrote %1"
1007 " and %2 [%3x%4@(%5,%6)]")
1023 av_freep(&
m_tmpl.data[0]);
1036 LOG(VB_COMMFLAG, LOG_INFO, QString(
"TF Time: analyze=%1s")