From b2d29a27a7a807d5c5a6358d1335b4e1a118bd9d Mon Sep 17 00:00:00 2001 From: Christopher Lin Date: Mon, 17 Jan 2022 18:18:15 -0500 Subject: [PATCH 1/6] Add new_vol_like --- src/NIfTI.jl | 53 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/NIfTI.jl b/src/NIfTI.jl index 64277c4..be0192d 100644 --- a/src/NIfTI.jl +++ b/src/NIfTI.jl @@ -6,7 +6,7 @@ module NIfTI using CodecZlib, Mmap, MappedArrays, TranscodingStreams import Base.getindex, Base.size, Base.ndims, Base.length, Base.write, Base64 -export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine +export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine, new_vol_like include("parsers.jl") include("extensions.jl") @@ -107,6 +107,17 @@ function byteswap(hdr::NIfTI1Header) hdr end +""" + NIVolume{T<:Number,N,R} <: AbstractArray{T,N} +An `N`-dimensional NIfTI volume, with raw data of type +`R`. Note that if `raw <: Number`, it will be converted to `Float32`. Additionally, the header is automatically +updated to be consistent with the raw volume. + +# Members +- `header`: a `NIfTI1Header` +- `extensions`: a Vector of `NIfTIExtension`s +- `raw`: Raw data of type `R` from the volume +""" struct NIVolume{T<:Number,N,R} <: AbstractArray{T,N} header::NIfTI1Header extensions::Vector{NIfTIExtension} @@ -127,11 +138,21 @@ NIVolume(header::NIfTI1Header, raw::AbstractArray{Bool,N}) where {N} = include("coordinates.jl") -# Always in mm + +""" + voxel_size(header::NIfTI1Header) + +Get the voxel size **in mm** from a `NIfTI1Header`. +""" voxel_size(header::NIfTI1Header) = [header.pixdim[i] * SPATIAL_UNIT_MULTIPLIERS[header.xyzt_units & Int8(3)] for i = 2:min(header.dim[1], 3)+1] # Always in ms +""" + time_step(header::NIfTI1Header) + +Get the TR **in ms** from a `NIfTI1Header`. +""" time_step(header::NIfTI1Header) = header.pixdim[5] * TIME_UNIT_MULTIPLIERS[header.xyzt_units >> 3] @@ -250,6 +271,15 @@ function NIVolume( (orientation[2, :]...,), (orientation[3, :]...,), string_tuple(intent_name, 16), NP1_MAGIC), extensions, raw) end +""" + new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} + +Convenience function to create a new NIfTI volume with data `raw`, using +the header and extensions of `vol`. +""" +new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} = NIVolume(vol.header,vol.extensions,raw) + + # Validates the header of a volume and updates it to match the volume's contents function niupdate(vol::NIVolume{T}) where {T} vol.header.sizeof_hdr = SIZEOF_HDR1 @@ -285,7 +315,11 @@ function write(io::IO, vol::NIVolume) end end -# Convenience function to write a NIfTI file given a path +""" + niwrite(path::AbstractString, vol::NIVolume) + +Write a NIVolume to a file specified by `path`. +""" function niwrite(path::AbstractString, vol::NIVolume) if split(path,".")[end] == "gz" io = open(path, "w") @@ -323,6 +357,11 @@ function isgz(io::IO) end end +""" + niread(file; mmap=false, mode="r") + +Read a NIfTI file to a NIVolume. Set `mmap=true` to memory map the volume. +""" function niread(file::AbstractString; mmap::Bool=false, mode::AbstractString="r") io = niopen(file, mode) hdr, swapped = read_header(io) @@ -347,7 +386,15 @@ end add1(x::Union{AbstractArray{T},T}) where {T<:Integer} = x + 1 add1(::Colon) = Colon() + +""" + vox(f::NIVolume, args...,) + +Get the value of a voxel from volume `f`, scaled by slope and intercept given in header, with 0-based indexing. +The length of `args` should be the number of dimensions in `f`. +""" @inline vox(f::NIVolume, args...,) = getindex(f, map(add1, args)...,) + size(f::NIVolume) = size(f.raw) size(f::NIVolume, d) = size(f.raw, d) ndims(f::NIVolume) = ndims(f.raw) From db663c5787ebec0e8d0cb63f568963c329e80f26 Mon Sep 17 00:00:00 2001 From: Christopher Lin Date: Thu, 24 Mar 2022 09:47:54 -0400 Subject: [PATCH 2/6] Revert "fix merge conflicts" This reverts commit cdda2d5c12a2d2f6b942498126e06fc1489693cd, reversing changes made to 06d4ce372ffc1a3ce31953ecad8273afa6a6c868. --- src/NIfTI.jl | 108 +-------------------------------------------------- 1 file changed, 2 insertions(+), 106 deletions(-) diff --git a/src/NIfTI.jl b/src/NIfTI.jl index 15d1700..9e5a0e1 100644 --- a/src/NIfTI.jl +++ b/src/NIfTI.jl @@ -3,112 +3,17 @@ module NIfTI using CodecZlib, Mmap, MappedArrays, TranscodingStreams import Base.getindex, Base.size, Base.ndims, Base.length, Base.write, Base64 -export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine, new_vol_like +export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine include("parsers.jl") include("extensions.jl") include("volume.jl") include("headers.jl") -function define_packed(ty::DataType) - packed_offsets = cumsum([sizeof(x) for x in ty.types]) - sz = pop!(packed_offsets) - pushfirst!(packed_offsets, 0) - - @eval begin - function Base.read(io::IO, ::Type{$ty}) - bytes = read!(io, Array{UInt8}(undef, $sz...)) - hdr = $(Expr(:new, ty, [:(unsafe_load(convert(Ptr{$(ty.types[i])}, pointer(bytes)+$(packed_offsets[i])))) for i = 1:length(packed_offsets)]...,)) - if hdr.sizeof_hdr == ntoh(Int32(348)) - return byteswap(hdr), true - end - hdr, false - end - function Base.write(io::IO, x::$ty) - bytes = UInt8[] - for name in fieldnames($ty) - append!(bytes, reinterpret(UInt8, [getfield(x,name)])) - end - write(io, bytes) - $sz - end - end - nothing -end - -mutable struct NIfTI1Header - sizeof_hdr::Int32 - - data_type::NTuple{10,UInt8} - db_name::NTuple{18,UInt8} - extents::Int32 - session_error::Int16 - regular::Int8 - - dim_info::Int8 - dim::NTuple{8,Int16} - intent_p1::Float32 - intent_p2::Float32 - intent_p3::Float32 - intent_code::Int16 - datatype::Int16 - bitpix::Int16 - slice_start::Int16 - pixdim::NTuple{8,Float32} - vox_offset::Float32 - scl_slope::Float32 - scl_inter::Float32 - slice_end::Int16 - slice_code::Int8 - xyzt_units::Int8 - cal_max::Float32 - cal_min::Float32 - slice_duration::Float32 - toffset::Float32 - - glmax::Int32 - glmin::Int32 - - descrip::NTuple{80,UInt8} - aux_file::NTuple{24,UInt8} - - qform_code::Int16 - sform_code::Int16 - quatern_b::Float32 - quatern_c::Float32 - quatern_d::Float32 - qoffset_x::Float32 - qoffset_y::Float32 - qoffset_z::Float32 - - srow_x::NTuple{4,Float32} - srow_y::NTuple{4,Float32} - srow_z::NTuple{4,Float32} - - intent_name::NTuple{16,UInt8} - - magic::NTuple{4,UInt8} -end -define_packed(NIfTI1Header) - -# byteswapping - -function byteswap(hdr::NIfTI1Header) - for fn in fieldnames(typeof(hdr)) - val = getfield(hdr, fn) - if isa(val, Number) && sizeof(val) > 1 - setfield!(hdr, fn, ntoh(val)) - elseif isa(val, NTuple) && sizeof(eltype(val)) > 1 - setfield!(hdr, fn, map(ntoh, val)) - end - end - hdr -end - """ NIVolume{T<:Number,N,R} <: AbstractArray{T,N} An `N`-dimensional NIfTI volume, with raw data of type -`R`. Note that if `raw <: Number`, it will be converted to `Float32`. Additionally, the header is automatically +`R`. Note that if `R <: Number`, it will be converted to `Float32`. Additionally, the header is automatically updated to be consistent with the raw volume. # Members @@ -271,15 +176,6 @@ function NIVolume( (orientation[2, :]...,), (orientation[3, :]...,), string_tuple(intent_name, 16), NP1_MAGIC), extensions, raw) end -""" - new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} - -Convenience function to create a new NIfTI volume with data `raw`, using -the header and extensions of `vol`. -""" -new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} = NIVolume(vol.header,vol.extensions,raw) - - # Validates the header of a volume and updates it to match the volume's contents function niupdate(vol::NIVolume{T}) where {T} vol.header.sizeof_hdr = SIZEOF_HDR1 From 22e25ad4cd1559032f4866902b12f09b38df9cf9 Mon Sep 17 00:00:00 2001 From: Christopher Lin Date: Mon, 17 Jan 2022 18:18:15 -0500 Subject: [PATCH 3/6] further fixing --- src/NIfTI.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/NIfTI.jl b/src/NIfTI.jl index 9e5a0e1..4294287 100644 --- a/src/NIfTI.jl +++ b/src/NIfTI.jl @@ -3,7 +3,7 @@ module NIfTI using CodecZlib, Mmap, MappedArrays, TranscodingStreams import Base.getindex, Base.size, Base.ndims, Base.length, Base.write, Base64 -export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine +export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine, new_vol_like include("parsers.jl") include("extensions.jl") @@ -176,6 +176,15 @@ function NIVolume( (orientation[2, :]...,), (orientation[3, :]...,), string_tuple(intent_name, 16), NP1_MAGIC), extensions, raw) end +""" + new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} + +Convenience function to create a new NIfTI volume with data `raw`, using +the header and extensions of `vol`. +""" +new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} = NIVolume(vol.header,vol.extensions,raw) + + # Validates the header of a volume and updates it to match the volume's contents function niupdate(vol::NIVolume{T}) where {T} vol.header.sizeof_hdr = SIZEOF_HDR1 From d79477fb759dd06690f2cd748b6f3df022a91a67 Mon Sep 17 00:00:00 2001 From: Christopher Lin Date: Mon, 17 Jan 2022 18:18:15 -0500 Subject: [PATCH 4/6] Merge master changes Revert "fix merge conflicts" This reverts commit cdda2d5c12a2d2f6b942498126e06fc1489693cd, reversing changes made to 06d4ce372ffc1a3ce31953ecad8273afa6a6c868. further fixing --- src/NIfTI.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/NIfTI.jl b/src/NIfTI.jl index 9e5a0e1..4294287 100644 --- a/src/NIfTI.jl +++ b/src/NIfTI.jl @@ -3,7 +3,7 @@ module NIfTI using CodecZlib, Mmap, MappedArrays, TranscodingStreams import Base.getindex, Base.size, Base.ndims, Base.length, Base.write, Base64 -export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine +export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine, new_vol_like include("parsers.jl") include("extensions.jl") @@ -176,6 +176,15 @@ function NIVolume( (orientation[2, :]...,), (orientation[3, :]...,), string_tuple(intent_name, 16), NP1_MAGIC), extensions, raw) end +""" + new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} + +Convenience function to create a new NIfTI volume with data `raw`, using +the header and extensions of `vol`. +""" +new_vol_like(vol::NIVolume{T}, raw::R) where {R,T} = NIVolume(vol.header,vol.extensions,raw) + + # Validates the header of a volume and updates it to match the volume's contents function niupdate(vol::NIVolume{T}) where {T} vol.header.sizeof_hdr = SIZEOF_HDR1 From 4fda850cf911bdee714bf857ab7311db09f8b668 Mon Sep 17 00:00:00 2001 From: Christopher Lin Date: Thu, 24 Mar 2022 11:10:34 -0400 Subject: [PATCH 5/6] add test for new_vol_like --- test/runtests.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 29c85c9..a1a72f5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -93,6 +93,13 @@ const TEMP_GZIPPED_FILE = joinpath(TEMP_DIR_NAME, "$(tempname()).nii.gz") niwrite(TEMP_GZIPPED_FILE, vol) niread(TEMP_GZIPPED_FILE) +# Test new_img_like +img = niread(GZIPPED_NII) +test_raw = ones(size(img)) +new_img = new_vol_like(img, test_raw) +@test new_img.header == img.header +@test new_img.raw == test_raw + # Write and read DT_BINARY const BOOL_WRITE = joinpath(TEMP_DIR_NAME, "$(tempname()).nii") const BIT_WRITE = joinpath(TEMP_DIR_NAME, "$(tempname()).nii") From 3a58f459cfe03acd318a4991824b1755811a4aa9 Mon Sep 17 00:00:00 2001 From: Christopher Lin Date: Thu, 24 Mar 2022 11:13:46 -0400 Subject: [PATCH 6/6] add test for extensions --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index a1a72f5..a072738 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -99,6 +99,7 @@ test_raw = ones(size(img)) new_img = new_vol_like(img, test_raw) @test new_img.header == img.header @test new_img.raw == test_raw +@test new_img.extensions == img.extensions # Write and read DT_BINARY const BOOL_WRITE = joinpath(TEMP_DIR_NAME, "$(tempname()).nii")