Drawing Dots on an Image with Mouse Clicks in PyQt6

How to paint markers on top of an image displayed in a QLabel
Heads up! You've already completed this tutorial.

I'm displaying an image on a QLabel and I want to draw colored dots on top of it when the user clicks. My draw_something method gets called (I can see print statements), but no dots appear on the image. I think the dots might be drawn under the image. How do I paint on top of the displayed image?

This is a common stumbling block when you start combining images and painting in PyQt6. The good news is that the fix is straightforward once you understand how QLabel, QPixmap, and QPainter work together. Let's walk through the problem, the solution, and then build a complete working example.

Understanding the Problem

When you load an image and display it on a QLabel, you typically do something like this:

python
pixmap = QPixmap("my_image.jpg")
self.label.setPixmap(pixmap)

The QLabel now holds a reference to that pixmap and uses it to paint itself on screen. When you want to draw on top of the image, you need to paint directly onto that same pixmap and then tell the label to update.

In the original code, the draw_something method looked like this:

python
painter = QtGui.QPainter(self.imgLabel.pixmap())

In older versions of PyQt (PyQt5), QLabel.pixmap() returned a reference to the label's internal pixmap, which meant painting on it could work — but the behavior was unreliable and the label wouldn't always refresh to show the changes. In PyQt6, QLabel.pixmap() returns a copy of the pixmap, so painting on it has no effect on what's displayed. Either way, this approach won't give you the results you want.

The Solution

The reliable approach is:

  1. Keep your own copy of the pixmap as an instance variable.
  2. Paint onto that pixmap whenever you need to add a dot.
  3. Set the modified pixmap back onto the label so it updates the display.

Here's what that looks like in practice:

python
def draw_dot(self, x, y, color="green"):
    painter = QPainter(self.pixmap)
    pen = QPen()
    pen.setWidth(10)
    pen.setColor(QColor(color))
    painter.setPen(pen)
    painter.drawPoint(x, y)
    painter.end()

    # Update the label to show the new dot
    self.label.setPixmap(self.pixmap)

The call to self.label.setPixmap(self.pixmap) at the end is what actually refreshes the display. Without it, you'd be painting onto the pixmap in memory but never telling the label to redraw itself.

Bring Your PyQt/PySide Application to Market — Specialized launch support for scientific and engineering software built using Python & Qt.

Find out More

Handling Mouse Clicks on the Label

To detect where the user clicks on the image, you can subclass QLabel and override its mousePressEvent. This gives you coordinates relative to the label itself, which is exactly what you need for painting. If you're new to handling mouse interactions in PyQt6, our guide to mouse events in widgets covers the fundamentals.

python
from PyQt6.QtWidgets import QLabel
from PyQt6.QtCore import pyqtSignal, Qt


class ClickableLabel(QLabel):
    clicked = pyqtSignal(int, int, Qt.MouseButton)

    def mousePressEvent(self, event):
        position = event.position().toPoint()
        self.clicked.emit(position.x(), position.y(), event.button())

This custom label emits a custom signal with the x, y coordinates and which mouse button was pressed. You can connect to this signal in your main window to handle the drawing logic.

Choosing Dot Color Based on Mouse Button

In the original question, left-clicks represent visible joints (drawn in green) and right-clicks represent occluded joints (drawn in yellow). You can check the mouse button in your slot and choose the color accordingly:

python
def handle_click(self, x, y, button):
    if button == Qt.MouseButton.LeftButton:
        color = "green"
    elif button == Qt.MouseButton.RightButton:
        color = "yellow"
    else:
        return

    self.draw_dot(x, y, color)

Complete Working Example

Here's a full, self-contained application that lets you open an image and draw colored dots on it with mouse clicks. Left-click draws a green dot, right-click draws a yellow dot.

python
import sys

from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QAction, QColor, QImage, QPainter, QPen, QPixmap
from PyQt6.QtWidgets import (
    QApplication,
    QFileDialog,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class ClickableLabel(QLabel):
    """A QLabel that emits a signal when clicked, reporting
    the position and which mouse button was used."""

    clicked = pyqtSignal(int, int, Qt.MouseButton)

    def mousePressEvent(self, event):
        position = event.position().toPoint()
        self.clicked.emit(position.x(), position.y(), event.button())


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Draw Dots on Image")
        self.resize(800, 600)

        # The pixmap we'll paint onto. Starts as None until an image is loaded.
        self.pixmap = None

        # Set up the menu bar with an "Open" action.
        menu = self.menuBar()
        file_menu = menu.addMenu("&File")

        open_action = QAction("&Open Image", self)
        open_action.setShortcut("Ctrl+O")
        open_action.triggered.connect(self.open_image)
        file_menu.addAction(open_action)

        # Set up the clickable label where the image will be displayed.
        self.image_label = ClickableLabel()
        self.image_label.setAlignment(
            Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
        )
        self.image_label.clicked.connect(self.handle_click)

        # A status label to show what's happening.
        self.status_label = QLabel(
            "Open an image with Ctrl+O, then click to place dots. "
            "Left-click = green, Right-click = yellow."
        )
        self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.image_label)
        layout.addWidget(self.status_label)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def open_image(self):
        """Open a file dialog and load the selected image."""
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "Open Image",
            "",
            "Image Files (*.png *.jpg *.jpeg *.bmp *.gif)",
        )
        if file_path:
            self.pixmap = QPixmap(file_path)
            self.image_label.setPixmap(self.pixmap)
            self.status_label.setText(
                f"Loaded: {file_path}  —  Click on the image to place dots."
            )

    def handle_click(self, x, y, button):
        """Draw a dot at the clicked position. Green for left-click,
        yellow for right-click."""
        if self.pixmap is None:
            return

        if button == Qt.MouseButton.LeftButton:
            color = "green"
        elif button == Qt.MouseButton.RightButton:
            color = "yellow"
        else:
            return

        self.draw_dot(x, y, color)
        self.status_label.setText(
            f"Dot placed at ({x}, {y}) — color: {color}"
        )

    def draw_dot(self, x, y, color):
        """Paint a dot onto the stored pixmap and refresh the label."""
        painter = QPainter(self.pixmap)
        pen = QPen()
        pen.setWidth(10)
        pen.setColor(QColor(color))
        painter.setPen(pen)
        painter.drawPoint(x, y)
        painter.end()

        # Push the updated pixmap back to the label so it redraws.
        self.image_label.setPixmap(self.pixmap)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Run this, press Ctrl+O to open an image, and then click anywhere on it. Green dots appear for left-clicks, yellow dots for right-clicks.

A Few Things to Keep in Mind

Dots are painted permanently onto the pixmap. Each call to draw_dot modifies the pixmap in memory. If you want an "undo" feature, you'd need to keep the original pixmap stored separately and repaint all the dots from a list each time. For example:

python
# Store the original so you can rebuild from scratch.
self.original_pixmap = QPixmap(file_path)
self.pixmap = self.original_pixmap.copy()

Then to undo, you copy from original_pixmap again and redraw only the dots you want to keep.

Coordinate alignment matters. If the label is larger than the image and you're using centered alignment, the click coordinates from the label won't line up with pixel positions on the image. You'd need to calculate the offset between the label's top-left corner and where the pixmap actually starts. For simplicity, the example above works best when the label and pixmap are the same size, or when the image fills the label completely.

You don't need OpenCV to load images. The original code used OpenCV (cv2.imread) and manually converted the image to a QImage. While that works, QPixmap can load common image formats directly, which simplifies the code considerably. If you do need OpenCV for other processing, you can still convert to QPixmap afterward — just be aware that OpenCV uses BGR color order, so you'll need to swap channels (as the original code did with rgbSwapped()).

If you want to go further with custom painting in PyQt6 — drawing lines, shapes, or building a full drawing application — take a look at our QPainter tutorial.

PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks

See the course

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

Drawing Dots on an Image with Mouse Clicks in PyQt6 was written by Martin Fitzpatrick.

Martin Fitzpatrick is the creator of Python GUIs, and has been developing Python/Qt applications for the past 12+ years. He has written a number of popular Python books and provides Python software development & consulting for teams and startups.