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"