{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%gui qt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n# Update data using timer events in a background thread\n\nUpdate VisPy visualizations from timer events in a background QThread.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import time\nfrom math import sin, pi\n\nimport numpy as np\nfrom PyQt5 import QtWidgets, QtCore\n\nfrom vispy.scene import SceneCanvas, visuals\nfrom vispy.app import use_app\n\nIMAGE_SHAPE = (600, 800) # (height, width)\nCANVAS_SIZE = (800, 600) # (width, height)\nNUM_LINE_POINTS = 200\n\nCOLORMAP_CHOICES = [\"viridis\", \"reds\", \"blues\"]\nLINE_COLOR_CHOICES = [\"black\", \"red\", \"blue\"]\n\n\nclass Controls(QtWidgets.QWidget):\n def __init__(self, parent=None):\n super().__init__(parent)\n layout = QtWidgets.QVBoxLayout()\n self.colormap_label = QtWidgets.QLabel(\"Image Colormap:\")\n layout.addWidget(self.colormap_label)\n self.colormap_chooser = QtWidgets.QComboBox()\n self.colormap_chooser.addItems(COLORMAP_CHOICES)\n layout.addWidget(self.colormap_chooser)\n\n self.line_color_label = QtWidgets.QLabel(\"Line color:\")\n layout.addWidget(self.line_color_label)\n self.line_color_chooser = QtWidgets.QComboBox()\n self.line_color_chooser.addItems(LINE_COLOR_CHOICES)\n layout.addWidget(self.line_color_chooser)\n\n layout.addStretch(1)\n self.setLayout(layout)\n\n\nclass CanvasWrapper:\n def __init__(self):\n self.canvas = SceneCanvas(size=CANVAS_SIZE)\n self.grid = self.canvas.central_widget.add_grid()\n\n self.view_top = self.grid.add_view(0, 0, bgcolor='cyan')\n image_data = _generate_random_image_data(IMAGE_SHAPE)\n self.image = visuals.Image(\n image_data,\n texture_format=\"auto\",\n cmap=COLORMAP_CHOICES[0],\n parent=self.view_top.scene,\n )\n self.view_top.camera = \"panzoom\"\n self.view_top.camera.set_range(x=(0, IMAGE_SHAPE[1]), y=(0, IMAGE_SHAPE[0]), margin=0)\n\n self.view_bot = self.grid.add_view(1, 0, bgcolor='#c0c0c0')\n line_data = _generate_random_line_positions(NUM_LINE_POINTS)\n self.line = visuals.Line(line_data, parent=self.view_bot.scene, color=LINE_COLOR_CHOICES[0])\n self.view_bot.camera = \"panzoom\"\n self.view_bot.camera.set_range(x=(0, NUM_LINE_POINTS), y=(0, 1))\n\n def set_image_colormap(self, cmap_name: str):\n print(f\"Changing image colormap to {cmap_name}\")\n self.image.cmap = cmap_name\n\n def set_line_color(self, color):\n print(f\"Changing line color to {color}\")\n self.line.set_data(color=color)\n\n def update_data(self, new_data_dict):\n print(\"Updating data...\")\n self.image.set_data(new_data_dict[\"image\"])\n self.line.set_data(new_data_dict[\"line\"])\n\n\ndef _generate_random_image_data(shape, dtype=np.float32):\n rng = np.random.default_rng()\n data = rng.random(shape, dtype=dtype)\n return data\n\n\ndef _generate_random_line_positions(num_points, dtype=np.float32):\n rng = np.random.default_rng()\n pos = np.empty((num_points, 2), dtype=np.float32)\n pos[:, 0] = np.arange(num_points)\n pos[:, 1] = rng.random((num_points,), dtype=dtype)\n return pos\n\n\nclass MyMainWindow(QtWidgets.QMainWindow):\n closing = QtCore.pyqtSignal()\n\n def __init__(self, canvas_wrapper: CanvasWrapper, *args, **kwargs):\n super().__init__(*args, **kwargs)\n\n central_widget = QtWidgets.QWidget()\n main_layout = QtWidgets.QHBoxLayout()\n\n self._controls = Controls()\n main_layout.addWidget(self._controls)\n self._canvas_wrapper = canvas_wrapper\n main_layout.addWidget(self._canvas_wrapper.canvas.native)\n\n central_widget.setLayout(main_layout)\n self.setCentralWidget(central_widget)\n\n self._connect_controls()\n\n def _connect_controls(self):\n self._controls.colormap_chooser.currentTextChanged.connect(self._canvas_wrapper.set_image_colormap)\n self._controls.line_color_chooser.currentTextChanged.connect(self._canvas_wrapper.set_line_color)\n\n def closeEvent(self, event):\n print(\"Closing main window!\")\n self.closing.emit()\n return super().closeEvent(event)\n\n\nclass DataSource(QtCore.QObject):\n \"\"\"Object representing a complex data producer.\"\"\"\n new_data = QtCore.pyqtSignal(dict)\n finished = QtCore.pyqtSignal()\n\n def __init__(self, num_iterations=1000, parent=None):\n super().__init__(parent)\n self._should_end = False\n self._count = 0\n self._num_iters = num_iterations\n self._image_data = _generate_random_image_data(IMAGE_SHAPE)\n self._line_data = _generate_random_line_positions(NUM_LINE_POINTS)\n\n def run_data_creation(self):\n if self._should_end or self._count >= self._num_iters:\n print(\"Data source finishing\")\n self.finished.emit()\n return\n\n time.sleep(1.0)\n image_data = self._update_image_data(self._count)\n line_data = self._update_line_data(self._count)\n self._count += 1\n\n data_dict = {\n \"image\": image_data,\n \"line\": line_data,\n }\n self.new_data.emit(data_dict)\n QtCore.QTimer.singleShot(0, self.run_data_creation)\n\n def _update_image_data(self, count):\n img_count = count % IMAGE_SHAPE[1]\n self._image_data[:, img_count] = img_count / IMAGE_SHAPE[1]\n rdata_shape = (IMAGE_SHAPE[0], IMAGE_SHAPE[1] - img_count - 1)\n self._image_data[:, img_count + 1:] = _generate_random_image_data(rdata_shape)\n return self._image_data.copy()\n\n def _update_line_data(self, count):\n self._line_data[:, 1] = np.roll(self._line_data[:, 1], -1)\n self._line_data[-1, 1] = abs(sin((count / self._num_iters) * 16 * pi))\n return self._line_data\n\n def stop_data(self):\n print(\"Data source is quitting...\")\n self._should_end = True\n\n\nif __name__ == \"__main__\":\n app = use_app(\"pyqt5\")\n app.create()\n\n canvas_wrapper = CanvasWrapper()\n win = MyMainWindow(canvas_wrapper)\n data_thread = QtCore.QThread(parent=win)\n data_source = DataSource()\n data_source.moveToThread(data_thread)\n\n # update the visualization when there is new data\n data_source.new_data.connect(canvas_wrapper.update_data)\n # start data generation when the thread is started\n data_thread.started.connect(data_source.run_data_creation)\n # if the data source finishes before the window is closed, kill the thread\n # to clean up resources\n data_source.finished.connect(data_thread.quit)\n # if the window is closed, tell the data source to stop\n win.closing.connect(data_source.stop_data)\n # when the thread has ended, delete the data source from memory\n data_thread.finished.connect(data_source.deleteLater)\n\n win.show()\n data_thread.start()\n app.run()\n\n print(\"Waiting for data source to close gracefully...\")\n data_thread.quit()\n data_thread.wait(5000)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.19" } }, "nbformat": 4, "nbformat_minor": 0 }