项目要求
给出一段停车场的视频,要求实时检测空停车位的数量及位置。
思路
- 从这段视频中取出一帧图片。
- 对图片进行处理,只保留图片中的有效信息(停车位)。
- 将所有停车位取出来,分别保存为单独的.jpg文件作为样本(包含空停车位和非空停车位)。
- 训练二分类模型识别此停车位上是否有车。
- 将空停车位在图上标记出来。
- 在视频中实时监测并标记空停车位。
实现过程
1、引入需要的库
import cv2 as cv
import numpy as np
import operator
import pickle
from keras.models import load_model
2、定义画图函数
def cv_show(name, img):
cv.imshow(name, img)
cv.waitKey(0)
cv.destroyAllWindows()
3、读取从视频中截取的图像
image = cv.imread('./test_images/scene1410.jpg')
cv_show('src', image)
图像:
4、掩膜以去除背景
4.1 制作mask
lower = np.uint8([120, 120, 120])
upper = np.uint8([255, 255, 255])
# lower_red和高于upper_red的部分分别变成0,lower_red~upper_red之间的值变成255,相当于过滤背景
white_mask = cv.inRange(image, lower, upper)
cv_show('white_mask',white_mask)
mask:
4.2 与操作去除背景
white_yellow_image = cv.bitwise_and(image, image, mask = white_mask)
cv_show('white_yellow_image', white_yellow_image)
去除背景后的图片:
5、将得到的图片转换成灰度图
# 转灰度图
gray_image = cv.cvtColor(white_yellow_image, cv.COLOR_BGR2GRAY)
cv_show('gray_image', gray_image)
灰度图:
6、进行边缘检测
edge_image = cv.Canny(gray_image, 50, 200)
cv_show('edge_image', edge_image)
边缘图片:
7、手动选择停车场区域
因为我们感兴趣的区域只有停车场区域,所以我们要手动将此区域提取出来。
7.1 在停车场区域周围做几个标记来将此区域围起来
# 手动选择区域
row, col = image.shape[:2]
pt_1 = [col*0.05, row*0.90]
pt_2 = [col*0.05, row*0.70]
pt_3 = [col*0.30, row*0.55]
pt_4 = [col*0.6, row*0.15]
pt_5 = [col*0.90, row*0.15]
pt_6 = [col*0.90, row*0.90]
vertices = np.array([[pt_1, pt_2, pt_3, pt_4, pt_5, pt_6]], dtype=np.int32)
point_img = edge_image.copy()
point_img = cv.cvtColor(point_img, cv.COLOR_GRAY2RGB)
for point in vertices[0]:
cv.circle(point_img, (point[0], point[1]), 10, (0, 0, 255), 4)
cv_show('point_img', point_img)
结果为:
7.2 将这几个红色圆圈所包围的地方做一个mask
mask = np.zeros_like(edge_image)
cv.fillPoly(mask, vertices, 255)
cv_show('mask', mask)
mask:
7.3 过滤操作(删除居民楼等区域)
roi_image = cv.bitwise_and(edge_image, mask)
cv_show('roi_image', roi_image)
过滤完之后的图像:
8、霍夫变换检测直线
lines = cv.HoughLinesP(roi_image, rho=0.1, theta=np.pi/10, threshold=15, minLineLength=9, maxLineGap=4)
print(lines.shape)
结果为:(2196, 1, 4)。
可见,霍夫变换总共检测出了2196条直线,并输出了其两个端点的坐标[x1, y1, x2, y2]。
9、挑出符合要求的直线并画出
在2196条直线中,我们只需要标记停车位的直线,所以我们需要进一步筛选。
筛选的条件为:
- 该直线两端点在y方向上的距离不能过远。
- 该直线两端点在x方向上的距离应在一定范围以内。
# 挑出符合要求的直线并画出
line_image = np.copy(image)
cleaned = []
for line in lines:
x1, y1, x2, y2 = line[0]
if abs(y2-y1) <= 1 and abs(x2-x1) >=25 and abs(x2-x1) <= 55:
cleaned.append((x1, y1, x2, y2))
cv.line(line_image, (x1, y1), (x2, y2), [255, 0, 0], 2)
print("No lines detected: ", len(cleaned))
cv_show('line_image', line_image)
结果为:No lines detected: 559。
可见,通过筛选,有559条直线满足要求。
我们在这一步中获得的cleaned数组中包含着所有帅选过后的直线的两个端点的坐标(即[x1, y1, x2, y2])。
将符合要求的直线在原图上画出来:
10、按列划分区域
从照片上可以看出,一共有12列停车位,在这一步操作中,我们需要按列划分区域,将每一列停车位看作一个小的整体,然后用矩形将每一列停车位围起来。
10.1 将得到的直线cleaned进行排序
由于得到的列表中的直线没有顺序,所以要先进行排序,排序原则:x1越来越大,x1相同的情况下,y1越来越大。
rect_image = np.copy(image)
list1 = sorted(cleaned, key=operator.itemgetter(0, 1))
10.2 将在同一列的直线放到字典中的一个键中
判断两条直线是否是同一列的方法是:如果两条直线的左端点的横坐标x1相差小于20,则认为这两条直线处于同一列当中;否则它们属于不同的列。
clusters = {}
dIndex = 0
clus_dist = 20
for i in range(len(list1) - 1):
distance = abs(list1[i+1][0] - list1[i][0])
if distance <= clus_dist:
if not dIndex in clusters.keys():
clusters[dIndex] = []
clusters[dIndex].append(list1[i])
clusters[dIndex].append(list1[i + 1])
else:
dIndex += 1
得到含有键值为0-11的字典clusters。clusters中的每个键代表一列区域中的所有直线的端点坐标。
10.3 得到矩形坐标
- 因为提取出的直线中有一些是重复的,所以要先用set()函数将它们剔除掉,得到新的直线列表list2。另外,一列中至少有5条直线,所以一列中没有5条直线的被认为是错误的列。
- 对list2中的直线进行排序,排序原则:直线左端点的纵坐标越大,直线越靠后。从而可以得到一列中最前面和最后面的纵坐标,即矩形的上下两边界的纵坐标。
- 对于矩形左右两边界的横坐标,采用取此列中所有直线两端点横坐标平均值的方法获得。
rect_coord = {}
i = 0
for key in clusters:
all_list = clusters[key]
list2 = list(set(all_list))
if len(list2) > 5:
list2 = sorted(list2, key=lambda tup: tup[1])
avg_y1 = list2[0][1]
avg_y2 = list2[-1][1]
avg_x1 = 0
avg_x2 = 0
for tup in list2:
avg_x1 += tup[0]
avg_x2 += tup[2]
avg_x1 = avg_x1/len(list2)
avg_x2 = avg_x2/len(list2)
rect_coord[i] = (avg_x1, avg_y1, avg_x2, avg_y2)
i += 1
print("Num Parking Lanes: ", len(rect_coord))
buff = 7 # 微调
for key in rect_coord:
tup_topLeft = (int(rect_coord[key][0] - buff), int(rect_coord[key][1]))
tup_botRight = (int(rect_coord[key][2] + buff), int(rect_coord[key][3]))
cv.rectangle(rect_image, tup_topLeft,tup_botRight,(0,255,0),3)
cv_show('rect_image', rect_image)
得到结果:Num Parking Lanes: 12。表示划分出了12个列。
按列划分区域后的图片为:
11、在每个列区域中画出横线
由图知:
- 第一列和最后一列每行只有一个停车位。
- 其他列每行有两个停车位。
所以,不仅要在每一列中画出横线,还要在除第一列和最后一列的列区域中间画出竖线。
# 每个区域画出横线
delineated = np.copy(image)
gap = 15.5 # 同一列中相邻停车位之间的纵向距离
spot_pos = {}
tot_spots = 0
#微调
adj_y1 = {0: 20, 1:-10, 2:0, 3:-11, 4:28, 5:5, 6:-15, 7:-15, 8:-10, 9:-30, 10:9, 11:-32}
adj_y2 = {0: 30, 1: 50, 2:15, 3:10, 4:-15, 5:15, 6:15, 7:-20, 8:15, 9:15, 10:0, 11:30}
adj_x1 = {0: -8, 1:-15, 2:-15, 3:-15, 4:-15, 5:-15, 6:-15, 7:-15, 8:-10, 9:-10, 10:-10, 11:0}
adj_x2 = {0: 0, 1: 15, 2:15, 3:15, 4:15, 5:15, 6:15, 7:15, 8:10, 9:10, 10:10, 11:0}
for key in rect_coord:
tup = rect_coord[key]
x1 = int(tup[0] + adj_x1[key])
x2 = int(tup[2] + adj_x2[key])
y1 = int(tup[1] + adj_y1[key])
y2 = int(tup[3] + adj_y2[key])
cv.rectangle(delineated, (x1, y1), (x2, y2), (0, 255, 0), 2)
num_splits = int(abs(y2-y1) // gap)
for i in range(num_splits + 1):
y = int(y1 + i * gap)
cv.line(delineated, (x1, y), (x2, y), [255, 0, 0], 2)
if key > 0 and key < len(rect_coord) - 1:
x = int((x1 + x2) / 2)
cv.line(delineated, (x, y1), (x, y2), [255, 0, 0], 2)
if key == 0 or key == (len(rect_coord) - 1):
tot_spots += num_splits + 1
else:
tot_spots += 2 * (num_splits + 1)
if key == 0 or key == (len(rect_coord) - 1):
for i in range(num_splits + 1):
cur_len = len(spot_pos)
y = int(y1 + i * gap)
spot_pos[(x1, y, x2, y+gap)] = cur_len + 1
else:
for i in range(num_splits + 1):
cur_len = len(spot_pos)
y = int(y1 + i * gap)
x = int((x1 + x2) / 2)
spot_pos[(x1, y, x, y+gap)] = cur_len +1
spot_pos[(x, y, x2, y+gap)] = cur_len +2
print("total parking spaces: ", tot_spots, cur_len)
cv_show('delineated', delineated)
以上操作中得到的字典spot_pos的键值是每个小矩形(围着一个停车位)的左上角和右下角的坐标,它指向停车位的编号。例如:{(87, 519, 126, 534.5): 1}。
最终得到561个停车位。
得到将停车位划分出来的图像:
12、将得到的每个停车位的位置信息写入文件
即将spot_pos字典写入pickle文件。
with open('spot_dict.pickle', 'wb') as handle:
pickle.dump(spot_pos, handle, protocol=pickle.HIGHEST_PROTOCOL)
13、将得到的每个停车位裁剪下来做成样本
- 因为字典spot_pos的键值即为每个停车位矩形的左上角和右下角的坐标,所以直接将它们取出并在原图像上剪裁。
- 由于剪完的图像太小了,所以进行一步缩放操作。
- 将剪裁下来的561个停车位图片保存到文件夹中。
for spot in spot_pos:
(x1, y1, x2, y2) = spot
(x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
#裁剪
spot_img = image[y1:y2, x1:x2]
spot_img = cv.resize(spot_img, (0,0), fx=2.0, fy=2.0)
spot_id = spot_pos[spot]
filename = 'spot' + str(spot_id) +'.jpg'
print(spot_img.shape, filename, (x1,x2,y1,y2))
cv.imwrite(os.path.join('cnn_data', filename), spot_img)
得到的图片如图所示:
共有561个。
14、将已经训练好的模型引入
weights_path = 'car1.h5'
model = load_model(weights_path)
15、对图片进行测试,识别出空停车位位置及数量
class_dictionary = {}
class_dictionary[0] = 'empty'
class_dictionary[1] = 'occupied'
predicted_images = np.copy(image)
overlay = np.copy(image)
# cv_show('predicted_images', predicted_images)
cnt_empty = 0
all_spots = 0
for spot in spot_pos.keys():
all_spots += 1
(x1, y1, x2, y2) = spot
(x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
spot_img = image[y1:y2, x1:x2]
spot_img = cv.resize(spot_img, (48, 48))
#预处理
img = spot_img/255.
#转换成4D tensor
spot_img = np.expand_dims(img, axis=0)
# 用训练好的模型进行训练
class_predicted = model.predict(spot_img)
inID = np.argmax(class_predicted[0])
label = class_dictionary[inID]
if label == 'empty':
cv.rectangle(overlay, (int(x1),int(y1)), (int(x2),int(y2)), [0, 255, 0], -1)
cnt_empty += 1
alpha = 0.5
cv.addWeighted(overlay, alpha, predicted_images, 1 - alpha, 0, predicted_images)
cv.putText(predicted_images, "Available: %d spots" %cnt_empty, (30, 95),
cv.FONT_HERSHEY_SIMPLEX,
0.7, (255, 255, 255), 2)
cv.putText(predicted_images, "Total: %d spots" %all_spots, (30, 125),
cv.FONT_HERSHEY_SIMPLEX,
0.7, (255, 255, 255), 2)
cv_show('predicted_images', predicted_images)
得到检测结果:
16、对录像视频进行测试,实时识别出空停车位位置及数量
video_name = 'parking_video.mp4'
cap = cv.VideoCapture(video_name)
count = 0
while 1:
ret, image = cap.read()
count += 1
if count == 5:
count = 0
new_image = np.copy(image)
overlay = np.copy(image)
cnt_empty = 0
all_spots = 0
color = [0, 255, 0]
alpha=0.5
for spot in spot_pos.keys():
all_spots += 1
(x1, y1, x2, y2) = spot
(x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
spot_img = image[y1:y2, x1:x2]
spot_img = cv.resize(spot_img, (48,48))
#预处理
img = spot_img/255.
#转换成4D tensor
spot_img = np.expand_dims(img, axis=0)
# 用训练好的模型进行训练
class_predicted = model.predict(spot_img)
inID = np.argmax(class_predicted[0])
label = class_dictionary[inID]
if label == 'empty':
cv.rectangle(overlay, (int(x1),int(y1)), (int(x2),int(y2)), color, -1)
cnt_empty += 1
cv.addWeighted(overlay, alpha, new_image, 1 - alpha, 0, new_image)
cv.putText(new_image, "Available: %d spots" %cnt_empty, (30, 95),
cv.FONT_HERSHEY_SIMPLEX,
0.7, (255, 255, 255), 2)
cv.putText(new_image, "Total: %d spots" %all_spots, (30, 125),
cv.FONT_HERSHEY_SIMPLEX,
0.7, (255, 255, 255), 2)
cv.imshow('frame', new_image)
if cv.waitKey(10) & 0xFF == ord('q'):
break
cv.destroyAllWindows()
cap.release()
PS:
需要源码的小伙伴请戳:Opencv之停车场车位识别