如何計算道路及十字路口的車流

撰寫/攝影 CH.Tseng
部落格 https://chtseng.wordpress.com/
Facebook https://www.facebook.com/chenghsun.tseng

傳統車流的計算是以人工計數器的方式,再搭配測速槍以計算行車速率,但採用此方式進行交通調查需要耗費龐大資源而且難以頻繁及大規模地執行,所以後來改採用車輛偵測器(Vehicle Detector, VD),有超音波反射式、環路線圈式、聲納式、紅外線以及微波雷達式等不同的方法,但使用VD會有購置成本及後續的維修成本,成本較高。

在目前電腦硬體成本大幅下降以及Deep learning和computer vision技術的支援下,透過影像方式來計算統計車流成為可行,不但成本更低且方便設置。下面我們來試看看,透過影像的方式來計算道路上的車流量,並且分類出車子的類型,以及不同方向的車流數目。

使用技術

要能作到分類車子類型以及不同方向的車流數目,需要使用到:

  1. 物件偵測( Object detection):

用於車輛位置及類型,本範例中我們使用YOLO V3進行車輛的偵測及定位,您也可以使用其它的物件偵測技術,例如SSD或R-CNN等等,只要能夠取得車輛的種類及位置就可以了。

  1. 物件追蹤 (Object tracking)

避免重複計算,因為每個影格frame每進行一次物件偵測都會得到一次新的物件列表,使得我們無從得知上下frame的物件是否為同一台車輛以避免重複計算,也無法掌握車子行進的方向,這必須要透過Object tracking才能解決。

  1. 影像處理

我們不需要針對整張圖計算車子流量及方向,只要加入繪製好的線段、熱區到隱藏圖層上,當車輛經過此線或此區域時再進行判斷及計算即可。

車輛偵測定位及分類

我們直接使用YOLO提供的pre-trained yolov3.weights來進行車輛的定位偵測及分類。因為這個pre-trained model是使用Coco dataset所訓練,可偵測多達80種物件,其中也包含了數種車輛類型,如:car、truck、bus、bicycle、motorbike等,若是想要做其它更多或更細的車輛分類,需要另外自行訓練哦。

車輛追蹤及計算

接下來,我們要在圖片上虛擬一塊熱區及一條計算線,它的位置及功能如下:

當我們得到車輛的類型以及位置之後,接著,我們將:

  • 判斷此車輛的位置是否在熱區的範圍。
  • 若處於熱區範圍,則與上一個frame的車輛進行比對,計算該車中心點與上一個frame所有車輛的中心點距離最短是那一台。
  • 若非處於熱區範圍則略過不處理。
  • 由於我們的熱區是定義於道路中間位置,因此理論上該熱區不會有突然新出現的車輛,每一台車應該能找到其一個frame的所在位置。
  • 經由上下frame得到該車的行徑方向,並將該方向的車輛數目加1。
  • 車子行徑的方向,可由上下frame車子的中心點(X,Y)的變化來判斷:

注意放置計算線的位置,我偏好放置於比較接近出口的附近,這是為了給YOLO有充份的時間進行偵測,若是放得太接近入口,那麼車子剛出現的尺寸可能不太大(因為距離較遠),而且可能還沒有偵測到就快離開計算線或熱區了,這樣會影響到計算的結果。

實例

以下面的影片為例,很明顯我們只要判斷Y值的變化即可判斷車流的方向。

我們在影片中加入南北雙向的兩條計算線,並以線的上下各30 pixels寬度作為熱區,可以想像影片如下:

輸入車輛的bbox(bounding box),判斷是否進入南下的熱區,若在熱區,則將該車的ID、bbox、中心點、車子種類、是否已計算過等資訊,放入儲存目前frame data的array中。

def in_range_N2S(bbox):

#only calculate the cars (from south to north) run across the line and not over Y +- calculateRange_y

x = bbox[0]

y = bbox[1]

w = bbox[2]

h = bbox[3]

cx = int(x+(w/2))

cy = int(y+(h/2))

if(cx>=calculateLine2[0][0] and cx<=calculateLine2[1][0] and \

cy>calculateLine2[0][1]-calculateRange_y and cy<=(calculateLine2[1][1]+calculateRange_y) ):

return True

else:

return False

if(in_range_S2N(box)==True or in_range_N2S(box)==True):

now_IDs.append(id)

now_BBOXES.append(box)

now_CENTROIDS.append(bbox2Centroid(box))

now_LABELS.append(labelName)

now_COUNTED.append(False)

cv2.rectangle(frame, (box[0], box[1]), (box[0]+box[2], box[1]+box[3]), (0,255,0), 2)

傳入上下兩個frame的車子bbox list,以上個frame的list為基準,從上一個frame熱區中車子裹,找出與目前frame熱區中距離最短的車子,放入新的list中,我們可以透過該list找到上下兩個frame中每台車子的對應。

兩點(x1,y1), (x2, y2) 距離的計算是使用歐式距離(又稱歐幾里得距離),指的就是兩點之間的直線最短距離。

def count_Object(centroid_last, centroid_now):

distances = []

for cent_last in centroid_last:

smallist = 99999.0

smallist_id = 0

dist = 0.0



for id, cent_now in enumerate(centroid_now):

dist = distance(cent_now, cent_last)

if(dist<=smallist):

smallist = dist

smallist_id = id

distances.append(smallist_id)

return distances

從車子對應表中,如果該車輛的中心點在上個frame位於計算線下方,而目前frame的中心點已超過計算線,則可判斷該車為北上,反之則為南下,分別針對不同車道、不同的車型累加1。

for id, now_id in enumerate(obj_target):

#if last Y is under the line and now Y is above or on the line, then count += 1

print(last_CENTROIDS[id][1], calculateLine2[0][1], now_CENTROIDS[now_id][1], calculateLine2[0][1])

UP = last_CENTROIDS[id][1]>calculateLine1[0][1] and now_CENTROIDS[now_id][1]<=calculateLine1[0][1]

DOWN = last_CENTROIDS[id][1]<calculateLine2[0][1] and now_CENTROIDS[now_id][1]>=calculateLine2[0][1]

if( UP is True):

if(now_LABELS[now_id]=="truck"):

count_Truck1 += 1

elif(now_LABELS[now_id]=="car"):

count_Car1 += 1

elif(now_LABELS[now_id]=="bus"):

count_Bus1 += 1

elif( DOWN is True):

if(now_LABELS[now_id]=="truck"):

count_Truck2 += 1

elif(now_LABELS[now_id]=="car"):

count_Car2 += 1

elif(now_LABELS[now_id]=="bus"):

count_Bus2 += 1

執行結果

非垂直或水平的道路

你可能會有個疑問,這類十字形垂直或水平的道路很容易用X或Y軸的變化來計算,但如果是傾斜或不規則形狀的道路那怎麼處理呢?比如像下圖的十字路口。

像這類斜向的道路,如果我們依樣畫葫蘆劃出斜度的線性直接才行,但問題就來了,要如何快速的判斷車輛行徑,而不需要很麻煩的計算出y=ax+b的函式?此外,斜線寬度為1的直線,實際上其寬的點數不一定為1,因此很有可能一台車輛速度很慢,上個frame的中心點在此線上,下一個frame也還沒脫離此線,不過此問題較小可透過程式來解決。

以隱藏的色線來取代計算線

我們可以加入此隱藏的圖層到圖片上,看起來如下(各路口都有紅綠的雙層線,看起來顏色雖然相同,但不同路口的顏色值有差一點點,可協助我們快速判斷目前車子是在那一個路口):

採用顏色的好處如下:

  1. 紅色與綠色的交界處就類似我們之前的計算線,其不同色系的交界處永遠為1,不會有寬度不均的問題。
  2. 取得車輛中心點在隱藏層的顏色,比起推論線性函式及計算該點位於該線的何種方位來得簡單且直覺多了。
  3. 提供自行定義的界面,讓使用者可在相片中抹上顏色或劃線,自行加入不規則形狀的禁區或熱區。例如,我們可以在如下的草坪區加入隱藏的色區,用於偵測是否有人侵入。

繪製隱藏圖層及顏色判斷程式說明:以此張圖為示範

定義十字路口上的線段並繪製到一複製的圖(該圖我們不顯示,因此是隱藏的)。

線條可以畫寬一點作為熱區來使用。範例中用40(px),當車輛在該顏色區域時,可開始進行追蹤。

calculateLine1 = [(36, 610), (1126, 900)]  #[(from(x,y), to(x,y)]

calculateLine2 = [(820, 350), (1574, 530)] #[(from(x,y), to(x,y)]

calculateLine3 = [(830, 380), (0, 580)] #[(from(x,y), to(x,y)]

calculateLine4 = [(1532, 590), (1224, 880)] #[(from(x,y), to(x,y))

def draw_CalculateLine(frame):

cv2.line(frame, (calculateLine1[0][0],calculateLine1[0][1]+20), (calculateLine1[1][0],calculateLine1[1][1]+25), (0, 255, 0), 40)

cv2.line(frame, (calculateLine2[0][0],calculateLine2[0][1]-20), (calculateLine2[1][0],calculateLine2[1][1]-25), (0, 255, 0), 40)

cv2.line(frame, (calculateLine3[0][0],calculateLine3[0][1]-20), (calculateLine3[1][0],calculateLine3[1][1]-25), (0, 255, 0), 40)

cv2.line(frame, (calculateLine4[0][0],calculateLine4[0][1]+25), (calculateLine4[1][0],calculateLine4[1][1]+35), (0, 255, 0), 45)

cv2.line(frame, (calculateLine1[0][0],calculateLine1[0][1]), (calculateLine1[1][0],calculateLine1[1][1]), (0, 0, 255), 40)

cv2.line(frame, (calculateLine2[0][0],calculateLine2[0][1]), (calculateLine2[1][0],calculateLine2[1][1]), (0, 0, 254), 40)

cv2.line(frame, (calculateLine3[0][0],calculateLine3[0][1]), (calculateLine3[1][0],calculateLine3[1][1]), (0, 0, 253), 40)

cv2.line(frame, (calculateLine4[0][0],calculateLine4[0][1]), (calculateLine4[1][0],calculateLine4[1][1]), (0, 0, 252), 40)

return frame

frameLayout = frame.copy()

frameLayout = draw_CalculateLine(frameLayout)

取得某車輛中心點在上一個frame以及目前frame的顏色,然後判斷比較其行徑方向。例如,目前上一個frame車子中心點所在為綠色,目前frame為紅色,則可判定為往十字路口中心走去。

取得圖片上某一點的RGB顏色值方法為:(B,G,R) = image[y,x]

顏色比對是否相同的方法為:image[y,x] == [B,G,R]).all()

color_now = frameLayout[now_CENTROIDS[now_id][1],now_CENTROIDS[now_id][0]]

color_last = frameLayout[last_CENTROIDS[id][1],last_CENTROIDS[id][0]]

UP_1 = (color_now == [0,0,255]).all() and (color_last == [0,255,0]).all()

DOWN_1 = (color_last == [0,0,255]).all() and (color_now==[0,255,0]).all()

UP_2 = (color_now == [0,0,254]).all() and (color_last == [0,255,0]).all()

DOWN_2 = (color_last == [0,0,254]).all() and (color_now==[0,255,0]).all()

UP_3 = (color_now == [0,0,253]).all() and (color_last == [0,255,0]).all()

DOWN_3 = (color_last == [0,0,253]).all() and (color_now==[0,255,0]).all()

UP_4 = (color_now == [0,0,252]).all() and (color_last == [0,255,0]).all()

DOWN_4 = (color_last == [0,0,252]).all() and (color_now==[0,255,0]).all()

示範影片:

想了解更多CH.Tseng,可以點此連結 瀏覽更多文章喔~

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *