def restore_settings(self, do_restore, filename=None): if (filename): s = QtCore.QSettings(filename, QtCore.QSettings.IniFormat) else: s = QtCore.QSettings() self._init_geometry(s) self._init_recorder(s) loaded_sources = [] try: if do_restore: loaded_sources = self._init_data_sources(s) except (TypeError, KeyError): # raise #raise # Be a bit more resilient against configuration problems logging.warning( "Failed to load data source settings! Continuing...") if do_restore: try: self._restore_data_windows(s, loaded_sources) except (TypeError, KeyError): # Be a bit more resilient against configuration problems logging.warning( "Failed to load data windows settings! Continuing...") self.plotdata_widget.restore_state(s) self.settings = s
def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, None) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self._enabled_sources = {} self.settings = QtCore.QSettings() self.setupUi(self) self.alertBlinkTimer = QtCore.QTimer() self.alertBlinkTimer.setInterval(500) self._setup_connections() self._parent = parent # If True this DataWindow was restored from saved settings self.restored = False self.alertBlinking = False self.set_sounds_and_volume()
def save_settings(self, filename=None): if (filename): s = QtCore.QSettings(filename, QtCore.QSettings.IniFormat) else: s = QtCore.QSettings() s.setValue("geometry", self.saveGeometry()) s.setValue("windowState", self.saveState()) # Save data sources ds_settings = [] for ds in self._data_sources: ds_settings.append([ds.hostname, ds.port, ds.ssh_tunnel, ds.conf]) s.setValue("dataSources", ds_settings) self.plotdata_widget.save_state(s) s.setValue("plotData", self.plotdata_widget.save_plotdata()) s.setValue("dataWindows", self.save_data_windows()) # Make sure settings are saved s.sync()
def _update_bg(self): if self._settings_diag.bg is not None: VB = self.plot.getViewBox() B = pyqtgraph.ImageItem(image=self._settings_diag.bg, autoLevels=True) xmin = float(self._settings_diag.bg_xmin.text()) ymin = float(self._settings_diag.bg_xmin.text()) width = float(self._settings_diag.bg_xmax.text()) - xmin height = float(self._settings_diag.bg_ymax.text()) - ymin rect = QtCore.QRectF(xmin, ymin, width, height) B.setRect(rect) VB.addItem(B, ignoreBounds=True)
def __init__(self, _type, parent=None, **kwargs): QtCore.QObject.__init__(self, parent, **kwargs) ctx = ZmqContext.instance() self._socket = ctx.socket(_type) fd = self._socket.getsockopt(FD) self._notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, self) self._notifier.activated.connect(self.activity) self._socket.setsockopt(RCVHWM, 100) self.filters = []
def _init_timer(self): """Initialize reploting timer.""" self._replot_timer = QtCore.QTimer() self._replot_timer.setInterval(1000) # Replot every 1000 ms self._replot_timer.timeout.connect(self._replot) self._replot_timer.start()
def __init__(self, parent): QtGui.QDialog.__init__(self, parent, QtCore.Qt.WindowTitleHint) self.setupUi(self) settings = QtCore.QSettings() self.outputPath.setText(settings.value("outputPath"))
class DataSource(QtCore.QObject): """Manages a connection with one backend""" plotdata_added = QtCore.Signal(PlotData) subscribed = QtCore.Signal(str) unsubscribed = QtCore.Signal(str) def __init__(self, parent, hostname, port, ssh_tunnel=None, conf={}): QtCore.QObject.__init__(self, parent) self._hostname = hostname self._port = port self._ssh_tunnel = ssh_tunnel self.connected = False self._plotdata = {} self._subscribed_titles = {} self._recorded_titles = {} self._recorder = None self._data_socket = ZmqSocket(SUB, self) self.conf = conf self._group_structure = {} try: self._connect() self.connected = True self._get_data_port() self.titles = None self.data_type = None except (RuntimeError, zmq.error.ZMQError): QtGui.QMessageBox.warning(self.parent(), "Connection failed!", "Could not connect to %s" % self.name()) raise def subscribe(self, title, plot): """Subscribe to the broadcast named title, and associate it with the given plot""" if title not in self._subscribed_titles: self._subscribed_titles[title] = [plot] try: self._data_socket.subscribe(title) self.subscribed.emit(title) logging.debug("Subscribing to %s on %s.", title, self.name()) # socket might still not exist except AttributeError: pass else: self._subscribed_titles[title].append(plot) def unsubscribe(self, title, plot): """Dissociate the given plot with the broadcast named title. If no one else is associated with it unsubscribe""" self._subscribed_titles[title].remove(plot) # Check if list is empty if not self._subscribed_titles[title]: self._data_socket.unsubscribe(title) self.unsubscribed.emit(title) logging.debug("Unsubscribing from %s on %s.", title, self.name()) self._subscribed_titles.pop(title) def subscribe_for_recording(self, title): """Subscribe to the broadcast named title, and associate it with recorder""" # Only subscribe if we are not already subscribing for plotting if title in self._subscribed_titles: return if title not in self._recorded_titles: self._recorded_titles[title] = True try: self._data_socket.subscribe(title) self.subscribed.emit(title) logging.debug("Subscribing to %s on %s.", title, self.name()) # socket might still not exist except AttributeError: pass def unsubscribe_for_recording(self, title): """Dissociate the recorder with the broadcast named title. If no one else is associated with it unsubscrine""" self._recorded_titles[title] = False if not title in self._subscribed_titles: self._data_socket.unsubscribe(title) self.unsubscribed.emit(title) logging.debug("Unsubscribing from %s on %s.", title, self.name()) self._recorded_titles.pop(title) def name(self): """Return a string representation of the data source""" if (self._ssh_tunnel): return '%s (%s)' % (self._hostname, self._ssh_tunnel) else: return self._hostname @property def plotdata(self): """Returns the data source dictionary of plotdata""" return self._plotdata def _connect(self): """Connect to the configured backend""" self._ctrl_socket = ZmqSocket(REQ) addr = "tcp://%s:%d" % (self._hostname, self._port) self._ctrl_socket.ready_read.connect(self._get_request_reply) self._ctrl_socket.connect_socket(addr, self._ssh_tunnel) def _get_data_port(self): """Ask to the backend for the data port""" self._ctrl_socket.send_multipart(['data_port'.encode('UTF-8')]) def query_configuration(self): """Ask to the backend for the configuration""" self._ctrl_socket.send_multipart(['conf'.encode('UTF-8')]) def query_reloading(self): """Ask the backend to reload its configuration""" self._ctrl_socket.send_multipart(['reload'.encode('UTF-8')]) def _get_request_reply(self, socket=None): """Handle the reply of the backend to a previous request""" if (socket is None): socket = self.sender() reply = socket.recv_json() if (reply[0] == 'data_port'): self._data_port = reply[1] logging.debug("Data source '%s' received data_port=%s", self.name(), self._data_port) addr = "tcp://%s:%s" % (self._hostname, self._data_port) self._data_socket.ready_read.connect(self._get_broadcast) self._data_socket.connect_socket(addr, self._ssh_tunnel) self.parent().add_backend(self) # Subscribe to stuff already requested for title in self._subscribed_titles.keys(): self._data_socket.subscribe(title) self.subscribed.emit(title) logging.debug("Subscribing to %s on %s.", title, self.name()) self.query_configuration() elif (reply[0] == 'conf'): self.conf = reply[1] self.titles = self.conf.keys() self.data_type = {} for k in self.conf.keys(): if ('data_type' not in self.conf[k]): # Broadcasts without any data will not have a data_type # Let's remove them from the title list and continue self.titles.remove(k) continue self.data_type[k] = self.conf[k]['data_type'] if (k not in self._plotdata): if "group" in self.conf[k]: group = self.conf[k]["group"] if group is None: group = "No group" else: group = "No group" self._plotdata[k] = PlotData(self, k, group=group) self.plotdata_added.emit(self._plotdata[k]) self.add_item_to_group_structure(k, group) # Remove PlotData which is no longer in the conf for k in self._plotdata.keys(): if k not in self.titles: self._plotdata.pop(k) def _get_broadcast(self): """Receive a data package on the data socket""" socket = self.sender() socket.blockSignals(True) QtCore.QCoreApplication.processEvents() socket.blockSignals(False) # Discard key socket.recv() data = socket.recv_json() for i in range(len(data)): if data[i] == '__ndarray__': data[i] = socket.recv_array() self._process_broadcast(data) def _process_broadcast(self, payload): """Handle a data package received by the data socket""" cmd = payload[1] title = payload[2] data = payload[3] if (title not in self.conf): # We're getting data we were not expecting # Let's discard it and order an immediate reconfigure logging.debug( "Received unexpected data with title %s on %s. Reconfiguring...", title, self.name()) return if (cmd == 'new_data'): data_x = payload[4] # At the moment x is always a timestamp so I'll add some metadata to show it if type(data_x) is not numpy.ndarray: data_x = numpy.array(data_x) data_x.dtype = numpy.dtype(data_x.dtype, metadata={'units': 's'}) conf = payload[5] self.conf[title].update(conf) if self._plotdata[title].recordhistory: self._recorder.append(title, data, data_x) if 'sum_over' in conf: if 'msg' in conf: self._plotdata[title].sum_over(data, data_x, conf['msg']) else: self._plotdata[title].sum_over(data, data_x, '') else: if 'msg' in conf: self._plotdata[title].append(data, data_x, conf['msg']) else: self._plotdata[title].append(data, data_x, '') @property def hostname(self): """Give access to the data source hostname""" return self._hostname @property def port(self): """Give access to the data source port""" return self._port @property def ssh_tunnel(self): """Give access to the data source ssh_tunnel""" return self._ssh_tunnel @property def subscribed_titles(self): """Returns the currently subscribed titles""" return self._subscribed_titles.keys() def restore_state(self, state): """Restores any plotdata that are saved in the state""" for pds in state: if (pds['data_source'][0] == self.hostname and pds['data_source'][1] == self.port and pds['data_source'][2] == self.ssh_tunnel): # It's a match! k = pds['title'] group = pds["group"] pd = PlotData(self, k, group=group) pd.restore_state(pds, self) self._plotdata[k] = pd self.plotdata_added.emit(self._plotdata[k]) self.add_item_to_group_structure(k, group) @property def group_structure(self): return self._group_structure def add_item_to_group_structure(self, title, group): if group in self.group_structure: self.group_structure[group].append(title) else: self.group_structure[group] = [title]
class ImageView(QtGui.QWidget): """ Widget used for display and analysis of image data. Implements many features: * Displays 2D and 3D image data. For 3D data, a z-axis slider is displayed allowing the user to select which frame is displayed. * Displays histogram of image data with movable region defining the dark/light levels * Editable gradient provides a color lookup table * Frame slider may also be moved using left/right arrow keys as well as pgup, pgdn, home, and end. * Basic analysis features including: * ROI and embedded plot for measuring image values across frames * Image normalization / background subtraction Basic Usage:: imv = pg.ImageView() imv.show() imv.setImage(data) """ sigProcessingChanged = QtCore.Signal(object) def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): """ By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem. Custom items may be given instead by specifying the *view* and/or *imageItem* arguments. """ QtGui.QWidget.__init__(self, parent, *args) self._parent = parent self.levelMax = 4096 self.levelMin = 0 self.name = name self.image = None self.axes = {} self.imageDisp = None self.ui = Ui_Form() self.ui.setupUi(self) self.scene = self.ui.graphicsView.scene() if view is None: self.view = pyqtgraph.ViewBox() else: self.view = view self.ui.graphicsView.setCentralItem(self.view) self.view.setAspectLocked(True) self.view.invertY() if imageItem is None: self.imageItem = pyqtgraph.ImageItem() else: self.imageItem = imageItem self.view.addItem(self.imageItem) self.currentIndex = 0 self.ui.histogram.setImageItem(self.imageItem) self.keysPressed = {} ## wrap functions from view box for fn in ['addItem', 'removeItem']: setattr(self, fn, getattr(self.view, fn)) ## wrap functions from histogram for fn in ['setHistogramRange', 'autoHistogramRange', 'getLookupTable', 'getLevels']: setattr(self, fn, getattr(self.ui.histogram, fn)) self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True): """ Set the image to be displayed in the widget. ================== ======================================================================= **Arguments:** img (numpy array) the image to be displayed. xvals (numpy array) 1D array of z-axis values corresponding to the third axis in a 3D image. For video, this array should contain the time of each frame. autoRange (bool) whether to scale/pan the view to fit the image. autoLevels (bool) whether to update the white/black levels to fit the image. levels (min, max); the white and black level values to use. axes Dictionary indicating the interpretation for each axis. This is only needed to override the default guess. Format is:: {'t':0, 'x':1, 'y':2, 'c':3}; pos Change the position of the displayed image scale Change the scale of the displayed image transform Set the transform of the displayed image. This option overrides *pos* and *scale*. autoHistogramRange If True, the histogram y-range is automatically scaled to fit the image data. ================== ======================================================================= """ if hasattr(img, 'implements') and img.implements('MetaArray'): img = img.asarray() if not isinstance(img, numpy.ndarray): raise Exception("Image must be specified as ndarray.") self.image = img if xvals is not None: self.tVals = xvals elif hasattr(img, 'xvals'): try: self.tVals = img.xvals(0) except: self.tVals = numpy.arange(img.shape[0]) else: self.tVals = numpy.arange(img.shape[0]) if axes is None: if img.ndim == 2: self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} elif img.ndim == 3: if img.shape[2] <= 4: self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} else: self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} elif img.ndim == 4: self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} else: raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) elif isinstance(axes, dict): self.axes = axes.copy() elif isinstance(axes, list) or isinstance(axes, tuple): self.axes = {} for i in range(len(axes)): self.axes[axes[i]] = i else: raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes))) for x in ['t', 'x', 'y', 'c']: self.axes[x] = self.axes.get(x, None) self.imageDisp = None self.currentIndex = 0 self.updateImage(autoHistogramRange=autoHistogramRange) if levels is None and autoLevels: self.autoLevels() if levels is not None: ## this does nothing since getProcessedImage sets these values again. self.setLevels(*levels) if self.axes['t'] is not None: if len(self.tVals) > 1: start = self.tVals.min() stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 elif len(self.tVals) == 1: start = self.tVals[0] - 0.5 stop = self.tVals[0] + 0.5 else: start = 0 stop = 1 self.imageItem.resetTransform() if scale is not None: self.imageItem.scale(*scale) if pos is not None: self.imageItem.setPos(*pos) if transform is not None: self.imageItem.setTransform(transform) if autoRange: self.autoRange() def autoLevels(self): """Set the min/max intensity levels automatically to match the image data.""" self.setLevels(self.levelMin, self.levelMax) def setLevels(self, min, max): """Set the min/max (bright and dark) levels.""" self.ui.histogram.setLevels(min, max) def autoRange(self): """Auto scale and pan the view around the image.""" image = self.getProcessedImage() self.view.autoRange() def getProcessedImage(self): """Returns the image data after it has been processed by any normalization options in use. This method also sets the attributes self.levelMin and self.levelMax to indicate the range of data in the image.""" if self.imageDisp is None: image = self.normalize(self.image) self.imageDisp = image self.levelMin, self.levelMax = list(map(float, ImageView.quickMinMax(self.imageDisp))) return self.imageDisp def close(self): """Closes the widget nicely, making sure to clear the graphics scene and release memory.""" self.ui.graphicsView.close() self.scene.clear() del self.image del self.imageDisp self.setParent(None) def keyPressEvent(self, ev): #print ev.key() if ev.key() == QtCore.Qt.Key_Home: self.setCurrentIndex(0) ev.accept() elif ev.key() == QtCore.Qt.Key_End: self.setCurrentIndex(self.getProcessedImage().shape[0]-1) ev.accept() elif ev.key() in self.noRepeatKeys: ev.accept() if ev.isAutoRepeat(): return self.keysPressed[ev.key()] = 1 self.evalKeyState() else: QtGui.QWidget.keyPressEvent(self, ev) def keyReleaseEvent(self, ev): if ev.key() in [QtCore.Qt.Key_Space, QtCore.Qt.Key_Home, QtCore.Qt.Key_End]: ev.accept() elif ev.key() in self.noRepeatKeys: ev.accept() if ev.isAutoRepeat(): return try: del self.keysPressed[ev.key()] except: self.keysPressed = {} self.evalKeyState() else: QtGui.QWidget.keyReleaseEvent(self, ev) def evalKeyState(self): if len(self.keysPressed) == 1: key = list(self.keysPressed.keys())[0] if key == QtCore.Qt.Key_Right: self.jumpFrames(1) elif key == QtCore.Qt.Key_Left: self.jumpFrames(-1) elif key == QtCore.Qt.Key_Up: self.jumpFrames(-10) elif key == QtCore.Qt.Key_Down: self.jumpFrames(10) elif key == QtCore.Qt.Key_PageUp: self.jumpFrames(-100) elif key == QtCore.Qt.Key_PageDown: self.jumpFrames(100) def setCurrentIndex(self, ind, autoHistogramRange=True): """Set the currently displayed frame index.""" self.currentIndex = numpy.clip(ind, 0, self.getProcessedImage().shape[0]-1) self.updateImage(autoHistogramRange=autoHistogramRange) def jumpFrames(self, n): """Move video frame ahead n frames (may be negative)""" if self.axes['t'] is not None: self.setCurrentIndex(self.currentIndex + n) self._parent.replot() def hasTimeAxis(self): return 't' in self.axes and self.axes['t'] is not None @staticmethod def quickMinMax(data): while data.size > 1e6: ax = numpy.argmax(data.shape) sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) data = data[sl] return data.min(), data.max() def normalize(self, image): return image def updateImage(self, autoHistogramRange=True): ## Redraw image on screen if self.image is None: return image = self.getProcessedImage() if autoHistogramRange: self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) if self.axes['t'] is None: self.imageItem.updateImage(image) else: self.imageItem.updateImage(image[self.currentIndex]) def getView(self): """Return the ViewBox (or other compatible object) which displays the ImageItem""" return self.view def getImageItem(self): """Return the ImageItem for this ImageView.""" return self.imageItem def getHistogramWidget(self): """Return the HistogramLUTWidget for this ImageView""" return self.ui.histogram
class ZmqSocket(QtCore.QObject): """Wrapper around a zmq socket. Provides Qt signal handling""" ready_read = QtCore.Signal() ready_write = QtCore.Signal() def __init__(self, _type, parent=None, **kwargs): QtCore.QObject.__init__(self, parent, **kwargs) ctx = ZmqContext.instance() self._socket = ctx.socket(_type) fd = self._socket.getsockopt(FD) self._notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, self) self._notifier.activated.connect(self.activity) self._socket.setsockopt(RCVHWM, 100) self.filters = [] def __del__(self): """Close socket on deletion""" self._socket.close() def set_identity(self, name): """Set zmq socket identity""" self._socket.setsockopt(IDENTITY, name) def identity(self): """Return the zmq socket identity""" return self._socket.getsockopt(IDENTITY) def subscribe(self, title): """Subscribe to a broadcast with the given title""" # only subscribe if we're not already subscribed if title in self.filters: return # scramble the filter to avoid spurious matches (like CCD matching CCD1) m = hashlib.md5() m.update(title.encode('UTF-8')) self._socket.setsockopt(SUBSCRIBE, m.digest()) self.filters.append(title) def unsubscribe(self, title): """Unsubscribe to a broadcast with the given title""" m = hashlib.md5() m.update(title.encode('UTF-8')) self._socket.setsockopt(UNSUBSCRIBE, m.digest()) self.filters.remove(title) def bind(self, addr): """Bind socket to address""" self._socket.bind(addr) def connect_socket(self, addr, tunnel=None): """Connect socket to endpoint, possible using an ssh tunnel The tunnel argument specifies the hostname of the ssh server to tunnel through. Note that this still succeeds even if there's no ZMQ server on the other end as the connection is asynchronous. For more details check the zmq_connect(3) documentation. """ if(tunnel): from zmq import ssh # If there's no ssh server listening we're gonna # get stuck here for a long time as there's no timeout ssh.tunnel_connection(self._socket, addr, tunnel) else: self._socket.connect(addr) def activity(self): """Callback run when there's activity on the socket""" self._notifier.setEnabled(False) while(self._socket.getsockopt(EVENTS) & POLLIN): self.ready_read.emit() self._notifier.setEnabled(True) def recv(self, flags=0): """Receive a message on the socket""" return self._socket.recv(flags) def recv_json(self, flags=0): """Receive and json decode a message on the socket""" return self._socket.recv_json(flags) def recv_multipart(self): """Receive a multipart message on the socket""" return self._socket.recv_multipart() def send(self, _msg): """Send a message on the socket""" return self._socket.send(_msg) def send_multipart(self, _msg): """Send a list of messages as a multipart message on the socket""" return self._socket.send_multipart(_msg) def recv_array(self, flags=0, copy=True, track=False): """Receive a numpy array""" md = self._socket.recv_json(flags=flags) msg = self._socket.recv(flags=flags, copy=copy, track=track) return numpy.ndarray(shape=md['shape'], dtype=md['dtype'], buffer=msg, strides=md['strides'])