plugin: add image analyser
This commit is contained in:
parent
d79b5b7f71
commit
2ccd3a8662
7 changed files with 298 additions and 255 deletions
|
|
@ -8,23 +8,28 @@ IconImage {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property color colour
|
required property color colour
|
||||||
property color dominantColour
|
|
||||||
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: Colouriser {
|
layer.effect: Colouriser {
|
||||||
sourceColor: root.dominantColour
|
sourceColor: analyser.dominantColour
|
||||||
colorizationColor: root.colour
|
colorizationColor: root.colour
|
||||||
}
|
}
|
||||||
|
|
||||||
layer.onEnabledChanged: {
|
layer.onEnabledChanged: {
|
||||||
if (layer.enabled && status === Image.Ready)
|
if (layer.enabled && status === Image.Ready)
|
||||||
CUtils.getDominantColour(this, c => dominantColour = c);
|
analyser.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (layer.enabled && status === Image.Ready)
|
if (layer.enabled && status === Image.Ready)
|
||||||
CUtils.getDominantColour(this, c => dominantColour = c);
|
analyser.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageAnalyser {
|
||||||
|
id: analyser
|
||||||
|
|
||||||
|
sourceItem: root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ qml_module(caelestia
|
||||||
appdb.hpp appdb.cpp
|
appdb.hpp appdb.cpp
|
||||||
requests.hpp requests.cpp
|
requests.hpp requests.cpp
|
||||||
toaster.hpp toaster.cpp
|
toaster.hpp toaster.cpp
|
||||||
|
imageanalyser.hpp imageanalyser.cpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
|
|
|
||||||
|
|
@ -116,232 +116,6 @@ bool CUtils::deleteFile(const QUrl& path) const {
|
||||||
return QFile::remove(path.toLocalFile());
|
return QFile::remove(path.toLocalFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CUtils::getDominantColour(QQuickItem* item, QJSValue callback) {
|
|
||||||
this->getDominantColour(item, 128, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CUtils::getDominantColour(QQuickItem* item, int rescaleSize, QJSValue callback) {
|
|
||||||
if (!item) {
|
|
||||||
qWarning() << "CUtils::getDominantColour: an item is required";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item->window()) {
|
|
||||||
// Fail silently to avoid warning
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QSharedPointer<const QQuickItemGrabResult> grabResult = item->grabToImage();
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, rescaleSize, callback, this]() {
|
|
||||||
const auto future = QtConcurrent::run(&CUtils::findDominantColour, this, grabResult->image(), rescaleSize);
|
|
||||||
auto* watcher = new QFutureWatcher<QColor>(this);
|
|
||||||
auto* engine = qmlEngine(this);
|
|
||||||
|
|
||||||
QObject::connect(watcher, &QFutureWatcher<QColor>::finished, this, [=]() {
|
|
||||||
if (callback.isCallable()) {
|
|
||||||
callback.call({ engine->toScriptValue(QVariant::fromValue(watcher->result())) });
|
|
||||||
}
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void CUtils::getDominantColour(const QString& path, QJSValue callback) {
|
|
||||||
this->getDominantColour(path, 128, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CUtils::getDominantColour(const QString& path, int rescaleSize, QJSValue callback) {
|
|
||||||
if (path.isEmpty()) {
|
|
||||||
qWarning() << "CUtils::getDominantColour: given path is empty";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto future = QtConcurrent::run([=, this]() {
|
|
||||||
const QImage image(path);
|
|
||||||
|
|
||||||
if (image.isNull()) {
|
|
||||||
qWarning() << "CUtils::getDominantColour: failed to load image" << path;
|
|
||||||
return QColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
return findDominantColour(image, rescaleSize);
|
|
||||||
});
|
|
||||||
auto* watcher = new QFutureWatcher<QColor>(this);
|
|
||||||
auto* engine = qmlEngine(this);
|
|
||||||
|
|
||||||
QObject::connect(watcher, &QFutureWatcher<QColor>::finished, this, [=]() {
|
|
||||||
if (watcher->result().isValid() && callback.isCallable()) {
|
|
||||||
callback.call({ engine->toScriptValue(QVariant::fromValue(watcher->result())) });
|
|
||||||
}
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor CUtils::findDominantColour(const QImage& image, int rescaleSize) const {
|
|
||||||
if (image.isNull()) {
|
|
||||||
qWarning() << "CUtils::findDominantColour: image is null";
|
|
||||||
return QColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage img = image;
|
|
||||||
|
|
||||||
if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) {
|
|
||||||
img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (img.format() != QImage::Format_ARGB32) {
|
|
||||||
img = img.convertToFormat(QImage::Format_ARGB32);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_map<quint32, int> colours;
|
|
||||||
const uchar* data = img.bits();
|
|
||||||
const int width = img.width();
|
|
||||||
const int height = img.height();
|
|
||||||
const qsizetype bytesPerLine = img.bytesPerLine();
|
|
||||||
|
|
||||||
for (int y = 0; y < height; ++y) {
|
|
||||||
const uchar* line = data + y * bytesPerLine;
|
|
||||||
for (int x = 0; x < width; ++x) {
|
|
||||||
const uchar* pixel = line + x * 4;
|
|
||||||
|
|
||||||
if (pixel[3] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
quint32 r = static_cast<quint32>(pixel[0] & 0xF8);
|
|
||||||
quint32 g = static_cast<quint32>(pixel[1] & 0xF8);
|
|
||||||
quint32 b = static_cast<quint32>(pixel[2] & 0xF8);
|
|
||||||
|
|
||||||
quint32 colour = (r << 16) | (g << 8) | b;
|
|
||||||
++colours[colour];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
quint32 dominantColour = 0;
|
|
||||||
int maxCount = 0;
|
|
||||||
for (const auto& [colour, count] : colours) {
|
|
||||||
if (count > maxCount) {
|
|
||||||
dominantColour = colour;
|
|
||||||
maxCount = count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return QColor((0xFFu << 24) | dominantColour);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CUtils::getAverageLuminance(QQuickItem* item, QJSValue callback) {
|
|
||||||
this->getAverageLuminance(item, 128, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CUtils::getAverageLuminance(QQuickItem* item, int rescaleSize, QJSValue callback) {
|
|
||||||
if (!item) {
|
|
||||||
qWarning() << "CUtils::getAverageLuminance: an item is required";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item->window()) {
|
|
||||||
// Fail silently to avoid warning
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QSharedPointer<const QQuickItemGrabResult> grabResult = item->grabToImage();
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, rescaleSize, callback, this]() {
|
|
||||||
const auto future =
|
|
||||||
QtConcurrent::run(&CUtils::findAverageLuminance, this, grabResult->image(), rescaleSize);
|
|
||||||
auto* watcher = new QFutureWatcher<qreal>(this);
|
|
||||||
|
|
||||||
QObject::connect(watcher, &QFutureWatcher<qreal>::finished, this, [=]() {
|
|
||||||
if (callback.isCallable()) {
|
|
||||||
callback.call({ QJSValue(watcher->result()) });
|
|
||||||
}
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void CUtils::getAverageLuminance(const QString& path, QJSValue callback) {
|
|
||||||
this->getAverageLuminance(path, 128, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CUtils::getAverageLuminance(const QString& path, int rescaleSize, QJSValue callback) {
|
|
||||||
if (path.isEmpty()) {
|
|
||||||
qWarning() << "CUtils::getAverageLuminance: given path is empty";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto future = QtConcurrent::run([=, this]() {
|
|
||||||
const QImage image(path);
|
|
||||||
|
|
||||||
if (image.isNull()) {
|
|
||||||
qWarning() << "CUtils::getAverageLuminance: failed to load image" << path;
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return findAverageLuminance(image, rescaleSize);
|
|
||||||
});
|
|
||||||
auto* watcher = new QFutureWatcher<qreal>(this);
|
|
||||||
|
|
||||||
QObject::connect(watcher, &QFutureWatcher<qreal>::finished, this, [=]() {
|
|
||||||
if (callback.isCallable()) {
|
|
||||||
callback.call({ QJSValue(watcher->result()) });
|
|
||||||
}
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal CUtils::findAverageLuminance(const QImage& image, int rescaleSize) const {
|
|
||||||
if (image.isNull()) {
|
|
||||||
qWarning() << "CUtils::findAverageLuminance: image is null";
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage img = image;
|
|
||||||
|
|
||||||
if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) {
|
|
||||||
img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (img.format() != QImage::Format_ARGB32) {
|
|
||||||
img = img.convertToFormat(QImage::Format_ARGB32);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uchar* data = img.bits();
|
|
||||||
const int width = img.width();
|
|
||||||
const int height = img.height();
|
|
||||||
const qsizetype bytesPerLine = img.bytesPerLine();
|
|
||||||
|
|
||||||
qreal totalLuminance = 0.0;
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
for (int y = 0; y < height; ++y) {
|
|
||||||
const uchar* line = data + y * bytesPerLine;
|
|
||||||
for (int x = 0; x < width; ++x) {
|
|
||||||
const uchar* pixel = line + x * 4;
|
|
||||||
|
|
||||||
if (pixel[3] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qreal r = pixel[0] / 255.0;
|
|
||||||
const qreal g = pixel[1] / 255.0;
|
|
||||||
const qreal b = pixel[2] / 255.0;
|
|
||||||
|
|
||||||
totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b);
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count == 0 ? 0.0 : totalLuminance / count;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString CUtils::toLocalFile(const QUrl& url) const {
|
QString CUtils::toLocalFile(const QUrl& url) const {
|
||||||
if (!url.isLocalFile()) {
|
if (!url.isLocalFile()) {
|
||||||
qWarning() << "CUtils::toLocalFile: given url is not a local file" << url;
|
qWarning() << "CUtils::toLocalFile: given url is not a local file" << url;
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,7 @@ public:
|
||||||
|
|
||||||
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
|
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
|
||||||
Q_INVOKABLE bool deleteFile(const QUrl& path) const;
|
Q_INVOKABLE bool deleteFile(const QUrl& path) const;
|
||||||
|
|
||||||
Q_INVOKABLE void getDominantColour(QQuickItem* item, QJSValue callback);
|
|
||||||
Q_INVOKABLE void getDominantColour(QQuickItem* item, int rescaleSize, QJSValue callback);
|
|
||||||
Q_INVOKABLE void getDominantColour(const QString& path, QJSValue callback);
|
|
||||||
Q_INVOKABLE void getDominantColour(const QString& path, int rescaleSize, QJSValue callback);
|
|
||||||
|
|
||||||
Q_INVOKABLE void getAverageLuminance(QQuickItem* item, QJSValue callback);
|
|
||||||
Q_INVOKABLE void getAverageLuminance(QQuickItem* item, int rescaleSize, QJSValue callback);
|
|
||||||
Q_INVOKABLE void getAverageLuminance(const QString& path, QJSValue callback);
|
|
||||||
Q_INVOKABLE void getAverageLuminance(const QString& path, int rescaleSize, QJSValue callback);
|
|
||||||
|
|
||||||
Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
|
Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
|
||||||
|
|
||||||
private:
|
|
||||||
QColor findDominantColour(const QImage& image, int rescaleSize) const;
|
|
||||||
qreal findAverageLuminance(const QImage& image, int rescaleSize) const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace caelestia
|
} // namespace caelestia
|
||||||
|
|
|
||||||
223
plugin/src/Caelestia/imageanalyser.cpp
Normal file
223
plugin/src/Caelestia/imageanalyser.cpp
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
#include "imageanalyser.hpp"
|
||||||
|
|
||||||
|
#include <QtConcurrent/qtconcurrentrun.h>
|
||||||
|
#include <QtQuick/qquickitemgrabresult.h>
|
||||||
|
#include <qfuturewatcher.h>
|
||||||
|
#include <qimage.h>
|
||||||
|
#include <qquickwindow.h>
|
||||||
|
|
||||||
|
namespace caelestia {
|
||||||
|
|
||||||
|
ImageAnalyser::ImageAnalyser(QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_futureWatcher(new QFutureWatcher<AnalyseResult>(this))
|
||||||
|
, m_source("")
|
||||||
|
, m_sourceItem(nullptr)
|
||||||
|
, m_rescaleSize(128)
|
||||||
|
, m_dominantColour(0, 0, 0)
|
||||||
|
, m_luminance(0) {
|
||||||
|
QObject::connect(m_futureWatcher, &QFutureWatcher<AnalyseResult>::finished, this, [this]() {
|
||||||
|
if (!m_futureWatcher->future().isResultReadyAt(0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = m_futureWatcher->result();
|
||||||
|
if (m_dominantColour != result.first) {
|
||||||
|
m_dominantColour = result.first;
|
||||||
|
emit dominantColourChanged();
|
||||||
|
}
|
||||||
|
if (qFuzzyCompare(m_luminance + 1.0, result.second + 1.0)) {
|
||||||
|
m_luminance = result.second;
|
||||||
|
emit luminanceChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ImageAnalyser::source() const {
|
||||||
|
return m_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAnalyser::setSource(const QString& source) {
|
||||||
|
if (m_source == source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_source = source;
|
||||||
|
emit sourceChanged();
|
||||||
|
|
||||||
|
if (m_sourceItem) {
|
||||||
|
m_sourceItem = nullptr;
|
||||||
|
emit sourceItemChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickItem* ImageAnalyser::sourceItem() const {
|
||||||
|
return m_sourceItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAnalyser::setSourceItem(QQuickItem* sourceItem) {
|
||||||
|
if (m_sourceItem == sourceItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_sourceItem = sourceItem;
|
||||||
|
emit sourceItemChanged();
|
||||||
|
|
||||||
|
if (!m_source.isEmpty()) {
|
||||||
|
m_source = "";
|
||||||
|
emit sourceChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ImageAnalyser::rescaleSize() const {
|
||||||
|
return m_rescaleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAnalyser::setRescaleSize(int rescaleSize) {
|
||||||
|
if (m_rescaleSize == rescaleSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_rescaleSize = rescaleSize;
|
||||||
|
emit rescaleSizeChanged();
|
||||||
|
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor ImageAnalyser::dominantColour() const {
|
||||||
|
return m_dominantColour;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal ImageAnalyser::luminance() const {
|
||||||
|
return m_luminance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAnalyser::requestUpdate() {
|
||||||
|
if (m_source.isEmpty() && !m_sourceItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_sourceItem || (m_sourceItem->window() && m_sourceItem->window()->isVisible() && m_sourceItem->width() > 0 &&
|
||||||
|
m_sourceItem->height() > 0)) {
|
||||||
|
update();
|
||||||
|
} else if (m_sourceItem) {
|
||||||
|
if (!m_sourceItem->window()) {
|
||||||
|
QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate,
|
||||||
|
Qt::SingleShotConnection);
|
||||||
|
} else if (!m_sourceItem->window()->isVisible()) {
|
||||||
|
QObject::connect(m_sourceItem->window(), &QQuickWindow::visibleChanged, this, &ImageAnalyser::requestUpdate,
|
||||||
|
Qt::SingleShotConnection);
|
||||||
|
}
|
||||||
|
if (m_sourceItem->width() <= 0) {
|
||||||
|
QObject::connect(
|
||||||
|
m_sourceItem, &QQuickItem::widthChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection);
|
||||||
|
}
|
||||||
|
if (m_sourceItem->height() <= 0) {
|
||||||
|
QObject::connect(m_sourceItem, &QQuickItem::heightChanged, this, &ImageAnalyser::requestUpdate,
|
||||||
|
Qt::SingleShotConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAnalyser::update() {
|
||||||
|
if (m_source.isEmpty() && !m_sourceItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_futureWatcher->isRunning()) {
|
||||||
|
m_futureWatcher->cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_sourceItem) {
|
||||||
|
const QSharedPointer<const QQuickItemGrabResult> grabResult = m_sourceItem->grabToImage();
|
||||||
|
QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() {
|
||||||
|
m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
m_futureWatcher->setFuture(QtConcurrent::run([=, this](QPromise<AnalyseResult>& promise) {
|
||||||
|
const QImage image(m_source);
|
||||||
|
analyse(promise, image, m_rescaleSize);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAnalyser::analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize) {
|
||||||
|
if (image.isNull()) {
|
||||||
|
qWarning() << "ImageAnalyser::analyse: image is null";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage img = image;
|
||||||
|
|
||||||
|
if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) {
|
||||||
|
img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promise.isCanceled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (img.format() != QImage::Format_ARGB32) {
|
||||||
|
img = img.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promise.isCanceled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uchar* data = img.bits();
|
||||||
|
const int width = img.width();
|
||||||
|
const int height = img.height();
|
||||||
|
const qsizetype bytesPerLine = img.bytesPerLine();
|
||||||
|
|
||||||
|
std::unordered_map<quint32, int> colours;
|
||||||
|
qreal totalLuminance = 0.0;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
const uchar* line = data + y * bytesPerLine;
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
if (promise.isCanceled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uchar* pixel = line + x * 4;
|
||||||
|
|
||||||
|
if (pixel[3] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quint32 mr = static_cast<quint32>(pixel[0] & 0xF8);
|
||||||
|
const quint32 mg = static_cast<quint32>(pixel[1] & 0xF8);
|
||||||
|
const quint32 mb = static_cast<quint32>(pixel[2] & 0xF8);
|
||||||
|
++colours[(mr << 16) | (mg << 8) | mb];
|
||||||
|
|
||||||
|
const qreal r = pixel[0] / 255.0;
|
||||||
|
const qreal g = pixel[1] / 255.0;
|
||||||
|
const qreal b = pixel[2] / 255.0;
|
||||||
|
totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b);
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quint32 dominantColour = 0;
|
||||||
|
int maxCount = 0;
|
||||||
|
for (const auto& [colour, colourCount] : colours) {
|
||||||
|
if (promise.isCanceled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colourCount > maxCount) {
|
||||||
|
dominantColour = colour;
|
||||||
|
maxCount = colourCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.addResult(qMakePair(QColor((0xFFu << 24) | dominantColour), count == 0 ? 0.0 : totalLuminance / count));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia
|
||||||
61
plugin/src/Caelestia/imageanalyser.hpp
Normal file
61
plugin/src/Caelestia/imageanalyser.hpp
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QtQuick/qquickitem.h>
|
||||||
|
#include <qfuture.h>
|
||||||
|
#include <qfuturewatcher.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
namespace caelestia {
|
||||||
|
|
||||||
|
class ImageAnalyser : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(QQuickItem* sourceItem READ sourceItem WRITE setSourceItem NOTIFY sourceItemChanged)
|
||||||
|
Q_PROPERTY(int rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged)
|
||||||
|
Q_PROPERTY(QColor dominantColour READ dominantColour NOTIFY dominantColourChanged)
|
||||||
|
Q_PROPERTY(qreal luminance READ luminance NOTIFY luminanceChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ImageAnalyser(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QString source() const;
|
||||||
|
void setSource(const QString& source);
|
||||||
|
|
||||||
|
[[nodiscard]] QQuickItem* sourceItem() const;
|
||||||
|
void setSourceItem(QQuickItem* sourceItem);
|
||||||
|
|
||||||
|
[[nodiscard]] int rescaleSize() const;
|
||||||
|
void setRescaleSize(int rescaleSize);
|
||||||
|
|
||||||
|
[[nodiscard]] QColor dominantColour() const;
|
||||||
|
[[nodiscard]] qreal luminance() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void requestUpdate();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void sourceChanged();
|
||||||
|
void sourceItemChanged();
|
||||||
|
void rescaleSizeChanged();
|
||||||
|
void dominantColourChanged();
|
||||||
|
void luminanceChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
using AnalyseResult = QPair<QColor, qreal>;
|
||||||
|
|
||||||
|
QFutureWatcher<AnalyseResult>* const m_futureWatcher;
|
||||||
|
|
||||||
|
QString m_source;
|
||||||
|
QQuickItem* m_sourceItem;
|
||||||
|
int m_rescaleSize;
|
||||||
|
|
||||||
|
QColor m_dominantColour;
|
||||||
|
qreal m_luminance;
|
||||||
|
|
||||||
|
void update();
|
||||||
|
static void analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia
|
||||||
|
|
@ -22,7 +22,7 @@ Singleton {
|
||||||
readonly property M3Palette current: M3Palette {}
|
readonly property M3Palette current: M3Palette {}
|
||||||
readonly property M3Palette preview: M3Palette {}
|
readonly property M3Palette preview: M3Palette {}
|
||||||
readonly property Transparency transparency: Transparency {}
|
readonly property Transparency transparency: Transparency {}
|
||||||
property real wallLuminance
|
readonly property alias wallLuminance: analyser.luminance
|
||||||
|
|
||||||
function getLuminance(c: color): real {
|
function getLuminance(c: color): real {
|
||||||
if (c.r == 0 && c.g == 0 && c.b == 0)
|
if (c.r == 0 && c.g == 0 && c.b == 0)
|
||||||
|
|
@ -85,16 +85,10 @@ Singleton {
|
||||||
onLoaded: root.load(text(), false)
|
onLoaded: root.load(text(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
ImageAnalyser {
|
||||||
target: Wallpapers
|
id: analyser
|
||||||
|
|
||||||
function onCurrentChanged(): void {
|
source: Wallpapers.current
|
||||||
const current = Wallpapers.current;
|
|
||||||
CUtils.getAverageLuminance(current, l => {
|
|
||||||
if (Wallpapers.current == current)
|
|
||||||
root.wallLuminance = l;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component Transparency: QtObject {
|
component Transparency: QtObject {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue