Related
I'm following this series on youtube Node Editor. I'm attempting to learn both Qt and C++ at the same time, possibly a stupid idea, but "hello world" tutorials don't do much for me.
Episode 1 We are to create a grid using the drawBackground method, however before this in the class constructor we have set the background to a light grey colour. When I run the code without the drawBackground the scene shows the light grey background. However when I run it with the drawBackground method uncommented the grid is drawn but the background is now white.
Why does the drawBackground() method override the QDMGraphicsScene() setBackgroundBrush?
qdmgraphicsscene.h
#ifndef QDMGRAPHICSSCENE_H
#define QDMGRAPHICSSCENE_H
#include <QGraphicsScene>
#include <QtGui>
#include <QtCore>
#include "math.h"
class QDMGraphicsScene : public QGraphicsScene
{
public:
QDMGraphicsScene();
//Settings
QColor _colour_background, _colour_light, _colour_dark;
QPen _pen_light, _pen_dark;
int scene_width, scene_height, gridSize, gridSquares;
QVector<QLine> lines_light, lines_dark;
void drawBackground(QPainter *painter, const QRectF &rect);
};
#endif // QDMGRAPHICSSCENE_H
qdmgraphicsscene.cpp
#include "qdmgraphicsscene.h"
QDMGraphicsScene::QDMGraphicsScene()
{
this->gridSize = 60;
this->gridSquares = 3;
this->_colour_background = QColor(57,57,57);
this->_colour_light = QColor(47,47,47);
this->_colour_dark = QColor(41, 41, 41);
this->_pen_light = QPen(this->_colour_light);
this->_pen_light.setWidth(1);
this->_pen_dark = QPen(this->_colour_dark);
this->_pen_dark.setWidth(2);
this->scene_width = 64000;
this->scene_height = 64000;
this->setSceneRect(-this->scene_width/2, -this->scene_height/2, this->scene_width, this->scene_height);
this->setBackgroundBrush(this->_colour_background);
}
void QDMGraphicsScene::drawBackground(QPainter *painter, const QRectF &rect)
{
//create grid
int left = (int)floor(rect.left());
int right = (int)ceil(rect.right());
int top = (int)floor(rect.top());
int bottom = (int)ceil(rect.bottom());
int first_left = left - (left % this->gridSize);
int first_top = top - (top % this->gridSize);
//compute lines
for(int x = first_left; x < right; x=x+this->gridSize){
if(x % (this->gridSize * this->gridSquares) != 0){
this->lines_light.append(QLine(x,top,x,bottom));
} else {
this->lines_dark.append(QLine(x,top,x,bottom));
}
}
for(int y = first_top; y < bottom; y=y+this->gridSize){
if(y % (this->gridSize * this->gridSquares) != 0){
this->lines_light.append(QLine(left,y,right,y));
} else {
this->lines_dark.append(QLine(left,y,right,y));
}
}
//draw lines
painter->setPen(this->_pen_light);
painter->drawLines(this->lines_light);
painter->setPen(this->_pen_dark);
painter->drawLines(this->lines_dark);
}
I suspect the problem is simply that your drawBackground override doesn't use the background brush -- you just set the QPainter pen and use it to draw lines.
I think the simplest solution would be to call the base class implementation of drawBackground at the top of your own, so...
void QDMGraphicsScene::drawBackground (QPainter *painter, const QRectF &rect)
{
/*
* Call base class drawBackground implementation to draw background.
*/
QGraphicsScene::drawBackground(painter, rect);
//create grid
.
.
.
I have QVector m_vertices and QVector m_lines in my drawingWidget.h
I currently trying to do simple drawing tool, where i can manipulate with vertices and lines. I managed to draw multiple vertices on my "MainWindow", now i am trying to implement line between two vertices
The main idea is, that i have a pop-up menu, where i can choose a "tool". I can add vertex, remove vertex, move vertex, add line, delete line. The idea is , when i choose for example "Add Vertex" then the "m_state" will change to "Adding" so i can only add vertices. When i choose "Add Line" for example, then the "m_state" again will change, so that i can only choose two created vertices, that will result a line between them.
enum DrawingWidgetState {
NO_TOOL_SELECTED,
ADD_VERTEX_SELECTED,
MOVE_VERTEX_SELECTED,
DELETE_VERTEX_SELECTED,
ADD_LINE_SELECTED,
DELETE_LINE_SELECTED
};
Here, in the end, it turns out that if I click "Add Vertex" then when I put the second vertex, then it automatically puts a line between them
void DrawingWidget::paintEvent(QPaintEvent *event) {
QPainter painter(this);
painter.fillRect(event->rect(), Qt::blue);
painter.setBrush(Qt::black);
for(int i = 0; i < m_vertices.size() ; i++) {
painter.drawEllipse(m_vertices[i], 20, 20);
}
for(int i = 0; i < m_vertices.size()-1 ; i++)
{
const QPoint& point1 = m_vertices[i];
const QPoint& point2 = m_vertices[i+1];
painter.drawLine(point1, point2);
}
}
void DrawingWidget::mousePressEvent(QMouseEvent *event) {
if(m_state == ADD_VERTEX_SELECTED) {
if(event->button() == Qt::LeftButton) {
//m_x = event->x();
//m_y = event->y();
//update();
QPoint point = event->pos();
m_vertices.append(point);
update();
}
}
if(m_state == ADD_LINE_SELECTED) {
if(event->button() == Qt::LeftButton) {
for(int i = 0; i < m_vertices.size()-1 ; i++) {
const auto &point1 = m_vertices[i];
const auto &point2 = m_vertices[i+1];
}
}
}
}
Could someone explain to me why this happens and what should i do to improve my code ?
If something requires explanation, i will tell.
I'm trying to render individual tiles from a tileset. For example, I want to display the grey tile in the tileset below:
In the real use case, these would be e.g. water, grass, etc. tiles in a game. There are some requirements for rendering these tiles:
They are 32x32 pixels and will be rendered fullscreen, so performance is important.
They should not be smoothed when scaled.
None of the built-in Qt Quick types meet these requirements (rendering a section of an image that's not smoothed), as far as I can tell. I've tried QQuickPaintedItem with various QPainter render hints (such as SmoothPixmapTransform set to false) without success; the image is "blurry" when upscaled. AnimatedSprite supports rendering sections of an image, but has no API to disable smoothing.
My idea was to implement a custom QQuickItem using the scene graph API.
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickItem>
#include <QQuickWindow>
#include <QSGImageNode>
static QImage image;
static const int tileSize = 32;
static const int tilesetSize = 8;
class Tile : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged)
public:
Tile() :
mIndex(-1) {
setWidth(tileSize);
setHeight(tileSize);
setFlag(QQuickItem::ItemHasContents);
}
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
if (!oldNode) {
oldNode = window()->createImageNode();
}
if (mIndex == -1)
return oldNode;
if (image.isNull()) {
image = QImage("C:/tileset.png");
if (image.isNull())
return oldNode;
}
QSGTexture *texture = window()->createTextureFromImage(image);
qDebug() << "textureSize:" << texture->textureSize();
if (!texture)
return oldNode;
QSGImageNode *imageNode = static_cast<QSGImageNode*>(oldNode);
// imageNode->setOwnsTexture(true);
imageNode->setTexture(texture);
qDebug() << "source rect:" << (mIndex % tileSize) * tileSize << (mIndex / tileSize) * tileSize << tileSize << tileSize;
imageNode->setSourceRect((mIndex % tileSize) * tileSize, (mIndex / tileSize) * tileSize, tileSize, tileSize);
return oldNode;
}
int index() const {
return mIndex;
}
void setIndex(int index) {
if (index == mIndex)
return;
mIndex = index;
emit indexChanged();
}
signals:
void indexChanged();
private:
int mIndex;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<Tile>("App", 1, 0, "Tile");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.9
import QtQuick.Controls 2.2
import App 1.0
ApplicationWindow {
id: window
width: 800
height: 800
visible: true
Slider {
id: slider
from: 1
to: 10
}
Tile {
scale: slider.value
index: 1
anchors.centerIn: parent
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "darkorange"
}
}
}
The output from this application looks fine, but nothing is rendered within the rectangle:
textureSize: QSize(256, 256)
source rect: 32 0 32 32
Judging from the minimal docs, my implementation (in terms of how I create nodes) seems OK. Where am I going wrong?
A year late to the party, but I was running into the same problem as you. For the sake of anyone else who is trying to subclass a QQuickItem and has come across this thread, there's a little nugget that's in the documentation in regards to updatePaintNode:
The function is called as a result of QQuickItem::update(),
if the user has set the QQuickItem::ItemHasContents flag on the item.
When I set that flag, everything rendered.
And I considered myself a detail-oriented person...
EDIT:
After the OP pointed out they had already set the ItemHasContents flag, I looked at the code again and saw that while the OP had set the sourceRect on the node, the OP hadn't set the rect of the node, and that indeed was the problem the OP was running into.
I ended up going with a friend's idea of using QQuickImageProvider:
tileimageprovider.h:
#ifndef TILEIMAGEPROVIDER_H
#define TILEIMAGEPROVIDER_H
#include <QQuickImageProvider>
#include <QHash>
#include <QString>
#include <QImage>
class TileImageProvider : public QQuickImageProvider
{
public:
TileImageProvider();
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
private:
QHash<QString, QImage> mTiles;
};
#endif // TILEIMAGEPROVIDER_H
tileimageprovider.cpp:
#include "tileimageprovider.h"
#include <QImage>
#include <QDebug>
TileImageProvider::TileImageProvider() :
QQuickImageProvider(QQmlImageProviderBase::Image)
{
QImage tilesetImage(":/sprites/tiles/tileset.png");
if (tilesetImage.isNull()) {
qWarning() << "Failed to load tileset image";
return;
}
int index = 0;
for (int row = 0; row < 8; ++row) {
for (int col = 0; col < 8; ++col) {
int sourceX = col * 32;
int sourceY = row * 32;
QImage subTile = tilesetImage.copy(sourceX, sourceY, 32, 32);
if (tilesetImage.isNull()) {
qWarning() << "Tile image at" << sourceX << sourceY << "is null";
return;
}
mTiles.insert(QString::number(index++), subTile);
}
}
}
QImage TileImageProvider::requestImage(const QString &id, QSize *size, const QSize &)
{
Q_ASSERT(mTiles.find(id) != mTiles.end());
*size = QSize(32, 32);
return mTiles.value(id);
}
I then create tile instances from the following Component:
Image {
width: 32
height: 32
smooth: false
asynchronous: true
source: "image://tile/" + index
property int index
}
There's also Tiled's tile rendering code, which uses scenegraph nodes and is likely more efficient.
It does seem like you can have it both easier and more efficient.
Instead of implementing a custom QQuickItem you can just use a trivial ShaderEffect. You can pass an offset and control the sampling.
Additionally, you would only need one single black and white plus, and you can have the color dynamic by passing it as a parameter to the shader.
Lastly, doing an atlas yourself is likely redundant, as the scenegraph will likely put small textures in atlases anyway. And of course, there is the advantage of not having to write a single line of C++, while still getting an efficient and flexible solution.
I'm trying to make an horizontal layout with 3 QLabel scale use all its available space. More specifically, this is what I have
this is what I am aiming for
At the moment, the second image is achieved by changing the stylesheet of the qlabels with a slider. Additionally, since I have the the three labels in a layout inside a groupbox, the groupbox resizes to fit its contents, cool.
Now I wanted to drop the slider approach and instead autofit the space available when moving the splitters. In this question, OP reimplements the resizeEvent, and I've seen other posts suggesting the same, changing point by point with this while( !doesFit ) or something similar.
I tried using this approach, both on the resize event and on the splitterMoved event. However, this approach is way prone to feedback loops and other display errors caused. In the other question, they suggest enabling ignoreSizePolicy to prevent the size policy retriggering the sizeevent, but I like how qt handles the size of the layout, how it keeps a minimum size and then it folds the widget if the user insists. Maybe it would work if the HLayout would ignore the resize events triggered by the QLabels, still IMHO unclean thought.
I was wondering if that's the recommended way of achieving this, and wether a less unstable solution exists, maybe using using stylesheets. There are some behaviours that I could also drop, the minimum size limit (so the user could potentially hide the groupbox).
If that's the recommended way of doing it, how should I use the fontmetrics if I have three separate labels, one of which (the number) changes its text dynamically and rapidly? It should not have an impact on performance, and that while loop makes me wary.
It doesn't sound like the while(!fit) approach is going to cut it. Or does it?
--- Edit regarding the duplicate question
Another post creates an event filter, which might also work if reworked to deal with a layout with 3 labels. I finally used a version of the first mentioned post with the variation of the post mentioned in the comments. I'll post the answer if the question is reopened.
One could apply the Newton's method approach from this answer to work on all widgets in a given layout. It will work on any widget with a settable font, not only on a QLabel.
The Newton's algorithm converges reasonably quickly when given a good starting point, e.g. when resizing interactively. It's not atypical to have the loop execute only once. On the other hand, QWidget::sizeHint is integer-valued and and widgets may round fractional font sizes, thus sometimes the iteration is a bit slower than one would expect. The number of iterations is capped to ensure decent performance.
A custom replacement for the label, that provided a QSizeF sizeHintF(), would work better here.
The minimum sizing for the widgets is a bit of a stretch, as the size is not updated as the widget contents change. This could be remedied easily, though.
// https://github.com/KubaO/stackoverflown/tree/master/questions/label-text-size-vert-40861305
#include <QtWidgets>
class LabelStretcher : public QObject {
Q_OBJECT
static constexpr const char kMinimumsAcquired[] = "ls_minimumsAcquired";
static constexpr const char kStretcherManaged[] = "ls_stretcherManaged";
public:
LabelStretcher(QObject *parent = 0) : QObject(parent) {
apply(qobject_cast<QWidget*>(parent));
}
void apply(QWidget *widget) {
if (!widget) return;
setManaged(widget);
setMinimumSize(widget);
widget->installEventFilter(this);
}
void setManaged(QWidget *w, bool managed = true) {
w->setProperty(kStretcherManaged, managed);
}
protected:
bool eventFilter(QObject * obj, QEvent * ev) override {
auto widget = qobject_cast<QWidget*>(obj);
if (widget && ev->type() == QEvent::Resize)
resized(widget);
return false;
}
private:
void onLayout(QLayout *layout, const std::function<void(QWidget*)> &onWidget) {
if (!layout) return;
auto N = layout->count();
for (int i = 0; i < N; ++i) {
auto item = layout->itemAt(i);
onWidget(item->widget());
onLayout(item->layout(), onWidget);
}
}
void setFont(QLayout *layout, const QFont &font) {
onLayout(layout, [&](QWidget *widget){ setFont(widget, font); });
}
void setFont(QWidget *widget, const QFont &font) {
if (!widget || !widget->property(kStretcherManaged).toBool()) return;
widget->setFont(font);
setFont(widget->layout(), font);
}
void setMinimumSize(QWidget *widget) {
if (widget->layout()) return;
widget->setMinimumSize(widget->minimumSizeHint());
}
static int dSize(const QSizeF & inner, const QSizeF & outer) {
auto dy = inner.height() - outer.height();
auto dx = inner.width() - outer.width();
return std::max(dx, dy);
}
qreal f(qreal fontSize, QWidget *widget) {
auto font = widget->font();
font.setPointSizeF(fontSize);
setFont(widget, font);
auto d = dSize(widget->sizeHint(), widget->size());
qDebug() << "f:" << fontSize << "d" << d;
return d;
}
qreal df(qreal fontSize, qreal dStep, QWidget *widget) {
fontSize = std::max(dStep + 1.0, fontSize);
return (f(fontSize + dStep, widget) - f(fontSize - dStep, widget)) / dStep;
}
void resized(QWidget *widget) {
qDebug() << "pre: " << widget->minimumSizeHint() << widget->sizeHint() << widget->size();
if (!widget->property(kMinimumsAcquired).toBool()) {
onLayout(widget->layout(), [=](QWidget *widget){ setMinimumSize(widget); });
widget->setProperty(kMinimumsAcquired, true);
}
// Newton's method
auto font = widget->font();
auto fontSize = font.pointSizeF();
qreal dStep = 1.0;
int i;
for (i = 0; i < 10; ++i) {
auto prevFontSize = fontSize;
auto d = df(fontSize, dStep, widget);
if (d == 0) {
dStep *= 2.0;
continue;
}
fontSize -= f(fontSize, widget)/d;
fontSize = std::max(dStep + 1.0, fontSize);
auto change = fabs(prevFontSize - fontSize)/fontSize;
qDebug() << "d:" << d << " delta" << change;
if (change < 0.01) break; // we're within 1% of target
}
font.setPointSizeF(fontSize);
setFont(widget, font);
qDebug() << "post:" << i << widget->minimumSizeHint() << widget->sizeHint() << widget->size();
}
};
constexpr const char LabelStretcher::kMinimumsAcquired[];
constexpr const char LabelStretcher::kStretcherManaged[];
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QWidget w;
QGridLayout layout{&w};
LabelStretcher stretch{&w};
QLabel labels[6];
QString texts[6] = {"V", "30.0", "kts", "H", "400.0", "ft"};
int i = 0, j = 0, k = 0;
for (auto & label : labels) {
stretch.setManaged(&label);
label.setFrameStyle(QFrame::Box);
label.setText(texts[k++]);
if (j == 0) label.setAlignment(Qt::AlignRight | Qt::AlignVCenter);
else if (j == 1) label.setAlignment(Qt::AlignCenter);
layout.addWidget(&label, i, j++);
if (j >= 3) { i++; j=0; }
}
w.show();
return app.exec();
}
#include "main.moc"
Althought I consider KubaOber's answer better, I'll post this in case it's helpful to someone who wants a solution in the line of the answers mentioned in the post.
Note that the sampletext could be retrieved from the labels as well, the font from the stylesheet, and the code could potentially be placed on a resizeEvent of the groupbox or layout. It wouldn't work on the resizeEvent of the labels since they would compete for the space.
That is one reason why KubaOber answer is superior. Other reasons I can think of is stability given that the 3 labels space differs from the sampletext, thus the font size is not as accurate as it could be. Therefore a resize event could potentially be triggered again by the font change.
static void fitGroupBoxLabels(QGroupBox* groupbox, const QFont &samplefont, const QLayout* const samplelayout)
{
groupbox->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
QString sampletext = "V 1000.0 kts";
QRect availablerect = samplelayout->contentsRect();
if(samplefont.pointSizeF() <= 0) return; //not initalized yet, return
QRect textrect = QFontMetrics(samplefont).boundingRect(sampletext);
if(!textrect.isValid() || !availablerect.isValid()) return; //not initalized yet, return
float factorh = availablerect.width() / (float)textrect.width();
float factorw = availablerect.height() / (float)textrect.height();
float factor = std::min(factorh, factorw);
if (factor < 0.95 || factor > 1.05)
{
float fontSize = samplefont.pointSizeF()*factor;
QString groupBoxStyle = QString("QGroupBox{font-size:8pt} QLabel{font-size:%1pt}").arg(fontSize);
groupbox->setStyleSheet(groupBoxStyle);
}
}
After struggling with this issue, I create DynamicFontSizeLabel and DynamicFontSizePushButton widgets. Hope it helps.
https://github.com/jonaias/DynamicFontSizeWidgets/
This is the output of what I have done till now:
Although this shows only black and white images, the code works for color images too. The code basically populates the larger image using smaller images from a small database.
So this is where I am stuck, rather clueless.
How can I get an effect like this one. or this one.
Qn1 : I need to take another input(large) image*(One for which the effect is to be seen)* and merge them, but how?
Qn2 : How can I evaluate the goodness of the photo-mosaic? I have a genetic algorithm written for this but am unable to fix the fitness function,(mutation and crossover work perfectly).
This is what I could think of(for Qn1):
1. Take alternate pixels of the image shown above and the image for which the mosaic has to be made.
2. Take average of the pixel values of the above and input image for which the mosaic has to be made.
But have no clue to evaluate the goodness.
Below is a self contained sketch. The mosaicing algorithm is mid-way through the algorithms implemented in an excellent reference. It works well enough for two hours of work, I think. I tried for the code to be reasonably correct, with two caveats, left, as they say, as an exercise to the reader.
I'm not tracking the worker threads - if you try to exit the application while workers are active, it is expected to crash on exit. This is not nice, but otherwise benign and doesn't affect the overall functionality. There may be a few corrupt images left on disk, but those should be ignored when reloading.
There is no scaling of the image displayed in the label. The window will resize to the image size.
The tile image database can be filled with random images from imgur, you can also fill it with your own images by storing them on disk yourself. It's located in a standard application data path suffixed by /so-photomosaic/image. The fetched images are added there. Upon startup, the image database is repopulated from disk in the background - that's how your own tile images would be loaded. In fact, all of image processing is done in non-GUI threads. On a rather unassuming 5 year old Core 2 OS X system, disk image loading proceeds at about 5000 images/s. The images requested from imgur are their small size, or 90x90.
The tile matching is done with a 4x4 subdivision grid (divs parameter to calcPropsFor). The images are downsampled to a 4x4 mosaic, and the RGB color values of consecutive pixels in that grid are stored in Props vectors. The squared sums of differences of elements of those vectors are the measure of fit. For each tile to be replaced, the images are sorted according to their fit, and one of the best ones are picked up at random. The randomness parameter is a power-of-to of the sample size from which the image is randomly selected.
It uses Qt 5 and C++11. Length: 300 lines, out of which 64 are the random image source, 25 are the disk image database, and 88 are actually to do with mosaics. The image processing code would probably look and perform better if OpenCV or Eigen was used instead of valarray/QImage, but oh well.
Also, all of this would be probably 50 lines in Mathematica :)
# main.pro
# Make sure to re-run quake once this is set.
TEMPLATE = app
QT += widgets network concurrent
CONFIG += c++11
SOURCES += main.cpp
TARGET = photomosaic
#include <QApplication>
#include <QLabel>
#include <QSlider>
#include <QPushButton>
#include <QCheckBox>
#include <QBoxLayout>
#include <QFileDialog>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QRegularExpression>
#include <QImage>
#include <QPainter>
#include <QColor>
#include <QAtomicInt>
#include <QMutex>
#include <QtConcurrent>
#include <QStandardPaths>
#include <algorithm>
#include <functional>
#include <valarray>
/// Provides random images. There may be more than one response per request.
class RandomImageSource : public QObject {
Q_OBJECT
int m_parallelism;
bool m_auto;
QNetworkAccessManager m_mgr;
QSet<QNetworkReply*> m_replies;
QList<QUrl> m_deferred;
QRegularExpression m_imgTagRE, m_imgUrlRE;
QUrl m_randomGallery;
void get(const QUrl & url) {
if (m_replies.count() < m_parallelism) {
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
m_replies.insert(m_mgr.get(req));
} else
m_deferred << url;
}
void finishReply(QNetworkReply * reply) {
m_replies.remove(reply);
if (reply) reply->deleteLater();
if (! m_deferred.isEmpty()) get(m_deferred.takeLast());
while (m_deferred.isEmpty() && m_auto) get(m_randomGallery);
}
Q_SLOT void rsp(QNetworkReply * reply) {
auto loc = reply->header(QNetworkRequest::LocationHeader);
if (loc.isValid()) {
get(loc.toUrl()); // redirect
} else {
auto ct = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (ct.startsWith("text/html"))
foreach (QUrl url, parseImageUrls(reply->readAll()))
get(url);
else if (ct.startsWith("image")) {
auto img = QImage::fromData(reply->readAll());
img.setText("filename", m_imgUrlRE.match(reply->url().toString()).captured(1));
if (!img.isNull()) emit rspImage(img);
}
}
finishReply(reply);
}
QList<QUrl> parseImageUrls(const QByteArray & html) {
QList<QUrl> urls;
auto it = m_imgTagRE.globalMatch(QString::fromUtf8(html));
while (it.hasNext()) { auto match = it.next(); // get small images
urls << QUrl("http:" + match.captured(1) + "s" + match.captured(2)); }
return urls;
}
public:
RandomImageSource(QObject * parent = 0) : QObject (parent),
m_parallelism(20), m_auto(false),
m_imgTagRE("<img src=\"(//i\\.imgur\\.com/[^.]+)(\\.[^\"]+)\""),
m_imgUrlRE("http://i\\.imgur\\.com/(.+)$"),
m_randomGallery("http://imgur.com/gallery/random")
{
connect(&m_mgr, SIGNAL(finished(QNetworkReply*)), SLOT(rsp(QNetworkReply*)));
}
Q_SLOT void reqImages(int count) {
while (count--) get(m_randomGallery);
}
Q_SIGNAL void rspImage(const QImage &);
bool automatic() const { return m_auto; }
Q_SLOT void setAutomatic(bool a) { if ((m_auto = a)) finishReply(0); }
int parallelism() const { return m_parallelism; }
Q_SLOT void setParallelism(int p) { m_parallelism = p; if (m_auto) finishReply(0); }
};
/// Stores images on disk, and loads them in the background.
class ImageStorage : public QObject {
Q_OBJECT
QString const m_path;
public:
ImageStorage() :
m_path(QStandardPaths::writableLocation(QStandardPaths::DataLocation)
+ "/images/")
{ QDir().mkpath(m_path); }
Q_SLOT void addImage(const QImage & img) {
QString path = img.text("filename");
if (path.isEmpty()) return;
path.prepend(m_path);
QtConcurrent::run([img, path]{ img.save(path); });
}
Q_SLOT void retrieveAll() {
QString const path = m_path;
QtConcurrent::run([this, path] {
QStringList const images = QDir(path).entryList(QDir::Files);
foreach (QString image, images) QtConcurrent::run([this, image, path] {
QImage img; if (img.load(path + image)) emit retrieved(img);
});
});
}
Q_SIGNAL void retrieved(const QImage &);
};
/// A memory database of images. Finds best match to a given image.
class ImageDatabase : public QObject {
Q_OBJECT
typedef std::valarray<qreal> Props;
typedef QPair<QImage, Props> ImageProps;
QMutex mutable m_mutex;
QList<ImageProps> m_images;
static void inline addProps(Props & p, int i, QRgb rgb) {
QColor const c = QColor::fromRgb(rgb);
p[i+0] += c.redF(); p[i+1] += c.greenF(); p[i+2] += c.blueF();
}
static Props calcPropsFor(const QImage & img, int divs = 4) {
Props props(0.0, 3 * divs * divs);
std::valarray<int> counts(0, divs * divs);
QSize div = img.size() / divs;
for (int y = 0; y < img.height(); ++y)
for (int x = 0; x < img.width(); ++x) {
int slice = x/div.width() + (y*divs/div.height());
if (slice >= divs*divs) continue;
addProps(props, slice*3, img.pixel(x, y));
counts[slice] ++;
}
for (size_t i = 0; i < props.size(); ++i) props[i] /= counts[i/3];
return props;
}
public:
Q_SIGNAL void newImageCount(int);
Q_SLOT void addImage(const QImage & img) {
QtConcurrent::run([this, img]{
Props props = calcPropsFor(img);
QMutexLocker lock(&m_mutex);
m_images << qMakePair(img, props);
int count = m_images.count();
lock.unlock();
emit newImageCount(count);
});
}
ImageProps bestMatchFor(const QImage & img, int randLog2) const {
QMutexLocker lock(&m_mutex);
QList<ImageProps> const images = m_images;
lock.unlock();
Props const props = calcPropsFor(img);
typedef QPair<qreal, const ImageProps *> Match;
QList<Match> matches; matches.reserve(images.size());
std::transform(images.begin(), images.end(), std::back_inserter(matches),
[props](const ImageProps & prop){
return qMakePair(pow(props - prop.second, 2).sum(), &prop);
});
std::sort(matches.begin(), matches.end(),
[](Match a, Match b) { return b.first < a.first; });
randLog2 = 1<<randLog2;
return *(matches.end()-randLog2+qrand()%randLog2)->second;
}
};
QImage getMosaic(QImage img, const ImageDatabase & db, int size, int randLog2)
{
QPainter p(&img);
for (int y = 0; y < img.height(); y += size)
for (int x = 0; x < img.width(); x += size) {
QImage r = db.bestMatchFor(img.copy(x, y, size, size), randLog2).first
.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
p.drawImage(x, y, r);
}
return img;
}
class MosaicGenerator : public QObject {
Q_OBJECT
QPointer<ImageDatabase> m_db;
int m_size, m_randLog2;
QAtomicInt m_busy;
QImage m_image;
void update() {
if (m_image.isNull() || m_busy.fetchAndAddOrdered(1)) return;
QImage image = m_image;
QtConcurrent::run([this, image]{ while (true) {
emit hasMosaic(getMosaic(image, *m_db, m_size, m_randLog2));
if (m_busy.testAndSetOrdered(1, 0)) return;
m_busy.fetchAndStoreOrdered(1);
}});
}
public:
MosaicGenerator(ImageDatabase * db) : m_db(db), m_size(16), m_randLog2(0) {}
Q_SLOT void setImage(const QImage & img) { m_image = img; update(); }
Q_SLOT void setSize(int s) { m_size = s; update(); }
Q_SLOT void setRandLog2(int r) { m_randLog2 = r; update(); }
Q_SIGNAL void hasMosaic(const QImage &);
};
class Window : public QWidget {
Q_OBJECT
bool m_showSource;
QImage m_source, m_mosaic;
QBoxLayout m_layout;
QSlider m_parallelism, m_cellSize, m_randomness;
QLabel m_imgCount, m_parCount, m_image;
QPushButton m_add, m_load, m_toggle;
MosaicGenerator m_gen;
Q_SIGNAL void newSource(const QImage &);
void updateImage() {
const QImage & img = m_showSource ? m_source : m_mosaic;
m_image.setPixmap(QPixmap::fromImage(img));
}
public:
Window(ImageDatabase * db, QWidget * parent = 0) : QWidget(parent),
m_showSource(true), m_layout(QBoxLayout::TopToBottom, this),
m_parallelism(Qt::Horizontal), m_cellSize(Qt::Horizontal),
m_randomness(Qt::Horizontal), m_add("Fetch Images"),
m_load("Open for Mosaic"), m_toggle("Toggle Mosaic"), m_gen(db)
{
QBoxLayout * row = new QBoxLayout(QBoxLayout::LeftToRight);
row->addWidget(new QLabel("Images in DB:"));
row->addWidget(&m_imgCount);
row->addWidget(new QLabel("Fetch parallelism:"));
row->addWidget(&m_parallelism);
row->addWidget(&m_parCount);
row->addWidget(&m_add);
m_parallelism.setRange(1, 100);
m_layout.addLayout(row);
m_layout.addWidget(&m_image);
row = new QBoxLayout(QBoxLayout::LeftToRight);
row->addWidget(new QLabel("Cell Size:"));
row->addWidget(&m_cellSize);
row->addWidget(new QLabel("Randomness:"));
row->addWidget(&m_randomness);
m_cellSize.setRange(4, 64); m_cellSize.setTracking(false);
m_randomness.setRange(0,6); m_randomness.setTracking(false);
m_layout.addLayout(row);
row = new QBoxLayout(QBoxLayout::LeftToRight);
row->addWidget(&m_load);
row->addWidget(&m_toggle);
m_layout.addLayout(row);
m_add.setCheckable(true);
m_parCount.connect(&m_parallelism, SIGNAL(valueChanged(int)), SLOT(setNum(int)));
connect(&m_add, SIGNAL(clicked(bool)), SIGNAL(reqAutoFetch(bool)));
connect(&m_parallelism, SIGNAL(valueChanged(int)), SIGNAL(reqParallelism(int)));
m_gen.connect(&m_cellSize, SIGNAL(valueChanged(int)), SLOT(setSize(int)));
m_gen.connect(&m_randomness, SIGNAL(valueChanged(int)), SLOT(setRandLog2(int)));
m_parallelism.setValue(20);
m_cellSize.setValue(16);
m_randomness.setValue(4);
connect(&m_load, &QPushButton::clicked, [this]{
QString file = QFileDialog::getOpenFileName(this);
QtConcurrent::run([this, file]{
QImage img; if (!img.load(file)) return;
emit newSource(img);
});
});
connect(this, &Window::newSource, [this](const QImage &img){
m_source = m_mosaic = img; updateImage(); m_gen.setImage(m_source);
});
connect(&m_gen, &MosaicGenerator::hasMosaic, [this](const QImage &img){
m_mosaic = img; updateImage();
});
connect(&m_toggle, &QPushButton::clicked, [this]{
m_showSource = !m_showSource; updateImage();
});
}
Q_SLOT void setImageCount(int n) { m_imgCount.setNum(n); }
Q_SIGNAL void reqAutoFetch(bool);
Q_SIGNAL void reqParallelism(int);
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setOrganizationDomain("stackoverflow.com");
a.setApplicationName("so-photomosaic");
RandomImageSource src;
ImageDatabase db;
ImageStorage stg;
Window ui(&db);
db.connect(&src, SIGNAL(rspImage(QImage)), SLOT(addImage(QImage)));
stg.connect(&src, SIGNAL(rspImage(QImage)), SLOT(addImage(QImage)));
db.connect(&stg, SIGNAL(retrieved(QImage)), SLOT(addImage(QImage)));
ui.connect(&db, SIGNAL(newImageCount(int)), SLOT(setImageCount(int)));
src.connect(&ui, SIGNAL(reqAutoFetch(bool)), SLOT(setAutomatic(bool)));
src.connect(&ui, SIGNAL(reqParallelism(int)), SLOT(setParallelism(int)));
stg.retrieveAll();
ui.show();
return a.exec();
}
#include "main.moc"