5 Best Ways to Draw a Rectangle with Mouse Events in OpenCV and Python

πŸ’‘ Problem Formulation: In computer vision and GUI development, you may want to allow users to draw rectangles on an image or a canvas with their mouse. The user’s task could be to select an area of interest (like marking an object to track), which can be achieved by clicking and dragging the mouse to form a rectangle. The expected output is a visual rectangle on the screen corresponding to the mouse movement.

Method 1: Basic Event Handling

This method involves setting up a mouse callback function to respond to different mouse events. We initialize two global variables to store the starting and ending points of the rectangle. The event handling function updates these points and uses cv2.rectangle() to draw the rectangle.

Here’s an example:

import cv2
# Initialize the global points
start_point = None
end_point = None

# The mouse callback function
def draw_rectangle(event, x, y, flags, params):
    global start_point, end_point
    if event == cv2.EVENT_LBUTTONDOWN:
        start_point = (x, y)
    elif event == cv2.EVENT_MOUSEMOVE:
        if start_point is not None:
            end_point = (x, y)
    elif event == cv2.EVENT_LBUTTONUP:
        end_point = (x, y)

# Create a window and bind the function to window
cv2.namedWindow('Frame')
cv2.setMouseCallback('Frame', draw_rectangle)

# Displaying the image
img = cv2.imread('image.jpg')
while True:
    if start_point and end_point:
        cv2.rectangle(img, start_point, end_point, (255, 0, 0), 2)
    cv2.imshow('Frame', img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()

Output: A window displaying the image with the user-drawn rectangles in real-time as the mouse is moved and clicked.

This code snippet sets up a window where users can draw rectangles on the loaded image. When the left mouse button is clicked, the starting point of the rectangle is set. As the mouse moves, the rectangle is continuously drawn to match the new position until the mouse button is released, finalizing the end point.

Method 2: Rectangle Drawing with Drag and Drop

In this method, we refine the drawing process to only show the final rectangle once the mouse button is released. This prevents the continuous redrawing seen in the first method and only displays the final rectangle, providing a cleaner interaction.

Here’s an example:

import cv2
start_point = None
end_point = None
drawing = False

def draw_rectangle(event, x, y, flags, param):
    global start_point, end_point, drawing
    if event == cv2.EVENT_LBUTTONDOWN:
        drawing = True
        start_point = (x, y)
    elif event == cv2.EVENT_MOUSEMOVE:
        if drawing:
            end_point = (x, y)
    elif event == cv2.EVENT_LBUTTONUP:
        drawing = False
        end_point = (x, y)
        cv2.rectangle(img, start_point, end_point, (0, 255, 0), 2)

cv2.namedWindow('Frame')
cv2.setMouseCallback('Frame', draw_rectangle)

img = cv2.imread('image.jpg')
while True:
    cv2.imshow('Frame', img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
    if start_point and end_point and not drawing:
        cv2.rectangle(img, start_point, end_point, (0, 255, 0), 2)

cv2.destroyAllWindows()

Output: A window displays the image with a green rectangle only when the user releases the mouse button.

This modified callback function introduces a boolean variable, drawing, to track whether the mouse is currently being dragged. The rectangle is now only drawn once the user has finished selecting the area, which provides a more user-friendly experience.

Method 3: Class-based Approach

The class-based approach encapsulates the functionality within a class structure. This promotes better organization of code, making it more maintainable and reusable. The class manages its state internally, and the drawing function is a method of the class.

Here’s an example:

import cv2
class RectangleDrawer:
    def __init__(self):
        self.start_point = None
        self.end_point = None
        self.drawing = False

    def draw_rectangle(self, event, x, y, flags, param):
        if event == cv2.EVENT_LBUTTONDOWN:
            self.drawing = True
            self.start_point = (x, y)
        elif event == cv2.EVENT_MOUSEMOVE and self.drawing:
            self.end_point = (x, y)
        elif event == cv2.EVENT_LBUTTONUP:
            self.drawing = False
            self.end_point = (x, y)

drawer = RectangleDrawer()
cv2.namedWindow('Frame')
cv2.setMouseCallback('Frame', drawer.draw_rectangle)

img = cv2.imread('image.jpg')
while True:
    if drawer.start_point and drawer.end_point:
        cv2.rectangle(img, drawer.start_point, drawer.end_point, (0, 0, 255), 2)
    cv2.imshow('Frame', img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()

Output: A window where the user can draw blue rectangles on the image, with the code neatly organized into a class.

The RectangleDrawer class encapsulates drawing logic and rectangle state, simplifying the main loop’s logic. OOP principles like encapsulation and separation of concerns are leveraged, making the code more modular and testable.

Method 4: Drawing Rectangles with Real-time Preview

This method provides real-time feedback as the user drags the mouse to draw the rectangle. Instead of directly drawing on the original image, we create a temporary copy and draw the rectangle on it, preserving the original image.

Here’s an example:

import cv2
start_point = None
end_point = None
# Global variable to allow real-time updating of the rectangle
temp_img = None

def draw_rectangle(event, x, y, flags, param):
    global start_point, end_point, temp_img
    if event == cv2.EVENT_LBUTTONDOWN:
        start_point = (x, y)
    elif event == cv2.EVENT_MOUSEMOVE:
        if start_point is not None:
            temp_img = img.copy()
            cv2.rectangle(temp_img, start_point, (x, y), (0, 255, 255), 2)
    elif event == cv2.EVENT_LBUTTONUP:
        end_point = (x, y)

cv2.namedWindow('Frame')
cv2.setMouseCallback('Frame', draw_rectangle)

img = cv2.imread('image.jpg')
temp_img = img.copy()
while True:
    cv2.imshow('Frame', temp_img if start_point else img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
    # Reset temp_img at every iteration
    temp_img = img.copy()

cv2.destroyAllWindows()

Output: A window where the user gets a real-time preview of the cyan rectangle as it’s being drawn.

The code now creates a temporary copy of the image on which the rectangle is drawn. This way, the user sees a live update of the rectangle’s shape and position without making permanent changes to the original image, which only gets updated upon mouse release.

Bonus One-Liner Method 5: Drawing with a Higher-level GUI library

In addition to raw OpenCV functions, Python offers higher-level GUI libraries, such as Tkinter, which can be used in tandem with OpenCV. This method might require less code but could be less flexible than the pure OpenCV approach.

Here’s an example:

# No example provided, as this method is more of a conceptual alternative.

Output: This would involve a higher-level abstraction and potentially a more responsive GUI element for rectangle drawing.

Although not specifically utilizing OpenCV, incorporating a higher-level GUI toolkit with OpenCV can simplify tasks but can introduce library dependencies and abstraction overheads.

Summary/Discussion

Method 1: Basic Event Handling. Straightforward to implement but not visually clean due to continuous drawing.
Method 2: Drawing with Drag and Drop. Cleaner end-user experience. Slightly more complex logic with an additional state variable.
Method 3: Class-based Approach. OOP approach, more structured, better code organization. Requires understanding of classes in Python.
Method 4: Real-time Preview Drawing. More responsive UX with live preview of the rectangle. Higher complexity due to image duplication.
Method 5: Higher-level GUI Library. Potentially simpler code through abstraction but at the cost of flexibility and additional dependencies.