Using OpenCV to count Domino Dots

Dipika Baad
7 min readSep 23, 2023

--

Dominoes Dot Detection by Dipika Baad

Finding the project to work on

It’s always a challenge in learning for me to first finding a problem to solve that is interesting for me. I wanted to get my hands dirty with computer vision algorithms this time. What better than trying to solve a small inconvenience I get during playing Mexican train i.e. counting the dots on dominoes at the end of the game when I have lost miserably.

Requirements for the project

  • Python

Library to use

Let’s dive right into it! I looked into OpenCV as I had heard and seen around people using this library and considered this to be a popular library given it’s download and user count. Library can be used to build object detection/edge detection for images. Thought this would be a good start for this project after seeing sample projects built with this library.

Intro to OpenCV

OpenCV (Open Source Computer Vision Library) is a powerful and versatile open-source tool for computer vision and image processing tasks. It has more than 2500 optimized algorithms as of this writing. You can check out more functionality here: https://docs.opencv.org/4.8.0/d6/d00/tutorial_py_root.html

If you are looking for tutorials in other languages — you can find it out here:

https://docs.opencv.org/4.8.0/

This is where I started getting acustomed to how the library can be used in a nutshell by following these tutorials.

You can install the library using:

pip install opencv-python

My requirements.txt file for this project looks something like this

numpy==1.25.2
opencv-python==4.8.0.76

If you want to install the exact packages, you can run the following command.

pip install -r requirements.txt

1. Loading an image

To look into the details about what kind of formats and options it has for reading an image — head to imread documentation. Make sure to update the path to image to match your local folder directory.

2. Displaying the Image in a window

This will be useful at different stages to visualize the output of image. cv.imshow can be used to show the output in a display window and you can name it to differentiate between the outputs when shown. cv.waitKey(0) waits for any keyboard input, and closes the window when some key received. Following I will show two example images that I will be playing with to find out how OpenCV performs on two different settings.
Example 1: Image used for calculating the domino counts

Example 1: Dominoes image in a Display Window by Dipika Baad

Processing the image

Now that we have been familiar with the documentation and basics of how to use OpenCV, let’s dive into the steps to preprocess the image next.

  1. Blurring the image
    I realised that this step was needed while experimenting with the next steps I went through first. In the later stages the output I received had details that were not needed at all and this helps with getting rid of those details/noise in the beginning itself. You can try out the next steps without applying this step and you would see the difference. I used
    cv.GaussianBlur to achieve this. You can try out few methods, to see which one behaves good for your end result goal you are trying to achieve.
blurred = cv.GaussianBlur(img,(3,3),cv.BORDER_DEFAULT)

2. Controlling the contrast & brightness

Some of images I took needed some contrast control, as the colors on the dominos can be quite similar to their white background for example yellow dots especially. Edges detected (we will see this step in the later stages) result differed for yellow dots after adjusting the contrast as follows. 1.5 is the contrast control (cv.convertScaleAbs).

adjusted = cv.convertScaleAbs(blurred, 1.5)
cv.imshow('Adjusted bightness window', adjusted)
s = cv.waitKey(0)
Deleted = Without contrast adjsutment, Added = After contrast adjustment by Dipika Baad

3. Converting to Gray Scale

BRG colors are not important in detecting the circle shapes for us in this usecase. Reducing the complexity of the model and dimensionality (grayscale has 1 vs. BRG 3 dimensions) is why lot of the experiments you would encounter where they convert it to Grayscale.
Here is the link to find out different mapping in Color Space — cv.cvtColor

img2gray = cv.cvtColor(adjusted, cv.COLOR_BGR2GRAY)
cv.imshow('Gray window', img2gray)
s = cv.waitKey(0)
GrayScale Output by Dipika Baad

4. Thresholding the image

Once you have the grayscale image, we can take it further by limiting the values have either 0 or max value. We can define threshold above which it will get max value else 0. Here is the link to the function — cv.threshold. Again you need to decide the threshold value by experimenting with your images, see what suits the best for your use case.

I have also captured the output with cv.imwrite for later comparisons with other images.

ret, thresh1 = cv.threshold(img2gray, 180, 255, cv.THRESH_BINARY)
index = 1
cv.imwrite(f"thresh_{index}.png", img)
cv.imshow('Thresh window', thresh1)
s = cv.waitKey(0)
Threshold output by Dipika Baad

Calculating the dots

After applying different filters and preprocessing the image, we are ready to apply edge detection and use geomterical properties to count the dots.

  1. Get the edges

I chose Canny Algorithm method for edge detection from OpenCV. Edges will help in getting the shapes in the next step to count the dots. Canny edge detection gives good flexibility to obtain the interesting edges by adjusting the thresholds. Again this requires fiddling with the threshold to get the edges you are interested in.

Above code includes, previous preprocessing steps + applying canny edge detection.

Canny Detection Output by Dipika Baad

2. Finding the Contours

Contours are useful for shape analysis hence, as a next step would be to get the contours that represent any curves covering continuous points. It is recommended to use edge detection or thresholding to get better results out of contour.

Output from canny is sent as input to findContours. Hierarchical mode was used as the option (cv.RETR_TREE). After printing the hierarchy and contours, you can find the count is the same. To get hierarchy information such as next contour, child, parent etc. of contour i you can fetch ith element from hierarchy output.

contours, hierarchy = cv.findContours(edges, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
cv.drawContours(img, contours, -1, (0,255,0), 3)
print(len(contours))
print(len(hierarchy[0]))
Contours Output by Dipika Baad
countors: 443
hierarchy[0]: 443
contours
(array([[[ 818, 3239]],

[[ 818, 3240]],

[[ 817, 3241]],

[[ 817, 3242]],

[[ 818, 3243]],

[[ 820, 3243]],

[[ 820, 3239]]], dtype=int32),)
hierarchy
[[[ 2 -1 1 -1]
[ -1 -1 -1 0]
[ 3 0 -1 -1]
...
[441 439 -1 -1]
[442 440 -1 -1]
[ -1 441 -1 -1]]]

hierarchy output structure -> [Next, Previous, First_Child, Parent]

3. Finding the dots 🎲

Using the hierarchy data and area of the contours, I tried out few values and variations to make it count the dots I am interested in.

Here is the logic to loop through the contours and extracting the features you needed. This is by no means the perfect solution 🙃, but gets the job done for the image in question. This helped me to understand how to use different methods that can be used from the OpenCV’s collection. At the end, it needed some use case specific logic to extract what I wanted to extract.

  1. Area of the dots that can distinguish from the rest of the image needed to be experimented with.
  2. Getting rid of the duplicate shapes found and also experimenting with approxPolyDP output to find the appropriate number that represents the shapes of dots with and without shadow.
  3. Lot of minor noise was still there, hence had to use some custom logic to get rid of objects that had parents.
Dots with Count Output by Dipika Baad

43 dots ☑️

Conclusion

I was impressed with how easy it was to start working on and dive into experimenting with the sort of resources and documentation OpenCV has. On the way towards achieving the goal, it was slightly frustrating to see how many edge cases that needed to be handled as it is very sensitive to noise, lighting in the image.

From the examples I could find online for their use cases, it felt that it should have been straightforward and easy to get simple shape detections working with OpenCV. End result didn’t feel like totally the robust one, as the moment you start adding anything more to the image like an extra shadow which is similar in shape it might pick it up as a dot or simply create noisy edges around the shadows.

Example image I took with shadow from the right side you can see the thresholds used were not enough, if we increase the threshold then I would loose the yellow dots very quickly.

Results with shadow on the right side of the image by Dipika Baad

As I do want to want to build a functioning app that can tolerate all the noisy things and detecting dots only on the domino shapes, I would go with some object detection algorithms rather than relying on edge detections and contours for this use case. Hence, as a followup to this little project — I will train a model on some training examples of dominoes and compare the results.

As always — Happy experimenting and learning :)

--

--

Dipika Baad

Big Data Consultant @Netlight | CoFounder @HuskyCodes | Web developer | Passionate about coding, dancing, reading