diff --git a/examples/imagecolorstest.qml b/examples/imagecolorstest.qml index 147963c6..ae42ba40 100644 --- a/examples/imagecolorstest.qml +++ b/examples/imagecolorstest.qml @@ -1,152 +1,152 @@ import QtQuick 2.12 import QtQuick.Layouts 1.4 import QtQuick.Controls 2.12 as Controls import org.kde.kirigami 2.13 as Kirigami RowLayout { id: root width: 500 height: 500 property var icons: ["desktop", "firefox", "vlc", "blender", "applications-games", "blinken", "adjustlevels", "adjustrgb", "cuttlefish", "folder-games", "applications-network", "multimedia-player", "applications-utilities", "accessories-dictionary", "calligraflow", "calligrakrita", "view-left-close","calligraauthor"] property int i Kirigami.ImageColors { id: palette - source: icon + source: icon.source } Kirigami.ImageColors { id: imgPalette source: image } ColumnLayout { Rectangle { Layout.preferredWidth: 200 Layout.preferredHeight: 200 z: -1 color: palette.dominantContrast Kirigami.Icon { id: icon anchors.centerIn: parent width: 128 height: 128 source: "desktop" } } Rectangle { Layout.preferredWidth: 30 Layout.preferredHeight: 30 color: palette.average } Controls.Button { text: "Next" onClicked: { i = (i+1)%icons.length icon.source = icons[i] - palette.update() + // palette.update() } } Repeater { model: palette.palette delegate: RowLayout { Layout.fillWidth: true Rectangle { implicitWidth: 10 + 300 * modelData.ratio implicitHeight: 30 color: modelData.color } Item { Layout.fillWidth: true } Rectangle { color: modelData.contrastColor implicitWidth: 30 implicitHeight: 30 } } } } Item { Layout.preferredWidth: 500 Layout.preferredHeight: 500/(image.sourceSize.width/image.sourceSize.height) Image { id: image source: "https://source.unsplash.com/random" anchors.fill: parent onStatusChanged: imgPalette.update() } ColumnLayout { Controls.Button { text: "Update" onClicked: { image.source = "https://source.unsplash.com/random#" + (new Date()).getMilliseconds() } } Repeater { model: imgPalette.palette delegate: RowLayout { Layout.fillWidth: true Rectangle { implicitWidth: 10 + 300 * modelData.ratio implicitHeight: 30 color: modelData.color } Item { Layout.fillWidth: true } Rectangle { color: modelData.contrastColor implicitWidth: 30 implicitHeight: 30 } } } } Item { width: 300 height: 150 Kirigami.Theme.backgroundColor: imgPalette.background Kirigami.Theme.textColor: imgPalette.foreground Kirigami.Theme.highlightColor: imgPalette.highlight anchors { bottom: parent.bottom right: parent.right } Rectangle { anchors.fill: parent opacity: 0.8 color: Kirigami.Theme.backgroundColor } ColumnLayout { anchors.centerIn: parent RowLayout { Rectangle { Layout.alignment: Qt.AlignCenter implicitWidth: 10 implicitHeight: 10 color: Kirigami.Theme.highlightColor } Controls.Label { text: "Lorem Ipsum dolor sit amet" color: Kirigami.Theme.textColor } } RowLayout { Controls.TextField { Kirigami.Theme.inherit: true text: "text" } Controls.Button { Kirigami.Theme.inherit: true text: "Ok" } } } } } } diff --git a/src/imagecolors.cpp b/src/imagecolors.cpp index eb9993a5..9807daaa 100644 --- a/src/imagecolors.cpp +++ b/src/imagecolors.cpp @@ -1,454 +1,449 @@ /* * Copyright 2020 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. */ #include "imagecolors.h" #include #include #include #include #include ImageColors::ImageColors(QObject *parent) : QObject(parent) { m_imageSyncTimer = new QTimer(this); m_imageSyncTimer->setSingleShot(true); m_imageSyncTimer->setInterval(100); /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() { generatePalette(); });*/ } ImageColors::~ImageColors() {} void ImageColors::setSource(const QVariant &source) { if (source.canConvert()) { setSourceItem(source.value()); } else if (source.canConvert()) { setSourceImage(source.value()); } else if (source.canConvert()) { setSourceImage(source.value().pixmap(128, 128).toImage()); } else if (source.canConvert()) { setSourceImage(QIcon::fromTheme(source.toString()).pixmap(128, 128).toImage()); } else { return; } m_source = source; emit sourceChanged(); } QVariant ImageColors::source() const { return m_source; } void ImageColors::setSourceImage(const QImage &image) { if (m_window) { disconnect(m_window.data(), nullptr, this, nullptr); } if (m_sourceItem) { disconnect(m_sourceItem.data(), nullptr, this, nullptr); } if (m_grabResult) { disconnect(m_grabResult.data(), nullptr, this, nullptr); m_grabResult.clear(); } m_sourceItem.clear(); - if (m_sourceImage.isNull()) { - m_sourceImage = image; - update(); - } else { - m_sourceImage = image; - update(); - } + m_sourceImage = image; + update(); } QImage ImageColors::sourceImage() const { return m_sourceImage; } void ImageColors::setSourceItem(QQuickItem *source) { if (m_sourceItem == source) { return; } if (m_window) { disconnect(m_window.data(), nullptr, this, nullptr); } if (m_sourceItem) { disconnect(m_sourceItem, nullptr, this, nullptr); } m_sourceItem = source; update(); if (m_sourceItem) { auto syncWindow = [this] () { if (m_window) { disconnect(m_window.data(), nullptr, this, nullptr); } m_window = m_sourceItem->window(); if (m_window) { connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update); } }; connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow); syncWindow(); } } QQuickItem *ImageColors::sourceItem() const { return m_sourceItem; } void ImageColors::update() { if (m_futureImageData) { m_futureImageData->cancel(); m_futureImageData->deleteLater(); } auto runUpdate = [this]() { QFuture future = QtConcurrent::run([this](){return generatePalette(m_sourceImage);}); m_futureImageData = new QFutureWatcher(this); connect(m_futureImageData, &QFutureWatcher::finished, this, [this] () { if (!m_futureImageData) { return; } m_imageData = m_futureImageData->future().result(); m_futureImageData->deleteLater(); m_futureImageData = nullptr; emit paletteChanged(); }); m_futureImageData->setFuture(future); }; if (!m_sourceItem || !m_window) { if (!m_sourceImage.isNull()) { runUpdate(); } return; } if (m_grabResult) { disconnect(m_grabResult.data(), nullptr, this, nullptr); m_grabResult.clear(); } m_grabResult = m_sourceItem->grabToImage(QSize(128, 128)); if (m_grabResult) { connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() { m_sourceImage = m_grabResult->image(); m_grabResult.clear(); runUpdate(); }); } } inline int squareDistance(QRgb color1, QRgb color2) { // https://en.wikipedia.org/wiki/Color_difference // Using RGB distance for performance, as CIEDE2000 istoo complicated if (qRed(color1) - qRed(color2) < 128) { return 2 * pow(qRed(color1) - qRed(color2), 2) + 4 * pow(qGreen(color1) - qGreen(color2), 2) + 3 * pow(qBlue(color1) - qBlue(color2), 2); } else { return 3 * pow(qRed(color1) - qRed(color2), 2) + 4 * pow(qGreen(color1) - qGreen(color2), 2) + 2 * pow(qBlue(color1) - qBlue(color2), 2); } } void ImageColors::positionColor(QRgb rgb, QList &clusters) { for (auto &stat : clusters) { if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) { stat.colors.append(rgb); return; } } ImageData::colorStat stat; stat.colors.append(rgb); stat.centroid = rgb; clusters << stat; } ImageData ImageColors::generatePalette(const QImage &sourceImage) { ImageData imageData; if (sourceImage.isNull() || sourceImage.width() == 0) { return imageData; } imageData.m_clusters.clear(); imageData.m_samples.clear(); QColor sampleColor; 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) { sampleColor = sourceImage.pixelColor(x, y); if (sampleColor.alpha() == 0) { continue; } QRgb rgb = sampleColor.rgb(); c++; r += qRed(rgb); g += qGreen(rgb); b += qBlue(rgb); imageData.m_samples << rgb; positionColor(rgb, imageData.m_clusters); } } if (imageData.m_samples.isEmpty()) { return imageData; } imageData.m_average = QColor(r/c, g/c, b/c, 255); for (int iteration = 0; iteration < 5; ++iteration) { for (auto &stat : imageData.m_clusters) { r = 0; g = 0; b = 0; c = 0; for (auto color : stat.colors) { c++; r += qRed(color); g += qGreen(color); b += qBlue(color); } r = r / c; g = g / c; b = b / c; stat.centroid = qRgb(r, g, b); stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count()); stat.colors = QList({stat.centroid}); } for (auto color : imageData.m_samples) { positionColor(color, imageData.m_clusters); } } std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [](const ImageData::colorStat &a, const ImageData::colorStat &b) { return a.colors.size() > b.colors.size(); }); // compress blocks that became too similar auto sourceIt = imageData.m_clusters.end(); QList::iterator> itemsToDelete; while (sourceIt != imageData.m_clusters.begin()) { sourceIt--; for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) { if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) { const qreal ratio = (*sourceIt).ratio / (*destIt).ratio; const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid)); const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid)); const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid)); (*destIt).ratio += (*sourceIt).ratio; (*destIt).centroid = qRgb(r, g, b); itemsToDelete << sourceIt; break; } } } for (const auto &i : itemsToDelete) { imageData.m_clusters.erase(i); } imageData.m_highlight = QColor(); imageData.m_dominant = QColor(imageData.m_clusters.first().centroid); imageData.m_closestToBlack = Qt::white; imageData.m_closestToWhite = Qt::black; imageData.m_palette.clear(); bool first = true; for (const auto &stat : imageData.m_clusters) { QVariantMap entry; const QColor color(stat.centroid); entry[QStringLiteral("color")] = color; entry[QStringLiteral("ratio")] = stat.ratio; QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue()); contrast.setHsl(contrast.hslHue(), contrast.hslSaturation(), 128 + (128 - contrast.lightness())); QColor tempContrast; int minimumDistance = 4681800; //max distance: 4*3*2*3*255*255 for (const auto &stat : imageData.m_clusters) { const int distance = squareDistance(contrast.rgb(), stat.centroid); if (distance < minimumDistance) { tempContrast = QColor(stat.centroid); minimumDistance = distance; } } if (imageData.m_clusters.size() <= 3) { if (qGray(imageData.m_dominant.rgb()) < 120) { contrast = QColor(230, 230, 230); } else { contrast = QColor(20, 20, 20); } // TODO: replace m_clusters.size() > 3 with entropy calculation } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) { contrast = tempContrast; } else { contrast = tempContrast; contrast.setHsl(contrast.hslHue(), contrast.hslSaturation(), contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20)); } entry[QStringLiteral("contrastColor")] = contrast; if (first) { imageData.m_dominantContrast = contrast; imageData.m_dominant = color; } first = false; if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) { imageData.m_highlight = color; } if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) { imageData.m_closestToWhite = color; } if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) { imageData.m_closestToBlack = color; } imageData.m_palette << entry; } return imageData; } QVariantList ImageColors::palette() const { return m_imageData.m_palette; } ColorUtils::Brightness ImageColors::paletteBrightness() const { return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light; } QColor ImageColors::average() const { return m_imageData.m_average; } QColor ImageColors::dominant() const { return m_imageData.m_dominant; } QColor ImageColors::dominantContrast() const { return m_imageData.m_dominantContrast; } QColor ImageColors::foreground() const { if (paletteBrightness() == ColorUtils::Dark) { if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { return QColor(230, 230, 230); } return m_imageData.m_closestToWhite; } else { if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { return QColor(20, 20, 20); } return m_imageData.m_closestToBlack; } } QColor ImageColors::background() const { if (paletteBrightness() == ColorUtils::Dark) { if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { return QColor(20, 20, 20); } return m_imageData.m_closestToBlack; } else { if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { return QColor(230, 230, 230); } return m_imageData.m_closestToWhite; } } QColor ImageColors::highlight() const { return m_imageData.m_highlight; } QColor ImageColors::closestToWhite() const { if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { return QColor(230, 230, 230); } return m_imageData.m_closestToWhite; } QColor ImageColors::closestToBlack() const { if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { return QColor(20, 20, 20); } return m_imageData.m_closestToBlack; } #include "moc_imagecolors.cpp"