def test_linear_altref(seed): with tempdir.tempdir(): src2ref = affine.scaleOffsetXform([1, 1, 1], [5, 5, 5]) altv2w = affine.scaleOffsetXform([1, 1, 1], [10, 10, 10]) srcdata = np.random.randint(1, 65536, (10, 10, 10)) src = fslimage.Image(srcdata, xform=np.eye(4)) ref = fslimage.Image(src.data, xform=src2ref) altref = fslimage.Image(src.data, xform=altv2w) src.save('src') ref.save('ref') altref.save('altref') x5.writeLinearX5('xform.x5', src2ref, src, ref) fsl_apply_x5.main('src xform.x5 out -r altref'.split()) result = fslimage.Image('out') expect = np.zeros(srcdata.shape) expect[:5, :5, :5] = srcdata[5:, 5:, 5:] assert result.sameSpace(altref) assert np.all(result.data == expect)
def test_applyDeformation_worldAligned(): refv2w = affine.scaleOffsetXform([1, 1, 1], [10, 10, 10]) fieldv2w = affine.scaleOffsetXform([2, 2, 2], [10.5, 10.5, 10.5]) src2ref = refv2w ref2src = affine.invert(src2ref) srcdata = np.random.randint(1, 65536, (10, 10, 10)) src = fslimage.Image(srcdata) ref = fslimage.Image(srcdata, xform=src2ref) field = _affine_field(src, ref, ref2src, 'world', 'world', shape=(5, 5, 5), fv2w=fieldv2w) field = nonlinear.DeformationField( nonlinear.convertDeformationType(field, 'absolute'), header=field.header, src=src, ref=ref, srcSpace='world', refSpace='world', defType='absolute') expect, xf = resample.resampleToReference( src, ref, matrix=src2ref, order=1, mode='constant', cval=0) result = nonlinear.applyDeformation( src, field, order=1, mode='constant', cval=0) expect = expect[1:-1, 1:-1, 1:-1] result = result[1:-1, 1:-1, 1:-1] assert np.all(np.isclose(expect, result))
def test_rmsdev(): t1 = np.eye(4) t2 = affine.scaleOffsetXform([1, 1, 1], [2, 0, 0]) assert np.isclose(affine.rmsdev(t1, t2), 2) assert np.isclose(affine.rmsdev(t1, t2, R=2), 2) assert np.isclose(affine.rmsdev(t1, t2, R=2, xc=(1, 1, 1)), 2) t1 = np.eye(3) lastdist = 0 for i in range(1, 11): rot = np.pi * i / 10.0 t2 = affine.axisAnglesToRotMat(rot, 0, 0) result = affine.rmsdev(t1, t2) assert result > lastdist lastdist = result for i in range(11, 20): rot = np.pi * i / 10.0 t2 = affine.axisAnglesToRotMat(rot, 0, 0) result = affine.rmsdev(t1, t2) assert result < lastdist lastdist = result
def getModulateValueXform(self): """Returns an affine transform to normalise alpha modulation values. The GL volume shaders need to normalise the modulate value by the modulation range to generate an opacity value. We calculate a suitable scale and offset by buildin an affine transform which transforms voxel values from the image/modulate image texture range to 0/1, where 0 corresponds to the low modulate range bound, and 1 to the high modulate range bound. The resulting scale/offset can be used by the shader to convert a modulate value directly into an opacity value. """ opts = self.opts if opts.modulateImage is None: modXform = self.imageTexture.voxValXform else: modXform = self.modulateTexture.voxValXform modlo, modhi = opts.modulateRange modrange = modhi - modlo if modrange == 0: modXform = np.eye(4) else: modXform = affine.concat( affine.scaleOffsetXform(1 / modrange, -modlo / modrange), modXform) return modXform
def test_applyDeformation_altref(): src2ref = affine.compose( np.random.randint(2, 5, 3), np.random.randint(1, 10, 3), np.random.random(3)) ref2src = affine.invert(src2ref) srcdata = np.random.randint(1, 65536, (10, 10, 10)) refdata = np.random.randint(1, 65536, (10, 10, 10)) src = fslimage.Image(srcdata) ref = fslimage.Image(refdata, xform=src2ref) field = _affine_field(src, ref, ref2src, 'world', 'world') altrefxform = affine.concat( src2ref, affine.scaleOffsetXform([1, 1, 1], [5, 0, 0])) altref = fslimage.Image(refdata, xform=altrefxform) expect, xf = resample.resampleToReference( src, altref, matrix=src2ref, order=1, mode='constant', cval=0) result = nonlinear.applyDeformation( src, field, ref=altref, order=1, mode='constant', cval=0) # boundary voxels can get truncated # (4 is the altref-ref overlap boundary) expect[4, :, :] = 0 result[4, :, :] = 0 expect = expect[1:-1, 1:-1, 1:-1] result = result[1:-1, 1:-1, 1:-1] assert np.all(np.isclose(expect, result))
def test_generateAffines(): v2w = affine.compose(np.random.random(3), np.random.random(3), np.random.random(3)) shape = (10, 10, 10) pixdim = (1, 1, 1) got, isneuro = fslimage.Nifti.generateAffines(v2w, shape, pixdim) w2v = npla.inv(v2w) assert isneuro == (npla.det(v2w) > 0) if not isneuro: v2f = np.eye(4) f2v = np.eye(4) f2w = v2w w2f = w2v else: v2f = affine.scaleOffsetXform([-1, 1, 1], [9, 0, 0]) f2v = npla.inv(v2f) f2w = affine.concat(v2w, f2v) w2f = affine.concat(v2f, w2v) assert np.all(np.isclose(v2w, got['voxel', 'world'])) assert np.all(np.isclose(w2v, got['world', 'voxel'])) assert np.all(np.isclose(v2f, got['voxel', 'fsl'])) assert np.all(np.isclose(f2v, got['fsl', 'voxel'])) assert np.all(np.isclose(f2w, got['fsl', 'world'])) assert np.all(np.isclose(w2f, got['world', 'fsl']))
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 = affine.scaleOffsetXform(1, -eye) proj = affine.concat(proj, eye) return proj
def updateShaderState(self): """Updates the vertex/fragment shader program(s) state, via a call to the GL-version specific ``updateShaderState`` function. """ dopts = self.opts canvas = self.canvas flatColour = dopts.getConstantColour() useNegCmap = (not dopts.useLut) and dopts.useNegativeCmap if dopts.useLut: delta = 1.0 / (dopts.lut.max() + 1) cmapXform = affine.scaleOffsetXform(delta, 0.5 * delta) else: cmapXform = self.cmapTexture.getCoordinateTransform() # calculate a scale+offset which transforms # modulate alpha value from the data range # into an alpha value, according to the # modulateRange modlo, modhi = dopts.modulateRange modRange = modhi - modlo if modRange == 0: modScale = 1 modOffset = 0 else: modScale = 1 / modRange modOffset = -modlo / modRange fslgl.glmesh_funcs.updateShaderState(self, useNegCmap=useNegCmap, cmapXform=cmapXform, modScale=modScale, modOffset=modOffset, flatColour=flatColour)
def roi(image, bounds): """Extract an ROI from the given ``image`` according to the given ``bounds``. This function can also be used to pad, or expand the field-of-view, of an image, by passing in negative low bound values, or high bound values which are larger than the image shape. The padded region will contain zeroes. :arg image: :class:`.Image` object :arg bounds: Must be a sequence of tuples, containing the low/high bounds for each voxel dimension, where the low bound is *inclusive*, and the high bound is *exclusive*. For 4D images, the bounds for the fourth dimension are optional. :returns: A new :class:`.Image` object containing the region specified by the ``bounds``. """ bounds = _normaliseBounds(image.shape, bounds) newshape = [hi - lo for lo, hi in bounds] oldslc = [] newslc = [] # Figure out how to slice the input image # data array, and the corresponding slice # in the output data array. for (lo, hi), oldlen, newlen in zip(bounds, image.shape, newshape): oldlo = max(lo, 0) oldhi = min(hi, oldlen) newlo = max(0, -lo) newhi = newlo + (oldhi - oldlo) oldslc.append(slice(oldlo, oldhi)) newslc.append(slice(newlo, newhi)) oldslc = tuple(oldslc) newslc = tuple(newslc) # Copy the ROI into the new data array newdata = np.zeros(newshape, dtype=image.dtype) newdata[newslc] = image.data[oldslc] # Create a new affine for the ROI, # with an appropriate offset along # each spatial dimension oldaff = image.voxToWorldMat offset = [lo for lo, hi in bounds[:3]] offset = affine.scaleOffsetXform([1, 1, 1], offset) newaff = affine.concat(oldaff, offset) return fslimage.Image(newdata, xform=newaff, header=image.header, name=image.name + '_roi')
def test_resample_image_dim(): with tempdir(): img = Image(make_random_image('image.nii.gz', dims=(10, 10, 10))) resample_image.main('image resampled -d 0.5,0.5,0.5'.split()) res = Image('resampled') expv2w = affine.concat(img.voxToWorldMat, affine.scaleOffsetXform([0.5, 0.5, 0.5], 0)) assert np.all(np.isclose(res.shape, (20, 20, 20))) assert np.all(np.isclose(res.pixdim, (0.5, 0.5, 0.5))) assert np.all(np.isclose(res.voxToWorldMat, expv2w))
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 = affine.scaleOffsetXform(1, (x, y, z)) xform = affine.concat(shift, xform) img.voxToWorldMat = xform img.save(outfile) return outfile
def test_determineAffine(): # sformcode, qformcode, intent, expaff tests = [ (constants.NIFTI_XFORM_ALIGNED_ANAT, constants.NIFTI_XFORM_ALIGNED_ANAT, constants.NIFTI_INTENT_NONE, 'sform'), (constants.NIFTI_XFORM_ALIGNED_ANAT, constants.NIFTI_XFORM_UNKNOWN, constants.NIFTI_INTENT_NONE, 'sform'), (constants.NIFTI_XFORM_UNKNOWN, constants.NIFTI_XFORM_ALIGNED_ANAT, constants.NIFTI_INTENT_NONE, 'qform'), (constants.NIFTI_XFORM_ALIGNED_ANAT, constants.NIFTI_XFORM_ALIGNED_ANAT, constants.FSL_FNIRT_DISPLACEMENT_FIELD, 'sform'), (constants.NIFTI_XFORM_ALIGNED_ANAT, constants.NIFTI_XFORM_ALIGNED_ANAT, constants.FSL_CUBIC_SPLINE_COEFFICIENTS, 'scaling'), (constants.NIFTI_XFORM_UNKNOWN, constants.NIFTI_XFORM_UNKNOWN, constants.NIFTI_INTENT_NONE, 'scaling'), ] for sformcode, qformcode, intent, exp in tests: sform = affine.compose(np.random.random(3), np.random.random(3), np.random.random(3)) qform = affine.compose(np.random.random(3), np.random.random(3), np.random.random(3)) pixdims = np.random.randint(1, 10, 3) hdr = nib.Nifti1Header() hdr.set_data_shape((10, 10, 10)) hdr.set_sform(sform, sformcode) hdr.set_qform(qform, qformcode) hdr.set_intent(intent) hdr.set_zooms(pixdims) # the randomly generated qform might # not be fully representable, so let # nibabel fix it for us sform = hdr.get_sform() qform = hdr.get_qform() got = fslimage.Nifti.determineAffine(hdr) if exp == 'sform': exp = sform elif exp == 'qform': exp = qform elif exp == 'scaling': exp = affine.scaleOffsetXform(pixdims, 0) assert np.all(np.isclose(got, exp))
def test_resampleToReference2(): # More specific test - output # data gets transformed correctly # into reference space img = np.zeros((5, 5, 5), dtype=float) img[1, 1, 1] = 1 img = fslimage.Image(img) refv2w = affine.scaleOffsetXform([1, 1, 1], [-1, -1, -1]) ref = np.zeros((5, 5, 5), dtype=float) ref = fslimage.Image(ref, xform=refv2w) res = resample.resampleToReference(img, ref, order=0) exp = np.zeros((5, 5, 5), dtype=float) exp[2, 2, 2] = 1 assert np.all(np.isclose(res[0], exp))
def swapdim(infile): basename = fslimage.removeExt(op.basename(infile)) outfile = '{}_swapdim.nii.gz'.format(basename) img = fslimage.Image(infile) data = img.data xform = img.voxToWorldMat data = data.transpose((2, 0, 1)) rot = affine.rotMatToAffine( affine.concat(affine.axisAnglesToRotMat(np.pi / 2, 0, 0), affine.axisAnglesToRotMat(0, 0, 3 * np.pi / 2))) xform = affine.concat(xform, affine.scaleOffsetXform((1, -1, -1), (0, 0, 0)), rot) fslimage.Image(data, xform=xform, header=img.header).save(outfile) return outfile
def test_resampleToReference3(): # Test resampling image to ref # with mismatched dimensions imgdata = np.random.randint(0, 65536, (5, 5, 5)) img = fslimage.Image(imgdata, xform=affine.scaleOffsetXform((2, 2, 2), (0.5, 0.5, 0.5))) # reference/expected data when # resampled to ref (using nn interp). # Same as image, upsampled by a # factor of 2 refdata = np.repeat(np.repeat(np.repeat(imgdata, 2, 0), 2, 1), 2, 2) refdata = np.array([refdata] * 8).transpose((1, 2, 3, 0)) ref = fslimage.Image(refdata) # We should be able to use a 4D reference resampled, xform = resample.resampleToReference(img, ref, order=0, mode='nearest') assert np.all(resampled == ref.data[..., 0]) # If resampling a 4D image with a 3D reference, # the fourth dimension should be passed through resampled, xform = resample.resampleToReference(ref, img, order=0, mode='nearest') exp = np.array([imgdata] * 8).transpose((1, 2, 3, 0)) assert np.all(resampled == exp) # When resampling 4D to 4D, only the # first 3 dimensions should be resampled imgdata = np.array([imgdata] * 15).transpose((1, 2, 3, 0)) img = fslimage.Image(imgdata, xform=img.voxToWorldMat) exp = np.array([refdata[..., 0]] * 15).transpose((1, 2, 3, 0)) resampled, xform = resample.resampleToReference(img, ref, order=0, mode='nearest') assert np.all(resampled == exp)
def roi(fname, roi): base = fslimage.removeExt(op.basename(fname)) outfile = '{}_roi_{}_{}_{}_{}_{}_{}'.format(base, *roi) img = fslimage.Image(fname) xs, xe, ys, ye, zs, ze = roi data = img[xs:xe, ys:ye, zs:ze, ...] xform = img.voxToWorldMat offset = [lo for lo in roi[::2]] offset = affine.scaleOffsetXform([1, 1, 1], offset) xform = affine.concat(xform, offset) img = fslimage.Image(data, xform=xform, header=img.header) img.save(outfile) return outfile
def updateShaderState(self): """Updates all variables used by the vertex/fragment shaders. The fragment shader is configured by the :func:`.gl21.glvector_funcs.updateFragmentShaderState` function. """ shader = self.shader shader.load() changed = glvector_funcs.updateShaderState(self) image = self.vectorImage opts = self.opts # see comments in gl21/glvector_funcs.py if self.vectorImage.niftiDataType == constants.NIFTI_DT_RGB24: vvxMat = affine.scaleOffsetXform(2, -1) else: vvxMat = self.imageTexture.voxValXform directed = opts.directed unitLength = opts.unitLength lengthScale = opts.lengthScale / 100.0 imageDims = image.pixdim[:3] d2vMat = opts.getTransform('display', 'voxel') v2dMat = opts.getTransform('voxel', 'display') xFlip = opts.orientFlip changed |= shader.set('vectorTexture', 4) changed |= shader.set('displayToVoxMat', d2vMat) changed |= shader.set('voxToDisplayMat', v2dMat) changed |= shader.set('voxValXform', vvxMat) changed |= shader.set('imageDims', imageDims) changed |= shader.set('directed', directed) changed |= shader.set('unitLength', unitLength) changed |= shader.set('lengthScale', lengthScale) changed |= shader.set('xFlip', xFlip) shader.unload() return changed
def test_resample_image_shape(): with tempdir(): img = Image(make_random_image('image.nii.gz', dims=(10, 10, 10))) resample_image.main('image resampled -s 20,20,20'.split()) res = Image('resampled') expv2w = affine.concat(img.voxToWorldMat, affine.scaleOffsetXform([0.5, 0.5, 0.5], 0)) assert np.all(np.isclose(res.shape, (20, 20, 20))) assert np.all(np.isclose(res.pixdim, (0.5, 0.5, 0.5))) assert np.all(np.isclose(res.voxToWorldMat, expv2w)) assert np.all( np.isclose( np.array(affine.axisBounds(res.shape, res.voxToWorldMat)) - 0.25, affine.axisBounds(img.shape, img.voxToWorldMat))) resample_image.main('image resampled -s 20,20,20 -o corner'.split()) res = Image('resampled') assert np.all( np.isclose(affine.axisBounds(res.shape, res.voxToWorldMat), affine.axisBounds(img.shape, img.voxToWorldMat)))
def test_resampleToReference4(): # the image and ref are out of # alignment, but this affine # will bring them into alignment img2ref = affine.scaleOffsetXform([2, 2, 2], [10, 10, 10]) imgdata = np.random.randint(0, 65536, (5, 5, 5)) refdata = np.zeros((5, 5, 5)) img = fslimage.Image(imgdata) ref = fslimage.Image(refdata, xform=img2ref) # Without the affine, the image # will be out of the FOV of the # reference resampled, xform = resample.resampleToReference(img, ref) assert np.all(resampled == 0) # But applying the affine will # cause them to overlap # perfectly in world coordinates resampled, xform = resample.resampleToReference(img, ref, matrix=img2ref) assert np.all(resampled == imgdata)
def _test_NewImageAction(panel, overlayList, displayCtx): act = panel.frame.menuActions[newimage.NewImageAction] def check(ovl, shape, pixdim, dtype, affine): assert tuple( ovl.shape) == tuple(shape) assert tuple( ovl.pixdim) == tuple(pixdim) assert ovl.dtype == dtype assert np.all(ovl.voxToWorldMat == affine) tests = [ ((100, 100, 100), (1, 1, 1), np.float32, np.eye(4)), (( 50, 50, 50), (2, 2, 2), np.uint8, np.diag([2, 2, 2, 1])), (( 20, 30, 40), (1.5, 1.2, 1.3), np.int32, fslaffine.scaleOffsetXform([2, 3, 4], [-4, -3, -2])), ((100, 100, 100), (1, 1, 1), np.float64, fslaffine.compose((2, 3, 1), (1, 2, 3), (1, 1.5, 2))), ] with mock.patch('fsleyes.actions.newimage.NewImageDialog', MockNewImageDialog): MockNewImageDialog.ShowModalRet = wx.ID_CANCEL MockNewImageDialog.initOverride = False act() realYield() assert len(overlayList) == 0 MockNewImageDialog.ShowModalRet = wx.ID_OK for shape, pixdim, dtype, affine in tests: MockNewImageDialog.shapeRet = shape MockNewImageDialog.pixdimRet = pixdim MockNewImageDialog.dtypeRet = dtype MockNewImageDialog.affineRet = affine act() realYield() assert len(overlayList) == 1 check(overlayList[0], shape, pixdim, dtype, affine) overlayList.clear() realYield()
def test_scaleOffsetXform(): # Test numerically testfile = op.join(datadir, 'test_transform_test_scaleoffsetxform.txt') lines = readlines(testfile) ntests = len(lines) // 5 for i in range(ntests): lineoff = i * 5 scales, offsets = lines[lineoff].decode('ascii').split(',') scales = [float(s) for s in scales.split()] offsets = [float(o) for o in offsets.split()] expected = lines[lineoff + 1:lineoff + 5] expected = [[float(v) for v in l.split()] for l in expected] expected = np.array(expected) result = affine.scaleOffsetXform(scales, offsets) assert np.all(np.isclose(result, expected)) # Test that different input types work: # - scalars # - lists/tuples of length <= 3 # - numpy arrays a = np.array stests = [ (5, [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ([5], [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ((5, ), [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), (a([5]), [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ([5, 6], [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ((5, 6), [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), (a([5, 6]), [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ([5, 6, 7], [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 0, 0, 0, 0, 1]), ((5, 6, 7), [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 0, 0, 0, 0, 1]), (a([5, 6, 7]), [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 0, 0, 0, 0, 1]), ] otests = [ (5, [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ([5], [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ((5, ), [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), (a([5]), [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), ([5, 6], [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 0, 0, 0, 0, 1]), ((5, 6), [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 0, 0, 0, 0, 1]), (a([5, 6]), [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 0, 0, 0, 0, 1]), ([5, 6, 7], [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 7, 0, 0, 0, 1]), ((5, 6, 7), [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 7, 0, 0, 0, 1]), (a([5, 6, 7]), [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 7, 0, 0, 0, 1]), ] for (scale, expected) in stests: expected = np.array(expected).reshape(4, 4) result = affine.scaleOffsetXform(scale, 0) assert np.all(np.isclose(result, expected)) for (offset, expected) in otests: expected = np.array(expected).reshape(4, 4) result = affine.scaleOffsetXform(1, offset) assert np.all(np.isclose(result, expected))
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 = affine.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 = affine.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 = affine.scaleOffsetXform(tuple(1.0 / shape), tuple(0.5 / shape)) idToVoxMat = affine.invert(voxToIdMat) idToPixdimMat = affine.concat(voxToPixdimMat, idToVoxMat) idToPixFlipMat = affine.concat(voxToPixFlipMat, idToVoxMat) idToWorldMat = affine.concat(voxToWorldMat, idToVoxMat) idToRefMat = affine.concat(voxToRefMat, idToVoxMat) idToTexMat = affine.concat(voxToTexMat, idToVoxMat) pixdimToVoxMat = affine.invert(voxToPixdimMat) pixdimToIdMat = affine.concat(voxToIdMat, pixdimToVoxMat) pixdimToPixFlipMat = affine.concat(voxToPixFlipMat, pixdimToVoxMat) pixdimToWorldMat = affine.concat(voxToWorldMat, pixdimToVoxMat) pixdimToRefMat = affine.concat(voxToRefMat, pixdimToVoxMat) pixdimToTexMat = affine.concat(voxToTexMat, pixdimToVoxMat) pixFlipToVoxMat = affine.invert(voxToPixFlipMat) pixFlipToIdMat = affine.concat(voxToIdMat, pixFlipToVoxMat) pixFlipToPixdimMat = affine.concat(voxToPixdimMat, pixFlipToVoxMat) pixFlipToWorldMat = affine.concat(voxToWorldMat, pixFlipToVoxMat) pixFlipToRefMat = affine.concat(voxToRefMat, pixFlipToVoxMat) pixFlipToTexMat = affine.concat(voxToTexMat, pixFlipToVoxMat) worldToVoxMat = affine.invert(voxToWorldMat) worldToIdMat = affine.concat(voxToIdMat, worldToVoxMat) worldToPixdimMat = affine.concat(voxToPixdimMat, worldToVoxMat) worldToPixFlipMat = affine.concat(voxToPixFlipMat, worldToVoxMat) worldToRefMat = affine.concat(voxToRefMat, worldToVoxMat) worldToTexMat = affine.concat(voxToTexMat, worldToVoxMat) refToVoxMat = affine.invert(voxToRefMat) refToIdMat = affine.concat(voxToIdMat, refToVoxMat) refToPixdimMat = affine.concat(voxToPixdimMat, refToVoxMat) refToPixFlipMat = affine.concat(voxToPixFlipMat, refToVoxMat) refToWorldMat = affine.concat(voxToWorldMat, refToVoxMat) refToTexMat = affine.concat(voxToTexMat, refToVoxMat) texToVoxMat = affine.invert(voxToTexMat) texToIdMat = affine.concat(voxToIdMat, texToVoxMat) texToPixdimMat = affine.concat(voxToPixdimMat, texToVoxMat) texToPixFlipMat = affine.concat(voxToPixFlipMat, texToVoxMat) texToWorldMat = affine.concat(voxToWorldMat, texToVoxMat) texToRefMat = affine.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
def prepareData(data, prefilter=None, prefilterRange=None, resolution=None, scales=None, normalise=None, normaliseRange=None): """This function 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``. """ dtype = data.dtype floatTextures = canUseFloatTextures() 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 = affine.scaleOffsetXform(scale, offset) invVoxValXform = affine.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) return data, voxValXform, invVoxValXform
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 updateShaderState(self): """Updates the state of the vector vertex and fragment shaders - the fragment shader may may be either the ``glvolume`` or the ``glvector`` shader. """ opts = self.opts useVolumeFragShader = opts.colourImage is not None modLow, modHigh = self.getModulateRange() clipLow, clipHigh = self.getClippingRange() clipping = [clipLow, clipHigh, -1, -1] if np.isclose(modHigh, modLow): mod = [0, 0, 0, -1] else: mod = [modLow, modHigh, 1.0 / (modHigh - modLow), -1] # Inputs which are required by both the # glvolume and glvetor fragment shaders self.shader.setFragParam('clipping', clipping) clipCoordXform = self.getAuxTextureXform('clip') colourCoordXform = self.getAuxTextureXform('colour') modCoordXform = self.getAuxTextureXform('modulate') self.shader.setVertParam('clipCoordXform', clipCoordXform) self.shader.setVertParam('colourCoordXform', colourCoordXform) self.shader.setVertParam('modCoordXform', modCoordXform) if useVolumeFragShader: voxValXform = self.colourTexture.voxValXform cmapXform = self.cmapTexture.getCoordinateTransform() voxValXform = affine.concat(cmapXform, voxValXform) voxValXform = [voxValXform[0, 0], voxValXform[0, 3], 0, 0] self.shader.setFragParam('voxValXform', voxValXform) # settings expected by glvolume # frag shader, but not used self.shader.setFragParam('negCmap', [-1, 0, 0, 0]) self.shader.setFragParam('modulate', [0, 0, -1, 1]) else: colours, colourXform = self.getVectorColours() # See comments in gl21/glvector_funcs.py if self.vectorImage.niftiDataType == constants.NIFTI_DT_RGB24: voxValXform = affine.scaleOffsetXform(2, -1) else: voxValXform = self.imageTexture.voxValXform voxValXform = [voxValXform[0, 0], voxValXform[0, 3], 0, 0] self.shader.setFragParam('voxValXform', voxValXform) self.shader.setFragParam('mod', mod) self.shader.setFragParam('xColour', colours[0]) self.shader.setFragParam('yColour', colours[1]) self.shader.setFragParam('zColour', colours[2]) self.shader.setFragParam('colourXform', [colourXform[0, 0], colourXform[0, 3], 0, 0]) return True
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 = affine.scaleOffsetXform([scale] * 3, 0) rotate = affine.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 = affine.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 = affine.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
def _draw(self): """Draws the scene to the canvas. """ if self.destroyed: return 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 = affine.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 = affine.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() if opts.showLight: self.__drawLight() # Testing click-to-near/far clipping plane transformation if hasattr(self, 'points'): colours = [(1, 0, 0, 1), (0, 0, 1, 1)] gl.glPointSize(5) gl.glBegin(gl.GL_LINES) for i, p in enumerate(self.points): gl.glColor4f(*colours[i % 2]) p = affine.transform(p, self.viewMatrix) gl.glVertex3f(*p) gl.glEnd()
def updateShaderState(self, useSpline=False): """Updates the state of the vector vertex fragment shader. The fragment shader may be either the ``glvolume`` or the ``glvector`` shader. """ changed = False opts = self.opts shader = self.shader imageShape = self.vectorImage.shape[:3] modLow, modHigh = self.getModulateRange() clipLow, clipHigh = self.getClippingRange() if opts.modulateImage is None: modShape = [1, 1, 1] else: modShape = opts.modulateImage.shape[:3] if opts.clipImage is None: clipShape = [1, 1, 1] else: clipShape = opts.clipImage.shape[:3] clipXform = self.getAuxTextureXform('clip') colourXform = self.getAuxTextureXform('colour') modXform = self.getAuxTextureXform('modulate') changed |= self.shader.set('clipCoordXform', clipXform) changed |= self.shader.set('colourCoordXform', colourXform) changed |= self.shader.set('modCoordXform', modXform) if self.useVolumeFragShader: voxValXform = self.colourTexture.voxValXform img2CmapXform = affine.concat( self.cmapTexture.getCoordinateTransform(), voxValXform) changed |= shader.set('clipTexture', 1) changed |= shader.set('imageTexture', 2) changed |= shader.set('colourTexture', 3) changed |= shader.set('negColourTexture', 3) changed |= shader.set('img2CmapXform', img2CmapXform) changed |= shader.set('imageShape', imageShape) changed |= shader.set('imageIsClip', False) changed |= shader.set('useNegCmap', False) changed |= shader.set('useSpline', useSpline) changed |= shader.set('clipLow', clipLow) changed |= shader.set('clipHigh', clipHigh) changed |= shader.set('invertClip', False) else: # If we are displaying an RGB24 image # as a vector, we map uint8 [0, 255] to # [-1, 1]. The integer values will be # automatically normalised to [0, 1], # so we transform from that range. if self.vectorImage.niftiDataType == constants.NIFTI_DT_RGB24: voxValXform = affine.scaleOffsetXform(2, -1) # Otherwise, if it's floating point, # it will not be normalised. else: voxValXform = self.imageTexture.voxValXform colours, colourXform = self.getVectorColours() changed |= shader.set('modulateTexture', 0) changed |= shader.set('clipTexture', 1) changed |= shader.set('vectorTexture', 4) changed |= shader.set('xColour', colours[0]) changed |= shader.set('yColour', colours[1]) changed |= shader.set('zColour', colours[2]) changed |= shader.set('colourXform', colourXform) changed |= shader.set('voxValXform', voxValXform) changed |= shader.set('imageShape', imageShape) changed |= shader.set('modImageShape', modShape) changed |= shader.set('clipImageShape', clipShape) changed |= shader.set('clipLow', clipLow) changed |= shader.set('clipHigh', clipHigh) changed |= shader.set('modLow', modLow) changed |= shader.set('modHigh', modHigh) changed |= shader.set('useSpline', useSpline) return changed