65.9K
CodeProject 正在变化。 阅读更多。
Home

为机器人构建用户界面 - 第二部分 b。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2014年10月26日

CPOL

11分钟阅读

viewsIcon

19689

downloadIcon

152

第二篇文章的续篇,我们将介绍 3D 环境和一些更复杂的 Visualization ToolKit (VTK) 主题。

第 2b 部分 - VTK 可视化

本文讨论了 VTK 中更复杂的主题,每个主题都针对 3D 环境的一个用例——复杂模型、地形和传感器可视化。项目中包含了两种不同类型的传感器可视化——一个视频流屏幕和一个 LIDAR 扫描仪点云。上一篇文章讨论了如何在 VTK 中设置基本环境,即如何创建窗口和渲染器,构建一些基本图元,并将它们包含在场景中。这将用于为环境构建一个完整的“Bot”类。随着项目的不断扩大,将完整的代码示例复制到文章中变得困难。因此,从本文开始,将只讨论代码片段,以节省篇幅。本文讨论的每个主题几乎都是一个独立的问题。讨论的主题如下:

  1. 我有基本模型(球体、圆柱体等),但我想导入复杂模型,例如来自 Blender3DS Max 的模型,我该怎么做?

    • 如何为模型指定纹理?

  2. 如何在我的环境中包含地形(也称为高度图)?

  3. 如何可视化机器人传感器信息?以下内容用作示例:

    • 机器人的视频流屏幕

    • 机器人的 LIDAR

导入复杂模型

在各种项目中,您可能需要包含复杂的道具模型或数据源。对于道具,这些模型可以在 Blender 或 3DS Max 中加载,并导出为合适的格式(本项目中为 STL 和 OBJ 格式)。本节将介绍如何加载这些模型并将它们添加到可视化环境中。要从文件加载模型,您需要:

  1. 将文件导出为可导入的文件格式

    1. 有多种不同的文件格式可供加载——大多数格式在 ParaView 用户指南 中有描述。一般来说,我一直使用立体光刻(*.STL)和 WaveFront(*.OBJ)格式,因为它们似乎很容易导入。

    2. 在查找模型方面,您可以在 TurboSquid 上找到大量免费模型,它们采用常见的文件格式。

    3. 您可以使用 Blender(免费)、MilkShape(便宜)或 3DS Max(便宜……如果您开着 Veyron)将文件导出为可读格式。

    4. 对于本项目,我们将“上”设置为 Y 轴,“前”设置为 Z 轴,因此在导出之前,您可能需要旋转模型,使其沿 Y 轴向上,沿 Z 轴向前。

    5. 最后,在导出模型之前,请检查模型是否位于原点,否则它将具有一个奇怪的偏移量,并且将围绕该偏移量旋转。

  2. 创建一个新的 Python 模块/类,它继承自“SceneObject”类。

  3. 在构造函数(__init__() 方法)中:

    1. 创建用于读取文件的特定读取器。本项目中使用 OBJ 或 STL 文件,因此两个可能的读取器分别是“vtkOBJReader”和“vtkSTLReader”。如果您使用的是其他格式,您需要在 VTK 类文档 中找到相应的读取器。

    2. 将读取器指向您的文件。

    3. 使用“vtkTransform”旋转/缩放模型,以防我们导出错误(对我来说,这种情况每次都会发生)。

    4. 为模型创建一个映射器(mapper)。

    5. 将映射器分配给继承自“SceneObject”类的现有“vtkActor”。

    6. 每个类的输入和输出的连接方式如下:读取器 -> 变换 -> 映射器 -> Actor。

  4. 模型将随之绘制并在场景中移动,如果我们使用上一篇文章中讨论的设置器和获取器来设置其位置。

在本项目的这部分中,我们将使用这一系列步骤将一个 Bot 模型添加到场景中(位于“scene”文件夹中的“Bot.py”)。该模型是从 TF3DM 下载并在 Blender 中进行清理的,但方向不正确,因此变换过滤器很有用。

快速讨论“Bot.py”中的步骤:

  • 创建一个继承自“SceneObject.py”模块的 bot 类。

class Bot(SceneObject):
    '''
    A template for loading complex models.
    '''
  • 将文件名设置为模型文件在硬盘上的位置,该文件位于场景目录的 media 文件夹中,然后创建一个文件读取器。

        # Call the parent constructor
        super(Bot,self).__init__(renderer)

        filename = "../scene/media/bot.stl"

        reader = vtk.vtkSTLReader()
        reader.SetFileName(filename)
  • 创建一个变换,设置旋转,使机器人沿 Z 轴向前,并将读取器的输出连接到变换的输入。

        # Do the internal transforms to make it look along unit-z, do it with a quick transform
        trans = vtk.vtkTransform()
        trans.RotateZ(90)
        transF = vtk.vtkTransformPolyDataFilter()
        transF.SetInputConnection(reader.GetOutputPort())
        transF.SetTransform(trans)
  • 创建一个映射器,将映射器的输入设置为变换的输出,并将“SceneObject”的 actor 设置为使用最终的映射器。

        # Create a mapper
        self.__mapper = vtk.vtkPolyDataMapper()
        self.__mapper.SetInputConnection(transF.GetOutputPort())

        self.vtkActor.SetMapper(self.__mapper)
  • 调用一个(当前为空)方法,该方法将添加此模型的子项,并将模型重置到世界原点,以便子项能够更新。

        # Set up all the children for this model
        self.__setupChildren(renderer)
        # Set it to [0,0,0] so that it updates all the children
        self.SetPositionVec3([0, 0, 0])
  • 此方法用于添加视频流和 LIDAR 传感器。如果您查看最终的项目文件,它们已经被填充——这将随着我们介绍传感器而完成。空方法定义如下:

    def __setupChildren(self, renderer):
        '''
        Configure the children for this bot - camera and other sensors.
        '''
        return

要在主程序中创建它,您只需要实例化一个 bot 类,并传入当前的渲染器(在本例中,我将几个机器人放在一个圆圈中)。

...
    # Initialize a set of test bots
    numBots = 8
    bots = []
    for i in xrange(0, numBots):
        bot = Bot.Bot(renderer)
        # Put the bot in a cool location
        location = [10 * cos(i / float(numBots) * 6.242), 0, 10 * sin(i / float(numBots) * 6.242)]
        bot.SetPositionVec3(location)

        # Make them all look outward
        yRot = 90.0 - i / float(numBots) * 360.0
        bot.SetOrientationVec3([0, yRot, 0])

        bots.append(bot)
....

添加纹理

在某些情况下,您可能希望为 UV 映射模型添加纹理。当前 bot 模型并未实现此功能,但稍后将用于视频流。

  • 文本化非常直接,只需在 bot 类的 init 方法中添加一小段代码:

        textReader = vtk.vtkPNGReader()
        textReader.SetFileName("[MY TEXTURE]")
        self.texture = vtk.vtkTexture()
        self.texture.SetInputConnection(textReader.GetOutputPort())
        self.texture.InterpolateOn()
  • 最后,将纹理分配给 actor 以应用它。

        self.vtkActor.SetTexture(self.texture)

创建地形

地形就相当于表面的 3D 图。VTK 示例中有一个很好的表面绘图示例——“expCos.py”——本项目中的地形就是基于此构建的。要创建地形:

  1. 使用平面源(plane source)创建一个平坦的细分表面。

  2. 该源将提供一个与 XY 平面对齐的表面,该表面面向屏幕,因此使用另一个变换过滤器将其放置在地面(XZ 平面)上。

  3. 使用变形过滤器(warp filter)设置每个点的高度。

    1. 创建一个可编程过滤器(programmable filter),用于为地形上的每个高度点设置标量值——在本项目中,我们使用一个简单的圆形公式来为表面增加一些定义。

    2. 您可以轻松替换此步骤,并从纹理加载高度图。如果您想要代码片段,请告诉我。

    3. 使用变形过滤器根据标量值中找到的高度来变形地形。

    4. 应用一个映射器,该映射器将使用标量值进行着色,并告知其地形的高度范围,以便正确着色。

如前所述,您可以轻松地通过从纹理加载地形高度来替换第三步。

此代码可以在“scene”文件夹中的“Terrain.py”文件中找到。对实现的简要讨论:

  • “Terrain.py”类的初始化函数会接收一个“surfaceSize”参数,用于地形的总宽度和高度。

  • 使用指定的参数创建平面源,将其缩放到“surfaceSize”参数,并旋转它使其成为地面平面。

...
        # We create a 'surfaceSize' by 'surfaceSize' point plane to sample
        plane = vtk.vtkPlaneSource()
        plane.SetXResolution(surfaceSize)
        plane.SetYResolution(surfaceSize)

        # We transform the plane by a factor of 'surfaceSize' on X and Y
        transform = vtk.vtkTransform()
        transform.Scale(surfaceSize, surfaceSize, 1)
        transF = vtk.vtkTransformPolyDataFilter()
        transF.SetInputConnection(plane.GetOutputPort())
        transF.SetTransform(transform)
  • 将一个可编程过滤器连接到变换的输出。

...
        # Compute the function that we use for the height generation.
        # [Original comment] Note the unusual GetPolyDataInput() & GetOutputPort() methods.
        surfaceF = vtk.vtkProgrammableFilter()
        surfaceF.SetInputConnection(transF.GetOutputPort())
  • 为可编程过滤器创建一个函数(此处使用任意的圆形公式),并将其分配给可编程过滤器。

        # [Original comment] The SetExecuteMethod takes a Python function as an argument
        # In here is where all the processing is done.
        def surfaceFunction():
            input = surfaceF.GetPolyDataInput()
            numPts = input.GetNumberOfPoints()
            newPts = vtk.vtkPoints()
            derivs = vtk.vtkFloatArray()

            for i in range(0, numPts):
                x = input.GetPoint(i)
                x, z = x[:2] # Get the XY plane point, which we'll make an XZ plane point so that's it a ground surface - this is a convenient point to remap it...

                # Now do your surface construction here, which we'll just make an arbitrary wavy surface for now.
                y = sin(x / float(surfaceSize) * 6.282) * cos(z / float(surfaceSize) * 6.282)

                newPts.InsertPoint(i, x, y, z)
                derivs.InsertValue(i, y)

            surfaceF.GetPolyDataOutput().CopyStructure(input)
            surfaceF.GetPolyDataOutput().SetPoints(newPts)
            surfaceF.GetPolyDataOutput().GetPointData().SetScalars(derivs)

        surfaceF.SetExecuteMethod(surfaceFunction)
  • 变形表面(通过将变形过滤器连接到可编程过滤器的输出)。

        # We warp the plane based on the scalar values calculated above
        warp = vtk.vtkWarpScalar()
        warp.SetInputConnection(surfaceF.GetOutputPort())
        warp.XYPlaneOn()
  • 创建映射器(默认情况下,它会将标量数据解释为颜色),并告知它将最亮/最暗点设置为地形的最大/最小高度。

        # Set the range of the colour mapper to the function min/max we used to generate the terrain.
        mapper = vtk.vtkPolyDataMapper()
        mapper.SetInputConnection(warp.GetOutputPort())
        mapper.SetScalarRange(-1, 1)
  • 最后,将地形设置为线框(严格可选,但会让它看起来很有 90 年代的复古风格),并将映射器分配给“SceneObject”actor 以便绘制。
        # Make our terrain wireframe so that it doesn't occlude the whole scene
        self.vtkActor.GetProperty().SetRepresentationToWireframe()

        # Finally assign this to the parent class actor so that it draws.
        self.vtkActor.SetMapper(mapper)

要在“bot_vis_main.py”中将其添加到场景中,使用了与之前相同的方法(这几乎是标准做法,但在这里我们为其提供了一个大小参数——此处设置为 100)。

...
    # Create our new scene objects...
    terrain = Terrain.Terrain(renderer, 100)
...

构建传感器显示

添加到场景的最后一项将是机器人的一些传感器显示。本项目假设我们有来自机器人的视频流和 LIDAR 流,并且希望在 3D 环境中表示它们。每个传感器显示都以与先前示例相同的方式构建,即继承“SceneObject”类,但它们都在实现上增加了一点复杂性。

  • 视频屏幕流将包含一个视频流纹理(在前面几部分中简要讨论过)。
  • LIDAR 流将利用点云来绘制深度图像。

视频流屏幕

视频流屏幕的代码可以在“scene”项目文件夹中的“CameraScreen.py”文件中找到。

这里对代码进行简要介绍:

  • 像创建地形一样,创建一个平面源。
...
    def __init__(self, renderer, screenDistance, width, height):
        '''
        Initialize the CameraScreen model.
        '''
        # Call the parent constructor
        super(CameraScreen,self).__init__(renderer)

        # Create a plane for the camera
        # Ref: http://www.vtk.org/doc/nightly/html/classvtkPlaneSource.html
        planeSource = vtk.vtkPlaneSource()
  • 平面将面向 XZ 轴,我们希望将其沿 Z 轴“推”一点,这样当它位于机器人前方时,它会比机器人“screenDistance”单位远(在它前面)。
        # Defaults work for this, so just push it out a bit
        #planeSource.Push(screenDistance)
  • 再次使用变换将其缩放到指定的视频大小(“CameraScreen.py”构造函数的两个参数),并将其翻转,使其不会被镜像绘制。
        # Transform scale it to the right size
        trans = vtk.vtkTransform()
        trans.Scale(width, height, 1)
        trans.Translate(0, 0, screenDistance)
        trans.RotateY(180) # Effectively flipping the UV (texture) mapping so that the video isn't left/right flipped
        transF = vtk.vtkTransformPolyDataFilter()
        transF.SetInputConnection(planeSource.GetOutputPort())
        transF.SetTransform(trans)
  • 读取纹理。
        # Create a test picture and assign it to the screen for now...
        # Ref: http://vtk.org/gitweb?p=VTK.git;a=blob;f=Examples/Rendering/Python/TPlane.py
        textReader = vtk.vtkPNGReader()
        textReader.SetFileName("../scene/media/semisortedcameralogo.png")
        self.cameraVtkTexture = vtk.vtkTexture()
        self.cameraVtkTexture.SetInputConnection(textReader.GetOutputPort())
        self.cameraVtkTexture.InterpolateOn()
  • 最后,创建一个映射器(与地形示例相同),并将纹理和映射器分配给 actor。
        # Finally assign the mapper and the actor
        planeMapper = vtk.vtkPolyDataMapper()
        planeMapper.SetInputConnection(transF.GetOutputPort())

        self.vtkActor.SetMapper(planeMapper)
        self.vtkActor.SetTexture(self.cameraVtkTexture)

接下来,我们需要编辑 bot 类。创建一个摄像头并将其分配为 bot 的子项之一(请记住,“SceneObject.py”具有位置和方向绑定的子项)。这在“Bot.__setupChildren()”方法中完成。

...
    def __setupChildren(self, renderer):
        '''
        Configure the children for this bot - camera and other sensors.
        '''
        # Create a camera screen and set the child's offset.
        self.camScreen = CameraScreen.CameraScreen(renderer, 3, 4, 3)
        self.camScreen.childPositionOffset = [0, 2.5, 0]
        # Add it to the bot's children
        self.childrenObjects.append(self.camScreen)
...

如果想不断更新视频流,这里有一个小提示:如果更改了摄像头的纹理(“CameraScreen.cameraVtkTexture”),它将在视频流屏幕上绘制更新后的纹理。您可能需要调用源的 update 方法,让 VTK 知道数据已更改。

LIDAR 流可视化

要添加的最后一个传感器是 LIDAR 流。这与其他项目有点不同——应视为可选附加项——因为它比其他情况更复杂一些。它可以在 scene 子文件夹的“LIDAR.py”中找到。在这种情况下:

  • 假设我们将获得一个 2D 点矩阵,每个点表示在特定点上的激光深度测量值。
  • 我们必须将此 2D 矩阵从极坐标转换,并将其投影到世界空间中作为半球形深度图。
  • 扫描仪将同时测量水平和垂直方向。
    • 水平轴从最小角度“minTheta”扫描到最大角度“maxTheta”,中间有“numThetaReadings”次读数。
    • 垂直轴从最小角度“minPhi”扫描到最大角度“maxPhi”,中间有“numPhiReadings”次读数。
  • 每次深度读数都将进行颜色编码(类似于地形),分别具有最小和最大深度值“minDepth”和“maxDepth”。
  • 我们需要将深度值设置为某个初始值,直到我们获得第一次扫描,这称为“initialValue”。

大部分工作在初始化方法中完成,如下所示。它接收这些参数并生成点云数据结构。对于这个模板,我一直在尝试将 2D 测量矩阵转换为可视化半球,因此我提供了一个名为“UpdatePoints()”的方法,该方法接收一个 NumPy 矩阵并将所有点放置在可视化所需的位置。对“LIDAR.py”代码的简要讨论:

  • 构造函数初始化了点云使用的数据结构(点、深度值和用于表示数据的单元)。
...
    def __init__(self, renderer, minTheta, maxTheta, numThetaReadings, minPhi, maxPhi, numPhiReadings, minDepth, maxDepth, initialDepth):
...
        # Cache these parameters
        self.numPhiReadings = numPhiReadings
        self.numThetaReadings = numThetaReadings
        self.thetaRange = [minTheta, maxTheta]
        self.phiRange = [minPhi, maxPhi]
        # Create a point cloud with the data
        self.vtkPointCloudPoints = vtk.vtkPoints()
        self.vtkPointCloudDepth = vtk.vtkDoubleArray()
        self.vtkPointCloudDepth.SetName("DepthArray")
        self.vtkPointCloudCells = vtk.vtkCellArray()
        self.vtkPointCloudPolyData = vtk.vtkPolyData()
        # Set up the structure
        self.vtkPointCloudPolyData.SetPoints(self.vtkPointCloudPoints)
        self.vtkPointCloudPolyData.SetVerts(self.vtkPointCloudCells)
        self.vtkPointCloudPolyData.GetPointData().SetScalars(self.vtkPointCloudDepth)
        self.vtkPointCloudPolyData.GetPointData().SetActiveScalars("DepthArray")
  • 为扫描中的每个“像素”创建具有临时值 [1,1,1] 的点,并将它们推入数据结构。然后调用 update 方法并传入初始深度值以计算测量值正确的实际世界位置。
        # Build the initial structure
        for x in xrange(0, self.numThetaReadings):
            for y in xrange(0, self.numPhiReadings):
                # Add the point
                point = [1, 1, 1]
                pointId = self.vtkPointCloudPoints.InsertNextPoint(point)
                self.vtkPointCloudDepth.InsertNextValue(1)
                self.vtkPointCloudCells.InsertNextCell(1)
                self.vtkPointCloudCells.InsertCellPoint(pointId)
        # Use the update method to initialize the points with a NumPy matrix
        initVals = numpy.ones((numThetaReadings, numPhiReadings)) * initialValue
        self.UpdatePoints(initVals)
  • 请注意,我在这里使用了 Numpy,它是 Python 组件所必需的——我很确定它已经安装好了,但如果不行,请留言。
  • 定义了一个法线映射器,并像上面一样将其连接到“SceneObject”actor。它被设置为使用点云的多边形数据(poly data)作为输入。点云结构中的标量值将用于颜色(表示深度),因此为映射器设置范围,以便为每个标量值呈现正确的深度颜色。
        # Now build the mapper and actor.
        mapper = vtk.vtkPolyDataMapper()
        mapper.SetInput(self.vtkPointCloudPolyData)
        mapper.SetColorModeToDefault()
        mapper.SetScalarRange(minDepth, maxDepth)
        mapper.SetScalarVisibility(1)
        self.vtkActor.SetMapper(mapper)
  • 最后,代码中包含了一个 update 方法的模板。传入一个 2D 深度测量矩阵,它将把测量值转换为世界中的绝对测量值。这是通过将极坐标转换为世界坐标来实现的。
    def UpdatePoints(self, points2DNPMatrix):
        '''Update the points with a 2D array that is numThetaReadings x numPhiReadings containing the depth from the source'''
        for x in xrange(0, self.numThetaReadings):
            theta = (self.thetaRange[0] + float(x) * (self.thetaRange[1] - self.thetaRange[0]) / float(self.numThetaReadings)) / 180.0 * 3.14159
            for y in xrange(0, self.numPhiReadings):
                phi = (self.phiRange[0] + float(y) * (self.phiRange[1] - self.phiRange[0]) / float(self.numPhiReadings))  / 180.0 * 3.14159

                r = points2DNPMatrix[x, y]
                # Polar coordinates to Euclidean space
                point = [r * sin(theta) * cos(phi), r * sin(phi), r * cos(theta) * cos(phi)]
                pointId = y + x * self.numPhiReadings
                self.vtkPointCloudPoints.SetPoint(pointId, point)
        self.vtkPointCloudCells.Modified()
        self.vtkPointCloudPoints.Modified()
        self.vtkPointCloudDepth.Modified()

最后,LIDAR 可视化也添加到“Bot.py”中 bot 的子项中。可视化是使用讨论的参数初始化的(这些参数取决于理论扫描仪的规格,此处仅为测试而选择),并将子对象移至机器人头部高度。

...
        # Create the LIDAR template and set it to the child's offset as well
        self.lidar = LIDAR.LIDAR(renderer, -90, 90, 180, -22.5, 22.5, 45, 5, 15, 5)
        self.lidar.childPositionOffset = [0, 2.5, 0]
        # Add it to the bot's children
        self.childrenObjects.append(self.lidar)

下一篇文章

这几乎就是本项目 3D 建模部分的全部内容了!下一篇文章将讨论交互和自定义摄像头,以便我们可以将摄像头绑定到第一人称和第三人称的机器人。机器人看到什么,我们就看到什么。

© . All rights reserved.