]> git.sesse.net Git - kdenlive/blob - src/vectorscope.cpp
Auto-Refresh for Vectorscope added.
[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
37 #include <QColor>
38 #include <QMouseEvent>
39 #include <QPainter>
40 #include <QDebug>
41
42 #include <qtconcurrentrun.h>
43 #include <QThread>
44
45 #include "vectorscope.h"
46
47 const float SCALING = 1/.7; // See class docs
48 const float P75 = .75;
49 const QPointF YUV_R(-.147,  .615);
50 const QPointF YUV_G(-.289, -.515);
51 const QPointF YUV_B(.437, -.100);
52 const QPointF YUV_Cy(.147, -.615);
53 const QPointF YUV_Mg(.289,  .515);
54 const QPointF YUV_Yl(-.437,  .100);
55
56 const QPen penThick(QBrush(QColor(250,250,250)), 2, Qt::SolidLine);
57 const QPen penThin(QBrush(QColor(250,250,250)), 1, Qt::SolidLine);
58 const QPen penLight(QBrush(QColor(144,255,100,50)), 1, Qt::SolidLine);
59
60
61 Vectorscope::Vectorscope(Monitor *projMonitor, Monitor *clipMonitor, QWidget *parent) :
62     QWidget(parent),
63     m_projMonitor(projMonitor),
64     m_clipMonitor(clipMonitor),
65     m_activeRender(clipMonitor->render),
66     iPaintMode(GREEN),
67     scaling(1),
68     circleEnabled(false),
69     initialDimensionUpdateDone(false)
70 {
71     setupUi(this);
72
73     paintMode->insertItem(GREEN, i18n("Green"));
74     paintMode->insertItem(CHROMA, i18n("Chroma"));
75     paintMode->insertItem(ORIG, i18n("Original Color"));
76
77     connect(paintMode, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPaintModeChanged(int)));
78     connect(cbMagnify, SIGNAL(stateChanged(int)), this, SLOT(slotMagnifyChanged()));
79     connect(this, SIGNAL(signalScopeCalculationFinished()), this, SLOT(slotScopeCalculationFinished()));
80     connect(cbMagnify, SIGNAL(stateChanged(int)), this, SLOT(slotRenderZoneUpdated()));
81
82     newFrames.fetchAndStoreRelaxed(0);
83
84     this->setMouseTracking(true);
85     updateDimensions();
86 }
87
88 Vectorscope::~Vectorscope()
89 {
90 }
91
92 /**
93
94   Input point is on [-1,1]², 0 being at the center,
95   and positive directions are top/right.
96
97   Maps to a QRect «inside» which is using the
98   0 point in the top left corner. The coordinates of
99   the rect «inside» are relative to «parent». The
100   coordinates returned can be used in the parent.
101
102
103     parent v
104   +-------------------+
105   | inside v          |
106   | +-----------+     |
107   | |    +      |     |
108   | |  --0++    |     | < point
109   | |    -      |     |
110   | +-----------+     |
111   |                   |
112   +-------------------+
113
114  */
115 QPoint Vectorscope::mapToCanvas(QRect inside, QPointF point)
116 {
117     return QPoint(point.x()/2*inside.width()  + inside.width()/2  + inside.x(),
118                  -point.y()/2*inside.height() + inside.height()/2 + inside.y());
119 }
120
121 bool Vectorscope::prodCalcThread()
122 {
123     if (m_scopeCalcThread.isRunning()) {
124         qDebug() << "Calc thread still running.";
125         return false;
126     } else {
127         // See http://doc.qt.nokia.com/latest/qtconcurrentrun.html#run about
128         // running member functions in a thread
129         qDebug() << "Calc thread not running anymore, finished: " << m_scopeCalcThread.isFinished() << ", Starting new thread";
130         m_scopeCalcThread = QtConcurrent::run(this, &Vectorscope::calculateScope);
131         return true;
132     }
133 }
134
135 void Vectorscope::calculateScope()
136 {
137     // Prepare the vectorscope data
138     QImage scope(cw, cw, QImage::Format_ARGB32);
139     scope.fill(qRgba(0,0,0,0));
140
141     QImage img(m_activeRender->extractFrame(m_activeRender->seekFramePosition()));
142     newFrames.fetchAndStoreRelease(0); // Reset number of new frames, as we just got the newest
143     uchar *bits = img.bits();
144
145     int r,g,b;
146     double dy, dr, dg, db, dmax;
147     double y,u,v;
148     QPoint pt;
149     QRgb px;
150
151     for (int i = 0; i < img.byteCount(); i+= 4) {
152         QRgb *col = (QRgb *) bits;
153
154         r = qRed(*col);
155         g = qGreen(*col);
156         b = qBlue(*col);
157
158         y = (double) 0.001173 * r + 0.002302 * g + 0.0004471 * b;
159         u = (double) -0.0005781*r -0.001135*g +0.001713*b;
160         v = (double) 0.002411*r -0.002019*g -0.0003921*b;
161
162         pt = mapToCanvas(QRect(QPoint(0,0), scope.size()), QPointF(SCALING*scaling*u, SCALING*scaling*v));
163
164         if (!(pt.x() <= scopeRect.width() && pt.x() >= 0
165             && pt.y() <= scopeRect.height() && pt.y() >= 0)) {
166             // Point lies outside (because of scaling), don't plot it
167             continue;
168         }
169
170         // Draw the pixel using the chosen draw mode
171         switch (iPaintMode) {
172         case CHROMA:
173             dy = 200;
174             dr = dy + 290.8*v;
175             dg = dy - 100.6*u - 148*v;
176             db = dy + 517.2*u;
177
178             dmax = dr;
179             if (dg > dmax) dmax = dg;
180             if (db > dmax) dmax = db;
181             dmax = 255/dmax;
182
183             dr *= dmax;
184             dg *= dmax;
185             db *= dmax;
186
187             scope.setPixel(pt, qRgba(dr, dg, db, 255));
188             break;
189         case ORIG:
190             scope.setPixel(pt, *col);
191             break;
192         case GREEN:
193             px = scope.pixel(pt);
194             scope.setPixel(pt, qRgba(qRed(px)+(255-qRed(px))/40, 255, qBlue(px)+(255-qBlue(px))/40, qAlpha(px)+(255-qAlpha(px))/20));
195             break;
196         }
197
198         bits += 4;
199     }
200
201     m_scope = scope;
202
203     qDebug() << "Scope rendered";
204     emit signalScopeCalculationFinished();
205 }
206
207 void Vectorscope::updateDimensions()
208 {
209     // Widget width/height
210     int ww = this->size().width();
211     int wh = this->size().height();
212
213     // Distance from top/left/right
214     int offset = 6;
215
216     // controlsArea contains the controls at the top;
217     // We want to paint below
218     QPoint topleft(offset, controlsArea->geometry().height()+offset);
219
220     // Circle Width: min of width and height
221     cw = wh - topleft.y();
222     if (ww < cw) { cw = ww; }
223     cw -= 2*offset;
224     scopeRect = QRect(topleft, QPoint(cw, cw) + topleft);
225 }
226
227 void Vectorscope::paintEvent(QPaintEvent *)
228 {
229
230     if (!initialDimensionUpdateDone) {
231         // This is a workaround.
232         // When updating the dimensions in the constructor, the size
233         // of the control items at the top are simply ignored! So do
234         // it here instead.
235         updateDimensions();
236         initialDimensionUpdateDone = true;
237     }
238
239     // Draw the vectorscope circle
240     QPainter davinci(this);
241     QPoint vinciPoint;
242     QPoint centerPoint = mapToCanvas(scopeRect, QPointF(0,0));
243     davinci.setRenderHint(QPainter::Antialiasing, true);
244     davinci.fillRect(0, 0, this->size().width(), this->size().height(), QColor(25,25,23));
245
246     davinci.setPen(penThick);
247     davinci.drawEllipse(scopeRect);
248
249     // Draw RGB/CMY points with 100% chroma
250     vinciPoint = mapToCanvas(scopeRect, SCALING*YUV_R);
251     davinci.drawEllipse(vinciPoint, 4,4);
252     davinci.drawText(vinciPoint-QPoint(20, -10), "R");
253
254     vinciPoint = mapToCanvas(scopeRect, SCALING*YUV_G);
255     davinci.drawEllipse(vinciPoint, 4,4);
256     davinci.drawText(vinciPoint-QPoint(20, 0), "G");
257
258     vinciPoint = mapToCanvas(scopeRect, SCALING*YUV_B);
259     davinci.drawEllipse(vinciPoint, 4,4);
260     davinci.drawText(vinciPoint+QPoint(15, 10), "B");
261
262     vinciPoint = mapToCanvas(scopeRect, SCALING*YUV_Cy);
263     davinci.drawEllipse(vinciPoint, 4,4);
264     davinci.drawText(vinciPoint+QPoint(15, -5), "Cy");
265
266     vinciPoint = mapToCanvas(scopeRect, SCALING*YUV_Mg);
267     davinci.drawEllipse(vinciPoint, 4,4);
268     davinci.drawText(vinciPoint+QPoint(15, 10), "Mg");
269
270     vinciPoint = mapToCanvas(scopeRect, SCALING*YUV_Yl);
271     davinci.drawEllipse(vinciPoint, 4,4);
272     davinci.drawText(vinciPoint-QPoint(25, 0), "Yl");
273
274     // Draw RGB/CMY points with 75% chroma (for NTSC)
275     davinci.setPen(penThin);
276     davinci.drawEllipse(centerPoint, 4,4);
277     davinci.drawEllipse(mapToCanvas(scopeRect, P75*SCALING*YUV_R), 3,3);
278     davinci.drawEllipse(mapToCanvas(scopeRect, P75*SCALING*YUV_G), 3,3);
279     davinci.drawEllipse(mapToCanvas(scopeRect, P75*SCALING*YUV_B), 3,3);
280     davinci.drawEllipse(mapToCanvas(scopeRect, P75*SCALING*YUV_Cy), 3,3);
281     davinci.drawEllipse(mapToCanvas(scopeRect, P75*SCALING*YUV_Mg), 3,3);
282     davinci.drawEllipse(mapToCanvas(scopeRect, P75*SCALING*YUV_Yl), 3,3);
283
284
285
286
287     davinci.setPen(penLight);
288
289
290     // Draw the scope data (previously calculated in a separate thread)
291     davinci.drawImage(scopeRect.topLeft(), m_scope);
292
293
294     if (circleEnabled) {
295         // Mouse moved: Draw a circle over the scope
296
297         int dx = centerPoint.x()-mousePos.x();
298         int dy = centerPoint.y()-mousePos.y();
299
300         QPoint reference = mapToCanvas(scopeRect, QPointF(1,0));
301
302         int r = sqrt(dx*dx + dy*dy);
303         float percent = (float) 100*r/SCALING/scaling/(reference.x() - centerPoint.x());
304
305         davinci.drawEllipse(centerPoint, r,r);
306         davinci.setPen(penThin);
307         davinci.drawText(scopeRect.bottomRight()-QPoint(40,0), QVariant((int)percent).toString().append(" %"));
308
309         circleEnabled = false;
310     }
311 }
312
313 void Vectorscope::slotPaintModeChanged(int index)
314 {
315     iPaintMode = index;
316     this->update();
317 }
318
319 void Vectorscope::slotMagnifyChanged()
320 {
321     if (cbMagnify->isChecked()) {
322         scaling = 1.4;
323     } else {
324         scaling = 1;
325     }
326     this->update();
327 }
328
329 void Vectorscope::slotActiveMonitorChanged(bool isClipMonitor)
330 {
331     if (isClipMonitor) {
332         m_activeRender = m_clipMonitor->render;
333         disconnect(this, SLOT(slotRenderZoneUpdated()));
334         connect(m_activeRender, SIGNAL(rendererPosition(int)), this, SLOT(slotRenderZoneUpdated()));
335     } else {
336         m_activeRender = m_projMonitor->render;
337         disconnect(this, SLOT(slotRenderZoneUpdated()));
338         connect(m_activeRender, SIGNAL(rendererPosition(int)), this, SLOT(slotRenderZoneUpdated()));
339     }
340 }
341
342 void Vectorscope::slotRenderZoneUpdated()
343 {
344     qDebug() << "Monitor incoming. New frames total: " << newFrames;
345     // Monitor has shown a new frame
346     newFrames.fetchAndAddRelaxed(1);
347     if (cbAutoRefresh->isChecked()) {
348         prodCalcThread();
349     }
350 }
351
352 void Vectorscope::mousePressEvent(QMouseEvent *)
353 {
354     // Update the scope on mouse press
355     prodCalcThread();
356 }
357
358 void Vectorscope::mouseMoveEvent(QMouseEvent *event)
359 {
360     // Draw a circle around the center,
361     // showing percentage number of the radius length
362
363     circleEnabled = true;
364     mousePos = event->pos();
365     this->update();
366 }
367
368 void Vectorscope::resizeEvent(QResizeEvent *event)
369 {
370     qDebug() << "Resized.";
371     updateDimensions();
372     QWidget::resizeEvent(event);
373 }
374
375 void Vectorscope::slotScopeCalculationFinished()
376 {
377     this->update();
378     qDebug() << "Scope updated.";
379
380     // If auto-refresh is enabled and new frames are available,
381     // just start the next calculation.
382     if (newFrames > 0 && cbAutoRefresh->isChecked()) {
383         qDebug() << "More frames in the queue: " << newFrames;
384         prodCalcThread();
385     }
386 }