Skip to content

ImageColors: use OpenMP to count pixels in parallel

Fushan Wen requested to merge work/fuf/openmp into master

This could hopefully accelerate the palette generation process and make the grabbed image have higher resolution, which will be helpful to plasma/kdeplasma-addons!249 (merged) as image extractor requires higher resolution to calculate the average color more precisely.

On my laptop (AMD Ryzen 4700u) the new code is about 5x faster than the original code.

Parallel 258
Single 1470
Test code
#include <QDebug>
#include <QElapsedTimer>
#include <QImage>

#include <omp.h>
#include <cmath>

struct LabColor {
    qreal l = 0;
    qreal a = 0;
    qreal b = 0;
};

struct XYZColor {
    qreal x = 0;
    qreal y = 0;
    qreal z = 0;
};

XYZColor colorToXYZ(const QColor &color)
{
    // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ
    qreal r = color.redF();
    qreal g = color.greenF();
    qreal b = color.blueF();
    // Apply gamma correction (i.e. conversion to linear-space)
    auto correct = [](qreal &v) {
        if (v > 0.04045) {
            v = std::pow((v + 0.055) / 1.055, 2.4);
        } else {
            v = v / 12.92;
        }
    };

    correct(r);
    correct(g);
    correct(b);

    // Observer. = 2°, Illuminant = D65
    const qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805;
    const qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722;
    const qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505;

    return XYZColor{x, y, z};
}

LabColor colorToLab(const QColor &color)
{
    // First: convert to XYZ
    const auto xyz = colorToXYZ(color);

    // Second: convert from XYZ to L*a*b
    qreal x = xyz.x / 0.95047; // Observer= 2°, Illuminant= D65
    qreal y = xyz.y / 1.0;
    qreal z = xyz.z / 1.08883;

    auto pivot = [](qreal &v) {
        if (v > 0.008856) {
            v = std::pow(v, 1.0 / 3.0);
        } else {
            v = (7.787 * v) + (16.0 / 116.0);
        }
    };

    pivot(x);
    pivot(y);
    pivot(z);

    LabColor labColor;
    labColor.l = std::max(0.0, (116 * y) - 16);
    labColor.a = 500 * (x - y);
    labColor.b = 200 * (y - z);

    return labColor;
}

double chroma(const QColor &color)
{
    LabColor labColor = colorToLab(color);

    // Chroma is hypotenuse of a and b
    return sqrt(pow(labColor.a, 2) + pow(labColor.b, 2));
}

int main()
{
    QImage sourceImage("/mnt/data/home/qydw/Pictures/Nature Ultra HD 4K Wallpapers/Nature Ultra HD 4K Wallpapers Pack-4 (25).jpg");
    qInfo() << sourceImage.size();
    sourceImage = sourceImage.scaledToWidth(128);

    QElapsedTimer timer;

    timer.start();
    static const int numCore = std::min(8, omp_get_num_procs());
    omp_set_num_threads(numCore);
    for (int i = 0; i < 1000; ++i)
    {
        QList<QRgb> m_samples;
        std::vector<decltype(m_samples)> tempSamples(numCore, decltype(m_samples){});
        int r = 0;
        int g = 0;
        int b = 0;
        int c = 0;

#pragma omp parallel for reduction(+ : r) reduction(+ : g) reduction(+ : b) reduction(+ : c)
        for (int x = 0; x < sourceImage.width(); ++x) {
            for (int y = 0; y < sourceImage.height(); ++y) {
                const QColor sampleColor = sourceImage.pixelColor(x, y);
                if (sampleColor.alpha() == 0) {
                    continue;
                }
                if (chroma(sampleColor) < 20) {
                    continue;
                }
                QRgb rgb = sampleColor.rgb();
                ++c;
                r += qRed(rgb);
                g += qGreen(rgb);
                b += qBlue(rgb);
                tempSamples[omp_get_thread_num()] << rgb;
            }
        } // END omp parallel for

        for (const auto &s : std::as_const(tempSamples)) {
            m_samples << s;
        }
    }
    qInfo() << "Parallel" << timer.elapsed();

    // Single
    timer.restart();
    for (int i = 0; i < 1000; ++i)
    {
        QList<QRgb> m_samples;
        int r = 0;
        int g = 0;
        int b = 0;
        int c = 0;


        for (int x = 0; x < sourceImage.width(); ++x) {
            for (int y = 0; y < sourceImage.height(); ++y) {
                const QColor sampleColor = sourceImage.pixelColor(x, y);
                if (sampleColor.alpha() == 0) {
                    continue;
                }
                if (chroma(sampleColor) < 20) {
                    continue;
                }
                QRgb rgb = sampleColor.rgb();
                ++c;
                r += qRed(rgb);
                g += qGreen(rgb);
                b += qBlue(rgb);
                m_samples << rgb;
            }
        }
    }
    qInfo() << "Single" << timer.elapsed();

    return 0;
}
Edited by Fushan Wen

Merge request reports