Given a .png image with a transparent background, I want to find the bounding box of the non-transparent data. Using nested for loops with QImage.pixel() is painfully slow. Is there a built-in method of doing this in Qt?
There is one option that involves using a QGraphicsPixmapItem and querying for the bounding box of the opaque area (QGraphicsPixmapItem::opaqueArea().boundingRect()). Not sure if it is the best way but it works :) It might be worth digging into Qt's source code to see what code is at the heart of it.
The following code will print out the width and height of the image followed by the width and height of the opaque portions of the image:
QPixmap p("image.png");
QGraphicsPixmapItem *item = new QGraphicsPixmapItem(p);
std::cout << item->boundingRect().width() << "," << item->boundingRect().height() << std::endl;
std::cout << item->opaqueArea().boundingRect().width() << "," << item->opaqueArea().boundingRect().height() << std::endl;
If pixel() is too slow for you, consider more efficient row-wise data adressing, given a QImage p:
int l =p.width(), r = 0, t = p.height(), b = 0;
for (int y = 0; y < p.height(); ++y) {
QRgb *row = (QRgb*)p.scanLine(y);
bool rowFilled = false;
for (int x = 0; x < p.width(); ++x) {
if (qAlpha(row[x])) {
rowFilled = true;
r = std::max(r, x);
if (l > x) {
l = x;
x = r; // shortcut to only search for new right bound from here
}
}
}
if (rowFilled) {
t = std::min(t, y);
b = y;
}
}
I doubt it will get any faster than this.
The easiest and also relatively fast solution is to do as follows:
QRegion(QBitmap::fromImage(image.createMaskFromColor(0x00000000))).boundingRect()
If you have a QPixmap rather than QImage, then you can use:
QRegion(pixmap.createMaskFromColor(Qt::transparent)).boundingRect()
QPixmap::createMaskFromColor internally will convert the pixmap to an image and do the same as above. An even shorter solution for QPixmap is:
QRegion(pixmap.mask()).boundingRect()
In this case, a QPixmap without alpha channel will result in an empty region, so you may need to check for that explicitly. Incidentally, this is also what QGraphicsPixmapItem::opaqueArea mentioned by #Arnold Spence is based on.
You may also want to try QImage::createAlphaMask, though the cutoff point will not be at 0 alpha but rather somewhere at half opacity.
Related
I am making a simple editor where the user can click on points of an image and crop out a shape. My implementation is terribly inefficient and as I'm new to qt, I have trouble deciphering all the functions on qt's docs.
QPolygonF polygon(points);
std::map<std::string, int> map = pointsHandler.getOutsideVals();
for(int i = map["Left"]; i < map["Right"]; i++){
for(int j = map["Top"]; j < map["Bottom"]; j++){
for(int n = 0; n < points.size(); n++){
if(polygon.containsPoint(QPointF(i,j), Qt::OddEvenFill)){
image.setPixelColor(QPoint(i - xOffset, j - yOffset), Qt::transparent);
}
}
}
}
painter.drawImage(xOffset,yOffset, image);
Currently how I'm doing it is looping through a rectangle given by the outer most points of the polygon. If a point is in the polygon or not I change the pixel value to be transparent. The polygon is made from the users clicked points which I then store the outer most values in a map. When I crop out large portions, it takes far to long and was I looking for some advice to make this more efficient. Thank you.
EDIT
I am now using setClipPath mentioned by G.M. and have no performance issues, however the way I found to get the job done now seems like a waste of memory. Using setClipPath(...) the best work around I found was to make multiple Qt class objects on the stack, it works great just seems like I'm working around to much stuff. Here's the updated code.
QPolygon clipPolygon = QPolygonF(points).toPolygon();
QRegion clippedRegion(clipPolygon, Qt::OddEvenFill);
QRect translatedImageRect = image.rect().translated(QPoint(xOffset, yOffset));
QRegion unClippedRegion = QRegion(translatedImageRect).subtracted(clippedRegion);
painter.save();
painter.setClipRegion(unClippedRegion, Qt::ReplaceClip);
painter.drawImage(xOffset, yOffset, image);
painter.restore();
It works great, just feel like I'm wasting memory.
You can use QPainter to make a rectangle of your image transparent.
QImage image("/home/tim/Bilder/Example.png");
QPainter painter(&image);
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.fillRect(0, 0, 10, 10, Qt::transparent);
painter.end();
image.save("/home/tim/Bilder/changed.png", "PNG");
I am drawing lines in Qt using Graphics View framework. Since i want my picture to take the same portion of space when the window is resized, I override MainWindow::resizeEvent, so that graphics view is rescaled according to the resize event:
void MainWindow::resizeEvent(QResizeEvent *event) {
int w = event->size().width(), h = event->size().height();
int prev_w = event->oldSize().width(), prev_h = event->oldSize().height();
if (prev_w != -1) {
int s1 = std::min(prev_w, prev_h), s2 = std::min(w, h);
qreal k = (qreal)s2 / s1;
std::cerr << k << std::endl;
ui->graphicsView->scale(k, k);
}
}
However, doing so, my lines (that should have thickness of 1 pixel) sometimes have different thickness after resize. As I understand, it happens because coordinates of the objects after transforming to the GraphicsView are real, so are sometimes drawn with different number of pixels. That is unacceptable! I want lines to have same 1-pixel thickness all the time.
So, my question is: what is the usual solution for this problem? For now (based on my assumption above) I can only think of deleting all objects and creating new with integer coordinates, but rescaled (manually).
You need to set your line drawing to "cosmetic" in the QPen. This makes the lines non-scalable. Otherwise, Qt scales the line widths along with the scaling of the view. Look up QPen::setCosmetic. By default, drawing lines is not cosmetic.
currently QTextEdit and QPlainTextEdit does not meet my requirements, so I need to subclass QAbstractTextDocumentLayout to provide custom layout of the document. I reference QPlainTextDocumentLayout and QTextDocumentLayout a lot, and finally got a simple layout to display the text. However, I couldn't see the cursor in QTextEdit, which should be blinking. I need help to figure this out.
I am using Qt 5.9.1. The simple project is here. The draw() function of VTextDocumentLayout looks like this:
void VTextDocumentLayout::draw(QPainter *p_painter, const PaintContext &p_context)
{
qDebug() << "VTextDocumentLayout draw()" << p_context.clip << p_context.cursorPosition << p_context.selections.size();
// Find out the blocks.
int first, last;
blockRangeFromRect(p_context.clip, first, last);
if (first == -1) {
return;
}
QTextDocument *doc = document();
QPointF offset(m_margin, m_blocks[first].m_offset);
QTextBlock block = doc->findBlockByNumber(first);
QTextBlock lastBlock = doc->findBlockByNumber(last);
qreal maximumWidth = m_width;
while (block.isValid()) {
const BlockInfo &info = m_blocks[block.blockNumber()];
const QRectF &rect = info.m_rect;
QTextLayout *layout = block.layout();
if (!block.isVisible()) {
offset.ry() += rect.height();
if (block == lastBlock) {
break;
}
block = block.next();
continue;
}
QTextBlockFormat blockFormat = block.blockFormat();
QBrush bg = blockFormat.background();
if (bg != Qt::NoBrush) {
fillBackground(p_painter, rect, bg);
}
auto selections = formatRangeFromSelection(block, p_context.selections);
QPen oldPen = p_painter->pen();
p_painter->setPen(p_context.palette.color(QPalette::Text));
layout->draw(p_painter,
offset,
selections,
p_context.clip.isValid() ? p_context.clip : QRectF());
// Draw the cursor.
int blpos = block.position();
int bllen = block.length();
bool drawCursor = p_context.cursorPosition >= blpos
&& p_context.cursorPosition < blpos + bllen;
Q_ASSERT(p_context.cursorPosition >= -1);
if (drawCursor) {
int cpos = p_context.cursorPosition;
cpos -= blpos;
qDebug() << "draw cursor" << block.blockNumber() << blpos << bllen << p_context.cursorPosition << cpos;
layout->drawCursor(p_painter, offset, cpos);
}
p_painter->setPen(oldPen);
offset.ry() += rect.height();
if (block == lastBlock) {
break;
}
block = block.next();
}
}
I called layout->drawCursor() to draw the cursor but this function seems to do nothing.
Any help is appreciated! Thanks!
Update:
Add the log as following:
VTextDocumentLayout draw() QRectF(67,0 9x13) 19 0
block range 0 1
draw cursor 1 5 15 19 14
blockBoundingRect() 1 13 QRectF(0,0 75x17)
VTextDocumentLayout draw() QRectF(67,0 9x13) -1 0
block range 0 1
blockBoundingRect() 1 13 QRectF(0,0 75x17)
VTextDocumentLayout draw() QRectF(67,0 9x13) 19 0
When running this project in Linux, I coundn't see the cursor. However, when running it in Windows, I could see the cursor but not blinking.
Update:
It seems that QTextEdit pass a wrong clip rect to the layout. If I just inserted one line of text (only one block within the document), I could see the blinking cursor. Quite strange!!!
The default value of PaintContext class cursorPosition is -1.
http://doc.qt.io/qt-4.8/qabstracttextdocumentlayout-paintcontext.html#cursorPosition-var
If any default value is not provided to the cursor position ever (it is a public variable), then my guess is value remains -1, and drawCursor is always false (as the block position index minimum value is zero in worst case).
Try setting some default value to PaintContext::cursorPosition, which may display the cursor.
When subclassing QAbstractTextDocumentLayout, blockBoundingRect() should return the geometry of the block, not just the rect as what QPlainTextDocumentLayout does.
In one word, QPlainTextDocumentLayout provides a BAD example for subsclassing QAbstractTextDocumentLayout.
I am trying to draw a 10 millisecond grid in a QGraphicsScene in Qt. I am not very familiar with Qt... it's the first time I used it, and only because the application needs to be portable between Windows and Linux.
I actually don't have a problem drawing the grid, it's just the performance when the grid gets big. The grid has to be able to change size to fit the SceneRect if/when new data is loaded into the program to be displayed.
This is how I do it at the moment, I hate this but it's the only way I can think of doing it...
void Plotter::drawGrid() {
unsigned int i;
QGraphicsLineItem *line;
QGraphicsTextItem *text;
char num[11];
QString label;
unsigned int width = scene->sceneRect().width();
unsigned int height = scene->sceneRect().height();
removeGrid();
for (i = 150; i < width; i+= 10) {
line = new QGraphicsLineItem(i, 0, i, scene->sceneRect().height(), 0, scene);
line->setPen(QPen(QColor(0xdd,0xdd,0xdd)));
line->setZValue(0);
_itoa_s(i - 150, num, 10);
label = num;
label += " ms";
text = new QGraphicsTextItem(label, 0, scene);
text->setDefaultTextColor(Qt::white);
text->setX(i);
text->setY(height - 10);
text->setZValue(2);
text->setScale(0.2);
//pointers to items stored in list for removal later.
gridList.append(line);
gridList.append(text);
}
for (i = 0; i < height; i+= 10) {
line = new QGraphicsLineItem(150, i, width, i, 0, scene);
line->setPen(QPen(QColor(0xdd,0xdd,0xdd)));
line->setZValue(0);
gridList.append(line);
}
}
When scene->sceneRect().width() gets too big, however, the application becomes very sluggish. I have tried using a QGLWidget, but the improvements in speed are marginal at best.
I ended up using a 10x10 square pixmap and drew it as the backgroundBrush on my QGraphicsView, as is suggested in the link in the comment under my initial question.
I'm trying to understand why the code below changes the QImage in Qt. It's not meant to do anything (yet), it's just for testing. When I run the code on an image with alpha, the alpha channel is lost and replaced by a black background.
QImage image;
image.load("image.png");
for (int y = 0; y < image.height(); y++) {
for (int x = 0; x < image.height(); x++) {
QColor c = QColor::fromRgba(image.pixel(x, y));
c.setHsv(c.hue(), c.saturation(), c.value());
image.setPixel(x, y, c.rgba());
}
}
Here is the result when I comment out the line image.setPixel(...):
And here is the result with the image.setPixel(...) line:
I would expect my code to do no change on the image. Any idea why it's doing this?
If you look at the documentation of setHsv(), you will see that alpha is set by default to 255 (or 1.0 for the float version) if you don't explicitly specify it.
Perhaps using the line c.setHsv(c.hue(), c.saturation(), c.value(), c.alpha()); will resolve your problem.