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
Installation
Use the package manager pip to install py2pyAnalysis.
pip install py2pyAnalysis
Samples
- Pentagon
Enlarged image Original image CSV file Analysis Cotour
- Nonagon
Enlarged Image Original image CSV file Analysis Contour
- Irregular Decagon
Enlarged Image Original Image CSV file Analysis Contour
- Regular Decagon
Enlarged Image Original Image CSV data Analysis Contour
- Parallelogram
Enlarged Image Original Image CSV file Analysis Contour
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
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
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
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
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

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
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,
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.
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
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
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,
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
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
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. 🙂