def test_normalise(seed): vectors = -100 + 200 * np.random.random((200, 3)) def parallel(v1, v2): v1 = v1 / affine.veclength(v1) v2 = v2 / affine.veclength(v2) return np.isclose(np.dot(v1, v2), 1) for v in vectors: vtype = random.choice((list, tuple, np.array)) v = vtype(v) vn = affine.normalise(v) vl = affine.veclength(vn) assert np.isclose(vl, 1.0) assert parallel(v, vn) # normalise should also be able # to do multiple vectors at once results = affine.normalise(vectors) lengths = affine.veclength(results) pars = np.zeros(200) for i in range(200): v = vectors[i] r = results[i] pars[i] = parallel(v, r) assert np.all(np.isclose(lengths, 1)) assert np.all(pars)
def calcVertexNormals(vertices, indices, fnormals): """Calculates vertex normals for the mesh described by ``vertices`` and ``indices``. :arg vertices: A ``(n, 3)`` array containing the mesh vertices. :arg indices: A ``(m, 3)`` array containing the mesh triangles. :arg fnormals: A ``(m, 3)`` array containing the face/triangle normals. :returns: A ``(n, 3)`` array containing normals for every vertex in the mesh. """ vnormals = np.zeros((vertices.shape[0], 3), dtype=float) # TODO make fast. I can't figure # out how to use np.add.at to # accumulate the face normals for # each vertex. for i in range(indices.shape[0]): v0, v1, v2 = indices[i] vnormals[v0, :] += fnormals[i] vnormals[v1, :] += fnormals[i] vnormals[v2, :] += fnormals[i] # normalise to unit length return affine.normalise(vnormals)
def needsFixing(vertices, indices, fnormals, loBounds, hiBounds): """Determines whether the triangle winding order, for the mesh described by ``vertices`` and ``indices``, needs to be flipped. If this function returns ``True``, the given ``indices`` and ``fnormals`` need to be adjusted so that all face normals are facing outwards from the centre of the mesh. The necessary adjustments are as follows:: indices[:, [1, 2]] = indices[:, [2, 1]] fnormals = fnormals * -1 :arg vertices: A ``(n, 3)`` array containing the mesh vertices. :arg indices: A ``(m, 3)`` array containing the mesh triangles. :arg fnormals: A ``(m, 3)`` array containing the face/triangle normals. :arg loBounds: A ``(3, )`` array contaning the low vertex bounds. :arg hiBounds: A ``(3, )`` array contaning the high vertex bounds. :returns: ``True`` if the ``indices`` and ``fnormals`` need to be adjusted, ``False`` otherwise. """ # Define a viewpoint which is # far away from the mesh. camera = loBounds - (hiBounds - loBounds) # Find the nearest vertex # to the viewpoint dists = np.sqrt(np.sum((vertices - camera) ** 2, axis=1)) ivert = np.argmin(dists) vert = vertices[ivert] # Get all the triangles # that this vertex is in # and their face normals itris = np.where(indices == ivert)[0] norms = fnormals[itris, :] # Calculate the angle between each # normal, and a vector from the # vertex to the camera. If more than # 50% of the angles are negative # (== more than 90 degrees == the # face is facing away from the # camera), assume that we need to # flip the triangle winding order. angles = np.dot(norms, affine.normalise(camera - vert)) return ((angles >= 0).sum() / len(itris)) < 0.5
def calculateRayCastSettings(self, viewmat): """Calculates a camera direction and ray casting step vector, based on the given view matrix. """ d2tmat = self.getTransform('display', 'texture') xform = affine.concat(d2tmat, viewmat) cdir = np.array([0, 0, 1]) cdir = affine.transform(cdir, xform, vector=True) cdir = affine.normalise(cdir) # sqrt(3) so the maximum number # of samplews is taken along the # diagonal of a cube rayStep = np.sqrt(3) * cdir / self.numSteps return cdir, rayStep
def calcFaceNormals(vertices, indices): """Calculates face normals for the mesh described by ``vertices`` and ``indices``. :arg vertices: A ``(n, 3)`` array containing the mesh vertices. :arg indices: A ``(m, 3)`` array containing the mesh triangles. :returns: A ``(m, 3)`` array containing normals for every triangle in the mesh. """ v0 = vertices[indices[:, 0]] v1 = vertices[indices[:, 1]] v2 = vertices[indices[:, 2]] fnormals = np.cross((v1 - v0), (v2 - v0)) fnormals = affine.normalise(fnormals) return np.atleast_2d(fnormals)
def planeEquation2(origin, normal): """Calculates the equation of a plane equation from a normal vector and a single point on the plane. Returns a ``numpy`` array containing four values, the coefficients of the equation: See also :func:`planeEquation`. """ normal = affine.normalise(normal) ax, by, cz = np.array(origin) * normal eqn = np.zeros(4, dtype=np.float64) eqn[:3] = normal eqn[3] = -np.sum((ax, by, cz)) return eqn
def get3DClipPlane(self, planeIdx): """A convenience method which calculates a point-vector description of the specified clipping plane. ``planeIdx`` is an index into the :attr:`clipPosition`, :attr:`clipAzimuth`, and :attr:`clipInclination`, properties. Returns the clip plane at the given ``planeIdx`` as an origin and normal vector, in the display coordinate system.. """ pos = self.clipPosition[ planeIdx] azimuth = self.clipAzimuth[ planeIdx] incline = self.clipInclination[planeIdx] b = self.bounds pos = pos / 100.0 azimuth = azimuth * np.pi / 180.0 incline = incline * np.pi / 180.0 xmid = b.xlo + 0.5 * b.xlen ymid = b.ylo + 0.5 * b.ylen zmid = b.zlo + 0.5 * b.zlen centre = [xmid, ymid, zmid] normal = [0, 0, -1] rot1 = affine.axisAnglesToRotMat(incline, 0, 0) rot2 = affine.axisAnglesToRotMat(0, 0, azimuth) rotation = affine.concat(rot2, rot1) normal = affine.transformNormal(normal, rotation) normal = affine.normalise(normal) offset = (pos - 0.5) * max((b.xlen, b.ylen, b.zlen)) origin = centre + normal * offset return origin, normal
CUBE_TRIANGLES_CCW[:, [1, 2]] = CUBE_TRIANGLES_CCW[:, [2, 1]] CUBE_CCW_FACE_NORMALS = np.array([ [ 0, 0, -1], [ 0, 0, -1], [ 0, 0, 1], [ 0, 0, 1], [ 0, -1, 0], [ 0, -1, 0], [ 0, 1, 0], [ 0, 1, 0], [-1, 0, 0], [-1, 0, 0], [ 1, 0, 0], [ 1, 0, 0], ]) CUBE_CCW_VERTEX_NORMALS = np.zeros((8, 3)) for i in range(8): faces = np.where(CUBE_TRIANGLES_CCW == i)[0] CUBE_CCW_VERTEX_NORMALS[i] = CUBE_CCW_FACE_NORMALS[faces].sum(axis=0) CUBE_CCW_VERTEX_NORMALS = affine.normalise(CUBE_CCW_VERTEX_NORMALS) def test_mesh_create(): verts = np.array(CUBE_VERTICES) tris = np.array(CUBE_TRIANGLES_CCW) mesh = fslmesh.Mesh(tris, vertices=verts) print(str(mesh)) assert mesh.name == 'mesh' assert mesh.dataSource is None assert mesh.nvertices == 8 assert np.all(np.isclose(mesh.vertices, verts))
def calculateRayCastSettings(self, view=None, proj=None): """Calculates various parameters required for 3D ray-cast rendering (see the :class:`.GLVolume` class). :arg view: Transformation matrix which transforms from model coordinates to view coordinates (i.e. the GL view matrix). :arg proj: Transformation matrix which transforms from view coordinates to normalised device coordinates (i.e. the GL projection matrix). Returns a tuple containing: - A vector defining the amount by which to move along a ray in a single iteration of the ray-casting algorithm. This can be added directly to the volume texture coordinates. - A transformation matrix which transforms from image texture coordinates into the display coordinate system. .. note:: This method will raise an error if called on a ``GLImageObject`` which is managing an overlay that is not associated with a :class:`.Volume3DOpts` instance. """ if view is None: view = np.eye(4) if proj is None: proj = np.eye(4) # In GL, the camera position # is initially pointing in # the -z direction. eye = [0, 0, -1] target = [0, 0, 1] # We take this initial camera # configuration, and transform # it by the inverse modelview # matrix t2dmat = self.getTransform('texture', 'display') xform = affine.concat(view, t2dmat) ixform = affine.invert(xform) eye = affine.transform(eye, ixform, vector=True) target = affine.transform(target, ixform, vector=True) # Direction that the 'camera' is # pointing, normalied to unit length cdir = affine.normalise(eye - target) # Calculate the length of one step # along the camera direction in a # single iteration of the ray-cast # loop. Multiply by sqrt(3) so that # the maximum number of steps will # be reached across the longest axis # of the image texture cube. rayStep = np.sqrt(3) * cdir / self.getNumSteps() # A transformation matrix which can # transform image texture coordinates # into the corresponding screen # (normalised device) coordinates. # This allows the fragment shader to # convert an image texture coordinate # into a relative depth value. # # The projection matrix puts depth into # [-1, 1], but we want it in [0, 1] zscale = affine.scaleOffsetXform([1, 1, 0.5], [0, 0, 0.5]) xform = affine.concat(zscale, proj, xform) return rayStep, xform
def _pickModeLeftMouseDown(self, ev, canvas, mousePos, canvasPos): """Called on mouse down events in ``pick`` mode. Updates the :attr:`DisplayContext.location` property. """ from fsl.data.mesh import Mesh displayCtx = self.displayCtx ovl = displayCtx.getSelectedOverlay() if ovl is None: return # The canvasPos is located on the near clipping # plane (see Scene3DCanvas.canvasToWorld). # We also need the corresponding point on the # far clipping plane. farPos = canvas.canvasToWorld(mousePos[0], mousePos[1], near=False) # For non-mesh overlays, we select a point which # is in between the near/far clipping planes. if not isinstance(ovl, Mesh): posDir = farPos - canvasPos dist = affine.veclength(posDir) posDir = affine.normalise(posDir) midPos = canvasPos + 0.5 * dist * posDir self.displayCtx.location.xyz = midPos else: opts = self.displayCtx.getOpts(ovl) rayOrigin = canvasPos rayDir = affine.normalise(farPos - canvasPos) # transform location from display into model space rayOrigin = opts.transformCoords(rayOrigin, 'display', 'mesh') rayDir = opts.transformCoords(rayDir, 'display', 'mesh', vector=True) loc, tri = ovl.rayIntersection([rayOrigin], [rayDir], vertices=True) if len(loc) == 0: return loc = loc[0] tri = ovl.indices[int(tri[0]), :] # The rayIntersection method gives us a # point on one of the mesh triangles - # we want the vertex on that triangle # which is nearest to the intersection. triVerts = ovl.vertices[tri, :] triDists = affine.veclength(loc - triVerts) vertIdx = np.argsort(triDists)[0] loc = ovl.vertices[tri[vertIdx], :] loc = opts.transformCoords(loc, 'mesh', 'display') self.displayCtx.location.xyz = loc