Coder Social home page Coder Social logo

dfsp-spirit / freesurferformats Goto Github PK

View Code? Open in Web Editor NEW
23.0 4.0 3.0 7.65 MB

GNU R package for reading and writing neuroimaging file formats. Not limited to FreeSurfer anymore, despite its name.

License: Other

R 100.00%
freesurfer r fileformats neuroimaging brain mri surface mesh voxel research

freesurferformats's Introduction

Hi, I'm Tim Schäfer. I am a bioinformatician and currently a Research Software Engineer in the department of Biodiversity Informatics at Senckenberg Society for Nature Research.

  • 🚀 I enjoy using tools like Python, Julia, C++, Docker and R to solve scientific problems, build high quality scientific software and services, and promote open science and open research data.
  • 🌱 I’m currently working on open linked data, FAIR digital objects, and sustainable research data management at Senckenberg. We use an internal GitLab instance, so my activity here on GitHub will decline a bit, but I still have an eye on the software I maintain.

I get a ton of Github notifications and things do slip through once in a while, so if you want to contact me on something unrelated to my software projects, please write me an email.

freesurferformats's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

freesurferformats's Issues

[Question] face index in read.fs.surface

Hi, @dfsp-spirit ,

I noticed a change(?) in read.fs.surface: it's face index now starts from 1. Is that change going to be in the future versions?

That brings me to a question: do you have any plan like from version xxx, file formats will be backward compatible? It'll be very nice if the format is consistent so that labs using your package could build a code repo that won't break easily.

One suggestion is you can make your return type environment, or R6Class so that you can use active bindings

# Just to store data, parent can be empty env
val <- new.env(parent = emptyenv())

# version 0.1, old formats
val$faces <- c(0,1,2)
val$faces

# > [1] 0 1 2

# version 0.2, new format, old one is available, but with "depricate" warning
# Now using a new format, the old format will result in a warning
val$faces2 <- 1:3
if(exists('faces', envir = val)){
  rm('faces', envir = val)
}
makeActiveBinding('faces', function(){
  warning('result$faces is depricated, use $faces2 instead')
  val$faces2 - 1
}, val)
val$faces

# > [1] 0 1 2
# > Warning message:
# >   In (function ()  : result$faces is depricated, use $faces2 instead

Best :)

Backup nitestdata on public, scientific repo

The extra data one can download with freesurferformats::download_opt_data() is hosted on my private server (in a professional data center, reachable via the rcmd.org domain).

This is fine with me, but we should create a backup of all the files and make it available on some public, scientific data repository like Zenodo.

It would be idea if the data was not in a zip, but as single files which can be directly linked to (so we can actually pull from there in fsbrain), but a zip archive as a pure backup of the data would be better than nothing, I guess.

This would mean that issues or changes on my server cannot affect fsbrain, and that future maintainers or contributors do not need access to my server to upload additional test data.

Support NIFTI v1 files with FreeSurfer hack

The mri_convert tool writes NIFTI v1 files with FreeSurfer hack if a single dimension in the data exceeds 32k entries. Since fs meshes typically have > 100k vertices, this affects almost all morphometry data files.

We should support reading the hacked NIFTI v1 files.

Surface writing: terminate header comment line with "\n\n"

When writing FreeSurfer brain meshes in "surf" format, the comment line in the header should be terminated with "\n\n".

This pattern is used by nibabel to identify the string length, and it does not break compatibility with anything else we know of, so I guess it's sane to change this.

[Feature Enhancement] Support asc, gii formats

Hi, I'm writing a 3D viewer (threeBrain) to visualize fs surface and volumes using R. Reading fs files is a headache for me until I came across your work. Thank you for developing this great package. I'm removing my old readers to depend on your fabulous freesurferformats.

Is there any plan to support reading asc or gii files just like reading from the default fs format?
For example, I only have rh.pial.asc, but I still want to use the same command to read from file as it were rh.pial via

read.fs.surface('rh.pial')

Hopefully the function first check if rh.pial exists, if not, try rh.pial.asc, then rh.pial.gii. In such way the users don't have to care the format they are using. Here's an example:

check_gifti <- function(){
  gifti_missing = system.file('', package = 'gifti') == ''
  if( gifti_missing ){
    stop('Package ', sQuote('gifti'), ' not installed. Please ...')
  }
}


read.fs.surface <- function(filepath, metadata = list()){
  
  if(!file.exists(filepath)){
    if(file.exists(file.path(filepath, '.asc'))){
      # read in ASCII file and return
      ...
    }else if(any(file.exists(file.path(filepath, c('.gii', '.gii.gz'))))){
      # check if gifti is installed
      check_gifti()
      
      # read in GIFTI file and return
      ...
    }
  }
  
  if (guess.filename.is.gzipped(filepath)) {
    fh = gzfile(filepath, "rb")
  }
  else {
    fh = file(filepath, "rb")
  }
  # always close the connection even if error occurs
  on.exit({ close(fh) }, add = TRUE)
  
  ...
  
}

[Feature request] Add `vox2ras_tkr` to mgh/mgz reader's headers

Freesurfer has vox2ras-tkr matrix, which has the same Mdc matrix as vox2ras_matrix but different location/center.

For example, I can read in mgz file with headers

res <- freesurferformats::read.fs.mgh(path, with_header = TRUE,
                                        flatten = FALSE, drop_empty_dims = FALSE)

Norig matrix (i.e. vox2ras_matrix)

# res$header$internal$M, or
res$header$vox2ras_matrix

>      [,1] [,2] [,3]      [,4]
> [1,]   -1    0    0 127.88193
> [2,]    0    0    1 -98.06789
> [3,]    0   -1    0 116.37625
> [4,]    0    0    0   1.00000

Torig matrix which is vox2ras-tkr

Mdc <- res$header$internal$Mdc
Pcrs_c <- res$header$internal$Pcrs_c
rbind(cbind(Mdc, - Mdc %*% Pcrs_c), c(0,0,0,1))

>      [,1] [,2] [,3] [,4]
> [1,]   -1    0    0  128
> [2,]    0    0    1 -128
> [3,]    0   -1    0  128
> [4,]    0    0    0    1

Reference: https://surfer.nmr.mgh.harvard.edu/fswiki/CoordinateSystems

[Question] re-orient data in read.fs.volume

Hi, is it possible to turn off the re-orient data feature of read.fs.volume? Is there a flag to do this?

> library(freesurferformats)
> read.fs.volume(
+   "brainWarp_jacobian.nii.gz",
+   format = "auto",
+   flatten = TRUE,
+   with_header = FALSE,
+   drop_empty_dims = FALSE
+ )

Error in performPermutation(trans, real.dimensions, data, verbose) : 
  Transformation is not simple, cannot reorient!

freesurfer surface import - row-major/column-major order

Thanks for sharing this excellent package! These freesurfer import functions are really helpful.

I've spotted what I think is a problem though (disclaimer: not an expert on the freesurfer surface format).

For context I have been working to try and calculate (in R) the average vertex-vertex euclidean distances for different surfaces.

As an early sense check I looked to see if neighbouring vertices (i.e. vertices in the same face) were close to each other. This wasn't the case.

I modified the import function to use the option matrix(..., byrow = TRUE) whenever a matrix is formed from a vector, and after this my check passed with the set of closest euclidean vertices belonging to the same faces as the probe vertex.

I think this is because R works with column major order data, but freesurfer is default row major (C/C++ style).

I've put a reproducible example. custom_read_fs is practically identical to freesurferformats::read.fs.surface, but uses byrow = TRUE in every matrix call.

Interestingly, this doesn't cause problems down the line for fsbrain::vis.data.on.subject which displays fine. I think this is because rgl::tmesh3d force-converts the input vertices to a wide matrix (each column is a vertex) with matrix(vertices, nrow = vrows). This implicit reshaping (without transposing) counteracts the column major import.

I have only investigated the read.fs.surface function, I would imagine this might need checking elsewhere if matrix data is being imported.

The other possibility is I don't know what I'm doing and have managed to mangle something in the code somewhere. I'd be grateful for your thoughts.

All the best,

Andrew


custom_read_fs <- function (filepath)
{
  TRIS_MAGIC_FILE_TYPE_NUMBER = 16777214
  QUAD_MAGIC_FILE_TYPE_NUMBER = 16777215
  if (freesurferformats:::guess.filename.is.gzipped(filepath)) {
    fh = gzfile(filepath, "rb")
  }
  else {
    fh = file(filepath, "rb")
  }
  ret_list = list()
  magic_byte = freesurferformats:::fread3(fh)
  if (magic_byte == QUAD_MAGIC_FILE_TYPE_NUMBER) {
    warning("Reading QUAD files in untested atm. Please use with care. This warning will be removed once the code has unit tests.")
    ret_list$mesh_face_type = "quads"
    num_vertices = freesurferformats:::fread3(fh)
    num_faces = freesurferformats:::fread3(fh)
    cat(sprintf("Reading surface file, expecting %d vertices and %d faces.\n",
                num_vertices, num_faces))
    ret_list$internal = list()
    ret_list$internal$num_vertices_expected = num_vertices
    ret_list$internal$num_faces_expected = num_faces
    num_vertex_coords = num_vertices * 3L
    vertex_coords = readBin(fh, integer(), size = 2L, n = num_vertex_coords,
                            endian = "big")
    vertex_coords = vertex_coords/100
    vertices = matrix(vertex_coords, nrow = num_vertices, byrow = TRUE,
                      ncol = 3L)
    if (length(vertex_coords) != num_vertex_coords) {
      stop(sprintf("Mismatch in read vertex coordinates: expected %d but received %d.\n",
                   num_vertex_coords, length(vertex_coords)))
    }
    num_face_vertex_indices = num_faces * 4L
    face_vertex_indices = rep(0, num_face_vertex_indices)
    faces = matrix(face_vertex_indices, nrow = num_faces, byrow = TRUE,
                   ncol = 4L)
    for (face_idx in 1L:num_faces) {
      for (vertex_idx_in_face in 1L:4L) {
        global_vertex_idx = freesurferformats:::fread3(fh)
        faces[face_idx, vertex_idx_in_face] = global_vertex_idx
      }
    }
  }
  else if (magic_byte == TRIS_MAGIC_FILE_TYPE_NUMBER) {
    ret_list$mesh_face_type = "tris"
    creation_date_text_line = readBin(fh, character(), endian = "big")
    seek(fh, where = 3, origin = "current")
    info_text_line = readBin(fh, character(), endian = "big")
    seek(fh, where = -5, origin = "current")
    ret_list$internal = list()
    ret_list$internal$creation_date_text_line = creation_date_text_line
    ret_list$internal$info_text_line = info_text_line
    num_vertices = readBin(fh, integer(), size = 4, n = 1,
                           endian = "big")
    num_faces = readBin(fh, integer(), size = 4, n = 1, endian = "big")
    ret_list$internal$num_vertices_expected = num_vertices
    ret_list$internal$num_faces_expected = num_faces
    num_vertex_coords = num_vertices * 3L
    vertex_coords = readBin(fh, numeric(), size = 4L, n = num_vertex_coords,
                            endian = "big")
    vertices = matrix(vertex_coords, nrow = num_vertices, byrow = TRUE,
                      ncol = 3L)
    if (length(vertex_coords) != num_vertex_coords) {
      stop(sprintf("Mismatch in read vertex coordinates: expected %d but received %d.\n",
                   num_vertex_coords, length(vertex_coords)))
    }
    num_face_vertex_indices = num_faces * 3L
    face_vertex_indices = readBin(fh, integer(), size = 4L,
                                  n = num_face_vertex_indices, endian = "big")
    faces = matrix(face_vertex_indices, nrow = num_faces, byrow = TRUE,
                   ncol = 3L)
    faces = faces + 1L
    if (length(face_vertex_indices) != num_face_vertex_indices) {
      stop(sprintf("Mismatch in read vertex indices for faces: expected %d but received %d.\n",
                   num_face_vertex_indices, length(face_vertex_indices)))
    }
  }
  else {
    stop(sprintf("Magic number mismatch (%d != (%d || %d)). The given file '%s' is not a valid FreeSurfer surface format file in binary format. (Hint: This function is designed to read files like 'lh.white' in the 'surf' directory of a pre-processed FreeSurfer subject.)\n",
                 magic_byte, TRIS_MAGIC_FILE_TYPE_NUMBER, QUAD_MAGIC_FILE_TYPE_NUMBER,
                 filepath))
  }
  close(fh)
  ret_list$vertices = vertices
  ret_list$vertex_indices_fs = 0L:(nrow(vertices) - 1)
  ret_list$faces = faces
  return(ret_list)
}

# reprex:
library(tidyverse)
library(fsbrain)
library(freesurferformats)

source('custom_read_fs.R')

subjects_dir = fsbrain::get_optional_data_filepath("subjects_dir")
surf <- freesurferformats::read.fs.surface(paste(subjects_dir, "subject1", "surf", "lh.white", sep = "/"))

# Probe vertex 10

# which other vertices share a face with vertex #10:
as.data.frame(surf$faces) %>% filter(V1 == 10 | V2 == 10 | V3 == 10)
#   V1    V2     V3
# 1 10 49506  99277
# 2 10 50521  99278
# 3 10 49506 100371
# 4 10 50520  99284
# 5 10 49507 100382
# 6 10 50528 100372

# save the vertex list:
v10.neighbours <- as.data.frame(surf$faces) %>%
  filter(V1 == 10 | V2 == 10 | V3 == 10) %>%
  unlist() %>% unique() %>% sort()
#  [1]     10  49506  49507  50520  50521  50528  99277  99278  99284 100371
# [11] 100372 100382

# calculate euclidean distances to vertex 10:
edist <- function(x1,x2) { sqrt(sum((x1 - x2)^2)) }
v10.distances <- sapply(1:nrow(surf$vertices), function(i) {
  edist(surf$vertices[i,], surf$vertices[10,])
} )

# print closest neighbour indices:
order(v10.distances)[1:10] %>% sort()
#  [1]     10  41683  41686  51154  51217  51220  68818  75268 137029 137032

# only the trivial node 10 is actually a neigbour:
order(v10.distances)[1:length(v10.neighbours)] %in% v10.neighbours
#  [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

# alternatively, what are the distances (in mm) for the face neighbours:
v10.distances[v10.neighbours]
#  [1]   0.00000 131.86736  29.65419 165.91990  25.04429  87.17706  15.43588
#  [8]  84.75748  85.86355 120.43829  19.10735  85.17581

# Repeat this, but with a modified function:
surf2 <- custom_read_fs(paste(subjects_dir, "subject1", "surf", "lh.white", sep = "/"))
as.data.frame(surf2$faces) %>% filter(V1 == 10 | V2 == 10 | V3 == 10)
#   V1  V2  V3
# 1 10   9   5
# 2  5 106  10
# 3  9  10  14
# 4 15  14  10
# 5 10 106 112
# 6 10 112  15

v10.neighbours2 <- as.data.frame(surf2$faces) %>%
  filter(V1 == 10 | V2 == 10 | V3 == 10) %>%
  unlist() %>% unique() %>% sort()
# [1]   5   9  10  14  15 106 112

# calculate euclidean distances to vertex 10:
v10.distances2 <- sapply(1:nrow(surf2$vertices), function(i) {
  edist(surf2$vertices[i,], surf2$vertices[10,])
} )

# print closest neighbour indices:
order(v10.distances2)[1:10] %>% sort()
#  [1]   4   5   9  10  14  15  22 106 112 119

# clostest vertices correctly identified the neighbours:
order(v10.distances2)[1:length(v10.neighbours2)] %in% v10.neighbours2
# [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE

# what are the distances (in mm) for the face neighbours:
v10.distances2[v10.neighbours2]
# [1] 0.7853750 0.8502197 0.0000000 1.1290272 0.8335467 0.9591681 0.7781360
# now all ~<1mm

Support reading MGH header data

Feature request: The MGH header data is already being read, but it is not returned to the user. Allow users to access it.

Included as maintainer in conda-forge recipe

Dear @dfsp-spirit , I recently submitted conda-forge recipes that make your open-source R package freesurferformats and pkgfilecache available on the conda-forge channel.

The goal is to let users to be able to run conda install r-freesurferformats to install your excellent R packages within the conda environment.

I have listed you as the maintainer of the corresponding recipes so that you will have write-access to modify and upgrade them in the future.

If you wish to be listed, please reply to the PR at conda-forge/staged-recipes#20776 (review) with "I wish to be included as the maintainer of r-freesurferformats and r-pkgfilecache" before Friday, Oct 28, 2022.

If you do not wish to be listed, please let me know, or simply ignore my request. I will remove you from the maintainer list. There will be no consequence and you can always request to be added in the future.

Thanks : )

Move `rmarkdown` to`Suggests` field

I wonder if rmarkdown can be in Suggests field instead of Imports in DESCRIPTION, since has_pandoc is the only place using rmarkdown. You can dynamically check if this package has been installed via

# old
has_pandoc <- function() {
    return(rmarkdown::pandoc_available());
}

# suggest
has_pandoc <- function() {
	if(system.file(package = "rmarkdown") == "") { return(FALSE) }
    return(rmarkdown::pandoc_available());
}

I can create a PR if you like this idea.

`read.fs.volume` silently returns NULL for files that do not satisfy any format checks

I am not sure if this is intended behavior, but the read.fs.volume command appears to silently return a NULL value and do nothing if the filepath argument is a file that is not true for any of the format conditionals. For example,

freesurferformats::read.fs.volume("fakefile")

This is somewhat unanticipated behavior, as virtually any other file-reading command in R will trigger an error if a file does not exist or satisfy some basic expectations for that command. If this is intended behavior, it should probably at least be documented in the help file or give a warning if, for example, no matching file extension is detected for format = "auto".

Wishlist: add mgh to nii conversion

Hi @dfsp-spirit , I wonder how hard it would be to write a mgh/mgz to nii converter? I tried to implement by myself, but I don't know where to get rest of the header information or if the conversion is generally applicable. Can I steal some knowledge from you?

Thanks!

m44_to_quaternion <- function(m) {
  m00 <- m[1, 1]
  m01 <- m[1, 2]
  m02 <- m[1, 3]
  m10 <- m[2, 1]
  m11 <- m[2, 2]
  m12 <- m[2, 3]
  m20 <- m[3, 1]
  m21 <- m[3, 2]
  m22 <- m[3, 3]

  tr <- m00 + m11 + m22 + 1.0

  # https://github.com/NIFTI-Imaging/nifti_clib/blob/d05a71ecdc0b811a509b77c3e82fd0674e901807/niftilib/nifti1_io.c
  if( tr > 0.5 ) {
    S <- sqrt( tr ) * 2.0
    qw <- 0.25 * S
    qx <- (m21 - m12) / S
    qy <- (m02 - m20) / S
    qz <- (m10 - m01) / S
  } else {
    Sx <- sqrt(1.0 + m00 - m11 - m22) * 2.0 # S = 4 * qx
    Sy <- sqrt(1.0 + m11 - m00 - m22) * 2.0 # S = 4 * qy
    Sz <- sqrt(1.0 + m22 - m00 - m11) * 2.0 # S = 4 * qz

    if( Sx > 2.0 ) {
      qw <- (m21 - m12) / Sx
      qx <- 0.25 * Sx
      qy <- (m01 + m10) / Sx
      qz <- (m02 + m20) / Sx
    } else if ( Sy > 2.0 ) {
      qw <- (m02 - m20) / Sy
      qx <- (m01 + m10) / Sy
      qy <- 0.25 * Sy
      qz <- (m12 + m21) / Sy
    } else {
      qw <- (m10 - m01) / Sz
      qx <- (m02 + m20) / Sz
      qy <- (m12 + m21) / Sz
      qz <- 0.25 * Sz
    }

    if( tr < 0.0 ) {
      qx <- -qx
      qy <- -qy
      qz <- -qz
    }

  }
  return(c(qw, qx, qy, qz))
}

mgh_to_nii <- function(from, to) {

  # from <- "/Users/dipterix/Dropbox (PennNeurosurgery)/RAVE/Samples/raw/PAV006/rave-imaging/fs/mri/aparc+aseg.mgz"
  # to <- tempfile(fileext = ".nii")

  mgh <- freesurferformats::read.fs.mgh(from, with_header = TRUE, drop_empty_dims = FALSE)

  # generate nii1 header
  header <- freesurferformats:::ni1header.for.data(mgh$data)
  header$dim <- c(3L, dim(mgh$data)[1:3], 1L, 1L, 1L, 1L)
  header$intent_p1 <- 0
  header$intent_p2 <- 0
  header$intent_p3 <- 0
  header$intent_code <- 0L
  # header$datatype <- 8L
  # header$bitpix <- 32L
  header$slice_start <- 0L
  header$pix_dim <- c(-1, 1, 1, 1, 0, 1, 1, 1)
  # header$vox_offset <- 352L
  # header$scl_slope
  # header$scl_inter
  # header$slice_end
  # header$slice_code
  header$xyzt_units <- 10L
  # header$cal_max
  # header$cal_min
  # header$slice_duration <- 0L
  # header$toffset
  # header$glmax
  header$qform_code <- 1L
  mat <- mgh$header$vox2ras_matrix
  # a = 0.5  * sqrt(1+R11+R22+R33)    (not stored)
  # b = 0.25 * (R32-R23) / a       => quatern_b
  # c = 0.25 * (R13-R31) / a       => quatern_c
  # d = 0.25 * (R21-R12) / a       => quatern_d

  # extract rotation
  rot <- mat
  rot[, 4] <- c(0,0,0,1)
  rot <- t(t(rot) / sqrt(colSums(rot^2)))
  if( det(rot) < 0 ) {
    # r13 = -r13 ; r23 = -r23 ; r33 = -r33 ;
    rot[,3] <- -rot[,3]
  }
  quatern <- m44_to_quaternion(rot)
  header$quatern_b <- quatern[2]
  header$quatern_c <- quatern[3]
  header$quatern_d <- quatern[4]

  header$sform_code <- 1L
  header$qoffset_x <- mat[1, 4]
  header$qoffset_y <- mat[2, 4]
  header$qoffset_z <- mat[3, 4]
  header$srow_x <- mat[1, ]
  header$srow_y <- mat[2, ]
  header$srow_z <- mat[3, ]

  freesurferformats::write.nifti1(
    filepath = to,
    niidata = mgh$data,
    niiheader = header
  )

}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.