我想我也把“我的”方法扔进锅里。当我发表评论时,我还没有看到詹姆斯的回答。我的方法非常相似,增加了一些方向约束。我还假设角彼此完全成一线,即 它们形成的轴 偏移
方法:
-
使用 “命中和未命中变换” ,找到这些角标记,并按它们指示的角分组
-
方向 上找到最接近匹配的角
如果所有角度都匹配,这将找到所有的盒子。
我将介绍两种将节点关联到矩形的方法。第一种方法更像“行人”,以固定顺序绕矩形移动。第二种方法构建一个图形,因此它可以更优雅地处理缺失的角落。
我不处理只有两个角的矩形的情况。如果它们彼此对角,则可以恢复。我不这样做。这只需要朝正确的方向看并考虑最近的角...但这必须确保不会捕捉到实际上属于完整矩形的角。
# hit-miss-transform
kernel = np.array([
[-1, -1, -1],
[-1, +1, +1],
[-1, +1, -1]
], dtype=np.int8)
def find_corners(im, kernel):
res = cv.morphologyEx(im, cv.MORPH_HITMISS, kernel)
coords = cv.findNonZero(res)
return coords.reshape((-1, 2)).tolist()
coords_tl = find_corners(im, kernel) # corner 1
coords_tr = find_corners(im, np.rot90(kernel, -1)) # corner 2
coords_br = find_corners(im, np.rot90(kernel, -2)) # corner 3
coords_bl = find_corners(im, np.rot90(kernel, -3)) # corner 4
第一个变体/替代方案:
# finding nearest corners in a certain direction
# dx and dy either lock that axis down or allow candidates in that direction
def closest_in_direction(x, y, dx, dy, coords):
candidates = [
i for i, (xx, yy) in enumerate(coords)
if (((xx - x) * dx >= 0) if dx != 0 else (xx == x))
and (((yy - y) * dy >= 0) if dy != 0 else (yy == y))
]
if not candidates:
return None
def distance_predicate(i):
xx, yy = coords[i]
return max(abs(xx - x), abs(yy - y))
return min(candidates, key=distance_predicate)
# assembling boxes from corners
boxes = []
while len(coords_tl) > 0:
x0, y0 = coords_tl.pop()
x1 = y1 = None
# looking for x1, y1 which is corner 3, diagonal to corner 1
if (i2 := closest_in_direction(x0, y0, dx=1, dy=0, coords=coords_tr)) is not None:
x1, y0_ = coords_tr.pop(i2)
assert y0_ == y0
if (i4 := closest_in_direction(x0, y0, dx=0, dy=1, coords=coords_bl)) is not None:
x0_, y1 = coords_bl.pop(i4)
assert x0_ == x0
if (x1 is not None) and (y1 is not None):
if (i3 := closest_in_direction(x1, y1, dx=0, dy=0, coords=coords_br)) is not None:
coords_br.pop(i3)
else:
print("BR corner not found, inferred")
boxes.append((x0, y0, x1, y1))
assert len(coords_tl) == 0
assert len(coords_tr) == 0
assert len(coords_br) == 0
assert len(coords_bl) == 0
# draw the boxes
pad = 5
canvas = cv.cvtColor(im, cv.COLOR_GRAY2BGR)
for box in boxes:
(x0, y0, x1, y1) = box
(x0, y0, x1, y1) = (x0 - pad, y0 - pad, x1 + pad, y1 + pad)
cv.rectangle(canvas, (x0, y0), (x1, y1), color=(0, 255, 0))
这是 构建图形并提取连通分量的 第二种
构建图表:
-
有一个节点列表
-
考虑所有对
-
检查一对之间是否应该有边,
寻找连通分量:
-
选择任何 未访问的 节点(它们最初都是未访问的)
-
找到它的邻居,找到他们的邻居,...直到找到所有邻居
-
将其标记为已访问
-
重复
# (x, y, dx, dy) # dx,dy suggest edges in specific direction
nodes = \
[ (x,y, +1, +1) for (x,y) in coords_tl ] + \
[ (x,y, -1, +1) for (x,y) in coords_tr ] + \
[ (x,y, -1, -1) for (x,y) in coords_br ] + \
[ (x,y, +1, -1) for (x,y) in coords_bl ]
def construct_edges(nodes):
edges = set()
# run through all pairings, test for edgability, maybe create edge
for i in range(len(nodes)-1):
for j in range(i+1, len(nodes)):
(ax, ay, adx, ady) = nodes[i]
(bx, by, bdx, bdy) = nodes[j]
dx = ax - bx
dy = ay - by
if ((ay == by) and (adx * bdx < 0) and (dx * adx < 0)) \
or ((ax == bx) and (ady * bdy < 0) and (dy * ady < 0)):
edges.add((i, j))
return edges
edges = construct_edges(nodes)
所有这些以任意顺序绘制的单边, 看起来 像是在正确的位置上画出了矩形……但实际上并不存在真正的矩形。这只是矩形的边缘。
def get_connected_components(nodes, edges):
components = set()
# set of sets, theoretically, frozenset etc...
# since our components are supposed to be contours, I'll give them order
# they'll be ordered TL, TR, BR, BL. nodes are already ordered in this way.
visited = np.zeros(len(nodes), dtype=bool)
while not visited.all():
i = np.argmin(visited) # find an unvisited node (min: false < true)
component = {i}
visited[i] = True
# add transitively until all neighbors are visited.
iterate = True # (component was extended, potentially more neighbors to visit)
while iterate:
iterate = False
for u,v in edges:
if (u in component) ^ (v in component):
component |= {u, v}
visited[u] = True
visited[v] = True
iterate = True
components.add(tuple(sorted(component))) # a set, theoretically, but eh
return components
components = get_connected_components(nodes, edges)
其中每个都是节点索引列表。只需查找节点并收集它们的坐标即可。如果您确定所有框都有四个角,那么您可以按顺序绘制边。如果您不确定,请获取 X 和 Y 方向上的最小和最大坐标,并将其用于矩形。
for component in components:
coords = np.array([nodes[k][:2] for k in component])
x0 = coords[:,0].min()
x1 = coords[:,0].max()
y0 = coords[:,1].min()
y1 = coords[:,1].max()
cv.rectangle(
img=canvas,
pt1=(x0 - pad, y0 - pad),
pt2=(x1 + pad, y1 + pad),
color=(0, 255, 0),
)
(看起来是一样的,除了节点到节点的线之外,应该是一样)