How To Create A Paint Brush Application Using OpenCV

How To Create A Paint Brush Application Using OpenCV


10 min read

Creating a paintbrush application is one of the most fun activity while learning computer vision and image processing with a little bit of GUI concepts. Using OpenCV for such an application saves us a lot of time by not needing to know the basics of GUI programming, so we can only be left with image processing tasks at hand.

In this article, we are going to make a paint application that features the brush functionality with customizations like:

  • Changing brush size using a trackbar.
  • Changing paint colour using a trackbar.
  • Viewing the selected colour on the window.

We will also see how current trackbar values can be displayed on the window, so we have a general idea of the colour variation.

Note: We are going to make this application on macOS and somehow the in-built create trackbar method of OpenCV does not show selected trackbar values, hence the need to display these values on the image window itself.

If you do not have OpenCV installed, I suggest you visit here and check Prerequisites steps on the process of installation.

Without any further ado, let's start making our first paintbrush application.

Imports and Variables

Create a python file, name it whatever you like. Add below code in it (make sure you have OpenCV installed):

# ./

import cv2
import numpy as np

draw = False
window_name = "Paint Brush Application"
bgr_track = {'B': 0, 'G': 0, 'R': 0}

img = np.zeros((512,512,3), np.uint8)

# Initial color window, showing black color, same as paint area
cv2.rectangle(img, color_win_position[0], color_win_position[1], (0,0,0), -1)
  • First 2 lines import necessary libraries. Here, we are going to import cv2 and numpy with an alias np.
  • Next 3 lines define some constants that we are going to use later. draw says if the mouse is drawing on image window, window_name describes the name of the application window that is Paint Brush Application and bgr_track is a dictionary that we will use to keep track of previous BGR values (you will know later why we are doing this).
  • Next 2 lines create an area for our paint application and set a name to the window. img contains a numpy array of 3 dimensions where each value is zero, meaning it will be a black coloured area. cv2.namedWindow attaches a name to the image we just created, we can later use this window name to place all other things like trackbar.
  • The last line of this code snippet creates a rectangle window which we will be using for viewing our colour that we are choosing from the trackbar.

Now, add the below code:

img = cv2.putText(img, "R: ", (10, 30), font, 0.5, (255,255,255), 1)
img = cv2.putText(img, "G: ", (90, 30), font, 0.5, (255,255,255), 1)
img = cv2.putText(img, "B: ", (170, 30), font, 0.5, (255,255,255), 1)

img = cv2.putText(img, "0", (30, 30), font, 0.5, (255,255,255), 1)
img = cv2.putText(img, "0", (110, 30), font, 0.5, (255,255,255), 1)
img = cv2.putText(img, "0", (190, 30), font, 0.5, (255,255,255), 1)
  • Now, we put text on the image window that will work as the counter for values of the BGR trackbar. Since on macOS, the track bar is not showing selected values, so we need this functionality. Also, take it as a side activity that we can do.
  • We put text using the cv2.putText method of OpenCV and set a font as well. We can set any font we want, here we are using Hershey simplex font.
  • We are placing text like R: and 0 separately because we need to change only number values and not constants like R:.
  • We can select any random position for these counters on the image and check which best suits our needs.

So far, we have all the variables that we need, now let's move onto creating some interesting functions!

Functions - All We Need

Add below python code snippet to the same file where we left above:

def nothing(x):

def update_R_value(x):
    global font, img, bgr_track
    img = cv2.putText(img, f"{bgr_track['R']}", (30, 30), font, 0.5, (0,0,0), 1)
    img = cv2.putText(img, f"{x}", (30, 30), font, 0.5, (255,255,255), 1)
    bgr_track['R'] = x

def update_G_value(x):
    global font, img, bgr_track
    img = cv2.putText(img, f"{bgr_track['G']}", (110, 30), font, 0.5, (0,0,0), 1)
    img = cv2.putText(img, f"{x}", (110, 30), font, 0.5, (255,255,255), 1)
    bgr_track['G'] = x

def update_B_value(x):
    global font, img, bgr_track
    img = cv2.putText(img, f"{bgr_track['B']}", (190, 30), font, 0.5, (0,0,0), 1)
    img = cv2.putText(img, f"{x}", (190, 30), font, 0.5, (255,255,255), 1)
    bgr_track['B'] = x
  • We create a function like nothing that does nothing just do a simple pass. We can use it to fulfil our need to pass it as a parameter where it is required but we want it to not do anything.
  • Next 3 functions are the same with just a difference of B, G and R. Let's explore update_R_value.
  • Here we are trying to update the counter value of R: 0 on the image window.
  • First, we get all global variables and try to replace the already present value of 0 against R:. In OpenCV, there is no feature that lets cv2.putText keep track of previous text on the same position so it could just replace it. Here we need to first put the same text as the previous value, like 0 in this case, and change colour similar to the background of the paint area (here it is black) so that it is like that it vanished from the paint area. Now we just put a new trackbar value, which is passed to the function as x with the colour white in the same place. Lastly, we update the bgr_track dictionary that keeps the track history of the last value it put and the same process repeats.

The same kind of thing is going on with the other 2 functions, just for different colour trackbar values.

In simple terms, take the below scenario:

  • Let's say we have a list - l = [0, 0, 0]. Here all the values were -1 initially representing the start stage of the list. It is a special kind of list where we can only change its values only if they are -1 first and nothing else.
  • What we are going to do now is that, replace the first index (R colour value let's say) with a new value.
  • So we try to replace 0 knowing its position with -1 because if we replace it directly it overlaps and doesn't work. So what we do instead is l[0] = -1 and then after setting it to -1, it is back to its original state from where we can do anything to its value. Now we do the actual replace l[0] = x.

So what just happened? If you need more insight on this please visit my answer on StackOverflow here.

Let's move on to mouse events, which will help us to draw using a paintbrush.

def draw_circle(event, x, y, flags, param):
    global draw, img

    if event == cv2.EVENT_LBUTTONDOWN:
        draw = True

    elif event == cv2.EVENT_MOUSEMOVE:
        if draw:
  , (x,y), cv2.getTrackbarPos("Brush Size", window_name),
                       (cv2.getTrackbarPos("B", window_name),
                        cv2.getTrackbarPos("G", window_name),
                        cv2.getTrackbarPos("R", window_name)),

    elif event==cv2.EVENT_LBUTTONUP:
        draw = False, (x,y), cv2.getTrackbarPos("Brush Size", window_name),
                       (cv2.getTrackbarPos("B", window_name),
                        cv2.getTrackbarPos("G", window_name),
                        cv2.getTrackbarPos("R", window_name)),
  • This method draw_circle will be passed to the mouse event callback method of OpenCV. It takes 5 parameters - event that gives what is the event type, x and y the position of mouse, flags that can give us any flags that mouse events raise and some other param related to mouse callbacks. We can ignore flags and params for this case.
  • We have 3 conditions here. if event == cv2.EVENT_LBUTTONDOWN checks for if the left mouse button down event occurred or not, if it happens then we will set draw to True.
  • elif event == cv2.EVENT_MOUSEMOVE this condition checks if the mouse is moving, we simply don't want a moving mouse to draw on the paint area but we want a left button down moving the mouse to draw on the paint area, that's where the draw variable comes in, which will be checked to see if a left mouse button is actually down or not. Then it creates a circle of defined radius that it gets from the Brush Size trackbar, the colour of the circle from the BGR trackbar and colour fill is done via the last -1 parameter value. This creates a circle while moving a mouse and this happens so smoothly that it shows an effect of a paintbrush.
  • elif event==cv2.EVENT_LBUTTONUP is for when the mouse button is not clicked and we don't want to draw on the paint area. Here, we set draw to False and draw the last circle on positions given.

Now, let's put trackbar and mouse event callback. Also, we will create a loop to continuous display the changes to the image window.

Trackbar, Mouse Event Call and Main Loop

Add the following code to the same file:

cv2.createTrackbar("R", window_name, 0 ,255, update_R_value)
cv2.createTrackbar("G", window_name, 0, 255, update_G_value)
cv2.createTrackbar("B", window_name, 0, 255, update_B_value)
cv2.createTrackbar("Brush Size", window_name, 1, 8, nothing)
cv2.setMouseCallback(window_name, draw_circle)
  • Here, we create a trackbar for R, G, B colour values and for Brush Size adjustment. cv2.createTrackbar method takes the following arguments, trackbar name R, window name where the trackbar needs to be put (here window_name), start value 0, end value 255 of trackbar and last is a callback function which calls update_R_value whenever trackbar value gets changed. All 4 trackbars are similar to this structure and are self-explanatory now.
  • cv2.setMouseCallback takes 2 parameters to set a mouse callback event, first one is the image window window_name on which this callback event should be tracked and the second is the callback function to call when a mouse event occurs.

Notice here, that we don't have separate callback methods for MOUSE_UP or MOUSE_DOWN, but only a single one that captures all events with a passed parameter to callback function event and later we can use conditionals to filter them out.

Now, add the following code to the same file for our main loop:

    cv2.imshow(window_name, img)
    key = cv2.waitKey(1) & 0xff
    if key==ord('q'):

    b = cv2.getTrackbarPos("B", window_name)
    g = cv2.getTrackbarPos("G", window_name)
    r = cv2.getTrackbarPos("R", window_name)
    cv2.rectangle(img, color_win_position[0], color_win_position[1], (b,g,r), -1)

  • Here, we want the loop to continue until it encounters a keyboard keypress event. We are going to quit the application whenever the Q key is clicked.
  • Inside the loop, we are checking for keyboard quit condition and also getting BGR values and displaying them in a rectangle at color_win_position.
  • At last, if the program exits the loop, then we simple destroy the unwanted image window using the cv2.destroyAllWindows method of OpenCV.

Well, that's complete our code.

For complete script, please visit here.

Now, let's look at the demo.

Conclusion Demo

Displaying colour trackbar values.


Displaying colour trackbar colours.


Changing brush size.


Hello World, CodeKaro!


I know, my paint skills are very poor!

What Next?

Well, that's it from me. If you want to explore further, the following activities can be done to add more feature to the paint application:

  • Add utility to select brush type. Add rectangle, diamond, hexagon, etc shaped brushes.
  • Try to include no-fill brush utility.
  • Try to add a function that creates a circle or rectangle with a given size with a double click of the mouse on the paint area.
  • Add a utility to clear the area and restart the work, by not having to close and restart the program.

  • Just starting your Open Source Journey? Don't forget to check Hello Open Source

  • Need inspiration or a different perspective on the Python projects or just out there to explore? Check Awesome Python Repos

  • Want to make a simple and awesome game from scratch? Check out PongPong

  • Want to ++ your GitHub Profile README? Check out Quote - README

Till next time!

Namaste ๐Ÿ™

Did you find this article valuable?

Support Siddharth Chandra by becoming a sponsor. Any amount is appreciated!