Polygon Analysis: Overview and Explanation

Polygon Analysis

Our aim is to build a module which is enough to analyze a polygon and return some fruitful information like an angle, slope, sides, corner coordinates etc. So we build py2pyAnalysis. Two line of code is sufficient to tear down a polygon of upto 15 sides.

Installation

Use the package manager pip to install py2pyAnalysis.

pip install py2pyAnalysis

Samples

  • Pentagon

  • Nonagon

  • Irregular Decagon

  • Regular Decagon

  • Parallelogram

Documents

Let’s take a look at it,

def polygon_analysis(file_name,
                     show_and_save_contour='yes',
                     show_and_save_analysis='yes',
                     show_sides='yes',
                     show_angles='yes',
                     show_slope='yes',
                     show_name='yes',
                     save_data_to_csv='yes',
                     font=cv2.FONT_HERSHEY_PLAIN)

  • file_name : as clear from the name, it takes a file name, if the file is in the same directory then you can simply type name, else type the full path to the file.
  • show_and _save_contour : it allows you to see the various shapes that program detects in the image, it will also save the image file in a folder to Data/file_name_analysis/contour image. Everything that saves during the program related to this file will save in this particular folder only.
  • show_and_save_analysis : it allows you to see the numerical data on the image itself. the data includes Sides, Angles, and Slope. also, the name of an image will also be written there, The image will also save in the same directory as above.
  • Show_angles : This will show the angles on the image, changing it to NO will result to not showing angles on the image.
  • Show_slope: This will show the Slope on the image, changing it to NO will result to not showing Slope on the image.
  • font: you can change the Font if you want, Here is the list of some fonts
cv2.FONT_HERSHEY_SIMPLEX
cv2.FONT_HERSHEY_PLAIN
cv2.FONT_HERSHEY_DUPLEX 
cv2.FONT_HERSHEY_COMPLEX 
cv2.FONT_HERSHEY_TRIPLEX
cv2.FONT_HERSHEY_COMPLEX_SMALL
cv2.FONT_HERSHEY_SCRIPT_SIMPLEX
cv2.FONT_HERSHEY_SCRIPT_COMPLEX
 

This program returns

 return (len(sides),sides,distance,slope,angles,Name) 
  • len(sides): Number of sides in a detected Polygon.
  • Sides: Tit returns the coordinates of the shape. Side[0] is the first [x,y] set. Similarly side[0][0] & side[0][1] will give you first x and y point respectivelly.
  • Distances: It returns the distance between two points in pixels.
  • Slope: It returns back the slope values corresponds to each side.
  • Angle: It returns back an array of angles.
  • Name: It returns back the name of the shape.

If anytime you forgot the which come after which then just type

import py2pyAnalysis as py 
py.help()
 

and it will bring you the code below

     print("""#https://pypi.org/project/py2pyAnalysis/
#https://github.com/Pushkar-Singh-14/Polygon-Analysis
#http://py2py.com/polygon-analysis-overview-and-explaination/

Number_of_sides,Coordinates,Distance_in_pixels,Slopes,Angles,Names= py.polygon_analysis ( file_name,
                     show_and_save_contour='yes',
                     show_and_save_analysis='yes',
                     show_sides='yes',
                     show_angles='yes',
                     show_slope='yes',
                     show_name='yes',
                     save_data_to_csv='yes',
                     font=cv2.FONT_HERSHEY_PLAIN
                     ) """)

Explanation

Lets understand the in depth working of the module.

This module detects the contour in the image, a contour is like the closed shape path, as soon as program find the contour it saves its coordinates in the variable (sides) and proceeds further if sides pass through the checkpoints which we will discuss later,

then it analyzes the polygon and shows the needful data, the whole program is about how to effectively use these coordinates. because there can be many contours in the image, like if something is written on it then there will be some small contours in the alphabets. so it should be cared of. Let’s tear down the program and understand how it works.

I am adding the full code here, if you want to understand the specific function or specific line then just navigate to the particular line in the explaination
import cv2
import csv
import numpy as np
import matplotlib.pyplot as plt
import image
from PIL import Image
import os
import math
from decimal import Decimal
from scipy import ndimage

def polygon_analysis(file_name,
                     show_and_save_contour='yes',
                     show_and_save_analysis='yes',
                     show_sides='yes',
                     show_angles='yes',
                     show_slope='yes',
                     show_name='yes',
                     save_data_to_csv='yes',
                     font=cv2.FONT_HERSHEY_PLAIN):


    cwd = os.getcwd()
    name_file=os.path.splitext(file_name)[0]
    counter3=0
    limit=3  #detection_limit #3

    
    if ((show_and_save_analysis=='yes') or (show_and_save_contour=='yes')or (save_data_to_csv=='yes')):
        
        path_save_temp=os.path.join(cwd,'Data')
        path_save=os.path.join(path_save_temp,f'{name_file}_analysis')
        
        
        if not os.path.exists(path_save):
            os.makedirs(path_save)

            
    image = Image.open(file_name, 'r')
    image_size = image.size
    width_old = image_size[0]
    height_old = image_size[1]

    bigside=int(max(width_old,height_old)*1.5)
    background = Image.new('RGBA', (bigside, bigside), (255, 255, 255, 255))
    offset = (0,0)
    background.paste(image, offset)
    file_name2=f'{width_old*2}X{height_old*2}_{name_file}.png'
    save_image=os.path.join(cwd,file_name2)
    save_image_in_data=os.path.join(path_save,file_name2)


    
    if ((show_and_save_analysis=='yes') or (show_and_save_contour=='yes') or (save_data_to_csv=='yes')):
        background.save(save_image_in_data)
        img = cv2.imread(save_image_in_data, cv2.IMREAD_GRAYSCALE)
        img1 = cv2.imread(save_image_in_data)
        image = Image.open(save_image_in_data)
        width, height = image.size
        blur = cv2.GaussianBlur(img,(5,5),0)
        img = plt.imread(save_image_in_data)
        plt.imshow(img)
        
    else:
        background.save(save_image)
        img = cv2.imread(save_image, cv2.IMREAD_GRAYSCALE)
        img1 = cv2.imread(save_image)
        image = Image.open(save_image)
        width, height = image.size
        blur = cv2.GaussianBlur(img,(5,5),0)
        img = plt.imread(save_image)
        plt.imshow(img)
    
        

    font_of_name=cv2.FONT_HERSHEY_TRIPLEX
    font_size_name=max(height,width)*0.002
    font=cv2.FONT_HERSHEY_TRIPLEX
    font_size=font_size_name/1.5
    


    colors = 10*['r', 'b', 'y','g','k','c', 'm', 'seagreen','navy','gold','coral', 'violet', 'crimson','skyblue','hotpink','slateblue', 'b', 'y','g','k','r', 'b', 'y','g','k']
    markers = 10*['*', '+', 'o', 'P', 'x','s', 'p', 'h', 'H', '<','>', 'd', 'D', '^', '1']
    shapes= ['Pentagon','Hexagon','Heptagon','Octagon','Nonagon','Decagon','Hendecagon','Dodecagon','Trisdecagon','Tetradecagon','Pentadecagon']

    abc=[]
    sides=[]
    distance=[]
    m=[]
    angles=[]
    slope=[]
    Name=[]




    def error_detection(abc):
        error = []
        for i in range(len(abc)):
            if (i== len(abc)-1):
                error.append(abs((abc[i]-abc[0])/abc[0]))
            else:
                error.append(abs((abc[i]-abc[i+1])/abc[i+1]))
        return (abs(np.mean(error)*100))




    def error_detection_alternate(abc):
        error = []
        for i in range(int(len(abc)/2)):
            alt_error= (abs((abc[i]-abc[i+2])/abc[i+2]))
            error.append(alt_error)
        return (abs(np.mean(error)*100))




    def sides_length_and_slope(sides):
        sides= np.reshape(sides,(len(sides),2))
        x=[]
        y=[]
        m=[]
        deg_tan=[]
        side_len=[]
        
        
        for a,b in sides:
            x.append(a)
            y.append(b)

        for i in range(len(sides)):
            if (i == (len(sides)-1)):
                side_len.append(round((math.sqrt(((x[i]-x[0])**2)+((y[i]-y[0])**2))),2))
                if ((x[0]-x[i])==0):
                    m.append(round(((y[0]-y[i])/1),2))
                else:
                    m.append(round(((y[0]-y[i])/(x[0]-x[i])),2))
            
            else:
                side_len.append(round((math.sqrt(((x[i]-x[i+1])**2)+((y[i]-y[i+1])**2))),2))
                if ((x[i+1]-x[i])==0):
                    m.append(round(((y[i+1]-y[i])/1),2))
                else:
                    m.append(round((((y[i+1]-y[i])/(x[i+1]-x[i]))),2))
        #print(side_len)    
        return side_len,m,x,y




    def allow(sides=sides,width=width,height=height):
        side,_,x,y =  sides_length_and_slope(sides)
        
        for i in range(len(sides)):
            if (side[i]<(width_old*0.05)) or (x[i]<(max(width_old,height_old)*0.010))or (y[i]<(max(width_old,height_old)*0.010))or (x[i]>(max(width_old,height_old)*0.98))or(y[i]>(max(width_old,height_old)*0.98)) :
                
               #height-height*0.02
    ##        if(x[i]==0)or(y[i]==0)or(x[i]>(height-5))or(y[i]>(width-5))or(side[i]<(width/20)):
                flag=0
                break
            else:
                flag=1
        if(flag==1):
            
            return (np.reshape(sides,(len(sides),2)))




    def angle(sides,m):
        
        for i in range(len(sides)):
            if (i == (len(sides)-1)):
                if math.degrees(math.atan(m[0])-math.atan(m[i]))< 0:
                    angles.append(round(math.degrees(math.atan(m[0])-math.atan(m[i]))+180,2))
                    
                else:
                    angles.append(round(math.degrees(math.atan(m[0])-math.atan(m[i])),2))
                    
            else:
                if math.degrees(math.atan(m[i+1])-math.atan(m[i]))< 0:
                    angles.append(round(math.degrees(math.atan(m[i+1])-math.atan(m[i]))+180,2))
                    
                else:
                    angles.append(round(math.degrees(math.atan(m[i+1])-math.atan(m[i])),2))
                
##        print(angles)        
        return angles
     


    def Fiveto15shape(sides):
        
        for i in range(11):
            if len(sides) == i+5:
                
                side,m,_,_= sides_length_and_slope(sides)
                angles =angle(sides,m)
                if (error_detection(angles)<limit):
                    
                    
                    print (f'Regular {shapes[i]}')
                    write_angle_slope_and_sides(sides,side,angles,m)
                    write_name(f'Regular {shapes[i]}')
                    save_to_csv(sides,side,angles,name=f"Regular {shapes[i]}", m=m)
                
                else:
                        
                    print (f'{shapes[i]}')
                    write_angle_slope_and_sides(sides,side,angles,m)
                    write_name(f'{shapes[i]}')
                    save_to_csv(sides,side,angles,name=f'{shapes[i]}',m=m)
                    
                    
                        

    def show_and_save_fig_data(sides,counter3):
        
        for i in range(len(sides)):
            counter2=0
            plt.scatter(np.reshape(sides,(len(sides),2))[i][counter2],np.reshape(sides,(len(sides),2))[i][counter2+1],marker= markers[counter3], c=colors[counter3])
            


    def write_angle_slope_and_sides(sides,side,angles,m,show_angles=show_angles,show_sides=show_sides):
        middle_point_X=[]
        middle_point_Y=[]
        for j in range(len(sides)):
            d=0
            if (j == (len(sides))-1):
                middle_point_X.append(int((((sides[j][d]+sides[0][d])/2))))
                middle_point_Y.append(int(((sides[j][d+1]+sides[0][d+1])/2)))
            else:
                middle_point_X.append(int((((sides[j][d]+sides[j+1][d])/2))))
                middle_point_Y.append(int(((sides[j][d+1]+sides[j+1][d+1])/2)))
    ##    print(middle_point_X)
    ##    print(middle_point_Y)
    ##    print(sides)
            
        if (show_angles=='yes'):
            for j in range(len(sides)):
                c=0
                cv2.putText(img1, f"{angles[j]}", (sides[j], sides[j]), font, font_size, ((183,9,93)))
        if(show_sides=='yes'):
            for j in range(len(sides)):
                c=0
                cv2.putText(img1, f"{side[j]}", (middle_point_X[j], middle_point_Y[j]), font, font_size, ((0,0,255))) #blue green red
        if(show_slope=='yes'):
            for j in range(len(sides)):
                c=0
                cv2.putText(img1, f"{(m[j])}", (middle_point_X[j], int(middle_point_Y[j]+(max(height,width)*0.05))), font, font_size, ((0,255,0))) #blue green red
        


    def save_to_csv(sides,side,angles,name,m):
        slope.append(m)
        distance.append(side)
        Name.append(name[:])
        
        if save_data_to_csv=='yes':
            x= 'csv_data_'+file_name[:(len(file_name)-4)]+'.csv'
            
            save_csv=os.path.join(path_save,f'csv_data_{name_file}.csv')
            with open(save_csv, mode='w') as data_file:
                data_writer = csv.writer(data_file, delimiter=';')
                fieldname=[['x_coordinate','y_coordinate','distance_in_pixels', 'angles', 'name', 'slope']]
                data_writer.writerows(fieldname)
                for i in range(len(side)):
                    c=0
                    data_writer.writerow([sides[i],sides[i],side[i], angles[i], name, m[i]])
            


    def write_name(name):
        if(show_name=='yes'):
            cv2.putText(img1, name, (int(max(height,width)*0.20), int(max(height,width)*0.80)), font_of_name, font_size_name, ((255,0,0))) #blue green red
            if(show_angles=='yes'):
                cv2.putText(img1, '# - Angles', (int(max(height,width)*0.70), int(max(height,width)*0.75)), font_of_name, font_size_name*0.40, ((183,9,93)))
            if(show_sides=='yes'):
                cv2.putText(img1, '# - Distance(in px)', (int(max(height,width)*0.70), int(max(height,width)*0.80)), font_of_name, font_size_name*0.40, ((0,0,255)))
            if(show_slope=='yes'):
                cv2.putText(img1, '# - Slope', (int(max(height,width)*0.70), int(max(height,width)*0.85)), font_of_name, font_size_name*0.40, ((0,255,0)))


               
    _, threshold = cv2.threshold(blur, 240, 255, cv2.THRESH_BINARY)
    _, contours, _ = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    for cnt in contours:
        sides = cv2.approxPolyDP(cnt, 0.01*cv2.arcLength(cnt, True), True)
        cv2.drawContours(img, [sides], 0, (0), 5)
        x = sides.ravel()[0]
        y = sides.ravel()[1]

        if (show_and_save_contour=='yes'):
            counter3+=1
            show_and_save_fig_data(sides,counter3)

    ##    print(sides)
        
        sides=allow(sides)
    ##    print(len(sides))
        if (sides is not None):
    ##        print(sides)

            if len(sides) == 3:
                
                side,m,_,_= sides_length_and_slope(sides)
                angles =angle(sides,m)
                if (error_detection(angles)<limit):
                    if (error_detection(side)<limit): 
                        print ('Eq Tri')
                        write_angle_slope_and_sides(sides,side,angles,m)
                        write_name('Eq Tri')
                        save_to_csv(sides,side,angles,name='Eq Tri', m=m)
                        break

                        
                else:
                    print ('Triangle')
                    write_angle_slope_and_sides(sides,side,angles,m)
                    write_name('Triangle')
                    save_to_csv(sides,side,angles,name='Triangle',m=m)
                    break
                    
               

            if len(sides) == 4:
                side,m,_,_= sides_length_and_slope(sides)
                angles =angle(sides,m)

                if (error_detection(angles)<limit):
                    if (error_detection(side)<limit):
                        print('square')
                        write_angle_slope_and_sides(sides,side,angles,m=m)
                        write_name('square')
                        save_to_csv(sides,side,angles,name='square',m=m)
                        distance.append(side)
                        break
                    elif (error_detection_alternate(side)<limit):
                        print('rectangle')
                        write_angle_slope_and_sides(sides,side,angles,m=m)
                        write_name('rectangle')
                        save_to_csv(sides,side,angles,name='rectangle',m=m)
                        break
                
                elif (error_detection_alternate(angles)<limit):
                    
                    if (error_detection(side)<limit):
                        print('Rhombus')
                        write_angle_slope_and_sides(sides,side,angles,m=m)
                        write_name('Rhombus')
                        save_to_csv(sides,side,angles,name='Rhombus',m=m)
                        break
                        
                    elif (error_detection_alternate(side)<limit):
                        print('Parallelogram')
                        write_angle_slope_and_sides(sides,side,angles,m=m)
                        write_name('Parallelogram')
                        save_to_csv(sides,side,angles,name='Parallelogram',m=m)
                        break
                            
                else:
                    print('Quadrilateral')
                    write_angle_slope_and_sides(sides,side,angles,m=m)
                    write_name('Quadrilateral')
                    save_to_csv(sides,side,angles,name='Quadrilateral',m=m)
                        
                    break

            if(len(sides)>4):
                
                Fiveto15shape(sides)
                
                break
        else:
            pass
  
    if (show_and_save_contour=='yes'):
        save_conotur=os.path.join(path_save,f"contour_{file_name}")
        plt.savefig(save_conotur)
        im= Image.open(save_conotur)
        im.show()
        plt.show()

        
    if (show_and_save_analysis=='yes'):
        save_analysis=os.path.join(path_save,f"analysis_{file_name}")
        cv2.imwrite(save_analysis,img1)
        im= Image.open(save_analysis)
        im.show()
    
    return len(sides),sides,distance,slope,angles,Name[0]    


def help():
    print("""#https://pypi.org/project/py2pyAnalysis/
#https://github.com/Pushkar-Singh-14/Polygon-Analysis
#https://py2py.com/polygon-analysis-overview-and-explanation/

Number_of_sides,Coordinates,Distance_in_pixels,Slopes,Angles,Names= py.polygon_analysis ( file_name,
                     show_and_save_contour='yes',
                     show_and_save_analysis='yes',
                     show_sides='yes',
                     show_angles='yes',
                     show_slope='yes',
                     show_name='yes',
                     save_data_to_csv='yes',
                     font=cv2.FONT_HERSHEY_PLAIN
                     ) """)

  


First of all we are importing the necessary packages

import cv2
import csv
import numpy as np
import matplotlib.pyplot as plt
import image
from PIL import Image
import os
import math

and here is our function

def polygon_analysis(file_name,
                     show_and_save_contour='yes',
                     show_and_save_analysis='yes',
                     show_sides='yes',
                     show_angles='yes',
                     show_slope='yes',
                     show_name='yes',
                     save_data_to_csv='yes',
                     font=cv2.FONT_HERSHEY_PLAIN):

Here we are doing some initial work which we may require in the program later here we are saving the folder path in a variable in which the program is running which helps the program to save the files if allowed.

In the next step we are extracting the file name without extention, For example we have file named square.jpeg then it extracts “square” and then initializing some variables, and limit,which we will discuss
later

    cwd = os.getcwd()
    name_file=os.path.splitext(file_name)[0]
    counter3=0
    limit=3  #detection_limit #3

Our plan is if we want to save any of these data or all of these, then a new separate folder should be created which has named data and within folder and new folder should also be created with the name of ({name of file}_Analysis) in which all the files are stored,

Thus if the program run again to detect another polygon then it will create folder next to the previously created (name of file_analysis) folder, it ensures the accessibility of data and cleanliness at the same time,

    if ((show_and_save_analysis=='yes') or (show_and_save_contour=='yes')or (save_data_to_csv=='yes')):
        
        path_save_temp=os.path.join(cwd,'Data')
        path_save=os.path.join(path_save_temp,f'{name_file}_analysis')
        
        
        if not os.path.exists(path_save):
            os.makedirs(path_save)

Here we are opening the image with an intention to read. We are extracting the width and height of the file. We saved the data in as width_old and height_old.

In the later step we increase the background by pasting the current image on a white square canvas whose side is determined by the max side between width_old and height_old. in the last step a path is saved as a new file with the name of (twice the width x twice the height)_name of file.

    image = Image.open(file_name, 'r')
    image_size = image.size
    width_old = image_size[0]
    height_old = image_size[1]

    bigside=int(max(width_old,height_old)*1.5)
    background = Image.new('RGBA', (bigside, bigside), (255, 255, 255, 255))
    offset = (0,0)
    background.paste(image, offset)
    file_name2=f'{width_old*2}X{height_old*2}_{name_file}.png'
    save_image=os.path.join(cwd,file_name2)

Here if any of these conditions satisfied then we will save that new image in the data folder else we will save it somewhere outside and access from there, then we will read the image in grayscale because color doesnt matter here and monotone makes it fast to process the data.

Here notice that we are opening the image again in another variable, this is because we show have to the contour data on separate file, i.e. one fo the name, sides, angle and slopes, and one for the contour.

Width and height are saved using ‘Image save function’, also Gaussian blur is used, this is because we want to smooth our image a little but just to blend the corner pixels in one another so that some unnecessary contours can be ignored.

    if ((show_and_save_analysis=='yes') or (show_and_save_contour=='yes') or (save_data_to_csv=='yes')):
        background.save(save_image_in_data)
        img = cv2.imread(save_image_in_data, cv2.IMREAD_GRAYSCALE)
        img1 = cv2.imread(save_image_in_data)
        image = Image.open(save_image_in_data)
        width, height = image.size
        blur = cv2.GaussianBlur(img,(5,5),0)
        img = plt.imread(save_image_in_data)
        plt.imshow(img)
        
    else:
        background.save(save_image)
        img = cv2.imread(save_image, cv2.IMREAD_GRAYSCALE)
        img1 = cv2.imread(save_image)
        image = Image.open(save_image)
        width, height = image.size
        blur = cv2.GaussianBlur(img,(5,5),0)
        img = plt.imread(save_image)
        plt.imshow(img)

In this step we are specifying the fonts, font_of_name is the font we used to show the name of the polygon, so, it should be bigger then the normal font, but if we hard-coded our code here then we end up bigger font even on the small resolution image or smaller font on higher resolution image.

So, to prevent this we define the font size as the 0.2% of max between height and width similarly for the normal font we described as the 3/4th of font_size_name. in order to maintain the image size to font size ratio.

 font_of_name=cv2.FONT_HERSHEY_TRIPLEX
    font_size_name=max(height,width)*0.002
    font=cv2.FONT_HERSHEY_TRIPLEX
    font_size=font_size_name/1.5

Here we are initializing the colors, marker, and shapes which we will use later. along with this we also initializing some lists.

colors = 10*['r', 'b', 'y','g','k','c', 'm', 'seagreen','navy','gold','coral', 'violet', 'crimson','skyblue','hotpink','slateblue', 'b', 'y','g','k','r', 'b', 'y','g','k']
markers = 10*['*', '+', 'o', 'P', 'x','s', 'p', 'h', 'H', '<','>', 'd', 'D', '^', '1']
shapes= ['Pentagon','Hexagon','Heptagon','Octagon','Nonagon','Decagon','Hendecagon','Dodecagon','Trisdecagon','Tetradecagon','Pentadecagon']

abc=[]
sides=[]
distance=[]
m=[]
angles=[]
slope=[]
Name=[]

This error_detection() function is used to measure the mean error in a list. like if we take an example of a square then all sides and angles should be equal but this program measures the distance in pixels so there is a possibility that angles will not equal it maybe like 89.5 or 91.0 and same with the sides.

So, to tackle this kind of problem we will use this function. it only returns the mean error value.

def error_detection(abc):
        error = []
        for i in range(len(abc)):
            if (i== len(abc)-1):
                error.append(abs((abc[i]-abc[0])/abc[0]))
            else:
                error.append(abs((abc[i]-abc[i+1])/abc[i+1]))
        return (abs(np.mean(error)*100))

This error_detection_alternate() is the same function as above but it’s for alternate sides like in rectangle. As we know in the parallelogram or rhombus the opposite interior angles are equal. so to check those we will this function.

    def error_detection_alternate(abc):
        error = []
        for i in range(int(len(abc)/2)):
            alt_error= (abs((abc[i]-abc[i+2])/abc[i+2]))
            error.append(alt_error)
        return (abs(np.mean(error)*100))

Here this function sides_length_and_slope() takes the contour coordinates of the polygon as we learn earlier, here length of sides denote the number of sides, like 4 for Rectangle, 5 for Pentagon and so on

So first we reshaped the array in (whatever no of sides X 2), in this method we tried to figure out the distance between two points and the slope of that line.
Note: here side_len is the length of individual sides and m is the slope.

Formula for distance between two points is sqrt((x2-x2)^2 + (y2-y1)^2)

Formula for slope is ((y2-y1)/(x2-x1))

Round function is used to round off the decimal digit up to 2, like 89.7367264774 will become 89.73 and then we store the values in their respective variables.

Note: Since the array is unidirectional but a polygon is cyclic so we have to define that if the value of ‘i’ is at the last point then it must be paired with the first coordinate to make a closed loop

    def sides_length_and_slope(sides):
        sides= np.reshape(sides,(len(sides),2))
        x=[]
        y=[]
        m=[]
        deg_tan=[]
        side_len=[]
        
        
        for a,b in sides:
            x.append(a)
            y.append(b)

        for i in range(len(sides)):
            if (i == (len(sides)-1)):
                side_len.append(round((math.sqrt(((x[i]-x[0])**2)+((y[i]-y[0])**2))),2))
                if ((x[0]-x[i])==0):
                    m.append(round(((y[0]-y[i])/1),2))
                else:
                    m.append(round(((y[0]-y[i])/(x[0]-x[i])),2))
            
            else:
                side_len.append(round((math.sqrt(((x[i]-x[i+1])**2)+((y[i]-y[i+1])**2))),2))
                if ((x[i+1]-x[i])==0):
                    m.append(round(((y[i+1]-y[i])/1),2))
                else:
                    m.append(round((((y[i+1]-y[i])/(x[i+1]-x[i]))),2))
       # print(side_len)    
        return side_len,m,x,y

Next is allow() function, If the image is not clean or if anything is written on it then the program will also treat it as a contour and take it in consideration, also the 4 corners of an image will also contribute in the shape detection so we always get a square in an image, Thus allow function tackle all these problems.

Here we specify few conditions and if the array of contour/corner points passes these condition then only it will allow for the further analysis.

The first condition is to check for any contour due to any alphabets or any such short sides which is not actually the desired polygon, here the sides or the length of the detected closed curve should be greater than the 5% of the width of an image,

Second and third condition checks that square detection problem which I mentioned above, here the x,y should greater than the 1 percent of whatever max between width or height, this eliminate the x,y coordinate which have zero in it.

   def allow(sides=sides,width=width,height=height):
        side,_,x,y =  sides_length_and_slope(sides)
        
        for i in range(len(sides)):
            if (side[i]<(width_old*0.05)) or (x[i]<(max(width_old,height_old)*0.010))or (y[i]<(max(width_old,height_old)*0.010))or (x[i]>(max(width_old,height_old)*0.98))or(y[i]>(max(width_old,height_old)*0.98)) :
                
               #height-height*0.02
    ##        if(x[i]==0)or(y[i]==0)or(x[i]>(height-5))or(y[i]>(width-5))or(side[i]<(width/20)):
                flag=0
                break
            else:
                flag=1
        if(flag==1):
            
            return (np.reshape(sides,(len(sides),2)))

Here is the angle() function which we use to detect and angle.

The formula used here is atan(slope2)-atan(slope1) this gives us a radian of the angle, to convert into degree we use math.degrees() function.

If this gives us a negative value then 180 should be added there in order to make it positive, then all the angles will store in the list named angles.

    def angle(sides,m):
        
        for i in range(len(sides)):
            if (i == (len(sides)-1)):
                if math.degrees(math.atan(m[0])-math.atan(m[i]))< 0:
                    angles.append(round(math.degrees(math.atan(m[0])-math.atan(m[i]))+180,2))
                    
                else:
                    angles.append(round(math.degrees(math.atan(m[0])-math.atan(m[i])),2))
                    
            else:
                if math.degrees(math.atan(m[i+1])-math.atan(m[i]))< 0:
                    angles.append(round(math.degrees(math.atan(m[i+1])-math.atan(m[i]))+180,2))
                    
                else:
                    angles.append(round(math.degrees(math.atan(m[i+1])-math.atan(m[i])),2))
                
##        print(angles)        
        return angles

The polygon with the length 4 has many possibilities, like a square, rectangle, parallelogram, and rhombus. We can say every polygon with 4 sides is a quadrilateral but if we need to specify between these 4 shapes then we have to classify them individually.

Thus after 4th sided polygon, every polygon can be classified by simple conditions, like if error detection ( which we learn above ) for an angle is in the limit (we can specify it, but I choose it as 3 percent) then the shape is said to be a Regular polygon, thus we use this Fiveto15shape() function

Here we are not checking for the sides because if all angle are equal then side will also be equal, it only increases the weight on the code.

Note: Here some functions like write_angle_slope_and_sides, write_name, or save_to_csv are also there, we will learn about them as we proceed further,

    def Fiveto15shape(sides):
        
        for i in range(11):
            if len(sides) == i+5:
                
                side,m,_,_= sides_length_and_slope(sides)
                angles =angle(sides,m)
                if (error_detection(angles)<limit):
                    
                    
                    print (f'Regular {shapes[i]}')
                    write_angle_slope_and_sides(sides,side,angles,m)
                    write_name(f'Regular {shapes[i]}')
                    save_to_csv(sides,side,angles,name=f"Regular {shapes[i]}", m=m)
                
                else:
                        
                    print (f'{shapes[i]}')
                    write_angle_slope_and_sides(sides,side,angles,m)
                    write_name(f'{shapes[i]}')
                    save_to_csv(sides,side,angles,name=f'{shapes[i]}',m=m)

Here show_and_save_fig_data() shows the contour data on the figure, if it selected yes during function calling then whenever a set of a contour is found, it plots all corner points with same marker and color so we can examine visually that where is the possible contour in the image.

def show_and_save_fig_data(sides,counter3):

    for i in range(len(sides)):
        counter2=0
        plt.scatter(np.reshape(sides,(len(sides),2))[i][counter2],np.reshape(sides,(len(sides),2))[i][counter2+1],marker= markers[counter3], c=colors[counter3])

Here write_angle_slope_and_sides() writes the angles, sides, and slope on the image if it is selected yes individually.

Initially, we detected the middle of sides so that we can show sides and slope there, we will do some adjustment with the slope position just to ensure that side and slope data will not overlap.

When show angles, show side and show slope is set to yes respectively at the time of function call, this function writes the required data on the image using the put_text function.

    def write_angle_slope_and_sides(sides,side,angles,m,show_angles=show_angles,show_sides=show_sides):
        middle_point_X=[]
        middle_point_Y=[]
        for j in range(len(sides)):
            d=0
            if (j == (len(sides))-1):
                middle_point_X.append(int((((sides[j][d]+sides[0][d])/2))))
                middle_point_Y.append(int(((sides[j][d+1]+sides[0][d+1])/2)))
            else:
                middle_point_X.append(int((((sides[j][d]+sides[j+1][d])/2))))
                middle_point_Y.append(int(((sides[j][d+1]+sides[j+1][d+1])/2)))
    ##    print(middle_point_X)
    ##    print(middle_point_Y)
    ##    print(sides)
            
        if (show_angles=='yes'):
            for j in range(len(sides)):
                c=0
                cv2.putText(img1, f"{angles[j]}", (sides[j], sides[j]), font, font_size, ((183,9,93)))
        if(show_sides=='yes'):
            for j in range(len(sides)):
                c=0
                cv2.putText(img1, f"{side[j]}", (middle_point_X[j], middle_point_Y[j]), font, font_size, ((0,0,255))) #blue green red
        if(show_slope=='yes'):
            for j in range(len(sides)):
                c=0
                cv2.putText(img1, f"{(m[j])}", (middle_point_X[j], int(middle_point_Y[j]+(max(height,width)*0.05))), font, font_size, ((0,255,0))) #blue green red

when set to yes then save_to_csv() will save all the data to the CSV file which will save in the data>filename folder with other images,

It stores the x,y coordinate of the corner of the polygon, the distance between two points (in pixels), angles, name of the shape detected and the slope.

def save_to_csv(sides,side,angles,name,m):
    slope.append(m)
    distance.append(side)
    Name.append(name[:])

    if save_data_to_csv=='yes':
        x= 'csv_data_'+file_name[:(len(file_name)-4)]+'.csv'

        save_csv=os.path.join(path_save,f'csv_data_{name_file}.csv')
        with open(save_csv, mode='w') as data_file:
            data_writer = csv.writer(data_file, delimiter=';')
            fieldname=[['x_coordinate','y_coordinate','distance_in_pixels', 'angles', 'name', 'slope']]
            data_writer.writerows(fieldname)
            for i in range(len(side)):
                c=0
                data_writer.writerow([sides[i],sides[i],side[i], angles[i], name, m[i]])

We use this write_name() function to write the name of the polygon on the image, here we determine the position, in which x coordinate is 20% of max among height and width, and y coordinate is the 80 percent of max among height and width.

Note: Height and Width should not be confused with height_old and width_old as previous, and also

Note that the font size of name is 40% for sides marker and slope marker. also the position is toggled too. just to maintian the space.

def write_name(name):
    if(show_name=='yes'):
        cv2.putText(img1, name, (int(max(height,width)*0.20), int(max(height,width)*0.80)), font_of_name, font_size_name, ((255,0,0))) #blue green red
        if(show_angles=='yes'):
            cv2.putText(img1, '# - Angles', (int(max(height,width)*0.70), int(max(height,width)*0.75)), font_of_name, font_size_name*0.40, ((183,9,93)))
        if(show_sides=='yes'):
            cv2.putText(img1, '# - Distance(in px)', (int(max(height,width)*0.70), int(max(height,width)*0.80)), font_of_name, font_size_name*0.40, ((0,0,255)))
        if(show_slope=='yes'):
            cv2.putText(img1, '# - Slope', (int(max(height,width)*0.70), int(max(height,width)*0.85)), font_of_name, font_size_name*0.40, ((0,255,0)))

Now from here our main program starts, here we take the file in which we used the Gaussian blur function to smooth the edges and we saved that file in blur variable.

In the next step are using a threshold function on that image, threshold function works here like threshold(img,A,B,operation) if the pixel intensity is greater then A then it is converted to B else zero, here 255 means white and zero means black, also the operation is important, here we use cv2.THRESH_BINARY, if we used THRESH_BINARY_INV then the mechanism become opposite, or inverted.

Next we use findContour function to find obviously the contours, we pass the threshold image and the RETR_TREE is used for hierarcical differentiation of contour. next is cv2.CHAIN_APPROX_SIMPLE, since the contour is the full closed path, but we are only interested in the corners, so this CHAIN_APPROX_SIMPLE method only shows the approx coordinates, i.e extreme ones or corner ones,

_, threshold = cv2.threshold(blur, 240, 255, cv2.THRESH_BINARY)
_, contours, _ = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

Then we extract each contour one by one and change the sides using cv2.approxPolyDP. here note that one sides variable is the list of all the corner points which found in the closed shape,

Here we comment out the function which we can use to draw the contours in the form of sides. We only show the contours in term of the corner point, then we saved the coordinates in x,y coordinate for further use.

for cnt in contours:
    sides = cv2.approxPolyDP
    ##cv2.drawContours(img, [sides], 0, (0), 5)
    x = sides.ravel()[0]
    y = sides.ravel()[1]

Here if show_and_save_contour is set to yes then we pass the sides and a counter which is always initialized as zero.

    if (show_and_save_contour=='yes'):
        counter3+=1
        show_and_save_fig_data(sides,counter3)

the allow() function is acting as a barrier to pass only legitimate contour when there is no contour left in the contours list then allow() function returns None, which then throw an error so to tackle this we use the if statement.

    sides=allow(sides)
##    print(len(sides))
    if (sides is not None):
##        print(sides)

Now here len(sides) means the number of sides, if it is 3, then obviously its a triangle, but if the error detection is within the limit i.e 3% then its called equilateral triangle else just a triangle then we pass the sides angle and slope to the write angle_slope_and_sides() function. and name to the write_name() funtion and same data to the csv file.

            if len(sides) == 3:
                
                side,m,_,_= sides_length_and_slope(sides)
                angles =angle(sides,m)
                if (error_detection(angles)<limit):
                    if (error_detection(side)<limit): 
                        print ('Eq Tri')
                        write_angle_slope_and_sides(sides,side,angles,m)
                        write_name('Eq Tri')
                        save_to_csv(sides,side,angles,name='Eq Tri', m=m)
                        break

                        
                else:
                    print ('Triangle')
                    write_angle_slope_and_sides(sides,side,angles,m)
                    write_name('Triangle')
                    save_to_csv(sides,side,angles,name='Triangle',m=m)
                    break

as the code is simple now if the error in angles is within the limit then it can be a square or rectangle because, in both cases, all angles are 90 deg, so we check if error_detection() function for sides is less then limit, if so then we conclude its a square and break the loop if not then we again check if error_detection_alternate() for sides is within the limit. if so then we conclude it a rectangle else we conclude its a quadrilateral.

Else if the error_detection_alternate() for sides is less than a limit then we have two options because, in both rhombus and parallelogram, alternate angles are equal. so we check if error_detection for sides is less then limit then its rhombus. else if error_detection_alternate() for sides is less then limit then its parallelogram else it’s a quadrilateral

        if len(sides) == 4:
            side,m,_,_= sides_length_and_slope(sides)
            angles =angle(sides,m)

            if (error_detection(angles)<limit):
                if (error_detection(side)<limit):
                    print('square')
                    write_angle_slope_and_sides(sides,side,angles,m=m)
                    write_name('square')
                    save_to_csv(sides,side,angles,name='square',m=m)
                    distance.append(side)
                    break
                elif (error_detection_alternate(side)<limit):
                    print('rectangle')
                    write_angle_slope_and_sides(sides,side,angles,m=m)
                    write_name('rectangle')
                    save_to_csv(sides,side,angles,name='rectangle',m=m)
                    break

            elif (error_detection_alternate(angles)<limit):

                if (error_detection(side)<limit):
                    print('Rhombus')
                    write_angle_slope_and_sides(sides,side,angles,m=m)
                    write_name('Rhombus')
                    save_to_csv(sides,side,angles,name='Rhombus',m=m)
                    break

                elif (error_detection_alternate(side)<limit):
                    print('Parallelogram')
                    write_angle_slope_and_sides(sides,side,angles,m=m)
                    write_name('Parallelogram')
                    save_to_csv(sides,side,angles,name='Parallelogram',m=m)
                    break

            else:
                print('Quadrilateral')
                write_angle_slope_and_sides(sides,side,angles,m=m)
                write_name('Quadrilateral')
                save_to_csv(sides,side,angles,name='Quadrilateral',m=m)

                break

for the shape having sides more then 4, we pass the sides to Fiveto15shape()

            if(len(sides)>4):
                
                Fiveto15shape(sides)
                
                break
        else:
            pass

If show_and_save_contour is set to yes then we save the file at the [name_file]_analysis folder within Data folder and then open that image from there and show it, using Image.

We did exactly the same with show_and_save_analysis. in the end, we return some variable like which might be useful in some other program

if (show_and_save_contour=='yes'):
    save_conotur=os.path.join(path_save,f"contour_{file_name}")
    plt.savefig(save_conotur)
    im= Image.open(save_conotur)
    im.show()
##    plt.show()


if (show_and_save_analysis=='yes'):
    save_analysis=os.path.join(path_save,f"analysis_{file_name}")
    cv2.imwrite(save_analysis,img1)
    im= Image.open(save_analysis)
    im.show()

return len(sides),sides,distance,slope,angles,Name[0] 

Here is the help() function in which we print all the function attributes so that if someone forgot which come after which then they can just type py2pyAnalysis.help() and it will display the following bunch of lines.

def help():
    print("""#https://pypi.org/project/py2pyAnalysis/
#https://github.com/Pushkar-Singh-14/Polygon-Analysis
#http://py2py.com/polygon-analysis-overview-and-explaination/

Number_of_sides,Coordinates,Distance_in_pixels,Slopes,Angles,Names= py.polygon_analysis ( file_name,
                     show_and_save_contour='yes',
                     show_and_save_analysis='yes',
                     show_sides='yes',
                     show_angles='yes',
                     show_slope='yes',
                     show_name='yes',
                     save_data_to_csv='yes',
                     font=cv2.FONT_HERSHEY_PLAIN
                     ) """)

Thats all from my side, please feel free to share your feedback. If you have any doubt then please comment below.

Thanks for reading. 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *