MythTV master
BlankFrameDetector.cpp
Go to the documentation of this file.
1// C++ headers
2#include <algorithm>
3#include <cmath>
4#include <cstdlib>
5#include <utility>
6
7// MythTV headers
11
12// Commercial Flagging headers
13#include "BlankFrameDetector.h"
14#include "CommDetector2.h"
15#include "FrameAnalyzer.h"
16#include "HistogramAnalyzer.h"
17#include "TemplateMatcher.h"
18#include "quickselect.h"
19
20using namespace commDetector2;
21using namespace frameAnalyzer;
22
23namespace {
24
25bool
26isBlank(unsigned char median, float stddev, unsigned char maxmedian,
27 float maxstddev)
28{
29 return ((median < maxmedian) ||
30 ((median == maxmedian) && (stddev <= maxstddev)));
31}
32
33int
34sort_ascending_uchar(const void *aa, const void *bb)
35{
36 return *(unsigned char*)aa - *(unsigned char*)bb;
37}
38
39int
40sort_ascending_float(const void *aa, const void *bb)
41{
42 float faa = *(float*)aa;
43 float fbb = *(float*)bb;
44 if (faa < fbb)
45 return -1;
46 if (faa == fbb)
47 return 0;
48 return 1;
49}
50
51bool
52pickmedian(const unsigned char medianval,
53 unsigned char minval, unsigned char maxval)
54{
55 return medianval >= minval && medianval <= maxval;
56}
57
58void
59computeBlankMap(FrameAnalyzer::FrameMap *blankMap, long long nframes,
60 const unsigned char *median, const float *stddev,
61 const unsigned char *monochromatic)
62{
63 /*
64 * Select a "black" value based on a curve, to deal with varying "black"
65 * levels.
66 */
67 const unsigned char MINBLANKMEDIAN = 1;
68 const unsigned char MAXBLANKMEDIAN = 96;
69 const float MEDIANPCTILE = 0.95;
70 const float STDDEVPCTILE = 0.85;
71
72 long long frameno = 1;
73 long long segb = 0;
74 long long sege = 0;
75
76 /* Count and select for monochromatic frames. */
77
78 long long nblanks = 0;
79 for (frameno = 0; frameno < nframes; frameno++)
80 {
81 if (monochromatic[frameno] && pickmedian(median[frameno],
82 MINBLANKMEDIAN, MAXBLANKMEDIAN))
83 nblanks++;
84 }
85
86 if (nblanks <= 0)
87 {
88 /* No monochromatic frames. */
89 LOG(VB_COMMFLAG, LOG_INFO,
90 "BlankFrameDetector::computeBlankMap: No blank frames.");
91 return;
92 }
93
94 /* Select percentile values from monochromatic frames. */
95
96 auto *blankmedian = new unsigned char[nblanks];
97 auto *blankstddev = new float[nblanks];
98 long long blankno = 0;
99 for (frameno = 0; frameno < nframes; frameno++)
100 {
101 if (monochromatic[frameno] && pickmedian(median[frameno],
102 MINBLANKMEDIAN, MAXBLANKMEDIAN))
103 {
104 blankmedian[blankno] = median[frameno];
105 blankstddev[blankno] = stddev[frameno];
106 blankno++;
107 }
108 }
109
110 qsort(blankmedian, nblanks, sizeof(*blankmedian), sort_ascending_uchar);
111 blankno = std::min(nblanks - 1, (long long)roundf(nblanks * MEDIANPCTILE));
112 if (blankno <= 0)
113 {
114 delete []blankmedian;
115 delete []blankstddev;
116 LOG(VB_COMMFLAG, LOG_INFO,
117 "BlankFrameDetector::computeBlankMap: No blank frames. (2)");
118 return;
119 }
120 uchar maxmedian = blankmedian[blankno];
121
122 qsort(blankstddev, nblanks, sizeof(*blankstddev), sort_ascending_float);
123 long long stddevno = std::min(nblanks - 1, (long long)roundf(nblanks * STDDEVPCTILE));
124 float maxstddev = blankstddev[stddevno];
125
126 /* Determine effective percentile ranges (for debugging). */
127
128 long long blankno1 = blankno;
129 long long blankno2 = blankno;
130 while (blankno1 > 0 && blankmedian[blankno1] == maxmedian)
131 blankno1--;
132 if (blankmedian[blankno1] != maxmedian)
133 blankno1++;
134 while (blankno2 < nblanks && blankmedian[blankno2] == maxmedian)
135 blankno2++;
136 if (blankno2 == nblanks)
137 blankno2--;
138
139 long long stddevno1 = stddevno;
140 long long stddevno2 = stddevno;
141 while (stddevno1 > 0 && blankstddev[stddevno1] == maxstddev)
142 stddevno1--;
143 if (blankstddev[stddevno1] != maxstddev)
144 stddevno1++;
145 while (stddevno2 < nblanks && blankstddev[stddevno2] == maxstddev)
146 stddevno2++;
147 if (stddevno2 == nblanks)
148 stddevno2--;
149
150 LOG(VB_COMMFLAG, LOG_INFO,
151 QString("Blanks selecting median<=%1 (%2-%3%), stddev<=%4 (%5-%6%)")
152 .arg(maxmedian)
153 .arg(blankno1 * 100 / nblanks).arg(blankno2 * 100 / nblanks)
154 .arg(maxstddev)
155 .arg(stddevno1 * 100 / nblanks).arg(stddevno2 * 100 / nblanks));
156
157 delete []blankmedian;
158 delete []blankstddev;
159
160 blankMap->clear();
161 if (monochromatic[0] && isBlank(median[0], stddev[0], maxmedian, maxstddev))
162 {
163 segb = 0;
164 sege = 0;
165 }
166 else
167 {
168 /* Fake up a dummy blank frame for interval calculations. */
169 blankMap->insert(0, 0);
170 segb = -1;
171 sege = -1;
172 }
173 for (frameno = 1; frameno < nframes; frameno++)
174 {
175 if (monochromatic[frameno] && isBlank(median[frameno], stddev[frameno],
176 maxmedian, maxstddev))
177 {
178 /* Blank frame. */
179 if (sege < frameno - 1)
180 {
181 /* Start counting. */
182 segb = frameno;
183 sege = frameno;
184 }
185 else
186 {
187 /* Continue counting. */
188 sege = frameno;
189 }
190 }
191 else if (sege == frameno - 1)
192 {
193 /* Transition to non-blank frame. */
194 long long seglen = frameno - segb;
195 blankMap->insert(segb, seglen);
196 }
197 }
198 if (sege == frameno - 1)
199 {
200 /* Possibly ending on blank frames. */
201 long long seglen = frameno - segb;
202 blankMap->insert(segb, seglen);
203 }
204
205 FrameAnalyzer::FrameMap::Iterator iiblank = blankMap->end();
206 --iiblank;
207 if (iiblank.key() + *iiblank < nframes)
208 {
209 /*
210 * Didn't end on blank frames, so add a dummy blank frame at the
211 * end.
212 */
213 blankMap->insert(nframes - 1, 0);
214 }
215}
216
217void
219 const FrameAnalyzer::FrameMap *blankMap, float fps,
220 int debugLevel)
221{
222 /*
223 * TUNABLE:
224 *
225 * Common commercial-break lengths.
226 */
227 struct breakType {
228 std::chrono::seconds m_len;
229 std::chrono::seconds m_delta;
230 };
231 static constexpr std::array<const breakType,4> kBreakType {{
232 /* Sort by "len". */
233 { 15s, 2s },
234 { 20s, 2s },
235 { 30s, 5s },
236 { 60s, 5s },
237 }};
238
239 /*
240 * TUNABLE:
241 *
242 * Shortest non-commercial length, used to coalesce consecutive commercial
243 * breaks that are usually identified due to in-commercial cuts.
244 */
245 static const int kMinContentLen = (int)roundf(10 * fps);
246
247 breakMap->clear();
248 for (FrameAnalyzer::FrameMap::const_iterator iiblank = blankMap->begin();
249 iiblank != blankMap->end();
250 ++iiblank)
251 {
252 long long brkb = iiblank.key();
253 long long iilen = *iiblank;
254 long long start = brkb + (iilen / 2);
255
256 for (const auto& type : kBreakType)
257 {
258 /* Look for next blank frame that is an acceptable distance away. */
259 FrameAnalyzer::FrameMap::const_iterator jjblank = iiblank;
260 for (++jjblank; jjblank != blankMap->end(); ++jjblank)
261 {
262 long long brke = jjblank.key();
263 long long jjlen = *jjblank;
264 long long end = brke + (jjlen / 2);
265
266 auto testlen = std::chrono::seconds(lroundf((end - start) / fps));
267 if (testlen > type.m_len + type.m_delta)
268 break; /* Too far ahead; break to next break length. */
269
270 std::chrono::seconds delta = testlen - type.m_len;
271 delta = std::chrono::abs(delta);
272
273 if (delta > type.m_delta)
274 continue; /* Outside delta range; try next end-blank. */
275
276 /* Mark this commercial break. */
277 bool inserted = false;
278 for (unsigned int jj = 0;; jj++)
279 {
280 long long newbrkb = brkb + jj;
281 if (newbrkb >= brke)
282 {
283 LOG(VB_COMMFLAG, LOG_INFO,
284 QString("BF [%1,%2] ran out of slots")
285 .arg(brkb).arg(brke - 1));
286 break;
287 }
288 if (!breakMap->contains(newbrkb))
289 {
290 breakMap->insert(newbrkb, brke - newbrkb);
291 inserted = true;
292 break;
293 }
294 }
295 if (inserted)
296 break; /* next break type */
297 }
298 }
299 }
300
301 if (debugLevel >= 1)
302 {
303 frameAnalyzerReportMap(breakMap, fps, "BF Break");
304 LOG(VB_COMMFLAG, LOG_INFO,
305 "BF coalescing overlapping/nearby breaks ...");
306 }
307
308 /*
309 * Coalesce overlapping or very-nearby breaks (handles cut-scenes within a
310 * commercial).
311 */
312 for (;;)
313 {
314 bool coalesced = false;
315 FrameAnalyzer::FrameMap::iterator iibreak = breakMap->begin();
316 while (iibreak != breakMap->end())
317 {
318 long long iib = iibreak.key();
319 long long iie = iib + *iibreak;
320
321 FrameAnalyzer::FrameMap::iterator jjbreak = iibreak;
322 ++jjbreak;
323 if (jjbreak == breakMap->end())
324 break;
325
326 long long jjb = jjbreak.key();
327 long long jje = jjb + *jjbreak;
328
329 if (jjb < iib)
330 {
331 /* (jjb,jje) is behind (iib,iie). */
332 ++iibreak;
333 continue;
334 }
335
336 if (iie + kMinContentLen < jjb)
337 {
338 /* (jjb,jje) is too far ahead. */
339 ++iibreak;
340 continue;
341 }
342
343 /* Coalesce. */
344 if (jje > iie)
345 {
346 breakMap->remove(iib); /* overlap */
347 breakMap->insert(iib, jje - iib); /* overlap */
348 }
349 breakMap->erase(jjbreak);
350 coalesced = true;
351 iibreak = breakMap->find(iib);
352 }
353 if (!coalesced)
354 break;
355 }
356
357 /* Adjust for blank intervals. */
358 FrameAnalyzer::FrameMap::iterator iibreak = breakMap->begin();
359 while (iibreak != breakMap->end())
360 {
361 long long iib = iibreak.key();
362 long long iie = iib + *iibreak;
363 iibreak = breakMap->erase(iibreak);
364
365 /* Trim leading blanks from commercial break. */
366 auto iter = blankMap->find(iib);
367 if (iter == blankMap->end())
368 break;
369 long long addb = *iter;
370 addb = addb / 2;
371 addb = std::min<long long>(addb, MAX_BLANK_FRAMES);
372 iib += addb;
373 /* Add trailing blanks to commercial break. */
374 iter = blankMap->find(iib);
375 if (iter == blankMap->end())
376 break;
377 long long adde = *iter;
378 iie += adde;
379 long long sube = adde / 2;
380 sube = std::min<long long>(sube, MAX_BLANK_FRAMES);
381 iie -= sube;
382 breakMap->insert(iib, iie - iib);
383 }
384}
385
386}; /* namespace */
387
388BlankFrameDetector::BlankFrameDetector(std::shared_ptr<HistogramAnalyzer>ha,
389 const QString &debugdir)
390 : m_histogramAnalyzer(std::move(ha))
391{
392 /*
393 * debugLevel:
394 * 0: no debugging
395 * 2: extra verbosity [O(nframes)]
396 */
397 m_debugLevel = gCoreContext->GetNumSetting("BlankFrameDetectorDebugLevel", 0);
398
399 if (m_debugLevel >= 1)
400 createDebugDirectory(debugdir,
401 QString("BlankFrameDetector debugLevel %1").arg(m_debugLevel));
402}
403
406{
408 m_histogramAnalyzer->MythPlayerInited(player, nframes);
409
410 m_fps = player->GetFrameRate();
411
412 QSize video_disp_dim = player->GetVideoSize();
413
414 LOG(VB_COMMFLAG, LOG_INFO,
415 QString("BlankFrameDetector::MythPlayerInited %1x%2")
416 .arg(video_disp_dim.width())
417 .arg(video_disp_dim.height()));
418
419 return ares;
420}
421
423BlankFrameDetector::analyzeFrame(const MythVideoFrame *frame, long long frameno,
424 long long *pNextFrame)
425{
426 *pNextFrame = kNextFrame;
427
428 if (m_histogramAnalyzer->analyzeFrame(frame, frameno) ==
430 return ANALYZE_OK;
431
432 LOG(VB_COMMFLAG, LOG_INFO,
433 QString("BlankFrameDetector::analyzeFrame error at frame %1")
434 .arg(frameno));
435 return ANALYZE_ERROR;
436}
437
438int
439BlankFrameDetector::finished(long long nframes, bool final)
440{
441 if (m_histogramAnalyzer->finished(nframes, final))
442 return -1;
443
444 LOG(VB_COMMFLAG, LOG_INFO, QString("BlankFrameDetector::finished(%1)")
445 .arg(nframes));
446
447 /* Identify all sequences of blank frames (blankMap). */
448 computeBlankMap(&m_blankMap, nframes,
449 m_histogramAnalyzer->getMedians(), m_histogramAnalyzer->getStdDevs(),
450 m_histogramAnalyzer->getMonochromatics());
451 if (m_debugLevel >= 2)
453
454 return 0;
455}
456
457int
459 const TemplateMatcher *templateMatcher)
460{
461 /*
462 * See TemplateMatcher::templateCoverage; some commercial breaks have
463 * logos. Conversely, any logo breaks are probably really breaks, so prefer
464 * those over blank-frame-calculated breaks.
465 */
466 const FrameAnalyzer::FrameMap *logoBreakMap = templateMatcher->getBreaks();
467
468 /* TUNABLE: see TemplateMatcher::adjustForBlanks */
469 const int MAXBLANKADJUSTMENT = (int)roundf(5 * m_fps); /* frames */
470
471 LOG(VB_COMMFLAG, LOG_INFO, "BlankFrameDetector adjusting for logo surplus");
472
473 /*
474 * For each logo break, find the blank frames closest to its beginning and
475 * end. This helps properly support CommSkipAllBlanks.
476 */
477 for (FrameAnalyzer::FrameMap::const_iterator ii =
478 logoBreakMap->constBegin();
479 ii != logoBreakMap->constEnd();
480 ++ii)
481 {
482 /* Get bounds of beginning of logo break. */
483 long long iikey = ii.key();
484 long long iibb = iikey - MAXBLANKADJUSTMENT;
485 long long iiee = iikey + MAXBLANKADJUSTMENT;
486 FrameAnalyzer::FrameMap::Iterator jjfound = m_blankMap.end();
487
488 /* Look for a blank frame near beginning of logo break. */
489 for (auto jj = m_blankMap.begin(); jj != m_blankMap.end(); ++jj)
490 {
491 long long jjbb = jj.key();
492 long long jjee = jjbb + *jj;
493
494 if (iiee < jjbb)
495 break; /* No nearby blank frames. */
496
497 if (jjee < iibb)
498 continue; /* Too early; keep looking. */
499
500 jjfound = jj;
501 if (iikey <= jjbb)
502 {
503 /*
504 * Prefer the first blank frame beginning after the logo break
505 * begins.
506 */
507 break;
508 }
509 }
510
511 /* Adjust blank frame to begin with logo break beginning. */
512 if (jjfound != m_blankMap.end())
513 {
514 long long jjee = jjfound.key() + *jjfound;
515 m_blankMap.erase(jjfound);
516 if (jjee <= iikey)
517 {
518 /* Move blank frame to beginning of logo break. */
519 m_blankMap.remove(iikey);
520 m_blankMap.insert(iikey, 1);
521 }
522 else
523 {
524 /* Adjust blank frame to begin with logo break. */
525 m_blankMap.insert(iikey, jjee - iikey);
526 }
527 }
528
529 /* Get bounds of end of logo break. */
530 long long kkkey = ii.key() + *ii;
531 long long kkbb = kkkey - MAXBLANKADJUSTMENT;
532 long long kkee = kkkey + MAXBLANKADJUSTMENT;
533 FrameAnalyzer::FrameMap::Iterator mmfound = m_blankMap.end();
534
535 /* Look for a blank frame near end of logo break. */
536 for (auto mm = m_blankMap.begin(); mm != m_blankMap.end(); ++mm)
537 {
538 long long mmbb = mm.key();
539 long long mmee = mmbb + *mm;
540
541 if (kkee < mmbb)
542 break; /* No nearby blank frames. */
543
544 if (mmee < kkbb)
545 continue; /* Too early; keep looking. */
546
547 /* Prefer the last blank frame ending before the logo break ends. */
548 if (mmee < kkkey || mmfound == m_blankMap.end())
549 mmfound = mm;
550 if (mmee >= kkkey)
551 break;
552 }
553
554 /* Adjust blank frame to end with logo break end. */
555 if (mmfound != m_blankMap.end())
556 {
557 long long mmbb = mmfound.key();
558 if (mmbb < kkkey)
559 {
560 /* Adjust blank frame to end with logo break. */
561 m_blankMap.remove(mmbb);
562 m_blankMap.insert(mmbb, kkkey - mmbb);
563 }
564 else
565 {
566 /* Move blank frame to end of logo break. */
567 m_blankMap.erase(mmfound);
568 m_blankMap.remove(kkkey - 1);
569 m_blankMap.insert(kkkey - 1, 1);
570 }
571 }
572 }
573
574 /*
575 * Compute breaks (breakMap).
576 */
578
579 /*
580 * Expand blank-frame breaks to fully include overlapping logo breaks.
581 * Fully include logo breaks that don't include any blank-frame breaks.
582 */
583 for (FrameAnalyzer::FrameMap::const_iterator ii =
584 logoBreakMap->constBegin();
585 ii != logoBreakMap->constEnd();
586 ++ii)
587 {
588 long long iibb = ii.key();
589 long long iiee = iibb + *ii;
590 bool overlap = false;
591
592 for (auto jj = m_breakMap.begin(); jj != m_breakMap.end(); )
593 {
594 long long jjbb = jj.key();
595 long long jjee = jjbb + *jj;
596
597 if (iiee < jjbb)
598 {
599 if (!overlap)
600 {
601 /* Fully incorporate logo break */
602 m_breakMap.insert(iibb, iiee - iibb);
603 }
604 break;
605 }
606
607 if (iibb < jjbb && jjbb < iiee)
608 {
609 /* End of logo break includes beginning of blank-frame break. */
610 overlap = true;
611 jj = m_breakMap.erase(jj);
612 m_breakMap.insert(iibb, std::max(iiee, jjee) - iibb);
613 continue;
614 }
615 if (jjbb < iibb && iibb < jjee)
616 {
617 /* End of blank-frame break includes beginning of logo break. */
618 overlap = true;
619 if (jjee < iiee)
620 {
621 m_breakMap.remove(jjbb);
622 m_breakMap.insert(jjbb, iiee - jjbb);
623 }
624 }
625
626 jj++;
627 }
628 }
629
631 return 0;
632}
633
634int
636 [[maybe_unused]] const TemplateMatcher *templateMatcher)
637{
638 LOG(VB_COMMFLAG, LOG_INFO, "BlankFrameDetector adjusting for "
639 "too little logo coverage (unimplemented)");
640 return 0;
641}
642
643int
645{
646 if (m_breakMap.empty())
647 {
648 /* Compute breaks (m_breakMap). */
651 }
652
653 breaks->clear();
654 for (auto bb = m_breakMap.begin(); bb != m_breakMap.end(); ++bb)
655 breaks->insert(bb.key(), *bb);
656
657 return 0;
658}
659
660int
662{
663 return m_histogramAnalyzer->reportTime();
664}
665
666/* vim: set expandtab tabstop=4 shiftwidth=4: */
static constexpr int64_t MAX_BLANK_FRAMES
int computeBreaks(FrameMap *breaks)
int reportTime(void) const override
FrameAnalyzer::FrameMap m_blankMap
std::shared_ptr< HistogramAnalyzer > m_histogramAnalyzer
FrameAnalyzer::FrameMap m_breakMap
enum analyzeFrameResult MythPlayerInited(MythPlayer *player, long long nframes) override
int finished(long long nframes, bool final) override
BlankFrameDetector(std::shared_ptr< HistogramAnalyzer > ha, const QString &debugdir)
int computeForLogoSurplus(const TemplateMatcher *templateMatcher)
static int computeForLogoDeficit(const TemplateMatcher *templateMatcher)
enum analyzeFrameResult analyzeFrame(const MythVideoFrame *frame, long long frameno, long long *pNextFrame) override
static const long long kNextFrame
Definition: FrameAnalyzer.h:59
QMap< long long, long long > FrameMap
Definition: FrameAnalyzer.h:45
int GetNumSetting(const QString &key, int defaultval=0)
QSize GetVideoSize(void) const
Definition: mythplayer.h:131
float GetFrameRate(void) const
Definition: mythplayer.h:133
const FrameAnalyzer::FrameMap * getBreaks(void) const
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
void computeBreakMap(FrameAnalyzer::FrameMap *breakMap, const FrameAnalyzer::FrameMap *blankMap, float fps, int debugLevel)
int sort_ascending_float(const void *aa, const void *bb)
bool pickmedian(const unsigned char medianval, unsigned char minval, unsigned char maxval)
void computeBlankMap(FrameAnalyzer::FrameMap *blankMap, long long nframes, const unsigned char *median, const float *stddev, const unsigned char *monochromatic)
bool isBlank(unsigned char median, float stddev, unsigned char maxmedian, float maxstddev)
int sort_ascending_uchar(const void *aa, const void *bb)
void createDebugDirectory(const QString &dirname, const QString &comment)
void frameAnalyzerReportMapms(const FrameAnalyzer::FrameMap *frameMap, float fps, const char *comment)
void frameAnalyzerReportMap(const FrameAnalyzer::FrameMap *frameMap, float fps, const char *comment)
STL namespace.