├── CircleD_v1.3.2.PNG ├── Features_MP4_GIF ├── AutoDetect_mode.gif ├── Binary_filter_mode.gif ├── Calibrate_Scale_Bar.gif ├── Export_spreadsheet.gif ├── Histogram_mode.gif ├── Manual_combine_mode.gif └── Upload_select_img.gif ├── How-To-Use_Guide_CircleD.pdf ├── LICENSE.txt ├── README.md ├── black250.png ├── circleD_V.png ├── histo244x183.png └── pyfiles ├── AutoDetectCircle.py ├── ManualDrawCircle.py ├── ManualDrawLine.py ├── ReadMe └── Software_CircleD.py /CircleD_v1.3.2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/CircleD_v1.3.2.PNG -------------------------------------------------------------------------------- /Features_MP4_GIF/AutoDetect_mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/Features_MP4_GIF/AutoDetect_mode.gif -------------------------------------------------------------------------------- /Features_MP4_GIF/Binary_filter_mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/Features_MP4_GIF/Binary_filter_mode.gif -------------------------------------------------------------------------------- /Features_MP4_GIF/Calibrate_Scale_Bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/Features_MP4_GIF/Calibrate_Scale_Bar.gif -------------------------------------------------------------------------------- /Features_MP4_GIF/Export_spreadsheet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/Features_MP4_GIF/Export_spreadsheet.gif -------------------------------------------------------------------------------- /Features_MP4_GIF/Histogram_mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/Features_MP4_GIF/Histogram_mode.gif -------------------------------------------------------------------------------- /Features_MP4_GIF/Manual_combine_mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/Features_MP4_GIF/Manual_combine_mode.gif -------------------------------------------------------------------------------- /Features_MP4_GIF/Upload_select_img.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/Features_MP4_GIF/Upload_select_img.gif -------------------------------------------------------------------------------- /How-To-Use_Guide_CircleD.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/How-To-Use_Guide_CircleD.pdf -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryan Lu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircleD Software 2 | (A Windows executable version of the software for those who do not know how to read/write Python code is available at my Dropbox link below. All you have to do is double click on the Main_v1.3.2.exe file and the software will run. No installation required!) 3 | *************** 4 | https://www.dropbox.com/s/9lchhq72om481zd/Software_CircleD_v1.3.2.zip?dl=1 5 | 6 | (~85mb file) 7 | The software may take up to 30 seconds to run after double-clicking on the Main_v1.3.2.exe file. 8 | 9 | *************** 10 | Please read the How-To-Use-Guide PDF file to get started. 11 | 12 | ![Main image of software](CircleD_v1.3.2.PNG) 13 | 14 | ........................ 15 | 16 | All the features of the software are demo'ed below: 17 | ........................ 18 | 19 | # Upload and select images 20 | 21 | ![Upload and select images](Features_MP4_GIF/Upload_select_img.gif) 22 | 23 | # Binary filter mode 24 | 25 | ![Binary filter mode](Features_MP4_GIF/Binary_filter_mode.gif) 26 | 27 | # Calibrating scale bar 28 | 29 | ![Calibrating scale bar](Features_MP4_GIF/Calibrate_Scale_Bar.gif) 30 | 31 | # Auto-Detection mode 32 | 33 | ![Auto-Detection mode](Features_MP4_GIF/AutoDetect_mode.gif) 34 | 35 | # Manual-Detection mode 36 | 37 | ![Manual-Detection mode](Features_MP4_GIF/Manual_combine_mode.gif) 38 | 39 | # Display results on histogram 40 | 41 | ![Display results on histogram](Features_MP4_GIF/Histogram_mode.gif) 42 | 43 | # Exporting data to CSV/spreadsheet file 44 | 45 | ![Exporting data to CSV/spreadsheet file](Features_MP4_GIF/Export_spreadsheet.gif) 46 | -------------------------------------------------------------------------------- /black250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/black250.png -------------------------------------------------------------------------------- /circleD_V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/circleD_V.png -------------------------------------------------------------------------------- /histo244x183.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rylu12/CircleD/d275d7804acd460f4ad13b9ee9342976df900fee/histo244x183.png -------------------------------------------------------------------------------- /pyfiles/AutoDetectCircle.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import pandas as pd 5 | import openpyxl 6 | 7 | #Get pixel/distance (using ImageJ software) to output actual diameters of circles 8 | 9 | dp = 1 10 | accum_ratio = 1 11 | min_dist = 5 12 | p1 = 40 13 | p2 = 30 14 | minDiam = 1 15 | maxDiam = 30 16 | scalebar = 10 17 | min_range = 0 18 | max_range = 100 19 | intervals = 10 20 | rad_list =[] 21 | detected_circles = [] 22 | dataForTable = {} 23 | 24 | def clear_plt(): 25 | plt.clf() 26 | 27 | def autoDetect(resized_img, accum_ratio, min_dist, p1, p2, minDiam, maxDiam, pixel_distance): 28 | global result, img, table_data, rad_list, detected_circles 29 | 30 | # Convert to grayscale. 31 | img = resized_img 32 | img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 33 | gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) 34 | 35 | # Blur using 3 * 3 kernel. 36 | gray_blurred = cv2.blur(gray, (3, 3)) 37 | 38 | 39 | minDist = int(min_dist*pixel_distance) 40 | minRadius = int(minDiam*pixel_distance/2) 41 | maxRadius = int(maxDiam*pixel_distance/2) 42 | 43 | if minDist < 1: 44 | minDist = 1 45 | if minRadius <1: 46 | minRadius =1 47 | if minRadius <1: 48 | minRadius =1 49 | 50 | # Apply Hough transform on the blurred image. 51 | detected_circles = cv2.HoughCircles(gray_blurred, 52 | cv2.HOUGH_GRADIENT, dp = int(accum_ratio), minDist = minDist, 53 | param1 = int(p1), param2 = int(p2), minRadius = minRadius, maxRadius = maxRadius) 54 | 55 | def autoDetectBin(resized_img, threshold,accum_ratio, min_dist, p1, p2, minDiam, maxDiam, pixel_distance): 56 | global result, img, table_data, rad_list, detected_circles 57 | 58 | img = resized_img 59 | img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 60 | gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) 61 | thres,binImg = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY) 62 | # Blur using 3 * 3 kernel. 63 | blurred = cv2.blur(binImg, (3, 3)) 64 | 65 | minDist = int(min_dist*pixel_distance) 66 | minRadius = int(minDiam*pixel_distance/2) 67 | maxRadius = int(maxDiam*pixel_distance/2) 68 | 69 | if minDist < 1: 70 | minDist = 1 71 | if minRadius <1: 72 | minRadius =1 73 | if minRadius <1: 74 | minRadius =1 75 | 76 | # Apply Hough transform on the blurred image. 77 | detected_circles = cv2.HoughCircles(blurred, 78 | cv2.HOUGH_GRADIENT, dp = int(accum_ratio), minDist = minDist, 79 | param1 = int(p1), param2 = int(p2), minRadius = minRadius, maxRadius = maxRadius) 80 | 81 | 82 | def processCircles(state, resized_img, filename, pixel_distance, manual_list): 83 | global detected_circles, rad_list, img, result, bottom_10percentile, top_90percentile, new_name 84 | # Draw circles that are detected. 85 | 86 | img = resized_img 87 | img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 88 | rad_list=[] 89 | 90 | if state == False: 91 | detected_circles = None 92 | 93 | result = '\n\n' 94 | try: 95 | if (detected_circles is None) and (len(manual_list) == 0): 96 | return '\nNo circles found!\n' 97 | 98 | elif len(manual_list) > 0 and (detected_circles is None): 99 | manual_list.sort() 100 | bottom_10percentile = int(len(manual_list)*0.1) 101 | top_90percentile = int(len(manual_list)*0.9) 102 | result += '# of circles found: ' + str(len(manual_list)) 103 | rad_list = manual_list 104 | 105 | else: 106 | 107 | # Convert the circle parameters a, b and r to integers. 108 | detected_circles = np.uint16(np.around(detected_circles)) 109 | 110 | for pt in detected_circles[0, :]: 111 | a, b, r = pt[0], pt[1], pt[2] 112 | 113 | # Draw the circumference of the circle. 114 | cv2.circle(img, (a, b), r, (0, 255, 0), 2) 115 | 116 | # Draw a small circle (of radius 1) to show the center. 117 | cv2.circle(img, (a, b), 1, (0, 0, 255), 2) 118 | 119 | 120 | new_name = filename[:-4] + '_detected' + filename[-4:] 121 | cv2.imwrite(new_name, img) 122 | 123 | #Loop to convert radius (pixel) values to diameter 124 | for x in range(detected_circles.shape[1]): 125 | diam = detected_circles[0,x,2]*2/pixel_distance 126 | rad_list.append(round(diam,1)) 127 | 128 | rad_list.sort() 129 | 130 | bottom_10percentile = int(len(rad_list)*0.1) 131 | top_90percentile = int(len(rad_list)*0.9) 132 | 133 | result += '# of circles found: ' + str(detected_circles.shape[1]) 134 | 135 | result +='\nAvg diam. = ' + "%.1f"%np.average(rad_list) + 'um' 136 | result +='\nD10 = '+ str(rad_list[bottom_10percentile])+'um'+'\nD50 = ' + "%.1f"%np.median(rad_list) + "um" 137 | result +='\nD90 = '+ str(rad_list[top_90percentile])+'um' 138 | 139 | except IndexError: 140 | pass 141 | return result 142 | 143 | def tableData(): 144 | global rad_list, row_list, dataForTable, col_list, bottom_10percentile, top_90percentile, detected_circles, dataForTable 145 | 146 | col_list = [] 147 | row_list = [] 148 | Diam_um = 'Diameter (um)' 149 | temp_2 = ' ' 150 | temp_3 = ' ' 151 | temp_4 = ' ' 152 | temp_5 = ' ' 153 | 154 | if len(rad_list)>0: 155 | for items in range(len(rad_list)): 156 | col_list.append(dict(Diam_um = rad_list[items])) 157 | 158 | for rows in range(len(rad_list)): 159 | row_list.append('rec'+ str(rows+1)) 160 | 161 | dataForTable = dict(zip(row_list,col_list)) 162 | 163 | try: 164 | if len(dataForTable) < 2: 165 | temp_1 = dataForTable['rec1']['Diam_um'] 166 | 167 | elif len(dataForTable) < 3: 168 | temp_1 = dataForTable['rec1']['Diam_um'] 169 | temp_2 = dataForTable['rec2']['Diam_um'] 170 | 171 | elif len(dataForTable) < 4: 172 | temp_1 = dataForTable['rec1']['Diam_um'] 173 | temp_2 = dataForTable['rec2']['Diam_um'] 174 | temp_3 = dataForTable['rec3']['Diam_um'] 175 | 176 | elif len(dataForTable) < 5: 177 | temp_1 = dataForTable['rec1']['Diam_um'] 178 | temp_2 = dataForTable['rec2']['Diam_um'] 179 | temp_3 = dataForTable['rec3']['Diam_um'] 180 | temp_4 = dataForTable['rec4']['Diam_um'] 181 | 182 | elif len(dataForTable) >= 5: 183 | temp_1 = dataForTable['rec1']['Diam_um'] 184 | temp_2 = dataForTable['rec2']['Diam_um'] 185 | temp_3 = dataForTable['rec3']['Diam_um'] 186 | temp_4 = dataForTable['rec4']['Diam_um'] 187 | temp_5 = dataForTable['rec5']['Diam_um'] 188 | 189 | dataForTable.update({'rec1':{'Diam_um': str(temp_1) , 'Col2': '# of Circles', 'Col3': str(len(rad_list))}, 190 | 'rec2':{'Diam_um': str(temp_2),'Col2': 'Avg Diam (um)', 'Col3': "%.1f"%np.average(rad_list)}, 191 | 'rec3':{'Diam_um': str(temp_3) ,'Col2': 'D10 (um)', 'Col3': str(rad_list[bottom_10percentile])}, 192 | 'rec4':{'Diam_um': str(temp_4),'Col2': 'D50 (um)', 'Col3': "%.1f"%np.median(rad_list)}, 193 | 'rec5':{'Diam_um': str(temp_5) ,'Col2': 'D90 (um)', 'Col3': str(rad_list[top_90percentile])} 194 | }) 195 | 196 | except KeyError: 197 | pass 198 | 199 | return dataForTable 200 | 201 | 202 | def histoPlot(filename, min_range, max_range, intervals): 203 | global rad_list 204 | #Plot histogram 205 | plt.xlabel('Diameter (um)') 206 | plt.ylabel('Frequency') 207 | plt.title('Particle Size Distribution') 208 | (n, bins, patch) = plt.hist([rad_list], bins=np.arange(min_range,max_range+1,intervals), rwidth=0.9) 209 | plt.xticks(np.arange(min_range,max_range,intervals)) 210 | # plt.gca().grid(which='major', axis='y') 211 | plt.savefig((filename[:-4] + '_histogram.png'), dpi = 500) 212 | plt.clf() 213 | 214 | 215 | 216 | # pd.DataFrame(rad_list).to_excel('emulsions_D50_list_1.xlsx',header=False, index=False) 217 | 218 | -------------------------------------------------------------------------------- /pyfiles/ManualDrawCircle.py: -------------------------------------------------------------------------------- 1 | # import the necessary packages 2 | import cv2 3 | import math 4 | import numpy as np 5 | 6 | # now let's initialize the list of reference point 7 | def initialize(): 8 | global m_x, m_y, b, image_prev, image_diam 9 | m_x = -1 10 | m_y = -1 11 | b = 0 12 | image_prev = [] 13 | image_diam = [0] 14 | 15 | def draw_circle(event, x, y, flags, param): 16 | # grab references to the global variables 17 | global m_x, m_y, image_prev, prev_img, currDiam, image, ratio, b 18 | 19 | # if the left mouse button was clicked, record the starting 20 | # (x, y) coordinates and indicate that cropping is being performed 21 | 22 | if event == cv2.EVENT_LBUTTONDOWN: 23 | m_x, m_y = x, y 24 | 25 | # check to see if the left mouse button was released 26 | elif event == cv2.EVENT_LBUTTONUP: 27 | # record the ending (x, y) coordinates and indicate that 28 | # the cropping operation is finished 29 | 30 | # draw a circle around the object (pt1 to pt2 is the diameter) 31 | c_x = int((m_x+x)/2) 32 | c_y = int((m_y+y)/2) 33 | rad = int(math.sqrt(((m_x-c_x)**2)+((m_y-c_y)**2))) 34 | 35 | #Copies the previous image before the last drawing, allows to delete later 36 | try: 37 | image_prev.append(image.copy()) 38 | cv2.circle(image, (c_x,c_y), rad , (0, 255, 0), 2) 39 | 40 | prev_img = np.array(image_prev) 41 | 42 | b = prev_img.shape[0] 43 | image_diam.append(np.round((rad*2/ratio),1)) 44 | except MemoryError: 45 | cv2.destroyAllWindows() 46 | 47 | def load_img(resized_cv2, pixel_dist): 48 | global image, image_prev, prev_img, ratio 49 | # load the image, clone it, and setup the mouse callback function 50 | image = resized_cv2 51 | image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 52 | ratio = pixel_dist 53 | 54 | cv2.imshow('Manual Draw Mode', image) 55 | cv2.setMouseCallback('Manual Draw Mode', draw_circle) 56 | 57 | def diamCircles(state): 58 | global image_diam, image_diam, image_prev, b 59 | 60 | if state == False: 61 | image_diam.pop() 62 | image_prev.pop() 63 | b = b-1 64 | return image_diam 65 | elif b == (len(image_diam)-1): 66 | return image_diam 67 | 68 | 69 | -------------------------------------------------------------------------------- /pyfiles/ManualDrawLine.py: -------------------------------------------------------------------------------- 1 | # import the necessary packages 2 | import cv2 3 | import math 4 | import numpy as np 5 | 6 | # now let's initialize the list of reference point 7 | 8 | x_down =-1 9 | y_down =-1 10 | x_up =-1 11 | y_up = -1 12 | cropped_prev = [] 13 | b=0 14 | cropped_line = [b] 15 | 16 | def draw_line(event, x, y, flags, param): 17 | 18 | global x_down, y_down, x_up, y_up, cropped_prev, b, prev_cropped, cropped_line, factor, cropped 19 | 20 | # if the left mouse button was clicked, record the starting 21 | # (x, y) coordinates and indicate that cropping is being performed 22 | 23 | if event == cv2.EVENT_LBUTTONDOWN: 24 | x_down, y_down = x, y 25 | 26 | # check to see if the left mouse button was released 27 | elif event == cv2.EVENT_LBUTTONUP: 28 | x_up = x 29 | y_up = y 30 | try: 31 | cropped_prev.append(cropped.copy()) 32 | cv2.line(cropped, (x_up, y_down), (x_down,y_down), (0,255, 0), 2) 33 | length_line = (x_up-x_down) 34 | cropped_line.append(length_line) 35 | prev_cropped = np.array(cropped_prev) 36 | b = prev_cropped.shape[0] 37 | except ValueError as e: 38 | return 39 | 40 | 41 | # load the image, clone it, and setup the mouse callback function 42 | def load_img(resized_cv2, c_corner): 43 | global cropped, factor, cropped_line 44 | image = resized_cv2 45 | image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 46 | orig = image.copy() 47 | 48 | height, width, rgb_value = image.shape 49 | 50 | #image = cv2.resize(image, (700, int(height/(width/700))), interpolation = cv2.INTER_AREA) 51 | 52 | #height, width, rgb_value = image.shape 53 | if c_corner == 'br': 54 | cropped = image[int(height*0.7):height, int(width*0.5):width] 55 | elif c_corner == 'bl': 56 | cropped = image[int(height*0.7):height, 0:int(width*0.4)] 57 | elif c_corner == 'tr': 58 | cropped = image[0:int(height*0.3), int(width*0.5):width] 59 | elif c_corner == 'tl': 60 | cropped = image[0:int(height*0.3), 0:int(width*0.4)] 61 | 62 | 63 | factor = 2 64 | resize_width = (width - int(width*0.5))*factor 65 | resize_height = (height - int(height*0.7))*factor 66 | 67 | # cv2.namedWindow("Cropped to measure scale-bar", cv2.WINDOW_NORMAL) 68 | cv2.resizeWindow("Cropped to measure scale-bar", int(resize_width), int(resize_height)) 69 | cv2.imshow("Cropped to measure scale-bar", cropped) 70 | cv2.setMouseCallback("Cropped to measure scale-bar", draw_line) 71 | 72 | # keep looping until the 'q' key is pressed to escape/end program 73 | 74 | 75 | def drawLine(): 76 | global b, cropped_line 77 | if b !=0: 78 | return cropped_line[b] 79 | else: 80 | return 1 81 | 82 | -------------------------------------------------------------------------------- /pyfiles/ReadMe: -------------------------------------------------------------------------------- 1 | The pyfiles folder contains the raw python code used in the software. 2 | 3 | "Software_CircleD.py" is the main driver of the software and where the GUI is maintained. 4 | -------------------------------------------------------------------------------- /pyfiles/Software_CircleD.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from PIL import ImageTk, Image 3 | import tkinter.scrolledtext as tkst 4 | from tkinter import filedialog 5 | import AutoDetectCircle as adc 6 | import cv2 7 | import matplotlib.pyplot as plt 8 | import tkintertable as tkt 9 | import numpy as np 10 | import ManualDrawLine as mdl 11 | import ManualDrawCircle as mdc 12 | 13 | #Initialize GUI with root and set window dimensions 14 | root = tk.Tk() 15 | root.title("CirlceD - The Circle Detection Program") 16 | root.geometry('1055x625') 17 | root.tk.call('tk', 'scaling', 1.2) 18 | 19 | 20 | #Initialize parameters 21 | reset = False 22 | calibrated = False 23 | 24 | #Initialize tkintertable dictionary set up 25 | table_Data = {'rec1': {'Col1': 'Diam.', 'Col2': 'values', 'Col3': 'will'}, 'rec2':{'Col1': 'go', 'Col2': 'here', 'Col3': ' '}, 26 | 'rec3': {'Col1': ' ', 'Col2': ' ', 'Col3': ' '}, 'rec4':{'Col1': ' ', 'Col2': ' ', 'Col3': ' '}, 27 | 'rec5':{'Col1': ' ', 'Col2': ' ', 'Col3': ' '}, 'rec6': {'Col1': ' ', 'Col2': ' ', 'Col3': ' '}} 28 | 29 | #Initialize output display 30 | output = 'Output results will display below...\n----------------------\n' 31 | start_state = False 32 | 33 | 34 | #Initialize window logo and logo frame 35 | logo_img = ImageTk.PhotoImage(Image.open('circleD_V.png')) 36 | logo_show = tk.Label(root, image=logo_img) 37 | logo_show.grid(row =0, column = 0, rowspan = 15) 38 | 39 | 40 | #Initialize intro text frame 41 | frame_intro = tk.LabelFrame(root, width=80) 42 | frame_intro.grid(row=0, column=1, rowspan=15, columnspan=5, pady=10, sticky = 'w') 43 | 44 | intro_text = tkst.ScrolledText(frame_intro, wrap=tk.WORD, width=60, height=13, undo=False) 45 | intro_text['font'] = ('consolas', '10') 46 | intro_text.insert(tk.INSERT, 47 | "Welcome to the Spherical Detection program!\n\nThis software program uses the 'Circle Hough Transform' (CHT) feature extraction technique to detect circles in images. " 48 | "Circle candidates are produced by 'voting' in the Hough parameter space and selecting the local maxima in the accumulator matrix. " 49 | "Hence, not all circles may be detected and the parameters have to be optimized to prevent over-detection/under-detection of circles." 50 | "\n-----------------------------------------------" 51 | "\nManual circle detection is also an option. Simply 'click and drag' from one side of a circle to the other side in the pop-up image window." 52 | "\n\nReadings:\nhttps://en.wikipedia.org/wiki/Circle_Hough_Transform\n\nDescription of Hough Circle parameters:\nhttps://docs.opencv.org/3.4/d3/de5/tutorial_js_houghcircles.html") 53 | 54 | intro_text.pack(expand=True, fill='both') 55 | 56 | 57 | #Initialize all other frames in GUI 58 | frame_output = tk.LabelFrame(root, bg='BLACK') 59 | frame_output.grid(row=0, column=8, rowspan=55, columnspan=2, padx=10, pady=10, sticky='n') 60 | 61 | output_text = tkst.ScrolledText(frame_output, wrap=tk.WORD, width=30, height=27, undo=False) 62 | output_text['font'] = ('consolas', '11') 63 | output_text.insert(tk.INSERT, str(output)) 64 | output_text.pack(expand=True, anchor='n') 65 | 66 | frame_rdbutton = tk.LabelFrame(root, width=70, padx=10, pady=5) 67 | frame_rdbutton.grid(row=15, column=0, rowspan=10, columnspan=5, padx=10, pady=5, sticky='w') 68 | frame_start = tk.LabelFrame(root, width=70, padx=10, pady=5) 69 | frame_start.grid(row=15, column=5, rowspan=10, padx=0, pady=5) 70 | frame_upload = tk.LabelFrame(root, width=70, padx=10, pady=5) 71 | frame_upload.grid(row=25, rowspan=10, column=5, columnspan=1, padx=0, pady=5, sticky = 's') 72 | 73 | frame_preview = tk.LabelFrame(root) 74 | frame_preview.grid(row=35, column=4, rowspan=35, columnspan=2, padx=5, pady=5, sticky = 'nw') 75 | 76 | frame_binary = tk.LabelFrame(root, text = "Binary Filter Mode") 77 | frame_binary.grid(row=56, column=8, rowspan = 13, columnspan=3, padx=10, pady=5, sticky = 'w') 78 | 79 | frame_histo_show = tk.LabelFrame(root) 80 | frame_histo_show.grid(row=40, column = 0, rowspan = 32, columnspan = 4, padx=10, pady=3, sticky = 'nw') 81 | 82 | frame_table = tk.LabelFrame(root) 83 | frame_table.grid(row = 0, column = 10, rowspan = 45, columnspan = 2, padx=5, pady=9, sticky = 'n') 84 | 85 | 86 | table_label = tk.Label(root, text ='To Export Table Above:\n------\nRight-Click -> File -> Export CSV', font=("Helvetica", 9)) 87 | table_label.grid(row = 56, rowspan = 10, column = 10, columnspan = 2) 88 | 89 | histo_img = Image.open('histo244x183.png') 90 | smaller_histo_img = ImageTk.PhotoImage(histo_img) 91 | new_img_histo = tk.Label(frame_histo_show,image = smaller_histo_img) 92 | new_img_histo.pack() 93 | 94 | 95 | frame_HoughCircle = tk.LabelFrame(root, text='Hough Circle Parameters', width=70, padx=10, pady=5) 96 | frame_HoughCircle.grid(row=25, column=0, rowspan = 10, columnspan=5, padx=10, pady=5, sticky='w') 97 | 98 | param_minDist = tk.Entry(frame_HoughCircle, width=4) 99 | param_minDist.insert(0, str(adc.min_dist)) 100 | param_minDist.grid(row=0, column=1, padx=1, pady=1) 101 | label_minDist = tk.Label(frame_HoughCircle, text='Min Distance (um):') 102 | label_minDist.grid(row=0, column=0, padx=1, pady=1) 103 | 104 | param_dp = tk.Entry(frame_HoughCircle, width=4) 105 | param_dp.insert(0, str(adc.accum_ratio)) 106 | param_dp.grid(row=1, column=1, padx=1, pady=1) 107 | label_dp = tk.Label(frame_HoughCircle, text='Accum/Res Ratio (dp):') 108 | label_dp.grid(row=1, column=0, padx=1, pady=1) 109 | 110 | param_minDiam = tk.Entry(frame_HoughCircle, width=4) 111 | param_minDiam.insert(0, str(adc.minDiam)) 112 | param_minDiam.grid(row=0, column=3, padx=1, pady=1) 113 | label_minDiam = tk.Label(frame_HoughCircle, text=' Min Diam (um):') 114 | label_minDiam.grid(row=0, column=2, padx=1, pady=1) 115 | 116 | param_maxDiam = tk.Entry(frame_HoughCircle, width=4) 117 | param_maxDiam.insert(0, str(adc.maxDiam)) 118 | param_maxDiam.grid(row=1, column=3, padx=1, pady=1) 119 | label_maxDiam = tk.Label(frame_HoughCircle, text=' Max Diam (um):') 120 | label_maxDiam.grid(row=1, column=2, padx=1, pady=1) 121 | 122 | param_p1 = tk.Entry(frame_HoughCircle, width=4) 123 | param_p1.insert(0, str(adc.p1)) 124 | param_p1.grid(row=0, column=5, padx=1, pady=1) 125 | label_p1 = tk.Label(frame_HoughCircle, text=' Param1:') 126 | label_p1.grid(row=0, column=4, padx=1, pady=1) 127 | 128 | param_p2 = tk.Entry(frame_HoughCircle, width=4) 129 | param_p2.insert(0, str(adc.p2)) 130 | param_p2.grid(row=1, column=5, padx=1, pady=1) 131 | label_p2 = tk.Label(frame_HoughCircle, text=' Param2:') 132 | label_p2.grid(row=1, column=4, padx=1, pady=1) 133 | 134 | 135 | frame_histo_param = tk.LabelFrame(root, text='Histogram Parameters', padx=10, pady=5) 136 | frame_histo_param.grid(row=35, column=0, rowspan = 5, columnspan=4, padx=10, pady=5, sticky = 'w') 137 | 138 | interval_bins = tk.Entry(frame_histo_param, width=4) 139 | interval_bins.insert(0, str(adc.intervals)) 140 | interval_bins.grid(row=0, column=1, padx=1) 141 | label_bins = tk.Label(frame_histo_param, text='Intervals') 142 | label_bins.grid(row=0, column=0, padx=1) 143 | 144 | minRange = tk.Entry(frame_histo_param, width=4) 145 | minRange.insert(0, str(adc.min_range)) 146 | minRange.grid(row=0, column=3, padx=1) 147 | label_minRange = tk.Label(frame_histo_param, text='Min\nRange ') 148 | label_minRange.grid(row=0, column=2, padx=1) 149 | 150 | maxRange = tk.Entry(frame_histo_param, width=4) 151 | maxRange.insert(0, str(adc.max_range)) 152 | maxRange.grid(row=0, column=5, padx=1) 153 | label_maxRange = tk.Label(frame_histo_param, text='Max\nRange ') 154 | label_maxRange.grid(row=0, column=4, padx=1) 155 | 156 | 157 | frame_scalebar = tk.LabelFrame(root, text = "Calibrates Pixel/Distance Ratio", padx=5, pady=5) 158 | frame_scalebar.grid(row=44, column=8, rowspan = 12, columnspan=3, padx=10, pady=5, sticky='nw') 159 | 160 | scalebar = tk.Entry(frame_scalebar, width=7) 161 | scalebar.insert(0, str(adc.scalebar)) 162 | scalebar.grid(row=2, column=4, columnspan = 2, padx=1, pady=1, sticky = 'w') 163 | label_scalebar = tk.Label(frame_scalebar, text='Known distance of scale-bar (um):') 164 | label_scalebar.grid(row=2, column=0, columnspan = 4, padx=1, pady=1) 165 | 166 | pixel_dist = tk.Entry(frame_scalebar, width=6, state = 'readonly') 167 | pixel_dist.grid(row=3, column=1, padx=1, pady=1, sticky = 'w') 168 | label_pixel_dist = tk.Label(frame_scalebar, text='Pixel/Distance Ratio:') 169 | label_pixel_dist.grid(row=3, column=0, padx=1, pady=1, sticky = 'w') 170 | 171 | frame_sbLocation = tk.LabelFrame(root, text ='Scale-Bar Location?', padx = 5, pady = 5) 172 | frame_sbLocation.grid(row = 44, column = 11, rowspan = 13, padx = 5, pady=7, sticky = 'nw') 173 | 174 | #Initialize radiobutton functions for scale calibration 175 | sb_text = tk.StringVar() 176 | sb_location = 'br' 177 | sb_text.set('br') 178 | 179 | tk.Radiobutton(frame_sbLocation, text="Top-Left", 180 | variable=sb_text, value='tl', command=lambda: sbLocation_clicked('tl')).grid(row = 0, column = 0, sticky = 'w') 181 | tk.Radiobutton(frame_sbLocation, text="Top-Right", 182 | variable=sb_text, value='tr', command=lambda: sbLocation_clicked('tr')).grid(row = 0, column = 1, sticky = 'w') 183 | tk.Radiobutton(frame_sbLocation, text="Bottom-Left", 184 | variable=sb_text, value='bl', command=lambda: sbLocation_clicked('bl')).grid(row = 1, column = 0, sticky = 'w') 185 | tk.Radiobutton(frame_sbLocation, text="Bottom-Right", 186 | variable=sb_text, value='br', command=lambda: sbLocation_clicked('br')).grid(row = 1, column = 1, sticky = 'w') 187 | 188 | 189 | def sbLocation_clicked(value): 190 | global sb_location 191 | if value == 'tl': 192 | sb_location = value 193 | elif value == 'tr': 194 | sb_location = value 195 | elif value == 'bl': 196 | sb_location = value 197 | elif value == 'br': 198 | sb_location = value 199 | 200 | #Initialize image display as default black image 201 | black_img = Image.open('black250.png') 202 | black_img = black_img.resize((250,250)) 203 | temp_img = ImageTk.PhotoImage(black_img) 204 | placeholder_img = tk.Label(frame_preview, image=temp_img).pack() 205 | 206 | 207 | table = tkt.TableCanvas(frame_table, data = table_Data, 208 | cellwidth=30, cellbackgr='white', 209 | thefont=('Arial',10), width = 182, height = 384, 210 | rowselectedcolor='#f8eba2') 211 | table.show() 212 | 213 | yesNoState = tk.StringVar() 214 | yesNoState.set('NO') 215 | binState = 'NO' 216 | filename_copy = '' 217 | 218 | 219 | def open_file(): 220 | """ 221 | Function to upload and open image files 222 | """ 223 | 224 | global open_img, window_img, placeholder_img, show_img, img_width, img_height, filename 225 | global max_wh, adj_height, binary_state, resized_img_cv2, resize_img, calibrated, filename_copy 226 | 227 | black_img = Image.open('black250.png') 228 | temp_img = ImageTk.PhotoImage(black_img) 229 | placeholder_img = tk.Label(frame_preview, image=temp_img, bg ='black').place(x=0, y=0) 230 | 231 | filename = filedialog.askopenfilename(initialdir='C:\\', title="Select a file", 232 | filetypes=( 233 | ("jpg Files", "*jpg"), ("png Files", "*png"), ("tif Files", "*tif"))) 234 | 235 | if (filename != (filename_copy[:-4] + '_detected' + filename_copy[-4:])): 236 | calibrated = False 237 | else: 238 | calibrated = True 239 | 240 | resize = False 241 | if filename: 242 | try: 243 | open_img = Image.open(filename).convert("RGB") 244 | except IOError: 245 | return 246 | 247 | img_width, img_height = open_img.size 248 | 249 | max_wh = max(img_width, img_height) 250 | 251 | resize_copy = max_wh/800 252 | resize_img = open_img.resize((int(img_width/resize_copy), int(img_height/resize_copy))) 253 | resized_img_cv2 = np.asarray(resize_img) 254 | 255 | factor = max_wh/250 256 | 257 | open_img = open_img.resize((int(img_width / factor), int(img_height / factor))) 258 | 259 | adj_height = (250 - (img_height / factor)) / 2 260 | 261 | window_img = ImageTk.PhotoImage(open_img) 262 | show_img = tk.Label(frame_preview, bg ="black", image=window_img).place(x=0, y=adj_height) 263 | binary_state.config(state = 'normal') 264 | 265 | 266 | def start_state(): 267 | """ 268 | Function to start auto detection or manual detection mode after uploading file 269 | """ 270 | 271 | global filename, temp_img, detected_img, output, smaller_histo_img, ratio, reset, binState, pixel_dist 272 | global new_img_histo, img_width, img_height, bin_img, table_Data, table, binary_thresholdBar 273 | global resized_img_cv2, max_wh, calibrated, output_text, filename_copy 274 | 275 | try: 276 | open_img_again = Image.open(filename).convert("RGB") 277 | max_wh = max(img_width, img_height) 278 | resize_copy = max_wh/800 279 | resize_img = open_img_again.resize((int(img_width/resize_copy), int(img_height/resize_copy))) 280 | resized_img_cv2 = np.asarray(resize_img) 281 | 282 | except (NameError, AttributeError) as e: 283 | output_text.insert(tk.INSERT, '\n\nERROR...Please upload an image before running!\n\n') 284 | 285 | filename = None 286 | 287 | if filename != None: 288 | 289 | filename_copy = filename 290 | 291 | if calibrated != True: 292 | output_text.insert(tk.INSERT, '\n\nERROR...Please calibrate image before running!\n\n') 293 | return 294 | if filename: 295 | pixel_dist.configure(state='normal') 296 | pixel_dist.delete(0, 'end') 297 | input_scale = float(str(scalebar.get())) 298 | 299 | ratio = np.round(abs(mdl.drawLine()/input_scale),1) 300 | pixel_dist.insert(0, str(ratio)) 301 | pixel_dist.configure(state='readonly') 302 | 303 | try: 304 | if auto_manual == 'auto': 305 | 306 | mdc.image_diam = [0,0] 307 | 308 | if binState == 'NO': 309 | adc.autoDetect(resized_img_cv2, int(param_dp.get()), int(param_minDist.get()), int(param_p1.get()), 310 | int(param_p2.get()), int(param_minDiam.get()), int(param_maxDiam.get()), ratio) 311 | 312 | elif binState == 'YES': 313 | adc.autoDetectBin(resized_img_cv2, int(binary_thresholdBar.get()),int(param_dp.get()), int(param_minDist.get()), int(param_p1.get()), 314 | int(param_p2.get()), int(param_minDiam.get()), int(param_maxDiam.get()), ratio) 315 | 316 | output = adc.processCircles(True, resized_img_cv2, filename, ratio, mdc.image_diam) 317 | 318 | 319 | if adc.detected_circles is None: 320 | output_text.insert(tk.INSERT, '\n\nNo circles found!\n\n') 321 | return 322 | else: 323 | 324 | manualDetect() 325 | if reset == True or len(mdc.image_diam) == 1: 326 | reset = False 327 | return 328 | 329 | mdc.image_diam.pop(0) 330 | 331 | for items in range(len(mdc.image_diam)): 332 | adc.rad_list.append(mdc.image_diam[items]) 333 | 334 | if "detected" in filename: 335 | response = tk.messagebox.askyesno(title = 'Combine Manual Detection Data' , message = 'Do you wish to append the previous diameter values?') 336 | if response == 1: 337 | total_list = adc.rad_list 338 | output = adc.processCircles(False, resized_img_cv2, filename, ratio, total_list) 339 | else: 340 | output = adc.processCircles(False, resized_img_cv2, filename, ratio, mdc.image_diam) 341 | else: 342 | output = adc.processCircles(False, resized_img_cv2, filename, ratio, mdc.image_diam) 343 | 344 | output_text.insert(tk.INSERT, str(output) + '\n\n') 345 | output_text.yview_moveto(1) 346 | output_text.update() 347 | 348 | try: 349 | maxRange.delete(0, 'end') 350 | maxRange.insert(0, str(adc.max_range)) 351 | if int(maxRange.get()) < int(np.max(adc.rad_list)): 352 | num = int(np.max(adc.rad_list)%int(interval_bins.get())) 353 | addtoMaxRange = int(interval_bins.get()) - num 354 | new_maxRange = int(np.max(adc.rad_list)) + addtoMaxRange 355 | else: 356 | new_maxRange = int(maxRange.get()) 357 | except ValueError1: 358 | output_text.insert(tk.INSERT, '\nERROR...Type: ValueError1!\n') 359 | return 360 | 361 | 362 | maxRange.delete(0, 'end') 363 | maxRange.insert(0, str(new_maxRange)) 364 | 365 | adc.histoPlot(filename, int(minRange.get()), new_maxRange, int(interval_bins.get())) 366 | 367 | histo_img = Image.open(filename[:-4] + '_histogram.png') 368 | width_histo, height_histo = histo_img.size 369 | 370 | factor_histo = width_histo/244 371 | histo_temp_img = histo_img.resize((int(width_histo/factor_histo), int(height_histo/factor_histo))) 372 | 373 | smaller_histo_img = ImageTk.PhotoImage(histo_temp_img) 374 | new_img_histo = tk.Label(frame_histo_show, image = smaller_histo_img).place(x=0,y=0) 375 | 376 | 377 | table_Data = adc.tableData() 378 | table = tkt.TableCanvas(frame_table, data = table_Data, 379 | cellwidth=30, cellbackgr='white', 380 | thefont=('Arial',10), width = 182, height = 384, 381 | rowselectedcolor='#f8eba2') 382 | #If fresh install tkintertable, go to Tables.py and use only celltxt=str(celltxt), remove if statement 383 | table.show() 384 | 385 | mdc.image_diam = [0] 386 | mdc.image_prev = [] 387 | 388 | if auto_manual == 'auto': 389 | 390 | cv2.imshow("Detected Circles", adc.img) 391 | cv2.waitKey(1) 392 | if cv2.getWindowProperty('Detected Circles',1) == -1 : 393 | cv2.destroyAllWindows() 394 | else: 395 | return 396 | #except NameError: 397 | #output_text.insert(tk.INSERT, '\nERROR...Type: NameError2!\n') 398 | # return 399 | except AttributeError: 400 | output_text.insert(tk.INSERT, '\nERROR...Type: AttributeError2!\n') 401 | output_text.yview_moveto(1) 402 | output_text.update() 403 | 404 | 405 | 406 | def calibrateScaleBar(): 407 | 408 | """ 409 | Function to calibrate the scale bar in images 410 | """ 411 | global filename, ratio, sb_location, resized_img_cv2, calibrated 412 | 413 | try: 414 | filename 415 | except NameError: 416 | output_text.insert(tk.INSERT, '\n\nERROR...Please upload an image first!\n\n') 417 | return 418 | 419 | if filename: 420 | 421 | mdl.load_img(resized_img_cv2, sb_location) 422 | 423 | while True: 424 | cv2.imshow("Cropped to measure scale-bar", mdl.cropped) 425 | 426 | key = cv2.waitKey(1) & 0xFF 427 | 428 | if key == ord("d"): 429 | mdl.b = mdl.b-1 430 | mdl.cropped = mdl.prev_cropped[mdl.b] 431 | mdl.cropped_prev.pop 432 | 433 | if cv2.getWindowProperty('Cropped to measure scale-bar',1) == -1 : 434 | cv2.destroyAllWindows() 435 | break 436 | 437 | pixel_dist.configure(state='normal') 438 | pixel_dist.delete(0, 'end') 439 | input_scale = float(str(scalebar.get())) 440 | 441 | ratio = np.round(abs(mdl.drawLine()/input_scale),1) 442 | 443 | pixel_dist.insert(0, str(ratio)) 444 | pixel_dist.configure(state='readonly') 445 | 446 | if abs(mdl.drawLine()) > 1: 447 | calibrated = True 448 | 449 | 450 | def manualDetect(): 451 | 452 | """ 453 | Function for manual detection mode 454 | """ 455 | 456 | global filename, ratio, reset, resized_img_cv2 457 | 458 | try: 459 | filename 460 | except NameError: 461 | output_text.insert(tk.INSERT, '\nERROR...Type: NameError4!\n') 462 | filename = None 463 | 464 | if filename != None: 465 | 466 | if filename: 467 | try: 468 | mdc.load_img(resized_img_cv2, ratio) 469 | except NameError: 470 | output_text.insert(tk.INSERT, '\nERROR...Type: NameError5!\n') 471 | reset = True 472 | return 473 | 474 | last_value = 1 475 | mdc.initialize() 476 | 477 | while True: 478 | cv2.imshow("Manual Draw Mode", mdc.image) 479 | 480 | if len(mdc.diamCircles(True)) != last_value : 481 | 482 | if len(mdc.image_diam)>1: 483 | 484 | currVal = mdc.image_diam[len(mdc.image_diam)-1] 485 | 486 | output_text.yview_moveto(1) 487 | output_text.insert(tk.INSERT, '\n'+str(currVal)) 488 | output_text.update() 489 | 490 | last_value = len(mdc.diamCircles(True)) 491 | 492 | key = cv2.waitKey(1) & 0xFF 493 | 494 | if key == ord("d") or key == ord("D"): 495 | try: 496 | output_text.yview_moveto(1) 497 | output_text.delete('end -1 lines', 'end') 498 | output_text.update() 499 | last_value = len(mdc.diamCircles(False)) 500 | mdc.image = mdc.prev_img[mdc.b] 501 | 502 | except IndexError: 503 | output_text.insert(tk.INSERT, '\nERROR...Type: IndexError1!\n') 504 | cv2.destroyAllWindows() 505 | break 506 | 507 | elif cv2.getWindowProperty('Manual Draw Mode',1) == -1 : 508 | break 509 | 510 | new_name = filename[:-4] + '_detected' + filename[-4:] 511 | cv2.imwrite(new_name,mdc.image) 512 | cv2.destroyAllWindows() 513 | 514 | 515 | def rd_button_clicked(value): 516 | 517 | """ 518 | Function to activate auto/manual detection mode 519 | """ 520 | 521 | global auto_manual 522 | if value == 'auto': 523 | auto_manual = value 524 | else: 525 | auto_manual = value 526 | 527 | def turn_binary(state): 528 | 529 | """ 530 | Function to convert image to binary image (black and white only) 531 | """ 532 | 533 | global open_img, window_img, adj_height, binImg, binary_state, binState 534 | try: 535 | open_img 536 | except NameError: 537 | open_img = None 538 | if open_img != None: 539 | 540 | window_img = ImageTk.PhotoImage(open_img) 541 | show_img = tk.Label(frame_preview, bg ="black", image=window_img).place(x=0, y=adj_height) 542 | binState = yesNoState.get() 543 | state = binState 544 | thresholdBar = int(binary_thresholdBar.get()) 545 | 546 | if state == 'YES': 547 | img = np.asarray(open_img) 548 | img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 549 | thres,binImg = cv2.threshold(img, thresholdBar, 255, cv2.THRESH_BINARY) 550 | new_img = Image.fromarray(binImg) 551 | window_img = ImageTk.PhotoImage(new_img) 552 | show_img = tk.Label(frame_preview, bg ="black", image=window_img).place(x=0, y=adj_height) 553 | return 'YES' 554 | 555 | elif state == 'NO': 556 | return 'NO' 557 | 558 | 559 | #Binary image threshold bar on GUI 560 | binary_thresholdBar = tk.Scale(frame_binary, from_ = 1, to = 254, orient = 'horizontal', length = 120, command = turn_binary) 561 | binary_thresholdBar.grid(row=0, column=4, columnspan = 3, padx=1, pady=1, sticky = 'w') 562 | binary_thresholdBar.set(127) 563 | 564 | binary_state = tk.Checkbutton(frame_binary, text='TURN ON', variable = yesNoState, onvalue='YES', offvalue='NO', state = 'disabled', command=lambda: turn_binary(yesNoState)) 565 | binary_state.grid(row=1, column=4, columnspan = 3, padx=1, pady=1) 566 | 567 | detect_method = tk.StringVar() 568 | auto_manual = 'auto' 569 | detect_method.set('auto detect') 570 | 571 | #Auto/Manual detect GUI button 572 | tk.Radiobutton(frame_rdbutton, text="MANUAL DETECT - Manually draw circles to get diameter values", 573 | variable=detect_method, value='manual detect', command=lambda: rd_button_clicked('manual')).pack(anchor='w') 574 | tk.Radiobutton(frame_rdbutton, text="AUTO DETECT - Uses 'Circle Hough Transform' to detect circles", 575 | variable=detect_method, value='auto detect', command=lambda: rd_button_clicked('auto')).pack(anchor='w') 576 | 577 | calibrate_button = tk.Button(frame_scalebar, text='Calibrate', relief='raised', borderwidth = 2, command=calibrateScaleBar) 578 | calibrate_button.config(height=1, width=9, font=('Helvetica', '8')) 579 | calibrate_button.grid(row = 3, column = 2, columnspan = 4, padx=1, pady=2, sticky ='e') 580 | 581 | img_button = tk.Button(frame_upload, text='CLICK\nto upload an image', relief='raised', command=open_file) 582 | img_button.config(height=2, width=15, font=('Helvetica', '10')) 583 | img_button.pack(fill='both') 584 | 585 | start_button = tk.Button(frame_start, text='START\nAuto/Manual Mode', relief='raised', command=start_state) 586 | start_button.config(height=2, width=15, font=('Helvetica', '10')) 587 | start_button.pack(fill='both') 588 | 589 | credit = tk.Label(root, text='R.Lu (v1.3.2), 2020', font='consolas 8 bold') 590 | credit.grid(row=69, column=11, padx = 5,sticky = 'ne') 591 | 592 | 593 | #Pop up window when GUI X-button is pressed 594 | def closing(): 595 | if tk.messagebox.askokcancel("Exit Program", "Do you wish to quit the program?"): 596 | cv2.destroyAllWindows() 597 | root.quit() 598 | 599 | root.protocol("WM_DELETE_WINDOW", closing) 600 | root.mainloop() 601 | 602 | 603 | --------------------------------------------------------------------------------