19、实时目标检测与距离测量:从原理到实践

实时目标检测与距离测量:从原理到实践

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/
  1. 重命名操作:
    • 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[结束]

通过以上步骤和方法,我们可以实现一个功能强大的实时汽车检测与距离测量应用,为实际场景中的目标检测和距离测量提供有效的解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值