1
2
3
4
5
6
7
8
9 """Basic (f)MRI plotting with ability to interactively perform thresholding
10
11 """
12
13 import pylab as P
14 import numpy as N
15 import matplotlib as mpl
16
17 from mvpa.base import warning, externals
18
19 if externals.exists('nifti', raiseException=True):
20 from nifti import NiftiImage
21
22 _interactive_backends = ['GTKAgg', 'TkAgg']
23
24 -def plotMRI(background=None, background_mask=None, cmap_bg='gray',
25 overlay=None, overlay_mask=None, cmap_overlay='autumn',
26 vlim=(0.0, None), vlim_type=None,
27 do_stretch_colors=False,
28 add_info=True, add_hist=True, add_colorbar=True,
29 fig=None, interactive=None,
30 nrows=None, ncolumns=None
31 ):
32 """Very basic plotting of 3D data with interactive thresholding.
33
34 Background/overlay could be nifti files names or NiftiImage
35 objects, or 3D ndarrays. if no mask provided, only non-0 elements
36 are plotted
37
38 :Parameters:
39 do_stretch_colors : bool
40 Stratch color range to the data (not just to visible data)
41 vlim
42 2 element tuple of low/upper bounds of values to plot
43 vlim_type : None or 'symneg_z'
44 If not None, then vlim would be treated accordingly:
45 symneg_z
46 z-score values of symmetric normal around 0, estimated
47 by symmetrizing negative part of the distribution, which
48 often could be assumed when total distribution is a mixture of
49 by-chance performance normal around 0, and some other in the
50 positive tail
51 ncolumns : int or None
52 Explicit starting number of columns into which position the
53 slice renderings.
54 If None, square arrangement would be used
55 nrows : int or None
56 Explicit starting number of rows into which position the
57 slice renderings.
58 If None, square arrangement would be used
59 add_hist : bool or tuple (int, int)
60 If True, add histogram and position automagically.
61 If a tuple -- use as (row, column)
62 add_info : bool or tuple (int, int)
63 If True, add information and position automagically.
64 If a tuple -- use as (row, column).
65
66 Available colormaps are presented nicely on
67 http://www.scipy.org/Cookbook/Matplotlib/Show_colormaps
68
69 TODO:
70 * Make interface more attractive/usable
71 * allow multiple overlays... or just unify for them all to be just a list of entries
72 * handle cases properly when there is only one - background/overlay
73 """
74
75 if False:
76 impath = '/research/fusion/herrman/be37/fMRI'
77 background = NiftiImage('%s/anat_slices_brain_inbold.nii.gz' % impath)
78 background_mask = None
79 overlay = NiftiImage('/research/fusion/herrman/code/CCe-1.nii.gz')
80 overlay_mask = NiftiImage('%s/masks/example_func_brain_mask.nii.gz' % impath)
81
82 do_stretch_colors = False
83 add_info = True
84 add_hist = True
85 add_colorbar = True
86 cmap_bg = 'gray'
87 cmap_overlay = 'hot'
88
89 fig = None
90
91
92 vlim = [2.3, None]
93 vlim_type = 'symneg_z'
94 interactive = False
95
96
97
98
99 def handle_arg(arg):
100 """Helper which would read in NiftiImage if necessary
101 """
102 if isinstance(arg, basestring):
103 arg = NiftiImage(arg)
104 argshape = arg.data.shape
105
106 if len(argshape)<3:
107 arg.data = arg.data.reshape((1,)*(3-len(argshape)) + argshape)
108 if isinstance(arg, N.ndarray):
109 if len(arg.shape) != 3:
110 raise ValueError, "For now just handling 3D volumes"
111 return arg
112
113 bg = handle_arg(background)
114 if isinstance(bg, NiftiImage):
115
116 fov = (N.array(bg.header['pixdim']) * bg.header['dim'])[3:0:-1]
117
118 aspect = fov[2]/fov[1]
119
120 bg = bg.data[...,::-1,::-1]
121 else:
122 aspect = 1.0
123
124 if bg is not None:
125 bg_mask = handle_arg(background_mask)
126 if isinstance(bg_mask, NiftiImage):
127 bg_mask = bg_mask.data[...,::-1,::-1]
128 if bg_mask is not None:
129 bg_mask = bg_mask != 0
130 else:
131 bg_mask = bg != 0
132
133 func = handle_arg(overlay)
134
135 if func is not None:
136 if isinstance(func, NiftiImage):
137 func = func.data[..., ::-1, :]
138
139 func_mask = handle_arg(overlay_mask)
140 if isinstance(func_mask, NiftiImage):
141 func_mask = func_mask.data[..., ::-1, :]
142 if func_mask is not None:
143 func_mask = func_mask != 0
144 else:
145 func_mask = func != 0
146
147
148
149 vlim = list(vlim)
150 vlim_orig = vlim[:]
151 add_dist2hist = []
152 if isinstance(vlim_type, basestring):
153 if vlim_type == 'symneg_z':
154 func_masked = func[func_mask]
155 fnonpos = func_masked[func_masked<=0]
156 fneg = func_masked[func_masked<0]
157
158 fsym = N.hstack((-fneg, fnonpos))
159 nfsym = len(fsym)
160
161 std = N.sqrt(N.mean(abs(fsym)**2))
162
163 for i,v in enumerate(vlim):
164 if v is not None:
165 vlim[i] = std * v
166
167 add_dist2hist = [(lambda x: nfsym/(N.sqrt(2*N.pi)*std)*N.exp(-(x**2)/(2*std**2)),
168 {})]
169 else:
170 raise ValueError, 'Unknown specification of vlim=%s' % vlim + \
171 ' Known is: symneg'
172
173
174 class Plotter(object):
175 """
176 TODO
177 """
178
179
180
181 def __init__(self, _locals):
182 """TODO"""
183 self._locals = _locals
184 self.fig = _locals['fig']
185
186 def do_plot(self):
187 """TODO"""
188
189 vlim = self._locals['vlim']
190 bg = self._locals['bg']
191 bg_mask = self._locals['bg_mask']
192 ncolumns = self._locals['ncolumns']
193 nrows = self._locals['nrows']
194 add_info = self._locals['add_info']
195 add_hist = self._locals['add_hist']
196
197 if N.isscalar(vlim): vlim = (vlim, None)
198 if vlim[0] is None: vlim = (N.min(func), vlim[1])
199 if vlim[1] is None: vlim = (vlim[0], N.max(func))
200 invert = vlim[1] < vlim[0]
201 if invert:
202 vlim = (vlim[1], vlim[0])
203 print "Not yet fully supported"
204
205
206 if vlim[0] < N.min(func[func_mask]):
207 vlim = list(vlim)
208 vlim[0] = N.min(func[func_mask])
209 vlim = tuple(vlim)
210
211 bound_above = (max(vlim) < N.max(func))
212 bound_below = (min(vlim) > N.min(func))
213
214
215
216 cmap_ = cmap_overlay
217 if not bound_below and bound_above:
218 if cmap_.endswith('_r'):
219 cmap_ = cmap_[:-2]
220 else:
221 cmap_ += '_r'
222
223 func_cmap = eval("P.cm.%s" % cmap_)
224 bg_cmap = eval("P.cm.%s" % cmap_bg)
225
226
227 if do_stretch_colors:
228 clim = (N.min(func), N.max(func))
229 else:
230 clim = vlim
231
232
233
234 extend, thresh_str = {
235 (True, True) : ('both', 'x in [%.3g, %.3g]' % tuple(vlim)),
236 (True, False): ('min', 'x in [%.3g, +inf]' % vlim[0]),
237 (False, True): ('max', 'x in (-inf, %.3g]' % vlim[1]),
238 (False, False): ('neither', 'none') }[(bound_below,
239 bound_above)]
240
241
242
243 dshape = func.shape
244 nslices = func.shape[0]
245
246
247 for v in (add_hist, add_info):
248 if v and not isinstance(v, bool):
249 ncolumns = max(ncolumns, v[1]+1)
250 nrows = max(nrows, v[0]+1)
251
252
253 if ncolumns is None:
254 ncolumns = int(N.sqrt(nslices))
255 ndcolumns = ncolumns
256 nrows = max(nrows, int(N.ceil(nslices*1.0/ncolumns)))
257
258
259
260 nadd = bool(add_info) + bool(add_hist)
261 while ncolumns*nrows - (nslices + nadd) < 0:
262 ncolumns += 1
263
264 locs = ['' for i in xrange(ncolumns*nrows)]
265
266
267 for v,vl in ((add_hist, 'hist'),
268 (add_info, 'info')):
269 if v and not isinstance(v, bool):
270 locs[ncolumns*v[0] + v[1]] = vl
271
272
273 for islice in xrange(nslices):
274 locs[locs.index('')] = islice
275
276
277 if add_hist and isinstance(add_hist, bool):
278 locs[locs.index('')] = 'hist'
279 if add_info and isinstance(add_info, bool):
280 locs[locs.index('')] = 'info'
281
282 print ncolumns, nrows
283 print locs
284
285
286 if P.matplotlib.get_backend() in _interactive_backends:
287 P.ioff()
288
289 if self.fig is None:
290 self.fig = P.figure(facecolor='white', figsize=(4*ncolumns, 4*nrows))
291 else:
292 self.fig.clf()
293 fig = self.fig
294
295
296
297
298 thresholder = lambda x: N.logical_and(x>=vlim[0], x<=vlim[1]) ^ invert
299
300
301
302 self.slices = []
303 for si in range(nslices)[::-1]:
304 ax = fig.add_subplot(nrows, ncolumns,
305 locs.index(si) + 1,
306 frame_on=False)
307 self.slices.append(ax)
308 ax.axison = False
309 slice_bg = bg[si]
310 slice_bg_ = N.ma.masked_array(slice_bg,
311 mask=N.logical_not(bg_mask[si]))
312
313 slice_sl = func[si]
314
315 in_thresh = thresholder(slice_sl)
316 out_thresh = N.logical_not(in_thresh)
317 slice_sl_ = N.ma.masked_array(slice_sl,
318 mask=N.logical_or(out_thresh,
319 N.logical_not(func_mask[si])))
320
321 kwargs = dict(aspect=aspect, origin='lower')
322
323
324
325 im = ax.imshow(N.ones(slice_sl_.shape),
326 cmap=bg_cmap,
327 extent=(0, slice_bg.shape[0],
328 0, slice_bg.shape[1]),
329 **kwargs)
330 im.set_clim((0,1))
331
332
333 ax.imshow(slice_bg_,
334 interpolation='bilinear',
335 cmap=bg_cmap,
336 **kwargs)
337
338 im = ax.imshow(slice_sl_,
339 interpolation='nearest',
340 cmap=func_cmap,
341 alpha=0.8,
342 extent=(0, slice_bg.shape[0],
343 0, slice_bg.shape[1]),
344 **kwargs)
345 im.set_clim(*clim)
346
347 if si == 0:
348 im0 = im
349
350 func_masked = func[func_mask]
351
352
353
354 func_thr = func[N.logical_and(func_mask, thresholder(func))]
355 if add_info and len(func_thr):
356 self.info = ax = fig.add_subplot(nrows, ncolumns,
357 locs.index('info')+1,
358 frame_on=False)
359
360
361 ax.axison = False
362
363
364
365
366 stats = {'v':len(func_masked),
367 'vt': len(func_thr),
368 'm': N.mean(func_masked),
369 'mt': N.mean(func_thr),
370 'min': N.min(func_masked),
371 'mint': N.min(func_thr),
372 'max': N.max(func_masked),
373 'maxt': N.max(func_thr),
374 'mm': N.median(func_masked),
375 'mmt': N.median(func_thr),
376 'std': N.std(func_masked),
377 'stdt': N.std(func_thr),
378 'sthr': thresh_str}
379 P.text(0, 0.5, """
380 Original:
381 voxels = %(v)d
382 range = [%(min).3g, %(max).3g]
383 mean = %(m).3g
384 median = %(mm).3g
385 std = %(std).3g
386
387 Thresholded: %(sthr)s:
388 voxels = %(vt)d
389 range = [%(mint).3g, %(maxt).3g]
390 median = %(mt).3g
391 mean = %(mmt).3g
392 std = %(stdt).3g
393 """ % stats,
394 horizontalalignment='left',
395 verticalalignment='center',
396 transform = ax.transAxes,
397 fontsize=14)
398
399 if add_colorbar:
400 kwargs_cb = {}
401
402
403 self.cb = cb = P.colorbar(
404 im0,
405 shrink=0.8, pad=0.0, drawedges=False,
406 extend=extend, cmap=func_cmap, **kwargs_cb)
407 cb.set_clim(*clim)
408
409
410 if add_hist:
411 self.hist = fig.add_subplot(nrows, ncolumns,
412 locs.index('hist') + 1,
413 frame_on=True)
414
415 minv, maxv = N.min(func_masked), N.max(func_masked)
416 if minv<0 and maxv>0:
417 maxx = max(-minv, maxv)
418 range_ = (-maxx, maxx)
419 else:
420 range_ = (minv, maxv)
421 H = N.histogram(func_masked, range=range_, bins=31)
422 H2 = P.hist(func_masked, bins=H[1], align='center',
423 facecolor='#FFFFFF', hold=True)
424 for a, kwparams in add_dist2hist:
425 dbin = (H[1][1] - H[1][0])
426 P.plot(H2[1], [a(x) * dbin for x in H2[1]], **kwparams)
427 if add_colorbar:
428 cbrgba = cb.to_rgba(H2[1])
429 for face, facecolor, value in zip(H2[2], cbrgba, H2[1]):
430 if not thresholder(value):
431 color = '#FFFFFF'
432 else:
433 color = facecolor
434 face.set_facecolor(color)
435
436
437 fig.subplots_adjust(left=0.01, right=0.95, hspace=0.01)
438 if ncolumns - int(bool(add_info) or bool(add_hist)) < 2:
439 fig.subplots_adjust(wspace=0.4)
440 else:
441 fig.subplots_adjust(wspace=0.1)
442
443 if P.matplotlib.get_backend() in _interactive_backends:
444 P.draw()
445 P.ion()
446
447 def on_click(self, event):
448 """Actions to perform on click
449 """
450 if id(event.inaxes) != id(plotter.hist):
451 return
452 xdata, ydata, button = event.xdata, event.ydata, event.button
453 print xdata, ydata, button
454 vlim = self._locals['vlim']
455 if button == 1:
456 vlim[0] = xdata
457 elif button == 3:
458 vlim[1] = xdata
459 elif button == 2:
460 vlim[0], vlim[1] = vlim[1], vlim[0]
461 self.do_plot()
462
463 plotter = Plotter(locals())
464 plotter.do_plot()
465
466 if interactive is None:
467 interactive = P.matplotlib.get_backend() in _interactive_backends
468
469
470 if interactive:
471
472 P.connect('button_press_event', plotter.on_click)
473 P.show()
474
475 plotter.fig.plotter = plotter
476 return plotter.fig
477