Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
F
fslpy
Manage
Activity
Members
Code
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Model registry
Operate
Environments
Terraform modules
Analyze
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Evan Edmond
fslpy
Commits
a33ff838
Commit
a33ff838
authored
8 years ago
by
Paul McCarthy
Browse files
Options
Downloads
Patches
Plain Diff
In the midst of re-working the Image/Nifti1 classes. Broken are the
things.
parent
3e7d72c2
No related branches found
No related tags found
No related merge requests found
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
fsl/data/image.py
+128
-209
128 additions, 209 deletions
fsl/data/image.py
with
128 additions
and
209 deletions
fsl/data/image.py
+
128
−
209
View file @
a33ff838
...
@@ -33,20 +33,17 @@ file names:
...
@@ -33,20 +33,17 @@ file names:
import
logging
import
logging
import
tempfile
import
string
import
string
import
os
import
os
import
os.path
as
op
import
os.path
as
op
import
subprocess
as
sp
import
six
import
six
import
numpy
as
np
import
numpy
as
np
import
nibabel
as
nib
import
props
import
fsl.utils.transform
as
transform
import
fsl.utils.transform
as
transform
import
fsl.utils.status
as
status
import
fsl.utils.status
as
status
import
fsl.utils.notifier
as
notifier
import
fsl.utils.path
as
fslpath
import
fsl.utils.path
as
fslpath
import
fsl.data.constants
as
constants
import
fsl.data.constants
as
constants
...
@@ -57,6 +54,9 @@ log = logging.getLogger(__name__)
...
@@ -57,6 +54,9 @@ log = logging.getLogger(__name__)
class
Nifti1
(
object
):
class
Nifti1
(
object
):
"""
The ``Nifti1`` class is intended to be used as a base class for
"""
The ``Nifti1`` class is intended to be used as a base class for
things which either are, or are associated with, a NIFTI1 image.
things which either are, or are associated with, a NIFTI1 image.
The ``Nifti1`` class is intended to represent information stored in
the header of a NIFTI1 file - if you want to load the data from
a file, use the :class:`Image` class instead.
When a ``Nifti1`` instance is created, it adds the following attributes
When a ``Nifti1`` instance is created, it adds the following attributes
...
@@ -64,13 +64,11 @@ class Nifti1(object):
...
@@ -64,13 +64,11 @@ class Nifti1(object):
================= ====================================================
================= ====================================================
``nibImage`` The :mod:`nibabel` image object.
``header`` The :mod:`nibabel.Nifti1Header` object.
``dataSource`` The name of the file that the image was loaded from.
``tempFile`` The name of the temporary file which was created (in
the event that the image was large and was gzipped -
see :func:`loadImage`).
``shape`` A list/tuple containing the number of voxels along
``shape`` A list/tuple containing the number of voxels along
each image dimension.
each image dimension.
``pixdim`` A list/tuple containing the length of one voxel
along each image dimension.
``voxToWorldMat`` A 4*4 array specifying the affine transformation
``voxToWorldMat`` A 4*4 array specifying the affine transformation
for transforming voxel coordinates into real world
for transforming voxel coordinates into real world
coordinates.
coordinates.
...
@@ -78,80 +76,26 @@ class Nifti1(object):
...
@@ -78,80 +76,26 @@ class Nifti1(object):
for transforming real world coordinates into voxel
for transforming real world coordinates into voxel
coordinates.
coordinates.
================= ====================================================
================= ====================================================
.. note:: The ``shape`` attribute may not precisely match the image shape
as reported in the NIFTI1 header, because trailing dimensions of
size 1 are squeezed out. See the :meth:`__determineShape` and
:meth:`mapIndices` methods.
"""
"""
def
__init__
(
self
,
def
__init__
(
self
,
header
):
image
,
xform
=
None
,
header
=
None
,
loadData
=
True
):
"""
Create a ``Nifti1`` object.
"""
Create a ``Nifti1`` object.
:arg image: A string containing the name of an image file to load,
:arg header: A :class:`nibabel.nifti1.Nifti1Header` to be used as
or a :mod:`numpy` array, or a :mod:`nibabel` image
the image header.
object.
:arg xform: A :math:`4
\\
times 4` affine transformation matrix
which transforms voxel coordinates into real world
coordinates.
:arg header: If not ``None``, assumed to be a
:class:`nibabel.nifti1.Nifti1Header` to be used as the
image header. Not applied to images loaded from file,
or existing :mod:`nibabel` images.
:arg loadData: Defaults to ``True``. If ``False``, the image data is
not loaded - this is useful if you
'
re only interested
in the header data, as the file will be loaded much
more quickly. The image data may subsequently be loaded
via the :meth:`loadData` method.
"""
"""
self
.
nibImage
=
None
self
.
dataSource
=
None
self
.
tempFile
=
None
self
.
shape
=
None
self
.
pixdim
=
None
self
.
voxToWorldMat
=
None
self
.
worldToVoxMat
=
None
if
header
is
not
None
:
header
=
header
.
copy
()
# The image parameter may be the name of an image file
if
isinstance
(
image
,
six
.
string_types
):
nibImage
,
filename
=
loadImage
(
addExt
(
image
))
self
.
nibImage
=
nibImage
self
.
dataSource
=
op
.
abspath
(
image
)
# if the returned file name is not the same as
# the provided file name, that means that the
# image was opened from a temporary file
if
filename
!=
image
:
self
.
tempFile
=
nibImage
.
get_filename
()
# Or a numpy array - we wrap it in a nibabel image,
# with an identity transformation (each voxel maps
# to 1mm^3 in real world space)
elif
isinstance
(
image
,
np
.
ndarray
):
if
xform
is
None
:
header
=
header
.
copy
()
if
header
is
None
:
xform
=
np
.
identity
(
4
)
origShape
,
shape
,
pixdim
=
self
.
__determineShape
(
header
)
else
:
xform
=
header
.
get_best_affine
()
self
.
nibImage
=
nib
.
nifti1
.
Nifti1Image
(
image
,
xform
,
header
=
header
)
# otherwise, we assume that it is a nibabel image
else
:
self
.
nibImage
=
image
shape
,
pixdim
=
self
.
__determineShape
(
self
.
nibImage
)
if
len
(
shape
)
<
3
or
len
(
shape
)
>
4
:
raise
RuntimeError
(
'
Only 3D or 4D images are supported
'
)
self
.
shape
=
shape
self
.
pixdim
=
pixdim
# We have to treat FSL/FNIRT images
# We have to treat FSL/FNIRT images
# specially, as FNIRT clobbers the
# specially, as FNIRT clobbers the
...
@@ -159,37 +103,41 @@ class Nifti1(object):
...
@@ -159,37 +103,41 @@ class Nifti1(object):
# to store other data. The hard coded
# to store other data. The hard coded
# numbers here are the intent codes
# numbers here are the intent codes
# output by FNIRT.
# output by FNIRT.
intent
=
self
.
nibImage
.
get_
header
()
.
get
(
'
intent_code
'
,
-
1
)
intent
=
header
.
get
(
'
intent_code
'
,
-
1
)
if
intent
in
(
2006
,
2007
,
2008
,
2009
):
if
intent
in
(
2006
,
2007
,
2008
,
2009
):
log
.
debug
(
'
FNIRT output image detected - using qform matrix
'
)
log
.
debug
(
'
FNIRT output image detected - using qform matrix
'
)
self
.
voxToWorldMat
=
self
.
nibImage
.
get_qform
()
voxToWorldMat
=
np
.
array
(
header
.
get_qform
()
)
# Otherwise we let nibabel decide
# Otherwise we let nibabel decide
# which transform to use.
# which transform to use.
else
:
else
:
self
.
voxToWorldMat
=
np
.
array
(
self
.
nibImage
.
ge
t_affine
())
voxToWorldMat
=
np
.
array
(
header
.
get_bes
t_affine
())
self
.
worldToVoxMat
=
transform
.
invert
(
self
.
voxToWorldMat
)
worldToVoxMat
=
transform
.
invert
(
voxToWorldMat
)
if
loadData
:
self
.
header
=
header
self
.
loadData
()
self
.
shape
=
shape
else
:
self
.
__origShape
=
origShape
self
.
data
=
None
self
.
pixdim
=
pixdim
self
.
voxToWorldMat
=
voxToWorldMat
self
.
worldToVoxMat
=
worldToVoxMat
if
len
(
self
.
shape
)
<
3
or
len
(
self
.
shape
)
>
4
:
raise
RuntimeError
(
'
Only 3D or 4D images are supported
'
)
def
__determineShape
(
self
,
header
):
"""
This method is called by :meth:`__init__`. It figures out the actual
shape of the image data, and the zooms/pixdims for each data axis. Any
empty trailing dimensions are squeezed, but the returned shape is
guaranteed to be at least 3 dimensions. Returns:
def
__determineShape
(
self
,
nibImage
):
- A sequence/tuple containing the image shape, as reported in the
"""
This method is called by :meth:`__init__`. It figures out the shape
header.
of the image data, and the zooms/pixdims for each data axis. Any empty
- A sequence/tuple containing the effective image shape.
trailing dimensions are squeezed, but the returned shape is guaranteed
- A sequence/tuple containing the zooms/pixdims.
to be at least 3 dimensions.
"""
"""
nibHdr
=
nibImage
.
get_header
()
origShape
=
list
(
header
.
get_data_shape
()
)
shape
=
list
(
nibImage
.
s
hape
)
shape
=
list
(
origS
hape
)
pixdims
=
list
(
nibHd
r
.
get_zooms
())
pixdims
=
list
(
heade
r
.
get_zooms
())
# Squeeze out empty dimensions, as
# Squeeze out empty dimensions, as
# 3D image can sometimes be listed
# 3D image can sometimes be listed
...
@@ -198,7 +146,8 @@ class Nifti1(object):
...
@@ -198,7 +146,8 @@ class Nifti1(object):
if
shape
[
i
]
==
1
:
shape
=
shape
[:
i
]
if
shape
[
i
]
==
1
:
shape
=
shape
[:
i
]
else
:
break
else
:
break
# But make sure the shape is 3D
# But make sure the shape
# has at 3 least dimensions
if
len
(
shape
)
<
3
:
if
len
(
shape
)
<
3
:
shape
=
shape
+
[
1
]
*
(
3
-
len
(
shape
))
shape
=
shape
+
[
1
]
*
(
3
-
len
(
shape
))
...
@@ -206,38 +155,28 @@ class Nifti1(object):
...
@@ -206,38 +155,28 @@ class Nifti1(object):
# doesn't return at least 3 values, we'll fall
# doesn't return at least 3 values, we'll fall
# back to the pixdim field in the header.
# back to the pixdim field in the header.
if
len
(
pixdims
)
<
3
:
if
len
(
pixdims
)
<
3
:
pixdims
=
nibHd
r
[
'
pixdim
'
][
1
:]
pixdims
=
heade
r
[
'
pixdim
'
][
1
:]
pixdims
=
pixdims
[:
len
(
shape
)]
pixdims
=
pixdims
[:
len
(
shape
)]
return
shape
,
pixdims
return
origShape
,
shape
,
pixdims
def
loadData
(
self
):
"""
Loads the image data from the file. This method only needs to
be called if the ``loadData`` parameter passed to :meth:`__init__`
was ``False``.
"""
# Get the data, and reshape it according
# to the shape that the __determineShape
# method figured out.
data
=
self
.
nibImage
.
get_data
()
origShape
=
data
.
shape
data
=
data
.
reshape
(
self
.
shape
)
# Tell numpy to make the
def
mapIndices
(
self
,
sliceobj
):
# data array read-only
"""
Adjusts the given slice object so that it may be used to index the
data
.
flags
.
writeable
=
False
underlying ``nibabel.Nifti1Image` object.
self
.
data
=
data
See the :meth:`__determineShape` method.
log
.
debug
(
'
Loaded image data ({}) - original
'
:arg sliceobj: Something that can be used to slice a
'
shape {}, squeezed shape {}
'
.
format
(
multi-dimensional array, e.g. ``arr[sliceobj]``.
self
.
dataSource
,
"""
origShape
,
data
.
shape
))
# How convenient - nibabel has a function
# that does the dirty work for us.
import
nibabel.fileslice
as
fileslice
return
fileslice
.
canonical_slicers
(
sliceobj
,
self
.
__origShape
)
# TODO: Remove this method, and use the shape attribute directly
# TODO: Remove this method, and use the shape attribute directly
def
is4DImage
(
self
):
def
is4DImage
(
self
):
...
@@ -265,7 +204,7 @@ class Nifti1(object):
...
@@ -265,7 +204,7 @@ class Nifti1(object):
elif
code
==
'
qform
'
:
code
=
'
qform_code
'
elif
code
==
'
qform
'
:
code
=
'
qform_code
'
else
:
raise
ValueError
(
'
code must be None, sform, or qform
'
)
else
:
raise
ValueError
(
'
code must be None, sform, or qform
'
)
code
=
self
.
nibImage
.
get_
header
()
[
code
]
code
=
self
.
header
[
code
]
# Invalid values
# Invalid values
if
code
>
4
:
code
=
constants
.
NIFTI_XFORM_UNKNOWN
if
code
>
4
:
code
=
constants
.
NIFTI_XFORM_UNKNOWN
...
@@ -346,54 +285,47 @@ class Nifti1(object):
...
@@ -346,54 +285,47 @@ class Nifti1(object):
return
code
return
code
class
Image
(
Nifti1
,
props
.
HasProperties
):
class
Image
(
Nifti1
,
notifier
.
Notifier
):
"""
Class which represents a 3D/4D NIFTI1 image. Internally, the image
"""
Class which represents a 3D/4D NIFTI1 image. Internally, the image
is loaded/stored using :mod:`nibabel`.
is loaded/stored using :mod:`nibabel`.
In addition to the :attr:`data`, and :attr:`saved` properties, and
.. todo:: If the image appears to be large, it is loaded using the
the attributes added by the :meth:`Nifti1.__init__` method, the
:mod:`indexed_gzip` module. Implement this, and also write
following attributes are present on an ``Image`` instance:
a note about what it means.
================= ====================================================
``name`` the name of this ``Image`` - defaults to the image
file name, sans-suffix.
``dataMin`` Minimum value in the image data. This is only
calculated if the ``loadData`` parameter to
:meth:`__init__` is ``True``, or when the
:meth:`loadData` method is called. If this is not
the case, ``dataMin`` will be ``None``. The
``dataMin`` value is updated on every call to
:meth:`applyChange`.
``dataMax`` Maximum value in the image data. This is calculated
In addition to the attributes added by the :meth:`Nifti1.__init__` method,
alongside ``dataMin``.
the following attributes are present on an ``Image`` instance:
================= ====================================================
"""
data
=
props
.
Object
()
================= ====================================================
"""
The image data. This is a read-only :mod:`numpy` array - all changes
``name`` The name of this ``Image`` - defaults to the image
to the image data must be via the :meth:`applyChange` method.
file name, sans-suffix.
"""
``dataSource`` The data source of this ``Image`` - the name of the
file from where it was loaded, or some other string
describing its origin.
================= ====================================================
saved
=
props
.
Boolean
(
default
=
False
)
"""
A read-only property (not enforced) which is ``True`` if the image,
as stored in memory, is saved to disk, ``False`` otherwise.
"""
The ``Image`` class implements the :class:`.Notifier` interface -
listeners may register to be notified on the following topics (see
the :class:`.Notifier` class documentation):
dataRange
=
props
.
Bounds
(
ndims
=
1
,
default
=
(
np
.
inf
,
-
np
.
inf
))
=============== ======================================================
"""
A read-only property (not enforced) which contains the image
``
'
data
'
`` This topic is notified whenever the image data changes
data range. This property is updated every time the image data
(via the :meth:`applyChange` method).
is changed (via :meth:`applyChange`).
``
'
saveState
'
`` This topic is notified whenever the saved state of the
image changes (i.e. it is edited, or saved to disk).
``
'
dataRange
'
`` This topic is notified whenever the image data range
is changed/adjusted.
=============== ======================================================
"""
"""
def
__init__
(
self
,
image
,
name
=
None
,
**
kwargs
):
def
__init__
(
self
,
image
,
name
=
None
,
header
=
None
,
xform
=
None
):
"""
Create an ``Image`` object with the given image data or file name.
"""
Create an ``Image`` object with the given image data or file name.
:arg image: A string containing the name of an image file to load,
:arg image: A string containing the name of an image file to load,
...
@@ -402,10 +334,44 @@ class Image(Nifti1, props.HasProperties):
...
@@ -402,10 +334,44 @@ class Image(Nifti1, props.HasProperties):
:arg name: A name for the image.
:arg name: A name for the image.
All other arguments are passed through to :meth:`Nifti1.__init__`.
:arg header: If not ``None``, assumed to be a
:class:`nibabel.nifti1.Nifti1Header` to be used as the
image header. Not applied to images loaded from file,
or existing :mod:`nibabel` images.
:arg xform: A :math:`4
\\
times 4` affine transformation matrix
which transforms voxel coordinates into real world
coordinates. Only used if ``image`` is a ``numpy``
array, and ``header`` is ``None``.
"""
"""
import
nibabel
as
nib
self
.
name
=
None
self
.
dataSource
=
None
# The image parameter may be the name of an image file
if
isinstance
(
image
,
six
.
string_types
):
image
=
op
.
abspath
(
addExt
(
image
))
self
.
nibImage
=
loadImage
(
image
)
self
.
dataSource
=
image
# Or a numpy array - we wrap it in a nibabel image,
# with an identity transformation (each voxel maps
# to 1mm^3 in real world space)
elif
isinstance
(
image
,
np
.
ndarray
):
if
header
is
not
None
:
xform
=
header
.
get_best_affine
()
elif
xform
is
None
:
xform
=
np
.
identity
(
4
)
Nifti1
.
__init__
(
self
,
image
,
**
kwargs
)
self
.
nibImage
=
nib
.
nifti1
.
Nifti1Image
(
image
,
xform
,
header
=
header
)
# otherwise, we assume that it is a nibabel image
else
:
self
.
nibImage
=
image
# Figure out the name of this image.
# Figure out the name of this image.
# It might have been explicitly passed in
# It might have been explicitly passed in
...
@@ -416,7 +382,6 @@ class Image(Nifti1, props.HasProperties):
...
@@ -416,7 +382,6 @@ class Image(Nifti1, props.HasProperties):
# from disk, use the file name
# from disk, use the file name
elif
isinstance
(
image
,
six
.
string_types
):
elif
isinstance
(
image
,
six
.
string_types
):
self
.
name
=
removeExt
(
op
.
basename
(
self
.
dataSource
))
self
.
name
=
removeExt
(
op
.
basename
(
self
.
dataSource
))
self
.
saved
=
True
# Or the image was created from a numpy array
# Or the image was created from a numpy array
elif
isinstance
(
image
,
np
.
ndarray
):
elif
isinstance
(
image
,
np
.
ndarray
):
...
@@ -425,6 +390,8 @@ class Image(Nifti1, props.HasProperties):
...
@@ -425,6 +390,8 @@ class Image(Nifti1, props.HasProperties):
# Or image from a nibabel image
# Or image from a nibabel image
else
:
else
:
self
.
name
=
'
Nibabel image
'
self
.
name
=
'
Nibabel image
'
Nifti1
.
__init__
(
self
,
self
.
nibImage
.
get_header
())
def
__hash__
(
self
):
def
__hash__
(
self
):
...
@@ -682,61 +649,13 @@ def addExt(prefix, mustExist=True):
...
@@ -682,61 +649,13 @@ def addExt(prefix, mustExist=True):
def
loadImage
(
filename
):
def
loadImage
(
filename
):
"""
Given the name of an image file, loads it using nibabel.
If the file is large, and is gzipped, it is decompressed to a temporary
location, so that it can be memory-mapped.
In any case, a tuple is returned, consisting of the nibabel image object,
and the name of the file that it was loaded from (either the passed-in
file name, or the name of the temporary decompressed file).
.. note:: The decompressing logic has been disabled for the time being.
"""
"""
"""
# realFilename = filename
# mbytes = op.getsize(filename) / 1048576.0
# # The mbytes limit is arbitrary
# if filename.endswith('.gz') and mbytes > 512:
# unzipped, filename = tempfile.mkstemp(suffix='.nii')
# unzipped = os.fdopen(unzipped)
# msg = fslstrings.messages['image.loadImage.decompress']
# msg = msg.format(op.basename(realFilename), mbytes, filename)
# status.update(msg, None)
# gzip = ['gzip', '-d', '-c', realFilename]
# log.debug('Running {} > {}'.format(' '.join(gzip), filename))
# # If the gzip call fails, revert to loading from the gzipped file
# try:
# sp.call(gzip, stdout=unzipped)
# unzipped.close()
# except OSError as e:
# log.warn('gzip call failed ({}) - cannot memory '
# 'map file: {}'.format(e, realFilename),
# exc_info=True)
# unzipped.close()
# os.remove(filename)
# filename = realFilename
log
.
debug
(
'
Loading image from {}
'
.
format
(
filename
))
log
.
debug
(
'
Loading image from {}
'
.
format
(
filename
))
import
nibabel
as
nib
import
nibabel
as
nib
return
nib
.
load
(
filename
)
# if mbytes > 512:
# msg = fslstrings.messages['image.loadImage.largeFile']
# msg = msg.format(op.basename(filename), mbytes)
# status.update(msg)
image
=
nib
.
load
(
filename
)
return
image
,
filename
def
saveImage
(
image
,
fromDir
=
None
):
def
saveImage
(
image
,
fromDir
=
None
):
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment