实时目标检测与距离测量:从原理到实践
1. 实时处理与性能优化
在处理视频时,无论是视频文件还是来自摄像头的实时视频流,视频的帧率通常约为24 - 30 FPS。这意味着我们处理每一帧的时间仅有33 - 40毫秒。若处理时间过长,实时视频流会丢帧,视频文件播放速度会变慢。
为了测量目标检测时每帧的处理时间,我们可以在应用中添加代码。具体操作如下:
1. 在
Detective.pro
项目文件中添加新的宏定义:
DEFINES += TIME_MEASURE=1
此宏用于开启或关闭时间测量代码。若要关闭,注释该行并运行
make clean && make
重新编译应用。
2. 在
capture_thread.cpp
文件的
CaptureThread::run
方法中,在调用
detectObjects
或
detectObjectsDNN
方法前后添加代码:
#ifdef TIME_MEASURE
int64 t0 = cv::getTickCount();
#endif
detectObjects(tmp_frame);
// detectObjectsDNN(tmp_frame);
#ifdef TIME_MEASURE
int64 t1 = cv::getTickCount();
double t = (t1 - t0) * 1000 / cv::getTickFrequency();
qDebug() << "Detecting time on a single frame: " << t << "ms";
#endif
cv::getTickCount
函数返回从系统启动开始的时钟周期数。我们在检测前后各调用一次,通过
t1 - t0
得到检测时的时钟周期数。
cv::getTickFrequency()
函数返回每秒的时钟周期数,通过
(t1 - t0) * 1000 / cv::getTickFrequency()
将时钟周期数转换为毫秒,最后用
qDebug()
输出时间信息。
使用级联分类器方法编译并运行
Detective
应用时,会看到类似如下的输出:
Detecting time on a single frame: 72.5715 ms
Detecting time on a single frame: 71.7724 ms
Detecting time on a single frame: 73.8066 ms
Detecting time on a single frame: 71.7509 ms
Detecting time on a single frame: 70.5172 ms
Detecting time on a single frame: 70.5597 ms
可以发现,每帧处理时间超过70毫秒,大于应有的33 - 40毫秒。这主要是因为计算机CPU较旧且未降低输入帧的分辨率。优化方法是使用更强大的CPU并将输入帧调整为更小、更合适的尺寸。
在使用之前的代码测量YOLO方法的时间之前,我们需要在
CaptureThread::detectObjectsDNN
方法中添加代码:
// ...
net.forward(outs, getOutputsNames(net));
#ifdef TIME_MEASURE
vector<double> layersTimes;
double freq = cv::getTickFrequency() / 1000;
double t = net.getPerfProfile(layersTimes) / freq;
qDebug() << "YOLO: Inference time on a single frame: " << t << "ms";
#endif
在对DNN模型进行前向传播后,调用
getPerfProfile
方法获取前向传播的时间,转换为毫秒并打印。这样我们会得到两个时间:调用
detectObjectsDNN
方法的总时间和推理时间(前向传播时间)。两者相减可得到blob准备和结果解码的时间。
切换到YOLO方法运行应用,会得到如下输出:
YOLO: Inference time on a single frame: 2197.44 ms
Detecting time on a single frame: 2209.63 ms
YOLO: Inference time on a single frame: 2203.69 ms
Detecting time on a single frame: 2217.69 ms
YOLO: Inference time on a single frame: 2303.73 ms
Detecting time on a single frame: 2316.1 ms
YOLO: Inference time on a single frame: 2203.01 ms
Detecting time on a single frame: 2215.23 ms
结果显示速度极慢。虽然YOLO性能可达45 FPS,即每帧仅需22毫秒,但这是在GPU上测量的。深度神经网络计算量巨大,不适合在CPU上运行,适合在GPU上运行。目前将计算转移到GPU最成熟的解决方案是CUDA和OpenCL,OpenCV库的DNN模块目前仅支持OpenCL方法。
cv::dnn::Net
类有两个方法可设置后端和目标设备:
-
setPreferableBackend()
-
setPreferableTarget()
若有GPU且正确安装了OpenCL和GPU驱动,可使用
-DWITH_OPENCL=ON
标志构建OpenCV以启用OpenCL支持。之后使用
net.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL)
使用GPU进行计算,能大幅提升性能。
2. 实时汽车检测应用搭建
在测量物体间距离之前,需先检测出感兴趣的物体。这里我们选择检测汽车,因为YOLOv3模型在准确性方面表现良好,且汽车类别在COCO数据集的类别列表中。
我们通过复制之前完成的项目来创建新项目。具体步骤如下:
1. 复制项目:
$ pwd
/home/kdr2/Work/Books/Qt-5-and-OpenCV-4-Computer-Vision-Projects
$ mkdir Chapter-07
# !!! you should copy it to a different dir
$ cp -r Chapter-06/Detective Chapter-07/DiGauge
$ ls Chapter-07
DiGauge
$ cd Chapter-07/DiGauge/
-
重命名操作:
-
将
Detective.pro项目文件重命名为DiGauge.pro。 -
在项目文件中将目标值从
Detective重命名为DiGauge。 -
在
main.cpp源文件中调用window.setWindowTitle时,将窗口标题从Detective改为DiGauge。 -
在
mainwindow.cpp源文件的MainWindow::initUI方法中调用mainStatusLabel->setText时,将状态栏文本从Detective is Ready改为DiGauge is Ready。 -
在
utilities.cpp源文件的Utilities::getDataPath方法中调用pictures_dir.mkpath和pictures_dir.absoluteFilePath时,将Detective字符串改为DiGauge。
-
将
为使项目代码简洁明了,我们移除与级联分类器方法相关的代码:
1. 在
DiGauge.pro
项目文件中,移除
LIBS
配置中的
opencv_objdetect
模块,同时移除
DEFINES
配置中定义的宏。
2. 在
capture_thread.h
文件中,从
CaptureThread
类中移除
void detectObjects(cv::Mat &frame)
私有方法和
cv::CascadeClassifier *classifier;
字段。
3. 在
capture_thread.cpp
源文件中:
- 移除
void CaptureThread::detectObjects(cv::Mat &frame)
方法的实现。
- 在
void CaptureThread::run()
方法中,移除所有与级联分类器相关的代码,包括分类器的创建、
detectObjects
方法的调用和分类器的删除。
- 移除
vector<string> getOutputsNames(const cv::dnn::Net& net)
函数。
- 将调用
getOutputsNames(net)
改为
net.getUnconnectedOutLayersNames()
以获取输出层的名称。
现在应用可使用YOLOv3模型检测视频或图像中的80类物体,但我们只对汽车感兴趣。在
coco.names
文件中查找汽车类:
$ grep -Hn car data/coco.names
data/coco.names:3:car
data/coco.names:52:carrot
$
可知汽车类在
coco.names
文件中是第三行,其类ID为2(索引从0开始)。我们重写
capture_thread.cpp
源文件中的
decodeOutLayers
函数,过滤掉除ID为2之外的所有类:
void decodeOutLayers(
cv::Mat &frame, const vector<cv::Mat> &outs,
vector<cv::Rect> &outBoxes
)
{
float confThreshold = 0.65; // confidence threshold
float nmsThreshold = 0.4; // non-maximum suppression threshold
// vector<int> classIds; // this line is removed!
// ...
}
函数的更改如下:
-
函数签名更改
:移除
outClassIds
参数,因为只检测一类物体;移除
outConfidences
参数,因为不关心每辆检测到的汽车的置信度。
-
函数体更改
:将
confThreshold
变量从0.5改为0.65以提高准确性;移除用于存储检测到的物体类ID的局部变量
classIds
;在处理检测到的物体的边界框时,检查类ID是否为2,若不是则忽略当前边界框;移除所有尝试更新已移除变量
classIds
、
outClassIds
和
outConfidences
的行。
对于
CaptureThread::detectObjectsDNN
方法,只需更新其主体的末尾部分:
// remove the bounding boxes with low confidence
// vector<int> outClassIds; // removed!
// vector<float> outConfidences; // removed!
vector<cv::Rect> outBoxes;
// decodeOutLayers(frame, outs, outClassIds, outConfidences, outBoxes); // changed
decodeOutLayers(frame, outs, outBoxes);
for(size_t i = 0; i < outBoxes.size(); i ++) {
cv::rectangle(frame, outBoxes[i], cv::Scalar(0, 0, 255));
}
移除与类ID和置信度相关的变量,调用
decodeOutLayers
函数时仅将
outBoxes
变量作为输出参数。然后遍历检测到的边界框并以红色绘制。
最后,编译并运行应用:
$ qmake
$ make
g++ -c -pipe -O2 -Wall #...
# output trucated
$ export LD_LIBRARY_PATH=/home/kdr2/programs/opencv/lib
$ ./DiGauge
别忘了将YOLOv3模型相关文件(
coco.names
、
yolov3.cfg
和
yolov3.weights
)复制到项目的数据子目录中,否则模型无法成功加载。应用启动后,在有汽车的场景中测试,会看到每辆检测到的汽车都有一个红色边界框。
3. 距离测量原理
测量物体间或物体与相机间的距离有多种方法。例如,若物体或相机以已知固定速度移动,结合运动检测和目标检测技术,可轻松估计相机视野中物体间的距离;若有立体相机,可参考相关资料测量距离。但这里我们只有一个固定位置的普通网络摄像头,在满足一定前提条件下也可进行距离测量。
3.1 物体间距离测量
测量物体间距离的前提条件是:将相机安装在固定位置,能以鸟瞰视角拍摄物体;视野中必须有一个已知固定大小的物体作为参考。
例如,照片中有两枚硬币,硬币直径为25毫米,在照片中占128像素,两枚硬币间的距离在照片中为282像素。由于照片中128像素代表实际的25毫米,所以282像素代表的实际距离为
25 / 128 * 282 = 55.07
毫米。在这种情况下,检测到参考物体和要测量距离的顶点后,通过简单计算即可得到距离。
3.2 物体与相机间距离测量
测量物体与相机间距离的前提条件是:将相机安装在固定位置,能以平视视角拍摄物体;也需要一个参考,但这里的参考与鸟瞰视角情况不同。
根据相机与物体的位置关系,可得到一些方程:
1. 第一个方程基于三角形相似性。
2. 由方程(1)可知,焦距
F
可按方程(2)计算。
3. 将物体移到另一个位置,标记距离为
D1
,镜头上图像的高度为
H1
,考虑相机焦距为固定值,可得到方程(3)。
4. 结合方程(2)和方程(3),可得到方程(4)。
5. 对方程(4)进行一些变换,可得到距离
D1
的计算公式(5)。
6. 由于与实际物体高度
Hr
相比,
H0
和
H1
的值极小,可认为
Hr - H0
和
Hr - H1
的值几乎相同,这就是方程(6)的含义。
7. 利用方程(6),可将方程(4)简化为方程(7)。
由于无论在镜头上以米为单位测量还是在照片上以像素为单位测量,
H0 / H1
的值始终相同,所以可将
H0
和
H1
改为它们占用的像素数,以便在数字照片中进行测量。
我们使用
D0
(米)和
H0
(像素)作为参考。例如,将一个文件夹放在离相机230厘米的桌子上拍照,其在垂直方向上占90像素;将其移近几厘米后再次拍照,此时高度为174像素。则可将左侧照片的值作为参考值:
-
D0
为230厘米
-
H0
为90像素
-
H1
为174像素
根据方程(7),可计算
D1
为
H0 / H1 * D0 = 90 / 174 * 230 = 118.96
厘米,结果与用尺子测量的值120厘米非常接近。
下面用mermaid流程图展示整个流程:
graph LR
A[开始] --> B[实时处理与性能优化]
B --> C[实时汽车检测应用搭建]
C --> D[距离测量原理]
D --> E[物体间距离测量]
D --> F[物体与相机间距离测量]
E --> G[结束]
F --> G[结束]
综上所述,我们介绍了实时处理的性能优化方法,搭建了实时汽车检测应用,并阐述了不同情况下的距离测量原理。通过这些方法和技术,我们可以实现更高效的目标检测和准确的距离测量。
4. 将距离测量原理应用于 DiGauge 应用
在了解了距离测量的原理后,我们可以将这些原理应用到之前搭建好的 DiGauge 应用中,实现汽车之间以及汽车与相机之间距离的测量。
4.1 物体间距离测量的实现
在 DiGauge 应用中,若要测量汽车之间的距离,需满足前文提到的前提条件:相机固定在能获取鸟瞰视角的位置,且视野中有已知固定大小的物体作为参考。
以下是实现步骤:
1.
检测参考物体和汽车
:使用 YOLOv3 模型检测出参考物体(如硬币)和汽车。
2.
获取参考信息
:确定参考物体在图像中所占像素数以及其实际大小。例如,硬币直径为 25 毫米,在图像中占 128 像素。
3.
测量汽车间像素距离
:检测汽车的位置,计算它们之间的像素距离。
4.
计算实际距离
:根据参考信息,将像素距离转换为实际距离。计算公式为:实际距离 = (参考物体实际大小 / 参考物体像素大小) * 汽车间像素距离。
在代码中,可以在检测到汽车和参考物体后,添加如下代码来计算距离:
// 假设已经检测到参考物体和汽车,获取相关信息
int referencePixelSize = 128; // 参考物体像素大小
float referenceActualSize = 25; // 参考物体实际大小
int carPixelDistance = 282; // 汽车间像素距离
float carActualDistance = (referenceActualSize / referencePixelSize) * carPixelDistance;
qDebug() << "汽车间实际距离: " << carActualDistance << "毫米";
4.2 物体与相机间距离测量的实现
若要测量汽车与相机之间的距离,需将相机固定在能获取平视视角的位置,并使用一个参考物体。
以下是实现步骤:
1.
获取参考值
:将参考物体放在相机前,测量其与相机的距离
D0
,并拍摄照片,记录物体在照片中的高度
H0
。
2.
检测汽车
:使用 YOLOv3 模型检测汽车,获取汽车在照片中的高度
H1
。
3.
计算距离
:根据公式
D1 = H0 / H1 * D0
计算汽车与相机的距离。
在代码中,可以在检测到汽车后,添加如下代码来计算距离:
// 假设已经获取到参考值
float D0 = 230; // 参考距离,单位:厘米
int H0 = 90; // 参考物体像素高度
int H1 = 174; // 汽车像素高度
float D1 = (float)H0 / H1 * D0;
qDebug() << "汽车与相机的距离: " << D1 << "厘米";
5. 整体应用流程总结
为了更清晰地展示整个 DiGauge 应用的流程,我们可以用表格来总结:
| 步骤 | 操作内容 |
| ---- | ---- |
| 1 | 进行实时处理与性能优化,测量目标检测时间,优化性能至 GPU 计算 |
| 2 | 搭建实时汽车检测应用,复制项目、重命名、移除级联分类器代码,过滤检测类别 |
| 3 | 理解距离测量原理,包括物体间和物体与相机间距离测量 |
| 4 | 将距离测量原理应用于 DiGauge 应用,实现汽车间和汽车与相机间距离测量 |
6. 注意事项与拓展应用
在实际应用中,还需要注意以下几点:
-
参考物体选择
:参考物体的大小和形状应易于检测和测量,且在不同光照条件下特征稳定。
-
相机稳定性
:确保相机固定在一个稳定的位置,避免因相机晃动导致测量误差。
-
光照条件
:光照条件的变化可能会影响物体的检测和测量,尽量保持光照均匀。
此外,这个应用还可以进行拓展:
-
多目标检测
:除了汽车,还可以检测其他物体,并测量它们之间的距离。
-
实时监控
:将应用与监控系统结合,实现对特定区域内物体的实时检测和距离测量。
下面用 mermaid 流程图展示整个 DiGauge 应用的完整流程:
graph LR
A[开始] --> B[实时处理与性能优化]
B --> C[实时汽车检测应用搭建]
C --> D[距离测量原理学习]
D --> E[物体间距离测量实现]
D --> F[物体与相机间距离测量实现]
E --> G[结果输出与展示]
F --> G[结果输出与展示]
G --> H[结束]
通过以上步骤和方法,我们可以实现一个功能强大的实时汽车检测与距离测量应用,为实际场景中的目标检测和距离测量提供有效的解决方案。
超级会员免费看

被折叠的 条评论
为什么被折叠?



