class FilteringTool(ToolWidget): """View and calculate mean and meidan filtered images.""" methods: dict = { "Rolling Mean": { "filter": rolling_mean, "params": [ ("size", 5, (2.5, 99), lambda x: (x + 1) % 2 == 0), ("σ", 3.0, (0.0, np.inf), None), ], "desc": [ "Window size for local mean.", "Filter if > σ stddevs from mean." ], }, "Rolling Median": { "filter": rolling_median, "params": [ ("size", 5, (2.5, 99), lambda x: (x + 1) % 2 == 0), ("M", 3.0, (0.0, np.inf), None), ], "desc": [ "Window size for local median.", "Filter if > M medians from median.", ], }, # "Simple High-pass": { # "filter": simple_highpass, # "params": [ # ("min", 1e3, (-np.inf, np.inf), None), # ("replace", 0.0, (-np.inf, np.inf), None), # ], # "desc": ["Filter if below this value.", "Value to replace with."], # }, # "Simple Low-pass": { # "filter": simple_lowpass, # "params": [ # ("max", 1e3, (-np.inf, np.inf), None), # ("replace", 0.0, (-np.inf, np.inf), None), # ], # "desc": ["Filter if above this value.", "Value to replace with."], # }, } def __init__(self, widget: LaserWidget): super().__init__(widget, graphics_label="Preview") self.graphics = LaserGraphicsView(self.viewspace.options, parent=self) self.graphics.cursorValueChanged.connect( self.widget.updateCursorStatus) self.graphics.setMouseTracking(True) self.action_toggle_filter = qAction( "visibility", "Filter Visible", "Toggle visibility of the filtering.", self.toggleFilter, ) self.action_toggle_filter.setCheckable(True) self.button_hide_filter = qToolButton(action=self.action_toggle_filter) self.button_hide_filter.setToolButtonStyle( QtCore.Qt.ToolButtonTextBesideIcon) self.combo_element = QtWidgets.QComboBox() self.combo_element.activated.connect(self.completeChanged) self.combo_element.activated.connect(self.refresh) self.combo_filter = QtWidgets.QComboBox() self.combo_filter.addItems(FilteringTool.methods.keys()) self.combo_filter.setCurrentText("Rolling Median") self.combo_filter.activated.connect(self.filterChanged) self.combo_filter.activated.connect(self.completeChanged) self.combo_filter.activated.connect(self.refresh) nparams = np.amax( [len(f["params"]) for f in FilteringTool.methods.values()]) self.label_fparams = [QtWidgets.QLabel() for _ in range(nparams)] self.lineedit_fparams = [ValidColorLineEdit() for _ in range(nparams)] for le in self.lineedit_fparams: le.textEdited.connect(self.completeChanged) le.editingFinished.connect(self.refresh) le.setValidator( ConditionalLimitValidator(0.0, 0.0, 4, condition=None)) layout_graphics = QtWidgets.QVBoxLayout() layout_graphics.addWidget(self.graphics) layout_graphics.addWidget(self.combo_element, 0, QtCore.Qt.AlignRight) self.box_graphics.setLayout(layout_graphics) layout_controls = QtWidgets.QFormLayout() layout_controls.addWidget(self.combo_filter) for i in range(len(self.label_fparams)): layout_controls.addRow(self.label_fparams[i], self.lineedit_fparams[i]) layout_controls.addWidget(self.button_hide_filter) self.box_controls.setLayout(layout_controls) self.initialise() def apply(self) -> None: self.modified = True name = self.combo_element.currentText() if self.button_hide_filter.isChecked(): self.widget.laser.data[name] = self.previewData( self.widget.laser.data[name]) else: self.widget.laser.data[name] = self.graphics.data self.initialise() @property def fparams(self) -> List[float]: return [ float(le.text()) for le in self.lineedit_fparams if le.isEnabled() ] def filterChanged(self) -> None: filter_ = FilteringTool.methods[self.combo_filter.currentText()] # Clear all the current params for le in self.label_fparams: le.setVisible(False) for le in self.lineedit_fparams: le.setVisible(False) params: List[Tuple[str, float, Tuple, Callable[[float], bool]]] = filter_["params"] for i, (symbol, default, range, condition) in enumerate(params): self.label_fparams[i].setText(f"{symbol}:") self.label_fparams[i].setVisible(True) self.lineedit_fparams[i].validator().setRange( range[0], range[1], 4) self.lineedit_fparams[i].validator().setCondition(condition) self.lineedit_fparams[i].setVisible(True) self.lineedit_fparams[i].setToolTip(filter_["desc"][i]) # keep input that's still valid if not self.lineedit_fparams[i].hasAcceptableInput(): self.lineedit_fparams[i].setText(str(default)) self.lineedit_fparams[i].revalidate() def initialise(self) -> None: elements = self.widget.laser.elements self.combo_element.clear() self.combo_element.addItems(elements) self.filterChanged() self.refresh() def isComplete(self) -> bool: if not all(le.hasAcceptableInput() for le in self.lineedit_fparams if le.isEnabled()): return False return True def previewData(self, data: np.ndarray) -> np.ndarray: filter_ = FilteringTool.methods[ self.combo_filter.currentText()]["filter"] return filter_(data, *self.fparams) def refresh(self) -> None: if not self.isComplete(): # Not ready for update to preview return element = self.combo_element.currentText() data = self.widget.laser.get(element, flat=True, calibrated=False) if not self.button_hide_filter.isChecked(): data = self.previewData(data) if data is None: return x0, x1, y0, y1 = self.widget.laser.config.data_extent(data.shape) rect = QtCore.QRectF(x0, y0, x1 - x0, y1 - y0) self.graphics.drawImage(data, rect, element) self.graphics.label.setText(element) self.graphics.setOverlayItemVisibility() self.graphics.updateForeground() self.graphics.invalidateScene() def toggleFilter(self, hide: bool) -> None: if hide: self.button_hide_filter.setIcon(QtGui.QIcon.fromTheme("hint")) else: self.button_hide_filter.setIcon( QtGui.QIcon.fromTheme("visibility")) self.refresh()
class LaserWidget(_ViewWidget): """Class that stores and displays a laser image. Tracks modification of the data, config, calibration and information. Create via `:func:pewpew.laser.LaserView.addLaser`. Args: laser: input options: graphics options for this widget view: parent view """ def __init__(self, laser: Laser, options: GraphicsOptions, view: LaserView = None): super().__init__(view) self.laser = laser self.is_srr = isinstance(laser, SRRLaser) self.graphics = LaserGraphicsView(options, parent=self) self.graphics.setMouseTracking(True) self.graphics.cursorValueChanged.connect(self.updateCursorStatus) self.graphics.label.labelChanged.connect(self.renameCurrentElement) self.graphics.colorbar.editRequested.connect(self.actionRequestColorbarEdit) self.combo_layers = QtWidgets.QComboBox() self.combo_layers.addItem("*") self.combo_layers.addItems([str(i) for i in range(0, self.laser.layers)]) self.combo_layers.currentIndexChanged.connect(self.refresh) if not self.is_srr: self.combo_layers.setEnabled(False) self.combo_layers.setVisible(False) self.combo_element = LaserComboBox() self.combo_element.namesSelected.connect(self.updateNames) self.combo_element.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.combo_element.currentIndexChanged.connect(self.refresh) self.populateElements() self.action_calibration = qAction( "go-top", "Ca&libration", "Edit the documents calibration.", self.actionCalibration, ) self.action_config = qAction( "document-edit", "&Config", "Edit the document's config.", self.actionConfig ) self.action_copy_image = qAction( "insert-image", "Copy &Image", "Copy image to clipboard.", self.actionCopyImage, ) self.action_duplicate = qAction( "edit-copy", "Duplicate image", "Open a copy of the image.", self.actionDuplicate, ) self.action_export = qAction( "document-save-as", "E&xport", "Export documents.", self.actionExport ) self.action_export.setShortcut("Ctrl+X") # Add the export action so we can use it via shortcut self.addAction(self.action_export) self.action_information = qAction( "documentinfo", "In&formation", "View and edit image information.", self.actionInformation, ) self.action_save = qAction( "document-save", "&Save", "Save document to numpy archive.", self.actionSave ) self.action_save.setShortcut("Ctrl+S") # Add the save action so we can use it via shortcut self.addAction(self.action_save) self.action_statistics = qAction( "dialog-information", "Statistics", "Open the statisitics dialog.", self.actionStatistics, ) self.action_select_statistics = qAction( "dialog-information", "Selection Statistics", "Open the statisitics dialog for the current selection.", self.actionStatisticsSelection, ) self.action_colocalisation = qAction( "dialog-information", "Colocalisation", "Open the colocalisation dialog.", self.actionColocal, ) self.action_select_colocalisation = qAction( "dialog-information", "Selection Colocalisation", "Open the colocalisation dialog for the current selection.", self.actionColocalSelection, ) # Toolbar actions self.action_select_none = qAction( "transform-move", "Clear Selection", "Clear any selections.", self.graphics.endSelection, ) self.action_widget_none = qAction( "transform-move", "Clear Widgets", "Closes any open widgets.", self.graphics.endWidget, ) self.action_select_rect = qAction( "draw-rectangle", "Rectangle Selector", "Start the rectangle selector tool, use 'Shift' " "to add to selection and 'Control' to subtract.", self.graphics.startRectangleSelection, ) self.action_select_lasso = qAction( "draw-freehand", "Lasso Selector", "Start the lasso selector tool, use 'Shift' " "to add to selection and 'Control' to subtract.", self.graphics.startLassoSelection, ) self.action_select_dialog = qAction( "dialog-information", "Selection Dialog", "Start the selection dialog.", self.actionSelectDialog, ) self.action_select_copy_text = qAction( "insert-table", "Copy Selection as Text", "Copy the current selection to the clipboard as a column of text values.", self.actionCopySelectionText, ) self.action_select_crop = qAction( "transform-crop", "Crop to Selection", "Crop the image to the current selection.", self.actionCropSelection, ) self.selection_button = qToolButton("select", "Selection") self.selection_button.addAction(self.action_select_none) self.selection_button.addAction(self.action_select_rect) self.selection_button.addAction(self.action_select_lasso) self.selection_button.addAction(self.action_select_dialog) self.action_ruler = qAction( "tool-measure", "Measure", "Use a ruler to measure distance.", self.graphics.startRulerWidget, ) self.action_slice = qAction( "tool-measure", "1D Slice", "Select and display a 1-dimensional slice of the image.", self.graphics.startSliceWidget, ) self.widgets_button = qToolButton("tool-measure", "Widgets") self.widgets_button.addAction(self.action_widget_none) self.widgets_button.addAction(self.action_ruler) self.widgets_button.addAction(self.action_slice) self.action_zoom_in = qAction( "zoom-in", "Zoom to Area", "Start zoom area selection.", self.graphics.zoomStart, ) self.action_zoom_out = qAction( "zoom-original", "Reset Zoom", "Reset zoom to full image extent.", self.graphics.zoomReset, ) self.view_button = qToolButton("zoom", "Zoom") self.view_button.addAction(self.action_zoom_in) self.view_button.addAction(self.action_zoom_out) self.graphics.viewport().installEventFilter(DragDropRedirectFilter(self)) # Filters for setting active view self.graphics.viewport().installEventFilter(self) self.combo_element.installEventFilter(self) self.selection_button.installEventFilter(self) self.widgets_button.installEventFilter(self) self.view_button.installEventFilter(self) layout_bar = QtWidgets.QHBoxLayout() layout_bar.addWidget(self.selection_button, 0, QtCore.Qt.AlignLeft) layout_bar.addWidget(self.widgets_button, 0, QtCore.Qt.AlignLeft) layout_bar.addWidget(self.view_button, 0, QtCore.Qt.AlignLeft) layout_bar.addStretch(1) layout_bar.addWidget(self.combo_layers, 0, QtCore.Qt.AlignRight) layout_bar.addWidget(self.combo_element, 0, QtCore.Qt.AlignRight) layout = QtWidgets.QVBoxLayout() layout.addWidget(self.graphics, 1) layout.addLayout(layout_bar) self.setLayout(layout) @property def current_element(self) -> str: return self.combo_element.currentText() @current_element.setter def current_element(self, element: str) -> None: self.combo_element.setCurrentText(element) @property def current_layer(self) -> Optional[int]: if not self.is_srr or self.combo_layers.currentIndex() == 0: return None return int(self.combo_layers.currentText()) # Virtual def refresh(self) -> None: """Redraw image.""" self.graphics.drawLaser( self.laser, self.current_element, layer=self.current_layer ) if self.graphics.widget is not None: self.graphics.widget.imageChanged(self.graphics.image, self.graphics.data) self.graphics.invalidateScene() super().refresh() def rename(self, text: str) -> None: """Set the 'Name' value of laser information.""" self.laser.info["Name"] = text self.modified = True # Other def renameCurrentElement(self, new: str) -> None: """Rename a single element.""" self.laser.rename({self.current_element: new}) self.modified = True self.populateElements() self.current_element = new self.refresh() def laserName(self) -> str: return self.laser.info.get("Name", "<No Name>") def laserFilePath(self, ext: str = ".npz") -> Path: path = Path(self.laser.info.get("File Path", "")) return path.with_name(self.laserName() + ext) def populateElements(self) -> None: """Repopulate the element combo box.""" self.combo_element.blockSignals(True) self.combo_element.clear() self.combo_element.addItems(self.laser.elements) self.combo_element.blockSignals(False) def clearCursorStatus(self) -> None: """Clear window statusbar, if it exists.""" status_bar = self.viewspace.window().statusBar() if status_bar is not None: status_bar.clearMessage() def updateCursorStatus(self, x: float, y: float, v: float) -> None: """Updates the windows statusbar if it exists.""" status_bar = self.viewspace.window().statusBar() if status_bar is None: # pragma: no cover return if self.viewspace.options.units == "index": # convert to indices p = self.graphics.mapToData(QtCore.QPointF(x, y)) x, y = p.x(), p.y() if v is None: status_bar.clearMessage() elif np.isfinite(v): status_bar.showMessage(f"{x:.4g},{y:.4g} [{v:.4g}]") else: status_bar.showMessage(f"{x:.4g},{y:.4g} [nan]") def updateNames(self, rename: dict) -> None: """Rename multiple elements.""" current = self.current_element self.laser.rename(rename) self.populateElements() current = rename[current] self.current_element = current self.modified = True # Transformations def cropToSelection(self) -> None: """Crop image to current selection and open in a new tab. If selection is not rectangular then it is filled with nan. """ if self.is_srr: # pragma: no cover QtWidgets.QMessageBox.information( self, "Transform", "Unable to transform SRR data." ) return mask = self.graphics.mask if mask is None or np.all(mask == 0): # pragma: no cover return ix, iy = np.nonzero(mask) x0, x1, y0, y1 = np.min(ix), np.max(ix) + 1, np.min(iy), np.max(iy) + 1 data = self.laser.data new_data = np.empty((x1 - x0, y1 - y0), dtype=data.dtype) for name in new_data.dtype.names: new_data[name] = np.where( mask[x0:x1, y0:y1], data[name][x0:x1, y0:y1], np.nan ) info = self.laser.info.copy() info["Name"] = self.laserName() + "_cropped" info["File Path"] = str(Path(info.get("File Path", "")).with_stem(info["Name"])) new_widget = self.view.addLaser( Laser( new_data, calibration=self.laser.calibration, config=self.laser.config, info=info, ) ) new_widget.activate() def transform(self, flip: str = None, rotate: str = None) -> None: """Transform the image. Args: flip: flip the image ['horizontal', 'vertical'] rotate: rotate the image 90 degrees ['left', 'right'] """ if self.is_srr: # pragma: no cover QtWidgets.QMessageBox.information( self, "Transform", "Unable to transform SRR data." ) return if flip is not None: if flip in ["horizontal", "vertical"]: axis = 1 if flip == "horizontal" else 0 self.laser.data = np.flip(self.laser.data, axis=axis) else: raise ValueError("flip must be 'horizontal', 'vertical'.") if rotate is not None: if rotate in ["left", "right"]: k = 1 if rotate == "right" else 3 if rotate == "left" else 2 self.laser.data = np.rot90(self.laser.data, k=k, axes=(1, 0)) else: raise ValueError("rotate must be 'left', 'right'.") self.modified = True self.refresh() # Callbacks def applyCalibration(self, calibrations: Dict[str, Calibration]) -> None: """Set laser calibrations.""" modified = False for element in calibrations: if element in self.laser.calibration: self.laser.calibration[element] = copy.copy(calibrations[element]) modified = True if modified: self.modified = True self.refresh() def applyConfig(self, config: Config) -> None: """Set laser configuration.""" # Only apply if the type of config is correct if type(config) is type(self.laser.config): # noqa self.laser.config = copy.copy(config) self.modified = True self.refresh() def applyInformation(self, info: Dict[str, str]) -> None: """Set laser information.""" # if self.laser.info["Name"] != info["Name"]: # pragma: ignore # self.view.tabs.setTabText(self.index(), info["Name"]) if self.laser.info != info: self.laser.info = info self.modified = True def saveDocument(self, path: Union[str, Path]) -> None: """Saves the laser to an '.npz' file. See Also: `:func:pewlib.io.npz.save` """ if isinstance(path, str): path = Path(path) io.npz.save(path, self.laser) self.laser.info["File Path"] = str(path.resolve()) self.modified = False def actionCalibration(self) -> QtWidgets.QDialog: """Open a `:class:pewpew.widgets.dialogs.CalibrationDialog` and applies result.""" dlg = dialogs.CalibrationDialog( self.laser.calibration, self.current_element, parent=self ) dlg.calibrationSelected.connect(self.applyCalibration) dlg.calibrationApplyAll.connect(self.viewspace.applyCalibration) dlg.open() return dlg def actionConfig(self) -> QtWidgets.QDialog: """Open a `:class:pewpew.widgets.dialogs.ConfigDialog` and applies result.""" dlg = dialogs.ConfigDialog(self.laser.config, parent=self) dlg.configSelected.connect(self.applyConfig) dlg.configApplyAll.connect(self.viewspace.applyConfig) dlg.open() return dlg def actionCopyImage(self) -> None: self.graphics.copyToClipboard() def actionCopySelectionText(self) -> None: """Copies the currently selected data to the system clipboard.""" data = self.graphics.data[self.graphics.mask].ravel() html = ( '<meta http-equiv="content-type" content="text/html; charset=utf-8"/>' "<table>" ) text = "" for x in data: html += f"<tr><td>{x:.10g}</td></tr>" text += f"{x:.10g}\n" html += "</table>" mime = QtCore.QMimeData() mime.setHtml(html) mime.setText(text) QtWidgets.QApplication.clipboard().setMimeData(mime) def actionCropSelection(self) -> None: self.cropToSelection() def actionDuplicate(self) -> None: """Duplicate document to a new tab.""" self.view.addLaser(copy.deepcopy(self.laser)) def actionExport(self) -> QtWidgets.QDialog: """Opens a `:class:pewpew.exportdialogs.ExportDialog`. This can save the document to various formats. """ dlg = exportdialogs.ExportDialog(self, parent=self) dlg.open() return dlg def actionInformation(self) -> QtWidgets.QDialog: """Opens a `:class:pewpew.widgets.dialogs.InformationDialog`.""" dlg = dialogs.InformationDialog(self.laser.info, parent=self) dlg.infoChanged.connect(self.applyInformation) dlg.open() return dlg def actionRequestColorbarEdit(self) -> None: if self.viewspace is not None: self.viewspace.colortableRangeDialog() def actionSave(self) -> QtWidgets.QDialog: """Save the document to an '.npz' file. If not already associated with an '.npz' path a dialog is opened to select one. """ path = Path(self.laser.info["File Path"]) if path.suffix.lower() == ".npz" and path.exists(): self.saveDocument(path) return None else: path = self.laserFilePath() dlg = QtWidgets.QFileDialog( self, "Save File", str(path.resolve()), "Numpy archive(*.npz);;All files(*)" ) dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) dlg.fileSelected.connect(self.saveDocument) dlg.open() return dlg def actionSelectDialog(self) -> QtWidgets.QDialog: """Open a `:class:pewpew.widgets.dialogs.SelectionDialog` and applies selection.""" dlg = dialogs.SelectionDialog(self.graphics, parent=self) dlg.maskSelected.connect(self.graphics.drawSelectionImage) self.refreshed.connect(dlg.refresh) dlg.show() return dlg def actionStatistics(self, crop_to_selection: bool = False) -> QtWidgets.QDialog: """Open a `:class:pewpew.widgets.dialogs.StatsDialog` with image data. Args: crop_to_selection: pass current selection as a mask """ data = self.laser.get(calibrate=self.viewspace.options.calibrate, flat=True) mask = self.graphics.mask if mask is None or not crop_to_selection: mask = np.ones(data.shape, dtype=bool) units = {} if self.viewspace.options.calibrate: units = {k: v.unit for k, v in self.laser.calibration.items()} dlg = dialogs.StatsDialog( data, mask, units, self.current_element, pixel_size=( self.laser.config.get_pixel_width(), self.laser.config.get_pixel_height(), ), parent=self, ) dlg.open() return dlg def actionStatisticsSelection(self) -> QtWidgets.QDialog: return self.actionStatistics(True) def actionColocal(self, crop_to_selection: bool = False) -> QtWidgets.QDialog: """Open a `:class:pewpew.widgets.dialogs.ColocalisationDialog` with image data. Args: crop_to_selection: pass current selection as a mask """ data = self.laser.get(flat=True) mask = self.graphics.mask if crop_to_selection else None dlg = dialogs.ColocalisationDialog(data, mask, parent=self) dlg.open() return dlg def actionColocalSelection(self) -> QtWidgets.QDialog: return self.actionColocal(True) # Events def contextMenuEvent(self, event: QtGui.QContextMenuEvent): menu = QtWidgets.QMenu(self) # menu.addAction(self.action_duplicate) menu.addAction(self.action_copy_image) menu.addSeparator() if self.graphics.posInSelection(event.pos()): menu.addAction(self.action_select_copy_text) menu.addAction(self.action_select_crop) menu.addSeparator() menu.addAction(self.action_select_statistics) menu.addAction(self.action_select_colocalisation) else: menu.addAction(self.view.action_open) menu.addAction(self.action_save) menu.addAction(self.action_export) menu.addSeparator() menu.addAction(self.action_config) menu.addAction(self.action_calibration) menu.addAction(self.action_information) menu.addSeparator() menu.addAction(self.action_statistics) menu.addAction(self.action_colocalisation) menu.popup(event.globalPos()) event.accept() def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: if event.matches(QtGui.QKeySequence.Cancel): self.graphics.endSelection() self.graphics.endWidget() elif event.matches(QtGui.QKeySequence.Paste): mime = QtWidgets.QApplication.clipboard().mimeData() if mime.hasFormat("arplication/x-pew2config"): with BytesIO(mime.data("application/x-pew2config")) as fp: array = np.load(fp) if self.is_srr: config = SRRConfig.from_array(array) else: config = Config.from_array(array) self.applyConfig(config) elif mime.hasFormat("application/x-pew2calibration"): with BytesIO(mime.data("application/x-pew2calibration")) as fp: npy = np.load(fp) calibrations = {k: Calibration.from_array(npy[k]) for k in npy} self.applyCalibration(calibrations) super().keyPressEvent(event) def showEvent(self, event: QtGui.QShowEvent) -> None: self.refresh() super().showEvent(event)
class CalculatorTool(ToolWidget): """Calculator for element data operations.""" functions = { "abs": ( (UnaryFunction("abs"), "(<x>)", "The absolute value of <x>."), (np.abs, 1), ), "kmeans": ( ( BinaryFunction("kmeans"), "(<x>, <k>)", "Returns lower bounds of 1 to <k> kmeans clusters.", ), (kmeans.thresholds, 2), ), "mask": ( ( BinaryFunction("mask"), "(<x>, <mask>)", "Selects <x> where <mask>, otherwise NaN.", ), (lambda x, m: np.where(m, x, np.nan), 2), ), "mean": ( (UnaryFunction("mean"), "(<x>)", "Returns the mean of <x>."), (np.nanmean, 1), ), "median": ( ( UnaryFunction("median"), "(<x>)", "Returns the median of <x>.", ), (np.nanmedian, 1), ), "nantonum": ( (UnaryFunction("nantonum"), "(<x>)", "Sets nan values to 0."), (np.nan_to_num, 1), ), "normalise": ( ( TernaryFunction("normalise"), "(<x>, <min>, <max>)", "Normalise <x> from from <min> to <max>.", ), (normalise, 3), ), "otsu": ( ( UnaryFunction("otsu"), "(<x>)", "Returns Otsu's threshold for <x>.", ), (otsu, 1), ), "percentile": ( ( BinaryFunction("percentile"), "(<x>, <percent>)", "Returns the <percent> percentile of <x>.", ), (np.nanpercentile, 2), ), "segment": ( ( BinaryFunction("segment"), "(<x>, <threshold(s)>)", "Create a masking image from the given thrshold(s).", ), (segment_image, 2), ), "threshold": ( ( BinaryFunction("threshold"), "(<x>, <value>)", "Sets <x> below <value> to NaN.", ), (lambda x, a: np.where(x > a, x, np.nan), 2), ), } def __init__(self, widget: LaserWidget): super().__init__(widget, graphics_label="Preview") self.graphics = LaserGraphicsView(self.viewspace.options, parent=self) self.graphics.cursorValueChanged.connect(self.widget.updateCursorStatus) self.graphics.setMouseTracking(True) self.output = QtWidgets.QLineEdit("Result") self.output.setEnabled(False) self.lineedit_name = CalculatorName( "", badnames=[], badparser=list(CalculatorTool.functions.keys()), ) self.lineedit_name.revalidate() self.lineedit_name.textEdited.connect(self.completeChanged) self.lineedit_name.editingFinished.connect(self.refresh) self.combo_element = QtWidgets.QComboBox() self.combo_element.activated.connect(self.insertVariable) functions = [k + v[0][1] for k, v in CalculatorTool.functions.items()] tooltips = [v[0][2] for v in CalculatorTool.functions.values()] self.combo_function = QtWidgets.QComboBox() self.combo_function.addItem("Functions") self.combo_function.addItems(functions) for i in range(0, len(tooltips)): self.combo_function.setItemData(i + 1, tooltips[i], QtCore.Qt.ToolTipRole) self.combo_function.activated.connect(self.insertFunction) self.reducer = Reducer({}) self.formula = CalculatorFormula("", variables=[]) self.formula.textChanged.connect(self.completeChanged) self.formula.textChanged.connect(self.refresh) self.reducer.operations.update( {k: v[1] for k, v in CalculatorTool.functions.items()} ) self.formula.parser.nulls.update( {k: v[0][0] for k, v in CalculatorTool.functions.items()} ) layout_combos = QtWidgets.QHBoxLayout() layout_combos.addWidget(self.combo_element) layout_combos.addWidget(self.combo_function) layout_graphics = QtWidgets.QVBoxLayout() layout_graphics.addWidget(self.graphics) self.box_graphics.setLayout(layout_graphics) layout_controls = QtWidgets.QFormLayout() layout_controls.addRow("Name:", self.lineedit_name) layout_controls.addRow("Insert:", layout_combos) layout_controls.addRow("Formula:", self.formula) layout_controls.addRow("Result:", self.output) self.box_controls.setLayout(layout_controls) self.initialise() # refreshes def apply(self) -> None: self.modified = True name = self.lineedit_name.text() data = self.reducer.reduce(self.formula.expr) if name in self.widget.laser.elements: self.widget.laser.data[name] = data else: self.widget.laser.add(self.lineedit_name.text(), data) # Make sure to repop elements self.widget.populateElements() self.initialise() def initialise(self) -> None: elements = self.widget.laser.elements self.combo_element.clear() self.combo_element.addItem("Elements") self.combo_element.addItems(elements) name = "calc0" i = 1 while name in elements: name = f"calc{i}" i += 1 self.lineedit_name.setText(name) self.formula.parser.variables = elements self.formula.setCompleter( QtWidgets.QCompleter( list(self.formula.parser.variables) + [k + "(" for k in CalculatorTool.functions.keys()] ) ) self.formula.valid = True self.formula.setText(self.widget.combo_element.currentText()) # refreshes def insertFunction(self, index: int) -> None: if index == 0: return function = self.combo_function.itemText(index) function = function[: function.find("(") + 1] self.formula.insertPlainText(function) self.combo_function.setCurrentIndex(0) self.formula.setFocus() def insertVariable(self, index: int) -> None: if index == 0: return self.formula.insertPlainText(self.combo_element.itemText(index)) self.combo_element.setCurrentIndex(0) self.formula.setFocus() def isComplete(self) -> bool: if not self.formula.hasAcceptableInput(): return False if not self.lineedit_name.hasAcceptableInput(): return False return True def previewData(self, data: np.ndarray) -> Optional[np.ndarray]: self.reducer.variables = {name: data[name] for name in data.dtype.names} try: result = self.reducer.reduce(self.formula.expr) if np.isscalar(result): self.output.setText(f"{result:.10g}") return None elif isinstance(result, np.ndarray) and result.ndim == 1: self.output.setText(f"{list(map('{:.4g}'.format, result))}") return None elif isinstance(result, np.ndarray): self.output.setText(f"{result.dtype.name} array: {result.shape}") return result except (ReducerException, ValueError) as e: self.output.setText(str(e)) return None def refresh(self) -> None: if not self.isComplete(): # Not ready for update to preview return data = self.previewData(self.widget.laser.get(flat=True, calibrated=False)) if data is None: return x0, x1, y0, y1 = self.widget.laser.config.data_extent(data.shape) rect = QtCore.QRectF(x0, y0, x1 - x0, y1 - y0) self.graphics.drawImage(data, rect, self.lineedit_name.text()) self.graphics.label.setText(self.lineedit_name.text()) self.graphics.setOverlayItemVisibility() self.graphics.updateForeground() self.graphics.invalidateScene()