目录
3.2.1 select_rgb_white_yellow()
1 项目介绍
我现在有这样一个停车场的航拍视频parking_video,mp4

处理视频与处理视频一样,就是把视频的每一帧抽出来然后处理图像
我们现在做三个事情
- 停车场一共有多少车位被占据
- 停车场一共有多少车位没被占据
- 标识出没有被占据的停车位
结果是这样的,我们可以看到也不是特别准,这个是因为我数据量不够,如果增加数据量会有更好的效果

由于我们要处理视频,这个项目对计算机的算力是有要求的,像我这个机器是NVIDIA GeForce GTX 970M,是无法让这个视频实时判定的(会卡,但是如果够耐心也可以走完)

我们这个项目一共有三个py文件
train.py会训练出模型carl.h5供park_test使用
Parking中定义了一个工具类,其中有若干种方法供park_test使用

2 展示图像
2.1 park_test
2.1.1 导入库

Parking是上面提到的提供方法的Py文件,pickle是存储文件用的,这个用的时候再提
之后我们就从main来分析
2.1.2 读取测试数据
![]()
我们的测试数据一共有两张图片

2.1.3 读取模型与视频

- 我们此时还没有模型,下面会进行训练,但是不影响在这里定义,因为weights_path现在只是一个字符串
2.1.4 定义分类

我们的分类是字典形式的
![]()
2.1.5 show_images()
实例化工具类后调用工具类种的show_images()方法

2.2 Parking
2.2.1 导入库

2.2.2 show_images()


定义工具类后定义show_images函数,参数为输入的图像与camp,camp是颜色映射,在这里我们没有使用,我们在主函数中只传入了两张图像

之后我们确认列与行,cols是列,row是行,由于我们只有两张图,计算出来是1行2列

将图片大小改变为15*12
![]()
对两张图片进行枚举,之后放在指定的位置
![]()
- enumerate的序号是从0开始的,需要+1才能放到指定的位置上,我举一个例子plt.subplot(1,2,1),这个的意思是按照1行2列排好,然后将该图放到1的位置上
进行判定,如果图像的shape长度为2(图像是灰度图),那么camp置为gray,否则保持不变,我们这里图像不是灰度图,所以camp还是None,我们print出来看一下

由于是两张图所以会出现两个None
![]()
将图绘制出来,不设置横纵坐标

把图像拉大填充至整个图像区域,pad是图像距离图像区域两侧的边距,h_pad是填充后图像之间的纵向间距,w_pad是填充后图像之间的横向间距,拉大后展示出来

如果不加入tight_layout的话会是这样

加入后是这样

3 图像预处理
3.1 park_test
之后回到主函数

3.1.1 img_process()
这个是我们主函数的方法

在img_process()中的第一步调用了park中的select_rgb_white_yellow的方法
3.2 Parking
3.2.1 select_rgb_white_yellow()

首先定义lower和upper两个阈值
![]()
之后我们使用inRange函数,inRange会筛选掉我们不像要的颜色,如果低于低阈值则将像素点值置为0,如果高于高阈值则将像素点置为0,在两阈值之间的值置为255,参数如下
- image 要处理的图像
- lower 低阈值
- upper 高阈值
![]()
- 这个函数会对三个通道分别进行处理
image这个参数是我们从map这个方法传入进来的
![]()
我们的white_mask是这样的


之后我们使用bitwise_and,这个是图像的与运算,如果对两张相同的图像做与运算,图像还是其本身,我们现在先看没使用mask的情况
![]()

- 后面的所有步骤我就只展示第一张图了
我们比较关注的点应该是图像中有一部分变为青色了,这是因为我们读取图像时用的是plt进行读取,plt与cv2的通道顺序不同,所以会变为青色,这个不重要,一会儿会进行二值处理,都会变成黑色或白色
现在我们再看一下带mask的,mask会保留图像中白色的区域,然后将黑色的区域置为黑色


得到返回值masked后我们回到 img_process()
3.3 park_test
3.3.1 img_process()
我们把得到的两个结果做成一个集合,然后一起展示一下
![]()

之后将上一步的结果使用park中的convert_gray_scale处理
![]()
这个实际上就是将图转为灰度图,我就不单开一个标题来写了
![]()
转换完之后我们展示一下


到上面这一步就消除了plt与cv2通道不同的问题,现在都变成白色了
下面我们把刚刚的结果传递给park中的detect_edges()
![]()
detect_edges实际上是用Canny做了边缘检测,阈值为(50,200)
![]()
x现在我们的edge_images是这样的

之后我们使用park中的select_region操作上面的结果
![]()
3.4 Parking
3.4.1 select_region()
这一步的功能是在指定的点上画圈

首先我们获取图片的宽与高,这里面用的变量名是rows和cols(行与列)后面赋值的内容是shape,所以为宽与高
![]()
画出图上的六个点

将六个点做成点集
![]()
将传入的图片复制一张,之后将其由灰度图转为RGB图
![]()
由彩色图转为灰度图后,再使用灰度图转到彩色图,图像不会变成彩色的,转前与转后的区别的通道的变化


画圈
![]()
展示
![]()

之后会返回我们工具类中的另一个方法filter_region()
![]()
3.4.2 filter_region()

首先我们看传入的参数
- image 图像
- vertices 点集
之后我们搞一个与图像大小相同的全0的numpy.ndarry
![]()
我们看一下mask的shape

![]()
之后进入判定,如果mask的shape的长度为2那么进入分支,进入分支后使用cv2.fillPoly为图像绘制有填充的多边形,参数
- mask 要绘制的图像
- vertices 点集
- 255 要填充的颜色(白色)
绘制完之后使用cv_show()展示出来

我们现在的mask是这样的

最后返回图像与mask的与运算

白色(255)与其他值做与运算结果为其他值,黑色(0)与其他值做与运算结果为黑色,我们下面做两个例子
- 白色(255)与100与运算
11111111 = 255
01100100 = 100
01100100 = 100
- 黑色(0)与100与运算
00000000 = 0
01100100 = 100
00000000 = 0
也就是说现在我们return的图像,除了在mask中白色的部分有内容,其余部分全部为黑色

由于我们只关注白色车位的区域,别的地方我们不关注,所以就过滤掉

3.4.3 select_region()
filter_region获得返回值后,select_region也走到了最后一步,select_region的返回值就是filter_region的返回值

3.5 park_test()
3.5.1 img_process()
我们看一下返回值的样子


之后我们对上面的结果使用工具类中的hough_lines方法
![]()
这个方法使用的是cv2.HoughLinesP这个方法,这个方法是专门找图像中的直线的,涉及到的参数如下
- image 要找直线的图像
- rho 距离精度
- theta 角度精度
- threshod 阈值,图像上更多的是看似直线的曲线,现在我们有若干个点,我们拟合成一条直线,拟合后的直线还能连到阈值个数的点我们就认为这条线是一条直线,这个阈值指定的越大,我们得到的线就会越少
- minLineLength 线的最短长度,比这个短的都会被忽略
- MaxLineCap 两条直线之间的最大间隔,小于此值系统会判定两条临近的直线为一条直线
这个方法的使用详情 python opencv检测直线 cv2.HoughLinesP_Snoopy_Dream-CSDN博客_cv2.houghlinesp
- HoughLines与HoughLinesP的用法一样,HoughLinesP的运算速度比HoughLines更快

我们执行完这个方法后会得到两组直线(一张图一组)

我们创建一个line_images的空列表,由于我们是两张图,每一张图有对应的一组线,我们把他们压到一起然后分开遍历,之后调用park的draw_lines方法

3.6 Parking
3.6.1 draw_lines()

首先看参数
- image 传入的单张图像
- lines 图像对应的单组直线
- color=[255,0,0] 颜色默认为红色
- thickness=2 线宽为2
- make_copy 是否复制,默认为True
我们在主函数中没有复制make_copy,所以默认为True,我们复制了一张image图像

然后创建一个空的列表clean
![]()
之后进入判断,首先遍历一组线获得每条线,之后对每条线进行遍历,每条线的信息有(x1,y1)与(x2,y2)(起始点与终止点),我们提取出x1,y1,x2,y2,之后进入判断
- 条件1 终止点y值与起始点y值的距离(起始点与终止点的纵向距离) <= 1,这个条件是保证如果是横线不会太斜,筛选掉几乎所有的纵向线
- 条件2 25 <= 起始点与终止点的横向距离 <=50
如果满足以上两个条件,将线的信息进入到cleaned中并将其连成一条线画出来

- abs()会返回运算的绝对值
之后打印过滤过后线的数量,之后返回画好线的图像

3.7 park_test
3.7.1 img_process()
之后回到park_test,我画红框的地方是一张画好线的图像,我们把这个图像添加到line_images中,之后展示出来


我们第一章图检测出来了588条线,第二章图检测出来了581条线

之后我们创建rect_images与rect_coords两个空列表
![]()
之后遍历两张图像与对应的线集,然后将元素传入park.identify_blocks()

3.8 Parking
3.8.1 identify_blocks()
这个太长了,我们就不截取全部了,先看传入的参数,图像与线集,make_copy默认为True,进入方法后先把图片复制下来

3.8.1.1 过滤部分直线
创建一个空列表cleaned,然后遍历线集中的每条线,遍历每条线中的两个点,之后进入判定
- 条件1 终止点y值与起始点y值的距离(起始点与终止点的纵向距离) <= 1,这个条件是保证如果是横线不会太斜,筛选掉几乎所有的纵向线
- 条件2 25 <= 起始点与终止点的横向距离 <=50

这一步和上面操作一样,只是因为我们过滤过的线没有返回,所以在这里要再做一遍,我们正好在这里看一下clean是什么样子的
我截取了头和尾,我们可以看到总体是一个列表,列表中有若干元组


3.8.1.2 根据起始点的坐标进行排序

operator是python内置的库,我模拟一下是如何排列cleaned的
import operator
a = [(744, 337, 794, 337), (744, 291, 700, 291), (358, 540, 387, 540)]
b = operator.itemgetter(0,1)
list1 = sorted(a,key=b)
print(list1)![]()
如果我们在itemgetter中只写0,那么只对x1进行排序,如果相等就随机,如果写itemgetter(0,1),那么当x1相同时会按照y1的大小排列

首先我们创建空字典clusters,然后定义dIndex与clus_dist

之后我们进入循环,循环次数为排序好的线集长度-1,,之后获取两个线的横坐标距离,如果获取的距离小于等于clus_dist(10)则进入分支
- if 分支: 如果clusters这个字典中没有key为dIndex的键值对,则创建一个以dIndex为键的键值对,键值对内容为空的列表,然后在列表中添加比较横坐标距离的两条直线
- else分支: dIndex + 1
上面这个分支走完会进行下一轮循环,clusters的一个key中会找出横向距离少于10的所有临近线

我们看一下此时的clusters

下面这个图只是一部分

我们这一步的目的是找列,所以我们关注的是键而不是值

之后创建一个rect的空字典,赋值i为0

之后遍历clusters,clusters是上面创建的字典,键为dIndex,值为一个列表,列表内容为比较横坐标的两条线

第一步我们提取clusters的key号键的值赋值为all_list,之后我们使用set去掉列表中重复的内容,然后使用list返回一个新的列表,我们看一下all_list与cleaned

下面这个是循环第一轮的结果

cleaned的结果意思是我一列中包含的横线的内容,之后对cleaned进行判断
如果我一列中超过了5条横线,那么进入分支,进入分支后以横线起始点的y值大小对cleaned进行排序,排序之后我们选择最小的y值赋值为avg_y1,最大的y值赋值为avg_y2,之后定义avg_x1与avg_x2都为0,之后遍历cleaned,将起始点的所有x值加和赋值为avg_x1,将终止点的所有x值加和赋值为avg_x2,之后将起始点的平均x值赋值给avg_x1,终止点的平均x值赋值给avg_x2,然后把(平均起始点x值,最小y值,平均终止点x值,最大y值)做成一个元组然后赋值给rects的i这个键,然后i自加

- 我们如果连接(avg_x1,avg_y1)(avg_x2,avg_y2)这两个点,这条线是一条斜线
我们此时的i就是len(rects)就是图像中的列数,我们看一下结果
![]()
两张图的结果都是12

3.8.1.5 画出列矩形
这个buff是根据图像做出的微调,我们刚刚获取到了两个点,现在我们对x进行微调后,画出列矩形,然后返回绘制好矩形的new_image与包含矩形左上角点与右下角点的字段rects

到这我们工具类中的identfy_blocks()就结束了,我们返回park_test看一下结果
3.9 park_test
3.9.1 img_process()
将获得到的返回值分别放入rect_images与rect_coords的空列表中
- 因为是两张图,所以要放在列表中

之后展示图像


我们继续向下看

创建delineated与spot_pos两个空列表

遍历test_images(原图)与rect_coords(矩形角点字典),进入循环后调用工具类中的darw_parking方法

3.10 Parking
3.10.1 draw_parking()
这个也很长,不展示了,先看传入的参数
- image 原图
- rects 矩形角点字典
- make_copy 是否赋值
- color 颜色
- thickness 线宽
- save 是否保存
![]()
进入方法后复制一张原图

定义gap,spot_dict,tot_spots三个值,后面会用到gap是每一列中横线的间距

一共是12列,对应12个键,我们对每个点的位置做微调,后面会调现在还没调

遍历矩形角点,对每个角点进行微调

把微调后的新矩形画出来
![]()
我们取y值的距离,然后整除15.5,之后将这个值变为整形赋给num_splits,这个值是每列的行数
![]()
之后循环行数次,y1是微调后起始点的y值,我们将这个值加上i个间距,然后赋值给y,然后在(起始点x,y),(终止点x,y)画一条横线

这样我们就会画出这样的横线,每一条横向的长度相同,每一条横线相互平行

之后进入判定,如果key >0且key<rects长度-1,这个的意思就是不算第0条线,其余全算,过滤之后平均起始点与终止点的x,然后画竖线

之后再进入一个判定,这次是rects中的第0列和最后一列,第0列与最后一列的车位我们算作 行数+1,其余的我们算作 2倍的(行数+1),因为其余列一行能放两辆车,然后将每一行的车位数都加在一起得到tot_spot(总车位数)

- 这里注意我们是顶着上面for循环的,我们每一轮的num_splits获取的值是不一样的
之后我们进入判定

- 如果当前是第0列或最后一列,我们进入循环,循环次数为每一列的行数+1,我们获取spot_dict,这个现在还是空字典,然后我们将起始点y+行数*间距,然后将spot_dict的值置为(起始点x值,y,终止点x值,y+gap),键位cur_len + 1

现在这个点的位置实际上是这样的,这一步是确定每个车位的角点

- 其余列的处理与上面的处理相似,我就简单画一下每个点的位置


上面这四个点是一行两个车位的角点
之后打印出全部车位与当前车位,这个实际上两个值应该是相似的
![]()
如果保存(默认为保存),我们会保存new_image名为with_parking.jpg,然后返回new_image(画好线的图片)与spot_dict(车位角点的字典)

到此我们draw_parking()结束,现在返回park_test
3.11 park_test
3.11.1 img_process()

将上面两个返回值分别保存起来,然后我们看一下两张画好线的图片

之后我们提取第二张图的车位角点字典,然后查看它的长度(一共有多少个车位)

![]()
- 提取第二张图的原因是第二张图画的好,如果还在这个位置拍摄停车场,我们认定有555个车位
之后我们使用pickle.dump把车位角点信息存储起来,这样我们下次读取车位信息我们直接读这个文件就行了,而不用再进行一次图像预处理

我们看一下pickle.dump的参数
- final_spot_dict 要存储的内容
- handle 要存储到的文件
- protocol 存储的协议,这个是可选参数,我们就用这个就完了,在读数据的时候保持和写数据时相同就可以了
之后我们使用park中的save_images_for_cnn()
![]()
4 获取数据
4.1 Parking
4.1.1 save_images_for_cnn()
我们先看传入的参数
- image 第1张图像
- spot_dict 车位角点字典
- folder_name 文件夹目录
进入方法后,遍历车位角点集的值,然后提取出每个值赋值到(x1,y1,x2,y2)元组中,如果x1,y1,x2,y2中有浮点数对其取整,之后对图片进行裁剪,把车位的图像菜蔬来,然后放大两倍,把值对应的键赋值给spot_id,然后确认文件名称,之后打印出来,然后存储

我们在cnn_data这个文件夹中会保存555张车位的图片

这个函数没有返回值,调用完之后我们回到park_test
4.2 park_test
4.2.1 img_process()

执行完save_images_for_cnn()后返回final_spot_dict(车位点集),至此img_process()结束
5 训练模型代码 train.py
这个属于人工智能范畴,在我的另一个专栏中会有介绍
https://blog.csdn.net/potato123232/category_11202555.html
在本项目中这一步的结果是会训练出carl.h5这个模型
5.1 导入库

numpy是我测试时用的,os是python自带的搞路径的库,其余都是keras这个库中的方法
我当前keras版本为2.4.3,使用pip install keras就可以安装了

tensorflow中也有keras这个库,如果有tensorflow我们在代码的最前面加上
import tensorflow.keras as keras
就可以使用keras了
如果这样做keras这个库是继承tensorflow的,我当前的tensorflow的版本为2.3.0
![]()
tensorflow-gpu的windows下的安装方式
1.安装tensorflow_potato123232的博客-CSDN博客
如果是linux或者mac与windows的安装方式相似
如果设备中没有GPU(显卡),我们也可以使用tensorflow的cpu,cpu不需要安装显卡驱动,cuda,cudnn
5.2 定义两个变量
![]()
- file_train 训练集数量
- files_validation 验证集数量
5.3 确定训练集图片数量与测试集数量
数据都是从上面获取的数据中挑出来的
5.3.1 训练集
我们在train_data/train中有两个文件夹

empty是没被占用的停车位图片

occupied是被占用的停车位图片

此时我们指定路径为train_data/train,然后使用os.listdir获取empty与occupied这两个文件夹,之后分别遍历两个文件夹下的所有文件,每遍历到一个文件files_train就+1

- os.walk()会返回一个迭代器,使用next遍历os.walk的结果
5.3.2 测试集

测试集中也有empty和occupied两个文件夹,empty内容为空闲的车位照片,occupied为占用的车位照片,测试集的照片与训练集的照片不相同
用同样的方法确认测试集数量

5.3.3 显示训练集与测试集照片个数
![]()
![]()
5.4 定义训练所需参数

- img_width 图像宽度
- img_height 图像高度
- train_data_dir 训练数据路径
- validation_data_dir 测试(验证)数据路径
- nb_train_samples 训练数据个数
- nb_validation_samples 测试(验证)数据个数
- batch_size 一次放多少数据
- epochs 训练轮数
- num_classes 训练类别
5.5 使用VGG模型

到这一步会自动下载一个文件,如果有请求失败多试几次就可以了
5.6 减少层
选中前10层,前10层不参与训练
由于我们使用的是预训练模型,我们想让模型简单一点,所以减少10层
![]()
5.7 改变输出层
我们将模型的输出层提取出来定义为x,然后我们查看一下x的情况

![]()
之后我们将输出层定义为Flatten

![]()
最后我们更改其为2分类,激活函数为softmax的dense层作为输出层

5.8 新建神经网络
我们之后的神经网络就为model_final,我们下面训练也是使用model_final进行训练的,我们现在看一下这个网络的概况
![]()


5.9 编译神经网络

- loss 损失值
- optimizer 优化器
- lr 学习速率
- momentum 动量值
- metrics 指标
5.10 图像增强
首先是训练数据增强

- rescale 归一化,因为图像的值区间是[0,255]所以一般就为1/255
- horizontal_flip 是否水平翻转
- fill_mode 填充模式
- zoom_range 缩放比例
- width_shift_range 水平方向平移幅度
- height_shift_range 垂直方向平移幅度
- rotation_range 数据提升时图片随机转动的角度
详情可见 tensorflow中图像增强的方法详解 - 帅帅的飞猪 - 博客园
然后是测试数据增强

5.11 数据集合
训练集

- target_size 目标尺寸
- batch_size 一次传多少数据
- class_mode 这个我们现在设置为分类
测试集

5.12 定义训练节点

- 模型名称carl.h5
- monitor 监控指标
- verbose 信息展示模式
- save_best_onle 是否只储存最好的
- save_weights_only 是否只储存权重
- mode 评判准则
- period 周期,每几个epoch存一次
详情参考 ModelCheckpoint详解_you 是 mine-CSDN博客
5.13 定义提前中止该轮训练

- monitor 监测数据
- min_delta 增幅或减小阈值
- patience 容忍程度
- verbose 信息展示模式
- mode 评判准则
详情参考 [深度学习] keras的EarlyStopping使用与技巧_小墨鱼的专栏-CSDN博客_earlystopping
5.14 训练

5.15 保存
![]()
此时我们就得到了 carl.h5 这个模型文件,之后我们会调用它,下面我们就按主函数park_test执行的顺序来分析了
6 读取模型
6.1 Park_test
6.1.1 keras_model()
我们现在回到main中执行keras_model


![]()
这个就是使用keras方法读取我们训练好的模型
7 图像预测
7.1 Park_test
7.1.1 img_test()
现在我们就要使用模型对图片进行预测了,参数都为上面的返回值

由于要对两张图片分别进行预测,我们我们搞一个for循环

进入循环后使用工具类中的predict_on_image(),与上面参数不同的是predict_on_image()只接收单张图像
7.2 Parking
7.2.1 predict_on_image()
这个也比较长,不整体展示了
先看参数
- image 单张图像
- spot_dict 车位点集
- model 模型
- class_dictionary 分类
- make_copy 是否复制
- color 颜色
- alpha 权重,下面做图像融合用的
![]()
进入方法后先复制两张图片

这两张内容相同,地址不同
定义两个整形变量,初始值为0

之后遍历点集的所有值,进入循环后all_spots自加,然后提出每个点的内容,之后取整,然后裁剪,之后把照片大小变为48*48,然后调用自身的make_prediction()

7.2.2 make_prediction()

进入方法后对图像进行归一化,然后使用np.expand_dims扩展数组形状,只有对图像及逆行扩展我们才能使用模型进行预测,我们下面做一个np.expand_dims的例子
import numpy as np
a = np.array([[1,2],[3,4]])
print(a)
print(a.shape)
print(type(a))
print()
c = np.expand_dims(a,axis=0)
print(c)
print(c.shape)
print(type(c))
print()
c = np.expand_dims(a,axis=1)
print(c)
print(c.shape)
print(type(c))
如果axis=0,那么我在0的位置上加一个维度,如果axis=1,那么我就在1的位置上加一个维度,也可以往后加数,同理
项目中我们使用np.expand_dims就是在0的位置给图像加上一个维度
![]()
之后我们就对增维的图像做预测了,我们把每一步的结果显示出来

由于我们是二分类问题,所以我们会获得两个值,第一个值是这个图是空着的概率,第二个值是这个值被被占用的概率,我们取这两个值的对大值的索引,然后把这个索引套到class_dictionary,就会的出来我们相应的标签,然后返回这个标签

7.2.3 predict_on_image()
此时我们回到predict_on_image(),现在我们获取了每个车位的情况(label)
如果我们识别到这个车位是空的,就在overlay这张图上画上空车位对应的矩形框,然后cnt_empty(空车位数量)+1

之后我们将画框的和没画框的图融合一下,比例0.5,0.6
![]()
然后把字写上,分别是可用的车位(空车位数量)与全部车位数量

之后我们在这将是否保存设置为不保存了,但是无论是否保存都会展示并返回已经画完框,写完字的图片

到此我们的predict_on_image()就结束了,我们看一下效果

8 视频预测
8.1 Park_test
8.1.1 video_test()
使用video_test方法,与图片预测的方法参数不同的是把图片换成了视频
![]()

8.2 Parking
8.2.1 predict_on_video()
这个和预测图片相似,我们简单过一下
确认视频,定义参数

大循环,读图像,定义参数,复制图像

对每一个车位进行判定

- 这个也正是我们卡的原因,我们算1s60帧,一共555个车位,也就是说我们1s要预测33,300张图片
融合图像,写字,关视频

9 项目总结
9.1 流程图
我们就以检测图像做图了,因为视频与图像处理方式相同,我们花时间最多的是图像预处理,我们梳理一下图像预处理中每个函数的功能

9.2 新了解的方法
- pickle 一共两个方法
- pickle.dump() 存储数据
- pickle.load() 读取数据,这个我们在这个项目中没用到
- np.expand_dims 扩展数组维度