def _update_preview_image(image_data: Optional[np.ndarray], image: ImageItem, redraw_histogram: Optional[Callable[[Any], None]]): image.clear() image.setImage(image_data) if redraw_histogram: # Update histogram redraw_histogram(image.getHistogram())
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imgdata = None self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] self.setImage(data) except IndexError: self.imageitem.clear() self.setText("UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imgdata = imgdata self.imageitem.setImage(np.log(self.imgdata * (self.imgdata > 0) + (self.imgdata < 1)), autoLevels=True) self.imageitem.setTransform( QTransform(1, 0, 0, -1, 0, self.imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imgdata = None self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) @threads.method(threadkey="preview", showBusy=False) def preview(self, data): if isinstance(data, NonDBHeader): self.preview_header(data) else: self.preview_catalog(data) @staticmethod def guess_stream_field(catalog: BlueskyRun): # TODO: use some metadata (techniques?) for guidance about how to get a preview streams = bluesky_utils.streams_from_run(catalog) if "primary" in streams: streams.remove("primary") streams.insert(0, "primary") for stream in streams: descriptor = bluesky_utils.descriptors_from_stream( catalog, stream)[0] fields = bluesky_utils.fields_from_descriptor(descriptor) for field in fields: field_ndims = bluesky_utils.ndims_from_descriptor( descriptor, field) if field_ndims > 1: return stream, field def preview_catalog(self, catalog: BlueskyRun): threads.invoke_in_main_thread(self.setText, "LOADING...") try: stream, field = self.guess_stream_field(catalog) data = getattr(catalog, stream).to_dask()[field].squeeze() for i in range(len(data.shape) - 2): data = data[0] threads.invoke_in_main_thread(self.setImage, np.asarray(data.compute())) except Exception as ex: msg.logError(ex) threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] threads.invoke_in_main_thread(self.setImage, data) except IndexError: threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imgdata = imgdata self.imageitem.setImage(np.log(self.imgdata * (self.imgdata > 0) + (self.imgdata < 1)), autoLevels=True) self.imageitem.setTransform( QTransform(1, 0, 0, -1, 0, self.imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
class FilterPreviews(GraphicsLayoutWidget): image_before: ImageItem image_after: ImageItem image_diff: ImageItem histogram_before: Optional[PlotItem] histogram_after: Optional[PlotItem] histogram: Optional[PlotItem] def __init__(self, parent=None, **kwargs): super(FilterPreviews, self).__init__(parent, **kwargs) widget_location = self.mapToGlobal(QPoint(self.width() / 2, 0)) # allow the widget to take up to 80% of the desktop's height self.ALLOWED_HEIGHT: QRect = QGuiApplication.screenAt(widget_location).availableGeometry().height() * 0.8 self.before_histogram_data = None self.after_histogram_data = None self.histogram = None self.before_histogram = None self.after_histogram = None self.combined_histograms = True self.histogram_legend_visible = True self.addLabel("Image before") self.addLabel("Image after") self.addLabel("Image difference") self.nextRow() self.image_before, self.image_before_vb, self.image_before_hist = self.image_in_vb(name="before") self.image_after, self.image_after_vb, self.image_after_hist = self.image_in_vb(name="after") self.image_difference, self.image_difference_vb, self.image_difference_hist = self.image_in_vb( name="difference") self.image_after_overlay = ImageItem() self.image_after_overlay.setZValue(10) self.image_after_vb.addItem(self.image_after_overlay) # Ensure images resize equally self.image_layout: GraphicsLayout = self.addLayout(colspan=6) self.image_layout.addItem(self.image_before_vb, 0, 0) self.image_layout.addItem(self.image_before_hist, 0, 1) self.image_layout.addItem(self.image_after_vb, 0, 2) self.image_layout.addItem(self.image_after_hist, 0, 3) self.image_layout.addItem(self.image_difference_vb, 0, 4) self.image_layout.addItem(self.image_difference_hist, 0, 5) self.nextRow() before_details = self.addLabel("") after_details = self.addLabel("") difference_details = self.addLabel("") self.display_formatted_detail = { self.image_before: lambda val: before_details.setText(f"Before: {val:.6f}"), self.image_after: lambda val: after_details.setText(f"After: {val:.6f}"), self.image_difference: lambda val: difference_details.setText(f"Difference: {val:.6f}"), } for img in self.image_before, self.image_after, self.image_difference: img.hoverEvent = lambda ev: self.mouse_over(ev) self.init_histogram() def resizeEvent(self, ev: QResizeEvent): if ev is not None: size = ev.size() self.image_layout.setFixedHeight(min(size.height() * 0.7, self.ALLOWED_HEIGHT)) super().resizeEvent(ev) def image_in_vb(self, name=None): im = ImageItem() vb = ViewBox(invertY=True, lockAspect=True, name=name) vb.addItem(im) hist = HistogramLUTItem(im) return im, vb, hist def clear_items(self): self.image_before.clear() self.image_after.clear() self.image_difference.clear() self.image_after_overlay.clear() def init_histogram(self): self.histogram = self.addPlot(row=histogram_coords["combined"].row, col=histogram_coords["combined"].col, labels=histogram_axes_labels, lockAspect=True, colspan=3) self.addLabel("Pixel values", row=label_coords["combined"].row, col=label_coords["combined"].col) self.legend = self.histogram.addLegend() def update_histogram_data(self): # Plot any histogram that has data, and add a legend if both exist before_data = self.image_before.getHistogram() after_data = self.image_after.getHistogram() if _data_valid_for_histogram(before_data): if self.combined_histograms: before_plot = self.histogram.plot(*before_data, pen=before_pen, clear=True) self.legend.addItem(before_plot, "Before") else: self.before_histogram.plot(*before_data, pen=before_pen, clear=True) if _data_valid_for_histogram(after_data): if self.combined_histograms: after_plot = self.histogram.plot(*after_data, pen=after_pen) self.legend.addItem(after_plot, "After") else: self.after_histogram.plot(*after_data, pen=after_pen, clear=True) def init_separate_histograms(self): hc = histogram_coords self.before_histogram = self.addPlot(row=hc["before"].row, col=hc["before"].col, labels=histogram_axes_labels, lockAspect=True) self.after_histogram = self.addPlot(row=hc["after"].row, col=hc["after"].col, labels=histogram_axes_labels, lockAspect=True) lc = label_coords self.addLabel("Pixel values before", row=lc["before"].row, col=lc["before"].col) self.addLabel("Pixel values after", row=lc["after"].row, col=lc["after"].col) if _data_valid_for_histogram(self.before_histogram_data): self.before_histogram.plot(*self.before_histogram_data, pen=before_pen) if _data_valid_for_histogram(self.after_histogram_data): self.after_histogram.plot(*self.after_histogram_data, pen=after_pen) def delete_histograms(self): coords = set(c for c in histogram_coords.values()) histograms = (self.getItem(*coord) for coord in coords) for histogram in filter(lambda h: h is not None, histograms): self.removeItem(histogram) self.histogram = None self.before_histogram = None self.after_histogram = None def delete_histogram_labels(self): coords = set(c for c in label_coords.values()) labels = (self.getItem(*coord) for coord in coords) for label in filter(lambda h: h is not None, labels): self.removeItem(label) @property def histogram_legend(self) -> Optional[LegendItem]: if self.histogram and self.histogram.legend: return self.histogram.legend return None def mouse_over(self, ev): # Ignore events triggered by leaving window or right clicking if ev.exit: return pos = CloseEnoughPoint(ev.pos()) for img in self.image_before, self.image_after, self.image_difference: if img.image is not None and pos.x < img.image.shape[0] and pos.y < img.image.shape[1]: pixel_value = img.image[pos.y, pos.x] self.display_formatted_detail[img](pixel_value) def link_all_views(self): for view1, view2 in [[self.image_before_vb, self.image_after_vb], [self.image_after_vb, self.image_difference_vb], [self.image_after_hist.vb, self.image_before_hist.vb]]: view1.linkView(ViewBox.XAxis, view2) view1.linkView(ViewBox.YAxis, view2) def unlink_all_views(self): for view in self.image_before_vb, self.image_after_vb, self.image_after_hist.vb: view.linkView(ViewBox.XAxis, None) view.linkView(ViewBox.YAxis, None) def add_difference_overlay(self, diff): diff = -diff diff[diff > 0.0] = 1.0 pos = np.array([0, 1]) color = np.array([[0, 0, 0, 0], [255, 0, 0, 255]], dtype=np.ubyte) map = ColorMap(pos, color) self.image_after_overlay.setOpacity(1) self.image_after_overlay.setImage(diff) lut = map.getLookupTable(0, 1, 2) self.image_after_overlay.setLookupTable(lut) def hide_difference_overlay(self): self.image_after_overlay.setOpacity(0) def auto_range(self): # This will cause the previews to all show by just causing autorange on self.image_before_vb self.image_before_vb.autoRange()
class PsdWaterfallPlotWidget(GraphicsLayoutWidget): """This class manages and displays the power spectrum distribution (PSD) data in a waterfall plot. Attributes ---------- arraySize : int The size of the data array to display. boundingRect : QtCore.QRectF The actual coordinate space base on frequency and time of acquisition. data : numpy.ndarray The 2D array for the PSD data. image : pyqtgraph.ImageItem The instance of the image item for display. timeScale : float The total time for the buffer to accumulate at the ROI FPS. """ def __init__(self, parent=None): """Initialize the class. Parameters ---------- parent : None, optional Top-level widget. """ super().__init__(parent) self.plot = self.addPlot() self.plot.invertY() self.image = ImageItem() self.image.setOpts(axisOrder='row-major') self.plot.addItem(self.image) self.data = None self.arraySize = None self.boundingRect = None self.timeScale = None self.colorMap = 'viridis' self.image.setLookupTable(getLutFromColorMap(self.colorMap)) def clearPlot(self): """Reset all data and clear the plot. """ self.data = None self.boundingRect = None self.image.clear() def getConfiguration(self): """Get the current plot configuration. Returns ------- int, str The set of current configuration parameters. """ return self.arraySize, self.colorMap def setConfiguration(self, config): """Set the new parameters into the widget. Parameters ---------- config : `config.PsdPlotConfig` The new parameters to apply. """ numBins = config.numWaterfallBins if self.arraySize != numBins: self.arraySize = numBins # Invalidate data self.data = None self.boundingRect = None colorMap = config.waterfallColorMap if self.colorMap != colorMap: self.colorMap = colorMap self.image.setLookupTable(getLutFromColorMap(self.colorMap)) def setTimeScale(self, timeScale): """Update the stored timescale and invalidate data and bounding rect. Parameters ---------- timeScale : float The new timescale. """ self.timeScale = timeScale self.data = None self.boundingRect = None def setup(self, arraySize, timeScale, axisLabel): """Setup the widget with the array size. Parameters ---------- arraySize : int The size fo the data array to display in terms of history. timeScale : float The total time for the buffer to accumulate at the ROI FPS. axisLabel : str Label for particular centroid coordinate. """ self.arraySize = arraySize self.timeScale = timeScale self.plot.setLabel('bottom', '{} {}'.format(axisLabel, HTML_NU), units='Hz') self.plot.setLabel('left', 'Time', units='s') def updatePlot(self, psd, freqs): """Update the current plot with the given data. Parameters ---------- psd : numpy.array The PSD data of a given centroid coordinate. freqs : numpy.array The frequency array associated with the PSD data. """ if self.data is None: self.data = np.zeros((self.arraySize, psd.size)) else: self.data[1:, ...] = self.data[:-1, ...] self.data[0, ...] = np.log(psd) self.image.setImage(self.data) if self.boundingRect is None: self.boundingRect = QtCore.QRectF(0, 0, freqs[-1], self.arraySize * self.timeScale) self.image.setRect(self.boundingRect)
class MIMiniImageView(GraphicsLayout, BadDataOverlay): def __init__(self, name: str = "MIMiniImageView"): super().__init__() self.name = name.title() self.im = ImageItem() self.vb = ViewBox(invertY=True, lockAspect=True, name=name) self.vb.addItem(self.im) self.hist = HistogramLUTItem(self.im) graveyard.append(self.vb) # Sub-layout prevents resizing issues when details text changes image_layout = self.addLayout(colspan=2) image_layout.addItem(self.vb) image_layout.addItem(self.hist) self.hist.setFixedWidth(100) # HistogramLUTItem used pixel sizes self.nextRow() self.details = self.addLabel("", colspan=2) self.im.hoverEvent = lambda ev: self.mouse_over(ev) self.axis_siblings: "WeakSet[MIMiniImageView]" = WeakSet() self.histogram_siblings: "WeakSet[MIMiniImageView]" = WeakSet() @property def image_item(self) -> ImageItem: return self.im @property def viewbox(self) -> ViewBox: return self.vb def clear(self): self.im.clear() def setImage(self, *args, **kwargs): self.im.setImage(*args, **kwargs) self.check_for_bad_data() @staticmethod def set_siblings(sibling_views: List["MIMiniImageView"], axis=False, hist=False): for view1 in sibling_views: for view2 in sibling_views: if view2 is not view1: if axis: view1.add_axis_sibling(view2) if hist: view1.add_hist_sibling(view2) def add_axis_sibling(self, sibling: "MIMiniImageView"): self.axis_siblings.add(sibling) def add_hist_sibling(self, sibling: "MIMiniImageView"): self.histogram_siblings.add(sibling) def get_parts(self) -> Tuple[ImageItem, ViewBox, HistogramLUTItem]: return self.im, self.vb, self.hist def mouse_over(self, ev): # Ignore events triggered by leaving window or right clicking if ev.exit: return pos = CloseEnoughPoint(ev.pos()) self.show_value(pos) for img_view in self.axis_siblings: img_view.show_value(pos) def show_value(self, pos): image = self.im.image if image is not None and pos.y < image.shape[ 0] and pos.x < image.shape[1]: pixel_value = image[pos.y, pos.x] value_string = ("%.6f" % pixel_value)[:8] self.details.setText(f"{self.name}: {value_string}") def link_sibling_axis(self): # Linking multiple viewboxes with locked aspect ratios causes # odd resizing behaviour. Use workaround from # https://github.com/pyqtgraph/pyqtgraph/issues/1348 self.vb.setAspectLocked(True) for view1, view2 in pairwise(chain([self], self.axis_siblings)): view2.vb.linkView(ViewBox.XAxis, view1.vb) view2.vb.linkView(ViewBox.YAxis, view1.vb) view2.vb.setAspectLocked(False) def unlink_sibling_axis(self): for img_view in chain([self], self.axis_siblings): img_view.vb.linkView(ViewBox.XAxis, None) img_view.vb.linkView(ViewBox.YAxis, None) img_view.vb.setAspectLocked(True) def link_sibling_histogram(self): for view1, view2 in pairwise(chain([self], self.histogram_siblings)): view1.hist.vb.linkView(ViewBox.YAxis, view2.hist.vb) for img_view in chain([self], self.histogram_siblings): img_view.hist.sigLevelChangeFinished.connect( img_view.update_sibling_histograms) def unlink_sibling_histogram(self): for img_view in chain([self], self.histogram_siblings): img_view.hist.vb.linkView(ViewBox.YAxis, None) try: img_view.hist.sigLevelChangeFinished.disconnect() except TypeError: # This is expected if there are slots currently connected pass def update_sibling_histograms(self): hist_range = self.hist.getLevels() for img_view in self.histogram_siblings: with BlockQtSignals(img_view.hist): img_view.hist.setLevels(*hist_range)
class FilterPreviews(GraphicsLayoutWidget): image_before: ImageItem image_after: ImageItem image_diff: ImageItem histogram_before: Optional[PlotItem] histogram_after: Optional[PlotItem] histogram: Optional[PlotItem] def __init__(self, parent=None, **kwargs): super(FilterPreviews, self).__init__(parent, **kwargs) self.before_histogram_data = None self.after_histogram_data = None self.histogram = None self.before_histogram = None self.after_histogram = None self.combined_histograms = True self.histogram_legend_visible = True self.addLabel("Image before") self.addLabel("Image after") self.addLabel("Image difference") self.nextRow() self.image_before, self.image_before_vb = self.image_in_vb( name="before") self.image_after, self.image_after_vb = self.image_in_vb(name="after") self.image_difference, self.image_difference_vb = self.image_in_vb( name="difference") self.image_after_overlay = ImageItem() self.image_after_overlay.setZValue(10) self.image_after_vb.addItem(self.image_after_overlay) # Ensure images resize equally image_layout = self.addLayout(colspan=3) image_layout.addItem(self.image_before_vb, 0, 0) image_layout.addItem(self.image_after_vb, 0, 1) image_layout.addItem(self.image_difference_vb, 0, 2) self.nextRow() before_details = self.addLabel("") after_details = self.addLabel("") difference_details = self.addLabel("") self.display_formatted_detail = { self.image_before: lambda val: before_details.setText(f"Before: {val:.6f}"), self.image_after: lambda val: after_details.setText(f"After: {val:.6f}"), self.image_difference: lambda val: difference_details.setText(f"Difference: {val:.6f}"), } for img in self.image_before, self.image_after, self.image_difference: img.hoverEvent = lambda ev: self.mouse_over(ev) def image_in_vb(self, name=None): im = ImageItem() im.setAutoDownsample(False) vb = ViewBox(invertY=True, lockAspect=True, name=name) vb.addItem(im) return im, vb def clear_items(self): self.image_before.clear() self.image_after.clear() self.image_difference.clear() self.image_after_overlay.clear() self.delete_histograms() # There seems to be a bug with pyqtgraph.PlotDataItem.setData not forcing a redraw. # We work around this by redrawing everything completely every time, which is unreasonably fast anyway. def redraw_histograms(self): self.delete_histograms() self.delete_histogram_labels() if self.combined_histograms: self.draw_combined_histogram() else: self.draw_separate_histograms() def delete_histograms(self): coords = set(c for c in histogram_coords.values()) histograms = (self.getItem(*coord) for coord in coords) for histogram in filter(lambda h: h is not None, histograms): self.removeItem(histogram) self.histogram = None self.before_histogram = None self.after_histogram = None self.diff_histogram = None def delete_histogram_labels(self): coords = set(c for c in label_coords.values()) labels = (self.getItem(*coord) for coord in coords) for label in filter(lambda h: h is not None, labels): self.removeItem(label) def draw_combined_histogram(self): self.histogram = self.addPlot(row=histogram_coords["combined"].row, col=histogram_coords["combined"].col, labels=histogram_axes_labels, lockAspect=True, colspan=3) self.addLabel("Pixel values", row=label_coords["combined"].row, col=label_coords["combined"].col) legend = self.histogram.addLegend() # Plot any histogram that has data, and add a legend if both exist if _data_valid_for_histogram(self.before_histogram_data): before_plot = self.histogram.plot(*self.before_histogram_data, pen=before_pen) legend.addItem(before_plot, "Before") if _data_valid_for_histogram(self.after_histogram_data): after_plot = self.histogram.plot(*self.after_histogram_data, pen=after_pen) legend.addItem(after_plot, "After") def draw_separate_histograms(self): hc = histogram_coords self.before_histogram = self.addPlot(row=hc["before"].row, col=hc["before"].col, labels=histogram_axes_labels, lockAspect=True) self.after_histogram = self.addPlot(row=hc["after"].row, col=hc["after"].col, labels=histogram_axes_labels, lockAspect=True) lc = label_coords self.addLabel("Pixel values before", row=lc["before"].row, col=lc["before"].col) self.addLabel("Pixel values after", row=lc["after"].row, col=lc["after"].col) if _data_valid_for_histogram(self.before_histogram_data): self.before_histogram.plot(*self.before_histogram_data, pen=before_pen) if _data_valid_for_histogram(self.after_histogram_data): self.after_histogram.plot(*self.after_histogram_data, pen=after_pen) def set_before_histogram(self, data: Tuple[ndarray]): self.before_histogram_data = data self.redraw_histograms() def set_after_histogram(self, data: Tuple[ndarray]): self.after_histogram_data = data self.redraw_histograms() @property def histogram_legend(self) -> Optional[LegendItem]: if self.histogram and self.histogram.legend: return self.histogram.legend return None def mouse_over(self, ev): # Ignore events triggered by leaving window or right clicking if ev.exit: return pos = CloseEnoughPoint(ev.pos()) for img in self.image_before, self.image_after, self.image_difference: if img.image is not None and pos.x < img.image.shape[ 0] and pos.y < img.image.shape[1]: pixel_value = img.image[pos.y, pos.x] self.display_formatted_detail[img](pixel_value) def link_all_views(self): for view1, view2 in zip( [self.image_before_vb, self.image_after_vb], [self.image_after_vb, self.image_difference_vb]): view1.linkView(ViewBox.XAxis, view2) view1.linkView(ViewBox.YAxis, view2) def unlink_all_views(self): for view in self.image_before_vb, self.image_after_vb, self.image_difference_vb: view.linkView(ViewBox.XAxis, None) view.linkView(ViewBox.YAxis, None) def add_difference_overlay(self, diff): diff = -diff diff[diff > 0.0] = 1.0 pos = np.array([0, 1]) color = np.array([[0, 0, 0, 0], [255, 0, 0, 255]], dtype=np.ubyte) map = ColorMap(pos, color) self.image_after_overlay.setOpacity(1) self.image_after_overlay.setImage(diff) lut = map.getLookupTable(0, 1, 2) self.image_after_overlay.setLookupTable(lut) def hide_difference_overlay(self): self.image_after_overlay.setOpacity(0)
def _update_preview_image(image_data: Optional[np.ndarray], image: ImageItem): image.clear() image.setImage(image_data)
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) @threads.method(threadkey="preview", showBusy=False) def preview(self, data): if isinstance(data, NonDBHeader): self.preview_header(data) else: self.preview_catalog(data) def preview_catalog(self, catalog: BlueskyRun): threads.invoke_in_main_thread(self.setText, "LOADING...") try: stream, field = bluesky_utils.guess_stream_field(catalog) data = bluesky_utils.preview(catalog, stream, field) threads.invoke_in_main_thread(self.setImage, data) except Exception as ex: msg.logError(ex) threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] threads.invoke_in_main_thread(self.setImage, data) except IndexError: threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imageitem.setImage(np.log(imgdata * (imgdata > 0) + (imgdata < 1)), autoLevels=True) self.imageitem.setTransform( QTransform(1, 0, 0, -1, 0, imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imgdata = None self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) def preview(self, data): if isinstance(data, NonDBHeader): self.preview_header(data) else: self.preview_catalog(data) def preview_catalog(self, catalog: BlueskyRun): try: dask_array = catalog.primary.to_dask() fields = dask_array.keys() # Filter out seq num and uid field = next(field for field in fields if not field in ["seq_num", "uid"]) data = dask_array[field] for i in range(len(data.shape) - 2): data = data[0] self.setImage(np.asarray(data.compute())) except IndexError: self.imageitem.clear() self.setText("UNKNOWN DATA FORMAT") def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] self.setImage(data) except IndexError: self.imageitem.clear() self.setText("UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imgdata = imgdata self.imageitem.setImage(np.log(self.imgdata * (self.imgdata > 0) + (self.imgdata < 1)), autoLevels=True) self.imageitem.setTransform(QTransform(1, 0, 0, -1, 0, self.imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
class Plot2DWidget(PlotWidget): def __init__(self, model_wrapper, statusbar, parent=None): """ The Plot2DWidget is responsible for rendering the 2D chromatogram data. :param model_wrapper: the wrapper of the model. :param parent: the parent of this Widget. """ super().__init__(parent=parent) self.listener = Plot2DListener(self, model_wrapper, statusbar) """ The listener for the 2D plot """ self.img = ImageItem() """ The image of the chromatogram""" self.wrapper_temp = model_wrapper # TEMPORARY TODO What is this for? """A temporary reference to the wrapper?""" # Add the image to the plot. self.addItem(self.img) # Disable right click context menu. self.getPlotItem().setMenuEnabled(False) self.getPlotItem().getAxis('bottom').enableAutoSIPrefix(False) self.getPlotItem().getAxis('left').enableAutoSIPrefix(False) model_wrapper.add_observer(self, self.notify) # call notify to draw the model. NOTE: The if statement isn't nesessary, it checks in notify if there is # a model or not. self.notify('model', model_wrapper.model) def refresh_x_period(self, x_period): if x_period == 0: self.getPlotItem().getAxis('bottom').setScale(1) else: self.getPlotItem().getAxis('bottom').setScale( x_period / self.wrapper_temp.model.get_width()) def refresh_y_period(self, y_period): if y_period == 0: self.getPlotItem().getAxis('left').setScale(1) else: self.getPlotItem().getAxis('left').setScale( y_period / self.wrapper_temp.model.get_height()) def refresh_x_unit(self, x_unit): if x_unit is TimeUnit.NONE: self.getPlotItem().getAxis('bottom').setLabel(units="") else: self.getPlotItem().getAxis('bottom').setLabel( units=x_unit.name.lower()) def refresh_y_unit(self, y_unit): if y_unit is TimeUnit.NONE: self.getPlotItem().getAxis('left').setLabel(units="") else: self.getPlotItem().getAxis('left').setLabel( units=y_unit.name.lower()) def notify(self, name, value): """ Updates the image rendered to match the model. :return: None """ if name == 'newIntegration': self.addItem(value.selector.roi) value.selector.set_viewport(self.img) elif name == 'removeIntegration': self.removeItem(value.selector.roi) elif name in {'model', 'model.viewTransformed'}: if value is None or value.get_2d_chromatogram_data() is None: self.img.clear() else: self.img.setImage(value.get_2d_chromatogram_data().clip( value.lower_bound, value.upper_bound), lut=value.palette) self.refresh_x_period( self.wrapper_temp.get_preference(ScaleEnum.X_PERIOD)) self.refresh_y_period( self.wrapper_temp.get_preference(ScaleEnum.Y_PERIOD)) self.refresh_x_unit( self.wrapper_temp.get_preference(ScaleEnum.X_UNIT)) self.refresh_y_unit( self.wrapper_temp.get_preference(ScaleEnum.Y_UNIT)) elif name == 'model.palette': self.img.setLookupTable(value.palette) elif name == 'model.lower_bound' or name == 'model.upper_bound': self.img.setImage(value.get_2d_chromatogram_data().clip( value.lower_bound, value.upper_bound), lut=value.palette) elif name == ScaleEnum.X_UNIT.name: self.refresh_x_unit(value) elif name == ScaleEnum.Y_UNIT.name: self.refresh_y_unit(value) elif name == ScaleEnum.X_PERIOD.name: self.refresh_x_period(value) elif name == ScaleEnum.Y_PERIOD.name: self.refresh_y_period(value)
class FilterPreviews(GraphicsLayoutWidget): image_before: ImageItem image_after: ImageItem image_diff: ImageItem histogram: Optional[PlotItem] def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) widget_location = self.mapToGlobal(QPoint(self.width() // 2, 0)) # allow the widget to take up to 80% of the desktop's height if QGuiApplication.screenAt(widget_location) is not None: screen_height = QGuiApplication.screenAt( widget_location).availableGeometry().height() else: screen_height = max( QGuiApplication.primaryScreen().availableGeometry().height(), 600) LOG.info( "Unable to detect current screen. Setting screen height to %s" % screen_height) self.ALLOWED_HEIGHT: QRect = screen_height * 0.8 self.histogram = None self.addLabel("Image before") self.addLabel("Image after") self.addLabel("Image difference") self.nextRow() self.imageview_before = MIMiniImageView(name="before") self.imageview_after = MIMiniImageView(name="after") self.imageview_difference = MIMiniImageView(name="difference") self.all_imageviews = [ self.imageview_before, self.imageview_after, self.imageview_difference ] MIMiniImageView.set_siblings(self.all_imageviews, axis=True) MIMiniImageView.set_siblings( [self.imageview_before, self.imageview_after], hist=True) self.image_before, self.image_before_vb, self.image_before_hist = self.imageview_before.get_parts( ) self.image_after, self.image_after_vb, self.image_after_hist = self.imageview_after.get_parts( ) self.image_difference, self.image_difference_vb, self.image_difference_hist = \ self.imageview_difference.get_parts() self.all_histograms = [ self.image_before_hist, self.image_after_hist, self.image_difference_hist ] self.image_diff_overlay = ImageItem() self.image_diff_overlay.setZValue(10) self.image_after_vb.addItem(self.image_diff_overlay) # Ensure images resize equally self.image_layout: GraphicsLayout = self.addLayout(colspan=3) self.image_layout.addItem(self.imageview_before) self.image_layout.addItem(self.imageview_after) self.image_layout.addItem(self.imageview_difference) self.nextRow() self.init_histogram() # Work around for https://github.com/mantidproject/mantidimaging/issues/565 self.scene().contextMenu = [ item for item in self.scene().contextMenu if "export" not in item.text().lower() ] self.auto_colour_actions = [] self._add_auto_colour_action(self.image_before_hist, self.image_before) self._add_auto_colour_action(self.image_after_hist, self.image_after) self._add_auto_colour_action(self.image_difference_hist, self.image_difference) self.imageview_before.link_sibling_axis() self.imageview_before.enable_nan_check() self.imageview_after.enable_nan_check() def resizeEvent(self, ev: QResizeEvent): if ev is not None and isinstance(self.histogram, PlotItem): size = ev.size() self.histogram.setFixedHeight( min(size.height() * 0.7, self.ALLOWED_HEIGHT) * 0.25) super().resizeEvent(ev) def clear_items(self, clear_before: bool = True): if clear_before: self.imageview_before.clear() self.imageview_after.clear() self.imageview_difference.clear() self.image_diff_overlay.clear() def init_histogram(self): self.histogram = self.addPlot(row=histogram_coords.row, col=histogram_coords.col, labels=histogram_axes_labels, lockAspect=True, colspan=3) self.addLabel("Pixel values", row=label_coords.row, col=label_coords.col) self.legend = self.histogram.addLegend() self.legend.setOffset((0, 1)) def update_histogram_data(self): # Plot any histogram that has data, and add a legend if both exist before_data = self.imageview_before.image_item.getHistogram() after_data = self.imageview_after.image_item.getHistogram() if _data_valid_for_histogram(before_data): before_plot = self.histogram.plot(*before_data, pen=before_pen, clear=True) self.legend.addItem(before_plot, "Before") if _data_valid_for_histogram(after_data): after_plot = self.histogram.plot(*after_data, pen=after_pen) self.legend.addItem(after_plot, "After") @property def histogram_legend(self) -> Optional[LegendItem]: if self.histogram and self.histogram.legend: return self.histogram.legend return None def link_all_views(self): self.imageview_before.link_sibling_axis() def unlink_all_views(self): self.imageview_before.unlink_sibling_axis() def add_difference_overlay(self, diff, nan_change): diff = np.absolute(diff) diff[diff > OVERLAY_THRESHOLD] = 1.0 diff[nan_change] = 1.0 pos = np.array([0, 1]) color = np.array([[0, 0, 0, 0], OVERLAY_COLOUR_DIFFERENCE], dtype=np.ubyte) map = ColorMap(pos, color) self.image_diff_overlay.setVisible(True) self.image_diff_overlay.setImage(diff) lut = map.getLookupTable(0, 1, 2) self.image_diff_overlay.setLookupTable(lut) def add_negative_overlay(self): self.imageview_after.enable_nonpositive_check() def hide_difference_overlay(self): self.image_diff_overlay.setVisible(False) def hide_negative_overlay(self): self.imageview_after.enable_nonpositive_check(False) def auto_range(self): # This will cause the previews to all show by just causing autorange on self.image_before_vb self.image_before_vb.autoRange() def record_histogram_regions(self): self.before_region = self.image_before_hist.region.getRegion() self.diff_region = self.image_difference_hist.region.getRegion() self.after_region = self.image_after_hist.region.getRegion() def restore_histogram_regions(self): self.image_before_hist.region.setRegion(self.before_region) self.image_difference_hist.region.setRegion(self.diff_region) self.image_after_hist.region.setRegion(self.after_region) def link_before_after_histogram_scales(self, create_link: bool): """ Connects or disconnects the scales of the before/after histograms. :param create_link: Whether the link should be created or removed. """ if create_link: self.imageview_after.link_sibling_histogram() else: self.imageview_after.unlink_sibling_histogram() def set_histogram_log_scale(self): """ Sets the y-values of the before and after histogram plots to a log scale. """ set_histogram_log_scale(self.image_before_hist) set_histogram_log_scale(self.image_after_hist) def _add_auto_colour_action(self, histogram: HistogramLUTItem, image: ImageItem): """ Adds an "Auto" action to the histogram right-click menu. :param histogram: The HistogramLUTItem :param image: The ImageItem to have the Jenks/Otsu algorithm performed on it. """ self.auto_colour_actions.append(QAction("Auto")) self.auto_colour_actions[-1].triggered.connect( lambda: self._on_change_colour_palette(histogram, image)) action = histogram.gradient.menu.actions()[12] histogram.gradient.menu.insertAction(action, self.auto_colour_actions[-1]) histogram.gradient.menu.insertSeparator(self.auto_colour_actions[-1]) def _on_change_colour_palette(self, main_histogram: HistogramLUTItem, image: ImageItem): """ Creates a Palette Changer window when the "Auto" option has been selected. :param main_histogram: The HistogramLUTItem. :param image: The ImageItem. """ other_histograms = self.all_histograms[:] other_histograms.remove(main_histogram) change_colour_palette = PaletteChangerView(self, main_histogram, image.image, other_histograms) change_colour_palette.show()