Skip to content

Commit

Permalink
Merge pull request #50 from JuliaNeuroscience/extensions
Browse files Browse the repository at this point in the history
Update extensions interface
  • Loading branch information
Tokazama authored Feb 12, 2021
2 parents 8c27ae2 + ceeba39 commit a328763
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 155 deletions.
23 changes: 1 addition & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,4 @@ jobs:
- uses: codecov/codecov-action@v1
with:
file: lcov.info
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
with:
version: '1'
- run: |
julia --project=docs -e '
using Pkg
Pkg.develop(PackageSpec(path=pwd()))
Pkg.instantiate()'
- run: |
julia --project=docs -e '
using Documenter: doctest
using NIfTI
doctest(NIfTI)'
- run: julia --project=docs docs/make.jl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}

2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "NIfTI"
uuid = "a3a9e032-41b5-5fc4-967a-a6b7a19844d3"
version = "0.5.1"
version = "0.5.2"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Expand Down
54 changes: 10 additions & 44 deletions src/NIfTI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Base.getindex, Base.size, Base.ndims, Base.length, Base.write, Base64
export NIVolume, niread, niwrite, voxel_size, time_step, vox, getaffine, setaffine

include("parsers.jl")
include("extensions.jl")

function define_packed(ty::DataType)
packed_offsets = cumsum([sizeof(x) for x in ty.types])
Expand Down Expand Up @@ -112,27 +113,22 @@ function string_tuple(x::String, n::Int)
end
string_tuple(x::AbstractString) = string_tuple(bytestring(x))

struct NIfTI1Extension
ecode::Int32
edata::Vector{UInt8}
end

struct NIVolume{T<:Number,N,R} <: AbstractArray{T,N}
header::NIfTI1Header
extensions::Vector{NIfTI1Extension}
extensions::Vector{NIfTIExtension}
raw::R
end
NIVolume(header::NIfTI1Header, extensions::Vector{NIfTI1Extension}, raw::R) where {R}=
NIVolume(header::NIfTI1Header, extensions::Vector{NIfTIExtension}, raw::R) where {R}=
niupdate(new(header, extensions, raw))

NIVolume(header::NIfTI1Header, extensions::Vector{NIfTI1Extension}, raw::AbstractArray{T,N}) where {T<:Number,N} =
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, NIfTI1Extension[], raw)
NIVolume(header::NIfTI1Header, extensions::Vector{NIfTI1Extension}, raw::AbstractArray{Bool,N}) where {N} =
NIVolume{typeof(one(T)*1f0+1f0),N,typeof(raw)}(header, NIfTIExtension[], raw)
NIVolume(header::NIfTI1Header, 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{Bool,N,typeof(raw)}(header, NIfTI1Extension[], raw)
NIVolume{Bool,N,typeof(raw)}(header, NIfTIExtension[], raw)

# Conversion factors to mm/ms
# http://nifti.nimh.nih.gov/nifti-1/documentation/nifti1fields/nifti1fields_pages/xyzt_units.html
Expand Down Expand Up @@ -259,7 +255,7 @@ end
function NIVolume(
# Optional MRI volume; if not given, an empty volume is used
raw::AbstractArray{T}=Int16[],
extensions::Union{Vector{NIfTI1Extension},Nothing}=nothing;
extensions::Union{Vector{NIfTIExtension},Nothing}=nothing;

# Fields specified as UNUSED in NIfTI1 spec
data_type::AbstractString="", db_name::AbstractString="", extents::Integer=Int32(0),
Expand Down Expand Up @@ -306,7 +302,7 @@ function NIVolume(
end

if extensions == nothing
extensions = NIfTI1Extension[]
extensions = NIfTIExtension[]
end

method2 = qfac != 0 || quatern_b != 0 || quatern_c != 0 || quatern_d != 0 || qoffset_x != 0 ||
Expand Down Expand Up @@ -342,9 +338,6 @@ function NIVolume(
(orientation[2, :]...,), (orientation[3, :]...,), string_tuple(intent_name, 16), NP1_MAGIC), extensions, raw)
end

# Calculates the size of a NIfTI extension
esize(ex::NIfTI1Extension) = 8 + ceil(Int, length(ex.edata)/16)*16

# 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
Expand Down Expand Up @@ -404,33 +397,6 @@ function read_header(io::IO)
header, swapped
end

# Read extension fields following NIfTI header
function read_extensions(io::IO, header::NIfTI1Header)
if eof(io)
return NIfTI1Extension[]
end

extension = read!(io, Array{UInt8}(undef, 4))
if extension[1] != 1
return NIfTI1Extension[]
end

extensions = NIfTI1Extension[]
should_bswap = header.dim[1] > 7
while header.magic == NP1_MAGIC ? position(io) < header.vox_offset : !eof(io)
esize = read(io, Int32)
ecode = read(io, Int32)

if should_bswap
esize = bswap(esize)
ecode = bswap(ecode)
end

push!(extensions, NIfTI1Extension(ecode, read!(io, Array{UInt8}(undef, esize-8))))
end
extensions
end

# Look for a gzip header in an IOStream
function isgz(io::IO)
ret = read(io, UInt8) == 0x1F && read(io, UInt8) == 0x8B
Expand All @@ -448,7 +414,7 @@ function niread(file::AbstractString; mmap::Bool=false, mode::AbstractString="r"
header_gzipped = isgz(file_io)
header_io = header_gzipped ? GzipDecompressorStream(file_io) : file_io
header, swapped = read_header(header_io)
extensions = read_extensions(header_io, header)
extensions = read_extensions(header_io, header.vox_offset - 348)
dims = convert(Tuple{Vararg{Int}}, header.dim[2:header.dim[1]+1])

dtype = to_eltype(header.datatype)
Expand Down
182 changes: 182 additions & 0 deletions src/extensions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@

@inline function to_ecode(x::Int32)
if x === Int32(2)
return :DICOM
elseif x === Int32(4)
return :AFNI
elseif x === Int32(6)
return :Comment
elseif x === Int32(8)
return :XCEDE
elseif x === Int32(10)
return :JimDimInfo
elseif x === Int32(12)
return :WorkflowFWDS
elseif x === Int32(14)
return :Freesurfer
elseif x === Int32(16)
return :PyPickly
elseif x === Int32(18)
return :MindIdent
elseif x === Int32(20)
return :BValue
elseif x === Int32(22)
return :SphericalDirection
elseif x === Int32(24)
return :DTComponent
elseif x === Int32(26)
return :SHCDegreeOrder
elseif x === Int32(28)
return :Voxbo
elseif x === Int32(30)
return :Caret
elseif x === Int32(32)
return :CIfTI
elseif x === Int32(34)
return :VariableFrameTiming
elseif x === Int32(38)
return :Eval
elseif x === Int32(40)
return :Matlab
else
return :Ignore
end
end

@inline function ecode_to_int(x::Symbol)
if x === :DICOM
return Int32(2)
elseif x === :AFNI
return Int32(4)
elseif x === :Comment
return Int32(6)
elseif x === :XCEDE
return Int32(8)
elseif x === :JimDimInfo
return Int32(10)
elseif x === :WorkflowFWDS
return Int32(12)
elseif x === :Freesurfer
return Int32(14)
elseif x === :PyPickly
return Int32(16)
elseif x === :MindIdent
return Int32(18)
elseif x === :BValue
return Int32(20)
elseif x === :SphericalDirection
return Int32(22)
elseif x === :DTComponent
return Int32(24)
elseif x === :SHCDegreeOrder
return Int32(26)
elseif x === :Voxbo
return Int32(28)
elseif x === :Caret
return Int32(30)
elseif x === :CIfTI
return Int32(32)
elseif x === :VariableFrameTiming
return Int32(34)
elseif x === :Eval
return Int32(38)
elseif x === :Matlab
return Int32(40)
else
return Int32(0)
end
end

"""
NIfTIExtension
* `ecode::Int32`: extension code that can be interpreted used `ecode(::NIfTIExtension)`
* `edata::Vector{UInt8}`: raw data with no byte-swapping
"""
struct NIfTIExtension
ecode::Int32
edata::Vector{UInt8}
end

"""
NIfTI Extension Codes
* Ignore
* DICOM: intended for raw DICOM attributes
* AFNI: Robert W Cox: [email protected] https://afni.nimh.nih.gov/afni
* Comment: plain ASCII text only
* XCEDE: http://www.nbirn.net/Resources/Users/Applications/xcede/index.html
* JIMDimInfo: Dimensionalinformation for the JIM software (XML format)
* WorkflowFWDS:
* Freesurfer:
* PyPickle: embedded Python objects
* MiNDIdent: LONI MiND codes: http://www.loni.ucla.edu/twiki/bin/view/Main/MiND
* BValue
* SphericalDirection
* DTComponent
* SHCDegreeOrder
* Voxbo: www.voxbo.org
* Caret: http://brainvis.wustl.edu/wiki/index.php/Caret:Documentation
* CIfTI
* VariableFrameTiming
* AgilentProcpar
* Eval
* Matlab
"""
ecode(x::NIfTIExtension) = to_ecode(x.ecode)
ecode(x::Vector{NIfTIExtension}) = map(ecode, x)


# Calculates the size of a NIfTI extension
"""
esize(ex::NIfTIExtension)
NIfTIExtensions should be of a byte size that is a mulitple of 16. This includes
raw encoding of the the `ecode` (as an Int32) and the esize itself (also as an
Int32). Therefore, `8 + sizeof(ex.edata)` should be divisible by 16.
"""
function esize(ex::NIfTIExtension)
ret = 8 + length(ex.edata)
@assert ret%16 != 0 "NIfTIExtension has innapropriate size. See docstrings for more details."
return ret
end

function read_extensions(io, n)
ret = NIfTIExtension[]
if eof(io)
return ret
else
b1 = read(io, UInt8)
# GZIP doesn't skip so we read and throw away
read(io, UInt8)
read(io, UInt8)
read(io, UInt8)
if b1 === zero(UInt8)
return ret
else
counter = position(io)
while counter < (n-1)
esize = read(io, Int32)
ec = read(io, Int32)
push!(ret, NIfTIExtension(ec, read!(io, Array{UInt8}(undef, esize-8))))
counter += esize
end
return ret
end
end
end

function write(io::IO, x::Vector{NIfTIExtension})
if isempty(x)
write(io, fill(UInt8(0), 4))
else
write(io, UInt8[1, 0, 0, 0])
for ex in x
write(io, Int32(esize(ex)))
write(io, ex.ecode)
write(io, ex.edata)
# write(io, zeros(UInt8, esize(ex) - length(ex.edata)))
end
end
end

Loading

2 comments on commit a328763

@Tokazama
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/29970

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.2 -m "<description of version>" a328763e05d0f3869b6921b23a56c5f12037d540
git push origin v0.5.2

Please sign in to comment.