def _new_roi(self): dialog = QtGui.QDialog(self) dialog.setWindowTitle("New ROI") vbox = QtGui.QVBoxLayout() dialog.setLayout(vbox) optbox = OptionBox() optbox.add("ROI name", TextOption(), key="name") optbox.add("Data space from", DataOption(self.ivm), key="grid") vbox.addWidget(optbox) buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) vbox.addWidget(buttons) ok = dialog.exec_() if ok: roiname = optbox.option("name").value grid = self.ivm.data[optbox.option("grid").value].grid roidata = np.zeros(grid.shape, dtype=np.int) self.ivm.add(NumpyData(roidata, grid=grid, roi=True, name=roiname), make_current=True) # Throw away old history. FIXME is this right, should we keep existing data and history? # Also should we cache old history in case we go back to this ROI? self._history = [] self._undo_btn.setEnabled(False) self.options.option("roi").value = roiname
class CheckerboardModelView: """ View for CheckerboardModel """ def __init__(self, ivm): self.model = CheckerboardModel(ivm) self.gui = OptionBox() self.gui.add("Number of voxels per patch (approx)", NumericOption(minval=1, maxval=1000, default=20, intonly=True), key="voxels-per-patch") self.gui.sig_changed.connect(self._update_options) def _update_options(self): self.model.options.update(self.gui.values())
class FastStructureModelView: """ View for FastStructureModel """ def __init__(self, ivm): self.model = FastStructureModel(ivm) self.gui = OptionBox() self.gui.add("Structural image (brain extracted)", DataOption(self.model._ivm, explicit=True), key="struc") self.gui.add("Image type", ChoiceOption(["T1 weighted", "T2 weighted", "Proton Density"], return_values=[1, 2, 3]), key="type") self.gui.sig_changed.connect(self._update_options) def _update_options(self): self.model.options.update(self.gui.values())
class ApplyTransform(QpWidget): """ Widget for applying previously calculated transformations """ def __init__(self, **kwargs): super(ApplyTransform, self).__init__(name="Apply Transform", icon="reg", desc="Apply previously calculated transformations", group="Registration", **kwargs) self.reg_methods = [] for method in get_plugins("reg-methods"): try: self.reg_methods.append(method(self.ivm)) except: traceback.print_exc() self.warn("Failed to create registration method: %s", method) def init_ui(self): layout = QtGui.QVBoxLayout() self.setLayout(layout) title = TitleWidget(self, help="reg") layout.addWidget(title) if not self.reg_methods: layout.addWidget(QtGui.QLabel("No registration methods found")) layout.addStretch(1) return self.options = OptionBox("General Options") self.options.add("Transform", TransformOption(self.ivm), key="transform") self.options.add("Apply to data", DataOption(self.ivm), key="data") self.options.add("Interpolation", ChoiceOption(["Nearest neighbour", "Linear", "Spline"], [0, 1, 3], default=1), key="interp-order") self.options.add("Output name", OutputNameOption(src_data=self.options.option("data"), suffix="_reg"), key="output-name") self.options.option("transform").sig_changed.connect(self._transform_changed) layout.addWidget(self.options) self.details = TransformDetails() layout.addWidget(self.details) layout.addWidget(RunButton(self)) layout.addStretch(1) self._transform_changed() def processes(self): return { "ApplyTransform" : self.options.values(), } def activate(self): self._transform_changed() def _transform_changed(self): trans_name = self.options.option("transform").value transform = self.ivm.data.get(trans_name, None) if transform is None or "QpReg" not in transform.metadata: transform = self.ivm.extras.get(trans_name, None) if transform is not None and "QpReg" in transform.metadata: self.details.transform = transform
class ModelOptions(OptionsWidget): def __init__(self, ivm, parent, model_type, abbrev, model_classes): OptionsWidget.__init__(self, ivm, parent) self._views = {} self.model = None self.view = None self._option_name = "%s-model" % abbrev for name, cls in model_classes.items(): self._views[name] = get_view_class(cls)(ivm) main_vbox = QtGui.QVBoxLayout() self.setLayout(main_vbox) self.options = OptionBox() self.options.add( "%s model" % model_type, ChoiceOption([v.model.display_name for v in self._views.values()], self._views.keys()), key=self._option_name) self.options.option(self._option_name).sig_changed.connect( self._model_changed) main_vbox.addWidget(self.options) self._create_guis(main_vbox) main_vbox.addStretch(1) self._model_changed() def _create_guis(self, main_vbox): # Create the GUIs for models - only one visible at a time! for view in self._views.values(): if view.gui is not None: view.gui.setVisible(False) if isinstance(view.gui, QtGui.QWidget): main_vbox.addWidget(view.gui) else: main_vbox.addLayout(view.gui) view.model.options.sig_changed.connect( self._model_option_changed) def _model_changed(self): chosen_name = self.options.option(self._option_name).value self.view = self._views[chosen_name] self.model = self.view.model for name, view in self._views.items(): view.gui.setVisible(chosen_name == name) self.sig_changed.emit() def _model_option_changed(self, _key, _value): self.sig_changed.emit()
class NoiseOptions(OptionsWidget): def __init__(self, ivm, parent): OptionsWidget.__init__(self, ivm, parent) main_vbox = QtGui.QVBoxLayout() self.setLayout(main_vbox) self.options = OptionBox() self.options.add("Add noise with SNR", NumericOption(minval=0.1, maxval=100, default=10), checked=True, key="snr") self.options.sig_changed.connect(self.sig_changed.emit) main_vbox.addWidget(self.options) main_vbox.addStretch(1)
class MotionOptions(OptionsWidget): def __init__(self, ivm, parent): OptionsWidget.__init__(self, ivm, parent) main_vbox = QtGui.QVBoxLayout() self.setLayout(main_vbox) self.options = OptionBox() self.options.add("Simulate motion", BoolOption(default=False), key="motion") self.options.option("motion").sig_changed.connect( self._update_widget_visibility) self.options.add("Random translation standard deviation (mm)", NumericOption(minval=0, maxval=5, default=1, decimals=2), key="std") self.options.add( "Random rotation standard deviation (\N{DEGREE SIGN})", NumericOption(minval=0, maxval=10, default=1, decimals=2), key="std_rot") self.options.add("Padding (mm)", NumericOption(minval=0, maxval=10, default=5, decimals=1), key="padding", checked=True) self.options.add( "Interpolation", ChoiceOption(["Nearest neighbour", "Linear", "Quadratic", "Cubic"], return_values=range(4), default=3), key="order") main_vbox.addWidget(self.options) main_vbox.addStretch(1) self._update_widget_visibility() def _update_widget_visibility(self): enabled = self.options.option("motion").value for option in ["std", "std_rot", "padding", "order"]: self.options.set_visible(option, enabled)
class AddNoiseWidget(QpWidget): """ Add noise to data """ def __init__(self, **kwargs): super(AddNoiseWidget, self).__init__(name="Add noise", icon="noise", desc="Add random noise to a data set", group="Simulation", **kwargs) def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) title = TitleWidget(self, title="Add Noise", help="noise") vbox.addWidget(title) self.option_box = OptionBox("Options") data = self.option_box.add("Data set", DataOption(self.ivm), key="data") self.option_box.add("Gaussian standard deviation", NumericOption(minval=0, maxval=100, default=50), key="std") self.option_box.add("Output name", OutputNameOption(src_data=data, suffix="_noisy"), key="output-name") vbox.addWidget(self.option_box) run_btn = QtGui.QPushButton('Run', self) run_btn.clicked.connect(self.run) vbox.addWidget(run_btn) vbox.addStretch(1) def batch_options(self): return "AddNoise", self.option_box.values() def run(self): options = self.batch_options()[1] process = AddNoiseProcess(self.ivm) process.execute(options)
class SimpleMathsWidget(QpWidget): """ Widget which lets you run arbitrary Python/Numpy code on the data in the IVM """ def __init__(self, **kwargs): super(SimpleMathsWidget, self).__init__(name="Simple Maths", icon="maths", desc="Simple mathematical operations on data", group="Processing", **kwargs) def init_ui(self): layout = QtGui.QVBoxLayout() self.setLayout(layout) title = TitleWidget(self, help="simple_maths") layout.addWidget(title) info = QtGui.QLabel(MATHS_INFO) info.setWordWrap(True) layout.addWidget(info) self.optbox = OptionBox() self.optbox.add("Data space from", DataOption(self.ivm), key="grid") self.optbox.add("Command", TextOption(), key="cmd") self.optbox.add("Output name", OutputNameOption(src_data=self.optbox.option("grid")), key="output-name") self.optbox.add("Output is an ROI", BoolOption(), key="output-is-roi") layout.addWidget(self.optbox) hbox = QtGui.QHBoxLayout() self.go_btn = RunButton(self) hbox.addWidget(self.go_btn) hbox.addStretch(1) layout.addLayout(hbox) layout.addStretch(1) def processes(self): return { "Exec": { "grid": self.optbox.option("grid").value, "output-is-roi": self.optbox.option("output-is-roi").value, self.optbox.option("output-name").value: self.optbox.option("cmd").value, } }
class SimMotionWidget(QpWidget): """ Widget to simulate random motion on a 4D data set """ def __init__(self, **kwargs): super(SimMotionWidget, self).__init__(name="Simulate motion", icon="reg", desc="Simulate random motion on a 4D data set", group="Simulation", **kwargs) def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) title = TitleWidget(self, title="Simulate Motion", help="sim_motion") vbox.addWidget(title) self.option_box = OptionBox("Options") data = self.option_box.add("Data set", DataOption(self.ivm, include_4d=True, include_3d=False), key="data") self.option_box.add("Motion standard deviation (mm)", NumericOption(minval=0, maxval=5, default=1, decimals=2), key="std") self.option_box.add("Padding (mm)", NumericOption(minval=0, maxval=10, default=5, decimals=1), key="padding", checked=True) self.option_box.add("Output name", OutputNameOption(src_data=data, suffix="_moving"), key="output-name") vbox.addWidget(self.option_box) run_btn = QtGui.QPushButton('Run', self) run_btn.clicked.connect(self.run) vbox.addWidget(run_btn) vbox.addStretch(1) def batch_options(self): return "SimMotion", self.option_box.values() def run(self): options = self.batch_options()[1] process = SimMotionProcess(self.ivm) process.execute(options)
def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) vbox.addWidget(TitleWidget(self)) optbox = OptionBox("Resampling options") self.data = optbox.add("Data to resample", DataOption(self.ivm)) self.grid_data = optbox.add("Resample onto grid from", DataOption(self.ivm)) self.order = optbox.add( "Interpolation", ChoiceOption(["Nearest neighbour", "Linear", "Quadratic", "Cubic"], [0, 1, 2, 3])) self.output_name = optbox.add( "Output name", OutputNameOption(src_data=self.data, suffix="_res")) vbox.addWidget(optbox) self.run = RunButton("Resample", self._run) vbox.addWidget(self.run) vbox.addStretch(1)
class AifWidget(QtGui.QWidget): """ Widget allowing choice of AIF """ def __init__(self, ivm): QtGui.QWidget.__init__(self) self.ivm = ivm vbox = QtGui.QVBoxLayout() self.setLayout(vbox) self.optbox = OptionBox() self.optbox.add( "AIF source", ChoiceOption(["Global sequence of values", "Voxelwise image"], ["global", "voxelwise"]), key="aif_source") self.optbox.option("aif_source").sig_changed.connect( self._aif_source_changed) self.optbox.add("AIF", NumberListOption(), key="aif") self.optbox.add("AIF image", DataOption(self.ivm), key="suppdata") self.optbox.add("AIF type", ChoiceOption(["DSC signal", "Concentration"], [False, True]), key="aifconc") vbox.addWidget(self.optbox) vbox.addStretch() self._aif_source_changed() def options(self): """ :return: Dictionary of options selected for the AIF""" opts = self.optbox.values() opts.pop("aif_source") return opts def _aif_source_changed(self): global_aif = self.optbox.option("aif_source").value == "global" self.optbox.set_visible("aif", global_aif) self.optbox.set_visible("suppdata", not global_aif)
class DscDataModelView: def __init__(self, ivm): self.model = DscDataModel(ivm) self.gui = OptionBox() self.gui.add("Time between volumes (s)", NumericOption(minval=0, maxval=5, default=1.0), key="delt") self.gui.add("TE (s)", NumericOption(minval=0, maxval=5, default=1.0), key="te") self.gui.add("AIF", NumberListOption(), key="aif") self.gui.sig_changed.connect(self._update_options) self._update_options() def _update_options(self): self.model.options.update(self.gui.values())
class SpinEchoDataModelView: def __init__(self, ivm): self.model = SpinEchoDataModel(ivm) self.gui = OptionBox() self.gui.add("TR (s)", NumericOption(minval=0, maxval=10, default=4.8), key="tr") self.gui.add("TE (ms)", NumericOption(minval=0, maxval=1000, default=0), key="te") self.gui.add("M0", NumericOption(minval=0, maxval=10000, default=1000), key="m0") self.gui.sig_changed.connect(self._update_options) self._update_options() def _update_options(self): self.model.options.update(self.gui.values())
class OutputOptions(OptionsWidget): def __init__(self, ivm, parent): OptionsWidget.__init__(self, ivm, parent) main_vbox = QtGui.QVBoxLayout() self.setLayout(main_vbox) self.options = OptionBox() self.options.add("Output name", TextOption("sim_data"), key="output") self.options.add("Output parameter maps", BoolOption(), default=False, key="output-param-maps") self.options.add("Output clean data (no noise/motion)", TextOption("sim_data_clean"), checked=True, default=True, key="output-clean") main_vbox.addWidget(self.options) main_vbox.addStretch(1)
class RegWidget(QpWidget): """ Generic registration / motion correction widget """ def __init__(self, **kwargs): super(RegWidget, self).__init__(name="Registration", icon="reg", desc="Registration and Motion Correction", group="Registration", **kwargs) self.reg_methods = [] for method in get_plugins("reg-methods"): try: self.reg_methods.append(method(self.ivm)) except: traceback.print_exc() self.warn("Failed to create registration method: %s", method) def init_ui(self): layout = QtGui.QVBoxLayout() self.setLayout(layout) title = TitleWidget(self, title="Registration and Motion Correction", help="reg") layout.addWidget(title) if not self.reg_methods: layout.addWidget(QtGui.QLabel("No registration methods found")) layout.addStretch(1) return self.options = OptionBox("General Options") self.options.add("Mode", ChoiceOption(["Registration", "Motion Correction"], ["reg", "moco"]), key="mode") self.options.add( "Method", ChoiceOption([method.display_name for method in self.reg_methods], self.reg_methods), key="method") self.options.add("Registration data", DataOption(self.ivm), key="reg") self.options.add("Reference data", DataOption(self.ivm), key="ref") self.options.add( "Reference volume", ChoiceOption(["Middle volume", "Mean volume", "Specified volume"], ["median", "mean", "idx"]), key="ref-vol") self.options.add("Reference volume index", NumericOption(intonly=True), key="ref-idx") self.options.add( "Output space", ChoiceOption(["Reference", "Registration", "Transformed"], ["ref", "reg", "trans"]), key="output-space") self.options.add("Output name", OutputNameOption(src_data=self.options.option("reg"), suffix="_reg"), key="output-name", checked=True) self.options.add("Also apply transform to", DataOption(self.ivm, multi=True), key="add-reg") self.options.add("Save transformation", TextOption(), key="save-transform", checked=True, default=False) self.options.option("mode").sig_changed.connect( self._update_option_visibility) self.options.option("method").sig_changed.connect(self._method_changed) self.options.option("ref").sig_changed.connect( self._update_option_visibility) self.options.option("ref-vol").sig_changed.connect( self._update_option_visibility) self.options.option("reg").sig_changed.connect( self._update_option_visibility) layout.addWidget(self.options) # Create the options boxes for reg methods - only one visible at a time! self.opt_boxes = {} for method in self.reg_methods: hbox = QtGui.QHBoxLayout() opt_box = QtGui.QGroupBox() opt_box.setTitle(method.display_name) vbox = QtGui.QVBoxLayout() opt_box.setLayout(vbox) vbox.addWidget(method.interface()) hbox.addWidget(opt_box) opt_box.setVisible(False) layout.addLayout(hbox) self.opt_boxes[method.name] = opt_box layout.addWidget(RunWidget(self)) layout.addStretch(1) self._method_changed() def _method_changed(self): method = self.options.option("method").value for name, box in self.opt_boxes.items(): box.setVisible(name == method.name) self.options.option("save-transform").value = "%s_trans" % method.name self._update_option_visibility() def _update_option_visibility(self): mode = self.options.option("mode").value regdata = self.ivm.data.get(self.options.option("reg").value, None) refdata = self.ivm.data.get(self.options.option("ref").value, None) refvol = self.options.option("ref-vol").value nvols_reg, nvols_ref = 1, 1 if regdata is not None: nvols_reg = regdata.nvols if mode == "moco" and regdata is not None: nvols_ref = regdata.nvols elif mode == "reg" and refdata is not None: nvols_ref = refdata.nvols self.options.set_visible("ref", mode == "reg") self.options.set_visible("ref-vol", nvols_ref > 1) self.options.set_visible("ref-idx", nvols_ref > 1 and refvol == "idx") self.options.set_visible("add-reg", nvols_reg == 1 and mode == "reg") self.options.set_visible("output-space", mode == "reg") if nvols_ref > 1: self.options.option("ref-idx").setLimits(0, nvols_ref - 1) self.options.option("ref-idx").value = int(nvols_ref / 2) def processes(self): options = self.options.values() if options.get("ref-vol", None) == "idx": options["ref-vol"] = options.pop("ref-idx") method = options.pop("method") options["method"] = method.name options.update(method.options()) return { "Reg": options, }
class HistogramWidget(QpWidget): """ Widget which displays data histograms """ def __init__(self, **kwargs): super(HistogramWidget, self).__init__(name="Histogram", icon="hist", desc="Display histograms from data", group="Visualisation", **kwargs) self._updating = False def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) title = TitleWidget(self) vbox.addWidget(title) self.options = OptionBox("Options") self.options.add("Data", DataOption(self.ivm, multi=True), key="data") self.options.add("Within ROI", DataOption(self.ivm, data=False, rois=True, none_option=True), key="roi") self.options.add("All volumes", BoolOption(default=False), key="allvols") self.options.add("Y-axis scale", ChoiceOption(["Count", "Probability"]), key="yscale") self.options.add("Number of bins", NumericOption(minval=5, maxval=500, default=100, intonly=True), key="bins") self.options.add("Min value", NumericOption(minval=0, maxval=100, default=0), key="min") self.options.add("Max value", NumericOption(minval=0, maxval=500, default=100), key="max") self.options.option("yscale").sig_changed.connect(self._yscale_changed) self.options.option("min").sig_changed.connect(self._min_changed) self.options.option("min").sig_changed.connect(self._max_changed) vbox.addWidget(self.options) self.plot = Plot(qpo=None, parent=self, title="Data histogram", display_mode=False) self.plot.set_xlabel("Data value") self.plot.set_ylabel("Count") vbox.addWidget(self.plot) vbox.addStretch(1) def activate(self): self.ivm.sig_all_data.connect(self._data_changed) self.options.option("data").sig_changed.connect(self._data_changed) self.options.sig_changed.connect(self._update) self._data_changed() def deactivate(self): self.ivm.sig_all_data.disconnect(self._data_changed) self.options.option("data").sig_changed.disconnect(self._data_changed) self.options.sig_changed.disconnect(self._update) def processes(self): opts = self.options.values() if not opts.pop("allvols", False): opts["vol"] = self.ivl.focus()[3] return { "Histogram" : opts } def _yscale_changed(self): self.plot.set_ylabel(self.options.option("yscale").value) def _min_changed(self): minval = self.options.option("min").value self.options.option("max").setLimits(minval=minval) def _max_changed(self): maxval = self.options.option("max").value self.options.option("min").setLimits(maxval=maxval) def _data_changed(self): if self._updating: return self._updating = True try: data_names = self.options.option("data").value vol = None if self.options.option("allvols").value: vol = self.ivl.focus()[3] dmin, dmax, multivol = None, None, False for data_name in data_names: qpdata = self.ivm.data[data_name] multivol = multivol or qpdata.nvols > 1 _dmin, _dmax = qpdata.range(vol=vol) if dmin is None or dmin > _dmin: dmin = _dmin if dmax is None or dmax > dmax: dmax = _dmax if dmin is not None and dmax is not None: self.options.option("min").value = dmin self.options.option("min").setLimits(dmin, dmax) self.options.option("max").value = dmax self.options.option("max").setLimits(dmin, dmax) self.options.set_visible("allvols", multivol) self._update() finally: self._updating = False def _update(self): opts = self.processes()["Histogram"] if opts["data"]: HistogramProcess(self.ivm).run(opts) self.plot.clear() histogram = self.ivm.extras.get("histogram", None) if histogram is not None: for idx, name in enumerate(histogram.col_headers[3:]): xvalues = [row[2] for row in histogram.arr] yvalues = [row[idx+3] for row in histogram.arr] self.plot.add_line(yvalues, name=name, xvalues=xvalues)
class NewPoolDialog(QtGui.QDialog): def __init__(self, parent): super(NewPoolDialog, self).__init__(parent) self.setWindowTitle("New Pool") vbox = QtGui.QVBoxLayout() self.optbox = OptionBox() vbox.addWidget(self.optbox) self.optbox.add("Name", TextOption(), key="name") self.optbox.add("PPM", NumericOption(minval=0, maxval=20, default=0, decimals=3, slider=False), key="ppm") self.optbox.add("Exchange rate", NumericOption(minval=0, maxval=1000, default=0, decimals=1, slider=False), key="exch") self.optbox.add("T1 (s)", NumericOption(minval=0, maxval=10, default=1.0, decimals=2, slider=False), key="t1") self.optbox.add("T2 (s)", NumericOption(minval=0, maxval=1.0, default=0.07, decimals=6, slider=False), key="t2") self.optbox.option("name").sig_changed.connect(self._validate) self.button_box = QtGui.QDialogButtonBox( QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) vbox.addWidget(self.button_box) self.setLayout(vbox) self._validate() def pool(self, b0): return Pool(self.optbox.option("name").value, True, vals={ b0: [ self.optbox.option(w).value for w in ["ppm", "exch", "t1", "t2"] ] }, userdef=True) def _validate(self): valid = all( [self.optbox.option(w).valid for w in ["ppm", "exch", "t1", "t2"]]) if self.optbox.option("name").value != "": self.optbox.option("name").setStyleSheet("") else: self.optbox.option("name").setStyleSheet( "QLineEdit {background-color: red}") valid = False self.button_box.button(QtGui.QDialogButtonBox.Ok).setEnabled(valid)
class FabberDceWidget(QpWidget): """ DCE modelling, using the Fabber process """ def __init__(self, **kwargs): QpWidget.__init__(self, name="Bayesian DCE", icon="dce", group="DCE-MRI", desc="DCE model fitting using Bayesian inference", **kwargs) def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) try: self.FabberProcess = get_plugins("processes", "FabberProcess")[0] except IndexError: self.FabberProcess = None if self.FabberProcess is None: vbox.addWidget( QtGui.QLabel( "Fabber core library not found.\n\n You must install Fabber to use this widget" )) return title = TitleWidget( self, help="fabber-dsc", subtitle="DSC modelling using the Fabber process %s" % __version__) vbox.addWidget(title) cite = Citation(FAB_CITE_TITLE, FAB_CITE_AUTHOR, FAB_CITE_JOURNAL) vbox.addWidget(cite) self.input = OptionBox("Input data") self.input.add("DCE data", DataOption(self.ivm, include_3d=False, include_4d=True), key="data") self.input.add("ROI", DataOption(self.ivm, data=False, rois=True), key="roi", checked=True) self.input.add("T1 map", DataOption(self.ivm, include_3d=True, include_4d=False), key="t1", checked=True) self.input.option("t1").sig_changed.connect(self._t1_map_changed) vbox.addWidget(self.input) self.acquisition = OptionBox("Acquisition") self.acquisition.add("Contrast agent R1 relaxivity (l/mmol s)", NumericOption(minval=0, maxval=10, default=3.7), key="r1") self.acquisition.add("Flip angle (\N{DEGREE SIGN})", NumericOption(minval=0, maxval=90, default=12), key="fa") self.acquisition.add("TR (ms)", NumericOption(minval=0, maxval=10, default=4.108), key="tr") self.acquisition.add("Time between volumes (s)", NumericOption(minval=0, maxval=30, default=12), key="delt") vbox.addWidget(self.acquisition) self.model = OptionBox("Model options") self.model.add( "Model", ChoiceOption([ "Standard Tofts model", "Extended Tofts model (ETM)", "2 Compartment exchange model", "Compartmental Tissue Update (CTU) model", "Adiabatic Approximation to Tissue Homogeneity (AATH) Model" ], ["dce_tofts", "dce_ETM", "dce_2CXM", "dce_CTU", "dce_AATH"]), key="model") self.model.add( "AIF", ChoiceOption([ "Population (Orton 2008)", "Population (Parker)", "Measured DCE signal", "Measured concentration curve" ], ["orton", "parker", "signal", "conc"]), key="aif") self.model.add("Bolus injection time (s)", NumericOption(minval=0, maxval=60, default=30), key="tinj") self.model.add("AIF data values", NumberListOption([ 0, ]), key="aif-data") self.model.add("T1 (s)", NumericOption(minval=0.0, maxval=5.0, default=1.0), key="t10") self.model.add("Allow T1 to vary", BoolOption(default=False), key="infer-t10") self.model.add("Bolus arrival time (s)", NumericOption(minval=0, maxval=2.0, default=0), key="delay") self.model.add("Allow bolus arrival time to vary", BoolOption(default=False), key="infer-delay") self.model.add("Infer kep rather than ve", BoolOption(default=False), key="infer-kep") self.model.add("Infer flow", BoolOption(default=True), key="infer-fp") self.model.add("Infer permeability-surface area", BoolOption(default=False), key="infer-ps") self.model.add("Spatial regularization", BoolOption(default=False), key="spatial") self.model.option("model").sig_changed.connect(self._model_changed) self.model.option("aif").sig_changed.connect(self._aif_changed) vbox.addWidget(self.model) # Run button and progress vbox.addWidget(RunWidget(self, title="Run modelling")) vbox.addStretch(1) self._aif_changed() self._model_changed() def _t1_map_changed(self): self.model.set_visible("t10", "t1" not in self.input.values()) def _aif_changed(self): aif_source = self.model.option("aif").value self.model.set_visible("tinj", aif_source not in ("signal", "conc")) self.model.set_visible("aif-data", aif_source in ("signal", "conc")) def _model_changed(self): self.model.set_visible( "infer-kep", self.model.option("model").value in ("dce_tofts", "dce_ETM")) self.model.set_visible("infer-fp", self.model.option("model").value == "dce_AATH") self.model.set_visible("infer-ps", self.model.option("model").value == "dce_AATH") def processes(self): options = { "model-group": "dce", "method": "vb", "noise": "white", "save-mean": True, "save-model-fit": True, "convergence": "trialmode", "max-trials": 20, "max-iterations": 50, "infer-sig0": True, } options.update(self.input.values()) options.update(self.acquisition.values()) options.update(self.model.values()) # Extended Tofts model is the same model name but with inference of Vp if options["model"] == "dce_ETM": options["model"] = "dce_tofts" options["infer-vp"] = True # T1 map is an image prior if "t1" in options: options.update({ "PSP_byname1": "t10", "PSP_byname1_type": "I", "PSP_byname1_image": options.pop("t1") }) if not options["infer-t10"]: # To treat the image prior as ground truth need to put T10 # into the model but give the image prior a high precision so # the parameter doesn't actually have any freedom to vary options["infer-t10"] = True options["PSP_byname1_prec"] = 1e6 # Delay time to include injection time for population AIF if "tinj" in options: options["delay"] = options["delay"] + options.pop("tinj") # Times in minutes and TR in s options["delt"] = options["delt"] / 60 options["delay"] = options["delay"] / 60 options["tr"] = options["tr"] / 1000 # Spatial mode if options.pop("spatial", False): options["method"] = "spatialvb" options["param-spatial-priors"] = "M+" return {"Fabber": options}
class SequenceOptions(QtGui.QWidget): """ Widget containing options for the CEST sequence """ sig_b0_changed = QtCore.Signal(float) def __init__(self, ivm=None): QtGui.QWidget.__init__(self) self._ivm = ivm vbox = QtGui.QVBoxLayout() self.setLayout(vbox) self.optbox = OptionBox() vbox.addWidget(self.optbox) self.optbox.add("CEST data", DataOption(self._ivm), key="data") self.optbox.add("ROI", DataOption(self._ivm, rois=True, data=False), key="mask") self.optbox.add("Frequency offsets", NumberListOption(), key="freqs") self.optbox.add("B0", ChoiceOption(B0_DEFAULTS), key="b0") self.optbox.add("Custom B0 (T)", NumericOption(minval=0.0, maxval=15, default=3.0, decimals=3), key="b0_custom") # FIXME multiple B1 values self.optbox.add("B1 (\u03bcT)", NumericOption(minval=0.0, maxval=2, default=0.55, decimals=6), key="b1") self.optbox.add( "Saturation", ChoiceOption(["Continuous Saturation", "Pulsed Saturation"], ["continuous", "pulsed"]), key="sat") self.optbox.add("Saturation time (s)", NumericOption(minval=0.0, maxval=5, default=2, decimals=2), key="sat_time") self.optbox.add("Pulse Magnitudes", NumberListOption(), key="pulse_mag") self.optbox.add("Pulse Durations (s)", NumberListOption(), key="pulse_dur") self.optbox.add("Pulse Repeats", NumberListOption(), key="pulse_repeats") self.optbox.option("b0").sig_changed.connect(self._b0_changed) self.optbox.option("b0_custom").sig_changed.connect(self._b0_changed) self.optbox.option("sat").sig_changed.connect(self._sat_changed) self.warn_box = WarningBox() vbox.addWidget(self.warn_box) # B1 field #hbox = QtGui.QHBoxLayout() #self.unsat_cb = QtGui.QCheckBox("Unsaturated") #self.unsat_cb.stateChanged.connect(self.update_ui) #hbox.addWidget(self.unsat_cb) #self.unsat_combo = QtGui.QComboBox() #self.unsat_combo.addItem("first") #self.unsat_combo.addItem("last") #self.unsat_combo.addItem("first and last ") #hbox.addWidget(self.unsat_combo) #hbox.addStretch(1) #grid.addLayout(hbox, 2, 2) vbox.addStretch(1) self._sat_changed() self._b0_changed() def _sat_changed(self): pulsed = self.optbox.option("sat").value == "pulsed" self.optbox.set_visible("pulse_mag", pulsed) self.optbox.set_visible("pulse_dur", pulsed) self.optbox.set_visible("pulse_repeats", pulsed) def _b0_changed(self): b0_sel = self.optbox.option("b0").value if b0_sel == "Custom": self.optbox.set_visible("b0_custom", True) b0 = self.optbox.option("b0_custom").value else: self.optbox.set_visible("b0_custom", False) b0 = float(b0_sel[:-1]) self.sig_b0_changed.emit(b0) def _get_dataspec(self, options): dataspec = [] freqs = options.pop("freqs") b1 = options.pop("b1") / 1e6 if options["sat"] == "pulsed": repeats = options.pop("pulse_repeats") else: repeats = 1 for idx, freq in enumerate(freqs): #if self.unsat_cb.isChecked(): # self.debug("Unsat", idx, self.unsat_combo.currentIndex()) # if idx == 0 and self.unsat_combo.currentIndex() in (0, 2): # b1 = 0 # elif idx == len(freqs)-1 and self.unsat_combo.currentIndex() in (1, 2): # b1 = 0 dataspec.append([freq, b1, repeats]) #self.debug(dataspec) return dataspec def _get_ptrain(self, options): ptrain = [] if options.pop("sat") == "pulsed": pms = options.pop("pulse_mag") pds = options.pop("pulse_dur") if len(pms) != len(pds): raise QpException( "Pulse magnitude and duration must contain the same number of values" ) for pm, pd in zip(pms, pds): ptrain.append([pm, pd]) else: ptrain.append([1, options.pop("sat_time")]) #self.debug(ptrain) return ptrain def options(self): options = self.optbox.values() options["spec"] = self._get_dataspec(options) options["ptrain"] = self._get_ptrain(options) options.pop("b0") options.pop("b0_custom", None) return options
class SimData(FabberWidget): """ Widget which uses Fabber models to generate simulated data """ def __init__(self, **kwargs): super(SimData, self).__init__(name="Simulated Fabber Data", icon="fabber", desc="Generate test data sets from Fabber models", group="Simulation", **kwargs) self._param_test_values = {} def init_ui(self): FabberWidget.init_ui(self) self.param_values_box = OptionBox("Parameter values") self.param_values_box.sig_changed.connect(self._param_values_changed) self.vbox.addWidget(self.param_values_box) run_btn = QtGui.QPushButton('Generate test data', self) run_btn.clicked.connect(self._run) self.vbox.addWidget(run_btn) self.vbox.addStretch(1) model_opts_btn = QtGui.QPushButton('Model Options') model_opts_btn.clicked.connect(self._show_model_options) self.options.add("Model group", ChoiceOption(), key="model-group") self.options.add("Model", ChoiceOption(), model_opts_btn, key="model") self.options.add("Number of volumes (time points)", NumericOption(intonly=True, minval=1, maxval=100, default=10), key="num-vols") self.options.add("Voxels per patch (approx)", NumericOption(intonly=True, minval=1, maxval=10000, default=1000), key="num-voxels") self.options.add("Noise (Gaussian std.dev)", NumericOption(intonly=True, minval=0, maxval=1000, default=0), key="noise") self.options.add("Output data name", OutputNameOption(initial="fabber_test_data"), key="output-name") self.options.add("Output noise-free data", BoolOption(), key="save-clean") self.options.add("Output parameter ROIs", BoolOption(), key="save-rois") self.options.option("model-group").sig_changed.connect(self._model_group_changed) model_groups = ["ALL"] for group in FabberProcess.api().get_model_groups(): model_groups.append(group.upper()) self.options.option("model-group").setChoices(model_groups) self.options.option("model-group").value = "ALL" self._model_group_changed() self.options.option("model").value = "poly" self._options_changed() # Start with something sensible for the polynomial model self._param_test_values = {"c0" : [-100, 0, 100], "c1" : [-10, 0, 10], "c2" : [-1, 0, 1]} self._update_params() def _update_params(self): FabberWidget._update_params(self) self.param_values_box.clear() for param in self._fabber_params: current_values = self._param_test_values.get(param, [1.0]) self.param_values_box.add(param, NumberListOption(initial=current_values)) self._param_test_values[param] = current_values # Remove references to parameters which no longer exist for param in list(self._param_test_values.keys()): if param not in self._fabber_params: del self._param_test_values[param] def _param_values_changed(self): self._param_test_values = self.param_values_box.values() num_variable = len([1 for v in self._param_test_values.values() if len(v) > 1]) if num_variable > 3: self.warn("Cannot have more than 3 varying parameters") def get_options(self): """ Return a copy of current Fabber options and parameter test values """ options = dict(self._fabber_options) options["param-test-values"] = self._param_test_values return options def _run(self): process = self.get_process() options = self.get_options() process.run(options) def get_process(self): return FabberTestDataProcess(self.ivm) def batch_options(self): return "FabberTestData", self.get_options()
class FlirtRegMethod(RegMethod): """ FLIRT/MCFLIRT registration method """ def __init__(self, ivm): RegMethod.__init__(self, "flirt", ivm, "FLIRT/MCFLIRT") self.options_widget = None self.cost_models = [ "Mutual information", "Woods", "Correlation ratio", "Normalized correlation", "Normalized mutual information", "Least squares" ] self.cost_model_options = [ "mutualinfo", "woods", "corratio", "normcorr", "normmi", "leastsq" ] @classmethod def apply_transform(cls, reg_data, transform, options, queue): """ Apply a previously calculated transformation to a data set We are not actually using FSL applyxfm for this although it would be an alternative option for the reference space output option. Instead we perform a non-lossy affine transformation and then resample onto the reference or registration spaces as required. """ log = "Performing non-lossy affine transformation\n" order = options.pop("interp-order", 1) affine = transform.voxel_to_world(reg_data.grid) grid = DataGrid(reg_data.grid.shape, affine) qpdata = NumpyData(reg_data.raw(), grid=grid, name=reg_data.name) output_space = options.pop("output-space", "ref") if output_space == "ref": qpdata = qpdata.resample(transform.ref_grid, suffix="", order=order) log += "Resampling onto reference grid\n" elif output_space == "reg": qpdata = qpdata.resample(transform.reg_grid, suffix="", order=order) log += "Resampling onto input grid\n" return qpdata, log @classmethod def reg_3d(cls, reg_data, ref_data, options, queue): """ Static function for performing 3D registration """ from fsl import wrappers as fsl reg = qpdata_to_fslimage(reg_data) ref = qpdata_to_fslimage(ref_data) set_environ(options) output_space = options.pop("output-space", "ref") interp = _interp(options.pop("interp-order", 1)) twod = reg_data.grid.shape[2] == 1 logstream = six.StringIO() flirt_output = fsl.flirt(reg, ref, interp=interp, out=fsl.LOAD, omat=fsl.LOAD, twod=twod, log={ "cmd": logstream, "stdout": logstream, "stderr": logstream }, **options) transform = FlirtTransform(ref_data.grid, flirt_output["omat"], name="flirt_xfm") if output_space == "ref": qpdata = fslimage_to_qpdata(flirt_output["out"], reg_data.name) elif output_space == "reg": qpdata = fslimage_to_qpdata(flirt_output["out"], reg_data.name).resample(reg_data.grid, suffix="") qpdata.name = reg_data.name elif output_space == "trans": trans_affine = transform.voxel_to_world(reg_data.grid) trans_grid = DataGrid(reg_data.grid.shape, trans_affine) qpdata = NumpyData(reg_data.raw(), grid=trans_grid, name=reg_data.name) return qpdata, transform, logstream.getvalue() @classmethod def moco(cls, moco_data, ref, options, queue): """ Motion correction We use MCFLIRT to implement this :param moco_data: A single 4D QpData instance containing data to motion correct. :param ref: Either 3D QpData containing reference data, or integer giving the volume index of ``moco_data`` to use :param options: Method options as dictionary :param queue: Queue object which method may put progress information on to. Progress should be given as a number between 0 and 1. :return Tuple of three items. First, motion corrected data as 4D QpData in the same space as ``moco_data`` Second, if options contains ``output-transform : True``, sequence of transformations found, one for each volume in ``reg_data``. Each is either an affine matrix transformation or a sequence of 3 warp images, the same shape as ``regdata`` If ``output-transform`` is not given, returns None instead. Third, log information from the registration as a string. """ from fsl import wrappers as fsl if moco_data.ndim != 4: raise QpException("Cannot motion correct 3D data") set_environ(options) reg = qpdata_to_fslimage(moco_data) if isinstance(ref, int): options["refvol"] = ref ref_grid = moco_data.grid elif isinstance(ref, QpData): options["reffile"] = qpdata_to_fslimage(ref) ref_grid = ref.grid else: raise QpException("invalid reference object type: %s" % type(ref)) interp = _interp(options.pop("interp-order", 1)) # FIXME ignored twod = moco_data.grid.shape[2] == 1 logstream = six.StringIO() result = fsl.mcflirt(reg, out=fsl.LOAD, mats=fsl.LOAD, twod=twod, log={ "cmd": logstream, "stdout": logstream, "stderr": logstream }, **options) qpdata = fslimage_to_qpdata(result["out"], moco_data.name) transforms = [ FlirtTransform(ref_grid, result[os.path.join("out.mat", "MAT_%04i" % vol)]) for vol in range(moco_data.nvols) ] return qpdata, transforms, logstream.getvalue() def interface(self, generic_options=None): """ :return: QWidget containing registration options """ if generic_options is None: generic_options = {} if self.options_widget is None: self.options_widget = QtGui.QWidget() vbox = QtGui.QVBoxLayout() self.options_widget.setLayout(vbox) cite = Citation(CITE_TITLE, CITE_AUTHOR, CITE_JOURNAL) vbox.addWidget(cite) self.optbox = OptionBox() self.optbox.add("Cost Model", ChoiceOption(self.cost_models, self.cost_model_options, default="normcorr"), key="cost") #self.optbox.add("Number of search stages", ChoiceOption([1, 2, 3, 4]), key="nstages") #self.optbox.option("stages").value = 2 #self.optbox.add("Final stage interpolation", ChoiceOption(["None", "Sinc", "Spline", "Nearest neighbour"], ["", "sinc_final", "spline_final", "nn_final"]), key="final") #self.optbox.add("Field of view (mm)", NumericOption(minval1, maxval=100, default=20), key="fov") self.optbox.add("Number of bins", NumericOption(intonly=True, minval=1, maxval=1000, default=256), key="bins") self.optbox.add("Degrees of freedom", ChoiceOption([6, 9, 12]), key="dof") #self.optbox.add("Scaling", NumericOption(minval=0.1, maxval=10, default=6), key="scaling") #self.optbox.add("Smoothing in cost function", NumericOption(minval=0.1, maxval=10, default=1), key="smoothing") #self.optbox.add("Scaling factor for rotation\noptimization tolerances", NumericOption(minval=0.1, maxval=10, default=1), key="rotscale") #self.optbox.add("Search on gradient images", BoolOption, key="grad") vbox.addWidget(self.optbox) return self.options_widget def options(self): """ :return: Dictionary of registration options selected """ self.interface() opts = self.optbox.values() for env_copy in ["FSLOUTPUTTYPE", "FSLDIR", "FSLDEVDIR"]: if env_copy in os.environ: opts[env_copy] = os.environ[env_copy] else: self.debug("%s is not in environment" % env_copy) for key, value in opts.items(): self.debug("%s: %s", key, value) return opts
class OrientDataWidget(QpWidget): """ Widget that lets you tweak the orientation of data """ def __init__(self, **kwargs): super(OrientDataWidget, self).__init__(name="Orient Data", icon="inspect.png", desc="Manipulate data orientation", group="Utilities", **kwargs) self._transform_cache = {} self.ivm.sig_all_data.connect(self._all_data_changed) def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) title = TitleWidget(self) vbox.addWidget(title) hbox = QtGui.QHBoxLayout() self.options = OptionBox("Re-orient data") data = self.options.add("Data item", DataOption(self.ivm), key="data") data.sig_changed.connect(self._data_changed) self.trans, self.rot = {}, {} self.options.add("Translation") for axis, label in {2: "axial", 0: "sagittal", 1: "coronal"}.items(): trans = self.options.add(" %s (mm)" % label.title(), NumericOption(minval=-100, maxval=100, default=0), key="trans-%s" % label) trans.sig_changed.connect(self._translate(axis, label)) self.trans[axis] = trans self.options.add("Rotation") for axis, label in {2: "axial", 0: "sagittal", 1: "coronal"}.items(): rot = self.options.add(" %s (degrees)" % label.title(), NumericOption(minval=-180, maxval=180, default=0), key="rot-%s" % label) rot.sig_changed.connect(self._rotate(axis, label)) self.rot[axis] = rot hbox.addWidget(self.options) vbox.addLayout(hbox) self.gridview = GridView(self.ivm, self.ivl) vbox.addWidget(self.gridview) hbox = QtGui.QHBoxLayout() reset_btn = QtGui.QPushButton("Reset to original") reset_btn.clicked.connect(self._reset) hbox.addWidget(reset_btn) hbox.addStretch(1) vbox.addLayout(hbox) vbox.addStretch(1) def activate(self): self._data_changed() def _all_data_changed(self, data): for name in list(self._transform_cache.keys()): if name not in data: del self._transform_cache[name] def _data_changed(self): name = self.options.values()["data"] qpdata = self.ivm.data.get(name, self.ivm.rois.get(name, None)) self.gridview.set_data(qpdata) if qpdata is not None: if name not in self._transform_cache: self._transform_cache[name] = ([0, 0, 0], [0, 0, 0]) translation, rotations = self._transform_cache[name] for axis in range(3): self.trans[axis].value = translation[axis] self.rot[axis].value = rotations[axis] self._set() def _translate(self, axis, label): def _trans(): name = self.gridview.data.name trans = self.options.values()["trans-%s" % label] self._transform_cache[name][0][axis] = trans self._set() return _trans def _rotate(self, axis, label): def _rot(): name = self.gridview.data.name angle = self.options.values()["rot-%s" % label] if axis == 1: angle = -angle self._transform_cache[name][1][axis] = angle self._set() return _rot def _reset(self): name = self.gridview.data.name del self._transform_cache[name] self._data_changed() def _set(self): name = self.gridview.data.name affine = self.gridview.data.grid.affine_orig grid_centre = [float(dim) / 2 for dim in self.gridview.data.grid.shape] world_centre = np.dot(affine[:3, :3], grid_centre) self.debug("Initial affine\n%s", affine) translation, rotations = self._transform_cache[name] R = np.identity(3) for axis in range(3): angle = rotations[axis] rot3d = self._rotmtx_3d(axis, angle) affine[:3, :3] = np.dot(rot3d, affine[:3, :3]) R = np.dot(rot3d, R) origin_offset = world_centre - np.dot(R, world_centre) origin_offset += translation self.debug("Origin offset\n%s", origin_offset) affine[:3, 3] += origin_offset self.debug("Final affine\n%s", affine) self.gridview.data.grid.affine = affine self.gridview.update() if self.gridview.data == self.ivm.main: self.ivm.sig_main_data.emit(self.ivm.main) if self.gridview.data.view.visible == Visibility.SHOW or self.gridview.data == self.ivm.main: self.ivl.redraw() def _rotmtx_3d(self, axis, angle): # FIXME this is not quite right when rotating in a plane where # the basis vectors have different lengths c, s = math.cos(math.radians(angle)), math.sin(math.radians(angle)) rot2d = np.array([[c, -s], [s, c]]) rot3d = np.identity(3) if axis == 0: rot3d[1:, 1:] = rot2d elif axis == 1: rot3d[0, 0] = rot2d[0, 0] rot3d[0, 2] = rot2d[0, 1] rot3d[2, 0] = rot2d[1, 0] rot3d[2, 2] = rot2d[1, 1] elif axis == 2: rot3d[:2, :2] = rot2d self.debug("3d rotation matrix: %i %f", axis, angle) self.debug("\n%s", rot3d) return rot3d
class PcaWidget(QpWidget): """ PCA widget """ def __init__(self, **kwargs): super(PcaWidget, self).__init__(name="PCA", icon="pca", desc="PCA reduction", group="Processing", **kwargs) def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) title = TitleWidget( self, title="PCA reduction", subtitle="Principal Component Analysis for 4D data") vbox.addWidget(title) self._options = OptionBox("Options") self._options.add("Data", DataOption(self.ivm, include_3d=False), key="data") self._options.add("ROI", DataOption(self.ivm, data=False, rois=True), key="roi") self._options.add("Number of components", NumericOption(minval=1, intonly=True, default=4), key="n-components") self._options.add("Output name", OutputNameOption( src_data=self._options.option("data"), suffix="_pca"), key="output-name") self._options.option("data").sig_changed.connect(self._data_changed) vbox.addWidget(self._options) self._run = RunWidget(self) self._run.sig_postrun.connect(self._postrun) vbox.addWidget(self._run) self.plot = Plot(qpo=None, parent=self, title="PCA modes") self.variance_model = QtGui.QStandardItemModel() variance_table = QtGui.QTableView() variance_table.verticalHeader().hide() variance_table.setModel(self.variance_model) tabs = QtGui.QTabWidget() tabs.addTab(self.plot, "PCA modes") tabs.addTab(variance_table, "Explained variance") tabs.setCurrentWidget(self.plot) vbox.addWidget(tabs) vbox.addStretch(1) self._data_changed() def processes(self): return {"PCA": self._options.values()} def _data_changed(self): self._run.setEnabled( self._options.option("data").value in self.ivm.data) def _postrun(self): self._update_plot() self._update_table() def _update_plot(self): self.plot.clear() extra = self.ivm.extras.get( self._options.option("output-name").value + "_modes", None) if extra is not None: arr = np.array(extra.arr) for idx in range(arr.shape[1] - 1): self.plot.add_line(arr[:, idx], name="Mode %i" % idx) self.plot.add_line(arr[:, -1], name="Mean", line_col=(255, 0, 0), line_width=3.0) def _update_table(self): self.variance_model.clear() extra = self.ivm.extras.get( self._options.option("output-name").value + "_variance", None) if extra is not None: self.debug(str(extra)) for idx, header in enumerate(extra.col_headers): self.variance_model.setHorizontalHeaderItem( idx, QtGui.QStandardItem(header)) for idx, variance in enumerate(extra.arr): self.variance_model.setItem( idx, 0, QtGui.QStandardItem(str(variance[0]))) self.variance_model.setItem( idx, 1, QtGui.QStandardItem(sf(variance[1]))) self.variance_model.setItem( idx, 2, QtGui.QStandardItem(sf(variance[2])))
class DceWidget(QpWidget): """ Widget for DCE Pharmacokinetic modelling """ def __init__(self, **kwargs): super(DceWidget, self).__init__(name="DCE Modelling", desc="DCE kinetic modelling", icon="dce", group="DCE-MRI", **kwargs) def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) title = TitleWidget(self, help="pk", batch_btn=True, opts_btn=False) vbox.addWidget(title) self.input = OptionBox("Input data") self.input.add("DCE data", DataOption(self.ivm, include_3d=False, include_4d=True), key="data") self.input.add("ROI", DataOption(self.ivm, data=False, rois=True), key="roi") self.input.add("T1 map", DataOption(self.ivm, include_3d=True, include_4d=False), key="t1") vbox.addWidget(self.input) self.options = OptionBox("Options") self.options.add("Contrast agent R1 relaxivity (l/mmol s)", NumericOption(minval=0, maxval=10, default=3.7), key="r1") self.options.add("Contrast agent R2 relaxivity (l/mmol s)", NumericOption(minval=0, maxval=10, default=4.8), key="r2") self.options.add("Flip angle (\N{DEGREE SIGN})", NumericOption(minval=0, maxval=90, default=12), key="fa") self.options.add("TR (ms)", NumericOption(minval=0, maxval=10, default=4.108), key="tr") self.options.add("TE (ms)", NumericOption(minval=0, maxval=10, default=1.832), key="te") self.options.add("Time between volumes (s)", NumericOption(minval=0, maxval=30, default=12), key="dt") self.options.add("Estimated injection time (s)", NumericOption(minval=0, maxval=60, default=30), key="tinj") self.options.add("Ktrans/kep percentile threshold", NumericOption(minval=0, maxval=100, default=100), key="ve-thresh") self.options.add("Dose (mM/kg) - preclinical only", NumericOption(minval=0, maxval=5, default=0.6), key="dose", visible=False) models = [ "Clinical: Toft / OrtonAIF (3rd) with offset", "Clinical: Toft / OrtonAIF (3rd) no offset", "Preclinical: Toft / BiexpAIF (Heilmann)", "Preclinical: Ext Toft / BiexpAIF (Heilmann)", ] self.options.add("Pharmacokinetic model choice", ChoiceOption(models, [1, 2, 3, 4]), key="model") self.options.option("model").sig_changed.connect(self._aif_changed) vbox.addWidget(self.options) # Run button and progress vbox.addWidget(RunWidget(self, title="Run modelling")) vbox.addStretch(1) self._aif_changed() def _aif_changed(self): self.options.set_visible("dose", self.options.option("model").value in (2, 3)) def processes(self): options = self.input.values() options.update(self.options.values()) return {"PkModelling": options}
class AnalysisOptions(QtGui.QWidget): """ Widget allowing model and output options to be changed """ def __init__(self, ivm=None): QtGui.QWidget.__init__(self) self._ivm = ivm self._poolvals_edited = False vbox = QtGui.QVBoxLayout() self.setLayout(vbox) self.optbox = OptionBox() vbox.addWidget(self.optbox) self.optbox.add("<b>Output options</b>") self.optbox.add("CEST R*", BoolOption(default=True), key="save-model-extras") self.optbox.add("Parameter maps", BoolOption(default=False), key="save-mean") #self.optbox.add("Parameter variance", BoolOption(default=False), key="var") self.optbox.add("Model fit", BoolOption(default=False), key="save-model-fit") self.optbox.add("Prefix for output", TextOption(), checked=True, key="output-prefix") self.optbox.add(" ") self.optbox.add("<b>Analysis options</b>") self.optbox.add("Spatial Regularization", BoolOption(), key="spatial") self.optbox.add("Allow uncertainty in T1/T2 values", BoolOption(), key="t12prior") self.optbox.add("Prior T1 map", DataOption(self._ivm), key="t1img", checked=True) self.optbox.add("Prior T2 map", DataOption(self._ivm), key="t2img", checked=True) self.optbox.add("Tissue PV map (GM+WM)", DataOption(self._ivm), key="pvimg", checked=True) self.optbox.option("t12prior").sig_changed.connect(self._update_ui) self.optbox.add("Use steady state solution for MT bias reduction", BoolOption(default=False), key="new-ss") self.optbox.option("new-ss").sig_changed.connect(self._update_ui) self.optbox.add("TR (s)", NumericOption(default=3.0, minval=0, maxval=5, digits=3, step=0.1), key="tr") self.optbox.add("Excitation flip angle (\N{DEGREE SIGN})", NumericOption(default=12.0, minval=0, maxval=25, digits=3, step=1.0), key="fa") self.optbox.add( "MT pool Line shape", ChoiceOption( ["None", "Gaussian", "Lorentzian", "Super Lorentzian"], ["none", "gaussian", "lorentzian", "superlorentzian"]), key="lineshape") self.alexmt_cite = Citation(ALEXMT_CITE_TITLE, ALEXMT_CITE_AUTHOR, ALEXMT_CITE_JOURNAL) vbox.addWidget(self.alexmt_cite) vbox.addStretch(1) self._update_ui() def _update_ui(self): t12prior = self.optbox.option("t12prior").value self.optbox.set_visible("t1img", t12prior) self.optbox.set_visible("t2img", t12prior) newss = self.optbox.values().get("new-ss", False) self.optbox.set_visible("tr", newss) self.optbox.set_visible("fa", newss) self.optbox.set_visible("lineshape", newss) self.alexmt_cite.setVisible(newss) def set_pools(self, pools): self.optbox.set_visible("new-ss", "MT" in [p.name for p in pools if p.enabled]) self._update_ui() def options(self): options = self.optbox.values() if options.pop("spatial", False): options["method"] = "spatialvb" options["param-spatial-priors"] = "MN+" else: options["method"] = "vb" options.pop("param-spatial-priors", None) # The new MT model is automatically triggered when the TR and FA options are given options.pop("new-ss", None) prior_num = 1 for idx in (1, 2): if "t%iimg" % idx in options: options["PSP_byname%i" % prior_num] = "T%ia" % idx options["PSP_byname%i_type" % prior_num] = "I" options["PSP_byname%i_image" % prior_num] = options.pop( "t%iimg" % idx) prior_num += 1 return options
class FnirtRegMethod(RegMethod): """ FNIRT registration method """ def __init__(self, ivm): RegMethod.__init__(self, "fnirt", ivm, display_name="FNIRT") self.options_widget = None @classmethod def apply_transform(cls, reg_data, transform, options, queue): """ Apply a previously calculated transformation to a data set """ output_space = options.pop("output-space", "ref") if output_space not in ("ref", "reg"): raise QpException( "FNIRT does not support output in transformed space") from fsl import wrappers as fsl reg = qpdata_to_fslimage(reg_data) trans = qpdata_to_fslimage(transform) # Applywarp generates an output for each volume of reference image # for some reason. So use just the first volume of the transform # as the reference space ref = qpdata_to_fslimage(transform.volume(0, qpdata=True)) log = six.StringIO() order = options.pop("interp-order", 1) interp = _interp(order) apply_output = fsl.applywarp(reg, ref, interp=interp, paddingsize=1, super=True, superlevel="a", out=fsl.LOAD, log={ "cmd": log, "stdout": log, "stderr": log }, warp=trans, rel=True) qpdata = fslimage_to_qpdata(apply_output["out"], name=reg_data.name) if output_space == "ref": # Default is to output in reference space pass else: qpdata = qpdata.resample(reg_data.grid, suffix="", order=order) log += "Resampling onto input grid\n" return qpdata, log.getvalue() @classmethod def reg_3d(cls, reg_data, ref_data, options, queue): """ Static function for performing 3D registration FIXME return jacobian as part of xform? """ output_space = options.pop("output-space", "ref") if output_space not in ("ref", "reg"): raise QpException( "FNIRT does not support output in transformed space") from fsl import wrappers as fsl reg = qpdata_to_fslimage(reg_data) ref = qpdata_to_fslimage(ref_data) log = six.StringIO() fnirt_output = fsl.fnirt(reg, ref=ref, iout=fsl.LOAD, fout=fsl.LOAD, log={ "cmd": log, "stdout": log, "stderr": log }, **options) transform = fslimage_to_qpdata(fnirt_output["fout"], name="fnirt_warp") transform.metadata["QpReg"] = "FNIRT" if output_space == "ref": qpdata = fslimage_to_qpdata(fnirt_output["iout"], name=reg_data.name) else: qpdata = fslimage_to_qpdata(fnirt_output["iout"], name=reg_data.name).resample( reg_data.grid, suffix="") return qpdata, transform, log.getvalue() def interface(self, generic_options=None): if generic_options is None: generic_options = {} if self.options_widget is None: self.options_widget = QtGui.QWidget() vbox = QtGui.QVBoxLayout() self.options_widget.setLayout(vbox) cite = Citation(CITE_TITLE, CITE_AUTHOR, CITE_JOURNAL) vbox.addWidget(cite) self.optbox = OptionBox() self.optbox.add("Mask for registration data", DataOption(self.ivm, rois=True, data=False), key="inmask", checked=True) self.optbox.add("Mask for reference data", DataOption(self.ivm, rois=True, data=False), key="refmask", checked=True) self.optbox.add("Spline order", ChoiceOption([2, 3]), key="splineorder", checked=True) self.optbox.add("Use pre-defined configuration", ChoiceOption( ["T1_2_MNI152_2mm", "FA_2_FMRIB58_1mm"]), key="config", checked=True) vbox.addWidget(self.optbox) return self.options_widget def options(self): self.interface() return self.optbox.values()
class AifWidget(QpWidget): """ Widget which allows the user to define an arterial input function from signal data """ def __init__(self, **kwargs): super(AifWidget, self).__init__(name="Arterial Input Function", icon="aif", desc="Tool for defining an AIF from measured data", group="Utilities", **kwargs) self._aif = [] self._aif_points = [] def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) title = TitleWidget(self) vbox.addWidget(title) self._options = OptionBox("Options") self._options.add("Signal data", DataOption(self.ivm), key="data") self._clear_btn = QtGui.QPushButton("Clear points") self._options.add("Method", ChoiceOption(["Pick points", "Use existing ROI"], ["points", "roi"]), self._clear_btn, key="method") self._options.add("ROI", DataOption(self.ivm, data=False, rois=True), key="roi") self._view_btn = QtGui.QPushButton("View") self._view_btn.setEnabled(False) self._save_btn = QtGui.QPushButton("Save") self._save_btn.setEnabled(False) self._options.add("AIF name", TextOption("aif"), self._view_btn, self._save_btn, key="output-name") self._options.option("method").sig_changed.connect(self._method_changed) self._options.option("data").sig_changed.connect(self._recalc_aif) self._options.option("roi").sig_changed.connect(self._recalc_aif) self._clear_btn.clicked.connect(self._clear_btn_clicked) self._save_btn.clicked.connect(self._save_btn_clicked) self._view_btn.clicked.connect(self._view_btn_clicked) vbox.addWidget(self._options) self._plot = Plot(qpo=None, parent=self, title="AIF", display_mode=False) self._plot.set_xlabel("Volume") self._plot.set_ylabel("Signal") vbox.addWidget(self._plot) vbox.addStretch(1) def activate(self): self.ivl.sig_selection_changed.connect(self._selection_changed) self._method_changed() def deactivate(self): self.ivl.sig_selection_changed.disconnect(self._selection_changed) self.ivl.set_picker(PickMode.SINGLE) @property def qpdata(self): return self.ivm.data.get(self._options.option("data").value, None) @property def roi(self): return self.ivm.data.get(self._options.option("roi").value, None) @property def method(self): return self._options.option("method").value def _method_changed(self): self._options.set_visible("roi", self.method == "roi") self._clear_btn.setVisible(self.method == "points") if self.method == "roi": self.ivl.set_picker(PickMode.SINGLE) else: self.ivl.set_picker(PickMode.MULTIPLE) self._selection_changed() self._recalc_aif() def _clear_btn_clicked(self): if self.method == "points": self.ivl.set_picker(PickMode.MULTIPLE) self._selection_changed() # FIXME should be signalled by picker def _save_btn_clicked(self): name = self._options.option("output-name").value extra = NumberListExtra(name, self._aif) self.ivm.add_extra(name, extra) self._save_btn.setEnabled(False) def _view_btn_clicked(self): aiftxt = ", ".join([str(v) for v in self._aif]) TextViewerDialog(self, title="AIF data", text=aiftxt).exec_() def _selection_changed(self): if self.method == "roi": return self._aif_points = [] for _col, points in self.ivl.picker.selection().items(): self._aif_points += list(points) self._recalc_aif() def _recalc_aif(self): self._aif = [] self._save_btn.setEnabled(True) self._view_btn.setEnabled(True) if self.qpdata is not None: if self.method == "roi": self._calc_aif_roi() else: self._calc_aif_points() self._update_plot() def _calc_aif_roi(self): if self.roi is None: return points = self.qpdata.raw()[self.roi.raw() > 0] if len(points) > 0: aif = None for sig in points: if aif is None: aif = np.zeros([len(sig)], dtype=np.float32) aif += sig self._aif = aif / len(points) def _calc_aif_points(self): aif = None num_points = 0 for point in self._aif_points: sig = self.qpdata.timeseries(point, grid=self.ivl.grid) self.debug("AIF signal: %s", sig) if aif is None: aif = np.zeros([len(sig)], dtype=np.float32) aif += sig num_points += 1 if num_points > 0: self._aif = aif / num_points def _update_plot(self): self._plot.clear() self._plot.add_line(self._aif, name="AIF")
class ResampleDataWidget(QpWidget): """ Widget that lets you resample data onto a different grid """ def __init__(self, **kwargs): super(ResampleDataWidget, self).__init__(name="Resample Data", icon="resample.png", desc="Resample data onto a different grid", group="Utilities", **kwargs) def init_ui(self): vbox = QtGui.QVBoxLayout() self.setLayout(vbox) vbox.addWidget(TitleWidget(self)) self.optbox = OptionBox("Resampling options") self.data = self.optbox.add("Data to resample", DataOption(self.ivm), key="data") self.resample_type = self.optbox.add( "Resampling method", ChoiceOption( ["On to grid from another data set", "Upsample", "Downsample"], ["data", "up", "down"]), key="type") self.grid_data = self.optbox.add("Use grid from", DataOption(self.ivm), key="grid") self.factor = self.optbox.add("Factor", NumericOption(default=2, minval=2, maxval=10, intonly=True), key="factor") self.slicewise = self.optbox.add("2D only", BoolOption(), key="2d") self.order = self.optbox.add( "Interpolation", ChoiceOption(["Nearest neighbour", "Linear", "Quadratic", "Cubic"], [0, 1, 2, 3], default=1), key="order") self.output_name = self.optbox.add("Output name", OutputNameOption(src_data=self.data, suffix="_res"), key="output-name") vbox.addWidget(self.optbox) self.resample_type.sig_changed.connect(self._resample_type_changed) self.run = RunButton("Resample", self._run) vbox.addWidget(self.run) vbox.addStretch(1) self._resample_type_changed() def _resample_type_changed(self): resample_type = self.resample_type.value self.optbox.set_visible("grid", resample_type == "data") self.optbox.set_visible("factor", resample_type != "data") self.optbox.set_visible("order", resample_type != "down") self.optbox.set_visible("2d", resample_type != "data") def batch_options(self): options = self.optbox.values() return "Resample", options def _run(self): _, options = self.batch_options() ResampleProcess(self.ivm).run(options)
class RoiBuilderWidget(QpWidget): """ Widget for building ROIs """ ADD = 1 ERASE = 2 MASK = 3 def __init__(self, **kwargs): super(RoiBuilderWidget, self).__init__(name="ROI Builder", icon="roi_builder", desc=DESC, group="ROIs", **kwargs) self._history = collections.deque(maxlen=10) self._tool = None self.grid = None self.roi = None self.roiname = None def init_ui(self): layout = QtGui.QVBoxLayout() title = TitleWidget(self, help="roibuilder", batch_btn=False) layout.addWidget(title) self.options = OptionBox("Options") btn = QtGui.QPushButton("New") btn.clicked.connect(self._new_roi) self.options.add("ROI", DataOption(self.ivm, rois=True, data=False), btn, key="roi") self.options.add("Current label", NumericOption(minval=1, slider=False, intonly=True), key="label") self.options.add("Label description", TextOption(), key="label_text") self.options.option("roi").sig_changed.connect(self._roi_changed) self.options.option("label").sig_changed.connect(self._label_changed) self.options.option("label_text").sig_changed.connect( self._label_text_changed) layout.addWidget(self.options) # Add toolbox buttons in a grid hbox = QtGui.QHBoxLayout() self._toolbox = QtGui.QGroupBox() self._toolbox.setTitle("Toolbox") self.tools_grid = QtGui.QGridLayout() self._toolbox.setLayout(self.tools_grid) x, y, cols = 0, 0, 4 for tool in TOOLS: self._add_tool(tool, y, x) x += 1 if x == cols: y += 1 x = 0 self._undo_btn = QtGui.QPushButton() self._undo_btn.clicked.connect(self.undo) self._undo_btn.setEnabled(False) undo_icon = QtGui.QIcon(get_icon("undo")) self._undo_btn.setIcon(undo_icon) self._undo_btn.setToolTip("Undo last action") self._undo_btn.setFixedSize(32, 32) self.tools_grid.addWidget(self._undo_btn, y, x) hbox.addWidget(self._toolbox) self._toolbox.setEnabled(False) hbox.addStretch(1) layout.addLayout(hbox) # Tool options box - initially invisible hbox = QtGui.QHBoxLayout() self._tool_options = QtGui.QGroupBox() self._tool_options.setVisible(False) hbox.addWidget(self._tool_options) hbox.addStretch(1) layout.addLayout(hbox) layout.addStretch(1) self.setLayout(layout) def activate(self): self._roi_changed() self.ivl.set_picker(PickMode.SINGLE) if self._tool is not None: self._tool.selected() def deactivate(self): self.ivl.set_picker(PickMode.SINGLE) if self._tool is not None: self._tool.deselected() def modify(self, vol=None, slice2d=None, points=None, mode=None): """ Make a change to the ROI we are building :param roi_new: Numpy array containing the updated ROI data on the current base grid :param vol: 3D 3D selection specified as binary Numpy array same shape as ROI :param slice2d: 2D slice selection specified as tuple of (2D binary Numpy array, axis index, position) :param points: Selection specified as list of 3D co-ordinates :param mode: ``ADD`` to add selection to the ROI (using current label), ``ERASE`` to erase selection from the ROI (set to 0), ``MASK`` to preserve ROI labels in selection but zero everything outside selection """ label = self.options.option("label").value self.debug("label=%i", label) # For undo functionality: selection is an object specifying which # points or ROI region were selected, data_orig is a corresponding # object which contains the data before the operation occurred. selection, data_orig = None, None if points is not None: selection = points data_orig = [ self.roidata[point[0], point[1], point[2]] for point in points ] for point in points: if mode == self.ADD: self.roidata[point[0], point[1], point[2]] = label elif mode == self.ERASE: self.roidata[point[0], point[1], point[2]] = 0 else: raise ValueError("Invalid mode: %i" % mode) else: # change_subset is a Numpy selection of the points to be # affected, data_new is a corresponding binary array identifying # the selected points in this subset to modify. if vol is not None: # Selection left as None to indicate whole volume selected_points = vol change_subset = self.roidata elif slice2d is not None: selected_points, axis, pos = slice2d slices = [slice(None)] * 3 slices[axis] = pos change_subset = self.roidata[slices] # Selection is the axis index and position selection = (axis, pos) else: raise ValueError( "Neither volume nor slice nor points provided") data_orig = np.copy(change_subset) if mode == self.ADD: self.debug("Adding: %i", np.count_nonzero(selected_points)) change_subset[selected_points > 0] = label elif mode == self.ERASE: self.debug("Erasing: %i", np.count_nonzero(selected_points)) change_subset[selected_points > 0] = 0 elif mode == self.MASK: self.debug("Masking: %i", np.count_nonzero(selected_points)) change_subset[selected_points == 0] = 0 else: raise ValueError("Invalid mode: %i" % mode) # Save the previous state of the data in the history list self._history.append((selection, data_orig)) self._undo_btn.setEnabled(True) # Update the ROI - note that the regions may have been affected so make # sure they are regenerated self._update_regions() self.ivl.redraw() self.debug("Now have %i nonzero", np.count_nonzero(self.roidata)) def _update_regions(self): """ ROI regions may have been created or added so regenerate them but put back existing label names """ current_regions = self.ivm.data[self.roiname].metadata.pop( "roi_regions", {}) new_regions = self.ivm.data[self.roiname].regions for label, desc in current_regions.items(): if label in new_regions: new_regions[label] = desc def undo(self): """ Undo the last change """ self.debug("ROI undo: %i", len(self._history)) if not self._history: return selection, data_orig = self._history.pop() # For selection, None indicates whole volume, tuple indicates # an (axis, pos) slice, otherwise we have a sequence of points if selection is None: self.roidata[:] = data_orig elif isinstance(selection, tuple): axis, pos = selection slices = [slice(None)] * 3 slices[axis] = pos self.roidata[slices] = data_orig else: for point, orig_value in zip(selection, data_orig): self.roidata[point[0], point[1], point[2]] = orig_value self._update_regions() self.ivl.redraw() self.debug("Now have %i nonzero", np.count_nonzero(self.roidata)) self._undo_btn.setEnabled(len(self._history) > 0) def _label_changed(self): self.debug("Label changed") roi = self.ivm.data.get(self.options.option("roi").value, None) if roi is not None: label = self.options.option("label").value self.debug(label) regions = roi.regions if label in regions: self.options.option("label_text").value = regions[label] else: self.options.option("label_text").value = "Region %i" % label def _label_text_changed(self): self.debug("Label text changed") roi = self.ivm.data.get(self.options.option("roi").value, None) if roi is not None: label = self.options.option("label").value label_text = self.options.option("label_text").value self.debug(label) self.debug(label_text) regions = roi.regions regions[label] = label_text def _roi_changed(self): roi = self.ivm.data.get(self.options.option("roi").value, None) self._toolbox.setEnabled(roi is not None) if roi is not None: # FIXME this will only work if ROI is NumpyData. Otherwise we are # manipulating a numpy array which may just be a proxy for the file # storage. regions = roi.regions current_label = self.options.option("label").value if self.roiname != roi.name or current_label not in regions.keys(): self.options.option("label").value = min( list(regions.keys()) + [ 1, ]) self.roiname = roi.name self.grid = roi.grid self.roidata = roi.raw() def _new_roi(self): dialog = QtGui.QDialog(self) dialog.setWindowTitle("New ROI") vbox = QtGui.QVBoxLayout() dialog.setLayout(vbox) optbox = OptionBox() optbox.add("ROI name", TextOption(), key="name") optbox.add("Data space from", DataOption(self.ivm), key="grid") vbox.addWidget(optbox) buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) vbox.addWidget(buttons) ok = dialog.exec_() if ok: roiname = optbox.option("name").value grid = self.ivm.data[optbox.option("grid").value].grid roidata = np.zeros(grid.shape, dtype=np.int) self.ivm.add(NumpyData(roidata, grid=grid, roi=True, name=roiname), make_current=True) # Throw away old history. FIXME is this right, should we keep existing data and history? # Also should we cache old history in case we go back to this ROI? self._history = [] self._undo_btn.setEnabled(False) self.options.option("roi").value = roiname def _tool_selected(self, tool): def _select(): if self._tool is not None: self._tool.btn.setStyleSheet("") self._tool.deselected() self._tool = tool self._tool.btn.setStyleSheet( "border: 2px solid QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffa02f, stop: 1 #d7801a);" ) # Replace the old tool options with the new one. Need to reparent the # existing layout to a temporary widget which will then get deleted QtGui.QWidget().setLayout(self._tool_options.layout()) self._tool_options.setLayout(self._tool.interface()) self._tool_options.setTitle(tool.name) self._tool_options.setVisible(True) self._tool.selected() return _select def _add_tool(self, tool, x, y): tool.ivm = self.ivm tool.ivl = self.ivl tool.builder = self btn = QtGui.QPushButton() btn.setIcon(QtGui.QIcon(get_icon(tool.name.lower()))) btn.setToolTip(tool.tooltip) btn.setFixedSize(32, 32) btn.clicked.connect(self._tool_selected(tool)) tool.btn = btn self.tools_grid.addWidget(btn, x, y)