Commit bfee81c6 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Merge branch 'rf/linevector-scaling' into 'master'

Scale line vector colours in addition to lengths

See merge request fsl/fsleyes/fsleyes!285
parents abf10fd4 86e9cbb8
......@@ -9,9 +9,12 @@ This document contains the ``fsleyes`` release history in reverse
chronological order.
1.2.0 (Monday 13th September 2021)
----------------------------------
1.2.0 (Under development)
-------------------------
Added
^^^^^
* The **Display space** setting can now be set to *Scaled voxel coordinates*
......@@ -20,6 +23,24 @@ chronological order.
``(0, 0, 0)`` (!286).
Changed
^^^^^^^
* The *scale vectors to unit length* option for line vector overlays now
scales the vector colouring, in addition to lengths (!285).
Fixed
^^^^^
* Fixed an issue on macOS / Big Sur whereby an image specified on the
command-line could be loaded twice (!285).
* Fixed some rendering issues for images stored as type ``NIFTI_TYPE_RGB24``
(!285).
1.1.0 (Friday 6th August 2021)
------------------------------
......
......@@ -88,9 +88,9 @@ PARAM zColour = {{ param_zColour }};
PARAM colourXform = {{ param_colourXform }};
# Bail if the textureq coordinate
# Bail if the texture coordinate
# is out of the image space.
# We use voxValue out of convenience
# We use voxValue as a temporary
{{
arb_call('textest.prog',
texCoord='{{ varying_vecTexCoord }}',
......@@ -105,6 +105,26 @@ TEX voxValue, {{ varying_vecTexCoord }}, {{ texture_vectorTexture }}, 3D;
TEX modValue, {{ varying_modTexCoord }}, {{ texture_modulateTexture }}, 3D;
TEX clipValue, {{ varying_clipTexCoord }}, {{ texture_clipTexture }}, 3D;
# Kill the fragment if this vector
# has length 0 or contains NaNs
# We use clipResult as a temporary
# kill if length is 0
DP3 clipResult.x, voxValue, voxValue;
MUL clipResult.x, clipResult.x, -1;
SGE clipResult.x, clipResult.x, 0;
MUL clipResult.x, clipResult.x, -1;
KIL clipResult.x;
# kill if vector contains NaNs.
# There is no nan test, or equality
# test, so we test whether
# (value >= value) || (value < value)
DP3 clipResult.x, voxValue, voxValue;
SGE clipResult.y, clipResult.x, clipResult.x;
SLT clipResult.z, clipResult.x, clipResult.x;
ADD clipResult.x, clipResult.y, clipResult.z;
SUB clipResult.x, clipResult.x, 0.5;
KIL clipResult.x;
# Clobber the clipping/modulation
# values we just looked up if their
# texture coords were out of bounds.
......@@ -164,11 +184,17 @@ MAD voxValue, voxValue, voxValXform.x, voxValXform.y;
ABS voxValue, voxValue;
# Cumulatively combine the rgb
# channels of those three colours
MOV fragColour, 0;
MAD fragColour, voxValue.x, xColour, fragColour;
MAD fragColour, voxValue.y, yColour, fragColour;
MAD fragColour, voxValue.z, zColour, fragColour;
# channels of those three colours.
# Opacity is not modulated by
# vector values.
MOV fragColour, 0;
MAD fragColour, voxValue.x, xColour, fragColour;
MAD fragColour, voxValue.y, yColour, fragColour;
MAD fragColour, voxValue.z, zColour, fragColour;
MOV fragColour.w, xColour.w;
ADD fragColour.w, fragColour.w, yColour.w;
ADD fragColour.w, fragColour.w, zColour.w;
MUL fragColour.w, fragColour.w, 0.333333;
# Apply the bri/con scale and offset
MAD fragColour.rgb, fragColour, colourXform.x, colourXform.y;
......
......@@ -12,7 +12,7 @@
uniform sampler3D vectorTexture;
/*
* Transformations between voxel and
* Transformations between voxel and
* display coordinate systems.
*/
uniform mat4 displayToVoxMat;
......@@ -44,7 +44,7 @@ uniform vec3 imageShape;
uniform vec3 imageDims;
/*
* If true, the vectors are
* If true, the vectors are
* inverted about the x axis.
*/
uniform bool xFlip;
......@@ -56,12 +56,6 @@ uniform bool xFlip;
*/
uniform bool directed;
/*
* If true, each vector is scaled to have a length
* of 1*lengthScale in the image coordinate system.
*/
uniform bool unitLength;
/*
* Scale vector lengths by this amount.
*/
......@@ -88,11 +82,9 @@ varying vec4 fragColourFactor;
void main(void) {
vec3 texCoord;
vec3 vector;
vec3 voxCoord;
float vectorLen;
vec3 texCoord;
vec3 vector;
vec3 voxCoord;
/*
* Normalise the voxel coordinates to [0.0, 1.0],
......@@ -114,41 +106,26 @@ void main(void) {
* texture range of [0,1] to the original
* data range
*/
vector *= voxValXform[0].x;
vector += voxValXform[3].x;
vectorLen = length(vector);
vector *= voxValXform[0].x;
vector += voxValXform[3].x;
/* Invert about the x axis if necessary */
if (xFlip)
if (xFlip) {
vector.x = -vector.x;
}
/*
* Kill the vector if its length is 0.
* We have to be tolerant of errors,
/*
* Kill the vector if its length is 0.
* We have to be tolerant of errors,
* because of the transformation to/
* from the texture data range. This
* from the texture data range. This
* may end up being too tolerant.
*/
if (vectorLen < 0.0001) {
if (length(vector) < 0.0001) {
fragColourFactor = vec4(0, 0, 0, 0);
return;
}
if (unitLength) {
/*
* Scale the vector so it has length 0.5.
*/
vector /= 2 * vectorLen;
/*
* Scale the vector by the minimum voxel length,
* so it is a unit vector within real world space
*/
vector /= imageDims / min(imageDims.x, min(imageDims.y, imageDims.z));
}
vector *= lengthScale;
/*
......
......@@ -142,6 +142,12 @@ void main(void) {
voxValue = texture3D(vectorTexture, fragVecTexCoord).xyz;
}
/* Do not draw vectors with length 0 or with NaNs */
float len = length(voxValue.xyz);
if (len == 0 || len != len) {
discard;
}
/* Look up the modulation and clipping values */
float modValue;
float clipValue;
......@@ -199,10 +205,17 @@ void main(void) {
voxValue += voxValXform[3].x;
voxValue = abs(voxValue);
/* Combine the xyz component colours. */
/* Combine the xyz component colours,
* modulating them by the vector values.
* Opacity is not modulated by vector
* value.
*/
vec4 voxColour = voxValue.x * xColour +
voxValue.y * yColour +
voxValue.z * zColour;
voxColour.a = (xColour.a +
yColour.a +
zColour.a) / 3;
/*
* Apply the colour scale/offset -
......
......@@ -65,7 +65,6 @@ def init(self):
# changes.
self.opts.addListener('orientFlip', name, update, weak=False)
self.opts.addListener('directed', name, update, weak=False)
self.opts.addListener('unitLength', name, update, weak=False)
self.opts.addListener('lengthScale', name, update, weak=False)
self.opts.addListener('transform',
name,
......@@ -79,7 +78,6 @@ def destroy(self):
self.opts.removeListener('orientFlip', self.name)
self.opts.removeListener('directed', self.name)
self.opts.removeListener('unitLength', self.name)
self.opts.removeListener('lengthScale', self.name)
self.opts.removeListener('transform', self.name)
......@@ -114,20 +112,32 @@ def updateShaderState(self):
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
# If the unitLength option is on, the vector
# data will have already been scaled to have
# length 1 (see GLLineVector.__init__). But
# we draw vectors in two parts, from the voxel
# centre. So we have to half the vector lengths.
if opts.unitLength:
lengthScale /= 2
# We also scale the vector data by the
# minimum voxel length, so that each
# vector has unit length relative to
# the voxel dimensions.
fac = (image.pixdim[:3] / min(image.pixdim[:3]))
lengthScale /= fac
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)
......
......@@ -7,10 +7,8 @@
"""This module provides the :class:`GLLineVector` class, for displaying 3D
vector :class:`.Image` overlays in line mode.
The :class:`.GLLineVertices` class is also defined in this module, and is used
in certain rendering situations - specifically, when running in OpenGL
1.4. See the :mod:`.gl14.gllinevector_funcs` and
when running in OpenGL 1.4. See the :mod:`.gl14.gllinevector_funcs` and
:mod:`.gl21.gllinevector_funcs` modules for more details.
"""
......@@ -70,17 +68,55 @@ class GLLineVector(glvector.GLVector):
if isinstance(image, dtifit.DTIFitTensor): vecImage = image.V1()
else: vecImage = image
def prefilter(data):
# Scale to unit length if required.
if not self.opts.unitLength:
return data
data = np.copy(data)
with np.errstate(invalid='ignore'):
# Vector images stored as RGB24 data
# type are assumed to map from [0, 255]
# to [-1, 1], so cannot be normalised
if vecImage.nvals > 1:
return data
# calculate lengths
x = data[0, ...]
y = data[1, ...]
z = data[2, ...]
lens = np.sqrt(x ** 2 + y ** 2 + z ** 2)
# scale lengths to 1
data[0, ...] = x / lens
data[1, ...] = y / lens
data[2, ...] = z / lens
return data
def prefilterRange(dmin, dmax):
if self.opts.unitLength:
return 0, 1
else:
return dmin, dmax
glvector.GLVector.__init__(self,
image,
overlayList,
displayCtx,
canvas,
threedee,
prefilter=prefilter,
prefilterRange=prefilterRange,
vectorImage=vecImage,
init=lambda: fslgl.gllinevector_funcs.init(
self))
self.opts.addListener('lineWidth', self.name, self.notify)
self.opts.addListener('lineWidth', self.name, self.notify)
self.opts.addListener('unitLength', self.name,
self.__unitLengthChanged)
def destroy(self):
......@@ -89,7 +125,8 @@ class GLLineVector(glvector.GLVector):
instance, calls the OpenGL version-specific ``destroy``
function, and calls the :meth:`.GLVector.destroy` method.
"""
self.opts.removeListener('lineWidth', self.name)
self.opts.removeListener('lineWidth', self.name)
self.opts.removeListener('unitLength', self.name)
fslgl.gllinevector_funcs.destroy(self)
glvector.GLVector.destroy(self)
......@@ -159,11 +196,26 @@ class GLLineVector(glvector.GLVector):
fslgl.gllinevector_funcs.postDraw(self, xform, bbox)
class GLLineVertices(object):
"""The ``GLLineVertices`` class is used in some cases when rendering a
:class:`GLLineVector`. It contains logic to generate vertices for every
vector in the vector :class:`.Image` that is being displayed by a
``GLLineVector`` instance.
def __unitLengthChanged(self, *a):
"""Called when the :attr:`.LineVectorOptsunitLength` property
changes. Refreshes the vector image texture data.
"""
self.imageTexture.refresh()
self.updateShaderState()
class GLLineVertices:
"""The ``GLLineVertices`` class is used when rendering a
:class:`GLLineVector` with OpenGL 1.4. It contains logic to generate
vertices for every vector in the vector :class:`.Image` that is being
displayed by a ``GLLineVector`` instance.
This class is used by the OpenGL 1.4 implementation - when using OpenGL
2.1, the logic encoded in this class is implemented in the line vector
vertex shader. This is because OpenGL 1.4 vertex programs (using the
ARB_vertex_program extension) are unable to perform texture lookups,
so cannot retrieve the vector data.
After a ``GLLineVertices`` instance has been created, the :meth:`refresh`
......@@ -259,7 +311,6 @@ class GLLineVertices(object):
# The image may either
# have shape (X, Y, Z, 3)
if image.nvals == 1:
vertices = np.array(data, dtype=np.float32)
# Or (we assume) a RGB
......
......@@ -82,6 +82,13 @@ class GLRGBVector(glvector.GLVector):
else: vecImage = image
def prefilter(data):
# Vector images stored as RGB24 data
# type are assumed to map from [0, 255]
# to [-1, 1], so cannot be normalised
if vecImage.nvals > 1:
return data
# make absolute, and scale to unit
# length if required. We must make
# the data absolute, otherwise we
......
......@@ -13,6 +13,7 @@ import numpy as np
import OpenGL.GL as gl
import fsl.utils.idle as idle
import fsl.transform.affine as affine
import fsleyes.gl as fslgl
import fsleyes.gl.routines as glroutines
......@@ -190,6 +191,23 @@ class GLRGBVolume(glimageobject.GLImageObject):
self.updateShaderState()
def generateVertices2D(self, zpos, axes, bbox=None):
"""Overrides :meth:`.GLImageObject.generateVertices2D`.
Appliies the :meth:`.ImageTextureBase.texCoordXform` to the texture
coordinates - this is performed to support 2D images/textures.
"""
vertices, voxCoords, texCoords = \
glimageobject.GLImageObject.generateVertices2D(
self, zpos, axes, bbox)
texCoords = affine.transform(
texCoords, self.imageTexture.texCoordXform(self.overlay.shape))
return vertices, voxCoords, texCoords
def channelColours(self):
"""Returns a ``numpy`` array of shape ``(3, 4)``, containing the
colours to use for each of the three channels.
......
......@@ -303,9 +303,6 @@ class Texture2D(texture.Texture):
if origShape is None:
return None
if self.nvals > 1:
origShape = origShape[1:]
# Here we apply a rotation to the
# coordinates to force the two major
# voxel axes to map to the first two
......
......@@ -189,6 +189,14 @@ class FSLeyesApp(wx.App):
# queue the files to open them later
# in SetOverlayListAndDisplayContext
if self.__overlayList is None:
# On certain systems (observed on Big Sur), when
# files are passed as command-line arguments, they
# are passed to MacOpenFiles. We don't want that,
# because cli arguments are parsed separately. So
# we remove dupes here.
filenames = [f for f in filenames if f not in sys.argv]
self.__filesToOpen.extend(filenames)
return
......
......@@ -793,8 +793,6 @@ def zero_centre(infile):
return outfile
def complex():
data = np.linspace(0, 1, 1000).reshape((10, 10, 10)) + \
......@@ -829,6 +827,16 @@ def invert(infile):
return outfile
def mul(infile, factor):
basename = fslimage.removeExt(op.basename(infile))
outfile = '{}_mul_{}.nii.gz'.format(basename, factor)
img = fslimage.Image(infile)
data = img[:]
img[:] = data * factor
img.save(outfile)
return outfile
def mockMouseEvent(profile, canvas, evType, canvasLoc):
"""Mock a mouse event on a SliceCanvas
"""
......
......@@ -5,9 +5,13 @@
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import os.path as op
import pytest
from fsleyes.tests import run_cli_tests, roi, asrgb
import fsl.data.image as fslimage
from fsleyes.tests import run_cli_tests, roi, asrgb, mul
pytestmark = pytest.mark.overlayclitest
......@@ -35,13 +39,20 @@ dti/dti_FA.nii.gz {{asrgb('dti/dti_V1.nii.gz')}} -ot linevector
# test anisotropic voxels
dti/anisotropic/dti_FA dti/anisotropic/dti_V1 -ot linevector
"""
# unit length / colour scaling
dti/dti_FA.nii.gz dti/dti_V1.nii.gz -ot linevector -nu
dti/dti_FA.nii.gz {{mul('dti/dti_V1.nii.gz', 0.5)}} -ot linevector
dti/dti_FA.nii.gz {{mul('dti/dti_V1.nii.gz', 0.5)}} -ot linevector -nu
dti/dti_FA.nii.gz {{mul('dti/dti_V1.nii.gz', 2.0)}} -ot linevector
dti/dti_FA.nii.gz {{mul('dti/dti_V1.nii.gz', 2.0)}} -ot linevector -nu
"""
def test_overlay_linevector():
extras = {
'roi' : roi,
'asrgb' : asrgb,
'mul' : mul,
}
run_cli_tests('test_overlay_linevector',
cli_tests,
......
......@@ -62,6 +62,9 @@ cli_tests = """
{{asrgb(roi('dti/dti_V1', (0, 8, 0, 8, 4, 5)))}}
{{asrgb(roi('dti/dti_V1', (0, 8, 4, 5, 0, 8)))}}
{{asrgb(roi('dti/dti_V1', (4, 5, 0, 8, 0, 8)))}}
{{asrgb(roi('dti/dti_V1', (0, 8, 0, 8, 4, 5)))}} -ot rgb
{{asrgb(roi('dti/dti_V1', (0, 8, 4, 5, 0, 8)))}} -ot rgb
{{asrgb(roi('dti/dti_V1', (4, 5, 0, 8, 0, 8)))}} -ot rgb
{{asrgb(roi('dti/dti_V1', (0, 8, 0, 8, 4, 5)))}} -ot rgbvector
{{asrgb(roi('dti/dti_V1', (0, 8, 4, 5, 0, 8)))}} -ot rgbvector
{{asrgb(roi('dti/dti_V1', (4, 5, 0, 8, 0, 8)))}} -ot rgbvector
......
......@@ -9,7 +9,7 @@ from unittest import mock
import pytest
from fsleyes.tests import run_cli_tests, asrgb
from fsleyes.tests import run_cli_tests, asrgb, mul
import fsleyes.gl.textures.data as texdata
......@@ -32,11 +32,18 @@ dti/dti_V1 -ot rgbvector -in linear -b 25 -c 25
dti/dti_V1 -ot rgbvector -in spline -b 25 -c 25
{{asrgb('dti/dti_V1')}} -ot rgbvector
# unit length scaling
{{mul('dti/dti_V1', 0.5)}} -ot rgbvector -u
{{mul('dti/dti_V1', 0.5)}} -ot rgbvector
{{mul('dti/dti_V1', 2.0)}} -ot rgbvector -u
{{mul('dti/dti_V1', 2.0)}} -ot rgbvector
"""
def test_overlay_rgbvector():
extras = {
'asrgb' : asrgb,
'mul' : mul,
}
run_cli_tests('test_overlay_rgbvector', cli_tests, extras=extras)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment