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_somethingmethod 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:
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:
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:
- Keep your own copy of the pixmap as an instance variable.
- Paint onto that pixmap whenever you need to add a dot.
- Set the modified pixmap back onto the label so it updates the display.
Here's what that looks like in practice:
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.
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.
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:
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.
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:
# 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