项目要求

给出一段停车场的视频,要求实时检测空停车位的数量及位置。

思路

  • 从这段视频中取出一帧图片。
  • 对图片进行处理,只保留图片中的有效信息(停车位)。
  • 将所有停车位取出来,分别保存为单独的.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之停车场车位识别

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐