基於霍夫變換的視頻流車道線檢測

霍夫變換檢測直線原理

霍夫變換:用極座標系表示直線
在直角座標系(x,y)中的直線通常用:
y=kx+b y=k x+b
表示。k爲斜率,b爲截距。
而在霍夫變換中,使用極座標系(r,θ)來表示直線。
直角座標系中的直線與x軸和y軸圍成一個直角三角形,我們作出這個三角形的高(即從原點出發到直線的垂線),則高的長度就是r,r稱爲該直線到原點的距離;θ就是高與x軸的夾角,θ稱爲該直線的垂線與x軸的夾角。
對於每一條在直線座標系中的直線,我們都能將其轉化成極座標系(r,θ)中的一個點(r0,θ0)。
現在我們再從直角座標系中直線上的某一個點來考慮:
直角座標系上某一點(x,y)轉換成極座標時,有:
x=rcosθ,y=rsinθ x=r \cos \theta, y=r \sin \theta
r2=x2+y2 r^{2}=x^{2}+y^{2}
tanθ=yx \tan \theta=\frac{y}{x}
在直角座標系上表示某條經過(x,y)的直線可以寫成(假設(x,y)是原點出發到該直線的垂線與這條直線的交點):
y=kx+b y=k x+b
用極座標系來表示該直線(假設(x,y)是原點出發到該直線的垂線與這條直線的交點):
y=(cosθsinθ)x+(rsinθ) y=\left(-\frac{\cos \theta}{\sin \theta}\right) x+\left(\frac{r}{\sin \theta}\right)
那麼通過點(x,y)的直線可以在極座標系中統一定義爲:
r=xcosθ+ysinθ r=x \cos \theta+y \sin \theta
我們可以發現一個點經過極座標系變換後在極座標系中是一條曲線,而在直角座標系中在同一條直線上的多個點經過極座標系變換形成的多條曲線都會在極座標系中相交於某一點。
一條在直角座標系上的直線上的所有點經過極座標系變換後得到的曲線都會在極座標系中通過同一個點,這個點就是該直線轉換成極座標系中的點(r0,θ0)。
霍夫變換就是追蹤圖像中每一個像素點對應曲線間的交點。越多曲線交於一點也就意味着這個交點表示的直線由更多的點組成(越亮)。如果交於一點的曲線的數量超過了閾值,那麼可以認爲這個交點所代表的參數對在原圖像中爲一條直線。再用下面的公式反向計算直線即可。
y=(cosθsinθ)x+(rsinθ) y=\left(-\frac{\cos \theta}{\sin \theta}\right) x+\left(\frac{r}{\sin \theta}\right)

opencv中的霍夫變換函數

標準霍夫線變換

函數原型:
void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0)
函數參數:
image:邊緣檢測的輸出圖像。它應該是個灰度圖 (但事實上是個二值化圖),這是霍夫變換的輸入圖像。
lines:儲存着檢測到的直線的參數對(r,θ)的容器。這是最終輸出,是極座標系形式的直線。
rho:參數極徑,即以像素值爲單位的分辨率,我們使用1像素。
theta:參數極角,即以弧度爲單位的分辨率,我們使用1度(即CV_PI/180)。
threshold:要”檢測” 一條直線所需最少的的曲線交點個數。
srn and stn:如果srn和stn同時爲0,就表示使用經典的霍夫變換。否則,這兩個參數應該都爲正數。

使用HoughLines函數需要子集將極座標系形式的直線反轉換成直角座標系形式的直線。

概率霍夫線變換

這是執行起來效率更高的概率霍夫線變換,它輸出檢測到的直線的端點 。
函數原型:
void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold,double minLineLength=0, double maxLineGap=0)
函數參數:
image:必須是二值圖像,推薦使用canny邊緣檢測的結果圖像。
rho:線段以像素爲單位的距離精度,double類型的,推薦用1.0。
theta:線段以弧度爲單位的角度精度,推薦用numpy.pi/180。
threshod:累加平面的閾值參數,int類型,超過設定閾值才被檢測出線段,值越大,基本上意味着檢出的線段越長,檢出的線段個數越少。根據情況推薦先用100試試。
lines:儲存着檢測到的直線的端點的容器。
minLineLength:線段以像素爲單位的最小長度,根據應用場景設置。
maxLineGap:同一方向上兩條線段判定爲一條線段的最大允許間隔(斷裂),超過了設定值,則把兩條線段當成一條線段,值越大,允許線段上的斷裂越大,越有可能檢出潛在的直線段。

車道線檢測原理

彩色圖像轉化爲灰度圖像;
使用高斯濾波平滑圖像;
使用膨脹填充內部空洞,使車道線變粗和更加連貫;
使用canny算子提取圖片中物體邊緣;
截取感興趣區域(Region of Interest,ROI)比如截取一個三角形區域,遍歷每個像素點座標,如果發現當前點的座標不在三角區域內,則將該點像素值置爲0。然後只保留圖片的ROI區域中的物體邊緣;
使用霍夫變換來提取圖片ROI區域中的直線(段);
過濾掉一些角度不太符合的直線(段);
對剩餘直線(段)按斜率歸類爲左車道直線集合和右車道直線集合,然後求取其斜率和截距的平均值,計算出平均值直線的兩個端點,這樣我們就得到了1條左車道直線和1條右車道直線;
在全黑背景的圖片上單獨繪製車道線,如果兩條車道線都存在,則填充兩條車道線之間的區域;
在原始彩色圖像與我們剛繪製的車道線圖像進行比例融合。

該算法在路面車道標誌比較完整的情況下效果較好,且經過上述優化後在路面一般顛簸的情況下(比如高速路上過減速帶時)仍能準確識別車道線,但如果標誌不清晰或路面嚴重不平導致攝像頭畫面抖動非常厲害時,該算法的效果就會變差。此外,該算法對於轉彎車道的識別較差。

Python-opencv實現代碼

import numpy as np
import cv2
import time


def region_of_interest(img, vertices):
	"""
	生成ROI區域
	:param img: 原始圖像,是提取了物體邊緣的圖像
	:param vertices: 多邊形座標
	:return: 返回只保留了ROI區域內的物體邊緣的圖像
	"""
	# 生成和img大小一致的圖像矩陣,全部填充0(黑色)
	roi = np.zeros_like(img)
	# vertices即三角形區域頂點,填充三角形內部區域爲白色
	ignore_mask_color = 255
	# 填充函數,將vertices多邊形區域填充爲指定的灰度值
	cv2.fillPoly(roi, vertices, ignore_mask_color)
	# 顯示ROI區域
	# cv2.imshow("ROI", roi)
	# cv2.waitKey(0)
	# cv2.destroyAllWindows()
	# 兩張圖片上的像素進行與操作,ROI區域中已經填充白色,其他是黑色
	# img中是canny算子提取的物體邊緣是白色,其他區域是黑色
	# 黑色與黑色與運算還是黑色,白色與白色與運算還是白色,白色與黑色與運算是黑色
	# bitwise_and即兩張圖片上相同位置像素點上的灰度值進行與運算
	masked_image = cv2.bitwise_and(img, roi)
	# cv2.imshow("masked_image", masked_image)
	# cv2.waitKey(0)
	# cv2.destroyAllWindows()
	return masked_image


def bypass_angle_filter(lines, low_thres, high_thres):
	"""
	角度濾波器
	:param lines: 概率霍夫變換得到的直線的端點對集合
	:param low_thres:低閾值
	:param high_thres:高閾值
	:return:得到過濾後的直線端點對集合
	"""
	filtered_lines = []
	if lines is None:
		return filtered_lines
	for line in lines:
		for x1, y1, x2, y2 in line:
			# 過濾掉角度0或90度的直線
			if x1 == x2 or y1 == y2:
				continue
			# 保留角度在low_thres到high_thres之間的直線,角度按360度的標度來算
			angle = abs(np.arctan((y2 - y1) / (x2 - x1)) * 180 / np.pi)
			if low_thres < angle < high_thres:
				filtered_lines.append([[x1, y1, x2, y2]])
	return filtered_lines


def average_lines(lines, y_min, y_max):
	"""
	求霍夫變換得到的直線檢測出的線的平均值,以平均值作爲左右車道的線,這樣左右車道各只有一條線,左右車道根據直線斜率判斷
	m爲斜率,b爲截距,norm爲長度
	:param lines:概率霍夫變換得到的直線兩個端點
	:param y_min:
	:param y_max:
	:return:
	"""
	# 直線y=mx+b
	# 左右車道的候選直線存儲集合,以斜率/截距/線段長度形式分別存儲
	hough_pts = {'m_left': [], 'b_left': [], 'norm_left': [], 'm_right': [], 'b_right': [], 'norm_right': []}
	if lines is not None:
		for line in lines:
			for x1, y1, x2, y2 in line:
				# poly_coef保存了多項式係數,對於一次項,第0個元素是k,第1個元素是b
				poly_coef = np.polyfit([x2, x1], [y2, y1], 1)
				norm = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
				if poly_coef[0] > 0:
					# 如果是右車道線
					hough_pts['m_right'].append(poly_coef[0])
					hough_pts['b_right'].append(poly_coef[1])
					hough_pts['norm_right'].append(norm)
				if poly_coef[0] < 0:
					# 如果是左車道線
					hough_pts['m_left'].append(poly_coef[0])
					hough_pts['b_left'].append(poly_coef[1])
					hough_pts['norm_left'].append(norm)
	if len(hough_pts['b_left']) != 0 or len(hough_pts['m_left']) != 0 or len(hough_pts['norm_left']) != 0:
		# 如果集合中左車道直線集合中有直線
		b_avg_left = np.mean(np.array(hough_pts['b_left']))
		m_avg_left = np.mean(np.array(hough_pts['m_left']))
		xmin_left = int((y_min - b_avg_left) / m_avg_left)
		xmax_left = int((y_max - b_avg_left) / m_avg_left)
		left_lane = [[xmin_left, y_min, xmax_left, y_max]]
	else:
		left_lane = [[0, 0, 0, 0]]
	if len(hough_pts['b_right']) != 0 or len(hough_pts['m_right']) != 0 or len(hough_pts['norm_right']) != 0:
		# 如果集合中右車道直線集合中有直線
		b_avg_right = np.mean(np.array(hough_pts['b_right']))
		m_avg_right = np.mean(np.array(hough_pts['m_right']))
		xmin_right = int((y_min - b_avg_right) / m_avg_right)
		xmax_right = int((y_max - b_avg_right) / m_avg_right)
		right_lane = [[xmin_right, y_min, xmax_right, y_max]]
	else:
		right_lane = [[0, 0, 0, 0]]
	return [left_lane, right_lane]


def draw_lines(img, lines, color, thickness=2):
	"""
	最後處理好的左右車道線集合輸入,根據線兩點座標畫直線
	:param img: 輸入背景圖像(全黑)
	:param lines: 輸入左右車道線集合
	:param color: 指定直線顏色
	:param thickness: 指定直線寬度
	:return:
	"""
	for line in lines:
		for x1, y1, x2, y2 in line:
			cv2.line(img, (x1, y1), (x2, y2), color, thickness)
	# 如果左右兩條車道線都存在,那麼填充兩條車道線之間的區域(高亮)
	if lines[0] != [0, 0, 0, 0] and lines[1] != [0, 0, 0, 0]:
		lines_np = np.array(lines)
		lines_np_del = np.squeeze(lines_np)
		polys = []
		poly_1 = [(lines_np_del[0][0], lines_np_del[0][1]), (lines_np_del[0][2], lines_np_del[0][3]),
		          (lines_np_del[1][2], lines_np_del[1][3]), (lines_np_del[1][0], lines_np_del[1][1])]
		polys.append(poly_1)
		cv2.fillPoly(img, np.array(polys), (0, 255, 0))


def process_image(img):
	"""
	對輸入圖像進行灰度化、高斯平滑、canny算子提取物體邊緣、只保留ROI區域內物體邊緣、霍夫變換求取圖片內直線、將直線畫到原圖圖像上
	:param img: 原始彩色圖像
	:return: 標註了車道線的彩色圖像
	"""
	# 霍夫像素單位,取1
	rho = 1
	# 霍夫角度移動步長,使用1度
	theta = np.pi / 180
	# 霍夫平面累加閾值,即檢測一條直線所需最少的曲線交點個數
	hof_threshold = 20
	# 線段最小長度
	min_line_len = 30
	# 線段最大允許斷裂長度
	max_line_gap = 60
	# 高斯濾波核大小size
	kernel_size = 5
	# canny算子邊緣檢測低閾值
	canny_low_threshold = 75
	# canny算子邊緣檢測高閾值
	canny_high_threshold = canny_low_threshold * 3
	# 原圖像權重
	alpha = 1
	# 車道線圖像權重
	beta = 0.5
	gamma = 0.
	# 獲取圖像大小,注意shape[0]是高,shape[1]是寬
	img_shape = img.shape
	# print(img_shape)
	# 彩色圖轉換成灰度圖,使用cv2.imread()讀到的img的數據排列爲BGR,因此這裏的參數爲BGR2GRAY
	gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
	# 高斯濾波平滑圖像
	blur_gray = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
	# 平滑後的圖像做一下膨脹,使物體邊緣更容易檢測到
	dilate_gray = cv2.dilate(blur_gray, (3, 3), iterations=10)
	# Canny算子邊緣檢測
	edge_image = cv2.Canny(dilate_gray, canny_low_threshold, canny_high_threshold)
	# 顯示canny算子提取的邊緣
	# cv2.imshow("canny_edge_image", edge_image)
	# cv2.waitKey(0)
	# cv2.destroyAllWindows()
	# 確定ROI多邊形區域,兩層的np數組是因爲可以輸入多個區域,但我們這裏其實只有一個區域,內層每一個np數組記錄了一個多邊形的座標
	# shape[0]是高,shape[1]是寬,圖像座標系以左上角爲原點,向下爲y軸正向,向右爲x軸正向
	# 下面是一個三角形,底下兩點是圖像的左下角和右下角
	vertices = np.array([[(0, img_shape[0]), (img_shape[1] / 2, 23 * img_shape[0] / 40), (img_shape[1], img_shape[0])]],
	                    dtype=np.int32)
	masked_edges = region_of_interest(edge_image, vertices)
	# 基於概率霍夫變換的直線檢測
	lines = cv2.HoughLinesP(masked_edges, rho, theta, hof_threshold, np.array([]), minLineLength=min_line_len,
	                        maxLineGap=max_line_gap)
	# img: 輸入只保留了ROI區域內的物體邊緣的圖像
	# rho: 線段以像素爲單位的距離精度, 一般取1
	# theta: 像素以弧度爲單位的角度精度, 一般取1度(np.pi / 180)
	# threshold: 霍夫平面累加的閾值, 即檢測一條直線所需最少的的曲線交點個數
	# min_line_len: 線段最小長度(像素級)
	# max_line_gap: 最大允許斷裂長度
	# 返回檢測到的直線的兩個端點對的集合
	# 過濾掉一些角度不符合的直線
	low_thres, high_thres = 30, 80
	angle_filter_lines = bypass_angle_filter(lines, low_thres, high_thres)
	# 求取左右車道直線集合的平均值作爲左右車道線
	y_min, y_max = int(img.shape[0] * 0.65), int(img.shape[0] * 1.0)
	avg_hlines = average_lines(angle_filter_lines, y_min, y_max)
	# 創建一個新黑色背景圖片,在圖片上繪製檢測到的車道線
	lines_image = np.zeros_like(img)
	lines_color = [0, 0, 255]
	# 繪製車道線線段
	draw_lines(lines_image, avg_hlines, lines_color, thickness=15)
	# cv2.imshow("lines_image", lines_image)
	# cv2.waitKey(0)
	# cv2.destroyAllWindows()
	# 原彩色圖像與車道線圖像融合,alpha爲第一個數組的權重,beta爲第二個數組的權重,gamma爲一個加到權重總和上的標量值
	lines_edges = cv2.addWeighted(img, alpha, lines_image, beta, gamma)
	return lines_edges


if __name__ == "__main__":
	# 檢測視頻流中的車道線
	# ./test_image_and_video/1.avi
	cap = cv2.VideoCapture("./test_image_and_video/video_1.mp4")
	cap_width, cap_height = int(cap.get(3)), int(cap.get(4))
	fps = 0
	while cap.isOpened():
		return_value, frame = cap.read()
		if return_value == 0:
			break
		start_time = time.time()
		processed_image = process_image(frame)
		# cv2.waitKey(1)
		fps = int(1.0 / (time.time() - start_time))
		fps_str = "FPS: " + str(fps)
		cv2.putText(processed_image, text=fps_str, org=(cap_width - 80, 15), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
		            fontScale=0.50, color=(255, 0, 0), thickness=2)
		cv2.imshow("image", processed_image)
		# 按q可以退出
		if cv2.waitKey(1) & 0xFF == ord('q'):
			break
	cap.release()
	cv2.destroyAllWindows()

	# 檢測圖片中車道線
	image = cv2.imread("./test_image_and_video/0.jpg")
	line_image = process_image(image)
	cv2.imshow("image", line_image)
	cv2.waitKey(0)
	cv2.destroyAllWindows()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章