1
2
3
4
5
6
7
8
9 """Base classes for Anatomy atlases support
10
11 TODOs:
12 ======
13
14 * major optimization. Now code is sloppy and slow -- plenty of checks etc
15
16 Module Organization
17 ===================
18 mvpa.atlases.base module contains support for various atlases
19
20 .. packagetree::
21 :style: UML
22
23 :group Base: BaseAtlas XMLBasedAtlas Label Level LabelsLevel
24 :group Talairach: PyMVPAAtlas LabelsAtlas ReferencesAtlas
25 :group Exceptions: XMLAtlasException
26
27 """
28
29 from mvpa.base import externals
30
31 externals.exists('lxml', raiseException=True)
32 from lxml import etree, objectify
33
34 import os, re
35 import numpy as N
36 from numpy.linalg import norm
37
38 from mvpa.atlases.transformation import SpaceTransformation, Linear
39 from mvpa.misc.support import reuseAbsolutePath
40
41 externals.exists('nifti', raiseException=True)
42 from nifti import NiftiImage
43
44 from mvpa.base import warning
45 if __debug__:
46 from mvpa.base import debug
47
48
50 """
51 Check if coordinates are within range (0,0,0) - (range)
52 Return True on success
53 """
54
55 if len(coord) != len(range):
56 raise ValueError("Provided coordinate %s and given range %s" % \
57 (`coord`, `range`) + \
58 " have different dimensionality"
59 )
60 for c,r in zip(coord, range):
61 if c<0 or c>=r:
62 return False
63 return True
64
65
67 """Base class for the atlases.
68 """
69
71 """
72 Create an atlas object based on the... XXX
73 """
74 self.__name = "blank"
75
76
78 """ Exception to be thrown if smth goes wrong dealing with XML based atlas
79 """
84
85
87
88 - def __init__(self, filename=None, resolution=None, query_voxel=False,
89 coordT=None, levels=None):
90 """
91 :Parameters:
92 filename : string
93 Filename for the xml definition of the atlas
94 resolution : None or float
95 Some atlases link to multiple images at different
96 resolutions. if None -- best resolution is selected
97 using 0th dimension resolution
98 query_voxel : bool
99 By default [x,y,z] assumes coordinates in space, but if
100 query_voxel is True, they are assumed to be voxel coordinates
101 coordT
102 Optional transformation to apply first
103 levels : None or slice or list of int
104 What levels by default to operate on
105 """
106 BaseAtlas.__init__(self)
107 self.__version = None
108 self.__type = None
109 self._imagefile = None
110 self.__atlas = None
111 self._filename = filename
112 self._resolution = resolution
113 self.query_voxel = query_voxel
114 self.levels = levels
115
116 if filename:
117 self.loadAtlas(filename)
118
119
120 if not self._checkVersion(self.version):
121 raise IOError("Version %s is not recognized to be native to class %s" % \
122 (self.version, self.__name__))
123
124 if not set(['header', 'data']) == set([i.tag for i in self.getchildren()]):
125 raise IOError("No header or data were defined in %s" % filename)
126
127 header = self.header
128 headerChildrenTags = XMLBasedAtlas._children_tags(header)
129 if not ('images' in headerChildrenTags) or \
130 not ('imagefile' in XMLBasedAtlas._children_tags(header.images)):
131 raise XMLAtlasException("Atlas requires image/imagefile header fields")
132
133
134 self._image = None
135 self._loadImages()
136 if self._image is not None:
137 self._extent = N.abs(N.asanyarray(self._image.extent[0:3]))
138 self._voxdim = N.asanyarray(self._image.voxdim)
139 self.relativeToOrigin = True
140
141
142 self.setCoordT(coordT)
143 self._loadData()
144
145
147 """ check and adjust the voxel coordinates"""
148
149
150 if __debug__: debug('ATL__', "Querying for voxel %s" % `list(c)`)
151 if not checkRange(c, self.extent):
152 msg = "Coordinates %s are not within the extent %s." \
153 "Reset to (0,0,0)" % ( `c`, `self.extent` )
154 if __debug__: debug('ATL_', msg)
155
156 c = [0]*3;
157 return c
158
159
160 @staticmethod
162 """To be overriden in the derived classes. By default anything is good"""
163 return True
164
165
167 """To be overriden in the derived classes. By default does nothing"""
168 pass
169
170
172 """To be overriden in the derived classes. By default does nothing"""
173 pass
174
175
177 if __debug__: debug('ATL_', "Loading atlas definition xml file " + filename)
178
179 parser = etree.XMLParser(remove_blank_text=True)
180 lookup = objectify.ObjectifyElementClassLookup()
181 parser.setElementClassLookup(lookup)
182 try:
183 self.__atlas = etree.parse(filename, parser).getroot()
184 except IOError:
185 raise XMLAtlasException("Failed to load XML file %s" % filename)
186
187 @property
189 if not self.__atlas is None \
190 and ("version" in self.__atlas.attrib.keys()):
191 return self.__atlas.get("version")
192 else:
193 return None
194
195 @staticmethod
197 raise RuntimeError, "DEPRECATED _compare_lists"
198 checkitems.sort()
199 neededitems.sort()
200 return (checkitems == neededitems)
201
202
203 @staticmethod
206
207
209 """
210 Lazy way to provide access to the definitions in the atlas
211 """
212 if not self.__atlas is None:
213 return getattr(self.__atlas, attr)
214 else:
215 raise XMLAtlasException("Atlas in " + self.__name__ + " was not read yet")
216
217
219 """Set coordT transformation.
220
221 spaceT needs to be adjusted since we glob those two
222 transformations together
223 """
224 self._coordT = coordT
225 if self._image is not None:
226
227 coordT = Linear(N.linalg.inv(self._image.qform),
228 previous=coordT)
229 self._spaceT = SpaceTransformation(
230 previous=coordT, toRealSpace=False
231 )
232
233
235 """Return labels for the given spatial point at specified levels
236
237 so we first transform point into the voxel space
238 """
239 coord_ = N.asarray(coord)
240
241
242
243
244
245
246
247
248 c = self.spaceT(coord_)
249
250 result = self.labelVoxel(c, levels)
251 result['coord_queried'] = coord
252
253 result['voxel_atlas'] = c
254 return result
255
256
258 lkeys = range(self.Nlevels)
259 return '\n'.join(['%d: ' % k + str(self._levels_dict[k])
260 for k in lkeys])
261
262
264 """Helper to provide list of levels to operate on
265
266 Depends on given `levels` as well as self.levels
267 """
268 if levels is None:
269 levels = [ i for i in xrange(self.Nlevels) ]
270 elif (isinstance(levels, slice)):
271
272 if levels.step: step = levels.step
273 else: step = 1
274
275 if levels.start: start = levels.start
276 else: start = 0
277
278 if levels.stop: stop = levels.stop
279 else: stop = self.Nlevels
280
281 levels = [ i for i in xrange(start, stop, step) ]
282
283 elif isinstance(levels, list) or isinstance(levels, tuple):
284
285 levels = list(levels)
286
287 elif isinstance(levels, int):
288 levels = [ levels ]
289
290 else:
291 raise TypeError('Given levels "%s" are of unsupported type' % `levels`)
292
293
294 levels_dict = self.levels_dict
295 for level in levels:
296 if not level in levels_dict:
297 raise ValueError, \
298 "Levels %s is not known (out of range?). Known levels are:\n%s" \
299 % (level, self.levelsListing())
300
301 return levels
302
303
305 """
306 Accessing the elements via simple indexing. Examples:
307 print atlas[ 0, -7, 20, [1,2,3] ]
308 print atlas[ (0, -7, 20), 1:2 ]
309 print atlas[ (0, -7, 20) ]
310 print atlas[ (0, -7, 20), : ]
311 """
312 if len(index) in [2, 4]:
313 levels_slice = index[-1]
314 else:
315 if self.levels is None:
316 levels_slice = slice(None,None,None)
317 else:
318 levels_slice = self.levels
319
320 levels = self._getLevels(levels=levels_slice)
321
322 if len(index) in [3, 4]:
323
324 coord = index[0:3]
325
326 elif len(index) in [1, 2]:
327 coord = index[0]
328 if isinstance(coord, list) or isinstance(coord, tuple):
329 if len(coord) != 3:
330 raise TypeError("Given coordinates must be in 3D")
331 else:
332 raise TypeError("Given coordinates must be a list or a tuple")
333
334 else:
335 raise TypeError("Unknown shape of parameters `%s`" % `index`)
336
337 if self.query_voxel:
338 return self.labelVoxel(coord, levels)
339 else:
340 return self.labelPoint(coord, levels)
341
342
343
346
348 return self._levels_dict
349
350 levels_dict = property(fget=_getLevelsDict)
351
352
353 origin = property(fget=lambda self:self._origin)
354 extent = property(fget=lambda self:self._extent)
355 voxdim = property(fget=lambda self:self._voxdim)
356 spaceT = property(fget=lambda self:self._spaceT)
357 coordT = property(fget=lambda self:self._spaceT,
358 fset=setCoordT)
359
361 """Represents a label. Just to bring all relevant information together
362 """
363 - def __init__ (self, text, abbr=None, coord=(None, None,None),
364 count=0, index=0):
365 """
366 :Parameters:
367 text : basestring
368 fullname of the label
369 abbr : basestring
370 abbreviated name (optional)
371 coord : tuple of float
372 coordinates (optional)
373 count : int
374 count of those labels in the atlas (optional)
375
376 """
377 self.__text = text.strip()
378 if abbr is not None:
379 abbr = abbr.strip()
380 self.__abbr = abbr
381 self.__coord = coord
382 self.__count = count
383 self.__index = int(index)
384
385
386 @property
389
391 return "Label(%s%s, coord=(%s, %s, %s), count=%s, index=%s)" % \
392 ((self.__text,
393 (', abbr=%s' % repr(self.__abbr), '')[int(self.__abbr is None)])
394 + tuple(self.__coord) + (self.__count, self.__index))
395
398
399 @staticmethod
401 kwargs = {}
402 if Elabel.attrib.has_key('x'):
403 kwargs['coord'] = ( Elabel.attrib.get('x'),
404 Elabel.attrib.get('y'),
405 Elabel.attrib.get('z') )
406 for l in ('count', 'abbr', 'index'):
407 if Elabel.attrib.has_key(l):
408 kwargs[l] = Elabel.attrib.get(l)
409 return Label(Elabel.text.strip(), **kwargs)
410
411 @property
412 - def count(self): return self.__count
413 @property
414 - def coord(self): return self.__coord
415 @property
416 - def text(self): return self.__text
417 @property
419 """Returns abbreviated version if such is available
420 """
421 if self.__abbr in [None, ""]:
422 return self.__text
423 else:
424 return self.__abbr
425
426
428 """Represents a level. Just to bring all relevant information together
429 """
431 self.description = description
432 self._type = "Base"
433
435 return "%s Level: %s" % \
436 (self.levelType, self.description)
437
439 return self.description
440
441 @staticmethod
458
459 levelType = property(lambda self: self._type)
460
461
463 """Level of labels.
464
465 XXX extend
466 """
467 - def __init__ (self, description, index=None, labels=[]):
472
474 return Level.__repr__(self) + " [%d] " % \
475 (self.__index)
476
477 @staticmethod
479
480
481
482 index = 0
483 if Elevel.attrib.has_key("index"):
484 index = int(Elevel.get("index"))
485
486 maxindex = max([int(i.get('index')) \
487 for i in Elevel.label[:]])
488 labels = [ None for i in xrange(maxindex+1) ]
489 for label in Elevel.label[:]:
490 labels[ int(label.get('index')) ] = Label.generateFromXML(label)
491
492 levelIndex[0] = max(levelIndex[0], index) + 1
493
494 return LabelsLevel(Elevel.get('description'),
495 index,
496 labels)
497
498 @property
499 - def index(self): return self.__index
500
501 @property
502 - def labels(self): return self.__labels
503
505 return self.__labels[index]
506
507
509 """Level which carries reference points
510 """
511 - def __init__ (self, description, indexes=[]):
515
516 @staticmethod
518
519 requiredAttrs = ['x', 'y', 'z', 'type', 'description']
520 if not set(requiredAttrs) == set(Elevel.attrib.keys()):
521 raise XMLAtlasException("ReferencesLevel has to have " +
522 "following attributes defined " +
523 `requiredAttrs`)
524
525 indexes = ( int(Elevel.get("x")), int(Elevel.get("y")),
526 int(Elevel.get("z")) )
527
528 return ReferencesLevel(Elevel.get('description'),
529 indexes)
530
531 @property
532 - def indexes(self): return self.__indexes
533
534
536 """Base class for PyMVPA atlases, such as LabelsAtlas and ReferenceAtlas
537 """
538
539 source = 'PyMVPA'
540
542 XMLBasedAtlas.__init__(self, *args, **kwargs)
543
544
545 header = self.header
546 headerChildrenTags = XMLBasedAtlas._children_tags(header)
547 if not ('space' in headerChildrenTags) or \
548 not ('space-flavor' in headerChildrenTags):
549 raise XMLAtlasException("PyMVPA Atlas requires specification of" +
550 " the space in which atlas resides")
551
552 self.__space = header.space.text
553 self.__spaceFlavor = header['space-flavor'].text
554
555
557
558 imagefile = self.header.images.imagefile
559
560
561
562
563
564 self._origin = N.array( (0,0,0) )
565 if imagefile.attrib.has_key('offset'):
566 self._origin = N.array( map(int,
567 imagefile.get('offset').split(',')) )
568
569
570 imagefilename = reuseAbsolutePath(self._filename, imagefile.text)
571
572 try:
573 self._image = NiftiImage(imagefilename)
574 except RuntimeError, e:
575 raise RuntimeError, " Cannot open file " + imagefilename
576
577 self._data = self._image.data
578
579
580 if len(self._data.shape[0:-4]) > 0:
581 bogus_dims = self._data.shape[0:-4]
582 if max(bogus_dims)>1:
583 raise RuntimeError, "Atlas %s has more than 4 of non-singular" \
584 "dimensions" % imagefilename
585 new_shape = self._data.shape[-4:]
586 self._data.reshape(new_shape)
587
588
589
590
591
592
594
595 self._levels_dict = {}
596
597 self._Nlevels = 0
598 index_incr = 0
599 for index, child in enumerate(self.data.getchildren()):
600 if child.tag == 'level':
601 level = Level.generateFromXML(child)
602 self._levels_dict[level.description] = level
603 if hasattr(level, 'index'):
604 index = level.index
605 else:
606
607
608 while index_incr in self._levels_dict:
609 index_incr += 1
610 index, index_incr = index_incr, index_incr+1
611 self._levels_dict[index] = level
612 else:
613 raise XMLAtlasException(
614 "Unknown child '%s' within data" % child.tag)
615 self._Nlevels += 1
616
617
620
623
624 @staticmethod
626
627 return version.startswith("pymvpa-") or version.startswith("rumba-")
628
629
630 space = property(fget=lambda self:self.__space)
631 spaceFlavor = property(fget=lambda self:self.__spaceFlavor)
632 Nlevels = property(fget=_getNLevels)
633
634
636 """
637 Atlas which provides labels for the given coordinate
638 """
639
641 """
642 Return labels for the given voxel at specified levels specified by index
643 """
644 levels = self._getLevels(levels=levels)
645
646 result = {'voxel_queried' : c}
647
648
649 c = self._checkRange(c)
650
651 resultLevels = []
652 for level in levels:
653 if self._levels_dict.has_key(level):
654 level_ = self._levels_dict[ level ]
655 else:
656 raise IndexError(
657 "Unknown index or description for level %d" % level)
658
659 resultIndex = int(self._data[ level_.index, \
660 c[2], c[1], c[0] ])
661
662 resultLevels += [ {'index': level_.index,
663 'id': level_.description,
664 'label' : level_[ resultIndex ]} ]
665
666 result['labels'] = resultLevels
667 return result
668
669
671 """
672 Atlas which provides references to the other atlases.
673
674 Example: the atlas which has references to the closest points
675 (closest Gray, etc) in another atlas.
676 """
677
678 - def __init__(self, distance=0, *args, **kwargs):
701
702
703
704
705
707 return self.__referenceAtlas.Nlevels
708
709
711 """
712 Set the level which will be queried
713 """
714 if self._levels_dict.has_key(level):
715 self.__referenceLevel = self._levels_dict[level]
716 else:
717 raise IndexError("Unknown reference level " + `level` +
718 ". Known are " + `self._levels_dict.keys()`)
719
720
722
723 if self.__referenceLevel is None:
724 warning("You did not provide what level to use "
725 "for reference. Assigning 0th level -- '%s'"
726 % (self._levels_dict[0],))
727 self.setReferenceLevel(0)
728
729
730 c = self._checkRange(c)
731
732
733 cref = self._data[ self.__referenceLevel.indexes, c[2], c[1], c[0] ]
734 dist = norm( (cref - c) * self.voxdim )
735 if __debug__:
736 debug('ATL__', "Closest referenced point for %s is "
737 "%s at distance %3.2f" % (`c`, `cref`, dist))
738 if (self.distance - dist) >= 1e-3:
739 result = self.__referenceAtlas.labelVoxel(cref, levels)
740 result['voxel_referenced'] = c
741 result['distance'] = dist
742 else:
743 result = self.__referenceAtlas.labelVoxel(c, levels)
744 if __debug__:
745 debug('ATL__', "Closest referenced point is "
746 "further than desired distance %.2f" % self.distance)
747 result['voxel_referenced'] = None
748 result['distance'] = 0
749 return result
750
751
754
757
759 """
760 Set desired maximal distance for the reference
761 """
762 if distance < 0:
763 raise ValueError("Distance should not be negative. "
764 " Thus '%f' is not a legal value" % distance)
765 if __debug__:
766 debug('ATL__',
767 "Setting maximal distance for queries to be %d" % distance)
768 self.__distance = distance
769
770 distance = property(fget=lambda self:self.__distance, fset=setDistance)
771