File indexing completed on 2024-04-28 04:52:23

0001 /*
0002     SPDX-FileCopyrightText: 2010 Simon Andreas Eugster <simon.eu@gmail.com>
0003     This file is part of kdenlive. See www.kdenlive.org.
0004 
0005 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 /**
0009 
0010   Vectorscope.
0011 
0012   The basis matrix for converting RGB to YUV is
0013   (in this example for calculating the YUV value of red):
0014 
0015 mRgb2Yuv =                       r =
0016 
0017    0.29900   0.58700   0.11400     1.00000
0018   -0.14741  -0.28939   0.43680  x  0.00000
0019    0.61478  -0.51480  -0.09998     0.00000
0020 
0021   The resulting YUV value is then drawn on the circle
0022   using U and V as coordinate values.
0023 
0024   The maximum length of such an UV vector is reached
0025   for the colors Red and Cyan: 0.632.
0026   To make optimal use of space in the circle, this value
0027   can be used for scaling.
0028 
0029   As we are dealing with RGB values in a range of {0,...,255}
0030   and the conversion values are made for [0,1], we already
0031   divide the conversion values by 255 previously, e.g. in
0032   GNU octave.
0033 
0034   The basis matrix for converting RGB to YPbPr is:
0035 
0036 mRgb2YPbPr =                        r =
0037 
0038    0.299000   0.587000   0.114000     1.0000
0039   -0.168736  -0.331264   0.500000  x  0.0000
0040    0.500000  -0.418688  -0.081312     0.0000
0041 
0042    Note that YUV and YPbPr are not the same.
0043    * YUV is used for analog transfer of PAL/NTSC
0044    * YPbPr is used for analog transfer of YCbCr sources
0045      like DVD/DVB.
0046    It does _not_ matter for the Vectorscope what color space
0047    the input video is; The used color space is just a tool
0048    to visualize the hue. The difference is merely that the
0049    scope looks slightly different when choosing the respectively
0050    other color space; The way the Vectorscope works stays the same.
0051 
0052    YCbCr is basically YPbPr for digital use (it is defined
0053    on a range of {16,219} for Y and {16,240} for Cb/Cr
0054    components).
0055 
0056   See also:
0057     http://www.poynton.com/ColorFAQ.html
0058     https://de.wikipedia.org/wiki/Vektorskop
0059     https://www.elektroniktutor.de/geraetetechnik/vektskop.html
0060 
0061  */
0062 
0063 #include "vectorscopegenerator.h"
0064 #include <cmath>
0065 
0066 // The maximum distance from the center for any RGB color is 0.63, so
0067 // no need to make the circle bigger than required.
0068 const double SCALING = 1 / .7;
0069 
0070 const double VectorscopeGenerator::scaling = 1 / .7;
0071 
0072 /**
0073   Input point is on [-1,1]², 0 being at the center,
0074   and positive directions are →top/→right.
0075 
0076   Maps to the coordinates used in QImages with the 0 point
0077   at the top left corner.
0078 
0079  -1          +1
0080 +1+-----------+
0081   |    +      |
0082   |  --0++    |
0083   |    -      |
0084 -1+-----------+
0085      vvv
0086    mapped to
0087       v
0088   0      x
0089  0+------+
0090   |0++   |
0091   |-     |
0092   |-     |
0093  y+------+
0094 
0095   With y:
0096   1. Scale from [-1,1] to [0,1] with                    y01 := (y+1)/2
0097   2. Invert (Orientation of the y axis changes) with    y10 := 1-y01
0098   3. Scale from [1,0] to [height-1,0] with              yy  := (height-1) * y10
0099   x does not need to be inverted.
0100 
0101  */
0102 QPoint VectorscopeGenerator::mapToCircle(const QSize &targetSize, const QPointF &point) const
0103 {
0104     return {int((targetSize.width() - 1) * (point.x() + 1) / 2), int((targetSize.height() - 1) * (1 - (point.y() + 1) / 2))};
0105 }
0106 
0107 QImage VectorscopeGenerator::calculateVectorscope(const QSize &vectorscopeSize, const QImage &image, const float &gain,
0108                                                   const VectorscopeGenerator::PaintMode &paintMode, const VectorscopeGenerator::ColorSpace &colorSpace, bool,
0109                                                   uint accelFactor) const
0110 {
0111     if (vectorscopeSize.width() <= 0 || vectorscopeSize.height() <= 0 || image.width() <= 0 || image.height() <= 0) {
0112         // Invalid size
0113         return QImage();
0114     }
0115     if (accelFactor < 1) { accelFactor = 1; }
0116 
0117     // Prepare the vectorscope data
0118     const int cw = (vectorscopeSize.width() < vectorscopeSize.height()) ? vectorscopeSize.width() : vectorscopeSize.height();
0119     QImage scope = QImage(cw, cw, QImage::Format_ARGB32);
0120     scope.fill(qRgba(0, 0, 0, 0));
0121 
0122     double dy, dr, dg, db, dmax;
0123     double /*y,*/ u, v;
0124     QPoint pt;
0125     QRgb px;
0126 
0127     // Just an average for the number of image pixels per scope pixel.
0128     // NOTE: byteCount() has to be replaced by (img.bytesPerLine()*img.height()) for Qt 4.5 to compile, see:
0129     // https://doc.qt.io/qt-5/qimage.html#bytesPerLine
0130     double avgPxPerPx = double(image.depth()) / 8 * (image.bytesPerLine() * image.height()) / scope.size().width() / scope.size().height() / accelFactor;
0131 
0132     // benchmarking code
0133     // const auto start = std::chrono::high_resolution_clock::now();
0134 
0135     const auto totalPixels = image.width() * image.height();
0136     for (int i = 0; i < totalPixels; i += accelFactor) {
0137         const QRgb pixel = image.pixel(i % image.width(), i / image.width());
0138         const int r = qRed(pixel);
0139         const int g = qGreen(pixel);
0140         const int b = qBlue(pixel);
0141 
0142         switch (colorSpace) {
0143         case VectorscopeGenerator::ColorSpace_YUV:
0144             //             y = (double)  0.001173 * r +0.002302 * g +0.0004471* b;
0145             u = -0.0005781 * r - 0.001135 * g + 0.001713 * b;
0146             v = 0.002411 * r - 0.002019 * g - 0.0003921 * b;
0147             break;
0148         case VectorscopeGenerator::ColorSpace_YPbPr:
0149         default:
0150             //             y = (double)  0.001173 * r +0.002302 * g +0.0004471* b;
0151             u = -0.0006671 * r - 0.001299 * g + 0.0019608 * b;
0152             v = 0.001961 * r - 0.001642 * g - 0.0003189 * b;
0153             break;
0154         }
0155 
0156         pt = mapToCircle(vectorscopeSize, QPointF(SCALING * double(gain) * u, SCALING * double(gain) * v));
0157 
0158         if (pt.x() >= scope.width() || pt.x() < 0 || pt.y() >= scope.height() || pt.y() < 0) {
0159             // Point lies outside (because of scaling), don't plot it
0160 
0161         } else {
0162 
0163             // Draw the pixel using the chosen draw mode.
0164             switch (paintMode) {
0165             case PaintMode_YUV:
0166                 // see yuvColorWheel
0167                 dy = 128; // Default Y value. Lower = darker.
0168 
0169                 // Calculate the RGB values from YUV/YPbPr
0170                 switch (colorSpace) {
0171                 case VectorscopeGenerator::ColorSpace_YUV:
0172                     dr = dy + 290.8 * v;
0173                     dg = dy - 100.6 * u - 148 * v;
0174                     db = dy + 517.2 * u;
0175                     break;
0176                 case VectorscopeGenerator::ColorSpace_YPbPr:
0177                 default:
0178                     dr = dy + 357.5 * v;
0179                     dg = dy - 87.75 * u - 182 * v;
0180                     db = dy + 451.9 * u;
0181                     break;
0182                 }
0183 
0184                 if (dr < 0) {
0185                     dr = 0;
0186                 }
0187                 if (dg < 0) {
0188                     dg = 0;
0189                 }
0190                 if (db < 0) {
0191                     db = 0;
0192                 }
0193                 if (dr > 255) {
0194                     dr = 255;
0195                 }
0196                 if (dg > 255) {
0197                     dg = 255;
0198                 }
0199                 if (db > 255) {
0200                     db = 255;
0201                 }
0202 
0203                 scope.setPixel(pt, qRgba(int(dr), int(dg), int(db), 255));
0204                 break;
0205 
0206             case PaintMode_Chroma:
0207                 dy = 200; // Default Y value. Lower = darker.
0208 
0209                 // Calculate the RGB values from YUV/YPbPr
0210                 switch (colorSpace) {
0211                 case VectorscopeGenerator::ColorSpace_YUV:
0212                     dr = dy + 290.8 * v;
0213                     dg = dy - 100.6 * u - 148 * v;
0214                     db = dy + 517.2 * u;
0215                     break;
0216                 case VectorscopeGenerator::ColorSpace_YPbPr:
0217                 default:
0218                     dr = dy + 357.5 * v;
0219                     dg = dy - 87.75 * u - 182 * v;
0220                     db = dy + 451.9 * u;
0221                     break;
0222                 }
0223 
0224                 // Scale the RGB values back to max 255
0225                 dmax = dr;
0226                 if (dg > dmax) {
0227                     dmax = dg;
0228                 }
0229                 if (db > dmax) {
0230                     dmax = db;
0231                 }
0232                 dmax = 255 / dmax;
0233 
0234                 dr *= dmax;
0235                 dg *= dmax;
0236                 db *= dmax;
0237 
0238                 scope.setPixel(pt, qRgba(int(dr), int(dg), int(db), 255));
0239                 break;
0240             case PaintMode_Original:
0241                 scope.setPixel(pt, pixel);
0242                 break;
0243             case PaintMode_Green:
0244                 px = scope.pixel(pt);
0245                 scope.setPixel(pt, qRgba(qRed(px) + int((255 - qRed(px)) / (3 * avgPxPerPx)), qGreen(px) + int(20 * (255 - qGreen(px)) / (avgPxPerPx)),
0246                                          qBlue(px) + int((255 - qBlue(px)) / (avgPxPerPx)), qAlpha(px) + int((255 - qAlpha(px)) / (avgPxPerPx))));
0247                 break;
0248             case PaintMode_Green2:
0249                 px = scope.pixel(pt);
0250                 scope.setPixel(pt, qRgba(qRed(px) + int(ceil((255 - qRed(px)) / (4 * avgPxPerPx))), 255,
0251                                          qBlue(px) + int(ceil((255 - qBlue(px)) / (avgPxPerPx))), qAlpha(px) + int(ceil((255 - qAlpha(px)) / (avgPxPerPx)))));
0252                 break;
0253             case PaintMode_Black:
0254             default:
0255                 px = scope.pixel(pt);
0256                 scope.setPixel(pt, qRgba(0, 0, 0, qAlpha(px) + (255 - qAlpha(px)) / 20));
0257                 break;
0258             }
0259         }
0260     }
0261     // const auto elapsed = std::chrono::high_resolution_clock::now() - start;
0262     // uint64_t us = std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
0263     // qDebug() << "Vectorscope calculated in" << us << " microseconds";
0264     return scope;
0265 }