From 830953d233b15803777ab04bb498eb45005609ee Mon Sep 17 00:00:00 2001 From: Kaidong Chai Date: Wed, 9 Oct 2024 14:51:38 -0400 Subject: [PATCH 1/5] Add support for NIfTI2 header --- src/NIfTI.jl | 104 ++++++++++++++++++++++++--------------------- src/coordinates.jl | 16 +++---- src/headers.jl | 96 ++++++++++++++++++++++++++++++----------- 3 files changed, 135 insertions(+), 81 deletions(-) diff --git a/src/NIfTI.jl b/src/NIfTI.jl index 1427c1f..b553a12 100644 --- a/src/NIfTI.jl +++ b/src/NIfTI.jl @@ -17,26 +17,26 @@ An `N`-dimensional NIfTI volume, with raw data of type updated to be consistent with the raw volume. # Members -- `header`: a `NIfTI1Header` +- `header`: a `NIfTIHeader` - `extensions`: a Vector of `NIfTIExtension`s - `raw`: Raw data of type `R` """ struct NIVolume{T<:Number,N,R} <: AbstractArray{T,N} - header::NIfTI1Header + header::NIfTIHeader extensions::Vector{NIfTIExtension} raw::R end -NIVolume(header::NIfTI1Header, extensions::Vector{NIfTIExtension}, raw::R) where {R}= +NIVolume(header::NIfTIHeader, extensions::Vector{NIfTIExtension}, raw::R) where {R} = niupdate(new(header, extensions, raw)) -NIVolume(header::NIfTI1Header, extensions::Vector{NIfTIExtension}, raw::AbstractArray{T,N}) where {T<:Number,N} = - NIVolume{typeof(one(T)*1f0+1f0),N,typeof(raw)}(header, extensions, raw) -NIVolume(header::NIfTI1Header, raw::AbstractArray{T,N}) where {T<:Number,N} = - NIVolume{typeof(one(T)*1f0+1f0),N,typeof(raw)}(header, NIfTIExtension[], raw) +NIVolume(header::NIfTIHeader, extensions::Vector{NIfTIExtension}, raw::AbstractArray{T,N}) where {T<:Number,N} = + NIVolume{typeof(one(T) * 1.0f0 + 1.0f0),N,typeof(raw)}(header, extensions, raw) +NIVolume(header::NIfTIHeader, raw::AbstractArray{T,N}) where {T<:Number,N} = + NIVolume{typeof(one(T) * 1.0f0 + 1.0f0),N,typeof(raw)}(header, NIfTIExtension[], raw) -NIVolume(header::NIfTI1Header, extensions::Vector{NIfTIExtension}, raw::AbstractArray{Bool,N}) where {N} = +NIVolume(header::NIfTIHeader, extensions::Vector{NIfTIExtension}, raw::AbstractArray{Bool,N}) where {N} = NIVolume{Bool,N,typeof(raw)}(header, extensions, raw) -NIVolume(header::NIfTI1Header, raw::AbstractArray{Bool,N}) where {N} = +NIVolume(header::NIfTIHeader, raw::AbstractArray{Bool,N}) where {N} = NIVolume{Bool,N,typeof(raw)}(header, NIfTIExtension[], raw) header(x::NIVolume) = getfield(x, :header) @@ -45,15 +45,15 @@ include("coordinates.jl") # Always in ms """ - time_step(header::NIfTI1Header) + time_step(header::NIfTIHeader) -Get the TR **in ms** from a `NIfTI1Header`. +Get the TR **in ms** from a `NIfTIHeader`. """ -time_step(header::NIfTI1Header) = - header.pixdim[5] * TIME_UNIT_MULTIPLIERS[header.xyzt_units >> 3] +time_step(header::NIfTIHeader) = + header.pixdim[5] * TIME_UNIT_MULTIPLIERS[header.xyzt_units>>3] # Gets the size of a type in bits -nibitpix(t::Type) = Int16(sizeof(t)*8) +nibitpix(t::Type) = Int16(sizeof(t) * 8) nibitpix(::Type{Bool}) = Int16(1) # Constructor @@ -68,25 +68,25 @@ function NIVolume( glmin::Integer=Int16(0), # The frequency encoding, phase encoding and slice dimensions. - dim_info::NTuple{3, Integer}=(0, 0, 0), + dim_info::NTuple{3,Integer}=(0, 0, 0), # Describes data contained in the file; for valid values, see # http://nifti.nimh.nih.gov/nifti-1/documentation/nifti1fields/nifti1fields_pages/group__NIfTI1__INTENT__CODES.html - intent_p1::Real=0f0, intent_p2::Real=0f0, intent_p3::Real=0f0, + intent_p1::Real=0.0f0, intent_p2::Real=0.0f0, intent_p3::Real=0.0f0, intent_code::Integer=Int16(0), intent_name::AbstractString="", # Information about which slices were acquired, in case the volume has been padded slice_start::Integer=Int16(0), slice_end::Integer=Int16(0), slice_code=UInt8(0), # The size of each voxel and the time step. These are formulated in mm unless xyzt_units is # explicitly specified - voxel_size::NTuple{3, Real}=(1f0, 1f0, 1f0), time_step::Real=0f0, xyzt_units=UInt8(18), + voxel_size::NTuple{3,Real}=(1.0f0, 1.0f0, 1.0f0), time_step::Real=0.0f0, xyzt_units=UInt8(18), # Slope and intercept by which volume shoudl be scaled - scl_slope::Real=1f0, scl_inter::Real=0f0, + scl_slope::Real=1.0f0, scl_inter::Real=0.0f0, # These describe how data should be scaled when displayed on the screen. They are probably # rarely used - cal_max::Real=0f0, cal_min::Real=0f0, + cal_max::Real=0.0f0, cal_min::Real=0.0f0, # The amount of time taken to acquire a slice - slice_duration::Real=0f0, + slice_duration::Real=0.0f0, # Indicates a non-zero start point for time axis - toffset::Real=0f0, + toffset::Real=0.0f0, # "Any text you like" descrip::AbstractString="", @@ -94,10 +94,10 @@ function NIVolume( aux_file::AbstractString="", # Parameters for Method 2. See the NIfTI spec - qfac::Float32=0f0, quatern_b::Real=0f0, quatern_c::Real=0f0, quatern_d::Real=0f0, - qoffset_x::Real=0f0, qoffset_y::Real=0f0, qoffset_z::Real=0f0, + qfac::Float32=0.0f0, quatern_b::Real=0.0f0, quatern_c::Real=0.0f0, quatern_d::Real=0.0f0, + qoffset_x::Real=0.0f0, qoffset_y::Real=0.0f0, qoffset_z::Real=0.0f0, # Orientation matrix for Method 3 - orientation::Union{Matrix{Float32},Nothing}=nothing) where {T <: Number} + orientation::Union{Matrix{Float32},Nothing}=nothing) where {T<:Number} local t if isempty(raw) raw = Int16[] @@ -111,7 +111,7 @@ function NIVolume( end method2 = qfac != 0 || quatern_b != 0 || quatern_c != 0 || quatern_d != 0 || qoffset_x != 0 || - qoffset_y != 0 || qoffset_z != 0 + qoffset_y != 0 || qoffset_z != 0 method3 = orientation != nothing if method2 && method3 @@ -132,19 +132,19 @@ function NIVolume( end NIVolume(NIfTI1Header(SIZEOF_HDR1, string_tuple(data_type, 10), string_tuple(db_name, 18), extents, session_error, - regular, to_dim_info(dim_info), to_dim_i16(size(raw)), intent_p1, intent_p2, - intent_p3, intent_code, eltype_to_int16(t), nibitpix(t), - slice_start, (qfac, voxel_size..., time_step, 0, 0, 0), 352, - scl_slope, - scl_inter, - slice_end, - UInt8(slice_code), - UInt8(xyzt_units), - cal_max, cal_min, slice_duration, - toffset, glmax, glmin, string_tuple(descrip, 80), string_tuple(aux_file, 24), (method2 || method3), - method3, quatern_b, quatern_c, quatern_d, - qoffset_x, qoffset_y, qoffset_z, (orientation[1, :]...,), - (orientation[2, :]...,), (orientation[3, :]...,), string_tuple(intent_name, 16), NP1_MAGIC), extensions, raw) + regular, to_dim_info(dim_info), to_dim_i16(size(raw)), intent_p1, intent_p2, + intent_p3, intent_code, eltype_to_int16(t), nibitpix(t), + slice_start, (qfac, voxel_size..., time_step, 0, 0, 0), 352, + scl_slope, + scl_inter, + slice_end, + UInt8(slice_code), + UInt8(xyzt_units), + cal_max, cal_min, slice_duration, + toffset, glmax, glmin, string_tuple(descrip, 80), string_tuple(aux_file, 24), (method2 || method3), + method3, quatern_b, quatern_c, quatern_d, + qoffset_x, qoffset_y, qoffset_z, (orientation[1, :]...,), + (orientation[2, :]...,), (orientation[3, :]...,), string_tuple(intent_name, 16), NP1_MAGIC), extensions, raw) end # Validates the header of a volume and updates it to match the volume's contents @@ -154,7 +154,7 @@ function niupdate(vol::NIVolume{T,N,R}) where {T,N,R} vol.header.datatype = eltype_to_int16(eltype(R)) vol.header.bitpix = nibitpix(T) vol.header.vox_offset = isempty(vol.extensions) ? Int32(352) : - Int32(mapreduce(esize, +, vol.extensions) + SIZEOF_HDR1) + Int32(mapreduce(esize, +, vol.extensions) + SIZEOF_HDR1) vol end @@ -188,7 +188,7 @@ end Write a NIVolume to a file specified by `path`. """ function niwrite(path::AbstractString, vol::NIVolume) - if split(path,".")[end] == "gz" + if split(path, ".")[end] == "gz" io = open(path, "w") stream = GzipCompressorStream(io) write(stream, vol) @@ -203,11 +203,19 @@ end # Read header from a NIfTI file function read_header(io::IO) - header, swapped = read(io, NIfTI1Header) - if header.sizeof_hdr != SIZEOF_HDR1 - error("This is not a NIfTI-1 file") + header_size = peek(io, Int32) + if header_size == SIZEOF_HDR1 || bswap(header_size) == SIZEOF_HDR1 + header, swapped = read(io, NIfTI1Header) + #error("This is not a NIfTI-1 file") + return header, swapped end - header, swapped + + if header_size == SIZEOF_HDR2 || bswap(header_size) == SIZEOF_HDR2 + header, swapped = read(io, NIfTI2Header) + #error("This is not a NIfTI-1 file") + return header, swapped + end + error("This is not a NIfTI file") end # Look for a gzip header in an IOStream @@ -221,7 +229,7 @@ function isgz(io::IO) @debug "reading the file resulted in an EOF error and \nthe end of the file was read.\nNo more data was available to read from the filestream.\nIt is likely that the file was corrupted or is empty (0 bytes)." rethrow(err) end - end + end end """ @@ -234,7 +242,7 @@ function niread(file::AbstractString; mmap::Bool=false, mode::AbstractString="r" hdr, swapped = read_header(io) ex = read_extensions(io, hdr.vox_offset - 352) - if hdr.magic === NP1_MAGIC + if hdr.magic === NP1_MAGIC || hdr.magic == NP2_MAGIC vol = read_volume(io, to_eltype(hdr.datatype), to_dimensions(hdr.dim), mmap) else vol = read_volume(niopen(hdr_to_img(file), mode), to_eltype(hdr.datatype), to_dimensions(hdr.dim), mmap) @@ -250,8 +258,8 @@ end # Allow file to be indexed like an array, but with indices yielding scaled data @inline getindex(f::NIVolume{T}, idx::Vararg{Int}) where {T} = f.header.scl_slope == zero(typeof(f.header.scl_slope)) ? - getindex(f.raw, idx...,) : - getindex(f.raw, idx...,) * f.header.scl_slope + f.header.scl_inter + getindex(f.raw, idx...,) : + getindex(f.raw, idx...,) * f.header.scl_slope + f.header.scl_inter add1(x::Union{AbstractArray{T},T}) where {T<:Integer} = x + 1 add1(::Colon) = Colon() diff --git a/src/coordinates.jl b/src/coordinates.jl index e9b0e21..39a1cbc 100644 --- a/src/coordinates.jl +++ b/src/coordinates.jl @@ -19,7 +19,7 @@ end end get_sform(x::NIVolume) = get_sform(x.header) -function get_sform(hdr::NIfTI1Header) +function get_sform(hdr::NIfTIHeader) if hdr.sform_code > 0 return @inbounds Float32[ hdr.srow_x[1] hdr.srow_x[2] hdr.srow_x[3] hdr.srow_x[4] @@ -34,7 +34,7 @@ function get_sform(hdr::NIfTI1Header) end get_qform(x::NIVolume) = get_qform(x.header) -function get_qform(hdr::NIfTI1Header) +function get_qform(hdr::NIfTIHeader) if hdr.qform_code <= 0 return @inbounds Float32[ hdr.pixdim[2] 0 0 0 @@ -84,11 +84,11 @@ getaffine(x::NIVolume) = getaffine(x.header) # Convert a NIfTI header to a 4x4 affine transformation matrix """ - getaffine(hdr::NIfTI1Header) + getaffine(hdr::NIfTIHeader) Gets a 4x4 affine transformation matrix from a header's sform """ -function getaffine(hdr::NIfTI1Header) +function getaffine(hdr::NIfTIHeader) if hdr.sform_code > 0 return get_sform(hdr) else @@ -97,11 +97,11 @@ function getaffine(hdr::NIfTI1Header) end """ - function setaffine(h::NIfTI1Header, affine::Array{T,2}) where {T} + function setaffine(h::NIfTIHeader, affine::Array{T,2}) where {T} -Set the affine of a `NIfTI1Header` to 4x4 affine matrix `affine` +Set the affine of a `NIfTIHeader` to 4x4 affine matrix `affine` """ -function setaffine(h::NIfTI1Header, affine::Array{T,2}) where {T} +function setaffine(h::NIfTIHeader, affine::Array{T,2}) where {T} size(affine, 1) == size(affine, 2) == 4 || error("affine matrix must be 4x4") affine[4, 1] == affine[4, 2] == affine[4, 3] == 0 && affine[4, 4] == 1 || @@ -127,7 +127,7 @@ end Returns a tuple providing the orientation of a NIfTI image. """ orientation(x) = orientation(x.header) -function orientation(hdr::NIfTI1Header) +function orientation(hdr::NIfTIHeader) if hdr.sform_code > 0 return @inbounds _dir2ori( hdr.srow_x[1], hdr.srow_x[2], hdr.srow_x[3], diff --git a/src/headers.jl b/src/headers.jl index 48c39ae..c3ef27f 100644 --- a/src/headers.jl +++ b/src/headers.jl @@ -7,7 +7,7 @@ function define_packed(ty::DataType) @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)]...,)) + 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 @@ -16,7 +16,7 @@ function define_packed(ty::DataType) function Base.write(io::IO, x::$ty) bytes = UInt8[] for name in fieldnames($ty) - append!(bytes, reinterpret(UInt8, [getfield(x,name)])) + append!(bytes, reinterpret(UInt8, [getfield(x, name)])) end write(io, bytes) $sz @@ -25,11 +25,13 @@ function define_packed(ty::DataType) nothing end -mutable struct NIfTI1Header +abstract type NIfTIHeader end + +mutable struct NIfTI1Header <: NIfTIHeader sizeof_hdr::Int32 - data_type::NTuple{10, UInt8} - db_name::NTuple{18, UInt8} + data_type::NTuple{10,UInt8} + db_name::NTuple{18,UInt8} extents::Int32 session_error::Int16 regular::Int8 @@ -58,8 +60,8 @@ mutable struct NIfTI1Header glmax::Int32 glmin::Int32 - descrip::NTuple{80, UInt8} - aux_file::NTuple{24, UInt8} + descrip::NTuple{80,UInt8} + aux_file::NTuple{24,UInt8} qform_code::Int16 sform_code::Int16 @@ -70,19 +72,63 @@ mutable struct NIfTI1Header qoffset_y::Float32 qoffset_z::Float32 - srow_x::NTuple{4, Float32} - srow_y::NTuple{4, Float32} - srow_z::NTuple{4, Float32} + srow_x::NTuple{4,Float32} + srow_y::NTuple{4,Float32} + srow_z::NTuple{4,Float32} - intent_name::NTuple{16, UInt8} + intent_name::NTuple{16,UInt8} - magic::NTuple{4, UInt8} + magic::NTuple{4,UInt8} end define_packed(NIfTI1Header) +mutable struct NIfTI2Header <: NIfTIHeader + sizeof_hdr::Int32 + magic::NTuple{8,UInt8} + datatype::Int16 + bitpix::Int16 + dim::NTuple{8,Int64} + intent_p::NTuple{3,Float64} + pixdim::NTuple{8,Float64} + vox_offset::Int64 + scl_slope::Float64 + scl_inter::Float64 + cal_max::Float64 + cal_min::Float64 + slice_duration::Float64 + toffset::Float64 + slice_start::Int64 + slice_end::Int64 + + descrip::NTuple{80,UInt8} + aux_file::NTuple{24,UInt8} + + qform_code::Int32 + sform_code::Int32 + quatern_b::Float32 + quatern_c::Float32 + quatern_d::Float32 + qoffset_x::Float32 + qoffset_y::Float32 + qoffset_z::Float32 + + srow_x::NTuple{4,Float64} + srow_y::NTuple{4,Float64} + srow_z::NTuple{4,Float64} + + slice_code::Int32 + xyzt_units::Int32 + intent_code::Int32 + intent_name::NTuple{16,UInt8} + dim_info::UInt8 + unused_str::NTuple{15,UInt8} +end +define_packed(NIfTI2Header) + + # byteswapping -function byteswap(hdr::NIfTI1Header) +function byteswap(hdr::NIfTIHeader) for fn in fieldnames(typeof(hdr)) val = getfield(hdr, fn) if isa(val, Number) && sizeof(val) > 1 @@ -99,7 +145,7 @@ end Which slice corresponds to the first slice acquired during MRI acquisition (i.e. not padded slices). """ -slice_start(x::NIfTI1Header) = Int(getfield(x, :slice_start)) + 1 +slice_start(x::NIfTIHeader) = Int(getfield(x, :slice_start)) + 1 slice_start(x) = slice_start(header(x)) """ @@ -107,7 +153,7 @@ slice_start(x) = slice_start(header(x)) Which slice corresponds to the last slice acquired during MRI acquisition (i.e. not padded slices). """ -slice_end(x::NIfTI1Header) = Int(getfield(x, :slice_end)) + 1 +slice_end(x::NIfTIHeader) = Int(getfield(x, :slice_end)) + 1 slice_end(x) = slice_end(header(x)) """ @@ -115,7 +161,7 @@ slice_end(x) = slice_end(header(x)) Time to acquire one slice. """ -slice_duration(x::NIfTI1Header) = getfield(x, :slice_duration) +slice_duration(x::NIfTIHeader) = getfield(x, :slice_duration) slice_duration(x) = slice_duration(header(x)) """ @@ -123,7 +169,7 @@ slice_duration(x) = slice_duration(header(x)) Return the number of spatial dimensions in the image. """ -sdims(x::NIfTI1Header) = min(Int(getfield(getfield(x, :dim), 1)), 3) +sdims(x::NIfTIHeader) = min(Int(getfield(getfield(x, :dim), 1)), 3) sdims(x) = sdims(header(x)) # Conversion factors to mm/ms @@ -143,11 +189,11 @@ function to_spatial_multiplier(xyzt_units::UInt8) end """ - NIfTI.voxel_size(header::NIfTI1Header) + NIfTI.voxel_size(header::NIfTIHeader) -Get the voxel size **in mm** from a `NIfTI1Header`. +Get the voxel size **in mm** from a `NIfTIHeader`. """ -function voxel_size(x::NIfTI1Header) +function voxel_size(x::NIfTIHeader) sd = sdims(x) if sd === 0 return () @@ -183,10 +229,10 @@ function to_dim_info(dim_info::Tuple{Integer,Integer,Integer}) end # Returns or sets dim_info as a tuple whose values are the frequency, phase, and slice dimensions -function dim_info(hdr::NIfTI1Header) +function dim_info(hdr::NIfTIHeader) return (hdr.dim_info & 0x03, (hdr.dim_info >> 0x02) & 0x03, (hdr.dim_info >> 0x04) & 0x03) end -function dim_info(header::NIfTI1Header, dim_info::Tuple{T, T, T}) where {T<:Integer} +function dim_info(header::NIfTIHeader, dim_info::Tuple{T,T,T}) where {T<:Integer} header.dim_info = to_dim_info(dim_info) end @@ -196,7 +242,7 @@ end Returns the frequency dimension associated with with `img`. `img` is an image or a collection of image related metadata. """ -freqdim(x::NIfTI1Header) = Int(getfield(x, :dim_info) & 0x03) +freqdim(x::NIfTIHeader) = Int(getfield(x, :dim_info) & 0x03) freqdim(x) = freqdim(header(x)) """ @@ -205,7 +251,7 @@ freqdim(x) = freqdim(header(x)) Returns the phase dimension associated with `img`. `img` is an image or a collection of image related metadata. """ -phasedim(x::NIfTI1Header) = Int((getfield(x, :dim_info) >> 0x02) & 0x03) +phasedim(x::NIfTIHeader) = Int((getfield(x, :dim_info) >> 0x02) & 0x03) phasedim(x) = phasedim(header(x)) """ @@ -214,6 +260,6 @@ phasedim(x) = phasedim(header(x)) Returns the slice dimension associated with `img`. `img` is an image or a collection of image related metadata. """ -slicedim(x::NIfTI1Header) = Int((getfield(x, :dim_info) >> 0x04) & 0x03) +slicedim(x::NIfTIHeader) = Int((getfield(x, :dim_info) >> 0x04) & 0x03) slicedim(x) = slicedim(header(x)) From e43f96d29634aaa645b29f31807eff909f78b136 Mon Sep 17 00:00:00 2001 From: Kaidong Chai Date: Wed, 23 Oct 2024 15:44:58 -0400 Subject: [PATCH 2/5] Fix hardcoded values --- src/NIfTI.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/NIfTI.jl b/src/NIfTI.jl index b553a12..d35de42 100644 --- a/src/NIfTI.jl +++ b/src/NIfTI.jl @@ -149,12 +149,11 @@ end # Validates the header of a volume and updates it to match the volume's contents function niupdate(vol::NIVolume{T,N,R}) where {T,N,R} - vol.header.sizeof_hdr = SIZEOF_HDR1 vol.header.dim = to_dim_i16(size(vol.raw)) vol.header.datatype = eltype_to_int16(eltype(R)) vol.header.bitpix = nibitpix(T) - vol.header.vox_offset = isempty(vol.extensions) ? Int32(352) : - Int32(mapreduce(esize, +, vol.extensions) + SIZEOF_HDR1) + vol.header.vox_offset = isempty(vol.extensions) ? vol.header.sizeof_hdr : + Int32(mapreduce(esize, +, vol.extensions) + vol.header.sizeof_hdr) vol end @@ -240,7 +239,7 @@ 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) - ex = read_extensions(io, hdr.vox_offset - 352) + ex = read_extensions(io, hdr.vox_offset - hdr.sizeof_hdr) if hdr.magic === NP1_MAGIC || hdr.magic == NP2_MAGIC vol = read_volume(io, to_eltype(hdr.datatype), to_dimensions(hdr.dim), mmap) From a68be020195d36aeb394b1425ccade4e02054339 Mon Sep 17 00:00:00 2001 From: Kaidong Chai Date: Thu, 24 Oct 2024 15:26:38 -0400 Subject: [PATCH 3/5] Add test data for NIfTI2 --- src/NIfTI.jl | 6 ++--- test/data/example_nifti2.nii.gz | Bin 0 -> 21160 bytes test/runtests.jl | 43 ++++++++++++++++---------------- 3 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 test/data/example_nifti2.nii.gz diff --git a/src/NIfTI.jl b/src/NIfTI.jl index 48ad818..554e342 100644 --- a/src/NIfTI.jl +++ b/src/NIfTI.jl @@ -111,7 +111,7 @@ function NIVolume( end method2 = qfac != 0 || quatern_b != 0 || quatern_c != 0 || quatern_d != 0 || qoffset_x != 0 || - qoffset_y != 0 || qoffset_z != 0 + qoffset_y != 0 || qoffset_z != 0 method3 = orientation !== nothing if method2 && method3 @@ -152,7 +152,7 @@ function niupdate(vol::NIVolume{T,N,R}) where {T,N,R} vol.header.dim = to_dim_i16(size(vol.raw)) vol.header.datatype = eltype_to_int16(eltype(R)) vol.header.bitpix = nibitpix(T) - vol.header.vox_offset = isempty(vol.extensions) ? vol.header.sizeof_hdr : + vol.header.vox_offset = isempty(vol.extensions) ? vol.header.sizeof_hdr + 4 : Int32(mapreduce(esize, +, vol.extensions) + vol.header.sizeof_hdr) vol end @@ -239,7 +239,7 @@ 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) - ex = read_extensions(io, hdr.vox_offset - hdr.sizeof_hdr) + ex = read_extensions(io, hdr.vox_offset - hdr.sizeof_hdr - 4) if hdr.magic === NP1_MAGIC || hdr.magic == NP2_MAGIC vol = read_volume(io, to_eltype(hdr.datatype), to_dimensions(hdr.dim), mmap) diff --git a/test/data/example_nifti2.nii.gz b/test/data/example_nifti2.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..a0d9e408f4ebb41268c17a05327e4a398f069ba4 GIT binary patch literal 21160 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 LOquGGV0r)mXjo1C literal 0 HcmV?d00001 diff --git a/test/runtests.jl b/test/runtests.jl index 1396f1b..e10da38 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -52,17 +52,17 @@ function image_tests(fname, mmap) @assert maximum(img) == maximum(img.raw) @test getaffine(img) ≈ [ - -2.0 6.714715653593746e-19 9.081024511081715e-18 117.8551025390625 - 6.714715653593746e-19 1.9737114906311035 -0.35552823543548584 -35.72294235229492 - 8.25548088896093e-18 0.3232076168060303 2.171081781387329 -7.248798370361328 - 0.0 0.0 0.0 1.0 + -2.0 6.714715653593746e-19 9.081024511081715e-18 117.8551025390625 + 6.714715653593746e-19 1.9737114906311035 -0.35552823543548584 -35.72294235229492 + 8.25548088896093e-18 0.3232076168060303 2.171081781387329 -7.248798370361328 + 0.0 0.0 0.0 1.0 ] @test NIfTI.get_qform(img) ≈ [ - -2.0 7.75482f-26 -6.93824f-27 117.855 - 7.75482f-26 1.97371 -0.355528 -35.7229 - 6.30749f-27 0.323208 2.17108 -7.2488 - 0.0 0.0 0.0 1.0 + -2.0 7.75482f-26 -6.93824f-27 117.855 + 7.75482f-26 1.97371 -0.355528 -35.7229 + 6.30749f-27 0.323208 2.17108 -7.2488 + 0.0 0.0 0.0 1.0 ] @test NIfTI.orientation(img) == (:right, :posterior, :inferior) end @@ -118,30 +118,31 @@ const WRITE = joinpath(TEMP_DIR_NAME, "$(tempname()).nii") const VERIFY_WRITE = joinpath(TEMP_DIR_NAME, "$(tempname()).nii") cp(NII, WRITE) img = niread(WRITE; mmap=true, mode="r+") -img.raw[1,1,1,1] = 5 -img.raw[:,2,1,1] = ones(size(img)[1]) +img.raw[1, 1, 1, 1] = 5 +img.raw[:, 2, 1, 1] = ones(size(img)[1]) cp(WRITE, VERIFY_WRITE) -@test niread(VERIFY_WRITE)[1,1,1,1] == 5 -@test niread(VERIFY_WRITE)[:,2,1,1] == ones(size(img)[1]) +@test niread(VERIFY_WRITE)[1, 1, 1, 1] == 5 +@test niread(VERIFY_WRITE)[:, 2, 1, 1] == ones(size(img)[1]) # Site is currently down TODO: reintroduce this test when site is up # Big endian # const BE = "$(tempname()).nii" # download("https://nifti.nimh.nih.gov/nifti-1/data/avg152T1_LR_nifti.nii.gz", BE) img = niread(joinpath(dirname(@__FILE__), "data/avg152T1_LR_nifti.nii.gz")) -@test size(img) == (91,109,91) +@test size(img) == (91, 109, 91) GC.gc() # closes mmapped files @test NIfTI._dir2ori(-1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0) == (:right, :posterior, :inferior) + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0) == (:right, :posterior, :inferior) -@test NIfTI._dir2ori(1.0, 0.0, 0.0, - 0.0, -1.0, 0.0, - 0.0, 0.0, 1.0) == (:left, :anterior, :inferior) +@test NIfTI._dir2ori(1.0, 0.0, 0.0, + 0.0, -1.0, 0.0, + 0.0, 0.0, 1.0) == (:left, :anterior, :inferior) -@test NIfTI._dir2ori(1.0, 0.0, 0.0, - 0.0, -1.0, 0.0, - 0.0, 0.0, -1.0) == (:left, :anterior, :superior) +@test NIfTI._dir2ori(1.0, 0.0, 0.0, + 0.0, -1.0, 0.0, + 0.0, 0.0, -1.0) == (:left, :anterior, :superior) + From 0c6470057e4f694ae7b366362d59fa72a5a6e202 Mon Sep 17 00:00:00 2001 From: Kaidong Chai Date: Mon, 28 Oct 2024 14:00:50 -0400 Subject: [PATCH 4/5] Fix Nifti-2 header definition --- src/headers.jl | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/headers.jl b/src/headers.jl index c3ef27f..ddfc995 100644 --- a/src/headers.jl +++ b/src/headers.jl @@ -88,7 +88,9 @@ mutable struct NIfTI2Header <: NIfTIHeader datatype::Int16 bitpix::Int16 dim::NTuple{8,Int64} - intent_p::NTuple{3,Float64} + intent_p1::Float64 + intent_p2::Float64 + intent_p3::Float64 pixdim::NTuple{8,Float64} vox_offset::Int64 scl_slope::Float64 @@ -105,12 +107,12 @@ mutable struct NIfTI2Header <: NIfTIHeader qform_code::Int32 sform_code::Int32 - quatern_b::Float32 - quatern_c::Float32 - quatern_d::Float32 - qoffset_x::Float32 - qoffset_y::Float32 - qoffset_z::Float32 + quatern_b::Float64 + quatern_c::Float64 + quatern_d::Float64 + qoffset_x::Float64 + qoffset_y::Float64 + qoffset_z::Float64 srow_x::NTuple{4,Float64} srow_y::NTuple{4,Float64} From a1a2986dc7a30695c001ac50e4714f655619ecdf Mon Sep 17 00:00:00 2001 From: Zachary P Christensen Date: Fri, 27 Dec 2024 07:55:43 -0500 Subject: [PATCH 5/5] Update headers.jl Add some minimal documentation --- src/headers.jl | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/headers.jl b/src/headers.jl index ddfc995..cb2db5b 100644 --- a/src/headers.jl +++ b/src/headers.jl @@ -25,9 +25,12 @@ function define_packed(ty::DataType) nothing end -abstract type NIfTIHeader end +""" + NIfTI1Header -mutable struct NIfTI1Header <: NIfTIHeader +NIfTI header for the version 1 specification. +""" +mutable struct NIfTI1Header sizeof_hdr::Int32 data_type::NTuple{10,UInt8} @@ -82,7 +85,12 @@ mutable struct NIfTI1Header <: NIfTIHeader end define_packed(NIfTI1Header) -mutable struct NIfTI2Header <: NIfTIHeader +""" + NIfTI2Header + +NIfTI header for the version 2 specification. +""" +mutable struct NIfTI2Header sizeof_hdr::Int32 magic::NTuple{8,UInt8} datatype::Int16 @@ -127,6 +135,17 @@ mutable struct NIfTI2Header <: NIfTIHeader end define_packed(NIfTI2Header) +""" + NIfTIHeader = Union{NIfTI1Header, NIfTI2Header} + +A union of both NIfTI header data structures. + +Most code should be written to be compatible with both headers in mind so that +functions work generically across versions (e.g., `foo(::NIfTIHeader)`). + +See also: [`NIfTI1Header`](@ref), [`NIfTI2Header`](@ref) +""" +const NIfTIHeader = Union{NIfTI1Header, NIfTI2Header} # byteswapping @@ -264,4 +283,3 @@ image related metadata. """ slicedim(x::NIfTIHeader) = Int((getfield(x, :dim_info) >> 0x04) & 0x03) slicedim(x) = slicedim(header(x)) -