Search code examples
python-3.xqtpyqtpyqt5qgraphicsscene

How to properly smooth a QPainterPath?


I am working on a Path drawing tool and made a previous post about how I could achieve smoother drawing. I have done some tinkering with cubicTo() and quadTo() but these have really not been much help. I don’t know if my math is correct, or how I would even calculate a smoother drawn path. Someone on my last post recommended a series of math formulas (polynomials) to achieve a smooth path. Any code snippets are appreciated.

    def on_path_draw_start(self, event):
        # Check the button being pressed
        if event.button() == Qt.LeftButton:
            # Create a new path
            self.path = QPainterPath()
            self.path.moveTo(self.mapToScene(event.pos()))
            self.last_point = self.mapToScene(event.pos())

            # Set drag mode
            self.setDragMode(QGraphicsView.NoDrag)

    def on_path_draw(self, event):
        # Check the buttons
        if event.buttons() == Qt.LeftButton:
            # Calculate average point for smoother curve
            mid_point = (self.last_point + self.mapToScene(event.pos())) / 2.0
            
            # Use the mid_point as control point for quadTo
            self.path.quadTo(self.last_point, mid_point)
            self.last_point = self.mapToScene(event.pos())
            
            # Remove temporary path if it exists
            if self.temp_path_item:
                self.canvas.removeItem(self.temp_path_item)

            # Load temporary path as QGraphicsItem to view it while drawing
            self.temp_path_item = CustomPathItem(self.path)
            self.temp_path_item.setPen(self.pen)
            if self.button3.isChecked():
                self.temp_path_item.setBrush(QBrush(QColor(self.stroke_fill_color)))
            self.temp_path_item.setZValue(2)
            self.canvas.addItem(self.temp_path_item)

            # Create a custom tooltip for the current coords
            scene_pos = self.mapToScene(event.pos())
            QToolTip.showText(event.pos(), f'dx: {round(scene_pos.x(), 1)}, dy: {round(scene_pos.y(), 1)}')

            self.canvas.update()

    def on_path_draw_end(self, event):
        # Check the buttons
        if event.button() == Qt.LeftButton:
            # Calculate average point for smoother curve
            mid_point = (self.last_point + self.mapToScene(event.pos())) / 2.0
            
            # Use the mid_point as control point for quadTo
            self.path.quadTo(self.last_point, mid_point)
            self.last_point = self.mapToScene(event.pos())

            # Check if there is a temporary path (if so, remove it now)
            if self.temp_path_item:
                self.canvas.removeItem(self.temp_path_item)

            # If stroke fill button is checked, close the subpath
            if self.button3.isChecked():
                self.path.closeSubpath()

            self.canvas.update()

            # Load main path as QGraphicsItem
            path_item = CustomPathItem(self.path)
            path_item.setPen(self.pen)
            path_item.setZValue(0)

            # If stroke fill button is checked, set the brush
            if self.button3.isChecked():
                path_item.setBrush(QBrush(QColor(self.stroke_fill_color)))

            # Add item
            self.canvas.addItem(path_item)

            # Set Flags
            path_item.setFlag(QGraphicsItem.ItemIsSelectable)
            path_item.setFlag(QGraphicsItem.ItemIsMovable)

            # Set Tooltop
            path_item.setToolTip('MPRUN Path Element')

I understand this is a huge undertaking, but I need some guidance. I am not a math wizard by any means. Or maybe I don’t even need math, I really don’t know.


Solution

  • For those who are curious, I solved my dilema (With Math)

    I basically just implemented a smoothing algorithm with Numpy and SciPy. Here's the code:

        def smooth_path(self, path):
            vertices = [(point.x(), point.y()) for point in path.toSubpathPolygons()[0]]
            x, y = zip(*vertices)
            tck, u = splprep([x, y], s=0)
            smooth_x, smooth_y = splev(np.linspace(0, 1, len(vertices) * 2), tck)  # Reduce the granularity
    
            smoothed_vertices = np.column_stack((smooth_x, smooth_y))
            simplified_vertices = approximate_polygon(smoothed_vertices, tolerance=2.0)  # Adjust the tolerance as needed
    
            smooth_path = QPainterPath()
            smooth_path.moveTo(simplified_vertices[0][0], simplified_vertices[0][1])
    
            for i in range(1, len(simplified_vertices) - 2, 3):
                smooth_path.cubicTo(
                    simplified_vertices[i][0], simplified_vertices[i][1],
                    simplified_vertices[i + 1][0], simplified_vertices[i + 1][1],
                    simplified_vertices[i + 2][0], simplified_vertices[i + 2][1]
                )
    
            return smooth_path
    

    To make this work, you just use the function: my_graphics_path_item.smooth_path(my_graphics_path_item.path())

    And you add the item to the scene, and so on...

    You can also change the tolerance of the smoothness (I found 2.0 works best)

    I hope this helps anybody with this peculiar problem.