Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
F
fslpy
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Deploy
Releases
Container Registry
Model registry
Monitor
Incidents
Analyze
Value stream analytics
Contributor 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
Michiel Cottaar
fslpy
Commits
83231d77
Commit
83231d77
authored
8 years ago
by
Paul McCarthy
Browse files
Options
Downloads
Patches
Plain Diff
ImageWrapper fixed so it now works. And some documentation added for
good measure. More work to be done.
parent
f9e3798e
No related branches found
No related tags found
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
fsl/data/imagewrapper.py
+101
-32
101 additions, 32 deletions
fsl/data/imagewrapper.py
fsl/utils/memoize.py
+4
-1
4 additions, 1 deletion
fsl/utils/memoize.py
with
105 additions
and
33 deletions
fsl/data/imagewrapper.py
+
101
−
32
View file @
83231d77
...
...
@@ -21,14 +21,23 @@ log = logging.getLogger(__name__)
class
ImageWrapper
(
notifier
.
Notifier
):
"""
def
__init__
(
self
,
image
,
name
=
None
):
Incrementally updates the data range as more image data is accessed.
"""
def
__init__
(
self
,
image
,
name
=
None
,
loadData
=
False
):
"""
:arg image: A ``nibabel.Nifti1Image``.
:arg image: A ``nibabel.Nifti1Image``.
:arg name: A name for this ``ImageWrapper``, solely used for debug
log messages.
:arg name: A name for this ``ImageWrapper``, solely used for debug
log messages.
:arg loadData: If ``True``, the image data is loaded into memory.
Otherwise it is kept on disk (and data access is
performed through the ``nibabel.Nifti1Image.dataobj``
array proxy).
"""
self
.
__image
=
image
...
...
@@ -44,16 +53,59 @@ class ImageWrapper(notifier.Notifier):
# the same part of the image. This is a list of
# (low, high) pairs, one pair for each dimension
# in the image data.
self
.
__rangeCover
=
[[
-
1
,
-
1
]
for
i
in
range
(
len
(
image
.
shape
))]
self
.
__sliceCoverage
=
[[
None
,
None
]
for
i
in
range
(
len
(
image
.
shape
))]
if
loadData
:
self
.
loadData
()
@property
def
dataRange
(
self
):
"""
Returns the currently known data range as a tuple of ``(min, max)``
values.
If the data range is completely unknown, returns ``(None, None)``.
"""
return
tuple
(
self
.
__range
)
def
__rangeCovered
(
self
,
slices
):
"""
Returns ``True`` if the range for the image data calculated by
def
loadData
(
self
):
"""
Forces all of the image data to be loaded into memory.
.. note:: This method will be called by :meth:`__init__` if its
``loadData`` parameter is ``True``.
"""
# If the data is not already
# loaded, this will cause
# nibabel to load and cache it
self
.
__image
.
get_data
()
def
__sanitiseSlices
(
self
,
slices
):
"""
Turns an array slice object into a tuple of (low, high) index
pairs, one pair for each dimension in the image data.
"""
indices
=
[]
for
dim
,
s
in
enumerate
(
slices
):
# each element in the slices tuple should
# be a slice object or an integer
if
isinstance
(
s
,
slice
):
i
=
[
s
.
start
,
s
.
stop
]
else
:
i
=
[
s
,
s
+
1
]
if
i
[
0
]
is
None
:
i
[
0
]
=
0
if
i
[
1
]
is
None
:
i
[
1
]
=
self
.
__image
.
shape
[
dim
]
indices
.
append
(
tuple
(
i
))
return
tuple
(
indices
)
def
__sliceCovered
(
self
,
slices
):
"""
Returns ``True`` if the portion of the image data calculated by
the given ``slices` has already been calculated, ``False`` otherwise.
"""
...
...
@@ -62,12 +114,15 @@ class ImageWrapper(notifier.Notifier):
# TODO You could adjust the slice so that
# it only spans the portion of the
# image that has not yet been covered.
# image that has not yet been covered,
# and return it to minimise the portion
# of the image over which the range is
# updated.
for
dim
,
size
in
enumerate
(
self
.
__image
.
shape
):
lowCover
,
highCover
=
self
.
__
rang
eCover
[
dim
]
lowCover
,
highCover
=
self
.
__
slic
eCover
age
[
dim
]
if
lowCover
==
-
1
or
highCover
==
-
1
:
if
lowCover
is
None
or
highCover
is
None
:
return
False
lowSlice
,
highSlice
=
slices
[
dim
]
...
...
@@ -81,28 +136,42 @@ class ImageWrapper(notifier.Notifier):
return
True
def
__update
CoveredRan
ge
(
self
,
slices
):
def
__update
SliceCovera
ge
(
self
,
slices
):
"""
"""
for
dim
,
(
lowSl
ice
,
highSl
ice
)
in
enumerate
(
slices
):
for
dim
,
(
lowSl
c
,
highSl
c
)
in
enumerate
(
slices
):
lowCov
er
,
highCov
er
=
self
.
__
rang
eCover
[
dim
]
lowCov
,
highCov
=
self
.
__
slic
eCover
age
[
dim
]
if
lowSl
ice
is
None
:
lowSl
ice
=
0
if
highSl
ice
is
None
:
highSl
ice
=
self
.
__image
.
shape
[
dim
]
if
lowSl
c
is
None
:
lowSl
c
=
0
if
highSl
c
is
None
:
highSl
c
=
self
.
__image
.
shape
[
dim
]
if
low
Slice
<
lowCov
er
:
lowCov
er
=
lowSl
ice
if
high
Slice
>
highCov
er
:
highCov
er
=
highSl
ice
if
low
Cov
is
None
or
lowSlc
<
lowCov
:
lowCov
=
lowSl
c
if
high
Cov
is
None
or
highSlc
>
highCov
:
highCov
=
highSl
c
self
.
__
rang
eCover
[
dim
]
=
[
lowCov
er
,
highCov
er
]
self
.
__
slic
eCover
age
[
dim
]
=
[
lowCov
,
highCov
]
@memoize.Instanceify
(
memoize
.
memoize
(
args
=
[
0
]))
def
__updateRangeOnRead
(
self
,
slices
,
data
):
def
__updateDataRangeOnRead
(
self
,
slices
,
data
):
"""
:arg slices: A sequence of ``(low, high)`` index pairs, one for each
dimension in the image. Tuples are used instead of
``slice`` objects, because this method is memoized (and
``slice`` objects are unhashable).
"""
oldmin
,
oldmax
=
self
.
__range
log
.
debug
(
'
Updating image {} data range (current range:
'
'
[{}, {}]; current coverage: {})
'
.
format
(
self
.
__name
,
self
.
__range
[
0
],
self
.
__range
[
1
],
self
.
__sliceCoverage
))
dmin
=
np
.
nanmin
(
data
)
dmax
=
np
.
nanmax
(
data
)
...
...
@@ -116,16 +185,15 @@ class ImageWrapper(notifier.Notifier):
else
:
newmax
=
oldmax
self
.
__range
=
(
newmin
,
newmax
)
self
.
__update
CoveredRan
ge
(
slices
)
self
.
__update
SliceCovera
ge
(
slices
)
if
newmin
!=
oldmin
or
newmax
!=
oldmax
:
log
.
debug
(
'
Image {} data range adjusted: {} - {}
'
.
format
(
self
.
__name
,
newmin
,
newmax
))
log
.
debug
(
'
Image {} range changed: [{}, {}]
'
.
format
(
self
.
__name
,
self
.
__range
[
0
],
self
.
__range
[
1
]))
self
.
notify
()
# def __updateRangeOnWrite(self, oldvals, newvals):
# def __update
Data
RangeOnWrite(self, oldvals, newvals):
# """Called by :meth:`__setitem__`. Re-calculates the image data
# range, and returns a tuple containing the ``(min, max)`` values.
# """
...
...
@@ -180,7 +248,10 @@ class ImageWrapper(notifier.Notifier):
sliceobj
=
nib
.
fileslice
.
canonical_slicers
(
sliceobj
,
self
.
__image
.
shape
)
# If the image has noy been loaded
# TODO Cache 3D images for large 4D volumes,
# so you don't have to hit the disk
# If the image has not been loaded
# into memory, we can use the nibabel
# ArrayProxy. Otheriwse if it is in
# memory, we can access it directly.
...
...
@@ -190,16 +261,14 @@ class ImageWrapper(notifier.Notifier):
# will give us out-of-date values (as
# the ArrayProxy reads from disk). So
# we have to read from the in-memory
# array.
# array
to get changed values
.
if
self
.
__image
.
in_memory
:
data
=
self
.
__image
.
get_data
()[
sliceobj
]
else
:
data
=
self
.
__image
.
dataobj
[
sliceobj
]
slices
=
tuple
((
s
.
start
,
s
.
stop
)
if
isinstance
(
s
,
slice
)
else
(
s
,
s
+
1
)
for
s
in
sliceobj
)
slices
=
self
.
__sanitiseSlices
(
sliceobj
)
if
not
self
.
__
rang
eCovered
(
slices
):
self
.
__updateRangeOnRead
(
slices
,
data
)
if
not
self
.
__
slic
eCovered
(
slices
):
self
.
__update
Data
RangeOnRead
(
slices
,
data
)
return
data
...
...
@@ -214,4 +283,4 @@ class ImageWrapper(notifier.Notifier):
# # (if it has not already done so).
# self.__image.get_data()[sliceobj] = values
# self.__updateRangeOnWrite(values)
# self.__update
Data
RangeOnWrite(values)
This diff is collapsed.
Click to expand it.
fsl/utils/memoize.py
+
4
−
1
View file @
83231d77
...
...
@@ -21,13 +21,16 @@ import functools
import
six
# TODO Make this a class, and add
# a "clearCache" method to it.
def
memoize
(
args
=
None
,
kwargs
=
None
):
"""
Memoize the given function by the value of the input arguments, allowing
the caller to specify which positional arguments (by index) and keyword
arguments (by name) are used for the comparison.
If no positional or keyword arguments are specified, the function is
memoized on all arguments.
memoized on all arguments. Note that the arguments used for memoization
must be hashable, as they are used as keys in a dictionary..
.. note:: This decorator must always be called with brackets, e.g.::
memoize()
...
...
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