Python OpenCV - 过滤掉不在附近的轮廓

Python OpenCV - filtering out contours, that are not nearby

我试图让我的程序通过过滤掉其余部分来仅向我显示道路的中心。 我首先找到道路中心每个矩形的宽度和长度,然后过滤掉其余部分。

问题: 一些不在道路中心的矩形仍然可见,因为它们在道路中心的其他矩形的宽度和长度范围内。

我的想法: 运行 显示的每个矩形的 for 循环将: 在矩形的一端使用一个小半径来查看另一个矩形是否在该半径内,如果是,则显示该矩形, -> 如果不是: 使用具有相同半径的矩形的另一端,看看是否有另一个矩形在该半径内,如果是,则显示该矩形, -> 如果这两个陈述都是错误的:不显示已用于在附近找到另一个矩形的矩形。

我的想法是过滤掉所有其他已找到的不在道路中心的矩形,因为道路中心的每个矩形彼此靠近(附近至少有 1 个矩形). 基本上每个不在道路中心的矩形与另一个矩形的距离都更大,这就是为什么我认为我可以在这里使用半径或距离,但这只是我的想法。 现在我有 运行 的想法,我需要一些帮助才能让它工作,因为我不仅是 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()

我作为评论发帖,因为我不知道该怎么办。 我找到了这段代码并认为它​​应该适用于我的代码:

    def get_laser_points(image):
        """
        Return centers of laser-points found in the given image as list of coordinate-tuples.
        """
        # get the contour areas for the steppers
        mask = cv2.inRange(image, whiteLower, whiteUpper)
        contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        # compute the center of the contour areas
        centroids = []
        for contour in contours:
            m = cv2.moments(contour)
            # avoid division by zero error!
            if m['m00'] != 0:
                cx = int(m['m10'] / m['m00'])
                cy = int(m['m01'] / m['m00'])
                centroids.append((cx, cy))
                # following line manages sorting the found contours from left to right, sorting
                # first tuple value (x coordinate) ascending
                centroids = sorted(centroids)
        return centroids

现在每个轮廓都有某种中心,我该怎么办?

我花了一段时间尝试使用您检测到的矩形作为中心线(其他人偏离中心的奇怪噪音)来获得轨道周围的序列。

我首先稍微调整您的代码以保存检测到的矩形以将它们标准化为始终宽于高,并保存到 json 文件

# from 

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 来寻找最近的邻居,但这并不是那么简单,因为存在间隙,所以连接并不总是与最近的两个邻居。相反,我试图在矩形的方向 'positive' 方向和相反方向的 'negative' 方向寻找最近的邻居。

三个处理阶段:

第一阶段很简单,可以计算细端的中心 - 将使用这些作为线段来计算相交的参数线。

第二阶段遍历所有段,试图找到最佳连接的邻居 - 这使用每个矩形的角度和方向来计算沿着这些角度从聚焦矩形到所有其他矩形的距离,同时拒绝那些矩形陡峭的角度,并选择每个方向上最接近的角度 - 我尝试了根据路径长度和角度计算的度量,为 pos/neg 方向选择最小值。这样做的并发症是检测到的矩形有点抖动,有些是平行的,所以这里和那里有点软糖来尝试消除歧义(特别是 denom 阈值 30 设置为使该图像工作,您可能需要调整那。如果图像的分辨率更高,这可能会更简单。相交逻辑是最复杂的部分。检测到的矩形是从图像的顶部到底部,它们的方向可以是任意方向,所以相交必须处理这个.还有当交点位于需要特殊处理的线段内时,因为如何计算它们之间的路径长度和角度差不是很明显。

最近邻居的搜索使用暴力搜索来对抗所有其他的搜索 - 它的工作速度非常快,没有理由只通过与“最近的”矩形进行比较来尝试优化。

搜索 positive/negative 邻居会愉快地弥合间隙 - 如果这些间隙附近有噪声矩形可能会导致问题,但令人高兴的是,在您的图像中情况并非如此 :-) 我对指标的想法是尝试并更喜欢较小角度的连接以抵抗噪音 - 您的图像并没有真正发挥作用。

下面第一张图片是所有连接重叠的图片示例。

第三阶段现在所有 pos/neg 连接都已知是通过构建序列。如果一个节点与另一个节点有 pos 或 neg 连接,则如果另一个节点有相互的正连接或负连接,则它是“连接的”。只接受往复连接意味着异常值被拒绝——它们变成一个矩形序列。注意相邻的矩形可以在相反的方向上有效,例如与另一个节点的正连接可以通过来自该节点的正连接或负连接有效地往复。

因为起始节点可以(希望如此)同时具有 pos 和 neg 连接,所以我保留了一堆连接以供检查。起始节点的正连接首先放在堆栈上,只有在负端不能再建立任何连接时才会被拉出,然后它被插入到序列的开头——这处理没有连接循环的地方,所以顺序仍然正确。

然后它会向您显示连接的序列:-) 请参见下面的第二张图片。

抱歉代码不完全 refined/pretty,但它适用于此图像 - 我很想知道它是否适用于其他轨道图像。

在 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)

可能可以改进的事情是整理交点计算,例如更改我的软糖,使用它们所处的角度而不是 denom 值将两条线段简单地视为平行。现在有一个已知的好答案,您可以使用它来验证更改没有破坏任何东西 - 如果显示所有连接,则很难检测到 neightbour 检测中的异常,当然在完整图像上也不容易,所以我不得不采取步骤通过检测并关注检测到的特定矩形的连接,以了解为什么连接跳到邻居,以修复逻辑。

第 2 阶段后的中间图像显示了所有连接 - 蓝色连接是正连接,绿色连接是负连接。注意许多连接相互重叠,特别是在相邻的矩形之间,您只能看到最后绘制的那个。

生成的图像(每个起点上有一个圆圈):

这是轨道的长序列 - 数字是 centres.json 数组的索引,矩形 0 位于图像中的最低点(视觉上),它连接到 1 和 18 可能是一个像素或更高的两个。

[18, 0, 1, 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 槽车轨道时,我会尝试拍照,看看这个代码是否有效:-)