去年11月寫了一篇《python+OpenCV做車輛追蹤》,分析定點攝影拍到的車流影片。不過之前的作法只有作到背景萃取出車輛的動態影像,對於車輛的計數是尚未作到的。
雖然openCV可以用cv2.findContours把影片中的每一幀中動態的部份分析出輪廓,得知每一幀中有多少移動的車輛,並且給予編號。但是同一輛車在前後幀的編號不一定相同的。
比如說第一幀有3輛車,分別給予編號123,到第二幀的時候,第一輛車已經消失在畫面,而有新的一輛車進入畫面。雖然前後兩幀的車輛數目都一樣,但是原本在第一幀的二號車,到第二幀時,就變成一號車了。
要讓電腦知道不同幀的車輛是屬於同一輛,可以利用一些演算法來做,像是knn、kmeans、meahshift、camshift、kalman filter等等。
不過我先用另外一種方式來做看看,因為我目的就是想把影片中的每一輛車擷取出來,所以我就在畫面中設定一個區域,只要車輛的輪廓進入這個區域,就把這個輪廓截圖出來。判定是否進入區域的方法,就是比較車輛的質心(或是其外框的一個角)是否在區域的範圍內。
這個區域的大小是一個重要因素,區域設得大,那麼同一輛車在不同幀的時候,可能都會落入這個區域,以至於同一輛車可能會被截圖二到三張。如果設得小,那麼某些開得快的車輛,可能會進不到這個區域(前一幀在區域的右邊,到下一幀已經到區域的左邊了)
雖然有這些缺點,但是還是可以滿足部份的需求,就至少可以把車輛截圖都抓取出來。以下就是某一分鐘內的車輛影像,白色或淺色的車輛佔比較高的比例呢。
後來我想到另一個方式讓電腦在不同幀中也能認出同一台車,原理就跟我們看車是類似的,如果給我們看兩張間隔一秒的靜態車流影像,我們怎麼認出兩張影像中的車輛是同一台呢?我們是藉由車輛的顏色、外型、位置去判斷的。
既然如此,那麼我就用位置來做吧。原理是把這一幀的車,跟上一幀的每一台車去比較位置,當位置差異最小而且在某一個範圍內,我就可以把不同幀的兩台車視作同一台車。
用程式實做出來,當然是可行,不過還是有一些問題。比方說一台車在某些幀的位置沒有被抓到(或是被抓歪了),那麼下一幀就沒辦法延續下去了。或是兩輛車在背景萃取的時候,因為太靠近,因此被視作同一部車,那麼到下一幀時,其中一部車也會無法延續下去。
以下影片就是實做出這樣的功能,藉由兩幀之間的位置變化,可以估算車速,也同時將車輛截圖下來,並且計算車輛數目。
雖然程式上的限制,會讓車輛數量被低估,但至少一些基本功能都已經有了。其實會想做這樣的事情,原因是十年前一個學生的科展需求。當初那學生的科展中有一部份要用到道路的車流量資料,那時她的作法就是週末花一些時間去站在路口算車子,當時我就很想看看是不是有其他方式可以達成目標,過了十年,總算是完成了(部份)。
=====
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import numpy as np
import cv2
import os
import math
from collections import deque
def carTracking(file,video_index):
cap = cv2.VideoCapture(file)
fgbg = cv2.createBackgroundSubtractorMOG2()
kernel = np.ones((11,11),np.uint8)
#scale = 13.3344 pixels/m
scale = 13.3344
car_num=1
allCarPos = []
posList = []
pictag = []
stamp = 1
#開始處理影格
while(cap.isOpened()):
ret, frame = cap.read()
#獲知每秒多少影格frame per second
fps = cap.get(cv2.CAP_PROP_FPS)
#影格播完了,就播放下一個,直到video_index到達影片檔案的數量,就停止
if frame is None:
video_index += 1
if video_index >= len(videofiles):
break
cap = cv2.VideoCapture(videofiles[ video_index ])
ret, frame = cap.read()
#把道路從影格中裁切出來
road=frame[475:644,250:1200]
fgmask = fgbg.apply(road) #background
fgmask = cv2.GaussianBlur(fgmask,(5,5),0) #GaussianBlue
ret,fgmask = cv2.threshold(fgmask,120,255,cv2.THRESH_BINARY) #Threshold
fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel) #影像型態open處理
fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_CLOSE, kernel) #影像型態close處理
#獲得真實車輛的影像
car = cv2.bitwise_and(road,road,mask = fgmask)
#inverse車輛的遮罩
fgmask_inv = cv2.bitwise_not(fgmask)
#用白色當背景
white=road.copy()
white[:,:]=255
#白色背景減去車輛遮罩,變成黑色車子在白色背景移動
road_withoutCar = cv2.bitwise_and(white,white,mask = fgmask_inv)
#[黑色車子在白色背景]+[真實車輛的影像]
whiteroad_car = cv2.add(road_withoutCar,car)
#取得車子的輪廓
image, contours, hierarchy = cv2.findContours(fgmask.copy(),cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#用線在車子輪廓上描出外框
car_contour1=road.copy()
car_contour1 = cv2.drawContours(car_contour1, contours, -1, (0,255,0), 3)
car_contour2=road.copy()
car_contour3=road.copy()
car_contour4=road.copy()
car1=road.copy()
##設定車輛追蹤範圍區
startX1 = 750
startX2 = 650
endX = 5
deltaDist = 30
#設定車子進入哪個區域產生觸發,被捕捉影像
bX1=500
bX2=600
bY1=10
bY2=100
cv2.line(car_contour3,(bX1,bY1),(bX1,bY2),color=(0,0,255),thickness=1)
cv2.line(car_contour3,(bX2,bY1),(bX2,bY2),color=(0,0,255),thickness=1)
cv2.line(car_contour3,(bX1,bY2),(bX2,bY2),color=(0,0,255),thickness=1)
#當畫面中沒有車輛,就清空posList
if len(contours)==0:
posList=[]
#對每一台車子做處理
for i in range(len(contours)):
cnt = contours[i]
#找出每個車輛的位置和大小
x,y,w,h = cv2.boundingRect(cnt)
#取出面積
area = cv2.contourArea(cnt)
#用綠線描繪最接近車輛外型的輪廓
epsilon = 0.1*cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt,epsilon,True)
car_contour2 = cv2.drawContours(car_contour2, [approx], -1, (0,255,0), 3)
#用綠線描繪hull包圍車輛外型的輪廓-car_contour3
hull = cv2.convexHull(cnt)
car_contour3 = cv2.drawContours(car_contour3, [hull], -1, (200,150,100), 2)
#如果車輛進入觸發區,就做什麼事
#if x<bX2 and x>bX1 and y<bY2 :
#car_num =car_num+1
#即時畫出車輛的方形外框
#car_contour4 = cv2.rectangle(car_contour4,(x,y),(x+w,y+h),(0,255,0),2) #draw car box contour
#將車輛方形外框剪裁入car1
#car1=road[y:y+h,x:x+w]
#cv2.imshow('car1',car1)
#print(car_num)
#把car1寫入檔案,檔名用車輛的計數
#cv2.imwrite("car/"+str(car_num)+".png",car1)
#用moment找出車輛質心 catteroid
M = cv2.moments(cnt)
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
#當車輛的位置進入範圍 >= endX and < startX1
if center[0] >= endX and center[0] < startX1:
#只要進到x1-x2的範圍,就加入posList 、allCarPos,並在pictag設定尚未拍照
if center[0] > startX2 :
allCarPos.append([(center[0],center[1])])
posList.append(len(allCarPos)-1)
pictag.append(0)
else:
#若posList不為空
#設定最小距離為一個很大的數值
minDist = 65535
#設定預測自己在allCarPos裡的k是-1
kPred = -1
#遍覽posList的數字k,這是車輛序號
for k in posList:
#用這數字到 allCarPos 去找最後一個座標
lastItem = allCarPos[k][len(allCarPos[k])-1]
#計算各allCarPos最後一個座標跟自己的距離
dist = math.sqrt((center[0]-lastItem[0])**2+(center[1]-lastItem[1])**2)
#如果跟自己的距離<最小距離而且位置是在現在位置的右邊
if dist < minDist and lastItem[0] > center[0] :
#就把自己距離設定成最小距離
minDist = dist
#把kPred設成現在的k
kPred = k
#當最小距離 < 特定數值(車子前後影格變少於某門檻值),確定就是這一台在前影格的位置
if minDist < deltaDist:
#計算速度
#距離/時間
# 距離=minDist/scale/1000 km
# 時間=1/fps/60/60
velocity = (minDist/scale/1000)/(1/fps/60/60)
velocity = int(velocity)
velocity = str(velocity)
#把自己的位置寫到allCarPos的kPred
allCarPos[kPred].append((center[0],center[1]))
#從上次位置到這次位置連線。用線相連
for i in range(1,len(allCarPos[kPred])):
cv2.line(car_contour3, allCarPos[kPred][i - 1], allCarPos[kPred][i], (0, 200, 105), 2)
#如果車輛進入觸發區,如果是,就拍一張影像
if x<bX2 and x>bX1 and y<bY2 :
#檢查在pictag的標籤是否Fale,代表尚未拍照
if pictag[kPred] == 0 :
#呈現速度
cv2.putText(car_contour3,velocity, (bX1, bY2), cv2.FONT_HERSHEY_SIMPLEX,3, (0, 0, 255), 3)
#即時畫出車輛的方形外框,代表被拍
car_contour3 = cv2.rectangle(car_contour3,(x,y),(x+w,y+h),(255,255,255),5)
#將車輛方形外框剪裁入car1
car1=road[y:y+h,x:x+w]
cv2.imshow('car1',car1)
#print(car_num)
#把car1寫入檔案,檔名用車輛的計數
cv2.imwrite("car/"+str(car_num)+".png",car1)
print("---")
print(car_num,":",velocity,"KM/H")
print("---")
#已拍照註記
pictag[kPred] = 1
#拍照序號+1
car_num =car_num+1
#自己的位置 < endX ,代表已經離開範圍了
if center[0] < endX:
#檢查posList是否為空 #若為空,代表自己已經不在車輛位置列表了,所以不做任何事情
#若不是空,代表還在位置列表中
if len(posList) > 0:
#若posList不為空
#設定最小距離為一個很大的數值
minDist = 65535
#設定預測自己在allCarPos裡的k是-1
kPred = -1
#遍覽posList的數字k
for k in posList:
#用這數字到 allCarPos 去找最後一個座標
lastItem = allCarPos[k][len(allCarPos[k])-1]
#計算跟自己的距離
dist = math.sqrt((center[0]-lastItem[0])**2+(center[1]-lastItem[1])**2)
#如果跟自己的距離<最小距離而且之前的位置是在現在位置的右邊
if dist < minDist and lastItem[0] > center[0] :
#就把自己距離設定成最小距離
minDist = dist
#把kPred設成現在的k
kPred = k
#當最小距離 < 特定數值(車子前後影格變動小於某個門檻值),就把自己位置寫入allCarPos
if minDist < deltaDist:
allCarPos[kPred].append((center[0],center[1]))
#把自己的位置kPred從posList中刪除,代表自己已經離開舞台
posList.remove(kPred)
if 0:
i=1
for row in allCarPos:
if len(row) > 5:
print(i,len(row))
i += 1
print("\n")
#cv2.imshow('road',road)
#cv2.imshow('fgmask',fgmask)
#cv2.imshow('car',car)
#cv2.imshow('road_white',whiteroad_car)
#cv2.imshow('car_contour1',car_contour1)
#cv2.imshow('car_contour2',car_contour2)
cv2.imshow('car_contour3',car_contour3)
#cv2.imwrite("road.png",car_contour3)
#cv2.imwrite("road"+str(stamp)+".png",car_contour3)
stamp += 1
#cv2.imshow('car_contour4',car_contour4)
#cv2.imshow('carBoundary',carBoundary)
#調整waitkey 控制播放速度
k = cv2.waitKey(5) & 0xff
if k == 27:
break
cap.release()
cv2.destroyAllWindows()
video_index = 0
videofiles = []
for dirPath, dirNames, fileNames in sorted(os.walk("/home/pancala/Desktop/webcam/2016Y12M21D08H")):
for f in sorted(fileNames):
inputFile=os.path.join(dirPath, f)
lastFile=os.path.splitext(f)[-1]
if lastFile==".mp4":
#file='output.mp4'
#把檔案路徑都放進videofiles的list
videofiles.append(inputFile)
#videofiles = [n for n in os.listdir('.') if n[0]=='c' and n[-4:]=='.mp4']
#videofiles = sorted(videofiles, key=lambda item: int( item.partition('.')[0][3:]))
carTracking(videofiles[0],video_index)