Spaces:
Runtime error
Runtime error
File size: 15,626 Bytes
3f42a6f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 |
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 88<angle(approx[1],approx[0],approx[2])<92 and 88<angle(approx[3],approx[0],approx[2])<92: #if cnt can be approx with only 4 points, it is a 4 side polygon
dx1 = abs(approx[0][0][0] - approx[1][0][0]) # Difference in x for the first side
dy1 = abs(approx[0][0][1] - approx[1][0][1]) # Difference in y for the first side
dx2 = abs(approx[1][0][0] - approx[2][0][0]) # Difference in x for the second side
dy2 = abs(approx[1][0][1] - approx[2][0][1]) # Difference in y for the second side
# Set a threshold to ensure the sides are aligned to the image axes
threshold = 10 # Allowable deviation from horizontal or vertical alignment
if (dx1 < threshold or dy1 < threshold) and (dx2 < threshold or dy2 < threshold):
return True
else:
return False
else:
return False
def find_rectangles(img, binary_thres = 127):
'''Returns a list with rectangles from an image
Args: img: the image (mechanical engineering drawing)
returns:
rect_list: list of rects
img_boxes: image with the contourns of the boxes'''
# Convert img to grayscale and threshold to binary image
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, binary_thres, 255, cv2.THRESH_BINARY_INV)
# Find contours
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
img_boxes = img.copy()
r = 0
rect_list=[]
for cnt in contours:
x1,y1 = cnt[0][0] #Top-left corner
approx = cv2.approxPolyDP(cnt, 0.01*cv2.arcLength(cnt, True), True) #Approach cnt to polygons with multiple points
if is_box(approx): #if cnt can be approx with only 4 points, it is a 4 side polygon
x, y, w, h = cv2.boundingRect(cnt) #get rectangle information
if w*h>1000: #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 |