1
# -*- coding: utf-8 -*-
3
# Copyright © 2010-2011 CEA
5
# Licensed under the terms of the CECILL License
6
# (see guiqwt/__init__.py for details)
9
SIFT, the Signal and Image Filtering Tool
10
Simple signal and image processing application based on guiqwt and guidata
13
SHOW = True # Show test in GUI-based test launcher
15
from PyQt4.QtGui import (QMainWindow, QMessageBox, QSplitter, QListWidget,
16
QFileDialog, QVBoxLayout, QHBoxLayout, QWidget,
17
QTabWidget, QMenu, QApplication, QCursor, QFont)
18
from PyQt4.QtCore import Qt, QT_VERSION_STR, PYQT_VERSION_STR, SIGNAL
20
import sys, platform, os.path as osp, os
23
from guidata.dataset.datatypes import DataSet, ValueProp
24
from guidata.dataset.dataitems import (IntItem, FloatArrayItem, StringItem,
25
ChoiceItem, FloatItem, DictItem,
27
from guidata.dataset.qtwidgets import DataSetEditGroupBox
28
from guidata.configtools import get_icon
29
from guidata.qthelpers import create_action, add_actions, get_std_icon
30
from guidata.qtwidgets import DockableWidget, DockableWidgetMixin
31
from guidata.utils import update_dataset
33
from guiqwt.config import _
34
from guiqwt.plot import CurveWidget, ImageWidget
35
from guiqwt.builder import make
36
from guiqwt.io import (imagefile_to_array, IMAGE_LOAD_FILTERS,
37
array_to_imagefile, IMAGE_SAVE_FILTERS)
40
APP_DESC = _("""Signal and Image Filtering Tool<br>
41
Simple signal and image processing application based on guiqwt and guidata""")
46
"""Compute FFT on X,Y data"""
48
x1 = np.fft.fftshift(np.fft.fftfreq(x.shape[-1], d=x[1]-x[0]))
52
"""Compute iFFT on X,Y data"""
54
x1 = np.fft.fftshift(np.fft.fftfreq(x.shape[-1], d=x[1]-x[0]))
57
def flatfield(rawdata, flatdata):
58
"""Compute flat-field correction"""
59
dtemp = np.array(rawdata, dtype=np.float64, copy=True)*flatdata.mean()
60
dunif = np.array(flatdata, dtype=np.float64, copy=True)
61
dunif[dunif == 0] = 1.
62
return np.array(dtemp/dunif, dtype=rawdata.dtype)
65
class SignalParam(DataSet):
66
title = StringItem(_("Title"), default=_("Untitled"))
67
xydata = FloatArrayItem(_("Data"), transpose=True, minmax="rows")
68
def copy_data_from(self, other, dtype=None):
69
self.xydata = np.array(other.xydata, copy=True, dtype=dtype)
70
def change_data_type(self, dtype):
71
self.xydata = np.array(self.xydata, dtype=dtype)
73
if self.xydata is not None:
75
def set_data(self, data):
77
data = property(get_data, set_data)
79
class SignalParamNew(DataSet):
80
title = StringItem(_("Title"), default=_("Untitled"))
81
xmin = FloatItem("Xmin", default=-10.)
82
xmax = FloatItem("Xmax", default=10.)
83
size = IntItem(_("Size"), help=_("Signal size (total number of points)"),
85
type = ChoiceItem(_("Type"),
86
(("rand", _("random")), ("zeros", _("zeros")),
87
("gauss", _("gaussian"))))
90
class ImageParam(DataSet):
91
title = StringItem(_("Title"), default=_("Untitled"))
92
data = FloatArrayItem(_("Data"))
93
metadata = DictItem(_("Metadata"), default=None)
94
def copy_data_from(self, other, dtype=None):
95
self.data = np.array(other.data, copy=True, dtype=dtype)
96
def change_data_type(self, dtype):
97
self.data = np.array(self.data, dtype=dtype)
99
class ImageParamNew(DataSet):
100
title = StringItem(_("Title"), default=_("Untitled"))
101
height = IntItem(_("Height"), help=_("Image height (total number of rows)"),
103
width = IntItem(_("Width"), help=_("Image width (total number of columns)"),
105
dtype = ChoiceItem(_("Data type"),
106
((np.uint8, "uint8"), (np.int16, "uint16"),
107
(np.float32, "float32"), (np.float64, "float64"),
109
type = ChoiceItem(_("Type"),
110
(("zeros", _("zeros")), ("empty", _("empty")),
111
("rand", _("random")),
115
class ObjectFT(QSplitter):
116
"""Object handling the item list, the selected item properties and plot"""
119
def __init__(self, parent, plot):
120
super(ObjectFT, self).__init__(Qt.Vertical, parent)
122
self.objects = [] # signals or images
123
self.items = [] # associated plot items
124
self.listwidget = None
125
self.properties = None
126
self._hsplitter = None
128
self.file_actions = None
129
self.edit_actions = None
130
self.operation_actions = None
131
self.processing_actions = None
135
self.directory = "" # last browsed directory
137
# Object selection dependent actions
138
self.actlist_1more = []
139
self.actlist_2more = []
143
#------Setup widget, menus, actions
144
def setup(self, toolbar):
145
self.listwidget = QListWidget()
146
self.listwidget.setAlternatingRowColors(True)
147
self.listwidget.setSelectionMode(QListWidget.ExtendedSelection)
148
self.properties = DataSetEditGroupBox(_("Properties"), self.PARAMCLASS)
149
self.properties.setEnabled(False)
151
self.connect(self.listwidget, SIGNAL("currentRowChanged(int)"),
152
self.current_item_changed)
153
self.connect(self.listwidget, SIGNAL("itemSelectionChanged()"),
154
self.selection_changed)
155
self.connect(self.properties, SIGNAL("apply_button_clicked()"),
156
self.properties_changed)
158
properties_stretched = QWidget()
159
hlayout = QHBoxLayout()
160
hlayout.addWidget(self.properties)
161
# hlayout.addStretch()
162
vlayout = QVBoxLayout()
163
vlayout.addLayout(hlayout)
165
properties_stretched.setLayout(vlayout)
167
self.addWidget(self.listwidget)
168
self.addWidget(properties_stretched)
171
duplicate_action = create_action(self, _("Duplicate"),
172
icon=get_icon('copy.png'),
173
triggered=self.duplicate_object)
174
self.actlist_1 += [duplicate_action]
175
remove_action = create_action(self, _("Remove"),
176
icon=get_icon('delete.png'),
177
triggered=self.remove_object)
178
self.actlist_1more += [remove_action]
179
self.edit_actions = [duplicate_action, remove_action]
182
sum_action = create_action(self, _("Sum"), triggered=self.compute_sum)
183
average_action = create_action(self, _("Average"),
184
triggered=self.compute_average)
185
diff_action = create_action(self, _("Difference"),
186
triggered=self.compute_difference)
187
prod_action = create_action(self, _("Product"),
188
triggered=self.compute_product)
189
div_action = create_action(self, _("Division"),
190
triggered=self.compute_division)
191
self.actlist_2more += [sum_action, average_action, prod_action]
192
self.actlist_2 += [diff_action, div_action]
193
self.operation_actions = [sum_action, average_action,
194
diff_action, prod_action, div_action]
196
#------GUI refresh/setup
197
def current_item_changed(self, row):
199
update_dataset(self.properties.dataset, self.objects[row])
200
self.properties.get()
202
def _get_selected_rows(self):
203
return [index.row() for index in
204
self.listwidget.selectionModel().selectedRows()]
206
def selection_changed(self):
207
"""Signal list: selection changed"""
208
row = self.listwidget.currentRow()
209
self.properties.setDisabled(row == -1)
211
nbrows = len(self._get_selected_rows())
212
for act in self.actlist_1more:
213
act.setEnabled(nbrows >= 1)
214
for act in self.actlist_2more:
215
act.setEnabled(nbrows >= 2)
216
for act in self.actlist_1:
217
act.setEnabled(nbrows == 1)
218
for act in self.actlist_2:
219
act.setEnabled(nbrows == 2)
221
def make_item(self, row):
222
raise NotImplementedError
224
def update_item(self, row):
225
raise NotImplementedError
227
def refresh_plot(self):
228
for item in self.items:
231
for row in self._get_selected_rows():
232
item = self.items[row]
234
item = self.make_item(row)
235
self.plot.add_item(item)
237
self.update_item(row)
238
self.plot.set_item_visible(item, True)
239
self.plot.set_active_item(item)
240
self.plot.do_autoscale()
242
def refresh_list(self, new_current_row='current'):
243
"""new_current_row: integer, 'first', 'last', 'current'"""
244
row = self.listwidget.currentRow()
245
self.listwidget.clear()
246
self.listwidget.addItems(["%s%03d: %s" % (self.PREFIX, i, obj.title)
247
for i, obj in enumerate(self.objects)])
248
if new_current_row == 'first':
250
elif new_current_row == 'last':
251
row = self.listwidget.count()-1
252
elif isinstance(new_current_row, int):
253
row = new_current_row
255
assert new_current_row == 'current'
256
if row < self.listwidget.count():
257
self.listwidget.setCurrentRow(row)
259
def properties_changed(self):
260
"""The properties 'Apply' button was clicked: updating signal"""
261
row = self.listwidget.currentRow()
262
update_dataset(self.objects[row], self.properties.dataset)
263
self.refresh_list(new_current_row='current')
264
self.listwidget.setCurrentRow(row)
267
def add_object(self, obj):
268
self.objects.append(obj)
269
self.items.append(None)
270
self.refresh_list(new_current_row='last')
271
self.listwidget.setCurrentRow(len(self.objects)-1)
272
self.emit(SIGNAL('object_added()'))
274
#------Edit operations
275
def duplicate_object(self):
276
row = self._get_selected_rows()[0]
277
obj = self.objects[row]
278
objcopy = self.PARAMCLASS()
279
objcopy.title = obj.title
280
objcopy.copy_data_from(obj)
281
self.objects.insert(row+1, objcopy)
282
self.items.insert(row+1, None)
283
self.refresh_list(new_current_row=row+1)
286
def remove_object(self):
287
rows = sorted(self._get_selected_rows(), reverse=True)
289
self.objects.pop(row)
290
item = self.items.pop(row)
291
self.plot.del_item(item)
292
self.refresh_list(new_current_row='first')
296
def compute_sum(self):
297
rows = self._get_selected_rows()
298
sumobj = self.PARAMCLASS()
299
sumobj.title = "+".join(["%s%03d" % (self.PREFIX, row) for row in rows])
302
obj = self.objects[row]
303
if sumobj.data is None:
304
sumobj.copy_data_from(obj)
306
sumobj.data += obj.data
307
except Exception, msg:
309
traceback.print_exc()
310
QMessageBox.critical(self.parent(), APP_NAME,
311
_(u"Error:")+"\n%s" % str(msg))
313
self.add_object(sumobj)
315
def compute_average(self):
316
rows = self._get_selected_rows()
317
sumobj = self.PARAMCLASS()
318
title = ", ".join(["%s%03d" % (self.PREFIX, row) for row in rows])
319
sumobj.title = _("Average")+("(%s)" % title)
320
original_dtype = self.objects[rows[0]].data.dtype
323
obj = self.objects[row]
324
if sumobj.data is None:
325
sumobj.copy_data_from(obj, dtype=np.float64)
327
sumobj.data += obj.data
328
except Exception, msg:
330
traceback.print_exc()
331
QMessageBox.critical(self.parent(), APP_NAME,
332
_(u"Error:")+"\n%s" % str(msg))
334
sumobj.data /= float(len(rows))
335
sumobj.change_data_type(dtype=original_dtype)
336
self.add_object(sumobj)
338
def compute_product(self):
339
rows = self._get_selected_rows()
340
sumobj = self.PARAMCLASS()
341
sumobj.title = "*".join(["%s%03d" % (self.PREFIX, row) for row in rows])
344
obj = self.objects[row]
345
if sumobj.data is None:
346
sumobj.copy_data_from(obj)
348
sumobj.data *= obj.data
349
except Exception, msg:
351
traceback.print_exc()
352
QMessageBox.critical(self.parent(), APP_NAME,
353
_(u"Error:")+"\n%s" % str(msg))
355
self.add_object(sumobj)
357
def compute_difference(self):
358
rows = self._get_selected_rows()
359
diffobj = self.PARAMCLASS()
360
diffobj.title = "-".join(["%s%03d" % (self.PREFIX, row)
363
obj0, obj1 = self.objects[rows[0]], self.objects[rows[1]]
364
diffobj.copy_data_from(obj0)
365
diffobj.data = obj1.data-obj0.data
366
except Exception, msg:
368
traceback.print_exc()
369
QMessageBox.critical(self.parent(), APP_NAME,
370
_(u"Error:")+"\n%s" % str(msg))
372
self.add_object(diffobj)
374
def compute_division(self):
375
rows = self._get_selected_rows()
376
diffobj = self.PARAMCLASS()
377
diffobj.title = "/".join(["%s%03d" % (self.PREFIX, row)
380
obj0, obj1 = self.objects[rows[0]], self.objects[rows[1]]
381
diffobj.copy_data_from(obj0)
382
diffobj.data = obj1.data/obj0.data
383
except Exception, msg:
385
traceback.print_exc()
386
QMessageBox.critical(self.parent(), APP_NAME,
387
_(u"Error:")+"\n%s" % str(msg))
389
self.add_object(diffobj)
391
#------Data Processing
392
def apply_11_func(self, obj, orig, func, param):
394
obj.data = func(orig.data)
396
obj.data = func(orig.data, param)
398
def compute_11(self, name, func, param=None, one_param_for_all=True,
400
if param is not None and one_param_for_all:
401
if not param.edit(parent=self.parent()):
403
rows = self._get_selected_rows()
405
if param is not None and not one_param_for_all:
406
if not param.edit(parent=self.parent()):
408
orig = self.objects[row]
409
obj = self.PARAMCLASS()
410
obj.title = "%s(%s%03d)" % (name, self.PREFIX, row)
411
if suffix is not None:
412
obj.title += "|"+suffix(param)
413
obj.copy_data_from(orig)
414
self.emit(SIGNAL("status_message(QString)"),
415
_("Computing:")+" "+obj.title)
416
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
419
self.apply_11_func(obj, orig, func, param)
420
except Exception, msg:
422
traceback.print_exc()
423
QMessageBox.critical(self.parent(), APP_NAME,
424
_(u"Error:")+"\n%s" % str(msg))
427
self.emit(SIGNAL("status_message(QString)"), "")
428
QApplication.restoreOverrideCursor()
431
class SignalFT(ObjectFT):
432
PARAMCLASS = SignalParam
435
def setup(self, toolbar):
436
super(SignalFT, self).setup(toolbar)
439
new_action = create_action(self, _("New signal..."),
440
icon=get_icon('filenew.png'),
441
tip=_("Create a new signal"),
442
triggered=self.new_signal)
443
open_action = create_action(self, _("Open signal..."),
444
icon=get_icon('fileopen.png'),
445
tip=_("Open a signal"),
446
triggered=self.open_signal)
447
save_action = create_action(self, _("Save signal..."),
448
icon=get_icon('filesave.png'),
449
tip=_("Save selected signal"),
450
triggered=self.save_signal)
451
self.actlist_1more += [save_action]
452
self.file_actions = [new_action, open_action, save_action]
455
gaussian_action = create_action(self, _("Gaussian filter"),
456
triggered=self.compute_gaussian)
457
wiener_action = create_action(self, _("Wiener filter"),
458
triggered=self.compute_wiener)
459
fft_action = create_action(self, _("FFT"),
460
tip=_("Warning: only real part is plotted"),
461
triggered=self.compute_fft)
462
ifft_action = create_action(self, _("Inverse FFT"),
463
tip=_("Warning: only real part is plotted"),
464
triggered=self.compute_ifft)
465
self.actlist_1more += [gaussian_action, wiener_action,
466
fft_action, ifft_action]
467
self.processing_actions = [gaussian_action, wiener_action, fft_action,
470
add_actions(toolbar, [new_action, open_action, save_action])
472
def make_item(self, row):
473
signal = self.objects[row]
475
item = make.mcurve(x, y.real, label=signal.title)
476
self.items[row] = item
479
def update_item(self, row):
480
signal = self.objects[row]
482
item = self.items[row]
483
item.set_data(x, y.real)
484
item.curveparam.label = signal.title
486
#------Signal Processing
487
def apply_11_func(self, obj, orig, func, param):
488
xor, yor = orig.xydata
490
obj.xydata = func(xor, yor)
492
obj.xydata = func(xor, yor, param)
494
def compute_wiener(self):
495
import scipy.signal as sps
497
return x, sps.wiener(y)
498
self.compute_11("WienerFilter", func)
500
def compute_gaussian(self):
501
class GaussianParam(DataSet):
502
sigma = FloatItem(u"σ", default=1.)
503
param = GaussianParam(_("Gaussian filter"))
504
import scipy.ndimage as spi
506
return x, spi.gaussian_filter1d(y, p.sigma)
507
self.compute_11("GaussianFilter", func, param,
508
suffix=lambda p: u"σ=%.3f pixels" % p.sigma)
510
def compute_fft(self):
511
self.compute_11("FFT", xy_fft)
513
def compute_ifft(self):
514
self.compute_11("iFFT", xy_ifft)
517
def new_signal(self):
518
"""Create a new signal"""
519
signalnew = SignalParamNew(title=_("Create a new signal"))
520
rows = self._get_selected_rows()
522
signalnew.size = len(self.objects[rows[-1]].data)
523
signalnew.title = "%s %d" % (signalnew.title, self.number+1)
524
if not signalnew.edit(parent=self.parent()):
527
signal = SignalParam()
528
signal.title = signalnew.title
529
xarr = np.linspace(signalnew.xmin, signalnew.xmax, signalnew.size)
530
if signalnew.type == 'zeros':
531
signal.xydata = np.vstack((xarr, np.zeros(signalnew.size)))
532
elif signalnew.type == 'rand':
533
signal.xydata = np.vstack((xarr, np.random.rand(signalnew.size)-.5))
534
elif signalnew.type == 'gauss':
535
class GaussParam(DataSet):
536
a = FloatItem("Norm", default=1.)
537
x0 = FloatItem("X0", default=0.0)
538
sigma = FloatItem(u"σ", default=5.)
539
param = GaussParam(_("New gaussian function"))
540
if not param.edit(parent=self.parent()):
542
ygauss = param.a*np.exp(-.5*((xarr-param.x0)/param.sigma)**2)
543
signal.xydata = np.vstack((xarr, ygauss))
544
self.add_object(signal)
546
def open_signal(self):
547
"""Open signal file"""
548
saved_in, saved_out, saved_err = sys.stdin, sys.stdout, sys.stderr
550
filters = '%s (*.txt *.csv)\n%s (*.npy)'\
551
% (_(u"Text files"), _(u"NumPy arrays"))
552
filenames = QFileDialog.getOpenFileNames(self.parent(), _("Open"),
553
self.directory, filters)
554
sys.stdin, sys.stdout, sys.stderr = saved_in, saved_out, saved_err
555
filenames = list(filenames)
556
for filename in filenames:
557
filename = unicode(filename)
558
self.directory = osp.dirname(filename)
559
signal = SignalParam()
560
signal.title = filename
562
if osp.splitext(filename)[1] == ".npy":
563
xydata =np.load(filename)
565
for delimiter in ('\t', ',', ' ', ';'):
567
xydata = np.loadtxt(filename, delimiter=delimiter)
573
except Exception, msg:
575
traceback.print_exc()
576
QMessageBox.critical(self.parent(), APP_NAME,
577
(_(u"%s could not be opened:") % osp.basename(filename))+\
580
if len(xydata.shape) == 1:
581
xydata = np.vstack( (np.arange(xydata.size), xydata) )
582
elif len(xydata.shape) == 2:
583
rows, cols = xydata.shape
584
if cols == 2 and rows > 2:
586
signal.xydata = xydata
587
self.add_object(signal)
589
def save_signal(self):
590
"""Save selected signal"""
591
rows = self._get_selected_rows()
593
filename = QFileDialog.getSaveFileName(self, _("Save as"),
594
self.directory, _(u"CSV files")+" (*.csv)")
597
filename = unicode(filename)
598
self.directory = osp.dirname(filename)
599
obj = self.objects[row]
601
np.savetxt(filename, obj.xydata, delimiter=',')
602
except Exception, msg:
604
traceback.print_exc()
605
QMessageBox.critical(self.parent(), APP_NAME,
606
(_(u"%s could not be written:") % osp.basename(filename))+\
610
class ImageFT(ObjectFT):
611
PARAMCLASS = ImageParam
614
def setup(self, toolbar):
615
super(ImageFT, self).setup(toolbar)
618
new_action = create_action(self, _("New image..."),
619
icon=get_icon('filenew.png'),
620
tip=_("Create a new image"),
621
triggered=self.new_image)
622
open_action = create_action(self, _("Open image..."),
623
icon=get_icon('fileopen.png'),
624
tip=_("Open an image"),
625
triggered=self.open_image)
626
save_action = create_action(self, _("Save image..."),
627
icon=get_icon('filesave.png'),
628
tip=_("Save selected image"),
629
triggered=self.save_image)
630
self.actlist_1more += [save_action]
631
self.file_actions = [new_action, open_action, save_action]
634
rotate_menu = QMenu(_("Rotation"), self)
635
hflip_action = create_action(self, _("Flip horizontally"),
636
triggered=self.flip_horizontally)
637
vflip_action = create_action(self, _("Flip vertically"),
638
triggered=self.flip_vertically)
639
rot90_action = create_action(self, _("Rotate 90° right"),
640
triggered=self.rotate_270)
641
rot270_action = create_action(self, _("Rotate 90° left"),
642
triggered=self.rotate_90)
643
rotate_action = create_action(self, _("Rotate arbitrarily..."),
644
triggered=self.rotate_arbitrarily)
645
resize_action = create_action(self, _("Resize"),
646
triggered=self.resize_image)
647
crop_action = create_action(self, _("Crop"), triggered=self.crop_image)
648
flatfield_action = create_action(self, _("Flat-field correction"),
649
triggered=self.flat_field_correction)
650
self.actlist_1 += [resize_action]
651
self.actlist_2 += [flatfield_action]
652
self.actlist_1more += [crop_action, hflip_action, vflip_action,
653
rot90_action, rot270_action, rotate_action]
654
add_actions(rotate_menu, [hflip_action, vflip_action,
655
rot90_action, rot270_action, rotate_action])
656
self.operation_actions += [None, rotate_menu, None,
657
resize_action, crop_action,
658
None, flatfield_action]
661
threshold_action = create_action(self, _("Thresholding"),
662
triggered=self.compute_threshold)
663
clip_action = create_action(self, _("Clipping"),
664
triggered=self.compute_clip)
665
gaussian_action = create_action(self, _("Gaussian filter"),
666
triggered=self.compute_gaussian)
667
wiener_action = create_action(self, _("Wiener filter"),
668
triggered=self.compute_wiener)
669
fft_action = create_action(self, _("FFT"),
670
tip=_("Warning: only real part is plotted"),
671
triggered=self.compute_fft)
672
ifft_action = create_action(self, _("Inverse FFT"),
673
tip=_("Warning: only real part is plotted"),
674
triggered=self.compute_ifft)
675
self.actlist_1more += [threshold_action, clip_action,
676
gaussian_action, wiener_action,
677
fft_action, ifft_action]
678
self.processing_actions = [threshold_action, clip_action, None,
679
gaussian_action, wiener_action, fft_action,
682
add_actions(toolbar, [new_action, open_action, save_action])
684
def make_item(self, row):
685
image = self.objects[row]
686
item = make.image(image.data.real, title=image.title, colormap='gray',
687
eliminate_outliers=2.)
688
self.items[row] = item
691
def update_item(self, row):
692
image = self.objects[row]
693
item = self.items[row]
694
lut_range = [item.min, item.max]
695
item.set_data(image.data.real, lut_range=lut_range)
696
item.imageparam.label = image.title
697
item.plot().update_colormap_axis(item)
699
#------Image operations
700
def rotate_arbitrarily(self):
701
boundaries = ('constant', 'nearest', 'reflect', 'wrap')
702
prop = ValueProp(False)
703
class RotateParam(DataSet):
704
angle = FloatItem(u"%s (°)" % _(u"Angle"))
705
mode = ChoiceItem(_(u"Mode"), zip(boundaries, boundaries),
706
default=boundaries[0])
707
cval = FloatItem(_("cval"), default=0.,
708
help=_(u"Value used for points outside the "
709
u"boundaries of the input if mode is "
711
prefilter = BoolItem(_(u"Prefilter the input image"),
712
default=True).set_prop("display", store=prop)
713
order = IntItem(_(u"Order"), default=3, min=0, max=5,
714
help=_("Spline interpolation order")
715
).set_prop("display", active=prop)
716
param = RotateParam(_("Rotation"))
717
import scipy.ndimage as spi
718
self.compute_11("Rotate",
720
spi.rotate(x, p.angle, order=p.order, mode=p.mode,
721
cval=p.cval, prefilter=p.prefilter),
722
param, suffix=lambda p: u"α=%.3f°, mode='%s'"\
726
self.compute_11("Rotate90", lambda x: np.rot90(x))
728
def rotate_270(self):
729
self.compute_11("Rotate270", lambda x: np.rot90(x, 3))
731
def flip_horizontally(self):
732
self.compute_11("HFlip", lambda x: np.fliplr(x))
734
def flip_vertically(self):
735
self.compute_11("VFlip", lambda x: np.flipud(x))
737
def resize_image(self):
738
rows = self._get_selected_rows()
739
obj = self.objects[rows[0]]
740
original_size = obj.data.shape[1], obj.data.shape[0]
741
from guiqwt.resizedialog import ResizeDialog
742
dlg = ResizeDialog(self.plot, new_size=original_size,
743
old_size=original_size,
744
text=_("Destination size:"))
747
boundaries = ('constant', 'nearest', 'reflect', 'wrap')
748
prop = ValueProp(False)
749
class ResizeParam(DataSet):
750
zoom = FloatItem(_(u"Zoom"), default=dlg.get_zoom())
751
mode = ChoiceItem(_(u"Mode"), zip(boundaries, boundaries),
752
default=boundaries[0])
753
cval = FloatItem(_("cval"), default=0.,
754
help=_(u"Value used for points outside the "
755
u"boundaries of the input if mode is "
757
prefilter = BoolItem(_(u"Prefilter the input image"),
758
default=True).set_prop("display", store=prop)
759
order = IntItem(_(u"Order"), default=3, min=0, max=5,
760
help=_("Spline interpolation order")
761
).set_prop("display", active=prop)
762
param = ResizeParam(_("Resize"))
763
import scipy.ndimage as spi
764
self.compute_11("Zoom", lambda x, p:
765
spi.interpolation.zoom(x, p.zoom, order=p.order,
766
mode=p.mode, cval=p.cval,
767
prefilter=p.prefilter),
768
param, suffix=lambda p: u"zoom=%.3f" % p.zoom)
770
def crop_image(self):
771
class CropParam(DataSet):
772
row1 = IntItem(_("First row index"), default=0, min=-1)
773
row2 = IntItem(_("Last row index"), default=-1, min=-1)
774
col1 = IntItem(_("First column index"), default=0, min=-1)
775
col2 = IntItem(_("Last column index"), default=-1, min=-1)
776
param = CropParam(_("Crop"))
777
self.compute_11("Crop", lambda x, p:
778
x.copy()[p.row1:p.row2, p.col1:p.col2],
779
param, suffix=lambda p: u"roi=%d:%d,%d:%d"
780
% (p.row1, p.row2, p.col1, p.col2))
782
def flat_field_correction(self):
783
rows = self._get_selected_rows()
784
robj = self.PARAMCLASS()
785
robj.title = "FlatField("+(','.join(["%s%03d" % (self.PREFIX, row)
786
for row in rows]))+")"
788
robj.data = flatfield(self.objects[rows[0]].data,
789
self.objects[rows[1]].data)
790
except Exception, msg:
792
traceback.print_exc()
793
QMessageBox.critical(self.parent(), APP_NAME,
794
_(u"Error:")+"\n%s" % str(msg))
796
self.add_object(robj)
798
#------Image Processing
799
def compute_threshold(self):
800
class ThresholdParam(DataSet):
801
value = FloatItem(_(u"Threshold"))
802
self.compute_11("Threshold", lambda x, p: np.clip(x, p.value, x.max()),
803
ThresholdParam(_("Thresholding")),
804
suffix=lambda p: u"min=%s lsb" % p.value)
806
def compute_clip(self):
807
class ClipParam(DataSet):
808
value = FloatItem(_(u"Clipping value"))
809
self.compute_11("Clip", lambda x, p: np.clip(x, x.min(), p.value),
810
ClipParam(_("Clipping")),
811
suffix=lambda p: u"max=%s lsb" % p.value)
813
def compute_wiener(self):
814
import scipy.signal as sps
815
self.compute_11("WienerFilter", sps.wiener)
817
def compute_gaussian(self):
818
class GaussianParam(DataSet):
819
sigma = FloatItem(u"σ", default=1.)
820
param = GaussianParam(_("Gaussian filter"))
821
import scipy.ndimage as spi
822
self.compute_11("GaussianFilter",
823
lambda x, p: spi.gaussian_filter(x, p.sigma), param,
824
suffix=lambda p: u"σ=%.3f pixels" % p.sigma)
826
def compute_fft(self):
827
self.compute_11("FFT", np.fft.fft2)
829
def compute_ifft(self):
830
self.compute_11("iFFT", np.fft.ifft2)
834
"""Create a new image"""
835
imagenew = ImageParamNew(title=_("Create a new image"))
836
rows = self._get_selected_rows()
838
data = self.objects[rows[-1]].data
839
imagenew.width = data.shape[1]
840
imagenew.height = data.shape[0]
841
imagenew.title = "%s %d" % (imagenew.title, self.number+1)
842
if not imagenew.edit(parent=self.parent()):
846
image.title = imagenew.title
847
shape = (imagenew.height, imagenew.width)
848
dtype = imagenew.dtype
849
if imagenew.type == 'zeros':
850
image.data = np.zeros(shape, dtype=dtype)
851
elif imagenew.type == 'empty':
852
image.data = np.empty(shape, dtype=dtype)
853
elif imagenew.type == 'rand':
854
data = np.random.rand(*shape)
855
from guiqwt.io import set_dynamic_range_from_dtype
856
image.data = set_dynamic_range_from_dtype(data, dtype)
857
self.add_object(image)
859
def open_image(self):
860
"""Open image file"""
861
saved_in, saved_out, saved_err = sys.stdin, sys.stdout, sys.stderr
863
filenames = QFileDialog.getOpenFileNames(self.parent(), _("Open"),
864
self.directory, IMAGE_LOAD_FILTERS)
865
sys.stdin, sys.stdout, sys.stderr = saved_in, saved_out, saved_err
866
filenames = list(filenames)
867
for filename in filenames:
868
filename = unicode(filename)
869
self.directory = osp.dirname(filename)
871
image.title = filename
873
image.data = imagefile_to_array(filename, to_grayscale=True)
874
if osp.splitext(filename)[1].lower() == ".dcm":
876
dcm = dicom.read_file(filename, stop_before_pixels=True)
878
for attr_str in dir(dcm):
879
if attr_str != 'GroupLength':
880
image.metadata[attr_str] = getattr(dcm, attr_str)
881
except Exception, msg:
883
traceback.print_exc()
884
QMessageBox.critical(self.parent(), APP_NAME,
885
(_(u"%s could not be opened:") % osp.basename(filename))+\
888
self.add_object(image)
890
def save_image(self):
891
"""Save selected image"""
892
rows = self._get_selected_rows()
894
filename = QFileDialog.getSaveFileName(self, _("Save as"),
895
self.directory, IMAGE_SAVE_FILTERS)
898
filename = unicode(filename)
899
self.directory = osp.dirname(filename)
900
obj = self.objects[row]
902
array_to_imagefile(obj.data, filename)
903
except Exception, msg:
905
traceback.print_exc()
906
QMessageBox.critical(self.parent(), APP_NAME,
907
(_(u"%s could not be written:") % osp.basename(filename))+\
912
class DockablePlotWidget(DockableWidget):
913
LOCATION = Qt.RightDockWidgetArea
914
def __init__(self, parent, plotwidgetclass, toolbar):
915
super(DockablePlotWidget, self).__init__(parent)
916
self.toolbar = toolbar
917
layout = QVBoxLayout()
918
self.plotwidget = plotwidgetclass()
919
layout.addWidget(self.plotwidget)
920
self.setLayout(layout)
924
return self.plotwidget.plot
927
title = unicode(self.toolbar.windowTitle())
928
self.plotwidget.add_toolbar(self.toolbar, title)
929
if isinstance(self.plotwidget, ImageWidget):
930
self.plotwidget.register_all_image_tools()
932
self.plotwidget.register_all_curve_tools()
934
#------DockableWidget API
935
def visibility_changed(self, enable):
936
"""DockWidget visibility has changed"""
937
super(DockablePlotWidget, self).visibility_changed(enable)
938
self.toolbar.setVisible(enable)
941
class DockableTabWidget(QTabWidget, DockableWidgetMixin):
942
LOCATION = Qt.LeftDockWidgetArea
943
def __init__(self, parent):
944
QTabWidget.__init__(self, parent)
945
DockableWidgetMixin.__init__(self, parent)
949
from spyderlib.widgets.internalshell import InternalShell
951
class DockableConsole(InternalShell, DockableWidgetMixin):
952
LOCATION = Qt.BottomDockWidgetArea
953
def __init__(self, parent, namespace, message, commands=[]):
954
InternalShell.__init__(self, namespace=namespace, message=message,
956
DockableWidgetMixin.__init__(self, parent)
960
font = QFont("Courier new")
961
font.setPointSize(10)
963
self.set_codecompletion_auto(True)
964
self.set_calltips(True)
965
self.setup_calltips(size=600, font=font)
966
self.setup_completion(size=(300, 180), font=font)
969
DockableConsole = None
972
class SiftProxy(object):
973
def __init__(self, win):
975
self.s = self.win.signalft.objects
976
self.i = self.win.signalft.objects
979
class MainWindow(QMainWindow):
981
QMainWindow.__init__(self)
983
self.setWindowIcon(get_icon('sift.svg'))
984
self.setWindowTitle(APP_NAME)
986
# Welcome message in statusbar:
987
status = self.statusBar()
988
status.showMessage(_("Welcome to %s!") % APP_NAME, 5000)
990
self.signal_toolbar = self.addToolBar(_("Signal Processing Toolbar"))
991
self.image_toolbar = self.addToolBar(_("Image Processing Toolbar"))
994
curveplot_toolbar = self.addToolBar(_("Curve Plotting Toolbar"))
995
self.curvewidget = DockablePlotWidget(self, CurveWidget,
997
curveplot = self.curvewidget.get_plot()
998
curveplot.add_item(make.legend("TR"))
999
self.signalft = SignalFT(self, plot=curveplot)
1000
self.signalft.setup(self.signal_toolbar)
1003
imagevis_toolbar = self.addToolBar(_("Image Visualization Toolbar"))
1004
self.imagewidget = DockablePlotWidget(self, ImageWidget,
1006
self.imageft = ImageFT(self, self.imagewidget.get_plot())
1007
self.imageft.setup(self.image_toolbar)
1009
for objectft in (self.signalft, self.imageft):
1010
self.connect(objectft, SIGNAL("status_message(QString)"),
1013
# Main window widgets
1014
self.tabwidget = DockableTabWidget(self)
1015
self.tabwidget.setMaximumWidth(500)
1016
self.tabwidget.addTab(self.signalft, get_icon('curve.png'),
1018
self.tabwidget.addTab(self.imageft, get_icon('image.png'),
1020
self.add_dockwidget(self.tabwidget, _(u"Main panel"))
1021
# self.setCentralWidget(self.tabwidget)
1022
self.curve_dock = self.add_dockwidget(self.curvewidget,
1023
title=_("Curve plotting panel"))
1024
self.image_dock = self.add_dockwidget(self.imagewidget,
1025
title=_("Image visualization panel"))
1026
self.tabifyDockWidget(self.curve_dock, self.image_dock)
1027
self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
1028
self.tab_index_changed)
1029
self.connect(self.signalft, SIGNAL('object_added()'),
1030
lambda: self.tabwidget.setCurrentIndex(0))
1031
self.connect(self.imageft, SIGNAL('object_added()'),
1032
lambda: self.tabwidget.setCurrentIndex(1))
1035
self.quit_action = create_action(self, _("Quit"), shortcut="Ctrl+Q",
1036
icon=get_std_icon("DialogCloseButton"),
1037
tip=_("Quit application"),
1038
triggered=self.close)
1039
self.file_menu = self.menuBar().addMenu(_("File"))
1040
self.connect(self.file_menu, SIGNAL("aboutToShow()"),
1041
self.update_file_menu)
1044
self.edit_menu = self.menuBar().addMenu(_("&Edit"))
1045
self.connect(self.edit_menu, SIGNAL("aboutToShow()"),
1046
self.update_edit_menu)
1049
self.operation_menu = self.menuBar().addMenu(_("Operations"))
1050
self.connect(self.operation_menu, SIGNAL("aboutToShow()"),
1051
self.update_operation_menu)
1054
self.proc_menu = self.menuBar().addMenu(_("Processing"))
1055
self.connect(self.proc_menu, SIGNAL("aboutToShow()"),
1056
self.update_proc_menu)
1059
self.view_menu = view_menu = self.createPopupMenu()
1060
view_menu.setTitle(_(u"&View"))
1061
self.menuBar().addMenu(view_menu)
1064
help_menu = self.menuBar().addMenu("?")
1065
about_action = create_action(self, _("About..."),
1066
icon=get_std_icon('MessageBoxInformation'),
1067
triggered=self.about)
1068
add_actions(help_menu, (about_action,))
1070
# Eventually add an internal console (requires 'spyderlib')
1071
self.sift_proxy = SiftProxy(self)
1072
if DockableConsole is None:
1075
import time, scipy.signal as sps, scipy.ndimage as spi
1076
ns = {'sift': self.sift_proxy,
1077
'np': np, 'sps': sps, 'spi': spi,
1078
'os': os, 'sys': sys, 'osp': osp, 'time': time}
1079
msg = "Example: sift.s[0] returns signal object #0\n"\
1080
"Modules imported at startup: "\
1081
"os, sys, os.path as osp, time, "\
1082
"numpy as np, scipy.signal as sps, scipy.ndimage as spi"
1083
self.console = DockableConsole(self, namespace=ns, message=msg)
1084
self.add_dockwidget(self.console, _(u"Console"))
1085
self.connect(self.console.interpreter.widget_proxy,
1086
SIGNAL("new_prompt(QString)"),
1087
lambda txt: self.refresh_lists())
1089
# Update selection dependent actions
1090
self.update_actions()
1092
# Show main window and raise the signal plot panel
1094
self.curve_dock.raise_()
1096
#------GUI refresh/setup
1097
def add_dockwidget(self, child, title):
1098
"""Add QDockWidget and toggleViewAction"""
1099
dockwidget, location = child.create_dockwidget(title)
1100
self.addDockWidget(location, dockwidget)
1103
def refresh_lists(self):
1104
self.signalft.refresh_list()
1105
self.imageft.refresh_list()
1107
def update_actions(self):
1108
self.signalft.selection_changed()
1109
self.imageft.selection_changed()
1110
is_signal = self.tabwidget.currentWidget() is self.signalft
1111
self.signal_toolbar.setVisible(is_signal)
1112
self.image_toolbar.setVisible(not is_signal)
1114
def tab_index_changed(self, index):
1115
dock = (self.curve_dock, self.image_dock)[index]
1117
self.update_actions()
1119
def update_file_menu(self):
1120
self.file_menu.clear()
1121
objectft = self.tabwidget.currentWidget()
1122
actions = objectft.file_actions+[None, self.quit_action]
1123
add_actions(self.file_menu, actions)
1125
def update_edit_menu(self):
1126
self.edit_menu.clear()
1127
objectft = self.tabwidget.currentWidget()
1128
add_actions(self.edit_menu, objectft.edit_actions)
1130
def update_operation_menu(self):
1131
self.operation_menu.clear()
1132
objectft = self.tabwidget.currentWidget()
1133
add_actions(self.operation_menu, objectft.operation_actions)
1135
def update_proc_menu(self):
1136
self.proc_menu.clear()
1137
objectft = self.tabwidget.currentWidget()
1138
add_actions(self.proc_menu, objectft.processing_actions)
1142
QMessageBox.about( self, _("About ")+APP_NAME,
1143
"""<b>%s</b> v%s<br>%s<p>%s Pierre Raybaut
1144
<br>Copyright © 2010 CEA
1145
<p>Python %s, Qt %s, PyQt %s %s %s""" % \
1146
(APP_NAME, VERSION, APP_DESC, _("Developped by"),
1147
platform.python_version(),
1148
QT_VERSION_STR, PYQT_VERSION_STR, _("on"), platform.system()) )
1150
def closeEvent(self, event):
1151
if self.console is not None:
1152
self.console.exit_interpreter()
1157
from guidata import qapplication
1158
app = qapplication()
1159
window = MainWindow()
1164
if __name__ == '__main__':
b'\\ No newline at end of file'