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