neurolabusc / surf-ice

GLSL surface rendering source code. Compiled versions available from NITRC. Loads 3DS, CTM, DXF, FreeSurfer, GII (GIfTI), GTS, LWO2, MS3D, MZ3, NV (BrainNetViewer), OBJ, OFF, PLY, STL, VTK. Tractography formats include BFloat, PDB, TCK, TRK, and VTK. Also NIfTI format voxelwise images.
https://www.nitrc.org/plugins/mwiki/index.php/surfice:MainPage
BSD 2-Clause "Simplified" License
106 stars 23 forks source link

Change the color scale #29

Closed ali4006 closed 3 years ago

ali4006 commented 3 years ago

Hi, Thanks for the interesting tool. I'm using Surf-ice v1.0.20201102 on Linux. I would change the color map scale by using a log scale for that. I'm wondering if there is such a possibility?

Thanks.

neurolabusc commented 3 years ago

You can't with the current version. Happy to consider for a future release. However, what should the software do if the an input value is negative? I like to have software that has universally defined behavior. You can always transform your data prior to loading it into Surfice or MRIcroGL (e.g. by definition, all MRI magnitude data is positive).

ali4006 commented 3 years ago

You can set the scale to symmetric log scale just like the 'symlog' scale in matplotlib which handles negative values.

neurolabusc commented 3 years ago

It seems symlog is based on this idea, with a Matlab implementation here. The challenge is choosing the scaling constant C. Is there a clear consensus about this?

@pauldmccarthy FSLeyes appears to set all values less than or equal to zero to zero when the user Overlay display scaling to logarithmic scaling (natural log) and log10 plot views. Are you happy with this solution? Perhaps @hanayik has some thoughts on this.

pauldmccarthy commented 3 years ago

Hi @neurolabusc, yes - I take the easy route in FSLeyes - any voxel for which log(value) evaluates to NaN is coloured with the lowest colour in the selected colour map (e.g. black, when greyscale is selected). The reason that this happens is that I do not actually log-transform the data - instead, I log transform the colour map itself, so that the mapping from voxel value to colour is as if the value had been log-transformed: https://github.com/pauldmccarthy/fsleyes/blob/master/fsleyes/gl/textures/colourmaptexture.py#L254-L261

I suppose that a more sensible option is to hide/clip those voxels, but I only added this feature as a convenience for a couple of people in FMRIB - it's not really intended for viewing images which contain negative values.

In the plotting views, any values that evaluate to NaN are not plotted, so a discontinuous line will be plotted.

neurolabusc commented 3 years ago

@pauldmccarthy your approach is sensible. While the symlog solution is nice for a plot where the spacing near zero is known when generating a plot, it seems unwieldy for continuous data in an overlay, where choosing C seems treacherous.

pauldmccarthy commented 3 years ago

@neurolabusc One point which I forgot to mention - in FSLeyes, it is possible to combine the negative colour map and the logarithmic scaling options, so that negative values are also log-scaled.

Edit: this may not actually be working correctly at the moment :)

ali4006 commented 3 years ago

@neurolabusc following the previous question, I just realized that the tool plots NaN values as zero. Is that possible to just leave NaN values transparent there? The overlay scale I'm using ranges from -3 to 3 including zero values. I don't want to show NaN values on the overlay. Thanks.

neurolabusc commented 3 years ago

Can you email me (email in my avatar) a link to your sample overlay (e.g. Google Drive or DropBox)?

neurolabusc commented 3 years ago

Neither MRIcroGL or Surfice transform NIfTI images that have NIFTI_INTENT_LOGPVAL or NIFTI_INTENT_LOG10PVAL intent code. However, these values seem extremely rare in the wold. Further, it seems that in these cases many users may want to visualize the log values.

For general storage of log values in NIfTI images, it seems like log(x) (aka ln()), log(x,2), and log(x,10) might be the more likely base values.

Given this variability, my own suggestion would be for users to use simple Python scripts to transform their data into the preferred representation. For example, the snippet below takes log10 data and transforms it to z-scores (where input is expected to be in the range weminus infinity to zero, corresponding to a probability of zero to one).

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#Example usage
#  ./logToZ.py f.nii.gz

import nibabel as nib
import numpy as np
import scipy.stats as st
import os
import sys

def log10t(fnm):
    niiLog = nib.load(fnm)
    img = niiLog.get_fdata()
    str = f'Input intensity range {np.nanmin(img)}..{np.nanmax(img)}'
    print(str)
    str = f'Image shape {img.shape[1]}x{img.shape[1]}x{img.shape[1]}'
    print(str)
    #https://stackoverflow.com/questions/51669367/how-to-get-the-inverse-of-a-log10-value-in-python
    #inverse log 10
    img = 10 ** img
    img = st.norm.ppf(img)
    nii = nib.Nifti1Image(img, niiLog.affine, niiLog.header)
    #set NIFTI_INTENT_ZSCORE (5)
    nii.header['intent_code'] = 5
    nii.header['scl_slope'] = 1.0
    nii.header['scl_inter'] = 0.0
    nii.header['cal_max'] = 0.0
    nii.header['cal_min'] = 0.0
    pth, nm = os.path.split(fnm)
    if not pth:
        pth = '.'
    outnm = pth + os.path.sep + 'z' + nm
    nib.save(nii, outnm)

if __name__ == '__main__':
    """Convert log10p to z-score
    Parameters
    ----------
    fnm : str
        NIfTI image to convert
    """
    if len(sys.argv) < 2:
        print('No filename provided: I do not know which image to convert!')
        sys.exit()
    fnm = sys.argv[1]
    log10t(fnm)
ali4006 commented 3 years ago

Thanks for your response. This may be helpful for others. To transparent NaN voxels, I ended up setting the min-max of the overlay to a value close to zero like 0.0001 and not exactly zero.