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 ...@@ -9,9 +9,12 @@ This document contains the ``fsleyes`` release history in reverse
chronological order. 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* * The **Display space** setting can now be set to *Scaled voxel coordinates*
...@@ -20,6 +23,24 @@ chronological order. ...@@ -20,6 +23,24 @@ chronological order.
``(0, 0, 0)`` (!286). ``(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) 1.1.0 (Friday 6th August 2021)
------------------------------ ------------------------------
......
...@@ -88,9 +88,9 @@ PARAM zColour = {{ param_zColour }}; ...@@ -88,9 +88,9 @@ PARAM zColour = {{ param_zColour }};
PARAM colourXform = {{ param_colourXform }}; PARAM colourXform = {{ param_colourXform }};
# Bail if the textureq coordinate # Bail if the texture coordinate
# is out of the image space. # is out of the image space.
# We use voxValue out of convenience # We use voxValue as a temporary
{{ {{
arb_call('textest.prog', arb_call('textest.prog',
texCoord='{{ varying_vecTexCoord }}', texCoord='{{ varying_vecTexCoord }}',
...@@ -105,6 +105,26 @@ TEX voxValue, {{ varying_vecTexCoord }}, {{ texture_vectorTexture }}, 3D; ...@@ -105,6 +105,26 @@ TEX voxValue, {{ varying_vecTexCoord }}, {{ texture_vectorTexture }}, 3D;
TEX modValue, {{ varying_modTexCoord }}, {{ texture_modulateTexture }}, 3D; TEX modValue, {{ varying_modTexCoord }}, {{ texture_modulateTexture }}, 3D;
TEX clipValue, {{ varying_clipTexCoord }}, {{ texture_clipTexture }}, 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 # Clobber the clipping/modulation
# values we just looked up if their # values we just looked up if their
# texture coords were out of bounds. # texture coords were out of bounds.
...@@ -164,11 +184,17 @@ MAD voxValue, voxValue, voxValXform.x, voxValXform.y; ...@@ -164,11 +184,17 @@ MAD voxValue, voxValue, voxValXform.x, voxValXform.y;
ABS voxValue, voxValue; ABS voxValue, voxValue;
# Cumulatively combine the rgb # Cumulatively combine the rgb
# channels of those three colours # channels of those three colours.
MOV fragColour, 0; # Opacity is not modulated by
MAD fragColour, voxValue.x, xColour, fragColour; # vector values.
MAD fragColour, voxValue.y, yColour, fragColour; MOV fragColour, 0;
MAD fragColour, voxValue.z, zColour, fragColour; 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 # Apply the bri/con scale and offset
MAD fragColour.rgb, fragColour, colourXform.x, colourXform.y; MAD fragColour.rgb, fragColour, colourXform.x, colourXform.y;
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
uniform sampler3D vectorTexture; uniform sampler3D vectorTexture;
/* /*
* Transformations between voxel and * Transformations between voxel and
* display coordinate systems. * display coordinate systems.
*/ */
uniform mat4 displayToVoxMat; uniform mat4 displayToVoxMat;
...@@ -44,7 +44,7 @@ uniform vec3 imageShape; ...@@ -44,7 +44,7 @@ uniform vec3 imageShape;
uniform vec3 imageDims; uniform vec3 imageDims;
/* /*
* If true, the vectors are * If true, the vectors are
* inverted about the x axis. * inverted about the x axis.
*/ */
uniform bool xFlip; uniform bool xFlip;
...@@ -56,12 +56,6 @@ uniform bool xFlip; ...@@ -56,12 +56,6 @@ uniform bool xFlip;
*/ */
uniform bool directed; 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. * Scale vector lengths by this amount.
*/ */
...@@ -88,11 +82,9 @@ varying vec4 fragColourFactor; ...@@ -88,11 +82,9 @@ varying vec4 fragColourFactor;
void main(void) { void main(void) {
vec3 texCoord;
vec3 texCoord; vec3 vector;
vec3 vector; vec3 voxCoord;
vec3 voxCoord;
float vectorLen;
/* /*
* Normalise the voxel coordinates to [0.0, 1.0], * Normalise the voxel coordinates to [0.0, 1.0],
...@@ -114,41 +106,26 @@ void main(void) { ...@@ -114,41 +106,26 @@ void main(void) {
* texture range of [0,1] to the original * texture range of [0,1] to the original
* data range * data range
*/ */
vector *= voxValXform[0].x; vector *= voxValXform[0].x;
vector += voxValXform[3].x; vector += voxValXform[3].x;
vectorLen = length(vector);
/* Invert about the x axis if necessary */ /* Invert about the x axis if necessary */
if (xFlip) if (xFlip) {
vector.x = -vector.x; vector.x = -vector.x;
}
/* /*
* Kill the vector if its length is 0. * Kill the vector if its length is 0.
* We have to be tolerant of errors, * We have to be tolerant of errors,
* because of the transformation to/ * because of the transformation to/
* from the texture data range. This * from the texture data range. This
* may end up being too tolerant. * may end up being too tolerant.
*/ */
if (vectorLen < 0.0001) { if (length(vector) < 0.0001) {
fragColourFactor = vec4(0, 0, 0, 0); fragColourFactor = vec4(0, 0, 0, 0);
return; 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; vector *= lengthScale;
/* /*
......
...@@ -142,6 +142,12 @@ void main(void) { ...@@ -142,6 +142,12 @@ void main(void) {
voxValue = texture3D(vectorTexture, fragVecTexCoord).xyz; 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 */ /* Look up the modulation and clipping values */
float modValue; float modValue;
float clipValue; float clipValue;
...@@ -199,10 +205,17 @@ void main(void) { ...@@ -199,10 +205,17 @@ void main(void) {
voxValue += voxValXform[3].x; voxValue += voxValXform[3].x;
voxValue = abs(voxValue); 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 + vec4 voxColour = voxValue.x * xColour +
voxValue.y * yColour + voxValue.y * yColour +
voxValue.z * zColour; voxValue.z * zColour;
voxColour.a = (xColour.a +
yColour.a +
zColour.a) / 3;
/* /*
* Apply the colour scale/offset - * Apply the colour scale/offset -
......
...@@ -65,7 +65,6 @@ def init(self): ...@@ -65,7 +65,6 @@ def init(self):
# changes. # changes.
self.opts.addListener('orientFlip', name, update, weak=False) self.opts.addListener('orientFlip', name, update, weak=False)
self.opts.addListener('directed', 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('lengthScale', name, update, weak=False)
self.opts.addListener('transform', self.opts.addListener('transform',
name, name,
...@@ -79,7 +78,6 @@ def destroy(self): ...@@ -79,7 +78,6 @@ def destroy(self):
self.opts.removeListener('orientFlip', self.name) self.opts.removeListener('orientFlip', self.name)
self.opts.removeListener('directed', self.name) self.opts.removeListener('directed', self.name)
self.opts.removeListener('unitLength', self.name)
self.opts.removeListener('lengthScale', self.name) self.opts.removeListener('lengthScale', self.name)
self.opts.removeListener('transform', self.name) self.opts.removeListener('transform', self.name)
...@@ -114,20 +112,32 @@ def updateShaderState(self): ...@@ -114,20 +112,32 @@ def updateShaderState(self):
vvxMat = self.imageTexture.voxValXform vvxMat = self.imageTexture.voxValXform
directed = opts.directed directed = opts.directed
unitLength = opts.unitLength
lengthScale = opts.lengthScale / 100.0 lengthScale = opts.lengthScale / 100.0
imageDims = image.pixdim[:3] imageDims = image.pixdim[:3]
d2vMat = opts.getTransform('display', 'voxel') d2vMat = opts.getTransform('display', 'voxel')
v2dMat = opts.getTransform('voxel', 'display') v2dMat = opts.getTransform('voxel', 'display')
xFlip = opts.orientFlip 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('vectorTexture', 4)
changed |= shader.set('displayToVoxMat', d2vMat) changed |= shader.set('displayToVoxMat', d2vMat)
changed |= shader.set('voxToDisplayMat', v2dMat) changed |= shader.set('voxToDisplayMat', v2dMat)
changed |= shader.set('voxValXform', vvxMat) changed |= shader.set('voxValXform', vvxMat)
changed |= shader.set('imageDims', imageDims) changed |= shader.set('imageDims', imageDims)
changed |= shader.set('directed', directed) changed |= shader.set('directed', directed)
changed |= shader.set('unitLength', unitLength)
changed |= shader.set('lengthScale', lengthScale) changed |= shader.set('lengthScale', lengthScale)
changed |= shader.set('xFlip', xFlip) changed |= shader.set('xFlip', xFlip)
......
...@@ -7,10 +7,8 @@ ...@@ -7,10 +7,8 @@
"""This module provides the :class:`GLLineVector` class, for displaying 3D """This module provides the :class:`GLLineVector` class, for displaying 3D
vector :class:`.Image` overlays in line mode. vector :class:`.Image` overlays in line mode.
The :class:`.GLLineVertices` class is also defined in this module, and is used The :class:`.GLLineVertices` class is also defined in this module, and is used
in certain rendering situations - specifically, when running in OpenGL when running in OpenGL 1.4. See the :mod:`.gl14.gllinevector_funcs` and
1.4. See the :mod:`.gl14.gllinevector_funcs` and
:mod:`.gl21.gllinevector_funcs` modules for more details. :mod:`.gl21.gllinevector_funcs` modules for more details.
""" """
...@@ -70,17 +68,55 @@ class GLLineVector(glvector.GLVector): ...@@ -70,17 +68,55 @@ class GLLineVector(glvector.GLVector):
if isinstance(image, dtifit.DTIFitTensor): vecImage = image.V1() if isinstance(image, dtifit.DTIFitTensor): vecImage = image.V1()
else: vecImage = image 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, glvector.GLVector.__init__(self,
image, image,
overlayList, overlayList,
displayCtx, displayCtx,
canvas, canvas,
threedee, threedee,
prefilter=prefilter,
prefilterRange=prefilterRange,
vectorImage=vecImage, vectorImage=vecImage,
init=lambda: fslgl.gllinevector_funcs.init( init=lambda: fslgl.gllinevector_funcs.init(
self)) 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): def destroy(self):
...@@ -89,7 +125,8 @@ class GLLineVector(glvector.GLVector): ...@@ -89,7 +125,8 @@ class GLLineVector(glvector.GLVector):
instance, calls the OpenGL version-specific ``destroy`` instance, calls the OpenGL version-specific ``destroy``
function, and calls the :meth:`.GLVector.destroy` method. 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) fslgl.gllinevector_funcs.destroy(self)
glvector.GLVector.destroy(self) glvector.GLVector.destroy(self)
...@@ -159,11 +196,26 @@ class GLLineVector(glvector.GLVector): ...@@ -159,11 +196,26 @@ class GLLineVector(glvector.GLVector):
fslgl.gllinevector_funcs.postDraw(self, xform, bbox) fslgl.gllinevector_funcs.postDraw(self, xform, bbox)
class GLLineVertices(object): def __unitLengthChanged(self, *a):
"""The ``GLLineVertices`` class is used in some cases when rendering a """Called when the :attr:`.LineVectorOptsunitLength` property
:class:`GLLineVector`. It contains logic to generate vertices for every changes. Refreshes the vector image texture data.
vector in the vector :class:`.Image` that is being displayed by a """
``GLLineVector`` instance. 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` After a ``GLLineVertices`` instance has been created, the :meth:`refresh`
...@@ -259,7 +311,6 @@ class GLLineVertices(object): ...@@ -259,7 +311,6 @@ class GLLineVertices(object):
# The image may either # The image may either
# have shape (X, Y, Z, 3) # have shape (X, Y, Z, 3)
if image.nvals == 1: if image.nvals == 1:
vertices = np.array(data, dtype=np.float32) vertices = np.array(data, dtype=np.float32)
# Or (we assume) a RGB # Or (we assume) a RGB
......
...@@ -82,6 +82,13 @@ class GLRGBVector(glvector.GLVector): ...@@ -82,6 +82,13 @@ class GLRGBVector(glvector.GLVector):
else: vecImage = image else: vecImage = image
def prefilter(data): 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 # make absolute, and scale to unit
# length if required. We must make # length if required. We must make
# the data absolute, otherwise we # the data absolute, otherwise we
......
...@@ -13,6 +13,7 @@ import numpy as np ...@@ -13,6 +13,7 @@ import numpy as np
import OpenGL.GL as gl import OpenGL.GL as gl
import fsl.utils.idle as idle import fsl.utils.idle as idle
import fsl.transform.affine as affine
import fsleyes.gl as fslgl import fsleyes.gl as fslgl
import fsleyes.gl.routines as glroutines import fsleyes.gl.routines as glroutines
...@@ -190,6 +191,23 @@ class GLRGBVolume(glimageobject.GLImageObject): ...@@ -190,6 +191,23 @@ class GLRGBVolume(glimageobject.GLImageObject):
self.updateShaderState() 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): def channelColours(self):
"""Returns a ``numpy`` array of shape ``(3, 4)``, containing the """Returns a ``numpy`` array of shape ``(3, 4)``, containing the
colours to use for each of the three channels. colours to use for each of the three channels.
......
...@@ -303,9 +303,6 @@ class Texture2D(texture.Texture): ...@@ -303,9 +303,6 @@ class Texture2D(texture.Texture):
if origShape is None: if origShape is None:
return None return None
if self.nvals > 1:
origShape = origShape[1:]
# Here we apply a rotation to the # Here we apply a rotation to the
# coordinates to force the two major # coordinates to force the two major
# voxel axes to map to the first two # voxel axes to map to the first two
......
...@@ -189,6 +189,14 @@ class FSLeyesApp(wx.App): ...@@ -189,6 +189,14 @@ class FSLeyesApp(wx.App):
# queue the files to open them later # queue the files to open them later
# in SetOverlayListAndDisplayContext # in SetOverlayListAndDisplayContext
if self.__overlayList is None: 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) self.__filesToOpen.extend(filenames)
return return
......