
我们将要分析的停车场 | 来源
这个项目是The Deep Hub系列的开始,我们将在其中创建端到端的机器学习项目。在这里查看。
目录
- 分析问题
- 掩盖基础知识
- 用面具分割图像
- 训练卷积神经网络
- 将模型与OpenCV集成
停车槽检测器的想法并不新鲜。我信任你至少见过一次停车场,停车场有一个柜台,可以跟踪其中可用免费插槽的数量。
传统方法主要基于手动系统,包括在每个停车场放置移动传感器以检测占用情况。
这个解决方案很有效,但超级昂贵,而且需要大量维护;想象一下,必须安装1000个移动传感器,每个停车位一个……
我们如何通过机器学习来改善这一点?
使用计算机视觉,方法是将一个摄像头(如果需要,可以放置更多)放在一个可以记录停车场所有部件的战略位置。
相机将能够检测机器学习模型占用的插槽。一个更有效的解决方案,由于我们只需要安装摄像头,而不是放置200个移动传感器。

停车槽探测器 | 来源
分析问题
与其他机器学习案例一样,我们应该问自己的第一件事是,我们是否真的需要机器学习方法来解决问题。
在这种情况下,我们有各种选择。让我们把它们都穿过去。
对象检测模型
一种流行的方法是使用对象检测模型,如YOLO(你只看一次)或R-CNN(基于区域的卷积神经网络),我们可以训练它来预测插槽的位置,并分类它们是空的还是占用的。

对象检测模型示例 | 来源
我们需要什么来训练物体检测模型?
为了训练这些神经网络之一,我们需要大量的数据,包括带有其位置和分类注释的图像。
(例如,在上图中,算法正在预测对象的尺寸及其所属的类)。
一般,这些特殊数据集存储在YOLO、COCO(上下文中的常见对象)或Pascal VOC(视觉对象类)格式中,这些格式存储对象类的标签及其在图像中的空间位置。
为这项任务做准备需要几个步骤:
- 数据收集:在各种条件下(例如,一天的不同时间和天气条件)收集一组多样化的停车场图像。
- 注释:在每个停车位周围用准确的边界框标记图像,并将每个图像归类为“占用”或“空”。
- 数据集准备:以YOLO、COCO或Pascal VOC等结构化格式组织注释数据。

边界框格式:COCO vs YOLO | 来源
对象检测模型可能不是最佳选择…
训练对象检测模型的方法是可以的,但在这种情况下,这可能不是最好的做法。
想想看,当我们必须预测物体的位置时,一般会使用YOLO,但在这种情况下,我们不必这样做。
停车位总是在同一个地方!
这意味着我们已经完成了一半的工作,我们只需要对每个停车位进行分类,以确定它们是否被占用。
训练对象检测模型很难,由于您必须创建带注释的数据集,包括每个对象的坐标和类别。
当我们必须预测物体的位置或其尺寸时,这很有用。不过,在这种情况下,这没有什么意义,由于停车场总是在同一个地方,而且总是有一样的尺寸。

相机不动,停车位总是在同一个地方 | 来源
太棒了!但即使我们知道插槽的位置。我们如何将它们各自的位置和尺寸传递给一个将它们归类为已占用或未占用的模型?
项目的第一部分来了。制作一个面具!
掩盖基础知识
屏蔽是一种用于隔离图像或视频特定部分的技术,以便可以单独在这些区域进行操作。
这就像给计算机一种专注于特定对象而忽略其余对象的方法。

二进制与RGB掩码 | 来源
在这个图中,创建了一个面具,用二进制和RGB面具隔离图片中的长颈鹿。
在我们的案例中,模型不需要有关颜色的信息来识别停车位是否被占用,因此我们将创建一个二进制掩码。
制作面具
为了制作口罩,我们有各种选择。也许最受欢迎和最自动的是OpenCV库。
不过,在这种情况下,我们希望创建一个更定制的口罩,覆盖停车场的每个插槽。我们可以尝试用OpenCV做到这一点,但是,它可能太复杂了,所以我会手动构建它。
您可以使用一些可以创建掩码的流行工具是:
- Adobe Photoshop
- Pixlr
- 素描
- Corel PHOTO-PAINT
(如果您想推荐任何其他提议,您可以在评论中留下提议)。
为了简单起见,并且不要过多地扩展这个解释,我从YouTube频道计算机视觉工程师那里挑选了一个面具,他使用Inkscape为这个特定视频文件构建了一个定制的面具。
你可以看看他是如何在他的Patreon中制作面具的。


原始图像和面具 | 来源
如上图所示,口罩只突出显示停车位,不思考其余部分。
方法是选择每个插槽,并将其传递给我们的模型,该模型将对它们是否被占用进行分类。
让我们看看如何将我们的面具与原始视频文件相结合。
用面具分割图像
正如您稍后将看到的,这是一个超级简单的操作,OpenCV将使我们更容易。
让我们通过一些编码吧!
您必须导入口罩和原始视频。你可以在这里下载它们。
如果您不熟悉OpenCV,您可以查看他们的文档。无论如何,我们不会创建一个超级复杂的程序,所以你可以毫无问题地遵循它。
导入cv2
mask_image_path = r"path oyourmask"
video_path = r"path oyourvideo"
video_capture = cv2。VideoCapture(video_path)
mask = cv2.imread(mask_image_path,cv2。IMREAD_GRAYSCALE)
太棒了!目前我们已经加载了文件,让我们看看如何将它们连接在一起。
在我们深入研究之前,让我们澄清一下关于视频文件的一件事:
– 视频本质上是按顺序链接的大量图像的汇编。这些单独的图像被称为帧。
-在处理视频时,我们参与处理帧循环,对循环中的每个“链接图像”应用转换。
以下是如何使用OpenCV加载视频的代码片段。
while video_capture.isOpened():
ret,frame = video_capture.read()
如果不是ret:
打印(“无法从视频中读取帧或视频结束。”)
打破
# 显示框架
cv2.imshow('处理视频',processed_frame)
#如果按下“q”,则打破循环
if cv2.waitKey(1) & 0xFF == ord('q'):
打破
#释放视频捕获对象并关闭所有窗口
video_capture.release()
cv2.destroyAllWindows()
在循环中,调用video_capture.read()从视频源读取下一帧。此方法返回两个值:
- ret:一个布尔值,指示是否成功读取帧。如果帧已成功抓取并可以解码/返回,则为True;如果没有可用帧,则为False(例如,如果已到达视频的结尾)。
- frame:从视频中读取的实际帧。如果ret为True,则为像素矩阵,如果ret为False,则为None。
最后一个“如果”条件允许您在必要时通过按q'键来打破循环。
太棒了,目前我们更好地了解了视频文件的工作原理,让我们完成我们需要遵循的步骤,将面具与我们的视频相结合。
#第1步:将每帧转换为灰度。
gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
此行使用OpenCV的cvtColor函数将输入frame从BGR(蓝色、绿色、红色)颜色空间转换为灰度。
灰度转换是为了简化操作,由于不需要颜色信息,而且只用一个通道工作会更容易。
#第2步:涂抹口罩
segmented = cv2.bitwise_and(gray, gray, mask=mask)
cv2.bitwise_and函数在gray图像和自身之间执行按位AND操作,使用mask作为掩码。
此操作将所有未在mask中定义的gray像素归零,对图像进行分割。
位运算将第一个操作数的每个位与第二个操作数的相应位进行比较。如果两个位都是1,则结果位设置为1;否则,设置为0。

二进制掩码表明 | 来源
#第3步:应用算法查找图像中的轮廓。
contours,_=cv2.findContours(segmented,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
该函数仅检索外部轮廓(cv2.RETR_EXTERNAL)并仅将水平、垂直和对角线段压缩到其端点中。

如何在二进制图像中检测轮廓 | 来源
#第4步:绘制边界框
一旦我们完成了这三个步骤,我们就会知道每个停车位的确切位置,我们可以在它们周围创建一个正方形,并进行一点Opencv操作。
对于轮廓的轮廓:
x,y,w,h = cv2.boundingRect(轮廓)
cv2.rectangle(框架,(x,y),(x+w,y+h),(0,255,0),2)
返回框架
x, y, w, h = cv2.boundingRect(contour):对于每个等高线,这条线计算最小的矩形,可以通过调用cv2.boundingRect(contour)包围它。该函数返回四个值:
- x和y:边界矩形左上角的坐标。
- w和h:分别是边界矩形的宽度和高度。
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2):这条线使用矩形的左上角(x, y)和右下角(x+w, y+h)在框架上绘制一个矩形。
矩形绘制具有以下属性:
- (0, 255, 0):矩形的颜色,以BGR(蓝色、绿色、红色)格式指定。它设置为绿色,强度为全强度(255),没有蓝色或红色。
- 2:构成矩形的线条的厚度。
太好了,既然我们已经完成了所有步骤,让我们定义我们的功能:
def draw_bounding_boxes(框架,掩码):
灰色=cv2.cvt颜色(框架,cv2。颜色_BGR2GRAY)
分段 = cv2.bitwise_and(灰色,灰色,mask=mask)
轮廓,_ = cv2.findContours(分段,cv2。RETR_EXTERNAL,
cv2。CHAIN_APPROX_SIMPLE)
对于轮廓的轮廓:
x,y,w,h = cv2.boundingRect(轮廓)
cv2.rectangle(框架,(x,y),(x+w,y+h),(0,255,0),2)
返回框架
足够简单,对吗?
目前,我们将更新代码,将此函数包含在我们的循环中。
这是我们目前在主要计划中所拥有的:
将我们的掩码与原始视频文件相结合的主脚本
如果您安装了必要的依赖项,并为下载的视频和遮罩添加了正确的路径,您应该会看到以下内容:

面具与视频文件相结合|由作者创建
太棒了!
一旦我们创建了掩码,这个过程就超级简单了。
让我们继续下一步。
但我们真的需要训练一个机器学习模型吗?
在计算机视觉中,解决我们问题的办法并不总是CNN!
您应该始终分析案例,并评估我们是否需要神经网络来解决它。
像素强度的变化
如果你注意,停车位的颜色强度总是一样。这在很大程度上简化了这个过程。
这个问题可以通过以下方式解决:
停车位总是具有一样的像素强度,如果这种情况发生变化,则意味着汽车已经占据了它。

汽车离开停车位并改变像素强度 | 来源
我们可以用梯度直方图(以及许多其他方法)来检测像素强度的变化。简单的像素操作。
这个解决方案看起来很完美,由于它超级快速和高效,但是,它并不那么强劲。
- 如果行人走进停车位怎么办?
- 如果是雨天或雾天,像素强度也可能发生变化。
- 如果相机没有清洁怎么办?
所有这些外部情况使这种方法有点弱。
通过更好地分析所有这些条件,我们可以创建一个能够适应像素强度所有这些变化的系统。
不过,为了简单起见(也由于可能是你要找的),我们将训练CNN!
使用TensorFlow训练卷积神经网络
如果你正在阅读这篇文章,我想你对CNN有一些想法,如果你不了解,你可以看看我的文章,了解你需要知道的理论概念!
卷积神经网络:综合指南
探索CNN在图像分析中的力量
medium.com
为了根据停车位是否被占用进行分类,我们将训练一个CNN分类器。这是一个很好的方法,由于我们有一个很棒的数据集,神经网络将易于训练。
我们将使用Python和TensorFlow,这是一个超级流行的机器学习框架。
第一,我们必须导入数据集。一般,处理图像的是将每个类别存储在不同的文件夹中。

存储在两个不同目录中的图像|由作者创建
在这种情况下,我们只有两个类:
- 空的
- not_空
每个类别包含3.045张图片。
使用TensorFlow导入图像数据集
如果您是这个领域的新手,您可能更熟悉导入表格数据(这一般对熊猫等库更直接)。
对于图像,这个过程并不那么简单。不过,TensorFlow对我们来说超级容易。我们可以通过多种方式创建数据集。
我将使用flow_from_directory()方法。
您必须将图像存储在目录中才能正常工作。
导入张量流
从tensorflow.keras.preprocessing.image导入ImageDataGenerator
data_gen = ImageDataGenerator(
重尺度=1。/255,
validation_split=0.2)
dataset_path = "/path/to/dataset"
train_ds = data_gen.flow_from_directory(
目录=dataset_path,
subset="training",
种子=123,
target_size=(29,68),
batch_size=32,
class_mode='sparse',
洗牌=True)
val_ds = data_gen.flow_from_directory(
目录=dataset_path,
subset="验证",
种子=123,
target_size=(29,68),
batch_size=32,
class_mode='sparse',
洗牌=True)
让我们回顾一下这里发生的事情:
第一,我们正在创建一个ImageDataGenerator实例。在这里,我们定义了我们将应用于数据集的转换。在这种情况下,我只会重新缩放它们,并定义将包含在验证聚焦的数据集的百分比。
重新缩放一般出于几个缘由,主要是为了使数据正常化,并使模型训练过程更加高效和稳定。

带有ImageDataGenerator的Tensorflow管道 | 来源
一旦我们定义了要应用于数据集的所有转换,我们就从flow_from_directory()开始。
让我们回顾一下这种方法的一些最重大的参数。
- directory:包含子目录的目标目录的路径,每个子目录都应包含属于一个类的图像。
- subset:数据集将被拆分的子集(train、val或test)。
- target_size:所有图像将调整到的尺寸。
(29,68)被选中是由于是数据聚焦的平均维度。 - batch_size:每批从生成器生成的图像数量。
图像通过插值调整大小,OpenCV中使用三种主要方法:双线性、双立方和最近邻居插值。
插值的艺术和科学
探索图像处理的支柱
medium.com
要了解有关此功能的更多信息,请查看TensorFlow中的文档。
构建模型
太棒了,目前我们导入了数据集,让我们构建模型。
在计算机视觉领域,一般使用预构建的卷积神经网络,而不是从头开始构建模型。
借此机会,由于任务超级简单,我们只需要对两个结果进行分类,我将自己从头开始构建架构。
我将包括:
- 2个卷积层
- 2个最大池层
- 1个扁平层
- 2个密集的层
您可以在下图中可视化它。

CNN的结构 | 来源:由作者使用visualkeras库创建
为了构建这个架构,我们有各种选择。我将使用Sequential API。
由于其简单易用性,顺序API是在TensorFlow中构建神经网络的绝佳起点。
将张量流导入为tf
从 tensorflow.keras.models import Sequential
从tensorflow.keras.layers导入Conv2D,MaxPooling2D,
扁平,密集,辍学
#模型架构
模型=顺序([
Conv2D(32, (3, 3), activation='relu', input_shape=(29, 68, 3)),
MaxPooling2D(2,2),
Conv2D(64,(3,3),激活='relu'),
MaxPooling2D(2,2),
扁平(),
密集(128,激活='relu'),
辍学(0.5),
密集(2,激活='softmax')])
如果您想了解更多关于顺序API的工作原理以及创建神经网络架构的其他方法,请查看本文:
Tensorflow中的顺序与功能与子类API
为您的模型选择最佳架构
medium.com
编译模型
汇编是构建和训练神经网络的重大一步。
此阶段通过指定关键属性(如损失函数、优化器和要监控的指标)来准备训练模型。
我将使用亚当优化器和精度指标。
损失将是在teger标签中工作的
sparse_categorical_crossentropy。
它被称为“稀疏”,由于您不需要将类标签转换为adense单热编码数组;您可以简单地使用直接整数标签。
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
指标=['准确性'])
训练模型
在训练阶段,模型学习根据输入数据和指定的损失函数调整权重来进行预测。
这个过程包括将训练数据输入模型,将模型的预测与实际结果进行比较,然后更新模型的权重以尽量减少损失。
为了指示这个过程将完成的次数,我们创建了确定的纪元数量。
纪元= 10
历史 = model.fit(
train_ds,
validation_data=val_ds,
时代=时代)
在这个要点中,你可以看到完整的脚本将是什么样子:
CNN培训脚本
如果您正确上传了训练数据集,该程序应该适合您。
在下图中,您可以看到我们获得的结果:

卷积神经网络结果|由作者创建
正确分析这一点很重大。
第一个准确性表明训练数据聚焦的结果,而“val_accuracy”是从验证数据聚焦获得的结果。
我们总是要检查第二个“准确性”分数,以防止过度拟合。
除了理论之外,结果都很棒,仅仅10个纪元,我们就能够获得99.1%的准确率。
这意味着我们的模型在几乎所有情况下都应该表现正常。
为了模仿训练神经网络的整个过程,我们在存储库中加入了超参数调优部分。我不会在本教程中浏览它,但你可以在这里查看!
节省重量
一旦创建了模型,我们只需要节省重量。通过这种方式,我们将能够从外部程序加载模型并将其合并到我们的程序中。
model.save('/content/model/path_to_model.H5')
该模型一般保存在一个。H5文件。此文件以分层数据格式(HDF5)保存。
HDF5是一种高性能数据格式,旨在存储和组织大量数据,使其适用于不同的科学领域和应用。
如果您想了解更多信息,请查看这篇文章:
HDF5文件
本章的目的是描述如何处理HDF5数据文件。如果要将HDF5数据写入或读取…
docs.hdfgroup.org
将模型与我们的程序集成
既然我们已经构建了模型,我们必须将其纳入我们的计划中。
要做到这一点,这个过程超级简单。
第一,我们必须创建一个预处理功能,以使我们分析的帧适应我们模型的第一层,该层旨在接受大小为(68,29)的图像。
一旦我们做到了这一点,我们将使其正常化。
我们还必须包括另一个维度,由于我们的模型将分批处理图像。
这将是我们的预处理功能:
def preprocess_for_prediction(roi, target_size=(68, 29)):
#将投资回报率调整为模型预期的目标大小
roi_resized = cv2.resize(roi,target_size)
#如果您的模型期望标准化,请规范化像素值
roi_normalized = roi_resized / 255.0
# 展开尺寸以添加批次大小
roi_expanded = np.expand_dims(roi_normalized,轴=0)
返回roi_expanded
目前我们必须将模型与我们的程序集成。还记得我们为用掩码分割视频而构建的主要脚本吗?
我们只需要稍微修改一下,将其与我们的神经网络集成。
看看新功能:
def draw_bounding_boxes_and_predict(框架,掩码,模型):
灰色=cv2.cvt颜色(框架,cv2。颜色_BGR2GRAY)
分段 = cv2.bitwise_and(灰色,灰色,mask=mask)
轮廓,_ = cv2.findContours(分段,cv2。RETR_EXTERNAL,cv2。CHAIN_APPROX_SIMPLE)
对于轮廓的轮廓:
x,y,w,h = cv2.boundingRect(轮廓)
#使用边界框坐标提取投资回报率
roi = 框架[y:y+h, x:x+w]
# 预处理模型预测的投资回报率
roi_preprocessed = preprocess_for_prediction(roi)
#使用您的模型预测入住率
预测 = model.predict(roi_preprocessed)
predicted_class = np.argmax(prediction, axis=1)[0] # 假设二进制分类:0为空,1为占用
#如果空,则用绿色绘制边界框,如果被占用,则用红色绘制边界框
color = (0, 255, 0) if predicted_class == 0 else (0, 0, 255)
cv2.rectangle(框架,(x,y),(x+w,y+h),颜色,2)
返回框架
本质上,我们正在执行与以前一样的操作,但目前我们包括一个预处理步骤、一个预测步骤和一个新的条件,如果我们的模型预测停车位被占用,则使边界框变为红色,如果没有,则使边界框变为绿色。
太棒了,目前我们已经为我们的主程序准备好了。我们´将只使用两个函数:
- preprocess_for_prediction()
- draw_bounding_boxes_and_predict()
这将是我们的主要脚本:
只有20行代码!?
是的,我们不需要更多。如果您正确遵循了所有步骤,您应该有类似于以下输出的东西:

最终结果|由作者创建
回顾我们迄今为止所做的工作:
- 我们制作了一个口罩来隔离停车位。
- 我们训练了一个卷积神经网络,以在被占用和未被占用之间进行分类。
- 我们将我们的模型集成到一个循环中,以检测每个框架中停车场的可用插槽数量。
没那么难,对吧?只需几行代码,我们就能够创建一个停车位分类器。
不过,我们没有完成!这是一个端到端的项目……还记得吗?
目前我们必须将我们的模型投入生产。
在第2部分中,我们将了解如何部署和托管机器学习模型!
提示:我们将使用Azure和Docker!
参考书目
- https://opencv.org/
- https://arxiv.org/abs/2308.08192
- https://www.frontiersin.org/articles/10.3389/fnbot.2020.00046/full
- https://medium.com/the-research-nest/parking-space-detection-using-deep-learning-9fc99a63875e
- https://viso.ai/product/computer-vision-parking-lot-occupancy-tutorial/
- https://github.com/computervisioneng/parking-space-counter

完整文章在哪里呢?