Search code examples
pythonpyqt5qgraphicsviewqtwidgetsqpolygon

Scaling QPolygon on its origin


I'm trying to scale a QPolygonF that is on a QGraphicsScene's QGraphicsView on its origin.

However, even after translating the polygon (poly_2) to its origin (using QPolygon.translate() and the center coordinates of the polygon received via boundingRect (x+width)/2 and (y+height)/2), the new polygon is still placed on the wrong location.

The blue polygon should be scaled according to the origin of poly_2 (please see the image below, black is the original polygon, blue polygon is the result of the code below, and the orange polygon is representing the intended outcome) enter image description here

I thought that the issue might be that coordinates are from global and should be local, yet this does solve the issue unfortunately.

Here's the code:

import PyQt5
from PyQt5 import QtCore

import sys
import PyQt5
from PyQt5.QtCore import *#QPointF, QRectF
from PyQt5.QtGui import *#QPainterPath, QPolygonF, QBrush,QPen,QFont,QColor, QTransform
from PyQt5.QtWidgets import *#QApplication, QGraphicsScene, QGraphicsView, QGraphicsSimpleTextItem


poly_2_coords= [PyQt5.QtCore.QPointF(532.35, 274.98), PyQt5.QtCore.QPointF(525.67, 281.66), PyQt5.QtCore.QPointF(518.4, 292.58), PyQt5.QtCore.QPointF(507.72, 315.49), PyQt5.QtCore.QPointF(501.22, 326.04), PyQt5.QtCore.QPointF(497.16, 328.47), PyQt5.QtCore.QPointF(495.53, 331.71), PyQt5.QtCore.QPointF(488.24, 339.02), PyQt5.QtCore.QPointF(480.94, 349.56), PyQt5.QtCore.QPointF(476.09, 360.1), PyQt5.QtCore.QPointF(476.89, 378.76), PyQt5.QtCore.QPointF(492.3, 393.35), PyQt5.QtCore.QPointF(501.22, 398.21), PyQt5.QtCore.QPointF(527.17, 398.21), PyQt5.QtCore.QPointF(535.28, 390.1), PyQt5.QtCore.QPointF(540.96, 373.89), PyQt5.QtCore.QPointF(539.64, 356.93), PyQt5.QtCore.QPointF(541.46, 329.0), PyQt5.QtCore.QPointF(543.39, 313.87), PyQt5.QtCore.QPointF(545.83, 300.89), PyQt5.QtCore.QPointF(545.83, 276.56), PyQt5.QtCore.QPointF(543.39, 267.64), PyQt5.QtCore.QPointF(537.81, 268.91)]





def main():
    app = QApplication(sys.argv)



    scene = QGraphicsScene()
    view = QGraphicsView(scene)


    pen = QPen(QColor(0, 20, 255))


    scene.addPolygon(QPolygonF(poly_2_coords))
    poly_2 = QPolygonF(poly_2_coords)
    trans = QTransform().scale(1.5,1.5)
    #poly_22 = trans.mapToPolygon(QRect(int(poly_2.boundingRect().x()),int(poly_2.boundingRect().y()),int(poly_2.boundingRect().width()),int(poly_2.boundingRect().height())))
    #trans.mapToPolygon()
    #scene.addPolygon(QPolygonF(poly_22),QPen(QColor(0, 20, 255)))

    poly_2.translate((poly_2.boundingRect().x()+poly_2.boundingRect().width())/2,(poly_2.boundingRect().y()+poly_2.boundingRect().height())/2)


    print(f'poly_2.boundingRect().x() {poly_2.boundingRect().x()}+poly_2.boundingRect().width(){poly_2.boundingRect().width()}')
    trans = QTransform().scale(1.4,1.4)
    #poly_2.setTransformOriginPoint()
    poly_22 = trans.map(poly_2)

    scene.addPolygon(poly_22,QPen(QColor(0, 20, 255)))


    view.show()

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

Edit: I've tried saving the polygon as a QGraphicsItem, and set its transformation origin point according the bbox's middle X,Y and then mapped from Global to Scene, yet no luck: the new polygon is still drawn to the wrong place.

    poly_2 = QPolygonF(poly_2_coords)
    poly = scene.addPolygon(poly_2)
    point = QPoint((poly_2.boundingRect().x()+poly_2.boundingRect().width())/2,(poly_2.boundingRect().y()+poly_2.boundingRect().height())/2)        
    poly.setTransformOriginPoint(point)
    poly.setScale(3)

If replacing point to equal only X,Y of the bounding rectangle, the result seems to be closer to what I need. However, in this case the origin point is obviously wrong. Is this just random luck that this answer seems to be closer to what I need?

enter image description here


Solution

  • Before considering the problem of the translation, there is a more important aspect that has to be considered: if you want to create a transformation based on the center of a polygon, you must find that center. That point is called centroid, the geometric center of any polygon.

    While there are simple formulas for all basic geometric shapes, finding the centroid of a (possibly irregular) polygon with an arbitrary number of vertices is a bit more complex.

    Using the arithmetic mean of vertices is not a viable option, as even in a simple square you might have multiple points on a single side, which would move the computed "center" towards those points.

    The formula can be found in the Wikipedia article linked above, while a valid python implementation is available in this answer.

    I modified the formula of that answer in order to accept a sequence of QPoints, while improving readability and performance, but the concept remains the same:

    def centroid(points):
        if len(points) < 3:
            raise ValueError('At least 3 points are required')
        # https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
        # https://en.wikipedia.org/wiki/Shoelace_formula
        # computation uses concatenated pairs from the sequence, with the
        # last point paired to the first one:
        # (p[0], p[1]), (p[1], p[2]) [...] (p[n], p[0])
        area = cx = cy = 0
        p1 = points[0]
        for p2 in points[1:] + [p1]:
            shoelace = p1.x() * p2.y() - p2.x() * p1.y()
            area += shoelace
            cx += (p1.x() + p2.x()) * shoelace
            cy += (p1.y() + p2.y()) * shoelace
            p1 = p2
        A = 0.5 * area
        factor = 1 / (6 * A)
        return cx * factor, cy * factor
    

    Then, you have two options, depending on what you want to do with the resulting item.

    Scale the item

    In this case, you create a QGraphicsPolygonItem like the original one, then set its transform origin point using the formula above, and scale it:

        poly_2 = QtGui.QPolygonF(poly_2_coords)
        item2 = scene.addPolygon(poly_2, QtGui.QPen(QtGui.QColor(0, 20, 255)))
        item2.setTransformOriginPoint(*centroid(poly_2_coords))
        item2.setScale(1.5)
    

    Use a QTransform

    With Qt transformations some special care must be taken, as scaling always uses 0, 0 as origin point.

    To scale around a specified point, you must first translate the matrix to that point, then apply the scale, and finally restore the matrix translation to its origin:

        poly_2 = QtGui.QPolygonF(poly_2_coords)
        cx, cy = centroid(poly_2_coords)
        trans = QtGui.QTransform()
        trans.translate(cx, cy)
        trans.scale(1.5, 1.5)
        trans.translate(-cx, -cy)
        poly_2_scaled = trans.map(poly_2)
        scene.addPolygon(poly_2_scaled, QtGui.QPen(QtGui.QColor(0, 20, 255)))
    

    This is exactly what QGraphicsItems do when using the basic setScale() and setRotation() transformations.


    Shape origin point and item position

    Remember that QGraphicsItems are always created with their position at 0, 0.
    This might not seem obvious especially for basic shapes: when you create a QGraphicsRectItem giving its x, y, width, height, the position will still be 0, 0. When dealing with complex geometry management, it's usually better to create basic shapes with the origin/reference at 0, 0 and then move the item at x, y.

    For complex polygons like yours, a possibility could be to translate the centroid of the polygon at 0, 0, and then move it at the actual centroid coordinates:

        item = scene.addPolygon(polygon.translated(-cx, -cy))
        item.setPos(cx, cy)
        item.setScale(1.5)
    

    This might make things easier for development (the mapped points will always be consistent with the item position), and the fact that you don't need to change the transform origin point anymore makes reverse mapping even simpler.