You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何映射不同父项的嵌套GLLinePlotItem并实现正确渲染

问题描述

我需要绘制隶属于不同父项的嵌套pyqtgraph.opengl.GLLinePlotItem,要求合并绘制后的渲染效果与未嵌套单独绘制时完全一致。具体场景为:GLViewWidget中包含两组带有不同层级变换的GLLinePlotItem集合,每组都有独立的父项与变换矩阵,希望在nestPar1的paint方法中合并所有子项的位置数组,同时保持正确的世界坐标。目前实现的变换方法仅支持平移操作,当应用缩放(如self.nestPar2.scale(-1,1,-1))时失效,现寻求正确的坐标变换方式,确保合并后的线条位置与单独绘制的目标线条完全匹配。

解决方案

核心问题是未正确计算从嵌套项局部坐标到合并项坐标系的完整变换链。要实现正确映射,需完成以下步骤:

  • 计算嵌套项的世界坐标变换矩阵:覆盖自身变换、所有父项变换的完整链
  • 计算合并项的世界坐标逆变换矩阵:将嵌套项的世界坐标转换到合并项的局部坐标系
  • 使用齐次坐标矩阵乘法处理所有 affine 变换(平移、缩放、旋转),并执行透视除法
修正后的完整代码
from PySide6 import QtWidgets, QtGui
import numpy as np, sys
from pyqtgraph.opengl import GLLinePlotItem, GLViewWidget, GLTextItem
from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem

class NestedLineItem(GLLinePlotItem):
    def __init__(self, *args, **kwargs):
        self._otherNests=[]
        self._lineItems=[]
        self.blockUpdates=False
        self.blockPaint=False
        super().__init__(*args, **kwargs)   

    def addNest(self, nestedLineItem):
        self._otherNests.append(nestedLineItem)
    
    def addLineItem(self, lineItem):
        self._lineItems.append(lineItem)
        
    def paint(self):
        if not self.blockPaint:
            combined_pos = None

            # 处理当前项的子线条
            for lineItem in self._lineItems:
                pos = self._apply_transform(lineItem.pos.copy(), lineItem.transform())
                combined_pos = pos if combined_pos is None else np.vstack((combined_pos, pos))
            
            # 计算自身到世界坐标系的变换矩阵及逆矩阵
            self_world_tr = self._get_world_transform(self)
            self_inv_tr = self_world_tr.inverted()[0]
            self_inv_matrix = self._qt_transform_to_np(self_inv_tr)

            # 处理嵌套的其他Nest项
            for nestedLineItem in self._otherNests:
                # 计算嵌套项到世界坐标系的完整变换矩阵
                nested_world_tr = self._get_world_transform(nestedLineItem)
                nested_matrix = self._qt_transform_to_np(nested_world_tr)

                for lineItem in nestedLineItem._lineItems:
                    # 1. 应用线条自身变换
                    local_pos = self._apply_transform(lineItem.pos.copy(), lineItem.transform())
                    # 2. 转换到世界坐标系
                    world_pos = self._apply_matrix(local_pos, nested_matrix)
                    # 3. 转换到当前项的局部坐标系
                    target_pos = self._apply_matrix(world_pos, self_inv_matrix)

                    combined_pos = target_pos if combined_pos is None else np.vstack((combined_pos, target_pos))
                
                self.last_child2pos = target_pos[-1]

            # 更新合并数据并绘制
            self.blockUpdates = True
            self.setData(pos=combined_pos)
            self.blockUpdates = False
            super().paint()

    def _get_world_transform(self, item):
        """递归获取item到世界坐标系的完整变换链"""
        tr = item.transform()
        parent = item.parentItem()
        while parent is not None:
            tr = parent.transform() * tr
            parent = parent.parentItem()
        return tr

    def _qt_transform_to_np(self, tr):
        """将Qt QTransform转换为numpy 4x4齐次矩阵"""
        return np.array(tr.data()).reshape(4, 4).T

    def _apply_transform(self, pos, tr):
        """对坐标应用单个Qt变换"""
        if tr.isIdentity():
            return pos
        matrix = self._qt_transform_to_np(tr)
        return self._apply_matrix(pos, matrix)

    def _apply_matrix(self, pos, matrix):
        """用齐次矩阵变换坐标,处理透视除法"""
        points_h = np.hstack([pos, np.ones((pos.shape[0], 1))])
        transformed_h = points_h @ matrix.T
        # 透视除法,确保缩放/旋转等变换生效
        transformed_h = transformed_h / transformed_h[:, 3:4]
        return transformed_h[:, :3]
    
    def update(self):
        if not self.blockUpdates:
            super().update()    

class Window(QtWidgets.QMainWindow):
    def __init__(self, *args, app=None, **kwargs):
        super().__init__(*args, **kwargs)   
        self.gl_widget = GLViewWidget()
        self.setCentralWidget(self.gl_widget)
        
        self.gP1, self.gP2, self.gP22 = GLGraphicsItem(), GLGraphicsItem(), GLGraphicsItem()        
        self.gP1.translate(0, 1, 1), self.gP2.translate(1, 0, -1), self.gP22.translate(1, 0, -1)

        self.nestPar1, self.nestPar2, self.nestPar22 = NestedLineItem(color="red", mode="lines"), NestedLineItem(color="yellow", mode="lines"), NestedLineItem(color="orange", mode="lines")
        self.nestPar1.translate(0.2, 1, 1), self.nestPar2.translate(-0.2, 0, 0.2), self.nestPar22.translate(-0.2, 0, 0.2)
        
        # 添加缩放变换测试
        self.nestPar2.scale(-1, 1, -1)
        
        self.nestPar1.setParentItem(self.gP1), self.nestPar2.setParentItem(self.gP2), self.nestPar22.setParentItem(self.gP22)
        
        lineLength = 1
        
        for z in np.linspace(0, 5, 6):
            pos1 = [[0, 0, z], [lineLength, 0, z]]
            pos2 = [[0, 1, z ], [lineLength, 1, z]]  
            pos22 = [[0, 1, z + 0.03], [lineLength, 1, z + 0.03]] # 小偏移用于对比
            l1, l2, l22 = GLLinePlotItem(pos=pos1, mode="lines"), GLLinePlotItem(pos=pos2, mode="lines"), GLLinePlotItem(pos=pos22, mode="lines")
            
            translation = -0.5, -1, 0
            
            l1.translate(*translation, True), l2.translate(*translation, True), l22.translate(*translation, True)
            self.nestPar1.addLineItem(l1), self.nestPar2.addLineItem(l2), self.nestPar22.addLineItem(l22)
        
        self.nestPar2.blockPaint=True
        self.nestPar1.addNest(self.nestPar2)
        
        self.gl_widget.addItem(self.gP1)
        self.gl_widget.addItem(self.gP22)
        
        self.showMaximized()
        self.nestPar1.paint()
        
        text2 = GLTextItem(pos=self.nestPar1.last_child2pos, text="合并后的child2位置(正确)", font=QtGui.QFont("Arial", 6))
        text2.setParentItem(self.gP1)
        
        text22 = GLTextItem(pos=pos22[-1], text="原始child2位置", font=QtGui.QFont("Arial", 6))
        text22.translate(*translation, True)
        text22.setParentItem(self.gP22)
        
        
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    win = Window()
    sys.exit(app.exec())
关键说明
  • _get_world_transform:递归遍历所有父项,生成从目标项到世界坐标系的完整变换矩阵
  • _qt_transform_to_np:将Qt变换对象转换为numpy矩阵,实现批量坐标运算
  • _apply_matrix:使用齐次坐标处理变换,并执行透视除法,解决缩放、旋转等非平移变换的失效问题
  • 变换流程:线条局部坐标 → 线条自身变换 → 嵌套项世界变换 → 合并项局部坐标,完整覆盖所有层级变换

内容的提问来源于stack exchange,提问作者THATS MY QUANT MY QUANTITATIVE

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 14:52:04