Пример #1
0
def lookAt(eye, centre, up):
    """Replacement for ``gluLookAt`. Creates a transformation matrix which
    transforms the display coordinate system such that a camera at position
    (0, 0, 0), and looking towards (0, 0, -1), will see a scene as if from
    position ``eye``, oriented ``up``, and looking towards ``centre``.

    See:
    https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/gluLookAt.xml
    """

    eye = np.array(eye)
    centre = np.array(centre)
    up = np.array(up)
    proj = np.eye(4)

    forward = centre - eye
    forward /= np.sqrt(np.dot(forward, forward))

    right = np.cross(forward, up)
    right /= np.sqrt(np.dot(right, right))

    up = np.cross(right, forward)
    up /= np.sqrt(np.dot(up, up))

    proj[0, :3] = right
    proj[1, :3] = up
    proj[2, :3] = -forward

    eye = transform.scaleOffsetXform(1, -eye)
    proj = transform.concat(proj, eye)

    return proj
Пример #2
0
    def updateShaderState(self):
        """Updates the vertex/fragment shader program(s) state, via a call to
        the GL-version specific ``updateShaderState`` function.
        """

        dopts = self.opts
        copts = self.canvas.opts
        lightPos = None
        flatColour = dopts.getConstantColour()
        useNegCmap = (not dopts.useLut) and dopts.useNegativeCmap

        if self.threedee:
            lightPos = np.array(copts.lightPos)
            lightPos *= (copts.zoom / 100.0)
        else:
            lightPos = None

        if dopts.useLut:
            delta = 1.0 / (dopts.lut.max() + 1)
            cmapXform = transform.scaleOffsetXform(delta, 0.5 * delta)
        else:
            cmapXform = self.cmapTexture.getCoordinateTransform()

        fslgl.glmesh_funcs.updateShaderState(self,
                                             useNegCmap=useNegCmap,
                                             cmapXform=cmapXform,
                                             flatColour=flatColour,
                                             lightPos=lightPos)
Пример #3
0
    def _draw(self):
        """Draws the scene to the canvas. """

        if not self._setGLContext():
            return

        opts = self.opts
        glroutines.clear(opts.bgColour)

        if not self.__setViewport():
            return

        overlays, globjs = self.getGLObjects()

        if len(overlays) == 0:
            return

        # If occlusion is on, we offset the
        # depth of each overlay so that, where
        # a depth collision occurs, overlays
        # which are higher in the list will get
        # drawn above (closer to the screen)
        # than lower ones.
        depthOffset = transform.scaleOffsetXform(1, [0, 0, 0.1])
        depthOffset = np.array(depthOffset, dtype=np.float32, copy=False)
        xform = np.array(self.__viewMat, dtype=np.float32, copy=False)

        for ovl, globj in zip(overlays, globjs):

            display = self.__displayCtx.getDisplay(ovl)

            if not globj.ready():
                continue
            if not display.enabled:
                continue

            if opts.occlusion:
                xform = transform.concat(depthOffset, xform)
            elif isinstance(ovl, fslimage.Image):
                gl.glClear(gl.GL_DEPTH_BUFFER_BIT)

            log.debug('Drawing {} [{}]'.format(ovl, globj))

            globj.preDraw(xform=xform)
            globj.draw3D(xform=xform)
            globj.postDraw(xform=xform)

        if opts.showCursor:
            with glroutines.enabled((gl.GL_DEPTH_TEST)):
                self.__drawCursor()

        if opts.showLegend:
            self.__drawLegend()
Пример #4
0
def translate(infile, x, y, z):
    basename = fslimage.removeExt(op.basename(infile))
    outfile  = '{}_translated_{}_{}_{}.nii.gz'.format(basename, x, y, z)
    img      = fslimage.Image(infile)
    xform    = img.voxToWorldMat

    shift             = transform.scaleOffsetXform(1, (x, y, z))
    xform             = transform.concat(shift, xform)
    img.voxToWorldMat = xform

    img.save(outfile)

    return outfile
Пример #5
0
    def __genViewMatrix(self, w, h):
        """Generate and return a transformation matrix to be used as the
        model-view matrix. This includes applying the current :attr:`zoom`,
        :attr:`rotation` and :attr:`offset` settings, and configuring
        the camera. This method is called by :meth:`__setViewport`.

        :arg w: Canvas width in pixels
        :arg h: Canvas height in pixels
        """

        opts = self.opts
        b = self.__displayCtx.bounds
        centre = [
            b.xlo + 0.5 * b.xlen, b.ylo + 0.5 * b.ylen, b.zlo + 0.5 * b.zlen
        ]

        # The MV matrix comprises (in this order):
        #
        #    - A rotation (the rotation property)
        #
        #    - Camera configuration. With no rotation, the
        #      camera will be looking towards the positive
        #      Y axis (i.e. +y is forwards), and oriented
        #      towards the positive Z axis (i.e. +z is up)
        #
        #    - A translation (the offset property)
        #    - A scaling (the zoom property)

        # Scaling and rotation matrices. Rotation
        # is always around the centre of the
        # displaycontext bounds (the bounding
        # box which contains all loaded overlays).
        scale = opts.zoom / 100.0
        scale = transform.scaleOffsetXform([scale] * 3, 0)
        rotate = transform.rotMatToAffine(opts.rotation, centre)

        # The offset property is defined in x/y
        # pixels, normalised to [-1, 1]. We need
        # to convert them into viewport space,
        # where the horizontal axis maps to
        # (-xhalf, xhalf), and the vertical axis
        # maps to (-yhalf, yhalf). See
        # gl.routines.ortho.
        offset = np.array(opts.offset[:] + [0])
        xlen, ylen = glroutines.adjust(b.xlen, b.ylen, w, h)
        offset[0] = xlen * offset[0] / 2
        offset[1] = ylen * offset[1] / 2
        offset = transform.scaleOffsetXform(1, offset)

        # And finally the camera.
        eye = list(centre)
        eye[1] += 1
        up = [0, 0, 1]
        camera = glroutines.lookAt(eye, centre, up)

        # Order is very important!
        xform = transform.concat(offset, scale, camera, rotate)
        np.array(xform, dtype=np.float32)

        self.__viewOffset = offset
        self.__viewScale = scale
        self.__viewRotate = rotate
        self.__viewCamera = camera
        self.__viewMat = xform
Пример #6
0
    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 = transform.concat(view, t2dmat)
        ixform = transform.invert(xform)

        eye = transform.transform(eye, ixform, vector=True)
        target = transform.transform(target, ixform, vector=True)

        # Direction that the 'camera' is
        # pointing, normalied to unit length
        cdir = transform.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 = transform.scaleOffsetXform([1, 1, 0.5], [0, 0, 0.5])
        xform = transform.concat(zscale, proj, xform)

        return rayStep, xform
Пример #7
0
def copyImage(overlayList,
              displayCtx,
              overlay,
              createMask=False,
              copy4D=True,
              copyDisplay=True,
              name=None,
              roi=None,
              data=None):
    """Creates a copy of the given :class:`.Image` overlay, and inserts it
    into the :class:`.OverlayList`.

    :arg overlayList: The :class:`.OverlayList`.

    :arg displayCtx:  The :class:`.DisplayContext`.

    :arg overlay:     The :class:`.Image` to be copied.

    :arg createMask:  If ``True``, the copy will be an empty ``Image`` the
                      same shape as the ``overlay``.

    :arg copy4D:      If ``True``, and the ``overlay`` is 4D, the copy will
                      also be 4D. Otherwise, the current 3D voluem is copied.

    :arg copyDisplay: If ``True``, the copy will inherit the display settings
                      of the ``overlay``. Otherwise, the copy will be
                      initialised  with default display settings.

    :arg name:        If provided, will be used as the :attr:`.Display.name`
                      of the copy. Otherwise the copy will be given a name.

    :arg roi:         If provided, the copy will be cropped to the low/high
                      voxel bounds specified in the image. Must be a sequence
                      of tuples, containing the low/high bounds for each voxel
                      dimension. For 4D images, the bounds for the fourth
                      dimension are optional.

    :arg data:        If provided, is used as the image data for the new copy.
                      Must match the shape dictated by the other arguments
                      (i.e. ``copy4D`` and ``roi``). If ``data`` is provided,
                      the ``createMask`` argument is ignored.
    """

    ovlIdx = overlayList.index(overlay)
    opts = displayCtx.getOpts(overlay)
    isROI = roi is not None
    is4D = len(overlay.shape) > 3 and overlay.shape[3] > 1

    if name is None:
        name = '{}_copy'.format(overlay.name)

    if roi is None:
        roi = [(0, s) for s in overlay.shape]

    # If the image is 4D, and an ROI of
    # length 3 has been given, add some
    # bounds for the fourth dimension
    if is4D and copy4D and len(roi) == 3:
        roi = list(roi) + [(0, overlay.shape[3])]

    # If we are only supposed to copy
    # the current 3D volume of a 4D
    # image, adjust the ROI accordingly.
    if is4D and not copy4D:
        roi = list(roi[:3]) + [(opts.volume, opts.volume + 1)]

    shape = [hi - lo for lo, hi in roi]
    slc = tuple([slice(lo, hi) for lo, hi in roi])

    if data is not None: pass
    elif createMask: data = np.zeros(shape)
    else: data = np.copy(overlay[slc])

    # If this is an ROI, we need to add
    # an offset to the image affine
    if isROI:
        xform = overlay.voxToWorldMat
        offset = [lo for lo, hi in roi[:3]]
        offset = transform.scaleOffsetXform([1, 1, 1], offset)
        xform = transform.concat(xform, offset)

    else:
        xform = None

    # Create the copy, put it in the list
    header = overlay.header.copy()
    copy = fslimage.Image(data, name=name, header=header, xform=xform)

    overlayList.insert(ovlIdx + 1, copy)

    # Copy the Display/DisplayOpts settings
    if copyDisplay:

        srcDisplay = displayCtx.getDisplay(overlay)
        destDisplay = displayCtx.getDisplay(copy)

        for prop in srcDisplay.getAllProperties()[0]:

            # Don't override the name
            # that we set above
            if prop == 'name':
                continue

            val = getattr(srcDisplay, prop)
            setattr(destDisplay, prop, val)

        # And after the Display has been configured
        # copy the DisplayOpts settings.
        srcOpts = displayCtx.getOpts(overlay)
        destOpts = displayCtx.getOpts(copy)

        for prop in srcOpts.getAllProperties()[0]:

            # But don't clobber the transform, and related,
            # properties, as it is (typically) automatically
            # controlled via the DisplayContext.displaySpace
            if prop in ('transform', 'bounds'):
                continue

            val = getattr(srcOpts, prop)
            setattr(destOpts, prop, val)
Пример #8
0
    def __realPrepareTextureData(self, data):
        """This method prepares and returns the given ``data``, ready to be
        used as GL texture data.

        This process potentially involves:

          - Resampling to a different resolution (see the
            :func:`.routines.subsample` function).

          - Pre-filtering (see the ``prefilter`` parameter to
            :meth:`__init__`).

          - Normalising (if the ``normalise`` parameter to :meth:`__init__`
            was ``True``, or if the data type cannot be used as-is).

          - Casting to a different data type (if the data type cannot be used
            as-is).

        :returns: A tuple containing:

                    - A ``numpy`` array containing the image data, ready to be
                       copied to the GPU.

                    - An affine transformation matrix which encodes an offset
                      and a scale, which may be used to transform the texture
                      data from the range ``[0.0, 1.0]`` to its raw data
                      range.

                    - Inverse of ``voxValXform``.
        """

        log.debug('Preparing data for {}({}) - this may take some time '
                  '...'.format(type(self).__name__, self.getTextureName()))

        dtype = data.dtype
        floatTextures = self.canUseFloatTextures()

        prefilter = self.__prefilter
        prefilterRange = self.__prefilterRange
        resolution = self.__resolution
        scales = self.__scales
        normalise = self.__normalise
        normaliseRange = self.__normaliseRange

        if normalise: dmin, dmax = normaliseRange
        else: dmin, dmax = 0, 0

        if normalise                  and \
           prefilter      is not None and \
           prefilterRange is not None:
            dmin, dmax = prefilterRange(dmin, dmax)

        # Offsets/scales which can be used to transform from
        # the texture data (which may be offset or normalised)
        # back to the original voxel data
        if normalise: offset = dmin
        elif dtype == np.uint8: offset = 0
        elif dtype == np.int8: offset = -128
        elif dtype == np.uint16: offset = 0
        elif dtype == np.int16: offset = -32768
        elif floatTextures: offset = 0

        if normalise: scale = dmax - dmin
        elif dtype == np.uint8: scale = 255
        elif dtype == np.int8: scale = 255
        elif dtype == np.uint16: scale = 65535
        elif dtype == np.int16: scale = 65535
        elif floatTextures: scale = 1

        # If the data range is 0 (min == max)
        # we just set an identity xform
        if scale == 0:
            voxValXform = np.eye(4)
            invVoxValXform = np.eye(4)

        # Otherwise we save a transformation
        # from the texture values back to the
        # original data range. Note that if
        # storing floating point data, this
        # will be an identity transform.
        else:
            invScale = 1.0 / scale
            voxValXform = transform.scaleOffsetXform(scale, offset)
            invVoxValXform = transform.scaleOffsetXform(
                invScale, -offset * invScale)

        if resolution is not None:
            data = glroutines.subsample(data, resolution, pixdim=scales)[0]

        if prefilter is not None:
            data = prefilter(data)

        # TODO if FLOAT_TEXTURES, you should
        #      save normalised values as float32
        if normalise:

            log.debug('Normalising to range {} - {}'.format(dmin, dmax))

            if dmax != dmin:
                data = np.clip((data - dmin) / float(dmax - dmin), 0, 1)

            data = np.round(data * 65535)
            data = np.array(data, dtype=np.uint16)

        elif dtype == np.uint8:
            pass
        elif dtype == np.int8:
            data = np.array(data + 128, dtype=np.uint8)
        elif dtype == np.uint16:
            pass
        elif dtype == np.int16:
            data = np.array(data + 32768, dtype=np.uint16)
        elif floatTextures and data.dtype != np.float32:
            data = np.array(data, dtype=np.float32)

        log.debug('Data preparation for {} complete [dtype={}, '
                  'scale={}, offset={}, dmin={}, dmax={}].'.format(
                      self.getTextureName(), data.dtype, scale, offset, dmin,
                      dmax))

        return data, voxValXform, invVoxValXform
Пример #9
0
    def __setupTransforms(self):
        """Calculates transformation matrices between all of the possible
        spaces in which the overlay may be displayed.

        These matrices are accessible via the :meth:`getTransform` method.
        """

        image = self.overlay
        shape = np.array(image.shape[:3])

        voxToIdMat = np.eye(4)
        voxToPixdimMat = np.diag(list(image.pixdim[:3]) + [1.0])
        voxToPixFlipMat = image.voxToScaledVoxMat
        voxToWorldMat = image.voxToWorldMat
        voxToWorldMat = transform.concat(self.displayXform, voxToWorldMat)
        ds = self.displayCtx.displaySpace

        # The reference transforms depend on the value of
        # displaySpace
        if ds == 'world':
            voxToRefMat = voxToWorldMat
        elif ds is self.overlay:
            voxToRefMat = voxToPixFlipMat
        else:
            voxToRefMat = transform.concat(ds.voxToScaledVoxMat,
                                           ds.worldToVoxMat, voxToWorldMat)

        # When going from voxels to textures,
        # we add 0.5 to centre the voxel (see
        # the note on coordinate systems at
        # the top of this file).
        voxToTexMat = transform.scaleOffsetXform(tuple(1.0 / shape),
                                                 tuple(0.5 / shape))

        idToVoxMat = transform.invert(voxToIdMat)
        idToPixdimMat = transform.concat(voxToPixdimMat, idToVoxMat)
        idToPixFlipMat = transform.concat(voxToPixFlipMat, idToVoxMat)
        idToWorldMat = transform.concat(voxToWorldMat, idToVoxMat)
        idToRefMat = transform.concat(voxToRefMat, idToVoxMat)
        idToTexMat = transform.concat(voxToTexMat, idToVoxMat)

        pixdimToVoxMat = transform.invert(voxToPixdimMat)
        pixdimToIdMat = transform.concat(voxToIdMat, pixdimToVoxMat)
        pixdimToPixFlipMat = transform.concat(voxToPixFlipMat, pixdimToVoxMat)
        pixdimToWorldMat = transform.concat(voxToWorldMat, pixdimToVoxMat)
        pixdimToRefMat = transform.concat(voxToRefMat, pixdimToVoxMat)
        pixdimToTexMat = transform.concat(voxToTexMat, pixdimToVoxMat)

        pixFlipToVoxMat = transform.invert(voxToPixFlipMat)
        pixFlipToIdMat = transform.concat(voxToIdMat, pixFlipToVoxMat)
        pixFlipToPixdimMat = transform.concat(voxToPixdimMat, pixFlipToVoxMat)
        pixFlipToWorldMat = transform.concat(voxToWorldMat, pixFlipToVoxMat)
        pixFlipToRefMat = transform.concat(voxToRefMat, pixFlipToVoxMat)
        pixFlipToTexMat = transform.concat(voxToTexMat, pixFlipToVoxMat)

        worldToVoxMat = transform.invert(voxToWorldMat)
        worldToIdMat = transform.concat(voxToIdMat, worldToVoxMat)
        worldToPixdimMat = transform.concat(voxToPixdimMat, worldToVoxMat)
        worldToPixFlipMat = transform.concat(voxToPixFlipMat, worldToVoxMat)
        worldToRefMat = transform.concat(voxToRefMat, worldToVoxMat)
        worldToTexMat = transform.concat(voxToTexMat, worldToVoxMat)

        refToVoxMat = transform.invert(voxToRefMat)
        refToIdMat = transform.concat(voxToIdMat, refToVoxMat)
        refToPixdimMat = transform.concat(voxToPixdimMat, refToVoxMat)
        refToPixFlipMat = transform.concat(voxToPixFlipMat, refToVoxMat)
        refToWorldMat = transform.concat(voxToWorldMat, refToVoxMat)
        refToTexMat = transform.concat(voxToTexMat, refToVoxMat)

        texToVoxMat = transform.invert(voxToTexMat)
        texToIdMat = transform.concat(voxToIdMat, texToVoxMat)
        texToPixdimMat = transform.concat(voxToPixdimMat, texToVoxMat)
        texToPixFlipMat = transform.concat(voxToPixFlipMat, texToVoxMat)
        texToWorldMat = transform.concat(voxToWorldMat, texToVoxMat)
        texToRefMat = transform.concat(voxToRefMat, texToVoxMat)

        self.__xforms['id', 'id'] = np.eye(4)
        self.__xforms['id', 'pixdim'] = idToPixdimMat
        self.__xforms['id', 'pixdim-flip'] = idToPixFlipMat
        self.__xforms['id', 'affine'] = idToWorldMat
        self.__xforms['id', 'reference'] = idToRefMat
        self.__xforms['id', 'texture'] = idToTexMat

        self.__xforms['pixdim', 'pixdim'] = np.eye(4)
        self.__xforms['pixdim', 'id'] = pixdimToIdMat
        self.__xforms['pixdim', 'pixdim-flip'] = pixdimToPixFlipMat
        self.__xforms['pixdim', 'affine'] = pixdimToWorldMat
        self.__xforms['pixdim', 'reference'] = pixdimToRefMat
        self.__xforms['pixdim', 'texture'] = pixdimToTexMat

        self.__xforms['pixdim-flip', 'pixdim-flip'] = np.eye(4)
        self.__xforms['pixdim-flip', 'id'] = pixFlipToIdMat
        self.__xforms['pixdim-flip', 'pixdim'] = pixFlipToPixdimMat
        self.__xforms['pixdim-flip', 'affine'] = pixFlipToWorldMat
        self.__xforms['pixdim-flip', 'reference'] = pixFlipToRefMat
        self.__xforms['pixdim-flip', 'texture'] = pixFlipToTexMat

        self.__xforms['affine', 'affine'] = np.eye(4)
        self.__xforms['affine', 'id'] = worldToIdMat
        self.__xforms['affine', 'pixdim'] = worldToPixdimMat
        self.__xforms['affine', 'pixdim-flip'] = worldToPixFlipMat
        self.__xforms['affine', 'reference'] = worldToRefMat
        self.__xforms['affine', 'texture'] = worldToTexMat

        self.__xforms['reference', 'reference'] = np.eye(4)
        self.__xforms['reference', 'id'] = refToIdMat
        self.__xforms['reference', 'pixdim'] = refToPixdimMat
        self.__xforms['reference', 'pixdim-flip'] = refToPixFlipMat
        self.__xforms['reference', 'affine'] = refToWorldMat
        self.__xforms['reference', 'texture'] = refToTexMat

        self.__xforms['texture', 'texture'] = np.eye(4)
        self.__xforms['texture', 'id'] = texToIdMat
        self.__xforms['texture', 'pixdim'] = texToPixdimMat
        self.__xforms['texture', 'pixdim-flip'] = texToPixFlipMat
        self.__xforms['texture', 'affine'] = texToWorldMat
        self.__xforms['texture', 'reference'] = texToRefMat