Fixing Crashes When Using NumPy Arrays with QImage in Qt Threads

How to safely pass image data between threads when streaming video or updating displays
Heads up! You've already completed this tutorial.

I'm using a threaded runner to stream a live video feed by converting a NumPy array to a QImage, then to a QPixmap, and displaying it on a QLabel. But I'm frequently encountering crashes when the label is resized too quickly or the scroll area is scrolled. Could this be a problem with the QImage memory buffer getting cleared before the QPixmap can update? Is this fixable, or is it a fundamental issue with threads in Python/Qt?

This is a common problem when working with NumPy arrays and QImage across threads. The good news is that it's fixable. The crashes come from how QImage handles the underlying memory of a NumPy array.

Why the crash happens

When you create a QImage from a NumPy array, QImage doesn't copy the data. Instead it holds a reference to the original memory buffer provided by the NumPy array.

python
import numpy as np
from PyQt6.QtGui import QImage

# Create a NumPy array (e.g. a video frame)
array = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)

# QImage points to the same memory — no copy is made
image = QImage(
    array.data,
    array.shape[1],
    array.shape[0],
    array.strides[0],
    QImage.Format.Format_RGB8888,
)

This is efficient, but it creates a dangerous situation in a multithreaded application. If the NumPy array is modified or goes out of scope in the worker thread while the GUI thread is still using the QImage to paint, the memory that QImage is pointing to may no longer be valid. The result is a segfault or (worse) a silent crash without any information about what has happened.

This is especially likely when frames are arriving quickly (as with a live video feed) and the GUI is being redrawn frequently — for example, during a resize or a scroll.

The fix: copy the image data

The simplest and most reliable fix is to make sure the QImage owns its own copy of the pixel data before you pass it to the GUI thread. You can do this by calling .copy() on the QImage:

python
image = QImage(
    array.data,
    array.shape[1],
    array.shape[0],
    array.strides[0],
    QImage.Format.Format_RGB888,
).copy()

The .copy() call creates a new QImage with its own independent memory buffer. Now it doesn't matter if the original NumPy array changes or disappears — the QImage is safe to use from the GUI thread.

If you've already tried using .copy() and it hasn't worked, bear in mind that where you use the copy matters just as much as using it at all.

Where to copy matters

If you create the QImage in the worker thread and emit it via a signal, the copy needs to happen before the signal is emitted. If the signal carries a reference to the original (non-copied) QImage, the data might still be invalidated before the GUI thread processes it.

PyQt/PySide Development Services — Stuck in development hell? I'll help you get your project focused, finished and released. Benefit from years of practical experience releasing software with Python.

Find out More

Here's the pattern that works:

python
# In the worker thread
image = QImage(
    frame.data,
    frame.shape[1],
    frame.shape[0],
    frame.strides[0],
    QImage.Format.Format_RGB888,
).copy()  # Copy immediately, before emitting

self.signals.result.emit(image)  # Now safe to send to GUI thread

And in the main thread, connect that signal to a slot that updates the display:

python
def update_display(self, image):
    pixmap = QPixmap.fromImage(image)
    self.label.setPixmap(pixmap)

Because the QImage was copied before it crossed the thread boundary, the GUI thread has full ownership of the data and can paint it safely.

Use signals to control the update flow

Another source of crashes is calling GUI methods directly from a worker thread. In Qt, all GUI updates must happen on the main thread. If you're calling label.setPixmap(...) from inside a worker or a callback running on a background thread, that's undefined behavior and will eventually crash.

The solution is to always use signals and slots to communicate between threads. Emit a signal from the worker carrying the processed image, and connect it to a slot on the main thread that performs the GUI update.

This also gives you a natural way to throttle updates. If frames are arriving faster than the GUI can paint them, you can use a flag to skip frames that arrive while the previous one is still being displayed.

Complete working example

Here's a full example that simulates a video feed using a QRunnable and a QThreadPool. It generates random NumPy frames in a background thread and safely displays them on a QLabel. If you're new to running background tasks with QThreadPool, see our detailed guide to multithreading PyQt6 applications.

python
import sys
import time

import numpy as np
from PyQt6.QtCore import (
    QObject,
    QRunnable,
    QThreadPool,
    pyqtSignal,
    pyqtSlot,
)
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QScrollArea,
    QVBoxLayout,
    QWidget,
)


class WorkerSignals(QObject):
    frame_ready = pyqtSignal(QImage)
    finished = pyqtSignal()


class VideoWorker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.running = True

    @pyqtSlot()
    def run(self):
        while self.running:
            # Simulate a video frame (e.g. from a camera or stream)
            frame = np.random.randint(
                0, 255, (480, 640, 3), dtype=np.uint8
            )

            # Create QImage and copy it immediately so it owns its data
            image = QImage(
                frame.data,
                frame.shape[1],
                frame.shape[0],
                frame.strides[0],
                QImage.Format.Format_RGB888,
            ).copy()

            # Emit the safe, copied image to the main thread
            self.signals.frame_ready.emit(image)

            # Simulate ~30 fps
            time.sleep(1 / 30)

        self.signals.finished.emit()

    def stop(self):
        self.running = False


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Threaded Video Display")

        self.label = QLabel("Waiting for frames...")
        self.label.setScaledContents(True)

        scroll_area = QScrollArea()
        scroll_area.setWidget(self.label)
        scroll_area.setWidgetResiizable(True)

        container = QWidget()
        layout = QVBoxLayout(container)
        layout.addWidget(scroll_area)
        self.setCentralWidget(container)

        self.resize(700, 520)

        # Set up threading
        self.threadpool = QThreadPool()
        self.worker = VideoWorker()
        self.worker.signals.frame_ready.connect(self.update_display)
        self.worker.signals.finished.connect(self.on_finished)
        self.threadpool.start(self.worker)

    def update_display(self, image):
        """Runs on the main thread — safe to update the GUI here."""
        pixmap = QPixmap.fromImage(image)
        self.label.setPixmap(pixmap)

    def on_finished(self):
        print("Worker finished.")

    def closeEvent(self, event):
        self.worker.stop()
        self.threadpool.waitForDone(2000)
        event.accept()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

When you run this, you'll see a window displaying rapidly changing random noise — a stand-in for a real video stream. You can resize the window and scroll around without crashes, because the image data is safely copied before it crosses the thread boundary, and all GUI updates happen on the main thread via signals.

Recap

When working with NumPy arrays and QImage across threads, keep these three things in mind:

  1. QImage does not copy NumPy data. It points to the original array's memory buffer. If that buffer changes or is freed, the QImage becomes invalid.

  2. Call .copy() on the QImage before emitting it across threads. This gives the QImage its own memory, independent of the NumPy array.

  3. Always update the GUI from the main thread. Use signals to send data from background workers to slots connected on the main thread, where it's safe to call setPixmap() and other GUI methods.

With these practices in place, you can stream video or display rapidly changing image data without encountering the mysterious crashes that come from shared memory across threads.

Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

Get the book

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Fixing Crashes When Using NumPy Arrays with QImage in Qt Threads was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.