]> git.sesse.net Git - kdenlive/blob - src/vectorscope.cpp
Vectorscope changes:
[kdenlive] / src / vectorscope.cpp
1 /***************************************************************************
2  *   Copyright (C) 2010 by Simon Andreas Eugster (simon.eu@gmail.com)      *
3  *   This file is part of kdenlive. See www.kdenlive.org.                  *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  ***************************************************************************/
10
11 /**
12
13   Vectorscope.
14
15   The basis matrix for converting RGB to YUV is:
16
17 mRgb2Yuv =                       r =
18
19    0.29900   0.58700   0.11400     1.00000
20   -0.14741  -0.28939   0.43680  x  0.00000
21    0.61478  -0.51480  -0.09998     0.00000
22
23   The resulting YUV value is then drawn on the circle
24   using U and V as coordinate values.
25
26   The maximum length of such an UV vector is reached
27   for the colors Red and Cyan: 0.632.
28   To make optimal use of space in the circle, this value
29   can be used for scaling.
30
31   As we are dealing with RGB values in a range of {0,...,255}
32   and the conversion values are made for [0,1], we already
33   divide the conversion values by 255 previously, e.g. in
34   GNU octave.
35
36   See also:
37     http://de.wikipedia.org/wiki/Vektorskop
38     http://www.elektroniktutor.de/techno/vektskop.html
39
40  */
41
42 #include <QColor>
43 #include <QMouseEvent>
44 #include <QPainter>
45 #include <QDebug>
46 #include <QAction>
47
48 #include <qtconcurrentrun.h>
49 #include <QThread>
50 #include <QTime>
51
52 #include "vectorscope.h"
53
54 const float SCALING = 1/.7; // See class docs
55 const float P75 = .75;
56 const unsigned char DEFAULT_Y = 255;
57
58 const QPointF YUV_R(-.147,  .615);
59 const QPointF YUV_G(-.289, -.515);
60 const QPointF YUV_B(.437, -.100);
61 const QPointF YUV_Cy(.147, -.615);
62 const QPointF YUV_Mg(.289,  .515);
63 const QPointF YUV_Yl(-.437,  .100);
64
65 const QPen penThick(QBrush(QColor(250,250,250)), 2, Qt::SolidLine);
66 const QPen penThin(QBrush(QColor(250,250,250)), 1, Qt::SolidLine);
67 const QPen penLight(QBrush(QColor(200,200,250,150)), 1, Qt::SolidLine);
68 const QPen penDark(QBrush(QColor(0,0,20,250)), 1, Qt::SolidLine);
69
70
71 Vectorscope::Vectorscope(Monitor *projMonitor, Monitor *clipMonitor, QWidget *parent) :
72     QWidget(parent),
73     m_projMonitor(projMonitor),
74     m_clipMonitor(clipMonitor),
75     m_activeRender(clipMonitor->render),
76     m_scaling(1),
77     circleEnabled(false),
78     initialDimensionUpdateDone(false)
79 {
80     setupUi(this);
81
82     m_colorTools = new ColorTools();
83     m_colorPlaneExport = new ColorPlaneExport(this);
84
85     paintMode->addItem(i18n("Green"), QVariant(PAINT_GREEN));
86     paintMode->addItem(i18n("Green 2"), QVariant(PAINT_GREEN2));
87     paintMode->addItem(i18n("Black"), QVariant(PAINT_BLACK));
88     paintMode->addItem(i18n("Modified YUV (Chroma)"), QVariant(PAINT_CHROMA));
89     paintMode->addItem(i18n("YUV"), QVariant(PAINT_YUV));
90     paintMode->addItem(i18n("Original Color"), QVariant(PAINT_ORIG));
91
92     backgroundMode->addItem(i18n("None"), QVariant(BG_NONE));
93     backgroundMode->addItem(i18n("YUV"), QVariant(BG_YUV));
94     backgroundMode->addItem(i18n("Modified YUV (Chroma)"), QVariant(BG_CHROMA));
95
96     cbAutoRefresh->setChecked(true);
97
98     connect(paintMode, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPaintModeChanged(int)));
99     connect(backgroundMode, SIGNAL(currentIndexChanged(int)), this, SLOT(slotBackgroundChanged()));
100     connect(cbMagnify, SIGNAL(stateChanged(int)), this, SLOT(slotMagnifyChanged()));
101     connect(this, SIGNAL(signalScopeCalculationFinished()), this, SLOT(slotScopeCalculationFinished()));
102     connect(m_colorTools, SIGNAL(signalWheelCalculationFinished()), this, SLOT(slotWheelCalculationFinished()));
103     connect(paintMode, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateScope()));
104     connect(cbAutoRefresh, SIGNAL(stateChanged(int)), this, SLOT(slotUpdateScope()));
105
106     newFrames.fetchAndStoreRelaxed(0);
107     newChanges.fetchAndStoreRelaxed(0);
108     newWheelChanges.fetchAndStoreRelaxed(0);
109
110
111     ///// Build context menu /////
112     setContextMenuPolicy(Qt::ActionsContextMenu);
113
114     m_aExportBackground = new QAction(i18n("Export background"), this);
115     addAction(m_aExportBackground);
116     connect(m_aExportBackground, SIGNAL(triggered()), this, SLOT(slotExportBackground()));
117
118     m_a75PBox = new QAction(i18n("75% box"), this);
119     m_a75PBox->setCheckable(true);
120     m_a75PBox->setChecked(false);
121     addAction(m_a75PBox);
122     connect(m_a75PBox, SIGNAL(changed()), this, SLOT(update()));
123
124     m_aAxisEnabled = new QAction(i18n("Draw axis"), this);
125     m_aAxisEnabled->setCheckable(true);
126     m_aAxisEnabled->setChecked(false);
127     addAction(m_aAxisEnabled);
128     connect(m_aAxisEnabled, SIGNAL(changed()), this, SLOT(update()));
129
130
131     this->setMouseTracking(true);
132     updateDimensions();
133     prodWheelThread();
134 }
135
136 Vectorscope::~Vectorscope()
137 {
138     delete m_colorPlaneExport;
139 }
140
141 /**
142
143   Input point is on [-1,1]², 0 being at the center,
144   and positive directions are top/right.
145
146   Maps to a QRect «inside» which is using the
147   0 point in the top left corner. The coordinates of
148   the rect «inside» are relative to «parent». The
149   coordinates returned can be used in the parent.
150
151
152     parent v
153   +-------------------+
154   | inside v          |
155   | +-----------+     |
156   | |    +      |     |
157   | |  --0++    |     | < point
158   | |    -      |     |
159   | +-----------+     |
160   |                   |
161   +-------------------+
162
163  */
164 QPoint Vectorscope::mapToCanvas(QRect inside, QPointF point)
165 {
166     return QPoint(point.x()/2*inside.width()  + inside.width()/2  + inside.x(),
167                  -point.y()/2*inside.height() + inside.height()/2 + inside.y());
168 }
169
170 bool Vectorscope::prodCalcThread()
171 {
172     if (m_scopeCalcThread.isRunning()) {
173         qDebug() << "Calc thread still running.";
174         return false;
175     } else {
176         // See http://doc.qt.nokia.com/latest/qtconcurrentrun.html#run about
177         // running member functions in a thread
178         qDebug() << "Calc thread not running anymore, finished: " << m_scopeCalcThread.isFinished() << ", Starting new thread";
179         m_scopeCalcThread = QtConcurrent::run(this, &Vectorscope::calculateScope);
180         newFrames.fetchAndStoreRelease(0); // Reset number of new frames, as we just got the newest
181         newChanges.fetchAndStoreRelease(0); // Do the same with the external changes counter
182         return true;
183     }
184 }
185
186 bool Vectorscope::prodWheelThread()
187 {
188     if (m_wheelCalcThread.isRunning()) {
189         qDebug() << "Wheel thread still running.";
190         return false;
191     } else {
192         switch (backgroundMode->itemData(backgroundMode->currentIndex()).toInt()) {
193         case BG_NONE:
194             qDebug() << "No background.";
195             m_wheel = QImage();
196             this->update();
197             break;
198         case BG_YUV:
199             qDebug() << "YUV background.";
200             m_wheelCalcThread = QtConcurrent::run(m_colorTools, &ColorTools::yuvColorWheel, m_scopeRect.size(), (unsigned char) 128, 1/SCALING, false, true);
201             break;
202         case BG_CHROMA:
203             m_wheelCalcThread = QtConcurrent::run(m_colorTools, &ColorTools::yuvColorWheel, m_scopeRect.size(), (unsigned char) 255, 1/SCALING, true, true);
204             break;
205         }
206         newWheelChanges.fetchAndStoreRelaxed(0);
207         return true;
208     }
209 }
210
211
212
213 void Vectorscope::calculateScope()
214 {
215     // Prepare the vectorscope data
216     QImage scope(cw, cw, QImage::Format_ARGB32);
217     scope.fill(qRgba(0,0,0,0));
218
219     QImage img(m_activeRender->extractFrame(m_activeRender->seekFramePosition()));
220     const uchar *bits = img.bits();
221
222     int r,g,b;
223     double dy, dr, dg, db, dmax;
224     double y,u,v;
225     QPoint pt;
226     QRgb px;
227
228     const QRect scopeRect(QPoint(0,0), scope.size());
229
230     for (int i = 0; i < img.byteCount(); i+= 4) {
231         QRgb *col = (QRgb *) bits;
232
233         r = qRed(*col);
234         g = qGreen(*col);
235         b = qBlue(*col);
236
237         y = (double)  0.001173 * r +0.002302 * g +0.0004471* b;
238         u = (double) -0.0005781* r -0.001135 * g +0.001713 * b;
239         v = (double)  0.002411 * r -0.002019 * g -0.0003921* b;
240
241         pt = mapToCanvas(scopeRect, QPointF(SCALING*m_scaling*u, SCALING*m_scaling*v));
242
243         if (!(pt.x() <= scopeRect.width() && pt.x() >= 0
244             && pt.y() <= scopeRect.height() && pt.y() >= 0)) {
245             // Point lies outside (because of scaling), don't plot it
246
247         } else {
248
249             // Draw the pixel using the chosen draw mode.
250             switch (paintMode->itemData(paintMode->currentIndex()).toInt()) {
251             case PAINT_YUV:
252                 // see yuvColorWheel
253                 dy = 128; // Default Y value. Lower = darker.
254
255                 // Calculate the RGB values from YUV
256                 dr = dy + 290.8*v;
257                 dg = dy - 100.6*u - 148*v;
258                 db = dy + 517.2*u;
259
260                 if (dr < 0) dr = 0;
261                 if (dg < 0) dg = 0;
262                 if (db < 0) db = 0;
263                 if (dr > 255) dr = 255;
264                 if (dg > 255) dg = 255;
265                 if (db > 255) db = 255;
266
267                 scope.setPixel(pt, qRgba(dr, dg, db, 255));
268                 break;
269
270             case PAINT_CHROMA:
271                 dy = 200; // Default Y value. Lower = darker.
272
273                 // Calculate the RGB values from YUV
274                 dr = dy + 290.8*v;
275                 dg = dy - 100.6*u - 148*v;
276                 db = dy + 517.2*u;
277
278                 // Scale the RGB values back to max 255
279                 dmax = dr;
280                 if (dg > dmax) dmax = dg;
281                 if (db > dmax) dmax = db;
282                 dmax = 255/dmax;
283
284                 dr *= dmax;
285                 dg *= dmax;
286                 db *= dmax;
287
288                 scope.setPixel(pt, qRgba(dr, dg, db, 255));
289                 break;
290             case PAINT_ORIG:
291                 scope.setPixel(pt, *col);
292                 break;
293             case PAINT_GREEN:
294                 px = scope.pixel(pt);
295                 scope.setPixel(pt, qRgba(qRed(px)+(255-qRed(px))/30, 255, qBlue(px)+(255-qBlue(px))/25, qAlpha(px)+(255-qAlpha(px))/20));
296                 break;
297             case PAINT_GREEN2:
298                 px = scope.pixel(pt);
299                 scope.setPixel(pt, qRgba(qRed(px)+(255-qRed(px))/40+5, 255, qBlue(px)+(255-qBlue(px))/30+10, qAlpha(px)+(255-qAlpha(px))/20));
300                 break;
301             case PAINT_BLACK:
302                 px = scope.pixel(pt);
303                 scope.setPixel(pt, qRgba(0,0,0, qAlpha(px)+(255-qAlpha(px))/20));
304                 break;
305             }
306         }
307
308         bits += 4;
309     }
310
311     m_scope = scope;
312
313     qDebug() << "Scope rendered";
314     emit signalScopeCalculationFinished();
315 }
316
317 void Vectorscope::updateDimensions()
318 {
319     // Widget width/height
320     int ww = this->size().width();
321     int wh = this->size().height();
322
323     // Distance from top/left/right
324     int offset = 6;
325
326     // controlsArea contains the controls at the top;
327     // We want to paint below
328     QPoint topleft(offset, controlsArea->geometry().height()+offset);
329
330     // Circle Width: min of width and height
331     cw = wh - topleft.y();
332     if (ww < cw) { cw = ww; }
333     cw -= 2*offset;
334     m_scopeRect = QRect(topleft, QPoint(cw, cw) + topleft);
335
336     centerPoint = mapToCanvas(m_scopeRect, QPointF(0,0));
337     pR75 = mapToCanvas(m_scopeRect, P75*SCALING*YUV_R);
338     pG75 = mapToCanvas(m_scopeRect, P75*SCALING*YUV_G);
339     pB75 = mapToCanvas(m_scopeRect, P75*SCALING*YUV_B);
340     pCy75 = mapToCanvas(m_scopeRect, P75*SCALING*YUV_Cy);
341     pMg75 = mapToCanvas(m_scopeRect, P75*SCALING*YUV_Mg);
342     pYl75 = mapToCanvas(m_scopeRect, P75*SCALING*YUV_Yl);
343 }
344
345 void Vectorscope::paintEvent(QPaintEvent *)
346 {
347
348     if (!initialDimensionUpdateDone) {
349         // This is a workaround.
350         // When updating the dimensions in the constructor, the size
351         // of the control items at the top are simply ignored! So do
352         // it here instead.
353         updateDimensions();
354         initialDimensionUpdateDone = true;
355     }
356
357     // Draw the vectorscope circle
358     QPainter davinci(this);
359     QPoint vinciPoint;
360
361
362     davinci.setRenderHint(QPainter::Antialiasing, true);
363     davinci.fillRect(0, 0, this->size().width(), this->size().height(), QColor(25,25,23));
364
365     davinci.drawImage(m_scopeRect.topLeft(), m_wheel);
366
367     davinci.setPen(penThick);
368     davinci.drawEllipse(m_scopeRect);
369
370     // Draw RGB/CMY points with 100% chroma
371     vinciPoint = mapToCanvas(m_scopeRect, SCALING*YUV_R);
372     davinci.drawEllipse(vinciPoint, 4,4);
373     davinci.drawText(vinciPoint-QPoint(20, -10), "R");
374
375     vinciPoint = mapToCanvas(m_scopeRect, SCALING*YUV_G);
376     davinci.drawEllipse(vinciPoint, 4,4);
377     davinci.drawText(vinciPoint-QPoint(20, 0), "G");
378
379     vinciPoint = mapToCanvas(m_scopeRect, SCALING*YUV_B);
380     davinci.drawEllipse(vinciPoint, 4,4);
381     davinci.drawText(vinciPoint+QPoint(15, 10), "B");
382
383     vinciPoint = mapToCanvas(m_scopeRect, SCALING*YUV_Cy);
384     davinci.drawEllipse(vinciPoint, 4,4);
385     davinci.drawText(vinciPoint+QPoint(15, -5), "Cy");
386
387     vinciPoint = mapToCanvas(m_scopeRect, SCALING*YUV_Mg);
388     davinci.drawEllipse(vinciPoint, 4,4);
389     davinci.drawText(vinciPoint+QPoint(15, 10), "Mg");
390
391     vinciPoint = mapToCanvas(m_scopeRect, SCALING*YUV_Yl);
392     davinci.drawEllipse(vinciPoint, 4,4);
393     davinci.drawText(vinciPoint-QPoint(25, 0), "Yl");
394
395     switch (backgroundMode->itemData(backgroundMode->currentIndex()).toInt()) {
396     case BG_NONE:
397         davinci.setPen(penLight);
398         break;
399     default:
400         davinci.setPen(penDark);
401         break;
402     }
403
404     // Draw axis
405     if (m_aAxisEnabled->isChecked()) {
406         davinci.drawLine(mapToCanvas(m_scopeRect, QPointF(0,-.9)), mapToCanvas(m_scopeRect, QPointF(0,.9)));
407         davinci.drawLine(mapToCanvas(m_scopeRect, QPointF(-.9,0)), mapToCanvas(m_scopeRect, QPointF(.9,0)));
408     }
409
410     // Draw center point
411     switch (backgroundMode->itemData(backgroundMode->currentIndex()).toInt()) {
412     case BG_CHROMA:
413         davinci.setPen(penDark);
414         break;
415     default:
416         davinci.setPen(penThin);
417         break;
418     }
419     davinci.drawEllipse(centerPoint, 5,5);
420
421
422     // Draw 75% box
423     if (m_a75PBox->isChecked()) {
424         davinci.drawLine(pR75, pYl75);
425         davinci.drawLine(pYl75, pG75);
426         davinci.drawLine(pG75, pCy75);
427         davinci.drawLine(pCy75, pB75);
428         davinci.drawLine(pB75, pMg75);
429         davinci.drawLine(pMg75, pR75);
430     }
431
432     // Draw RGB/CMY points with 75% chroma (for NTSC)
433     davinci.setPen(penThin);
434     davinci.drawEllipse(pR75, 3,3);
435     davinci.drawEllipse(pG75, 3,3);
436     davinci.drawEllipse(pB75, 3,3);
437     davinci.drawEllipse(pCy75, 3,3);
438     davinci.drawEllipse(pMg75, 3,3);
439     davinci.drawEllipse(pYl75, 3,3);
440
441
442
443     // Draw the scope data (previously calculated in a separate thread)
444     davinci.drawImage(m_scopeRect.topLeft(), m_scope);
445
446
447     if (circleEnabled) {
448         // Mouse moved: Draw a circle over the scope
449
450         int dx = centerPoint.x()-mousePos.x();
451         int dy = centerPoint.y()-mousePos.y();
452
453         QPoint reference = mapToCanvas(m_scopeRect, QPointF(1,0));
454
455         int r = sqrt(dx*dx + dy*dy);
456         float percent = (float) 100*r/SCALING/m_scaling/(reference.x() - centerPoint.x());
457
458         switch (backgroundMode->itemData(backgroundMode->currentIndex()).toInt()) {
459         case BG_NONE:
460             davinci.setPen(penLight);
461             break;
462         default:
463             davinci.setPen(penDark);
464             break;
465         }
466         davinci.drawEllipse(centerPoint, r,r);
467         davinci.setPen(penThin);
468         davinci.drawText(m_scopeRect.bottomRight()-QPoint(40,0), QVariant((int)percent).toString().append(" %"));
469
470         circleEnabled = false;
471     }
472 }
473
474
475
476
477 ///// Slots /////
478
479 void Vectorscope::slotMagnifyChanged()
480 {
481     if (cbMagnify->isChecked()) {
482         m_scaling = 1.4;
483     } else {
484         m_scaling = 1;
485     }
486     prodCalcThread();
487 }
488
489 void Vectorscope::slotActiveMonitorChanged(bool isClipMonitor)
490 {
491     if (isClipMonitor) {
492         m_activeRender = m_clipMonitor->render;
493         disconnect(this, SLOT(slotRenderZoneUpdated()));
494         connect(m_activeRender, SIGNAL(rendererPosition(int)), this, SLOT(slotRenderZoneUpdated()));
495     } else {
496         m_activeRender = m_projMonitor->render;
497         disconnect(this, SLOT(slotRenderZoneUpdated()));
498         connect(m_activeRender, SIGNAL(rendererPosition(int)), this, SLOT(slotRenderZoneUpdated()));
499     }
500 }
501
502 void Vectorscope::slotRenderZoneUpdated()
503 {
504     qDebug() << "Monitor incoming. New frames total: " << newFrames;
505     // Monitor has shown a new frame
506     newFrames.fetchAndAddRelaxed(1);
507     if (cbAutoRefresh->isChecked()) {
508         prodCalcThread();
509     }
510 }
511
512 void Vectorscope::slotScopeCalculationFinished()
513 {
514     if (!m_scopeCalcThread.isFinished()) {
515         // Wait for the thread to finish. Otherwise the scope might not get updated
516         // as prodCalcThread may see it still running.
517         QTime start = QTime::currentTime();
518         qDebug() << "Scope renderer has not finished yet, waiting ...";
519         m_scopeCalcThread.waitForFinished();
520         qDebug() << "Done. Waited for " << start.msecsTo(QTime::currentTime()) << " ms";
521     }
522
523     this->update();
524     qDebug() << "Scope updated.";
525
526     // If auto-refresh is enabled and new frames are available,
527     // just start the next calculation.
528     if (newFrames > 0 && cbAutoRefresh->isChecked()) {
529         qDebug() << "More frames in the queue: " << newFrames;
530         prodCalcThread();
531     } else if (newChanges > 0) {
532         qDebug() << newChanges << " changes (e.g. resize) in the meantime.";
533         prodCalcThread();
534     } else {
535         qDebug() << newFrames << " new frames, " << newChanges << " new changes. Not updating.";
536     }
537 }
538
539 void Vectorscope::slotWheelCalculationFinished()
540 {
541     if (!m_wheelCalcThread.isFinished()) {
542         QTime start = QTime::currentTime();
543         qDebug() << "Wheel calc has not finished yet, waiting ...";
544         m_wheelCalcThread.waitForFinished();
545         qDebug() << "Done. Waited for " << start.msecsTo(QTime::currentTime()) << " ms";
546     }
547
548     qDebug() << "Wheel calculated. Updating.";
549     qDebug() << m_wheelCalcThread.resultCount() << " results from the Wheel thread.";
550     if (m_wheelCalcThread.resultCount() > 0) {
551         m_wheel = m_wheelCalcThread.resultAt(0);
552     }
553     this->update();
554     if (newWheelChanges > 0) {
555         prodWheelThread();
556     }
557 }
558
559 void Vectorscope::slotUpdateScope()
560 {
561     prodCalcThread();
562 }
563
564 void Vectorscope::slotUpdateWheel()
565 {
566     prodWheelThread();
567 }
568
569 void Vectorscope::slotExportBackground()
570 {
571     qDebug() << "Exporting background";
572     m_colorPlaneExport->show();
573
574 }
575
576 void Vectorscope::slotBackgroundChanged()
577 {
578     // Background changed, switch to a suitable color mode now
579     int index;
580     switch (backgroundMode->itemData(backgroundMode->currentIndex()).toInt()) {
581     case BG_YUV:
582         index = paintMode->findData(QVariant(PAINT_BLACK));
583         if (index >= 0) {
584             paintMode->setCurrentIndex(index);
585         }
586         break;
587
588     case BG_NONE:
589         if (paintMode->itemData(paintMode->currentIndex()) == PAINT_BLACK) {
590             index = paintMode->findData(QVariant(PAINT_GREEN));
591             paintMode->setCurrentIndex(index);
592         }
593         break;
594     }
595     newWheelChanges.fetchAndAddAcquire(1);
596     prodWheelThread();
597 }
598
599
600
601 ///// Events /////
602
603 void Vectorscope::mousePressEvent(QMouseEvent *)
604 {
605     // Update the scope on mouse press
606     prodCalcThread();
607 }
608
609 void Vectorscope::mouseMoveEvent(QMouseEvent *event)
610 {
611     // Draw a circle around the center,
612     // showing percentage number of the radius length
613
614     circleEnabled = true;
615     mousePos = event->pos();
616     this->update();
617 }
618
619 void Vectorscope::resizeEvent(QResizeEvent *event)
620 {
621     qDebug() << "Resized.";
622     updateDimensions();
623     newChanges.fetchAndAddAcquire(1);
624     prodCalcThread();
625     QWidget::resizeEvent(event);
626 }