Qt: Extract intensity values clipped by a region of interest polygon - qt

Based on a grayscale image and an ordered closed polygon (may be concave), I want to get all grayscale values that lie inside a region of interest polygon (as likewise described in SciPy Create 2D Polygon Mask). What is the most performant realisation of that in Qt 4.8? Endpoint should be some kind of QList<double>. Thanks for your advices.
In addition, is it possible to compute a floating point mask (e.g. 0 for outside the polygon, 0.3 for 30% of the pixel area is within the polygon, 1 for completely inside the polygon)? However, that's just an extra, endpoint would be QPair<double, double> (percentage, value) then. Thanks.

First you need to scanline convert the polygon into horizontal lines -- this is done by getScanLines(). The scanlines are used to sample the image to get all the values within the endpoints of each line, using scanlineScanner().
Below is a complete, standalone and compileable example I had laying around to show that the scanline algorithm is well behaved. It could be tweaked to calculate the fixed point mask as well. So far a point is included if the scanline covers more than half of it in the horizontal extent (due to round()s in scanlineScanner).
Upon startup, resize the window and click around to define consecutive points in the polygon. The polygon you see is rendered solely using the scanlines. For comparison, you can enable the outline of the polygon.
I'm sure the scanline converter could be further optimized. I'm not doing any image sampling, but the scanlineScanner is there to show that it's a trivial thing to do.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scanline-converter-11037252
#define QT_DISABLE_DEPRECATED_BEFORE 5
#include <QtGui>
#if QT_VERSION > QT_VERSION_CHECK(5,0,0)
#include <QtWidgets>
#endif
#include <algorithm>
typedef QVector<QPointF> PointVector;
typedef QVector<QLineF> LineVector;
// A list of vertex indices in the polygon, sorted ascending in their y coordinate
static QVector<int> sortedVertices(const QPolygonF & poly)
{
Q_ASSERT(! poly.isEmpty());
QVector<int> vertices;
vertices.reserve(poly.size());
for (int i = 0; i < poly.size(); ++i) { vertices << i; }
std::sort(vertices.begin(), vertices.end(), [&](int i, int j){
return poly[i].y() < poly[j].y();
});
return vertices;
}
// Returns point of intersection of infinite lines ref and target
static inline QPointF intersect(const QLineF & ref, const QLineF & target)
{
QPointF p;
target.intersect(ref, &p);
return p;
}
// Allows accessing polygon vertices using an indirect index into a vector of indices.
class VertexAccessor {
const QPolygonF & p;
const QVector<int> & i;
public:
VertexAccessor(const QPolygonF & poly, const QVector<int> & indices) :
p(poly), i(indices) {}
inline QPointF operator[](int ii) const {
return p[i[ii]];
}
inline QPointF prev(int ii) const {
int index = i[ii] - 1;
if (index < 0) index += p.size();
return p[index];
}
inline QPointF next(int ii) const {
int index = i[ii] + 1;
if (index >= p.size()) index -= p.size();
return p[index];
}
};
// Returns a horizontal line scanline rendering of an unconstrained polygon.
// The lines are generated on an integer grid, but this could be modified for any other grid.
static LineVector getScanlines(const QPolygonF & poly)
{
LineVector lines;
if (poly.isEmpty()) return lines;
const QVector<int> indices = sortedVertices(poly);
VertexAccessor vertex{poly, indices};
const QRectF bound = poly.boundingRect();
const auto l = bound.left();
const auto r = bound.right();
int ii = 0;
int yi = qFloor(vertex[0].y());
QList<int> active;
PointVector points;
forever {
const qreal y = yi;
const QLineF sweeper{l, y, r, y};
// Remove vertex from the active list if both lines extending from it are above sweeper
for (int i = 0; i < active.size(); ) {
const int ii = active.at(i);
// Remove vertex
if (vertex.prev(ii).y() < y && vertex.next(ii).y() < y) {
active.removeAt(i);
} else {
++ i;
}
}
// Add new vertices to the active list
while (ii < poly.count() && vertex[ii].y() < y) {
active << ii++;
}
if (ii >= poly.count() && active.isEmpty()) break;
// Generate sorted intersection points
points.clear();
for (auto ii : active) {
const auto a = vertex[ii];
auto b = vertex.prev(ii);
if (b.y() >= y)
points << intersect(sweeper, QLineF{a, b});
b = vertex.next(ii);
if (b.y() >= y)
points << intersect(sweeper, QLineF{a, b});
}
std::sort(points.begin(), points.end(), [](const QPointF & p1, const QPointF & p2){
return p1.x() < p2.x();
});
// Generate horizontal fill segments
for (int i = 0; i < points.size() - 1; i += 2) {
lines << QLineF{points.at(i).x(), y, points.at(i+1).x(), y};
}
yi++;
};
return lines;
}
QVector<int> scanlineScanner(const QImage & image, const LineVector & tess)
{
QVector<int> values;
for (auto & line : tess) {
for (int x = round(line.x1()); x <= round(line.x2()); ++ x) {
values << qGray(image.pixel(x, line.y1()));
}
}
return values;
}
class Ui : public QWidget
{
Q_OBJECT
QPointF lastPoint;
QPolygonF polygon;
LineVector scanlines;
QGridLayout layout{this};
QPushButton reset{"Reset"};
QCheckBox outline{"Outline"};
public:
Ui() {
setMinimumSize(200, 200);
layout.addItem(new QSpacerItem{0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding}, 0, 0, 1, 3);
layout.addWidget(&reset, 1, 0);
layout.addWidget(&outline, 1, 1);
layout.addItem(new QSpacerItem{0, 0, QSizePolicy::Expanding}, 1, 2);
reset.setObjectName("reset");
outline.setObjectName("outline");
QMetaObject::connectSlotsByName(this);
}
protected:
Q_SLOT void on_reset_clicked() {
polygon.clear();
scanlines.clear();
lastPoint = QPointF{};
update();
}
Q_SLOT void on_outline_stateChanged() {
update();
}
void paintEvent(QPaintEvent *) override {
QPainter p{this};
if (false) p.setRenderHint(QPainter::Antialiasing);
p.setPen("cadetblue");
if (!polygon.isEmpty() && scanlines.isEmpty()) {
scanlines = getScanlines(polygon);
qDebug() << "new scanlines";
}
p.drawLines(scanlines);
if (outline.isChecked()) {
p.setPen("orangered");
p.setBrush(Qt::NoBrush);
p.drawPolygon(polygon);
}
if (!lastPoint.isNull()) {
p.setPen("navy");
p.drawEllipse(lastPoint, 3, 3);
}
}
void mousePressEvent(QMouseEvent * ev) override {
lastPoint = ev->posF();
polygon << ev->posF();
scanlines.clear();
update();
}
};
int main(int argc, char** argv)
{
QApplication app{argc, argv};
Ui ui;
ui.show();
return app.exec();
}
#include "main.moc"

Related

QCharts Crop to Rectangle and Use Horizontal Scroll

am trying to implement a custom graph going off the QtCharts Callout example. I want to restrict the selection of the chart to a specific area and make it possible to scroll horizontally while still displaying the Axis Values.
the classes i am using are below
callout.cpp
callout.h
main.cpp
view.cpp
view.h
here is an example of what i mean
say i want the selection region point1 = (5,0) point2 = (15,8) and the region is a QRect(point1,point2)
All points in the graph should be rendered but I want to be able to scroll sideways and keep the y_axis in view.
One possible solution is to override the mousePressEvent and mouseMoveEvent methods to apply the scroll, and correct using the axes ranges if necessary:
#include <QtWidgets>
#include <QtCharts>
#include <algorithm>
QT_CHARTS_USE_NAMESPACE
class ChartView: public QChartView{
public:
using QChartView::QChartView;
void setRange(qreal xmin, qreal xmax, qreal ymin, qreal ymax){
if(!chart()) return;
if(QValueAxis *xaxis = qobject_cast<QValueAxis *>(chart()->axes(Qt::Horizontal).first())){
xaxis->setRange(xmin, xmax);
}
if(QValueAxis *yaxis = qobject_cast<QValueAxis *>(chart()->axes(Qt::Vertical).first())){
yaxis->setRange(ymin, ymax);
}
}
void setLimits(qreal min, qreal max, Qt::Orientation orientation){
m_limit_min = min;
m_limit_max = max;
m_orientation = orientation;
}
protected:
void mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && chart())
m_lastMousePos = mapToScene(event->pos());
QGraphicsView::mousePressEvent(event);
}
void mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() & Qt::LeftButton && chart()){
QPointF newValue = mapToScene(event->pos());
QPointF delta = newValue - m_lastMousePos;
if(m_orientation == Qt::Horizontal)
chart()->scroll(-delta.x(), 0);
else
chart()->scroll(0, -delta.y());
if(QValueAxis * axis = qobject_cast<QValueAxis *>(chart()->axes(m_orientation).first()) ){
qreal deltaX = axis->max() - axis->min();
if(axis->min() < m_limit_min){
axis->setRange(m_limit_min, m_limit_min + deltaX);
}
else if(axis->max() > m_limit_max){
axis->setRange(m_limit_max - deltaX, m_limit_max);
}
}
m_lastMousePos = newValue;
}
QGraphicsView::mouseMoveEvent(event);
}
private:
QPointF m_lastMousePos;
qreal m_limit_min;
qreal m_limit_max;
Qt::Orientation m_orientation;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
ChartView chartView;
chartView.setRenderHint(QPainter::Antialiasing);
chartView.resize(640, 480);
QLineSeries *series = new QLineSeries();
series->append(0, 6);
series->append(2, 4);
series->append(3, 8);
series->append(7, 4);
series->append(10, 5);
*series << QPointF(11, 1) << QPointF(13, 3) << QPointF(17, 6) << QPointF(18, 3) << QPointF(20, 2);
QChart *chart = chartView.chart();
chart->legend()->hide();
chart->addSeries(series);
chart->createDefaultAxes();
chartView.show();
chartView.setRange(5, 15, 0, 8);
chartView.setLimits(0, 20, Qt::Horizontal);
return a.exec();
}

Animate ellipses along QPainterPath

I have a bunch of ellipses that initially are lined on top of a path and should move along the QPainterPath. I have it working for the first ellipse but I can't figure out how to get the correct position for the other ellipses.
Is there a way to check if it passed the end of the path and move it back to the beginning?
class Animation : public QAbstractAnimation
{
public:
Animation(const QPainterPath& path, QObject *parent = Q_NULLPTR);
virtual void updateCurrentTime(int ms) override;
virtual int duration() const override;
QPainterPath mPath;
QVector<EllipseGraphicsItem*> mAnimationElements;
};
Animation::Animation (const QPainterPath& path, QObject *parent) : QAbstractAnimation(parent)
, mPath(path)
{
qreal pos = 0;
qreal length = mPath.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
EllipseGraphicsItem * item = new EllipseGraphicsItem(parentItem());
mAnimationElements.append(item);
item->setPos(pointAtPercent);
}
}
void Animation::updateCurrentTime(int ms)
{
QPointF point = mPath.pointAtPercent(qreal(ms) / 6000);
if (mAnimationElements.size() > 0)
mAnimationElements[0]->setPos(point);
for (int i = 0; i < mAnimationElements.size(); i++) {
// how to update each circle's position?
}
}
Start the animation:
QPainterPath path;
path.moveTo(10, 10);
path.lineTo(QPointF(500, 10));
path.lineTo(QPointF(500, 700));
path.lineTo(QPointF(10, 700));
Animation *animation = new Animation(path, this);
animation->setLoopCount(-1);
animation->start();
Imho, it would be easier to use a QGraphicsObject with a QPropertyAnimation:
Use a property that varies between 0 and the length of the path and place your elements by calculating their positions from its value and their position in the list.
A quick example :
class AnimatedEllipses: public QGraphicsObject
{
Q_OBJECT
Q_PROPERTY(int progress READ progress WRITE setProgress)
private:
QGraphicsPathItem path;
QList<QGraphicsEllipseItem*> ellipses;
int propProgress;
public:
int progress() const { return propProgress;}
void setProgress(int value)
{
propProgress = value;
int index = 0;
for (QGraphicsEllipseItem* ellipse: ellipses)
{
// Keep value between 0 and length.
int lgt = (propProgress + index * 40) % int(path.path().length());
qreal percent = path.path().percentAtLength(lgt);
++index;
ellipse->setPos(path.path().pointAtPercent(percent));
}
}
AnimatedEllipses(QPainterPath const& path): QGraphicsObject(), path(path), propProgress(0)
{
qreal pos = 0;
qreal length = path.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-10, -10, 20, 20, this);
item->setPos(pointAtPercent);
ellipses << item;
}
QPropertyAnimation* animation = new QPropertyAnimation(this, "progress");
animation->setStartValue(0);
animation->setEndValue(length);
animation->setDuration(10000);
animation->setLoopCount(-1);
animation->start();
}
// QGraphicsItem interface
public:
QRectF boundingRect() const { return path.boundingRect();}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget){}
};
The modulo allows you to create an infinte loop for each ellipse.

QChart how to get points that are displayed while zoomed in

The QChart contains some series.
Is it possible to retrieve the list of points that are currently visible on the chart while the chart is zoomed in ?
Means getting a list/vector of point of the actually displayed points.
Digested and simplified from Kuba answer:
QList<QVector<QPointF>> XXX::getDisplayedPoints(QChart *chart)
{
QList<QVector<QPointF>> result;
foreach (QAbstractSeries * series, chart->series())
{
QVector<QPointF> vector;
auto inScene = chart->plotArea();
auto inChart = chart->mapFromScene(inScene);
auto inChartRect = inChart.boundingRect();
auto inItem1 = chart->mapToValue(inChartRect.topLeft(), series);
auto inItem2 = chart->mapToValue(inChartRect.bottomRight(), series);
QRectF rect = QRectF(inItem1, inItem2).normalized();
const QVector<QPointF> points = static_cast<QLineSeries*>(series)->pointsVector();
std::copy_if(points.begin(), points.end(), std::back_inserter(vector),
[rect](QPointF const &p) { return rect.contains(p); });
result.append(vector);
}
return result;
}
Can be called from signal
connect(static_cast<QValueAxis *>(m_chart->axisX()), &QValueAxis::rangeChanged, this, &XXX::on_zoomUpdated);
You need to map the coordinates of the opposite corners of the chart's plotArea to the world coordinate system of the series you show, and then iterate the series to extract the points fitting that rectangle.
// https://github.com/KubaO/stackoverflown/tree/master/questionschart-visible-points-52777058
#include <QtCharts>
#include <algorithm>
#include <cmath>
auto seriesRect(QChart *chart, QAbstractSeries *series = nullptr) {
auto inScene = chart->plotArea();
auto inChart = chart->mapFromScene(inScene);
auto inChartRect = inChart.boundingRect();
auto inItem1 = chart->mapToValue(inChartRect.topLeft(), series);
auto inItem2 = chart->mapToValue(inChartRect.bottomRight(), series);
return QRectF(inItem1, inItem2).normalized();
}
auto pointsInRect(QXYSeries *series, const QRectF &rect) {
QVector<QPointF> result;
auto const points = series->pointsVector();
std::copy_if(points.begin(), points.end(), std::back_inserter(result),
[rect](auto &p) { return rect.contains(p); });
return result;
}
Rest of the example:
auto data() {
QVector<QPointF> result;
for (auto x = 0.; x < 10.; x += 0.1) result.append({x, exp(x)});
return result;
}
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QWidget ui;
QVBoxLayout layout(&ui);
QChartView view1, view2;
QLabel status;
layout.addWidget(&view1);
layout.addWidget(&view2);
layout.addWidget(&status);
layout.setMargin(4);
QLineSeries series;
series.replace(data());
auto *chart = view1.chart();
chart->addSeries(&series);
view1.setRubberBand(QChartView::RectangleRubberBand);
QLineSeries subSeries;
subSeries.setPointsVisible(true);
auto *subChart = view2.chart();
subChart->addSeries(&subSeries);
for (auto *chart : {view1.chart(), view2.chart()}) {
chart->legend()->hide();
chart->createDefaultAxes();
chart->layout()->setContentsMargins(0, 0, 0, 0);
}
auto update = [&] {
auto rect = seriesRect(chart, &series);
auto const points = pointsInRect(&series, rect);
status.setText(QStringLiteral("Visible Range: (%1,%2)-(%3,%4)")
.arg(rect.left())
.arg(rect.top())
.arg(rect.right())
.arg(rect.bottom()));
subSeries.replace(points);
subChart->axisX(&subSeries)->setRange(rect.left(), rect.right());
subChart->axisY(&subSeries)->setRange(rect.top(), rect.bottom());
};
QObject::connect(chart, &QChart::plotAreaChanged, update);
ui.setMinimumSize(400, 400);
ui.show();
return a.exec();
}
For the mapping itself, see this answer.

How to align Graphics items in Graphic scene based on first selected item?

I have Graphic scene in that i have to alight right,left,top or bottom based on first selected item(Reference Item). i searched i got some code but in this its aligning to scene right position. I have to align items based on first selected item. how can i do this?
void GraphicScene::ItemsRightAlign()
{
if (selectedItems().isEmpty())
return;
QRectF refRect = selectedItems().first()->boundingRect();
QList<QGraphicsItem*> sel =selectedItems(); // for example
foreach(QGraphicsItem* selItem, sel)
{
qreal dx = 0, dy = 0;
QRectF itemRect = selItem->mapToScene(selItem->boundingRect()).boundingRect();
//if(align_right)
dx = refRect.right() - itemRect.right();
qDebug() << "item pos "<< dx << dy << selItem->mapToScene(selItem->boundingRect()).boundingRect() ;
selItem->moveBy(dx, dy);
}
}
For more details
Output should be like this output
The resolution method is to map the point that determines the right, left, up, down to the scene of the first item and the other items obtaining the difference that must be compensated.
graphicsscene.h
#ifndef GRAPHICSSCENE_H
#define GRAPHICSSCENE_H
#include <QGraphicsScene>
#include <QGraphicsItem>
class GraphicsScene: public QGraphicsScene{
Q_OBJECT
public:
GraphicsScene(QObject *parent=nullptr);
void moveSelecteds(Qt::Alignment aligment);
private slots:
void onSelectionChanged();
private:
void move(QGraphicsItem *ref, QList<QGraphicsItem *> others, Qt::Alignment aligment);
QGraphicsItem *mRef;
};
#endif // GRAPHICSSCENE_H
graphicsscene.cpp
#include "graphicsscene.h"
GraphicsScene::GraphicsScene(QObject *parent):
QGraphicsScene(parent),
mRef(nullptr)
{
connect(this, &GraphicsScene::selectionChanged, this, &GraphicsScene::onSelectionChanged);
}
void GraphicsScene::moveSelecteds(Qt::Alignment aligment){
QList<QGraphicsItem *> its= selectedItems();
if(its.size() < 2)
return;
if(!its.removeOne(mRef))
return;
move(mRef, its, aligment);
}
void GraphicsScene::onSelectionChanged(){
if(selectedItems().isEmpty()){
mRef = nullptr;
}
else if(selectedItems().size() == 1){
mRef = selectedItems().first();
}
}
void GraphicsScene::move(QGraphicsItem *ref, QList<QGraphicsItem *> others, Qt::Alignment aligment){
QPointF p;
switch (aligment) {
case Qt::AlignLeft:
p = QPointF(ref->mapToScene(ref->boundingRect().topLeft()).x(), 0);
break;
case Qt::AlignRight:
p = QPointF(ref->mapToScene(ref->boundingRect().topRight()).x(), 0);
break;
case Qt::AlignTop:
p = QPointF(0, ref->mapToScene(ref->boundingRect().topLeft()).y());
break;
case Qt::AlignBottom:
p = QPointF(0, ref->mapToScene(ref->boundingRect().bottomLeft()).y());
break;
}
for(QGraphicsItem *o: others){
QPointF delta;
switch (aligment) {
case Qt::AlignLeft:{
delta = p - QPointF(o->mapToScene(o->boundingRect().topLeft()).x(), 0);
break;
}
case Qt::AlignRight:{
delta = p - QPointF(o->mapToScene(o->boundingRect().topRight()).x(), 0);
break;
}
case Qt::AlignTop:{
delta = p - QPointF(0, o->mapToScene(o->boundingRect().topLeft()).y());
break;
}
case Qt::AlignBottom:{
delta = p - QPointF(0, o->mapToScene(o->boundingRect().bottomLeft()).y());
break;
}
}
o->moveBy(delta.x(), delta.y());
}
}
In this example you can use the up, down, left, right keys to move the items.
main.cpp
#include "graphicsscene.h"
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QShortcut>
#include <random>
static void create_items(QGraphicsScene & scene){
std::default_random_engine generator;
std::uniform_int_distribution<int> dist_size(30, 40);
std::uniform_int_distribution<int> dist_pos(-50, 50);
for(const QString & colorname : {"red", "green", "blue", "gray", "orange"}){
QRectF r(QPointF(dist_pos(generator), dist_pos(generator)),
QSizeF(dist_size(generator), dist_size(generator)));
auto item = new QGraphicsRectItem(r);
item->setPos(QPointF(dist_pos(generator), dist_pos(generator)));
item->setBrush(QColor(colorname));
item->setFlag(QGraphicsItem::ItemIsSelectable);
scene.addItem(item);
}
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
GraphicsScene scene;
create_items(scene);
QGraphicsView view(&scene);
const QList<QPair<Qt::Key, Qt::Alignment>> k_a {
{Qt::Key_Up, Qt::AlignTop},
{Qt::Key_Down, Qt::AlignBottom},
{Qt::Key_Left, Qt::AlignLeft},
{Qt::Key_Right, Qt::AlignRight}
};
for(const QPair<Qt::Key, Qt::Alignment> & p : k_a){
QShortcut *shorcut = new QShortcut(p.first, &view);
QObject::connect(shorcut, &QShortcut::activated, std::bind(&GraphicsScene::moveSelecteds, &scene, p.second));
}
view.resize(640, 480);
view.show();
return a.exec();
}
The complete example can be found in the following link.
Replace topLeft.x -- for Right align with topRight.x ,For Top Align replace topLeft.y and dx with dy,For Bottom Align replace bottomLeft.y and dx with dy
void GraphicScene::ItemsLeftAlign
{
if (selectedItems().isEmpty())
return;
QGraphicsItem *FirstSelItem = selectedItems().first();
QList<QGraphicsItem*> sel =selectedItems(); // for example
foreach(QGraphicsItem* selItem, sel)
{
qreal dx = 0, dy = 0;
dx = (FirstSelItem->mapToScene(FirstSelItem->boundingRect().topLeft()).x()) -
(selItem->mapToScene(selItem->boundingRect().topLeft()).x());
selItem->moveBy(dx, dy);
}
}

Finding the point of intersection between a line and a QPainterPath

I'm trying to determine the point where a hitscan projectile's path (basically a line, but I've represented it as a QPainterPath in my example) intersects with an item in my scene. I am not sure if there is a way to find this point using the functions provided by QPainterPath, QLineF, etc. The code below illustrates what I'm trying to do:
#include <QtWidgets>
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// hitPos = /* the point at which projectilePath hits targetShape */
hitPos = closest->pos(); // incorrect; always gives top left
qDebug() << projectilePath.intersects(targetShape); // true
qDebug() << projectilePath.intersected(targetShape); // QPainterPath: Element count=0
// To show that they do actually intersect..
QPen p1(Qt::green);
p1.setWidth(2);
QPen p2(Qt::blue);
p2.setWidth(2);
scene->addPath(projectilePath, p1);
scene->addPath(targetShape, p2);
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
view.setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
projectilePath.intersects(targetShape) returns true, but projectilePath.intersected(targetShape) returns an empty path.
Is there a way to achieve this?
As the answer to Intersection point of QPainterPath and line (find QPainterPath y by x) points out, QPainterPath::intersected() only accounts for paths which have fill areas. The rectangular path trick which is also mentioned there can be implemented like this:
#include <QtWidgets>
/*!
Returns the closest element (position) in \a sourcePath to \a target,
using \l{QPoint::manhattanLength()} to determine the distances.
*/
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance = sourcePath.elementAt(0) - target;
qreal shortestLength = shortestDistance.manhattanLength();
for (int i = 1; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}
/*!
Returns \c true if \a projectilePath intersects with any items in \a scene,
setting \a hitPos to the position of the intersection.
*/
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// QLineF has normalVector(), which is useful for extending our path to a rectangle.
// The path needs to be a rectangle, as QPainterPath::intersected() only accounts
// for intersections between fill areas, which projectilePath doesn't have.
QLineF pathAsLine(projectileStartPos, projectilePath.elementAt(1));
// Extend the first point in the path out by 1 pixel.
QLineF startEdge = pathAsLine.normalVector();
startEdge.setLength(1);
// Swap the points in the line so the normal vector is at the other end of the line.
pathAsLine.setPoints(pathAsLine.p2(), pathAsLine.p1());
QLineF endEdge = pathAsLine.normalVector();
// The end point is currently pointing the wrong way; move it to face the same
// direction as startEdge.
endEdge.setLength(-1);
// Now we can create a rectangle from our edges.
QPainterPath rectPath(startEdge.p1());
rectPath.lineTo(startEdge.p2());
rectPath.lineTo(endEdge.p2());
rectPath.lineTo(endEdge.p1());
rectPath.lineTo(startEdge.p1());
// Visualize the rectangle that we created.
scene->addPath(rectPath, QPen(QBrush(Qt::blue), 2));
// Visualize the intersection of the rectangle with the item.
scene->addPath(targetShape.intersected(rectPath), QPen(QBrush(Qt::cyan), 2));
// The hit position will be the element (point) of the rectangle that is the
// closest to where the projectile was fired from.
hitPos = closestPointTo(projectileStartPos, targetShape.intersected(rectPath));
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
This has pretty good precision (± 1 pixel, since QLineF::length() is an integer), but there might be a neater way to achieve the same thing.
Just for the record (and if someone else steps here). The above answer is excellent. There's just a little bug in the closestPoint function that may happens if the first point is already the closest one. It should return elementAt(0) instead of elementAt(0) - target.
Here is the fixed function:
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance;
qreal shortestLength = std::numeric_limits<int>::max();
for (int i = 0; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}

Resources