我试图通过过滤掉其余部分,使程序仅向我显示道路的中心。
我首先为道路中心的每个矩形找到宽度和长度,然后滤除其余部分。
问题:
某些不在道路中心的矩形仍然可见,因为它们位于道路中心的其他矩形的宽度和长度之内。
我的点子:
为显示的每个矩形运行一个for循环,它将:
在矩形的一端使用较小的半径,查看另一个矩形是否在该半径内,如果是,则显示该矩形,
->如果不是:
使用半径相同的矩形的另一端,看看该半径内是否还有另一个矩形,如果是,则显示该矩形,
->如果这两个语句均为假:不显示矩形,该矩形已被用来在附近查找另一个矩形。
我的想法是过滤掉找到的其他所有不在道路中心的矩形,因为道路中心的每个矩形都彼此靠近(附近至少有一个矩形)。
基本上每个不在道路中心的矩形都与另一个矩形有更大的距离,这就是为什么我认为我可以在此处使用半径或距离,但这只是我的想法。
现在,我已经没有足够的想法了,我需要一些帮助以使其工作,因为我不仅对OpenCV还是Python都不陌生。
我希望此代码不仅适用于此道路,而且适用于图像看起来相同的其他道路。
原始图片:
This is the original picture
左侧矩形的图片:
This is the picture with the rectangles left
编辑!:有人告诉我我可以使用scipy.spatial.KDTree查找邻居。
这是我的代码:
import cv2
import numpy as np
# Read image
image = cv2.imread('Resources/StreckeUni.png')
# Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# show image
cv2.imshow('Gray', gray)
cv2.waitKey(0)
# Adjust Contrass and brightness
alpha = 50 # Kontrast (0-100)
beta = 0 # Helligkeit (0-100)
adjusted = cv2.convertScaleAbs(gray, alpha=alpha, beta=beta)
# Bild anzeigen
cv2.imshow('Contrast', adjusted)
cv2.waitKey(0)
# find Canny Edges
edged = cv2.Canny(adjusted, 30, 200)
# Use Blur
blur = cv2.GaussianBlur(edged, (3, 3), 0)
# find Conturs
contours, hierarchy = cv2.findContours(blur, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# show img
cv2.imshow('Canny Edges', blur)
cv2.waitKey(0)
# show number of contours
print("Anzahl der Konturen = " + str(len(contours)))
# draw rectangles
for cnt in contours:
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
if rect[1][0] > rect[1][1]:
laenge = rect[1][0]
breite = rect[1][1]
else:
laenge = rect[1][1]
breite = rect[1][0]
if 13.9 < laenge < 25.1 and 3.2 < breite < 7.7:
cv2.drawContours(image, [box], -1, (0, 255, 0), 1)
# show final pic
cv2.imshow('Rectangles', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
最佳答案
我花了一段时间尝试将您检测到的矩形用作中心线(其他中心的杂音带有偏心),以获取轨道周围的序列。
我首先稍微调整一下代码以保存检测到的矩形,以将其标准化为始终宽于高,然后保存到json文件
# from https://stackoverflow.com/questions/62430766/python-opencv-filtering-out-contours-that-are-not-nearby
import cv2
import numpy as np
import json
import scipy.spatial
import math
IMAGE="track"
# Read image
image = cv2.imread(IMAGE+".png")
# Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# show image
#cv2.imshow('Gray', gray)
#cv2.waitKey(0)
# Adjust Contrass and brightness
alpha = 50 # Kontrast (0-100)
beta = 0 # Helligkeit (0-100)
adjusted = cv2.convertScaleAbs(gray, alpha=alpha, beta=beta)
# Bild anzeigen
#cv2.imshow('Contrast', adjusted)
#cv2.waitKey(0)
# find Canny Edges
edged = cv2.Canny(adjusted, 30, 200)
# Use Blur
blur = cv2.GaussianBlur(edged, (3, 3), 0)
# find Conturs
contours, hierarchy = cv2.findContours(blur, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# show img
#cv2.imshow('Canny Edges', blur)
#cv2.waitKey(0)
# show number of contours
print("Anzahl der Konturen = " + str(len(contours)))
possiblecentres = []
# draw rectangles
for cnt in contours:
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
# print(f"{rect=} {box=}" )
if rect[1][0] > rect[1][1]:
print( f"gt {rect[2]}")
laenge = rect[1][0]
breite = rect[1][1]
angle = rect[2]
tag="gt"
else:
print( f"lt {rect[2]}")
laenge = rect[1][1]
breite = rect[1][0]
angle = rect[2]+90
tag="lt"
rect1 = [[rect[0][0],rect[0][1]],[laenge,breite],angle]
if 13.9 < laenge < 25.1 and 3.2 < breite < 7.7:
cv2.drawContours(image, [box], -1, (0, 255, 0), 1)
# possiblecentres.append(rect)
possiblecentres.append(rect1)
# print( rect)
# show final pic
cv2.imshow('Rectangles', image)
cv2.waitKey(0)
#cv2.destroyAllWindows()
open( "centres.json","w").write(json.dumps(possiblecentres,indent=4))
cv2.imwrite(IMAGE+"_ol.png",image)
然后,我编写了代码以获取此json文件并连接矩形。我考虑使用例如kdtree可以找到最近的邻居,但这并不是那么简单,因为存在间隙,因此连接并不总是与最近的两个邻居保持联系。相反,我尝试在矩形方向上寻找“正”方向上最接近的邻居,而在相反方向上寻找“负”。
处理的三个阶段:
第一阶段很容易计算出细端的中心-将它们用作相交的参数线计算的线段。
第二阶段遍历所有段以尝试找到最佳连接的邻居-这使用每个矩形的 Angular 和方向来计算沿着这些 Angular 从聚焦矩形到所有其他矩形的距离,以太陡的 Angular 拒绝那些矩形,以及选择在每个方向上最接近的那些-我尝试了根据路径和 Angular 长度计算出的指标,选择正负方向的最小值。这样做的复杂之处在于,检测到的矩形有些抖动,并且有些是平行的,因此在这里和那里有一些软糖可以尝试消除歧义(特别是将定标阈值设置为30以使该图像正常工作,您可能需要调整如果图像具有更高的分辨率,则可能会更简单。相交逻辑是最复杂的部分。检测到的矩形是从图像的顶部到底部,并且其方向可以是任一种方式,因此相交必须处理该问题。同样,当相交在线段内时,需要特殊处理,因为如何计算它们之间的路径长度和 Angular 差不是很明显。
这种对最近邻居的搜索对所有其他邻居使用了蛮力搜索-它的运行速度非常快,以至于没有理由只通过与“最近”矩形进行比较来进行优化。
寻找正负邻居会很高兴地弥合差距-如果在这些差距附近有噪声矩形可能会引起问题,但是很高兴,您的图像中并非如此:-)我用度量标准的想法是尝试并希望使用较小的连接 Angular 以抵抗噪音-您的图像并没有真正发挥作用。
所有连接覆盖的图像示例是下面的第一张图像。
现在所有已知的pos / neg连接的第三阶段将经历构建顺序。如果一个节点具有到另一节点的pos或neg连接,则如果该另一节点具有反向的正向或反向连接,则该节点“连接”。仅接受往复连接意味着异常值被拒绝-它们成为一个矩形序列。请注意,相邻矩形可以在相反的方向上有效,例如与另一个节点的正连接可以通过该节点的正或负连接有效地往复移动。
因为起始节点可以(希望有)同时具有pos和neg连接,所以我保留一堆连接以供检查。起始节点的正连接首先放在堆栈上,只有在负侧无法建立更多连接时才拉断,然后将其插入序列的开头-这处理没有连接循环的地方,因此顺序仍然正确。
然后,它向您显示连接的序列:-)参见下面的第二张图像。
不好意思,代码并不精确/漂亮,但它适用于此图像-我很想知道它是否可与其他轨道图像一起使用。
在Windows / AMD64上使用Python 3.8.3和opencv 4.2进行编码
import cv2
import numpy as np
import json
import scipy.spatial
import math
# set to true to produce copious debugging printout while trying to connect
DEBUG = False
# set to true to display image after each iteration of trying to connect
STEP = False
# the image to draw over
TRACK = 'track_ol.png'
def dist(x1,y1,x2,y2):
return math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))
# normalise ang to +/-pi
def normalise(ang):
while ang<-math.pi:
ang += 2.0*math.pi
while ang>math.pi:
ang -= 2.0*math.pi
return ang
# returns the parametric distance of the intersection of l1 and l2 in terms of l1
def intersect(l1,l2):
if DEBUG:
print( f"{l1=}" )
print( f"{l2=}" )
x1,y1=l1[0][0],l1[0][1]
x2,y2=l1[1][0],l1[1][1]
x3,y3=l2[0][0],l2[0][1]
x4,y4=l2[1][0],l2[1][1]
if DEBUG:
print( f"{x1=} {y1=}" )
print( f"{x2=} {y2=}" )
print( f"{x3=} {y3=}" )
print( f"{x4=} {y4=}" )
denom = (x1-x2)*(y3-y4)-(y1-y2)*(x3-x4)
xc1,yc1=(x1+x2)/2.0,(y1+y2)/2.0
xc2,yc2=(x3+x4)/2.0,(y3+y4)/2.0
if DEBUG:
print( f"{denom=}" )
if abs(denom)<30.0: # this is a bit of a fudge - basically prefer finding parallel lines to slighly divergent ones
# parallel
# work out distance between the parallel lines https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
dist1 = abs((y2-y1)*x3-(x2-x1)*y3+x2*y1-y2*x1)/math.sqrt(dist(x1,y1,x2,y2))
if DEBUG:
print( f"{dist1=}" )
if dist1>30.0:
if DEBUG:
print( f"Parallel rejected dist {dist1=}" )
return None,None
# work out the centres of the line joining centres of l1 and l2 - going to use the line joining these centres
intx,inty = (xc1+xc2)/2.0, (yc1+yc2)/2.0 # and if angle of intersection would be too greate
pathlength = dist(xc1,yc1,xc2,yc2)
# t = dist(xc1,yc1,intx,inty)/dist(x1,y1,x2,y2)
# u = -dist(xc2,yc2,intx,inty)/dist(x1,y1,x2,y2)
# choose the x or y which is most different
if abs(y2-y1)>abs(x2-x1):
t = (inty-y1)/(y2-y1)
else:
t = (intx-x1)/(x2-x1)
if abs(y4-y3)>abs(x4-x3):
u = (inty-y3)/(y4-y3)
else:
u = (intx-x3)/(x4-x3)
ang1 = math.atan2(y2-y1,x2-x1)
ang2 = math.atan2(yc2-yc1,xc2-xc1)
# choose the smaller change
ang = normalise(ang2-ang1)
if abs(ang)>0.5*math.pi:
pathlength = -pathlength
ang = normalise(math.pi-ang)
if DEBUG:
print( f"Parallel {xc1=} {yc1=} {xc2=} {yc2=}" )
print( f"Parallel {intx=} {inty=}" )
print( f"Parallel {pathlength} {t=} {u=} {ang=} {ang1=} {ang2=}" )
if abs(ang)>0.5*math.pi:
# print( f"Rejected ang {ang=}" )
return None,None
# if abs(ang) >0.5*math.pi:
# t = 1.0-t
if DEBUG:
print( f"Parallel {pathlength} {t=} {u=} {ang=}" )
return pathlength,ang
# work out the intersection https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
t = ((x1-x3)*(y3-y4)-(y1-y3)*(x3-x4))/denom
u = -((x1-x2)*(y1-y3)-(y1-y2)*(x1-x3))/denom
intx,inty = x1+t*(x2-x1),y1+t*(y2-y1)
pathlength = dist(xc1,yc1,intx,inty)+dist(intx,inty,xc2,yc2)
if DEBUG:
print( f"{intx=} {inty=}" )
# get the raw angle of l1 and l2
# if t is <0, turn ang1 by 180 to point towards the intersection
ang1 = math.atan2( y2-y1,x2-x1 )
ang2 = math.atan2( y4-y3,x4-x3 )
# get the angle of line from centre of l1 to centre of l2
angc = math.atan2( yc2-yc1,xc2-xc1 )
# decide if centre l2 is "forwards" (in the direction l1 points) of centre of l1 - used only when there is an intersection within one of them
# so sign of pathlength can be set correctly
# MUST do this before normalising ang1 to point to the intersection and ang2 to point away
if abs(normalise(angc-ang1)) < 0.5*math.pi:
l1forwards = True
else:
l1forwards = False
if DEBUG:
print( f"{l1forwards=} {angc=} {ang1=}" )
# now normalise ang1 to point towards the intersection
# NOTE if either intersection is within either line, ang1/ang2 normalisation doesn;t matter
# because the smaller difference angle will be used.
if t < 0.0:
ang1 = normalise(ang1+math.pi)
# we want ang2 going away from the intersection, so it u is positive reverse the angle
# now normalise ang2 to point away the intersection
if u > 1.0:
ang2 = normalise(ang2+math.pi)
# check for intersection within l1 or l2
if 0.0 <= t <= 1.0:
# internal to l1
if 0.0 <= u <= 1.0:
# this is internal to both l1 and l2
raise Exception( "This should never happen because it means the blobs overlap!")
else:
# internal only to l1
# find the smallest angle of intersection
if abs(normalise(ang2-ang1))<abs(normalise(math.pi-(ang2-ang1))):
ang = normalise(ang2-ang1)
if DEBUG:
print( "t norev" )
else:
ang = normalise(math.pi-(ang2-ang1))
if DEBUG:
print( "t rev" )
if abs(ang)>0.25*math.pi:
if DEBUG:
print( f"Rejected1 {ang=} {ang1=} {ang2=}" )
return None,None
pathlength = pathlength if l1forwards else -pathlength
else:
# not internal to l1
if 0.0 <= u <= 1.0:
# internal only to l2
if abs(normalise(ang2-ang1))<abs(normalise(math.pi-(ang2-ang1))):
ang = normalise(ang2-ang1)
if DEBUG:
print( "u norev" )
else:
ang = normalise(math.pi-(ang2-ang1))
if DEBUG:
print( "u-rev" )
if abs(ang)>0.25*math.pi:
if DEBUG:
print( f"Rejected2 {ang=} {ang1=} {ang2=}" )
return None,None
pathlength = pathlength if l1forwards else -pathlength
else:
# this is external to both l1 and l2 - use t to decide if forwards or backwards
ang = normalise(ang2-ang1)
pathlength = pathlength if t >= 0.0 else -pathlength
# ang = math.atan2(y2-y1,x2-x1)-math.atan2(y4-y3,x4-x3)
if DEBUG:
print( f"{pathlength=} {t=} {u=} {ang=} {ang1=} {ang2=}" )
if abs(ang)>0.5*math.pi:
if DEBUG:
print( f"Rejected ang {ang=}" )
return None,None
return pathlength,ang
#cv2.waitKey(0)
#cv2.destroyAllWindows()
possiblecentres = json.loads(open( "centres.json","r").read() )
# phase 1 - add the x/y coord of each end of the rectangle as the ends of a line segment
for i,candidate in enumerate(possiblecentres):
candidate.append(0) # usecount
xc,yc,dx,dy,angle = candidate[0][0],candidate[0][1],candidate[1][0]/2.0,candidate[1][1]/2.0,candidate[2]*math.pi/180.0
x1,y1 = xc-dx*math.cos(angle),yc-dx*math.sin(angle)
x2,y2 = xc+dx*math.cos(angle),yc+dx*math.sin(angle)
candidate.append(((x1,y1),(x2,y2))) #centre-endpoints
image = cv2.imread(TRACK)
# phase 2 - for each blob, find closest pos/neg blob
for i,candidate in enumerate(possiblecentres):
posintersect,pospt,posang,len1 = 1e10,None,None,None
negintersect,negpt,negang,len1 = 1e10,None,None,None
l1 = candidate[4]
if DEBUG:
print( f"\n\n\n===================================\n{i=} {l1=}" )
print( f"{candidate=}" )
for j,other in enumerate(possiblecentres):
if i==j:
continue
l2 = other[4]
if DEBUG:
print( f"\n============\n{j=} {l2=}" )
print( f"{other=}" )
pathlen,ang = intersect(l1,l2)
if pathlen is None:
continue
metric =pathlen*pathlen*math.exp(abs(ang))
if metric > 100000000:
if DEBUG:
print( f"Rejected metric {metric=}" )
continue
# print( f"{pathlen=} {ang=} {metric=}" )
# find closest intersection in +ve direction
if pathlen>=0.0 and metric<posintersect:
posintersect,pospt,posang,len1 = metric,j,ang,pathlen
if DEBUG:
print( "POS updated")
if pathlen<0.0 and metric<negintersect:
negintersect,negpt,negang,len1 = metric,j,ang,pathlen
if DEBUG:
print( "NEG updated")
if DEBUG:
print( i,candidate,posintersect,pospt,posang,negintersect,negpt,negang )
print( f"{i=} {pospt=} {negpt=}" )
# foundconnections[i] = 0
possiblecentres[i].append(((posintersect,pospt,posang,len1),(negintersect,negpt,negang,len1)))
# cv2.line(image,(int(candidate[4][0][0]),int(candidate[4][0][1])),(int(candidate[4][1][0]),int(candidate[4][1][1])),(255,255,255))
if STEP:
image = cv2.imread(TRACK)
thisrect = (candidate[0],candidate[1],candidate[2])
box = cv2.boxPoints(thisrect)
box = np.int0(box)
cv2.drawContours(image, [box], -1, (255, 255, 255), 1)
if pospt is not None:
cv2.line(image,(int(candidate[0][0]),int(candidate[0][1])),(int(possiblecentres[pospt][0][0]),int(possiblecentres[pospt][0][1])),(255,0,0))
if negpt is not None:
cv2.line(image,(int(candidate[0][0]),int(candidate[0][1])),(int(possiblecentres[negpt][0][0]),int(possiblecentres[negpt][0][1])),(0,255,0))
if STEP:
cv2.imshow('Rectangles', image)
if cv2.waitKey(0)==113:
break
# finally phase 3 - for each blob's connections, match up with another blob that reverses that connection
# This eliminates odd blobs that aren't recipricolly connected to the things they connect to
sequences = [] # each entry in this list is a sequence of indexes into possiblecentres
todos = list(range(len(possiblecentres)))
stacktochecks = [] # this should never have more than two items on it - and the first one is only removed once the circuit is complete
while True:
if stacktochecks:
print( f"{stacktochecks=}" )
print( f"{sequences[-1]=}" )
# continued sequence
startpt = stacktochecks.pop()
if stacktochecks:
# we are on the outbound leg - for a fully-connected start point there's a non-empty stack which is the other connection from that starting element
sequences[-1].append(startpt)
else:
# stack is empty so insert new points at the start of this sequence - this ensures the sequence on the list is consecutive even if the outgoing sequence didn't come back here
sequences[-1].insert(0,startpt)
print( f"continuing from {startpt=}" )
else:
if sequences:
print( f"FINISHED {sequences[-1]=}" )
if len(todos)==0:
break
# new sequence
startpt = todos.pop(0)
sequences.append([startpt])
print( f"starting from {startpt}" )
nextpos = possiblecentres[startpt][-1][0][1]
nextneg = possiblecentres[startpt][-1][1][1]
print( f"{nextpos=} {nextneg=}" )
if nextpos is not None:
if nextpos in todos:
# hasn't been used yet
if startpt == possiblecentres[nextpos][-1][0][1]:
print( f"pos match1 pushing {nextpos}" )
stacktochecks.append(nextpos)
todos.remove(nextpos)
elif startpt == possiblecentres[nextpos][-1][1][1]:
print( f"pos match2 pushing {nextpos}" )
stacktochecks.append(nextpos)
todos.remove(nextpos)
else:
#
todos.remove(nextpos)
pass
# burp3
else:
if nextpos in sequences[-1]:
# already in sequences, so we closed the loop!
# burp1
pass
else:
# not in current sequence and not in todos - that's weird, but end the sequence
pass
# burp2
# pass
if nextneg is not None:
if nextneg in todos:
# hasn't been used yet
if startpt == possiblecentres[nextneg][-1][0][1]:
print( f"neg match1 pushing {nextneg}" )
stacktochecks.append(nextneg)
todos.remove(nextneg)
elif startpt == possiblecentres[nextneg][-1][1][1]:
print( f"neg match2 pushing {nextneg}" )
stacktochecks.append(nextneg)
todos.remove(nextneg)
else:
#
todos.remove(nextneg)
pass
# burp4
else:
if nextneg in sequences[-1]:
# already in sequences, so we closed the loop!
# burp5
pass
else:
# not in current sequence and not in todos - that's weird, but end the sequence
pass
# burp6
# pass
print( f"{sequences=}")
image = cv2.imread(TRACK)
# now display the sequences, and put circles on the startpoints
for i,seq in enumerate(sequences):
for j,node in enumerate(seq):
thisnode = possiblecentres[node]
thisrect = (thisnode[0],thisnode[1],thisnode[2])
box = cv2.boxPoints(thisrect)
box = np.int0(box)
cv2.drawContours(image, [box], -1, (255, 255, 0), 1)
thisx,thisy = thisnode[0][0],thisnode[0][1]
if j == 0:
cv2.circle(image,(int(thisx),int(thisy)),10,(0,127,127),2)
firstx,firsty = thisx,thisy
if j > 0:
# draw a thick line to previous
cv2.line(image,(int(lastx),int(lasty)),(int(thisx),int(thisy)),(127,127,127),5)
if j!=0 and j+1==len(seq):
# this is the connection back to the start
cv2.line(image,(int(firstx),int(firsty)),(int(thisx),int(thisy)),(127,127,127),5)
lastx,lasty = thisx,thisy
cv2.imshow('Sequences', image)
cv2.waitKey(0)
可能需要改进的事情是整理交叉点的计算,例如更改我的软糖,只使用两条线段的 Angular 而不是denom
值将两条线段视为平行。现在有一个已知的好答案,您可以使用它来验证更改没有破坏任何东西-如果显示所有连接,则很难在整个图像上检测neightbour检测中的异常,当然这并不容易,所以我必须采取步骤通过检测并专注于针对特定矩形的检测到的连接,以了解该连接为何跳出邻居的原因,以解决逻辑问题。阶段2之后的中间图片显示了所有连接-蓝色连接为正极,绿色为负极。注意许多连接相互重叠,因此特别是在相邻矩形之间,您只会看到最后绘制的一个。
产生的图像(在每个起点上都有一个圆圈):
这是音轨的长序列-数字是centers.json数组的索引,矩形0(在视觉上)位于图像的最低点,该像素与1和18相连,可能高出一个像素或两个。
[18,0,1,2,2,3,4,5,6,7,8,9,10,11,19,20,21,22,12,13,23,30,33,35,37,39 ,41、43、45、47、49、51、53、55、57、72、88、93、94、95、97、99、102、104、105、119、124、143、144、145、147 ,149,152,159,163,168,172,175,177,178,176,174,169,167,165,162,160,156,155,153,154,157,161,164,170,180 ,182,184,186,189,191,194,193,192,190,188,185,183,181,173,166,158,151,148,146,142,135,131,127,123,120 ,112、111、110、108、109、114、116、117、121、125、128、132、134、137、139、140、141、138、136、133、130、126、122、118、113 ,107,106,103,101,98,96,87,68,60,59,61,63,62,64,65,67,66,70,71,73,75,74,76,78,77 ,79,81,80,82,85,84,86,90,89,92,91,83,69,58,56,54,52,50,48,46,44,42,40,38,36 ,34、32、31、28、29、24、25、26、27、14、15、16、17]
下次我设置scalextric槽车轨道时,我将尝试拍照,看看此代码是否有效:-)
关于python - Python OpenCV-过滤掉不在附近的轮廓,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62430766/