From d9ce1d09e5d0bfe785744cf17b30442e9e4eccca Mon Sep 17 00:00:00 2001 From: Rahul Rajendra Pai Date: Sun, 4 Jun 2023 14:27:10 +0200 Subject: [PATCH 1/5] Adding icons --- assets/bbox.png | Bin 0 -> 6840 bytes assets/polygon.png | Bin 0 -> 14639 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/bbox.png create mode 100644 assets/polygon.png diff --git a/assets/bbox.png b/assets/bbox.png new file mode 100644 index 0000000000000000000000000000000000000000..ad692bbf53c557ba282ac08ab8628ace484d3744 GIT binary patch literal 6840 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 G^Zx}=&6KYI literal 0 HcmV?d00001 diff --git a/assets/polygon.png b/assets/polygon.png new file mode 100644 index 0000000000000000000000000000000000000000..75f5943a67a8414760ee71d45022458b33c329df GIT binary patch literal 14639 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 Date: Sun, 4 Jun 2023 14:28:04 +0200 Subject: [PATCH 2/5] Updated GUI --- PixelSAM.py | 33 +++++++++++++++-- utils/predict.py | 96 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/PixelSAM.py b/PixelSAM.py index d337313..f293c64 100644 --- a/PixelSAM.py +++ b/PixelSAM.py @@ -21,9 +21,27 @@ def __init__(self, container): super().__init__(container) # self['text'] = 'Options' + # The left frame containing buttons + self.left_tab = ttk.Frame(self) + # Load button - self.load_btn = tk.Button(self, text="Load Dataset", font='sans 10 bold', height=2, width=12, background="#343434", foreground="white", command=self.load_data) - self.load_btn.pack(side=tk.LEFT, padx=(30,30), pady=20, anchor="n") + self.load_btn = tk.Button(self.left_tab, text="Load Dataset", font='sans 10 bold', height=2, width=12, background="#343434", foreground="white", command=self.load_data) + self.load_btn.pack(side=tk.TOP, padx=(30,30), pady=20, anchor="n") + + self.boundingbox_var = tk.IntVar() + self.boundingbox_logo = ImageTk.PhotoImage(Image.open(os.path.join(".","assets","bbox.png")).resize((40,40), Image.Resampling.LANCZOS)) + self.boundingbox_selector = tk.Checkbutton(self.left_tab, image=self.boundingbox_logo, variable=self.boundingbox_var, font='sans 10 bold', indicatoron=False, text="Show Bbox", width=100, height = 60, compound="top", selectcolor="#34b233", command=self.statechange_callback) + self.boundingbox_selector.pack(side=tk.BOTTOM, padx=(30,30), pady=20, anchor="n") + + # Checkbox for selecting between outer edged and all points + # The coco based annotations have only outer edge marked + # To make it compatible this is being added here. + self.checkbox_var = tk.IntVar() + self.polygon_logo = ImageTk.PhotoImage(Image.open(os.path.join(".","assets","polygon.png")).resize((40,40), Image.Resampling.LANCZOS)) + self.checkbox = tk.Checkbutton(self.left_tab, image=self.polygon_logo, variable=self.checkbox_var, font='sans 10 bold', indicatoron=False, text="Outer Edge", width=100, height = 60, compound="top", selectcolor="#34b233", command=self.statechange_callback) + self.checkbox.pack(side=tk.BOTTOM, padx=(30,30), pady=20, anchor="n") + + self.left_tab.pack(side=tk.LEFT, padx=5, pady=5, anchor="n") # The frame which includes the image player self.imageplayer = ttk.Frame(self) @@ -90,6 +108,7 @@ def __init__(self, container): self.cur_image_index = 0 # The index of the current image self.image_list = [] # The list of images to be displayed self.window_height = 0 # The height of the app window + self.state_changed = False # The state of the bbox and outer edge buttons # Path to the intro image self.cur_image_path = os.path.join(".","assets","intro.png") @@ -143,7 +162,8 @@ def frame_update(self): # Get the height of the app window self.window_height = app.winfo_height()-20 # Display the image - if self.cur_image_path != self.prev_image_path or len(self.cur_annotation) != self.annotation_count or self.window_height != self.prev_window_height or self.mask_count != len(self.mask_images): + # If a new image is loaded or there is a new annotation or the window is resized, or there is a change in the state of the buttons, then update the image + if self.cur_image_path != self.prev_image_path or len(self.cur_annotation) != self.annotation_count or self.window_height != self.prev_window_height or self.state_changed: # Read the image and convert it to RGB self.OCV_image = cv2.imread(self.cur_image_path) self.cv2image = cv2.cvtColor(self.OCV_image, cv2.COLOR_BGR2RGB) @@ -160,13 +180,14 @@ def frame_update(self): self.annotation_count = len(self.cur_annotation) self.prev_window_height = self.window_height self.mask_count = len(self.mask_images) + self.state_changed = False # Get the image dimensions self.img_height, self.img_width, _ = self.OCV_image.shape # Draw the annotations if len(self.cur_annotation) > 0: - self.cv2image, self.mask_image, self.bbox_corners = SAM_prediction(self.cv2image, self.cur_annotation, self.predictor, self.img_height, self.img_width,self.mask_images) + self.cv2image, self.mask_image, self.bbox_corners = SAM_prediction(self.cv2image, self.cur_annotation, self.predictor, self.img_height, self.img_width, self.mask_images, self.checkbox_var.get(), self.boundingbox_var.get()) #get SAM polygons @@ -309,6 +330,10 @@ def save_annotation(self, event): messagebox.showwarning("Warning","Please select an object from the list before saving") else: messagebox.showwarning("Warning","Please label the image before saving") + + # When there is a state change + def statechange_callback(self, event=None): + self.state_changed = True # App class diff --git a/utils/predict.py b/utils/predict.py index 691978a..3c682fc 100644 --- a/utils/predict.py +++ b/utils/predict.py @@ -5,7 +5,46 @@ import numpy as np from segment_anything import sam_model_registry, SamPredictor +# Function to convert the mask to a polygon +def mask_to_polygon(mask_image, input_point): + # Add padding to the mask_image to capture the outer edge + padded_mask_image = cv2.copyMakeBorder(mask_image, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=0) + # Get the contours of the mask + contours, _ = cv2.findContours(cv2.Canny(padded_mask_image, 100, 200), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS) + + # Set the best contour and polygon to be empty + best_contour = [] + best_polygon = [] + + # Find the contours which include the any of the input_points + for contour in contours: + + # Check if the number of points in the contour is less than 10: Skip the contour + if len(contour) < 10: + continue + + # Subtract the padding from the contour + contour = np.subtract(contour,(1,1)) + + # Approximate the contour to a polygon + epsilon = 0.001 * cv2.arcLength(contour,True) + approx_contour = cv2.approxPolyDP(contour, epsilon, True) + + # Check if it is a closed contour + if not np.array_equal(approx_contour[0], approx_contour[-1]): + approx_contour = np.vstack((approx_contour, approx_contour[0][np.newaxis, :])) + + best_contour.append(approx_contour) + + # To avoid the polygon having negative coordinates + polygon = [0 if i < 0 else i for i in approx_contour.flatten().tolist()] + best_polygon.append(polygon) + + return best_contour, best_polygon + + +# Function to setup the SAM model def SAM_setup(model_type, model_path, device_id): sam = sam_model_registry[model_type](checkpoint=model_path) if device_id != "cpu": @@ -14,7 +53,9 @@ def SAM_setup(model_type, model_path, device_id): print("Warning: Running on CPU. This will be slow.") return SamPredictor(sam) -def SAM_prediction(image, points, predictor, img_height, img_width, mask_array=[]): + +# Function to predict the mask +def SAM_prediction(image, points, predictor, img_height, img_width, mask_array=[], outer_edge=0, bounding_box=0): # The points are in the format [x, y, color, label] input_point = np.array([[p[0], p[1]] for p in points]) input_label = np.array([p[3] for p in points]) @@ -45,20 +86,50 @@ def SAM_prediction(image, points, predictor, img_height, img_width, mask_array=[ # Morphological operations on the mask to be saved mask_save_image = cv2.morphologyEx(mask_save_image, cv2.MORPH_OPEN, kernel) - - # Get the edges of the mask - edges = cv2.Canny(mask_image[:, :, 0], 100, 200) - - # Overlay the mask on the image - image = cv2.addWeighted(mask_image, 0.3, image, 0.7, 0) + + # Add the previous masks to the image if len(mask_array) > 0: for m in mask_array: image = cv2.addWeighted(m, 0.3, image, 1, 0) - # Plot the edges on the image - gy, gx = np.where(edges != 0) - for i in range(len(gx)): - image = cv2.circle(image, (gx[i], gy[i]), int((img_height+img_width)/400), (0, 0, 255),-1) + # Check if the outer edge is to be detected + if outer_edge==1: + + # Create a copy of the image + overlay = image.copy() + + # Get the best contours in the mask and its corresponding polygon + best_contour, best_polygon = mask_to_polygon(mask_image, input_point) + + # Overlay the controur + cv2.drawContours(overlay, best_contour, -1, (0, 0, 255), thickness=cv2.FILLED) + + # Add the overlay to the image + image = cv2.addWeighted(overlay, 0.5, image, 0.5, 0) + + # Marking the bounding box for each of the contours + if bounding_box==1: + for cnt_cur in best_contour: + x, y, w, h = cv2.boundingRect(cnt_cur) + image = cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), int((img_height+img_width)/400)) + + gx, gy = [], [] + # Extract x and y coordinates into separate arrays + if len(best_polygon) > 0: + gx = np.array(np.hstack(best_polygon)).reshape(-1, 2)[:, 0] + gy = np.array(np.hstack(best_polygon)).reshape(-1, 2)[:, 1] + + else: + # Overlay the mask on the image + image = cv2.addWeighted(mask_image, 0.3, image, 0.7, 0) + + # Get the edges of the mask + edges = cv2.Canny(mask_image[:, :, 0], 100, 200) + + # Plot the edges on the image + gy, gx = np.where(edges != 0) + for i in range(len(gx)): + image = cv2.circle(image, (gx[i], gy[i]), int((img_height+img_width)/400), (0, 0, 255),-1) # Find the bounding box for the object if len(gx) > 0: @@ -73,5 +144,8 @@ def SAM_prediction(image, points, predictor, img_height, img_width, mask_array=[ else: bbox_corners = [0, 0, 0, 0] + # If bounding_box is 1, plot the bounding box for the object + if bounding_box==1: + image = cv2.rectangle(image, (int((bbox_corners[0] - bbox_corners[2]/2) * img_width), int((bbox_corners[1] - bbox_corners[3]/2) * img_height)), (int((bbox_corners[0] + bbox_corners[2]/2) * img_width), int((bbox_corners[1] + bbox_corners[3]/2) * img_height)), (255, 0, 0), int((img_height+img_width)/400)) return image, mask_save_image, bbox_corners From fbccf30659c81461b1d20ac4efe24ff043106601 Mon Sep 17 00:00:00 2001 From: Rahul Rajendra Pai Date: Sun, 4 Jun 2023 14:31:53 +0200 Subject: [PATCH 3/5] Updated logo credits --- PixelSAM.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PixelSAM.py b/PixelSAM.py index f293c64..39c5e43 100644 --- a/PixelSAM.py +++ b/PixelSAM.py @@ -29,7 +29,7 @@ def __init__(self, container): self.load_btn.pack(side=tk.TOP, padx=(30,30), pady=20, anchor="n") self.boundingbox_var = tk.IntVar() - self.boundingbox_logo = ImageTk.PhotoImage(Image.open(os.path.join(".","assets","bbox.png")).resize((40,40), Image.Resampling.LANCZOS)) + self.boundingbox_logo = ImageTk.PhotoImage(Image.open(os.path.join(".","assets","bbox.png")).resize((40,40), Image.Resampling.LANCZOS)) # https://www.flaticon.com/free-icon/square_7559227 self.boundingbox_selector = tk.Checkbutton(self.left_tab, image=self.boundingbox_logo, variable=self.boundingbox_var, font='sans 10 bold', indicatoron=False, text="Show Bbox", width=100, height = 60, compound="top", selectcolor="#34b233", command=self.statechange_callback) self.boundingbox_selector.pack(side=tk.BOTTOM, padx=(30,30), pady=20, anchor="n") @@ -37,7 +37,7 @@ def __init__(self, container): # The coco based annotations have only outer edge marked # To make it compatible this is being added here. self.checkbox_var = tk.IntVar() - self.polygon_logo = ImageTk.PhotoImage(Image.open(os.path.join(".","assets","polygon.png")).resize((40,40), Image.Resampling.LANCZOS)) + self.polygon_logo = ImageTk.PhotoImage(Image.open(os.path.join(".","assets","polygon.png")).resize((40,40), Image.Resampling.LANCZOS)) # https://www.flaticon.com/free-icon/polygon_9726538 self.checkbox = tk.Checkbutton(self.left_tab, image=self.polygon_logo, variable=self.checkbox_var, font='sans 10 bold', indicatoron=False, text="Outer Edge", width=100, height = 60, compound="top", selectcolor="#34b233", command=self.statechange_callback) self.checkbox.pack(side=tk.BOTTOM, padx=(30,30), pady=20, anchor="n") From c68ee2d206ac81f7777909dd334fbc54ba0c9179 Mon Sep 17 00:00:00 2001 From: Rahul Rajendra Pai Date: Sun, 16 Jul 2023 04:30:00 +0200 Subject: [PATCH 4/5] Updated mask to polygon Added filtering of contours. --- utils/predict.py | 71 ++++++++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/utils/predict.py b/utils/predict.py index 3c682fc..865dd42 100644 --- a/utils/predict.py +++ b/utils/predict.py @@ -5,41 +5,60 @@ import numpy as np from segment_anything import sam_model_registry, SamPredictor + +# Function to filter the contour by area +def filter_contour_by_area(contour, min_area, max_area): + area = cv2.contourArea(contour) + if area < min_area or area > max_area: + return False + return True + +# Function to approximate the polygon +def approx_contour(contour, percentage, epsilon_step=0.005): + if percentage < 0 or percentage >= 1: + raise ValueError("Percentage must be in the range [0, 1).") + + target_points = max(int(contour.shape[0] * (1 - percentage)), 3) + + epsilon = 0 + while True: + epsilon += epsilon_step + approximated_contour = cv2.approxPolyDP(contour, epsilon, closed=True) + if approximated_contour.shape[0] <= target_points: + break + + return approximated_contour + # Function to convert the mask to a polygon -def mask_to_polygon(mask_image, input_point): +def mask_to_polygon(mask_image, approximation_percentage = 0.75): + # Add padding to the mask_image to capture the outer edge padded_mask_image = cv2.copyMakeBorder(mask_image, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=0) # Get the contours of the mask contours, _ = cv2.findContours(cv2.Canny(padded_mask_image, 100, 200), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS) - # Set the best contour and polygon to be empty - best_contour = [] - best_polygon = [] + # Remove the contours which have less than 10 points + contours = [contour for contour in contours if contour.shape[0] >= 10] - # Find the contours which include the any of the input_points - for contour in contours: + # Remove the previously added padding from the contours + contours = [np.subtract(contour,(1,1)) for contour in contours] - # Check if the number of points in the contour is less than 10: Skip the contour - if len(contour) < 10: - continue + # Filter the contours by area + height, width = mask_image.shape[-2:] + contours = [contour for contour in contours if filter_contour_by_area( + contour=contour, + min_area=0.005*height*width, + max_area=1*height*width)] - # Subtract the padding from the contour - contour = np.subtract(contour,(1,1)) - - # Approximate the contour to a polygon - epsilon = 0.001 * cv2.arcLength(contour,True) - approx_contour = cv2.approxPolyDP(contour, epsilon, True) - - # Check if it is a closed contour - if not np.array_equal(approx_contour[0], approx_contour[-1]): - approx_contour = np.vstack((approx_contour, approx_contour[0][np.newaxis, :])) - - best_contour.append(approx_contour) + # Reduce the complexity of contours + best_contour = [approx_contour(contour=contour, percentage=0.75) for contour in contours] + + # Ensure that the contour is closed + best_contour = [np.vstack((contour, contour[0][np.newaxis, :])) for contour in best_contour if not np.array_equal(contour[0], contour[-1])] - # To avoid the polygon having negative coordinates - polygon = [0 if i < 0 else i for i in approx_contour.flatten().tolist()] - best_polygon.append(polygon) + # Convert the contours to polygons + best_polygon = [contour.flatten().tolist() for contour in best_contour] return best_contour, best_polygon @@ -99,7 +118,7 @@ def SAM_prediction(image, points, predictor, img_height, img_width, mask_array=[ overlay = image.copy() # Get the best contours in the mask and its corresponding polygon - best_contour, best_polygon = mask_to_polygon(mask_image, input_point) + best_contour, best_polygon = mask_to_polygon(mask_image) # Overlay the controur cv2.drawContours(overlay, best_contour, -1, (0, 0, 255), thickness=cv2.FILLED) @@ -118,7 +137,7 @@ def SAM_prediction(image, points, predictor, img_height, img_width, mask_array=[ if len(best_polygon) > 0: gx = np.array(np.hstack(best_polygon)).reshape(-1, 2)[:, 0] gy = np.array(np.hstack(best_polygon)).reshape(-1, 2)[:, 1] - + else: # Overlay the mask on the image image = cv2.addWeighted(mask_image, 0.3, image, 0.7, 0) From a4e57be331b6a7426e7f3959af93a1ab6b70bddd Mon Sep 17 00:00:00 2001 From: Rahul Rajendra Pai Date: Sun, 16 Jul 2023 06:08:17 +0200 Subject: [PATCH 5/5] BBox bug fix --- PixelSAM.py | 8 +++++--- utils/predict.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/PixelSAM.py b/PixelSAM.py index 39c5e43..3ff1a7a 100644 --- a/PixelSAM.py +++ b/PixelSAM.py @@ -284,7 +284,8 @@ def new_object(self, event=None): if len(self.object_list.curselection())>0: self.cur_annotation = [] self.mask_images.append(self.mask_image) - self.bbox_list.append([self.object_list.curselection()[0], *self.bbox_corners]) + if len(self.bbox_corners) > 0: + self.bbox_list.append([self.object_list.curselection()[0], *self.bbox_corners]) self.object_list.selection_clear(0, tk.END) print("Previous masks:",len(self.mask_images)) else: @@ -314,14 +315,15 @@ def save_annotation(self, event): if len(self.cur_annotation) > 0: self.cur_annotation = [] self.mask_images.append(self.mask_image) - self.bbox_list.append([self.object_list.curselection()[0], *self.bbox_corners]) + if len(self.bbox_corners) > 0: + self.bbox_list.append([self.object_list.curselection()[0], *self.bbox_corners]) self.object_list.selection_clear(0, tk.END) # Save the bbox list to the file with open(os.path.join(self.file_path,os.path.basename(self.cur_image_path).split(".")[0]+".txt"), "w") as f: for bbox in self.bbox_list: f.write(str(bbox[0])+" "+str(bbox[1])+" "+str(bbox[2])+" "+str(bbox[3])+" "+str(bbox[4])+"\n") # If there is only one object labelled - elif len(self.cur_annotation) > 0: + elif len(self.cur_annotation) > 0 and len(self.bbox_corners)>0: if len(self.object_list.curselection())>0: # Save the bbox list to the file with open(os.path.join(self.file_path,os.path.basename(self.cur_image_path).split(".")[0]+".txt"), "w") as f: diff --git a/utils/predict.py b/utils/predict.py index 865dd42..f9d09c8 100644 --- a/utils/predict.py +++ b/utils/predict.py @@ -5,6 +5,28 @@ import numpy as np from segment_anything import sam_model_registry, SamPredictor +# BBox to Yolo format +def bbox_to_yolo(bbox, image_width, image_height): + # Get the center of the bbox + center_x = (bbox[0] + bbox[2]) / 2 + center_y = (bbox[1] + bbox[3]) / 2 + + # Get the width and height of the bbox + width = bbox[2] - bbox[0] + height = bbox[3] - bbox[1] + + # Convert the bbox to Yolo format + yolo_bbox = [ + center_x / image_width, + center_y / image_height, + width / image_width, + height / image_height, + ] + + # Round the values to 6 decimal places + yolo_bbox = [round(value, 6) for value in yolo_bbox] + + return yolo_bbox # Function to filter the contour by area def filter_contour_by_area(contour, min_area, max_area): @@ -150,18 +172,11 @@ def SAM_prediction(image, points, predictor, img_height, img_width, mask_array=[ for i in range(len(gx)): image = cv2.circle(image, (gx[i], gy[i]), int((img_height+img_width)/400), (0, 0, 255),-1) + bbox_corners = [] # Find the bounding box for the object if len(gx) > 0: # Calculate bounding box dimensions and center coordinates in YOLO format - bbox_width = (np.max(gx) - np.min(gx)) / img_width - bbox_height = (np.max(gy) - np.min(gy)) / img_height - bbox_center_x = (np.max(gx) + np.min(gx)) / (2 * img_width) - bbox_center_y = (np.max(gy) + np.min(gy)) / (2 * img_height) - - bbox_corners = [bbox_center_x, bbox_center_y, bbox_width, bbox_height] - bbox_corners = [round(x, 6) for x in bbox_corners] - else: - bbox_corners = [0, 0, 0, 0] + bbox_corners = bbox_to_yolo([np.min(gx), np.min(gy), np.max(gx), np.max(gy)], img_width, img_height) # If bounding_box is 1, plot the bounding box for the object if bounding_box==1: