Qt : Drawing Blended Transparent Lines/Curves/Paths - c++

I have been able to draw long transparent curves with the QPainterPath so I wont get overlapping opacity joints that would result in connecting lines between points like in Scribble. But is there a way to make a path blend its continuous transparency through out in Qt as such:

I suspect the most visually satisfying solution will be to render the strokes yourself. For example, the image you posted was rendered by drawing a large number of partially-transparent circles over one another. This could be optimized by rendering a large number of ellipses onto a QImage, then later drawing the pre-rendered image to save time.

With the help of this question/answer I wrote this code that does the job:
/* Start and end point. */
const QPointF start{ 0,0 };
const QPointF end{ 100,100 };
QGraphicsLineItem line{ QLine(start, end) };
/* Make the Gradient for this line. */
QLinearGradient gradient(start, end);
QColor color(123, 123, 231); //some color
color.setAlphaF(0.9); //change alpha
gradient.setColorAt(0, color);
color.setAlphaF(0.1); //change alpha again
gradient.setColorAt(1, color );
/* Set the line's pen. */
QPen pen(QBrush(gradient), 10);
line.setPen(pen);

Related

QPixmap with a transparent part

I'm making a simple puzzle game. The piece is a square and it contains the corresponding part of an image. What I need to do is to change the shape from the square to the common puzzle shape. I've made a template in Photoshop:
Corner piece
Now I need somehow to use it as a mask to change the shape of my square piece. I tried the following:
QImage temp;
temp.load("://Templates/c1.png");
temp = temp.convertToFormat(QImage::Format_Mono);
QBitmap bit;
bit = QBitmap::fromImage(temp);
puz->img.setMask(bit);
Where puzzle->img is my square piece. It works, but I get "rough corners" in the connectors. I suppose all of half-transparent pixels become fully filled with a color (or fully transparent?), so that's why it doesn't look smooth.
Is there a way to make it smoothier?
UPDATE: I tried a different approach: since setAlphaChannel() is an obsolete function, documentation says that I need to use QPainter::CompositionMode. I tried the following
void puzzle::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget )
{
painter->setCompositionMode(QPainter::CompositionMode_Source);
painter->fillRect(x, y, 90, 90, Qt::transparent);
painter->setCompositionMode(QPainter::CompositionMode_SourceOver);
painter->drawPixmap(x, y, 90, 90, img);
painter->setCompositionMode(QPainter::CompositionMode_DestinationIn);
painter->drawImage(x,y, mask);
}
It works like that in Image Composition example, but I get black pixels instead of transparent in my project: Output. Can't figure out, what's wrong. mask initialization:
mask.load("://Templates/c1.png");
mask = mask.scaled(90, 90, Qt::KeepAspectRatio, Qt::SmoothTransformation);
mask = mask.convertToFormat(QImage::Format_ARGB32_Premultiplied);
You should use setAlphaChannel instead. A mask only has values of 0 and 1 while the alpha channel should allow 256 variations in transparency.

QPainter composition not working as expected with background

I'm trying to draw two rectangles with same color and transparency on a QFrame with a white background. These rectangles should overlap and the their transparency should not change (also in the overlapping region). So like this:
Here is the code I have so far:
class Canvas : public QFrame
{
public:
void paintEvent(QPaintEvent * event) override;
};
void Canvas::paintEvent(QPaintEvent *event)
{
QPainter painter( this );
painter.setPen(QPen(Qt::NoPen));
painter.setBrush(QBrush(QColor(0,0,255,125)));
painter.drawRect(QRect(10,10,100,100));
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.setBrush(QBrush(QColor(0, 0, 255, 125)));
painter.drawRect(QRect(80, 80, 100, 100));
}
int main( int argc, char **argv )
{
QApplication a( argc, argv );
Canvas canvas;
canvas.setAutoFillBackground(true);
QPalette pal;
pal.setColor(QPalette::Window, QColor(Qt::red));
canvas.setBackgroundRole(QPalette::Window);
canvas.setPalette(pal);
canvas.show();
return a.exec();
}
However this produces the following image:
I have tried every possible composition mode for the painter, but none seem to give me the desired effect. I guess CompositionMode_Source is the correct one since if I use the following code:
QPixmap pixmap(200, 200);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setPen(QPen(Qt::NoPen));
painter.setBrush(QBrush(QColor(0, 0, 255, 125)));
painter.drawRect(QRect(10, 10, 100, 100));
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.setBrush(QBrush(QColor(0, 0, 255, 125)));
painter.drawRect(QRect(80, 80, 100, 100));
QLabel label;
label.setPixmap(pixmap);
label.show();
I do get the desired effect (but without the red background):
However if I change the fill to Qt::red I get again:
What am I missing here? How can I get my desired effect? The actual application for this is that I want to draw rectangles on a QFrame derived class which is implemented in a third party lib over which I have limited control.
I spot three problems with the code:
the first rectangle is drawn with alpha blending (Source Over mode) because you're setting the composition mode after the first draw call. The second one instead uses Source mode (i.e. copy the source pixels as-is, do not perform alpha blending).
Indeed Source does not perform alpha blending, which you seem to want. So don't use that! The default composition mode does what you want.
Drawing two different shapes will perform composition between them. That's obviously expected, since you're doing two draw calls; the second draw call will find the destination already changed by the first. If you don't want that, you must find a way to draw both shapes in one draw call (for instance: add both of them to one QPainterPath, then draw the path in one draw call), or perform composition at a later stage (for instance: draw them onto an opaque QImage, then blend the image over the destination in one draw call).

QPixmap copy contents of a square drawn into another image

I have an image onto which I draw a Rectangle as such. After that I am attempting to copy the contents of the rectangle onto another QLabel. This seems to work however i cannot seem to align the copied image starting from the left top corner of the image. Here is what I am doing
QPixmap original_image;
original_image.load("c:\\Images\\myimg.jpg");
original_image = original_image.scaled(ui.label->size().width(),ui.label->size().height());
//-----------------------------------------------------------------------
//Draw rectangle on this
QPixmap target_two(ui.label->size().width(),ui.label->size().height());
target_two.fill(Qt::transparent);
QPixmap target(ui.label->size().width(),ui.label->size().height());
target.fill(Qt::transparent);
QPainter painter(&target);
QPainter painter_two(&target_two);
QRegion r(QRect(0, 0, ui.label->size().width(), ui.label->size().height()), QRegion::RegionType::Rectangle); //Region to start copying
painter.setClipRegion(r);
painter.drawPixmap(0, 0, original_image); //Draw the original image in the clipped region
QRectF rectangle(x_start,y_start,clipRegion);
painter.drawRoundedRect(rectangle,0,0); //Last two parameters define the radius of the corners higher the radius more rounded it is
QRegion r_two(rectangle.toRect(), QRegion::RegionType::Rectangle);
painter_two.setClipRegion(r_two);
painter_two.drawPixmap(0,0,target);
ui.label->setPixmap(target);
ui.label_2->setPixmap(target_two);
The bottom picture is the image with the red rectangle in it and that is fine.
The top picture is a copy of the contents of the square.The only problem is its not starting from the top left corner.
Any suggestion on why I am not getting the copied content on the top left corner.
The problem in your logic is that both target and target_two images have the same sizes - size of the label, and you draw the copied image in the same position as it was in the initial label. So far so good. I would solve this by the following code:
[..]
// This both lines can be removed.
// QRegion r_two(rectangle.toRect(), QRegion::RegionType::Rectangle);
// painter_two.setClipRegion(r_two);
// Target rect. in the left top corner.
QRectF targetRect(0, 0, rectangle.width(), rectangle.height());
QRectF sourceRect(rectangle);
// Draw only rectangular area of the source image into the new position.
painter_two.drawPixmap(targetRect, target, sourceRect);
[..]

How to draw custom shapes in Qt with QPainter or QPainterPath using one shape or a group of shapes joined

How can I draw a shape like a tear? I need to draw without using more than one shape (an ellipse and a polygon) because QPen will draw for each shape. I need to join shapes to create a new one, or tell QT to join the border across both shapes, something like this:
If the shape you want to draw can be represented as a layering of other shapes, as with the image you've linked to, it's pretty easy to do:
First we need to build a QPainterPath to represent the outer edge of the shape. We build it by layering up simpler shapes; in the case of your example we need a circle and a square. Note the use of QPainterPath::setFillRule(Qt::WindingFill): this will later affect the way that the path is painted (try removing it to see the difference!).
QPainterPath OuterPath;
OuterPath.setFillRule(Qt::WindingFill);
OuterPath.addEllipse(QPointF(60, 60), 50, 50);
OuterPath.addRect(60, 10, 50, 50);
With the example you've given we'll also need to remove a circular area from the centre of our filled shape. Let's represent that inner 'border' as a QPainterPath and then use QPainterPath::subtracted() to subtract InnerPath from OuterPath and produce our final shape:
QPainterPath InnerPath;
InnerPath.addEllipse(QPointF(60, 60), 20, 20);
QPainterPath FillPath = OuterPath.subtracted(InnerPath);
Once we've built the shape paths, we need to use them to fill/outline the shape. Let's first create a QPainter and set it to use antialiasing:
QPainter Painter(this);
Painter.setRenderHint(QPainter::Antialiasing);
We then need to fill the shape that we've built:
Painter.fillPath(FillPath, Qt::blue);
Finally, let's paint the outlines. Note that, because we have separate paths for the inner and outer borders, we are able to stroke each border with different line thicknesses. Note also the use of QPainterPath::simplified(): this converts the set of layered shapes into one QPainterPath which has no intersections:
Painter.strokePath(OuterPath.simplified(), QPen(Qt::black, 1));
Painter.strokePath(InnerPath, QPen(Qt::black, 3));
If we put all of that together, it looks like this:
void Shape::paintEvent(QPaintEvent *)
{
QPainterPath OuterPath;
OuterPath.setFillRule(Qt::WindingFill);
OuterPath.addEllipse(QPointF(60, 60), 50, 50);
OuterPath.addRect(60, 10, 50, 50);
QPainterPath InnerPath;
InnerPath.addEllipse(QPointF(60, 60), 20, 20);
QPainterPath FillPath = OuterPath.subtracted(InnerPath);
QPainter Painter(this);
Painter.setRenderHint(QPainter::Antialiasing);
Painter.fillPath(FillPath, Qt::blue);
Painter.strokePath(OuterPath.simplified(), QPen(Qt::black, 1));
Painter.strokePath(InnerPath, QPen(Qt::black, 3));
}
This is actually quite difficult to do without a good math background. If you knew the formula to create that shape, you could just put it into your QGraphicsItem::paint() function. But there are some alternatives:
Make the image in a vector editing program like Inkscape (free), save it as a .svg file, and then load it into a QGraphicsSvgItem. (This is what I would do.)
Have a look at QPainterPath::cubicTo(), which allows you to make a Bezier curve

OpenCV Image to Black and White Shape

i want the hand image to be a black and white shape of the hand. here's a sample of the input and the desired output:
using a threshold doesn't give the desired output because some of the colors inside the hand are the same with the background color. how can i get the desired output?
Adaptive threshold, find contours, floodfill?
Basically, adaptive threshold turns your image into black and white, but takes the threshold level based on local conditions around each pixel - that way, you should avoid the problem you're experiencing with an ordinary threshold. In fact, I'm not sure why anyone would ever want to use a normal threshold.
If that doesn't work, an alternative approach is to find the largest contour in the image, draw it onto a separate matrix and then floodfill everything inside it with black. (Floodfill is like the bucket tool in MSPaint - it starts at a particular pixel, and fills in everything connected to that pixel which is the same colour with another colour of your choice.)
Possibly the most robust approach against various lighting conditions is to do them all in the sequence at the top. But you may be able to get away with only the threshold or the countours/floodfill.
By the way, perhaps the trickiest part is actually finding the contours, because findContours returns an arraylist/vector/whatever (depends on the platform I think) of MatOfPoints. MatOfPoint is a subclass of Mat but you can't draw it directly - you need to use drawContours. Here's some code for OpenCV4Android that I know works:
private Mat drawLargestContour(Mat input) {
/** Allocates and returns a black matrix with the
* largest contour of the input matrix drawn in white. */
List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Imgproc.findContours(input, contours, new Mat() /* hierarchy */,
Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
double maxArea = 0;
int index = -1;
for (MatOfPoint contour : contours) { // iterate over every contour in the list
double area = Imgproc.contourArea(contour);
if (area > maxArea) {
maxArea = area;
index = contours.indexOf(contour);
}
}
if (index == -1) {
Log.e(TAG, "Fatal error: no contours in the image!");
}
Mat border = new Mat(input.rows(), input.cols(), CvType.CV_8UC1); // initialized to 0 (black) by default because it's Java :)
Imgproc.drawContours(border, contours, index, new Scalar(255)); // 255 = draw contours in white
return border;
}
Two quick things you can try:
After thresholding you can:
Do a morphological closing,
or, the most straightforward: cv::findContours, keep the largest if it's more than one, then draw it using cv::fillConvexPoly and you will get this mask. (fillConvexPoly will fill the holes for you)