qt show an image for 1/4 of a second - c++

Hey I have tried several times to complete this using usleep or the qt sleep when showing an image but sometimes (almost every time) it shows up white instead of the image basically i want it to accept any input to say im ready cin will do, then show a random image numbered 1-28 in a random time ranging 1.5-3secs then show it for 250milisecs then hide and wait 2secs the show the picture for 3secs then repeat. I am on debian, g++;
thanks in advance.
int getRandInt(int x){
return rand() % x;
}
class I : public QThread
{
public:
static void sleep(unsigned long secs) {
QThread::msleep(secs);
}
};
QApplication app(argc, argv);
std::ostringstream oss;
oss << "images/" << getRandInt(28) << ".jpg";
std::cout << oss.str();
QString qstr = QString::fromStdString(oss.str());
QPixmap pixmap(qstr);
QPixmap pixmap2(qstr);
QSplashScreen splash(pixmap);
QSplashScreen splash2(pixmap2);
QMainWindow mainWin;
while(1==1){
splash.show();
splash.showMessage(QObject::tr("test"),
Qt::AlignLeft | Qt::AlignTop, Qt::black);
app.processEvents();
I::sleep(250);
splash.finish(0);
splash.raise();
I::sleep(2*1000);
splash2.show();
splash2.showMessage(QObject::tr("test"),Qt::AlignLeft | Qt::AlignTop, Qt::black);
app.processEvents(); I::sleep(5000);splash2.finish(&mainWin);splash2.raise();
}

With an understanding that "show a random image numbered 1-28 in a random time ranging 1.5-3secs then show it for 250milisecs" means "show a random image numbered 1-28 in a random time between 1.75s and 3.25s", the below does it.
The code requires Qt 5.x and a C++11 compiler. I tried to highlight proper use of C++11 pseudorandom number facilities. I don't know the end-use of your code, but if you substituted std::mt19937_64 for random_engine, it'd be good enough for running psychological research as far as randomness goes.
The images shown are randomly drawn from a set of 28 images. Each of the source images are also randomly generated.
Notice the conspicuous absence of any form of sleep or busy-looping, and zealous use of lambdas. This code wouldn't be any shorter were it written in Python. That's what C++11 buys you: conciseness, among other things :)
#include <QApplication>
#include <QStateMachine>
#include <QTimer>
#include <QStackedWidget>
#include <QPainter>
#include <QLabel>
#include <QPushButton>
#include <random>
typedef std::default_random_engine random_engine;
QImage randomImage(int size, random_engine & rng) {
QImage img(size, size, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::white);
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing);
int N = std::uniform_int_distribution<>(25, 200)(rng);
std::uniform_real_distribution<> dP(0, size);
std::uniform_int_distribution<> dC(0, 255);
QPointF pt1(dP(rng), dP(rng));
for (int i = 0; i < N; ++i) {
QColor c(dC(rng), dC(rng), dC(rng));
p.setPen(QPen(c, 3));
QPointF pt2(dP(rng), dP(rng));
p.drawLine(pt1, pt2);
pt1 = pt2;
}
return img;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
std::random_device rd;
random_engine gen(rd());
int imageSize = 300;
QList<QImage> images;
for (int n = 0; n < 28; ++n) images << randomImage(imageSize, gen);
std::uniform_int_distribution<> dImage(0, images.size()-1);
QStackedWidget display;
QPushButton ready("I'm Ready!");
QLabel label, labelHidden;
display.addWidget(&ready);
display.addWidget(&label);
display.addWidget(&labelHidden);
QTimer splashTimer;
QStateMachine machine;
QState s1(&machine), s2(&machine), s3(&machine), s4(&machine);
splashTimer.setSingleShot(true);
QObject::connect(&s1, &QState::entered, [&]{
display.setCurrentWidget(&ready);
ready.setDefault(true);
ready.setFocus();
});
s1.addTransition(&ready, "clicked()", &s2);
QObject::connect(&s2, &QState::entered, [&]{
label.setPixmap(QPixmap::fromImage(images.at(dImage(gen))));
display.setCurrentWidget(&label);
splashTimer.start(250 + std::uniform_int_distribution<>(1500, 3000)(gen));
});
s2.addTransition(&splashTimer, "timeout()", &s3);
QObject::connect(&s3, &QState::entered, [&]{
display.setCurrentWidget(&labelHidden);
splashTimer.start(2000);
});
s3.addTransition(&splashTimer, "timeout()", &s4);
QObject::connect(&s4, &QState::entered, [&]{
display.setCurrentWidget(&label);
splashTimer.start(3000);
});
s4.addTransition(&splashTimer, "timeout()", &s1);
machine.setInitialState(&s1);
machine.start();
display.show();
return a.exec();
}

Related

QtCharts: how to plot line series with error bars

I have seen this question already being asked:
in Qt forum topic
in another Qt forum topic
in yet another Qt forum topic
and even in SO
Surprisingly I found no solutions (well, qcustomplot has this feature, but we are not allowed to use it in my company). My task is to represent some scientific data as a line plot with error bars representing the data standard deviation. I have tried some hacks to solve it, but the results are not sufficient for our application:
creating one QLineSeries for data mean values, and then add N QLineSeries to represent error bars (see approachWithMultipleQLineSeries in the example below, ); this is clearly an overkill and my plot is glitching on the slower machines (with Windows :/)
creating one QLineSeries for data mean values and then add QBoxPlotSeries to represent error bars (see approachWithQBoxSet in the example below, ); this does not glitch anymore, but the error bars do not correspond well to the line plot (e.g. look at the values close to zero), probably because Boxes are designed for slightly different X axes (with categories, not floating point numbers);
Could you please suggest some better approach where error bar values will actually be associated with X value and mean Y value (maybe subclass QXYSeries, or sth similar, I do not have any good ideas how to do it exactly)?
MCVE if someone would like to try:
CMakeLists.txt:
cmake_minimum_required(VERSION 3.11.0)
project(QtChartsErrBars CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(Qt5 COMPONENTS Charts Core Widgets REQUIRED)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
add_executable(QtChartsErrBars
main.cpp
Demo.cpp
Demo.hpp
)
target_link_libraries(QtChartsErrBars
Qt5::Charts
Qt5::Core
Qt5::Widgets
)
target_compile_options(QtChartsErrBars PUBLIC -g)
Demo.hpp:
#pragma once
/**
* /author Pawel Ptasznik
*/
#include <QMainWindow>
#include <QWidget>
#include <memory>
class Demo : public QMainWindow
{
Q_OBJECT
public:
explicit Demo(QWidget* parent = nullptr);
~Demo() override = default;
};
Demo.cpp:
#include "Demo.hpp"
#include <QDebug>
#include <QVector>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QAreaSeries>
#include <QtCharts/QLegendMarker>
#include <QtCharts/QBoxSet>
#include <QtCharts/QBoxPlotSeries>
#include <algorithm>
#include <random>
static std::random_device rd{};
static std::mt19937 gen{ rd() };
// for each input generate n samples using normal distribution with expected value == x^2
QVector<QVector<double> > generateData(QVector<double> in, int nSamples)
{
QVector<QVector<double> > results(in.size());
for (int i = 0; i < in.size(); i++)
{
results[i].resize(nSamples);
std::normal_distribution<double> d{ in[i]*in[i], 1 };
for (auto& sample : results[i])
{
sample = d(gen);
}
}
return results;
}
/************* HERE the solutions I have tried and I do not like ************/
namespace
{
class VerticalLine : public QtCharts::QLineSeries
{
public:
VerticalLine(double x, double yMin, double yMax)
{
replace(QVector<QPointF>{ { x, yMin }, { x, yMax } });
}
};
}
void approachWithMultipleQLineSeries(QtCharts::QChart *chart,
const QVector<double>& x, const QVector<double>& yMean, const QVector<double>& yStdDev)
{
for (int i = 0; i < x.size(); i++)
{
auto series = new VerticalLine(x[i], yMean[i]-yStdDev[i], yMean[i]+yStdDev[i]);
chart->addSeries(series);
chart->legend()->markers(series)[0]->setVisible(false);
}
}
void approachWithQBoxSet(QtCharts::QChart *chart,
const QVector<double>& x, const QVector<double>& yMean, const QVector<double>& yStdDev)
{
QList<QtCharts::QBoxSet*> boxList;
for (int i = 0; i < x.size(); i++)
{
boxList.push_back(new QtCharts::QBoxSet(yMean[i]-yStdDev[i], yMean[i]-yStdDev[i]/2,
yMean[i], yMean[i]+yStdDev[i]/2, yMean[i]+yStdDev[i]));
}
auto series = new QtCharts::QBoxPlotSeries();
series->append(boxList);
series->setBoxWidth(0.1);
series->attachAxis(chart->axes(Qt::Horizontal)[0]);
chart->addSeries(series);
chart->legend()->markers(series)[0]->setVisible(false);
}
/************* HERE quick example that plots quadratic function over 200 points ************/
Demo::Demo(QWidget* parent): QMainWindow(parent)
{
QVector<double> xAxisValues(200); // lets have 200 points between -10 and 10
std::generate(xAxisValues.begin(), xAxisValues.end(), [x = -10.0]() mutable { return x += 0.1; });
QVector<double> yAxisMeanValues(xAxisValues.size());
QVector<double> yAxisStdDev(xAxisValues.size());
// lets generate 50 samples for each x value
auto data = generateData(xAxisValues, 50);
// calculating mean values
std::transform(data.begin(), data.end(), yAxisMeanValues.begin(), [](const auto& samples)
{
double sum = 0.0;
for (auto sample : samples) sum += sample;
return sum / samples.size();
});
// calculating standard deviation
std::transform(data.begin(), data.end(), yAxisMeanValues.begin(), yAxisStdDev.begin(), [](const auto& samples, auto mean)
{
double var = 0.0;
for (auto sample : samples) var += (sample - mean)*(sample - mean);
var /= samples.size();
return std::sqrt(var);
});
QtCharts::QLineSeries *series = new QtCharts::QLineSeries();
for (int i = 0; i < xAxisValues.size(); i++)
{
series->append(xAxisValues[i], yAxisMeanValues[i]);
}
QtCharts::QChart *chart = new QtCharts::QChart();
chart->addSeries(series);
// OK, now how do I add the error bars basing on x, yMean, yStdDev?
// approachWithMultipleQLineSeries(chart, xAxisValues, yAxisMeanValues, yAxisStdDev);
chart->createDefaultAxes();
approachWithQBoxSet(chart, xAxisValues, yAxisMeanValues, yAxisStdDev);
chart->setTitle("Demo chart");
QtCharts::QChartView *chartView = new QtCharts::QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
setCentralWidget(chartView);
resize(400, 300);
}
main.cpp:
#include "Demo.hpp"
#include <QApplication>
int main(int argc, char** argv)
{
QApplication a(argc, argv);
Demo mainWindow;
mainWindow.show();
return a.exec();
}

Problem Changing Regular Value Axis to Log Axis

I have a QT chart with regular value axes. When I toggle a checkbox, I want to change the X Axis from a QValueAxis to a QLogValueAxis. The problem is, when I do this, my data no longer plots to the correct point.
I've tried two approaches (and a bunch of variations on them) to get the log scale to work, but no what I've tried it seems like the data scales itself to fit in the window linearly and ignores the log axis completely.
Approach 1 - Replace the old axis:
QLogValueAxis* xLogAxis = new QLogValueAxis();
xLogAxis->setBase(10);
xLogAxis->setMinorTickCount(10);
dataSeries->attachedAxes()[0] = xLogAxis; //Replace the old X Axis on the series
chart->setAxisX(xLogAxis);
Approach 2 - Make a completely new Chart:
chart->removeSeries(data); //release the data so you don't destroy it
QChart* newChart = new QChart();
ui->graphView->setChart(newChart); //Swap the old chart out then delete it
delete chart;
chart = newChart;
//get rid of the old axes on the data
for(QAbstractAxis* axis : data->attachedAxes()){
data->detachAxis(axis);
}
QValueAxis* yAxis = new QValueAxis();
data->attachAxis(xLogAxis);
data->attachAxis(yAxis);
chart->addAxis(xLogAxis, Qt::AlignBottom);
chart->addAxis(yAxis, Qt::AlignLeft);
chart->addSeries(data);
chart->legend()->setVisible(false);
Any ideas on how to hotswap to a log axis at runtime? Thanks in advance!
In the following example how to exchange axis types:
#include <QtWidgets>
#include <QtCharts>
QT_CHARTS_USE_NAMESPACE
typedef std::function<qreal (const qreal &)> function;
static std::vector<std::pair<function, std::string>> functions{
{[](const qreal & v){ return v;}, "linear"},
{[](const qreal & v){ return v*v; }, "quadratic"},
{[](const qreal & v){ return std::exp(0.01*v);}, "exponential"},
{[](const qreal & v){ return std::sqrt(1 + std::abs(v));}, "square root"}
};
class Widget: public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent=nullptr):
QWidget(parent),
xLogAxis(new QLogValueAxis),
xLinAxis(new QValueAxis),
yAxis(new QValueAxis)
{
xLogAxis->setBase(10);
xLogAxis->setMinorTickCount(10);
view.setChart(&chart);
checkbox.setText("Log Axis");
connect(&checkbox, &QCheckBox::stateChanged, this, &Widget::onStateChanged);
QVBoxLayout *lay = new QVBoxLayout(this);
lay->addWidget(&checkbox);
lay->addWidget(&view);
chart.addAxis(yAxis, Qt::AlignLeft);
// chart.legend()->hide();
// create series
for(const std::pair<function, std::string> & func: functions){
QLineSeries *serie = new QLineSeries;
serie->setName(QString::fromStdString(func.second));
for(int i=0; i< 1000; ++i){
*serie << QPointF(i+1, func.first(i));
}
chart.addSeries(serie);
serie->attachAxis(yAxis);
}
onStateChanged(checkbox.checkState());
}
private slots:
void onStateChanged(int state){
QAbstractAxis *removeaxis, *insertaxis;
if(state == Qt::Checked){
removeaxis = xLinAxis;
insertaxis = xLogAxis;;
}
else{
removeaxis = xLogAxis;
insertaxis = xLinAxis;
}
if(chart.axes(Qt::Horizontal).contains(removeaxis))
chart.removeAxis(removeaxis);
chart.addAxis(insertaxis, Qt::AlignBottom);
for(auto serie: chart.series()){
if(serie->attachedAxes().contains(removeaxis))
serie->detachAxis(removeaxis);
serie->attachAxis(insertaxis);
}
}
private:
QCheckBox checkbox;
QChartView view;
QChart chart;
QLogValueAxis *xLogAxis;
QValueAxis *xLinAxis;
QValueAxis *yAxis;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.resize(640, 480);
w.show();
return a.exec();
}
#include "main.moc"

Gtkmm scroll_to() purpose

I'm wondering about this member-function's scroll_to(TextBuffer::iterator& iter, double within_margin = 0) parameter within_margin. The API says this:
The effective screen for purposes of this function is reduced by a margin of size within_margin.
...
Parameters
within_margin margin as a [0.0,0.5] fraction of screen size.
I just don't get it. What and when does this parameter modifies the behaviour? Every langugage-binding of Gtk includes the same description. I've written a small application, so you can change the passed argument to the parameter yourself.
#include <gtkmm.h>
#include <iostream>
#include <string>
using namespace std;
Gtk::TextView* text_view;
void on_add_button_clicked();
void on_scroll_button_clicked();
int main(int argc, char* argv[]) {
Glib::RefPtr<Gtk::Application> app = Gtk::Application::create(argc, argv, "org.gtkmm.examples.base");
Gtk::Window window;
Glib::RefPtr<Gdk::Monitor> primary_monitor = window.get_screen()->get_display()->get_primary_monitor();
Gdk::Rectangle monitor_size;
primary_monitor->get_geometry(monitor_size);
// half-size of primary-monitor
int width = monitor_size.get_width() / 2;
int height = monitor_size.get_height() / 2;
window.set_default_size(width, height);
window.set_title(__FILE__);
Gtk::Grid grid;
grid.set_row_spacing(5);
grid.set_column_spacing(5);
Gtk::ScrolledWindow scroll_window;
text_view = new Gtk::TextView();
text_view->set_editable(true);
scroll_window.add(*text_view);
scroll_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
scroll_window.set_hexpand(true);
scroll_window.set_vexpand(true);
Glib::RefPtr<Gtk::TextBuffer> text_buffer = text_view->get_buffer();
text_buffer->set_text("Hello!\n");
text_view->set_buffer(text_buffer);
grid.attach(scroll_window, 0, 0, 2, 2);
Gtk::Button add_button("add text");
add_button.signal_clicked().connect(sigc::ptr_fun(&on_add_button_clicked));
grid.attach_next_to(add_button, scroll_window, Gtk::POS_BOTTOM, 1, 1);
Gtk::Button scroll_button("scroll to somewhere");
scroll_button.signal_clicked().connect(sigc::ptr_fun(&on_scroll_button_clicked));
grid.attach_next_to(scroll_button, add_button, Gtk::POS_RIGHT, 1, 1);
window.add(grid);
window.show_all();
return app->run(window);
}
void on_add_button_clicked() {
Glib::RefPtr<Gtk::TextBuffer> text_buffer = text_view->get_buffer();
for (int i = 0; i != 100; ++i) {
text_buffer->insert_at_cursor("foobar\n");
}
}
void on_scroll_button_clicked() {
Glib::RefPtr<Gtk::TextBuffer> text_buffer = text_view->get_buffer();
Gtk::TextBuffer::iterator it = text_buffer->end();
text_view->scroll_to(it, 0.49);
}
You can compile the code with g++ -o scroll scroll.cpp -Wall -pedantic-errors `pkg-config gtkmm-3.0 --cflags --libs` .
Thank you
If margin is 0, then scroll_to() is free to put the target anywhere in the screen. If margin is 0.45, for example, then scroll_to() will put the target in the middle 10% of the screen, if possible.
The reason you don't see this in your example, is because you are scrolling to the end iterator, and it's not possible to scroll view so that the end of the text is displayed in the middle of the screen. (Some text views include extra space after the text in order to make this possible; Gtk::TextView doesn't.)

Rendering a section of a non-smooth QImage using QSGImageNode

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.

Making my own photo-mosaic app with Qt using C++

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"