MythTV master
HistogramAnalyzer.cpp
Go to the documentation of this file.
1// ANSI C headers
2#include <cmath>
3#include <utility>
4
5// MythTV headers
9
10// Commercial Flagging headers
11#include "BorderDetector.h"
12#include "CommDetector2.h"
13#include "FrameAnalyzer.h"
14#include "HistogramAnalyzer.h"
15#include "PGMConverter.h"
16#include "TemplateFinder.h"
17#include "quickselect.h"
18
19using namespace commDetector2;
20using namespace frameAnalyzer;
21
22namespace {
23
24bool
25readData(const QString& filename, float *mean, unsigned char *median, float *stddev,
26 int *frow, int *fcol, int *fwidth, int *fheight,
27 HistogramAnalyzer::Histogram *histogram, unsigned char *monochromatic,
28 long long nframes)
29{
30 std::array<quint32,UCHAR_MAX + 1> counter {};
31
32 QByteArray fname = filename.toLocal8Bit();
33 FILE *fp = fopen(fname.constData(), "r");
34 if (fp == nullptr)
35 return false;
36
37 // Automatically clean up file at function exit
38 auto close_fp = [&](FILE *fp2) {
39 if (fclose(fp2) == 0)
40 return;
41 LOG(VB_COMMFLAG, LOG_ERR, QString("Error closing %1: %2")
42 .arg(filename, strerror(errno)));
43 };
44 std::unique_ptr<FILE,decltype(close_fp)> cleanup { fp, close_fp };
45
46 for (long long frameno = 0; frameno < nframes; frameno++)
47 {
48 int monochromaticval = 0;
49 int medianval = 0;
50 int widthval = 0;
51 int heightval = 0;
52 int colval = 0;
53 int rowval = 0;
54 float meanval = NAN;
55 float stddevval = NAN;
56 int nitems = fscanf(fp, "%20d %20f %20d %20f %20d %20d %20d %20d",
57 &monochromaticval, &meanval, &medianval, &stddevval,
58 &widthval, &heightval, &colval, &rowval);
59 if (nitems != 8)
60 {
61 LOG(VB_COMMFLAG, LOG_ERR,
62 QString("Not enough data in %1: frame %2")
63 .arg(filename).arg(frameno));
64 return false;
65 }
66 if (monochromaticval < 0 || monochromaticval > 1 ||
67 medianval < 0 || (uint)medianval > UCHAR_MAX ||
68 widthval < 0 || heightval < 0 || colval < 0 || rowval < 0)
69 {
70 LOG(VB_COMMFLAG, LOG_ERR,
71 QString("Data out of range in %1: frame %2")
72 .arg(filename).arg(frameno));
73 return false;
74 }
75 for (uint & ctr : counter)
76 {
77 if (fscanf(fp, "%20x", &ctr) != 1)
78 {
79 LOG(VB_COMMFLAG, LOG_ERR,
80 QString("Not enough data in %1: frame %2")
81 .arg(filename).arg(frameno));
82 return false;
83 }
84 if (ctr > UCHAR_MAX)
85 {
86 LOG(VB_COMMFLAG, LOG_ERR,
87 QString("Data out of range in %1: frame %2")
88 .arg(filename).arg(frameno));
89 return false;
90 }
91 }
92 mean[frameno] = meanval;
93 median[frameno] = medianval;
94 stddev[frameno] = stddevval;
95 frow[frameno] = rowval;
96 fcol[frameno] = colval;
97 fwidth[frameno] = widthval;
98 fheight[frameno] = heightval;
99 for (size_t ii = 0; ii < counter.size(); ii++)
100 histogram[frameno][ii] = counter[ii];
101 monochromatic[frameno] = !widthval || !heightval ? 1 : 0;
102 /*
103 * monochromaticval not used; it's written to file for debugging
104 * convenience
105 */
106 }
107 return true;
108}
109
110bool
111writeData(const QString& filename, float *mean, unsigned char *median, float *stddev,
112 int *frow, int *fcol, int *fwidth, int *fheight,
113 HistogramAnalyzer::Histogram *histogram, unsigned char *monochromatic,
114 long long nframes)
115{
116 FILE *fp = fopen(filename.toLocal8Bit().constData(), "w");
117 if (fp == nullptr)
118 return false;
119 for (long long frameno = 0; frameno < nframes; frameno++)
120 {
121 (void)fprintf(fp, "%3u %10.6f %3u %10.6f %5d %5d %5d %5d",
122 monochromatic[frameno],
123 static_cast<double>(mean[frameno]), median[frameno],
124 static_cast<double>(stddev[frameno]),
125 fwidth[frameno], fheight[frameno],
126 fcol[frameno], frow[frameno]);
127 for (unsigned int ii = 0; ii < UCHAR_MAX + 1; ii++)
128 (void)fprintf(fp, " %02x", histogram[frameno][ii]);
129 (void)fprintf(fp, "\n");
130 }
131 if (fclose(fp))
132 LOG(VB_COMMFLAG, LOG_ERR, QString("Error closing %1: %2")
133 .arg(filename, strerror(errno)));
134 return true;
135}
136
137}; /* namespace */
138
139HistogramAnalyzer::HistogramAnalyzer(std::shared_ptr<PGMConverter> pgmc,
140 std::shared_ptr<BorderDetector> bd,
141 const QString& debugdir)
142 : m_pgmConverter(std::move(pgmc))
143 , m_borderDetector(std::move(bd))
145 , m_debugdata(debugdir + "/HistogramAnalyzer-pgm.txt")
146#else /* !PGM_CONVERT_GREYSCALE */
147 , m_debugdata(debugdir + "/HistogramAnalyzer-yuv.txt")
148#endif /* !PGM_CONVERT_GREYSCALE */
149{
150 /*
151 * debugLevel:
152 * 0: no debugging
153 * 1: cache frame information into debugdata [1 file]
154 */
155 m_debugLevel = gCoreContext->GetNumSetting("HistogramAnalyzerDebugLevel", 0);
156
157 if (m_debugLevel >= 1)
158 {
159 createDebugDirectory(debugdir,
160 QString("HistogramAnalyzer debugLevel %1").arg(m_debugLevel));
161 m_debugHistVal = true;
162 }
163}
164
166{
167 delete []m_monochromatic;
168 delete []m_mean;
169 delete []m_median;
170 delete []m_stddev;
171 delete []m_fRow;
172 delete []m_fCol;
173 delete []m_fWidth;
174 delete []m_fHeight;
175 delete []m_histogram;
176 delete []m_buf;
177}
178
181{
182 if (m_histValDone)
184
185 if (m_monochromatic)
187
188 QSize buf_dim = player->GetVideoBufferSize();
189 unsigned int width = buf_dim.width();
190 unsigned int height = buf_dim.height();
191
192 if (m_logoFinder != nullptr)
193 {
196 if (m_logo != nullptr)
197 {
200 }
201 }
202 QString details = m_logo ? QString("logo %1x%2@(%3,%4)")
203 .arg(m_logoWidth).arg(m_logoHeight).arg(m_logoCc1).arg(m_logoRr1) :
204 QString("no logo");
205
206 LOG(VB_COMMFLAG, LOG_INFO,
207 QString("HistogramAnalyzer::MythPlayerInited %1x%2: %3")
208 .arg(width).arg(height).arg(details));
209
210 if (m_pgmConverter->MythPlayerInited(player))
212
213 if (m_borderDetector->MythPlayerInited(player))
215
216 // processFrame() sometimes returns frame numbers higher than
217 // m_player->GetTotalFrameCount(). Add extra space at the end
218 // of the arrays to handle this case.
219 LOG(VB_COMMFLAG, LOG_INFO,
220 QString("HistogramAnalyzer::MythPlayerInited nframes %1, allocating %2")
221 .arg(nframes).arg(nframes+128));
222 nframes += 128;
223 m_mean = new float[nframes];
224 m_median = new unsigned char[nframes];
225 m_stddev = new float[nframes];
226 m_fRow = new int[nframes];
227 m_fCol = new int[nframes];
228 m_fWidth = new int[nframes];
229 m_fHeight = new int[nframes];
230 m_histogram = new Histogram[nframes];
231 m_monochromatic = new unsigned char[nframes];
232
233 memset(m_mean, 0, nframes * sizeof(*m_mean));
234 memset(m_median, 0, nframes * sizeof(*m_median));
235 memset(m_stddev, 0, nframes * sizeof(*m_stddev));
236 memset(m_fRow, 0, nframes * sizeof(*m_fRow));
237 memset(m_fCol, 0, nframes * sizeof(*m_fCol));
238 memset(m_fWidth, 0, nframes * sizeof(*m_fWidth));
239 memset(m_fHeight, 0, nframes * sizeof(*m_fHeight));
240 memset(m_histogram, 0, nframes * sizeof(*m_histogram));
241 memset(m_monochromatic, 0, nframes * sizeof(*m_monochromatic));
242
243 unsigned int npixels = width * height;
244 m_buf = new unsigned char[npixels];
245
246 if (m_debugHistVal)
247 {
250 {
251 LOG(VB_COMMFLAG, LOG_INFO,
252 QString("HistogramAnalyzer::MythPlayerInited read %1")
253 .arg(m_debugdata));
254 m_histValDone = true;
256 }
257 }
258
260}
261
262void
264{
265 m_logoFinder = finder;
266}
267
268static constexpr int ROUNDUP(int a, int b) { return (a + b - 1) / b * b; }
269
271HistogramAnalyzer::analyzeFrame(const MythVideoFrame *frame, long long frameno)
272{
273 /*
274 * Various statistical computations over pixel values: mean, median,
275 * (running) standard deviation over sample population.
276 */
277 static constexpr int kDefaultColor = 0;
278
279 /*
280 * TUNABLE:
281 *
282 * Sampling coarseness of each frame. Higher values will allow analysis to
283 * proceed faster (lower resolution), but might be less accurate. Lower
284 * values will examine more pixels (higher resolution), but will run
285 * slower.
286 */
287 static constexpr int kRInc = 4;
288 static constexpr int kCInc = 4;
289
290 int pgmwidth = 0;
291 int pgmheight = 0;
292 bool ismonochromatic = false;
293 int croprow = 0;
294 int cropcol = 0;
295 int cropwidth = 0;
296 int cropheight = 0;
297 unsigned int borderpixels = 0;
298 unsigned int livepixels = 0;
299 unsigned int npixels = 0;
300 unsigned int halfnpixels = 0;
301 unsigned char *pp = nullptr;
302 unsigned char bordercolor = 0;
303 unsigned long long sumval = 0;
304 unsigned long long sumsquares = 0;
305 int rr1 = 0;
306 int cc1 = 0;
307 int rr2 = 0;
308 int cc2 = 0;
309 int rr3 = 0;
310 int cc3 = 0;
311 std::chrono::microseconds start {0us};
312 std::chrono::microseconds end {0us};
313
314 if (m_lastFrameNo != kUncached && m_lastFrameNo == frameno)
316
317 const AVFrame *pgm = m_pgmConverter->getImage(frame, frameno, &pgmwidth, &pgmheight);
318 if (pgm == nullptr)
319 {
320 LOG(VB_COMMFLAG, LOG_ERR,
321 QString("HistogramAnalyzer::analyzeFrame error at frame %1")
322 .arg(frameno));
323
325 }
326
327 ismonochromatic = m_borderDetector->getDimensions(pgm, pgmheight, frameno,
328 &croprow, &cropcol, &cropwidth, &cropheight) != 0;
329
330 start = nowAsDuration<std::chrono::microseconds>();
331
332 m_fRow[frameno] = croprow;
333 m_fCol[frameno] = cropcol;
334 m_fWidth[frameno] = cropwidth;
335 m_fHeight[frameno] = cropheight;
336
337 if (ismonochromatic)
338 {
339 /* Optimization for monochromatic frames; just sample center area. */
340 croprow = pgmheight * 3 / 8;
341 cropheight = pgmheight / 4;
342 cropcol = pgmwidth * 3 / 8;
343 cropwidth = pgmwidth / 4;
344 }
345
346 rr1 = ROUNDUP(croprow, kRInc);
347 cc1 = ROUNDUP(cropcol, kCInc);
348 rr2 = ROUNDUP(croprow + cropheight, kRInc);
349 cc2 = ROUNDUP(cropcol + cropwidth, kCInc);
350 rr3 = ROUNDUP(pgmheight, kRInc);
351 cc3 = ROUNDUP(pgmwidth, kCInc);
352
353 borderpixels = ((rr1 / kRInc) * (cc3 / kCInc)) + /* top */
354 (((rr2 - rr1) / kRInc) * (cc1 / kCInc)) + /* left */
355 (((rr2 - rr1) / kRInc) * ((cc3 - cc2) / kCInc)) + /* right */
356 (((rr3 - rr2) / kRInc) * (cc3 / kCInc)); /* bottom */
357
358 pp = &m_buf[borderpixels];
359 m_histVal.fill(0);
360 m_histVal[kDefaultColor] += borderpixels;
361 for (int rr = rr1; rr < rr2; rr += kRInc)
362 {
363 int rroffset = rr * pgmwidth;
364
365 for (int cc = cc1; cc < cc2; cc += kCInc)
366 {
367 if (m_logo && rr >= m_logoRr1 && rr <= m_logoRr2 &&
368 cc >= m_logoCc1 && cc <= m_logoCc2)
369 continue; /* Exclude logo area from analysis. */
370
371 unsigned char val = pgm->data[0][rroffset + cc];
372 *pp++ = val;
373 sumval += val;
374 sumsquares += 1ULL * val * val;
375 livepixels++;
376 m_histVal[val]++;
377 }
378 }
379 npixels = borderpixels + livepixels;
380
381 /* Scale scores down to [0..255]. */
382 halfnpixels = npixels / 2;
383 for (unsigned int color = 0; color < UCHAR_MAX + 1; color++)
384 m_histogram[frameno][color] =
385 ((m_histVal[color] * UCHAR_MAX) + halfnpixels) / npixels;
386
387 bordercolor = 0;
388 if (ismonochromatic && livepixels)
389 {
390 /*
391 * Fake up the margin pixels to be of the same color as the sampled
392 * area.
393 */
394 bordercolor = (sumval + livepixels - 1) / livepixels;
395 sumval += 1ULL * borderpixels * bordercolor;
396 sumsquares += 1ULL * borderpixels * bordercolor * bordercolor;
397 }
398
399 memset(m_buf, bordercolor, borderpixels * sizeof(*m_buf));
400 m_monochromatic[frameno] = ismonochromatic ? 1 : 0;
401 m_mean[frameno] = (float)sumval / npixels;
402 m_median[frameno] = quick_select_median<uint8_t>(m_buf, npixels);
403 m_stddev[frameno] = npixels > 1 ?
404 sqrt((sumsquares - ((float)sumval * sumval / npixels)) / (npixels - 1)) :
405 0;
406
407 end = nowAsDuration<std::chrono::microseconds>();
408 m_analyzeTime += (end - start);
409
410 m_lastFrameNo = frameno;
411
413}
414
415int
416HistogramAnalyzer::finished(long long nframes, bool final)
417{
419 {
422 {
423 LOG(VB_COMMFLAG, LOG_INFO,
424 QString("HistogramAnalyzer::finished wrote %1")
425 .arg(m_debugdata));
426 m_histValDone = true;
427 }
428 }
429
430 return 0;
431}
432
433int
435{
436 if (m_pgmConverter->reportTime())
437 return -1;
438
439 if (m_borderDetector->reportTime())
440 return -1;
441
442 LOG(VB_COMMFLAG, LOG_INFO, QString("HA Time: analyze=%1s")
444 return 0;
445}
446
447/* vim: set expandtab tabstop=4 shiftwidth=4: */
AVFrame AVFrame
static constexpr int ROUNDUP(int a, int b)
#define PGM_CONVERT_GREYSCALE
Definition: PGMConverter.h:28
static const long long kUncached
HistogramAnalyzer(std::shared_ptr< PGMConverter > pgmc, std::shared_ptr< BorderDetector > bd, const QString &debugdir)
TemplateFinder * m_logoFinder
unsigned char * m_monochromatic
enum FrameAnalyzer::analyzeFrameResult MythPlayerInited(MythPlayer *player, long long nframes)
const struct AVFrame * m_logo
unsigned char * m_median
std::array< int, UCHAR_MAX+1 > m_histVal
std::array< uint8_t, UCHAR_MAX+1 > Histogram
int finished(long long nframes, bool final)
std::shared_ptr< BorderDetector > m_borderDetector
unsigned char * m_buf
void setLogoState(TemplateFinder *finder)
std::chrono::microseconds m_analyzeTime
int reportTime(void) const
enum FrameAnalyzer::analyzeFrameResult analyzeFrame(const MythVideoFrame *frame, long long frameno)
Histogram * m_histogram
std::shared_ptr< PGMConverter > m_pgmConverter
int GetNumSetting(const QString &key, int defaultval=0)
QSize GetVideoBufferSize(void) const
Definition: mythplayer.h:129
const struct AVFrame * getTemplate(int *prow, int *pcol, int *pwidth, int *pheight) const
unsigned int uint
Definition: compat.h:60
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
bool writeData(const QString &filename, float *mean, unsigned char *median, float *stddev, int *frow, int *fcol, int *fwidth, int *fheight, HistogramAnalyzer::Histogram *histogram, unsigned char *monochromatic, long long nframes)
bool readData(const QString &filename, float *mean, unsigned char *median, float *stddev, int *frow, int *fcol, int *fwidth, int *fheight, HistogramAnalyzer::Histogram *histogram, unsigned char *monochromatic, long long nframes)
void createDebugDirectory(const QString &dirname, const QString &comment)
QString strftimeval(std::chrono::microseconds usecs)
int FILE
Definition: mythburn.py:137
static QString cleanup(const QString &str)