Live camera-based angle calculator using python and OpenCV

ANGLE CALC GIF.gif

After building a portion of your foundation, it’s best to continue learning by building something that’s not only useful, but also looks insanely impressive. So here you have it, a basic angle calculator.

UPDATE: WordPress is changing some of my code blocks to ‘amp’ and I haven’t yet found a way to fix this. For further guidance (although it would be a good exercise to infer), head over to my github repository.

LINK TO GITHUB GIST WITH ANGLECALC: https://gist.github.com/botforge/c88b842cafaa077a91048d51c2db0bdf

Prerequisites

This tutorial assumes you have some degree of proficiency with Python and can reasonably understand the OpenCV code here.

Determine HSV Range (again)

Before you continue writing the code you’ll need to use this HSV Trackbar to determine the Hue Low/High, Saturation Low/High and Value Low/High for the object you want to track. Mess around with the trackbars until you can only see the color of the object you are looking for. Repeat this process twice, for 2 differently colored objects. Note these values down, you will need them for later.

Filter for HSV Colors

Creating functions makes life a billion times easier, and allows you to organize your code much more effectively. I wrote the code initially with the functions findorange and findblue , although I eventually ended up using green and orange.

#import libs
import cv2
import numpy as np
import math

#uses distance formula to calculate distance
def distance((x1, y1), (x2,y2)):
    dist = math.sqrt((math.fabs(x2-x1))**2+((math.fabs(y2-y1)))**2)
    return dist

#filters for blue color and returns blue color position.
def findblue(frame):
    maxcontour = None
    blue = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    bluelow = np.array([55, 74, 0])#replace with your HSV Values
    bluehi = np.array([74, 227, 255])#replace with your HSV Values
    mask = cv2.inRange(blue, bluelow, bluehi)
    res = cv2.bitwise_and(frame, frame, mask=mask)

#filters for orange color and returns orange color position.
def findorange(frame):
    maxcontour = None
    orange = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    orangelow =  np.array([0, 142, 107])#replace with your HSV Values
    orangehi = np.array([39, 255, 255])#replace with your HSV Values
    mask = cv2.inRange(orange, orangelow, orangehi)
    res = cv2.bitwise_and(frame, frame, mask=mask)

Just remember to change the bluelow and bluehi and orangelow and orangehi array’s elements to those that suit your color choice. All of the functions used should be familiar from my tutorial on ‘Object Tracking and Following with OpenCV Python‘; read that if you don’t get some of it. What we’ve essentially done is sent the initial frame as a parameter to each of these functions, where they then convert to HSV and threshold for the color.

Return object positions

Next, you want to continue building on findblue() and findorange() by allowing them to return the coordinates of your objects.

def findblue(frame):
    blue = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    bluelow = np.array([55, 74, 0])#replace with your HSV Values
    bluehi = np.array([74, 227, 255])#replace with your HSV Values
    mask = cv2.inRange(blue, bluelow, bluehi)
    res = cv2.bitwise_and(frame, frame, mask=mask)
    cnts, hir = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    if len(cnts) >0:
        maxcontour = max(cnts, key = cv2.contourArea)

        #All this stuff about moments and M['m10'] etc.. are just to return center coordinates
        M = cv2.moments(maxcontour)
        if M['m00'] > 0 and cv2.contourArea(maxcontour)>2000:
            cx = int(M['m10'] / M['m00'])
            cy = int(M['m01'] / M['m00'])
            return (cx, cy), True
        else:
            #(700,700), arbitrary random values that will conveniently not be displayed on screen
            return (700,700), False
    else:
        return (700,700), False
#filters for orange color and returns orange color position.
def findorange(frame):
    orange = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    orangelow =  np.array([0, 142, 107])#replace with your HSV Values
    orangehi = np.array([39, 255, 255])#replace with your HSV Values
    mask = cv2.inRange(orange, orangelow, orangehi)
    res = cv2.bitwise_and(frame, frame, mask=mask)
    cnts, hir = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    if len(cnts) >0:
        maxcontour = max(cnts, key = cv2.contourArea)
        M = cv2.moments(maxcontour)
        if M['m00'] > 0 and cv2.contourArea(maxcontour)>2000:
            cx = int(M['m10'] / M['m00'])
            cy = int(M['m01'] / M['m00'])
            return (cx, cy), True
        else:
            return (700,700), False
    else:
        return (700,700), False

The cv2.Moments function and the cx and cy variable declarations are best explained in OpenCV’s introduction to contours. But simply put, lines n – n just return the coordinates of the center of the contour.

The reason we have the blogic boolean variable is just to validate that the object is present on screen. If the object isn’t present, this variable will be set to False, and the coordinates will be (700,700). I chose this set of points arbitrarily, as these, even when plotted, couldn’t be seen on my 300 x 400 window.

Distance Function

We’ll be using trigonometry to calculate the angle, so for this you’ll need to create a function that measures the distance between both points. For this, we use the standard distance equation  you should’ve learnt in high school.

#uses distance formula to calculate distance
def distance((x1, y1), (x2,y2)):
    dist = math.sqrt((math.fabs(x2-x1))**2+((math.fabs(y2-y1)))**2)
    return dist

 

Main Loop

#capture video
cap = cv2.VideoCapture(0)

while(1):
    _, frame = cap.read()
#if you're sending the whole frame as a parameter,easier to debug if you send a copy
    fra = frame.copy() 

     #get coordinates of each object
    (bluex, bluey), blogic = findblue(fra)
    (orangex, orangey), ologic = findorange(fra)
    #draw two circles around the objects (you can change the numbers as you like)
    cv2.circle(frame, (bluex, bluey), 20, (255, 0, 0), -1)
    cv2.circle(frame, (orangex, orangey), 20, (0, 128, 255), -1)

Our foundation is set. We’ve made a program that tracks the position of 2 different colored objects on screen, next, we need to apply trig to calculate the angle and display the entire setup in the most grandiose manner possible.

    if blogic and ologic:
        #quantifies the hypotenuse of the triangle
        hypotenuse =  distance((bluex,bluey), (orangex, orangey))
        #quantifies the horizontal of the triangle
        horizontal = distance((bluex, bluey), (orangex, bluey))
        #makes the third-line of the triangle
        thirdline = distance((orangex, orangey), (orangex, bluey))
        #calculates the angle using trigonometry
        angle = np.arcsin((thirdline/hypotenuse))* 180/math.pi

        #draws all 3 lines
        cv2.line(frame, (bluex, bluey), (orangex, orangey), (0, 0, 255), 2)
        cv2.line(frame, (bluex, bluey), (orangex, bluey), (0, 0, 255), 2)
        cv2.line(frame, (orangex,orangey), (orangex, bluey), (0,0,255), 2)

Our code is officially complete…sort of. If you run it, it’ll look great and work great but you’ll notice that it won’t detect any angle over 90 degrees. If you’re familiar with trig, you’ll know why, but to evade this situation and allow it to calculate angles until 180 degrees, we need a few more lines of code.

     #Allows for calculation until 180 degrees instead of 90
if orangey < bluey and orangex > bluex:
    cv2.putText(frame, str(int(angle)), (bluex-30, bluey), cv2.FONT_HERSHEY_SCRIPT_COMPLEX, 1, (0,128,220), 2)
elif orangey < bluey and orangex < bluex:
    cv2.putText(frame, str(int(180 - angle)),(bluex-30, bluey), cv2.FONT_HERSHEY_SCRIPT_COMPLEX, 1, (0,128,220), 2)
elif orangey > bluey and orangex < bluex:
    cv2.putText(frame, str(int(180 + angle)),(bluex-30, bluey), cv2.FONT_HERSHEY_SCRIPT_COMPLEX, 1, (0,128,220), 2)
elif orangey > bluey and orangex > bluex:
    cv2.putText(frame, str(int(360 - angle)),(bluex-30, bluey), cv2.FONT_HERSHEY_SCRIPT_COMPLEX, 1, (0,128, 229), 2)
if k == ord('q'): break

And that’s it! If the object tracker didn’t impress already, now you have a live angle calculator using just your camera.

Leave a comment