import cv2 import numpy as np class Rect(): def __init__(self, name, x, y ,w, h, state = 'green', parent = None, children = None): self.name = name self.x = x self.y = y self.w = w self.h = h self.state = state self.parent = parent self.children = children if children is not None else [] def __repr__(self): return f"{self.name} ({self.x}, {self.y}, {self.w}, {self.h})" def angle(pt0, pt1, pt2): '''get the angle of the lines conformed by pt0-pt1 and pt0-pt2 Args: pt0: np array with x,y coordinates pt1: np array with x,y coordinates pt2: np array with x,y coordinates Returns: angle: angle conformed by the lines, in degrees ''' # Vector differences d1 = pt1[0] - pt0[0] d2 = pt2[0] - pt0[0] # Dot product and magnitudes of the vectors dot_product = np.dot(d1, d2) norm_d1 = np.linalg.norm(d1) norm_d2 = np.linalg.norm(d2) # Calculate the angle in radians and convert to degrees angle_rad = np.arccos(dot_product / (norm_d1 * norm_d2)) angle_deg = np.degrees(angle_rad) return angle_deg def is_contained(inner_rect, outer_rect): """Check if inner_rect is fully contained inside outer_rect.""" return (outer_rect.x <= inner_rect.x <= outer_rect.x + outer_rect.w) and \ (outer_rect.y <= inner_rect.y <= outer_rect.y + outer_rect.h) and \ (outer_rect.x <= inner_rect.x + inner_rect.w <= outer_rect.x + outer_rect.w) and \ (outer_rect.y <= inner_rect.y + inner_rect.h <= outer_rect.y + outer_rect.h) def build_hierarchy(rectangles): """Organize Rect objects into a nested hierarchy based on containment.""" # Sort rectangles by size (area) in descending order rectangles.sort(key=lambda r: r.w * r.h, reverse=True) hierarchy = [] def add_to_hierarchy(parent, child): """Recursively add a child rect to the appropriate parent rect.""" for rect in parent.children: if is_contained(child, rect): add_to_hierarchy(rect, child) return parent.children.append(child) child.parent = parent # Add the largest rectangle to the hierarchy as root for rect in rectangles: if not hierarchy: hierarchy.append(rect) # First rectangle becomes the root else: # Try to insert this rect in one of the existing parents for root in hierarchy: if is_contained(rect, root): add_to_hierarchy(root, rect) break else: hierarchy.append(rect) # If no parent found, it's at root level return hierarchy def print_hierarchy(rectangles, level=0): """Recursively print the rectangle hierarchy.""" for rect in rectangles: print(" " * level + f"{rect.name}") print_hierarchy(rect.children, level + 1) def is_box(approx): if len(approx) == 4 and 881000: #Clean very small rectangles #cv2.putText(img_boxes, 'rect_'+str(r), (x1, y1), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 127, 83), 2) #Add rectangle tag img_boxes = cv2.drawContours(img_boxes, [cnt], -1, (0,127,255),4) #Plot rectangle contourn rect_list.append(Rect('rect_'+str(r),x,y,w,h)) #Get a list of rectangles r = r + 1 #print('Number of rectangles:', len(rect_list)) hierarchy = build_hierarchy(rect_list) return img_boxes, rect_list, hierarchy def find_frame(img, frame_thres): """ Detects the innermost frame of a mechanical drawing within an image. Parameters: - img: Input image (assumed to be a BGR image). - frame_thres: Threshold value to determine the minimum size of a valid frame as a fraction of image dimensions. Returns: - best_frame: A Rect object representing the detected frame, or None if no valid frame is found. """ from scipy.signal import find_peaks gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred = cv2.bilateralFilter(gray, 11, 61, 39) edges = cv2.Canny(blurred, 0, 255) kernel = np.ones((5, 5), np.uint8) edges = cv2.dilate(edges, kernel, iterations=1 ) # Create structuring elements for detecting vertical and horizontal lines v_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,20)) h_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (20,1)) # Extract vertical and horizontal lines using morphological operations v_morphed = cv2.morphologyEx(edges, cv2.MORPH_OPEN, v_kernel, iterations=2) v_morphed = cv2.dilate(v_morphed, None) h_morphed = cv2.morphologyEx(edges, cv2.MORPH_OPEN, h_kernel, iterations=2) h_morphed = cv2.dilate(h_morphed, None) # Reduce vertical and horizontal lines to their respective projections v_acc = cv2.reduce(v_morphed, 0, cv2.REDUCE_SUM, dtype=cv2.CV_32S) h_acc = cv2.reduce(h_morphed, 1, cv2.REDUCE_SUM, dtype=cv2.CV_32S) # Define a helper function to smooth projections def smooth(y, box_pts): box = np.ones(box_pts)/box_pts y_smooth = np.convolve(y, box, mode='same') return y_smooth # Smooth the vertical and horizontal projections s_v_acc = smooth(v_acc[0,:],9) s_h_acc = smooth(h_acc[:,0],9) # Detect peaks in the smoothed projections v_peaks, v_props = find_peaks(s_v_acc, 0.8*np.max(np.max(s_v_acc))) h_peaks, h_props = find_peaks(s_h_acc, 0.8*np.max(np.max(s_h_acc))) '''tmp = img.copy() for peak_index in v_peaks: cv2.line(tmp, (peak_index, 0), (peak_index, img.shape[0]), (0, 127, 255),4) for peak_index in h_peaks: cv2.line(tmp, (0, peak_index), (img.shape[1], peak_index), (255,127,0),4) cv2.imwrite('frame.png', tmp)''' # Ensure we have enough peaks to define a box if len(v_peaks) < 2 or len(h_peaks) < 2: return None # Not enough peaks to define a frame min_frame_tup = (img.shape[0] * frame_thres, img.shape[1] * frame_thres) #(y,x) def find_best_peaks(peaks, v_h): #v_h is a int= 0 or 1 to identify if we are talking vertical or horizontal peaks """ Identifies the best pair of peaks to define a frame along a given dimension. Parameters: - peaks: Detected peaks in the dimension. - v_h: Indicator (0 for vertical, 1 for horizontal). Returns: - A tuple (max_p, min_p) representing the selected peaks. """ min_peaks = np.array([peak for peak in peaks if peak < img.shape[v_h] / 2]) max_peaks = np.array([peak for peak in peaks if peak > img.shape[v_h] / 2]) min_frame = min_frame_tup[v_h] diff_matrix = np.subtract.outer(max_peaks, min_peaks) valid_diff_mask = (diff_matrix > min_frame) if np.any(valid_diff_mask): min_diff = np.min(diff_matrix[valid_diff_mask]) # Find the index of the minimum difference min_diff_index = np.where(diff_matrix == min_diff) max_idx, min_idx = min_diff_index[0][0], min_diff_index[1][0] max_p = max_peaks[max_idx] min_p = min_peaks[min_idx] return max_p, min_p # Return the best (innermost) frame found bottom, top = find_best_peaks(h_peaks, 0) right, left = find_best_peaks(v_peaks, 1) best_frame = Rect('frame', left, top, right - left, bottom - top) return best_frame def touching_box(cl, cl_fire, thres=1.1): '''returns true if cl is adjacent to cl_fire, a threshold ratio is applied to scale up cl Args: cl: rect to analyze cl_fire: list of rect thres: scale up ratio for cl return: boolean''' # Calculate scaled-up boundaries for cl scaled_w, scaled_h = cl.w * thres, cl.h * thres cl_center_x, cl_center_y = cl.x + cl.w / 2, cl.y + cl.h / 2 # Get new x, y based on the center of the rectangle after scaling scaled_x, scaled_y = cl_center_x - scaled_w / 2, cl_center_y - scaled_h / 2 # Check if scaled box overlaps with any in cl_fire for f in cl_fire: # Check if cl_box_ overlaps with fire_box if not (scaled_x + scaled_w < f.x or scaled_x > f.x + f.w or scaled_y + scaled_h < f.y or scaled_y > f.y + f.h): return True # There is an overlap return False def fire_propagation(class_list, cl_fire): '''Returns all boxes that are either touching cl_fire or each other, as a fire that propagates Args: class_list: list of rect cl_fire: the origin of the fire return: burnt: a list of rect in contact with cl_fire or its propagations''' cl_fire.state = 'fire' on_fire = [cl_fire] green = class_list burnt = [] l = 0 while len(on_fire): l=l+1 for g in green: if touching_box(g, on_fire, 1.1) is True: g.state = 'fire' for f in on_fire: f.state = 'burnt' on_fire=[cl for cl in class_list if cl.state == 'fire'] green=[cl for cl in class_list if cl.state == 'green'] burnt=[cl for cl in class_list if cl.state == 'burnt'] return burnt def find_clusters(rect_list): """ Groups rectangles into clusters based on their proximity and state. Clusters are defined as groups of rectangles where at least one rectangle propagates its "green" state to its neighbors. Parameters: - rect_list: List of Rect objects to be clustered. Returns: - clusters: A list of dictionaries, where each dictionary contains a covering Rect as the key and a list of the clustered Rect objects as the value. - singles: A list of Rect objects that do not belong to any cluster. """ new_list =[] for rect in rect_list: if len(rect.children) == 0: new_list.append(rect) clusters = [] singles = [] while len(new_list): rect = new_list[0] if rect.state == 'green': cluster = fire_propagation(new_list, rect) if len(cluster) > 1: min_x = min(rect.x for rect in cluster) min_y = min(rect.y for rect in cluster) max_x = max(rect.x + rect.w for rect in cluster) max_y = max(rect.y + rect.h for rect in cluster) # Calculate width and height of the covering rectangle width = max_x - min_x height = max_y - min_y # Create a new Rect object that covers all rectangles covering_rect = Rect(name='cluster_'+ str(len(clusters)), x=min_x, y=min_y, w=width, h=height) clusters.append({covering_rect:cluster}) else: singles.append(cluster[0]) new_list = [b for b in new_list if b not in cluster] return clusters, singles def cluster_criteria(clusters, singles, GDT_thres): """ Classifies clusters and single rectangles into categories based on size and containment. Parameters: - clusters: A list of clusters (each represented as a dictionary of a covering rectangle and its members). - singles: A list of single Rect objects not part of any cluster. - GDT_thres: A threshold for determining the size of large objects (e.g., tables vs GD&T boxes). Returns: - gdt: A list of clusters classified as GD&T boxes (smaller than the threshold). - tab: A list of clusters classified as tables (larger than the threshold). - dim: A list of single rectangles classified as dimensions (not contained in any cluster and small). """ gdt, tab, dim = [], [], [] for cl in clusters: for k_cl in cl: if k_cl.w * k_cl.h > GDT_thres: tab.append(cl) else: gdt.append(cl) for sl in singles: is_contained = False for cl in clusters: # Calculate the bounding box of the entire cluster cluster_min_x = min(k_cl.x for k_cl in cl) cluster_max_x = max(k_cl.x + k_cl.w for k_cl in cl) cluster_min_y = min(k_cl.y for k_cl in cl) cluster_max_y = max(k_cl.y + k_cl.h for k_cl in cl) # Check if the single rect (sl) is fully contained within the cluster's bounding box if (cluster_min_x <= sl.x <= cluster_max_x and cluster_min_y <= sl.y <= cluster_max_y and cluster_min_x <= sl.x + sl.w <= cluster_max_x and cluster_min_y <= sl.y + sl.h <= cluster_max_y): is_contained = True break # No need to check further if it’s already contained # If not contained in any cluster, append to dim if not is_contained: if sl.w * sl.h < GDT_thres / 4: dim.append(sl) return gdt, tab, dim def segment_img(img, frame_thres = 0.85, autoframe = True, GDT_thres = 0.02, binary_thres = 127): #Find rectangles img_boxes, rect_list, _ = find_rectangles(img, binary_thres= binary_thres) #print_hierarchy(hierarchy) frame = False if autoframe: frame = find_frame(img, frame_thres) clusters, singles = find_clusters(rect_list) gdt_boxes, tables, dim_boxes = cluster_criteria(clusters, singles, GDT_thres * img.shape[0] * img.shape[1]) if not frame: frame = Rect('frame', int(img.shape[1] * (1-frame_thres)/2), int(img.shape[0] * (1-frame_thres)/2), int(img.shape[1] * frame_thres), int(img.shape[0] * frame_thres)) filtered_gdt_boxes = [ {cluster: rects} for g in gdt_boxes for cluster, rects in g.items() if is_contained(cluster, frame) ] return img_boxes, frame, filtered_gdt_boxes, tables, dim_boxes