Search code examples
pythonopencvimage-processingcontouredge-detection

Tolerance issue while detecting Trapezium and Rhombus using Opencv


I am supposed to write a code to recognise different shape, we were given a folder in which images of different shapes are given (Circle/ Triangle/ Trapezium/ Rhombus/ Square/ Quadrilateral/ Parallelogram/ Pentagon/ Hexagon). On recognising them it should return the output in the following format: { 'Shape': ['color', Area, cX, cY] }

My code keeps confusing Trapezium and Rhombus. I debugged it and found it's because of +/-1 tolerance in sides and angle. What should I do?.

Here are some of the links of StackOverflow that I tried: OpenCV: How to detect rhombus on an image? Detecting trapezium, rhombus, square, quadrilateral, parallelogram by Opencv in Python OpenCV shape detection Tutorial for iPhone OpenCV on shape recognising [closed]

There were more but they had community issues, so I won't waste your time on them. Long story short I did not find anything useful.

import cv2
import numpy as np

img = cv2.imread('Sample3.png',-1)


class ShapeColorRecognition():
    shape = 'unidentified'
    color = 'undetected'

def __init__(self,img):
    global shape,color,cX, cY,area
    self.shapeList = []
    
    #Getting Contours
    self.img = img
    gray = cv2.cvtColor(self.img,cv2.COLOR_BGR2GRAY)
    blurred = cv2.blur(gray,(5,5))
    ret,thresh = cv2.threshold(blurred,230,255,cv2.THRESH_BINARY)
    contours,_ = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

    for cnt in contours[1:]:
        #Approximating the contours
        approxCont = cv2.approxPolyDP(cnt,0.1*cv2.arcLength(cnt,True),True)
        #Calculating the Centroid coordinates of the particular object by moments
        M = cv2.moments(cnt)
        cX = int(M['m10']/M['m00'])
        cY = int(M['m01']/M['m00'])
        area = cv2.contourArea(cnt)
        #Converting image to the HSV format
        hsv = cv2.cvtColor(self.img,cv2.COLOR_BGR2HSV)
        #Calling the functions for Shapes and Colors
        self.shape(approxCont)
        self.color(hsv,cX, cY)
        #Final List Containing the shape and the color of the object
        self.shapeList.append(tuple([shape,color, area, cX,cY]))

def shape(self,approx):
    """Getting the name of shape of approximated contour"""
    global shape
    p1 = approx[0][0]
    p2 = approx[1][0]
    p3 = approx[-1][0]
    p4 = approx[2][0]
    if len(approx) == 3:
        shape = 'Triangle'
    elif len(approx) == 4:
        (degrees) = self.get_corner_angle(p1, p2, p3)
        (degrees_opp) = self.get_corner_angle_opp(p4, p2, p3)
        dist1 = self.distance(p1, p2, p3, p4)

        # print(degrees)
        if ((89 <= int(degrees) <= 91) and (89 <= int(degrees_opp) <= 91)) and (a == b):
            shape = "Square"
        elif (a == True or b == True)  and (int(degrees) != 90 and int(degrees_opp) != 90):
            shape = "Trapezoid"
            print(int(degrees_opp))
            print(int(degrees))
            print(l1)
            print(l2)
            print(l3)
            print(l4)
        elif (int(degrees) == int(degrees_opp)) and(a == b):
            shape = "Rhombus"
        elif (a == True or b == True) and (int(degrees) == int(degrees_opp)):
            shape  = "Parallelogram"
        elif (int(degrees) != int(degrees_opp)) and (a == False and b == False):
            shape = "Quadilateral"
            print(int(degrees))
            print(l1)
            print(l2)
            print(l3)
            print(l4)

    elif len(approx) == 5:
        shape = 'Pentagon'
    elif len(approx) == 6:
        shape = 'Hexagon'
    else:
        shape = 'Circle'

def unit_vector(self, v):
    return v / np.linalg.norm(v)

def distance(self, p1, p2, p3, p4):
    global l1, l2, l3, l4, a ,b 
    l1 = int(((p4[0] - p3[0])**2 + (p4[1] - p3[1])**2)**0.5)
    l2 = int(((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)**0.5)
    l3 = int(((p3[0] - p1[0])**2 + (p3[1] - p1[1])**2)**0.5)
    l4 = int(((p4[0] - p2[0])**2 + (p4[1] - p2[1])**2)**0.5)
    a = l1 == l2
    b = l3 == l4
    return l1, l2, l3, l4, a, b

def get_corner_angle(self, p1, p2, p3):
    v1 = np.array([p1[0] - p2[0], p1[1] - p2[1]])
    v2 = np.array([p1[0] - p3[0], p1[1] - p3[1]])
    v1_unit = self.unit_vector(v1)
    v2_unit = self.unit_vector(v2)
    radians = np.arccos(np.clip(np.dot(v1_unit, v2_unit), -1, 1))
    return np.degrees(radians)

def get_corner_angle_opp(self, p4, p2, p3):
    v3 = np.array([p4[0] - p2[0], p4[1] - p2[1]])
    v4 = np.array([p4[0] - p3[0], p4[1] - p3[1]])
    v3_unit = self.unit_vector(v3)
    v4_unit = self.unit_vector(v4)
    radians = np.arccos(np.clip(np.dot(v3_unit, v4_unit), -1, 1))
    return np.degrees(radians)

def color(self,hsv_img,cX,cY):
    """Gives the name of the color of the shape"""
    global color

    #Getting Hue,Saturation,value of the centroid of the shape from HSV image
    h,s,v = hsv_img[cY,cX]

    #Getting final name of the color according their ranges in the HSV color space
    h,s,v = hsv_img[cY, cX]
    if h in range(0,11) or h in range(170,180):
        color = 'Red'
    elif h in range(51,76):
        color = 'Green'
    elif h in range(106,131):
        color = 'Blue'
    return color

#Creating the Object of class
shapeColorObj = ShapeColorRecognition(img)

#Final output

for ans in shapeColorObj.shapeList:
    value = []
    key = (ans[0])
    value.append(ans[1])
    value.append(ans[2])
    value.append(ans[3])
    value.append(ans[4])


# shape.update({key: value})
print(value)
print(key)
# print(shape)

Also as you can see my shapes names are store in a variable and whenever I try to use as a key, it gives an error

AttributeError: 'str' object has no attribute 'update'

or

TypeError: 'str' object does not support item assignment

This is my Output:

 (base) C:\Users\Windows 10\OneDrive\Desktop\Python\Eynatra assignment>image.py
['Green', 36612.5, 191, 361]
Trapezoid
['Red', 22709.0, 831, 392]
Triangle
['Blue', 50968.0, 524, 361]
Square

Thank you a lot in advance.

This is one of many images (an example)


Solution

  • As I mentioned, I was getting a tolerance of +/-1, so I used that tolerance to create a range and set if-elif-else conditions. I guess that's it. Correct this if you think it's not enough

    p.s. I already have less reputation point so please don't vote. I am always open to constructive suggestions.

    import cv2
    import numpy as np
    
    img = cv2.imread('Sample4.png',-1)
    
    #Class for recognition of the Shapes and Colors the shapes in the image given
    class ShapeColorRecognition():
        shape = 'unidentified'
        color = 'undetected'
        
        def __init__(self,img):
            global shape,color,cX, cY,area
            self.shapeList = []
            
            #Getting Contours
            self.img = img
            gray = cv2.cvtColor(self.img,cv2.COLOR_BGR2GRAY)
            blurred = cv2.blur(gray,(5,5))
            ret,thresh = cv2.threshold(blurred,230,255,cv2.THRESH_BINARY)
            contours,_ = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
     
            for cnt in contours[1:]:
                #Approximating the contours
                approxCont = cv2.approxPolyDP(cnt,0.01*cv2.arcLength(cnt,True),True)
                #Calculating the Centroid coordinates of the particular object by moments
                M = cv2.moments(cnt)
                cX = int(M['m10']/M['m00'])
                cY = int(M['m01']/M['m00'])
                area = cv2.contourArea(cnt)
                #Converting image to the HSV format
                hsv = cv2.cvtColor(self.img,cv2.COLOR_BGR2HSV)
                #Calling the functions for Shapes and Colors
                self.shape(approxCont)
                self.color(hsv,cX, cY)
                #Final List Containing the shape and the color of the object
                self.shapeList.append(tuple([shape,color, area, cX,cY]))
    
        def shape(self,approx):
            """Getting the name of shape of approximated contour"""
            global shape
            p1 = approx[0][0]
            p2 = approx[1][0]
            p3 = approx[-1][0]
            p4 = approx[2][0]
            if len(approx) == 3:
                shape = 'Triangle'
            elif len(approx) == 4:
                (degrees) = self.get_corner_angle(p1, p2, p3)
                (degrees_opp) = self.get_corner_angle_opp(p4, p2, p3)
                dist1 = self.distance(p1, p2, p3, p4)
    
                # print(degrees)
                if ((89 <= int(degrees) <= 91) and (89 <= int(degrees_opp) <= 91)) and (a == b):
                    shape = "Square"
                elif (a == True or b == True)  and (int(degrees) or int(degrees_opp) !=90) and (int(degrees_opp)-int(degrees) not in (-1,0,1)):
                    shape = "Trapezoid"
                elif (int(degrees)-int(degrees_opp) in (-1,0,1)) and(a-b in (-1,0,1)):
                    shape = "Rhombus"
                elif (a == True or b == True) and (int(degrees) == int(degrees_opp)):
                    shape  = "Parallelogram"
                elif (int(degrees) != int(degrees_opp)) and (a == False and b == False):
                    shape = "Quadilateral"
            elif len(approx) == 5:
                shape = 'Pentagon'
            elif len(approx) == 6:
                shape = 'Hexagon'
            else:
                shape = 'Circle'
    
        def unit_vector(self, v):
            return v / np.linalg.norm(v)
    
        def distance(self, p1, p2, p3, p4):
            global l1, l2, l3, l4, a ,b 
            l1 = int(((p4[0] - p3[0])**2 + (p4[1] - p3[1])**2)**0.5)
            l2 = int(((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)**0.5)
            l3 = int(((p3[0] - p1[0])**2 + (p3[1] - p1[1])**2)**0.5)
            l4 = int(((p4[0] - p2[0])**2 + (p4[1] - p2[1])**2)**0.5)
            a = l1 == l2
            b = l3 == l4
            return l1, l2, l3, l4, a, b
    
        def get_corner_angle(self, p1, p2, p3):
            v1 = np.array([p1[0] - p2[0], p1[1] - p2[1]])
            v2 = np.array([p1[0] - p3[0], p1[1] - p3[1]])
            v1_unit = self.unit_vector(v1)
            v2_unit = self.unit_vector(v2)
            radians = np.arccos(np.clip(np.dot(v1_unit, v2_unit), -1, 1))
            return np.degrees(radians)
        
        def get_corner_angle_opp(self, p4, p2, p3):
            v3 = np.array([p4[0] - p2[0], p4[1] - p2[1]])
            v4 = np.array([p4[0] - p3[0], p4[1] - p3[1]])
            v3_unit = self.unit_vector(v3)
            v4_unit = self.unit_vector(v4)
            radians = np.arccos(np.clip(np.dot(v3_unit, v4_unit), -1, 1))
            return np.degrees(radians)
    
        def color(self,hsv_img,cX,cY):
            """Gives the name of the color of the shape"""
            global color
    
            #Getting Hue,Saturation,value of the centroid of the shape from HSV image
            h,s,v = hsv_img[cY,cX]
    
            #Getting final name of the color according their ranges in the HSV color space
            h,s,v = hsv_img[cY, cX]
            if h in range(0,11) or h in range(170,180):
                color = 'Red'
            elif h in range(51,76):
                color = 'Green'
            elif h in range(106,131):
                color = 'Blue'
            return color
    
    #Creating the Object of class
    shapeColorObj = ShapeColorRecognition(img)
    
    #Final output
    output=[]
    
    for ans in shapeColorObj.shapeList:
        value = []
        key = (ans[0])
        value.append(ans[1])
        value.append(ans[2])
        value.append(ans[3])
        value.append(ans[4])
        
        output.append((key,value))
    
    print(dict(sorted(output, key=lambda t: t[1][1], reverse=True)))