Skip to content

Commit 509b980

Browse files
committed
core/colorquant: add ColorQuantizer
1 parent 9506c1b commit 509b980

File tree

4 files changed

+373
-0
lines changed

4 files changed

+373
-0
lines changed

src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ qt_add_library(quickshell-core STATIC
3737
common.cpp
3838
iconprovider.cpp
3939
scriptmodel.cpp
40+
colorquantizer.cpp
4041
)
4142

4243
qt_add_qml_module(quickshell-core

src/core/colorquantizer.cpp

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#include "colorquantizer.hpp"
2+
#include <algorithm>
3+
4+
#include <qatomic.h>
5+
#include <qcolor.h>
6+
#include <qdatetime.h>
7+
#include <qimage.h>
8+
#include <qlist.h>
9+
#include <qlogging.h>
10+
#include <qloggingcategory.h>
11+
#include <qminmax.h>
12+
#include <qnamespace.h>
13+
#include <qnumeric.h>
14+
#include <qobject.h>
15+
#include <qqmllist.h>
16+
#include <qrgb.h>
17+
#include <qthreadpool.h>
18+
#include <qtmetamacros.h>
19+
#include <qtypes.h>
20+
21+
namespace {
22+
Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
23+
}
24+
25+
ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
26+
: source(source)
27+
, maxDepth(depth)
28+
, rescaleSize(rescaleSize) {
29+
setAutoDelete(false);
30+
}
31+
32+
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
33+
if (shouldCancel.loadAcquire() || source->isEmpty()) return;
34+
35+
colors.clear();
36+
37+
auto image = QImage(source->toLocalFile());
38+
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
39+
image = image.scaled(
40+
static_cast<int>(rescaleSize),
41+
static_cast<int>(rescaleSize),
42+
Qt::KeepAspectRatio,
43+
Qt::SmoothTransformation
44+
);
45+
}
46+
47+
if (image.isNull()) {
48+
qCWarning(logColorQuantizer) << "Failed to load image from" << source;
49+
return;
50+
}
51+
52+
QList<QColor> pixels;
53+
for (int y = 0; y != image.height(); ++y) {
54+
for (int x = 0; x != image.width(); ++x) {
55+
auto pixel = image.pixel(x, y);
56+
if (qAlpha(pixel) == 0) continue;
57+
58+
pixels.append(QColor::fromRgb(pixel));
59+
}
60+
}
61+
62+
auto startTime = QDateTime::currentDateTime();
63+
64+
colors = quantization(pixels, 0);
65+
66+
auto endTime = QDateTime::currentDateTime();
67+
auto milliseconds = startTime.msecsTo(endTime);
68+
qCDebug(logColorQuantizer) << "Color Quantization took: " << milliseconds << "ms";
69+
}
70+
71+
QList<QColor> ColorQuantizerOperation::quantization(
72+
QList<QColor>& rgbValues,
73+
qreal depth,
74+
const QAtomicInteger<bool>& shouldCancel
75+
) {
76+
if (shouldCancel.loadAcquire()) return QList<QColor>();
77+
78+
if (depth >= maxDepth || rgbValues.isEmpty()) {
79+
if (rgbValues.isEmpty()) return QList<QColor>();
80+
81+
auto totalR = 0;
82+
auto totalG = 0;
83+
auto totalB = 0;
84+
85+
for (const auto& color: rgbValues) {
86+
if (shouldCancel.loadAcquire()) return QList<QColor>();
87+
88+
totalR += color.red();
89+
totalG += color.green();
90+
totalB += color.blue();
91+
}
92+
93+
auto avgColor = QColor(
94+
qRound(totalR / static_cast<double>(rgbValues.size())),
95+
qRound(totalG / static_cast<double>(rgbValues.size())),
96+
qRound(totalB / static_cast<double>(rgbValues.size()))
97+
);
98+
99+
return QList<QColor>() << avgColor;
100+
}
101+
102+
auto dominantChannel = findBiggestColorRange(rgbValues);
103+
std::ranges::sort(rgbValues, [dominantChannel](const auto& a, const auto& b) {
104+
if (dominantChannel == 'r') return a.red() < b.red();
105+
else if (dominantChannel == 'g') return a.green() < b.green();
106+
return a.blue() < b.blue();
107+
});
108+
109+
auto mid = rgbValues.size() / 2;
110+
111+
auto leftHalf = rgbValues.mid(0, mid);
112+
auto rightHalf = rgbValues.mid(mid);
113+
114+
QList<QColor> result;
115+
result.append(quantization(leftHalf, depth + 1));
116+
result.append(quantization(rightHalf, depth + 1));
117+
118+
return result;
119+
}
120+
121+
char ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) {
122+
if (rgbValues.isEmpty()) return 'r';
123+
124+
auto rMin = 255;
125+
auto gMin = 255;
126+
auto bMin = 255;
127+
auto rMax = 0;
128+
auto gMax = 0;
129+
auto bMax = 0;
130+
131+
for (const auto& color: rgbValues) {
132+
rMin = qMin(rMin, color.red());
133+
gMin = qMin(gMin, color.green());
134+
bMin = qMin(bMin, color.blue());
135+
136+
rMax = qMax(rMax, color.red());
137+
gMax = qMax(gMax, color.green());
138+
bMax = qMax(bMax, color.blue());
139+
}
140+
141+
auto rRange = rMax - rMin;
142+
auto gRange = gMax - gMin;
143+
auto bRange = bMax - bMin;
144+
145+
auto biggestRange = qMax(rRange, qMax(gRange, bRange));
146+
if (biggestRange == rRange) {
147+
return 'r';
148+
} else if (biggestRange == gRange) {
149+
return 'g';
150+
} else {
151+
return 'b';
152+
}
153+
}
154+
155+
void ColorQuantizerOperation::finishRun() {
156+
QMetaObject::invokeMethod(this, &ColorQuantizerOperation::finished, Qt::QueuedConnection);
157+
}
158+
159+
void ColorQuantizerOperation::finished() {
160+
emit this->done(colors);
161+
delete this;
162+
}
163+
164+
void ColorQuantizerOperation::run() {
165+
if (!this->shouldCancel) {
166+
this->quantizeImage();
167+
168+
if (this->shouldCancel.loadAcquire()) {
169+
qCDebug(logColorQuantizer) << "Color quantization" << this << "cancelled";
170+
}
171+
}
172+
173+
this->finishRun();
174+
}
175+
176+
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
177+
178+
void ColorQuantizer::componentComplete() {
179+
componentCompleted = true;
180+
if (!mSource.isEmpty()) quantizeAsync();
181+
}
182+
183+
void ColorQuantizer::setSource(const QUrl& source) {
184+
if (mSource != source) {
185+
mSource = source;
186+
emit this->sourceChanged();
187+
188+
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
189+
}
190+
}
191+
192+
void ColorQuantizer::setDepth(qreal depth) {
193+
if (mDepth != depth) {
194+
mDepth = depth;
195+
emit this->depthChanged();
196+
197+
if (this->componentCompleted) quantizeAsync();
198+
}
199+
}
200+
201+
void ColorQuantizer::setRescaleSize(int rescaleSize) {
202+
if (mRescaleSize != rescaleSize) {
203+
mRescaleSize = rescaleSize;
204+
emit this->rescaleSizeChanged();
205+
206+
if (this->componentCompleted) quantizeAsync();
207+
}
208+
}
209+
210+
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
211+
bColors = result;
212+
this->liveOperation = nullptr;
213+
emit this->colorsChanged();
214+
}
215+
216+
void ColorQuantizer::quantizeAsync() {
217+
if (this->liveOperation) this->cancelAsync();
218+
219+
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
220+
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
221+
222+
QObject::connect(
223+
this->liveOperation,
224+
&ColorQuantizerOperation::done,
225+
this,
226+
&ColorQuantizer::operationFinished
227+
);
228+
229+
QThreadPool::globalInstance()->start(this->liveOperation);
230+
}
231+
232+
void ColorQuantizer::cancelAsync() {
233+
if (!this->liveOperation) return;
234+
235+
this->liveOperation->tryCancel();
236+
QThreadPool::globalInstance()->waitForDone();
237+
238+
QObject::disconnect(this->liveOperation, nullptr, this, nullptr);
239+
this->liveOperation = nullptr;
240+
//
241+
}

src/core/colorquantizer.hpp

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#pragma once
2+
3+
#include <qlist.h>
4+
#include <qobject.h>
5+
#include <qproperty.h>
6+
#include <qqmlintegration.h>
7+
#include <qqmlparserstatus.h>
8+
#include <qrunnable.h>
9+
#include <qtmetamacros.h>
10+
#include <qtypes.h>
11+
#include <qurl.h>
12+
13+
class ColorQuantizerOperation
14+
: public QObject
15+
, public QRunnable {
16+
Q_OBJECT;
17+
18+
public:
19+
explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
20+
21+
void run() override;
22+
void tryCancel();
23+
24+
signals:
25+
void done(QList<QColor> colors);
26+
27+
private slots:
28+
void finished();
29+
30+
private:
31+
static char findBiggestColorRange(const QList<QColor>& rgbValues);
32+
33+
void quantizeImage(const QAtomicInteger<bool>& shouldCancel = false);
34+
35+
QList<QColor> quantization(
36+
QList<QColor>& rgbValues,
37+
qreal depth,
38+
const QAtomicInteger<bool>& shouldCancel = false
39+
);
40+
41+
void finishRun();
42+
43+
QAtomicInteger<bool> shouldCancel = false;
44+
QList<QColor> colors;
45+
QUrl* source;
46+
qreal maxDepth;
47+
qreal rescaleSize;
48+
};
49+
50+
///! Color Quantization Utility
51+
/// A color quantization utility used for getting prevalent colors in an image, by
52+
/// averaging out the image's color data recursively.
53+
///
54+
/// #### Example
55+
/// ```qml
56+
/// ColorQuantizer {
57+
/// id: colorQuantizer
58+
/// source: Qt.resolvedUrl("./yourImage.png")
59+
/// depth: 3 // Will produce 8 colors (2³)
60+
/// rescaleSize: 64 // Rescale to 64x64 for faster processing
61+
/// }
62+
/// ```
63+
class ColorQuantizer
64+
: public QObject
65+
, public QQmlParserStatus {
66+
67+
Q_OBJECT;
68+
QML_ELEMENT;
69+
Q_INTERFACES(QQmlParserStatus);
70+
/// Access the colors resulting from the color quantization performed.
71+
/// > [!NOTE] The amount of colors returned from the quantization is determined by
72+
/// > the property depth, specifically 2ⁿ where n is the depth.
73+
Q_PROPERTY(QList<QColor> colors READ default NOTIFY colorsChanged BINDABLE bindableColors);
74+
75+
/// Path to the image you'd like to run the color quantization on.
76+
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged);
77+
78+
/// Max depth for the color quantization. Each level of depth represents another
79+
/// binary split of the color space
80+
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
81+
82+
/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
83+
/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
84+
/// > reccommended to rescale, otherwise the quantization process will take much longer.
85+
Q_PROPERTY(qreal rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged);
86+
87+
public:
88+
explicit ColorQuantizer(QObject* parent = nullptr): QObject(parent) {}
89+
90+
void componentComplete() override;
91+
void classBegin() override {}
92+
93+
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
94+
95+
[[nodiscard]] QUrl source() const { return mSource; }
96+
void setSource(const QUrl& source);
97+
98+
[[nodiscard]] qreal depth() const { return mDepth; }
99+
void setDepth(qreal depth);
100+
101+
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
102+
void setRescaleSize(int rescaleSize);
103+
104+
signals:
105+
void colorsChanged();
106+
void sourceChanged();
107+
void depthChanged();
108+
void rescaleSizeChanged();
109+
110+
public slots:
111+
void operationFinished(const QList<QColor>& result);
112+
113+
private:
114+
void quantizeAsync();
115+
void cancelAsync();
116+
117+
bool componentCompleted = false;
118+
ColorQuantizerOperation* liveOperation = nullptr;
119+
QUrl mSource;
120+
qreal mDepth = 0;
121+
qreal mRescaleSize = 0;
122+
123+
Q_OBJECT_BINDABLE_PROPERTY(
124+
ColorQuantizer,
125+
QList<QColor>,
126+
bColors,
127+
&ColorQuantizer::colorsChanged
128+
);
129+
///
130+
};

src/core/module.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ headers = [
2929
"qsmenuanchor.hpp",
3030
"clock.hpp",
3131
"scriptmodel.hpp",
32+
"colorquantizer.hpp",
3233
]
3334
-----

0 commit comments

Comments
 (0)