1.项目介绍
本文档是对于“车流量检测平台”的应用技术进行汇总,适用于此系统所有开发,测试以及使用人员,其中包括设计背景,应用场景,系统架构,技术分析,系统调度,环境依赖,以及运行指导,其中技术分析为主要部分,包括UI设计,视频输入输出,图像预处理,数据提取,处理及预测等。
2.设计背景
政治角度:
随着城市化进程的加快和交通拥堵问题的日益严重,政府对于智能交通系统的建设与管理给予了高度重视。基于视频的车流量检测平台作为智能交通系统的重要组成部分,其开发与应用有助于提升城市治理水平,满足政府对交通状况实时监测与管理的需求。
经济角度:
随着信息技术的快速发展,视频处理技术、计算机视觉和深度学习等领域取得了显著进步,为基于视频的车流量检测平台的开发提供了技术支撑。同时,随着汽车保有量的不断增加,交通管理领域对于智能化、自动化的需求也日益旺盛,推动了基于视频的车流量检测平台的研发与应用。
社会角度:
公众对于出行效率、交通安全和环境质量的关注度不断提高,对于智能交通系统的期待也日益增强。基于视频的车流量检测平台能够实时监测交通状况,为公众提供更加便捷、安全的出行环境,同时也有助于减少交通拥堵和环境污染,提升城市生活质量。
此外,传统的车流量检测方法主要依赖于传感器设备,如地磁传感器、红外线传感器等。这些传感器需要在道路上布设,成本较高且安装维护困难。而基于计算机视觉的车流量检测系统则可以通过分析道路上的摄像头图像来实现,具有成本低、安装方便、实时性强等优势。因此,设计一种高效、准确且易于实现的汽车流量检测系统显得尤为重要。
3.场景应用
随着城市化进程的加快和交通工具的普及,车辆数量的快速增长给城市交通管理带来了巨大的挑战。因此,交通流量检测成为了交通管理的重要组成部分。它能够提供实时的交通状况信息,帮助交通管理部门制定合理的交通策略,优化交通流量,提高道路利用效率,减少交通拥堵和事故发生的可能性。
基于视频的车流量检测平台可广泛应用于城市道路、高速公路、桥梁隧道等交通场景中。通过高清摄像头实时拍摄车辆图像,利用图像处理算法对图像进行分析和处理,提取出车辆的位置、数量和运动轨迹等信息。这些信息可用于交通管理部门对道路交通情况的监测和分析,也可用于交通规划和设计、交通预测和智能交通系统等领域。
解决的社会痛点
1. 交通拥堵问题:
基于视频的车流量检测平台能够实时监测交通流量和车辆速度等信息,为交通管理部门提供实时、准确的数据支持。通过数据分析,可以及时发现交通拥堵点,采取相应的交通控制措施,优化交通流动,减少交通拥堵现象。
2. 交通安全问题:
通过对车辆运动轨迹的监测和分析,平台可以及时发现异常行驶行为,如超速、逆行等,为交通管理部门提供预警信息,有助于减少交通事故的发生。
3. 环境质量问题:
平台还可以监测城市环境质量,如空气质量、噪声等。通过对这些数据的分析,可以及时发现环境质量不佳的区域,采取相应措施减少污染源,提高城市环境质量。
4. 公共服务效率问题:
在公共交通领域,基于视频的车流量检测平台可以实时监测公交、地铁等公共交通的运行情况,为公众提供更加精准的出行信息,提高公共服务的效率和质量。
4.系统架构
4.1用户界面层
使用QT框架开发图形用户界面(GUI),展示实时视频流、预处理视频流,流量统计数据、当天车流信息以及未来车流信息等。用户可以通过此界面进行参数设置、启动/停止检测等操作。
4.2视频处理层
接入摄像头或视频流,利用OpenCV库进行视频捕获和预处理。实现车辆检测算法,包括提取物体掩码、运动目标检测、车辆识别等步骤。将检测到的车辆信息传递给下一层。
4.3数据处理与存储层
接收视频处理层传递的车辆信息,并进行进一步的分析和处理,包括提取真实汽车掩码,以及Opencv格式的图片与Qt支持的图片进行格式转化。将可用数据提取出来并进一步的过滤,并且利用梯度下降法拟合数据的函数模型。
4.5数据存储层
将处理后的数据存储在数据库中,包括车辆流量统计数据、时间戳。实现数据查询,预测和报表生成功能,供用户层调用和展示。
5.技术分析
1视频处理流程图
2 UI设计图
客户端界面利用QT5搭建而成,这些窗口分别是源视频识别并标记显示窗口,计算机图像学处理后的窗口,未来数据预期窗口,用来显示未来某天二十四小时的车流量,当天的车流量情况展示窗口,最后一个是日历,可以选择日期,还有对于视频流的播放控制有两个按钮控制。
使用Qlabel控件,在其之上利用绘图事件,分别将标记后的视频流和预处理视频流播放出来。
折线统计图用于展示以前某天或未来某天24小时的车流量变化情况,条形统计图用于实时展示当天的车流情况。
中间两个窗口分别显示实时监控的视频图像以及汽车轮廓和视频流处理过程。
利用QToolButton控件设置两个按钮来控制视频流的播放。
两个QEditLine 控件用于输入机器学习的频率和次数。
那个大的按钮是主动学习的控制开关,点击触发后变为红色,开始学习,并再右上角的编辑框显示学习进度。
使用日历控件可以先择事件,如果选择过去的某天,就将过去的某天二十四小时的车流量情况图像化的显示,如果选择未来某天,就将这天预测的数据显示出来。
在C++中,使用OpenCV库处理视频流时,cv::VideoCapture 类是一个非常重要的组件,它用于捕获视频流,可以从摄像头、视频文件或IP摄像头流中读取帧。
使用实例
cv::VideoCapture cap(0); // 检查摄像头是否成功打开 if (!cap.isOpened()) { std::cerr << "Error opening video stream or file" << std::endl; return -1; } cv::namedWindow("Video", cv::WINDOW_AUTOSIZE); while (true) { // 读取一帧图像 cap >> frame; // 检查是否成功读取帧 if (frame.empty()) { std::cout << "Can't receive frame (stream end?). Exiting ..." << std::endl; break; } cv::imshow(”Video”,res); } |
3 最终呈现图
图片前景掩码(通常简称为前景掩码)在图像处理中是一个重要的概念,主要用于从图片中分离出前景元素。掩码是一个二值图像,其中前景元素被标记为白色(或其他高亮颜色),而背景元素则被标记为黑色(或其他暗色)。通过这种方式,掩码可以帮助我们在后续的图像处理过程中只处理或操作前景元素,而忽略背景元素。基于颜色或亮度的差异:如果前景和背景在颜色或亮度上有明显的差异,可以使用阈值操作或色彩分割等方法来生成掩码。一旦生成了前景掩码,就可以将其应用于各种图像处理任务,如前景元素的提取、背景的替换、图像合成等。
4 汽车前景掩码图
接口解释:
setShadowThreshold()
这个方法是用来设置阴影检测阈值的。阴影阈值决定了算法如何区分前景对象和其可能产生的阴影。通过调整这个阈值,你可以影响算法对阴影的敏感度。
apply()
apply()方法是BackgroundSubtractorMOG2类的主要方法,用于执行背景减除。当你调用这个方法时,它会处理传入的图像,并返回一个新的图像,其中前景对象(即移动的对象)被高亮显示(通常是白色),而背景则被抑制(通常是黑色)。
二值图像中一类主要处理是对提取的目标图形进行形态分析。形态学处理中最基本的是腐蚀和膨胀。腐蚀和膨胀是两个互为对偶的运算。腐蚀的作用是将目标图像收缩,而膨胀是将图像扩大。结构元素是指具有某种确定形状的基本结构元素,例如,一定大小的矩形、圆形等。结构元素具有原点。
5.3.2.1 形态学
形态学操作是根据图像形状进行的简单操作。一般情况下对二值化图像/灰度图像进行操作。它需要输入两个操作,一个是原始图像,另一个被称为结构化元素或核,它是用来决定操作的性质的。两个基本的形态学操作是腐蚀和膨胀,它们的变体构成了开运算、闭运算和梯度等。
5 腐蚀/膨胀对比图
5.3.2.2 腐蚀
腐蚀的效果是把图片"变瘦",其原理是在原图的小区域内取局部最小值。因为是二值化图,只有 0 和 255,所以小区域内有一个是 0 该像素点就为 0:
5.3.2.3 膨胀
膨胀与腐蚀相反,取的是局部最大值,效果是把图片"变胖"。
5.3.2.4 开/闭运算
开运算:先腐蚀后膨胀,可用以消除黑色背景中的白点杂质。
闭运算:先膨胀后腐蚀,可用以消除白色前景中的黑点杂质。
6 形态学闭运算处理图
接口:
腐蚀(Erosion)
void erode(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), intiterations=1,IntborderType=BORDER_CONSTANT,
const Scalar&borderValue=morphologyDefaultBorderValue());
参数解释:
src:输入图像,通常是二值图像。
dst:输出图像,与输入图像具有相同的尺寸和类型。
kernel:用于腐蚀的结构元素,定义了邻域的形状和大小。
anchor:结构元素的锚点位置。默认值是(-1, -1),表示锚点位于结构元素的中心。
iterations:腐蚀操作的迭代次数。
borderType:像素外推法的类型。
borderValue:使用边界类型BORDER_CONSTANT时的边界值。
膨胀(Dilation)
void dilate(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1),
int iterations=1,int borderType=BORDER_CONSTANT,
const Scalar& borderValue=morphologyDefaultBorderValue());
参数解释:
src:输入图像,通常是二值图像。
dst:输出图像,与输入图像具有相同的尺寸和类型。
kernel:用于膨胀的结构元素,定义了邻域的形状和大小。
anchor:结构元素的锚点位置。默认值是(-1, -1),表示锚点位于结构元素的中心。
iterations:膨胀操作的迭代次数。
borderType:像素外推法的类型。
borderValue:使用边界类型BORDER_CONSTANT时的边界值。
高斯滤波(Gauss Filter)是线性滤波中的一种。在OpenCV图像滤波处理中,高斯滤波用于平滑图像,或者说是图像模糊处理,因此高斯滤波是低通的。其广泛的应用在图像处理的减噪过程中,尤其是被高斯噪声所污染的图像上。
高斯滤波的基本思想是: 图像上的每一个像素点的值,都由其本身和邻域内其他像素点的值经过加权平均后得到。其具体操作是,用一个核(又称为卷积核、掩模、矩阵)扫描图像中每一个像素点,将邻域内各个像素值与对应位置的权值相称并求和。从数学的角度来看,高斯滤波的过程是图像与高斯正态分布做卷积操作。
注意: 高斯滤波是将二维高斯正态分布放在图像矩阵上做卷积运算。考虑的是邻域内像素值的空间距离关系,因此对彩色图像处理时应分通道进行操作,也就是说操作的图像原矩阵时用单通道数据,最后合并为彩色图像。
5.3.3.1 一维高斯函数
可以看到,G(x)的跟sigma的取值有极大的关系。sigma取值越大,图像越平缓,sigma取值越小,图像越尖锐。
7 一维高斯函数
5.3.3.2 二维高斯函数
二维高斯是构建高斯滤波器的基础。可以看到,G(x,y)在x轴y轴上的分布是一个突起的帽子的形状。这里的sigma可以看作两个值,一个是x轴上的分量sigmaX,另一个是y轴上的分量sigmaY。对图像处理可以直接使用sigma并对图像的行列操作,也可以用sigmaX对图像的行操作,再用sigmaY对图像的列操作。它们是等价的。当sigmaX和sigmaY取值越大,整个形状趋近于扁平;当sigmaX和sigmaY取值越小,整个形状越突起。
高斯滤波原理就是将上图的二维正态分布应用在二维的矩阵上,G(x,y)的值就是矩阵上的权值,将得到的权值进行归一化,将权值的范围约束在[0,1]之间,并且所有的值的总和为1。可以看到,权值的分布是以中间高四周低来分布的。并且距离中心越远,其对中心点的影响就越小,权值也就越小。
因此可以总结:
(1)在核大小固定的情况下,sigma值越大,权值分布越平缓。因此,邻域各个点的值对输出值的影响越大,最终结果造成图像越模糊。
(2)在核大小固定的情况下,sigma值越小,权值分布越突起。因此,邻域各个点的值对输出值的影响越小,图像变化也越小。假如中心点权值为1,其他点权值为0,那么最终结果是图像没有任何变化。
(3)sigma固定时,核越大图像越模糊。
(4)sigma固定时,核越小图像变化越小。
9 采用高斯滤波后的图
接口解释:
void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT);
参数解释:
src:输入图像,可以是多通道图像。
dst:输出图像,与输入图像具有相同的尺寸和类型。
ksize:高斯核的大小。它必须是正奇数。
sigmaX:表示高斯核函数在X方向的标准差。
sigmaY:表示高斯核函数在Y方向的标准差。如果sigmaY设置为0,那么它会被设置为与sigmaX相同的值。两个sigma值决定了高斯核的形状。
borderType:像素外推法的类型,决定了当高斯核应用到图像边界时如何处理边界外的像素。默认值是BORDER_DEFAULT。
原图通过一系列图形学处理后,已经可以提取出想要的数据了,接下来就是数据数据除杂和过滤的过程。
5.4.1.1提取汽车掩码
寻找所有汽车掩码前景的连通图,如果连通图的长宽,以及面积都复合实验阈值,那么此掩码就是汽车的掩码。
10 计算汽车前景掩码流程图
核心算法
findContours(dst, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); for (size_t i = 0; i < contours.size(); i++) { Rect2d re = c2d(boundingRect(contours[i])); if (re.width>90&& re.width<440 &&re.height>140&& re.height<440) { box3.push_back(re); rectangle(frame, re, cv::Scalar(0, 255, 0), 2); } } |
5.4.1.2 数据过滤
依次和前一次的连通图组逐个进行比较,如果出现重叠面积大于等于当前面积的百分之十,则认为此汽车已经计数,否则依次向更久的连通组比较,直到遍历完所有历史数据。最终用所有车减去历史出现的车等于新出现的车。
11 统计汽车数量流程图
核心算法
int Run::findsum() { int sum = 0; for (size_t i = 0; i < box3.size(); i++) { bool bo = false; for (size_t j = 0; j < box2.size(); j++) { if (overlapArea(box3[i], box2[j]) / box3[i].area()> 0.1) { bo = true; break; } else { for (size_t k = 0; k < box1.size(); k++) { if (overlapArea(box3[i], box1[k]) / box3[i].area()> 0.1) { bo = true; break; } } } } if (bo)sum++; } return sum; } |
如图所示,利用连续三帧图像来确定是否有新出现的车辆,这三帧中总共出现了七辆车。
流量检测过程图:
12-1 第一帧有四辆车
12-2 第二帧有两辆车,其中新出现了一辆
12-3 第三帧有四辆车,其中新出现了量辆
利用当帧图像中汽车的总数量-前一帧汽车的数量=当前帧新出现的汽车数量。
而其中计算是否为之前已经出现过的车利用了面积重叠法。
13 判断是否为已经扫描过的车辆示意图
核心算法
double Run::overlapArea(Rect2d rect1, Rect2d rect2) { int overlapLeft = std::max(rect1. x, rect2.x); int overlapRight = std::min(rect1.x+rect1.width, rect2.x+rect2.width); int overlapTop = std::max(rect1.y, rect2.y); int overlapBottom = std::min(rect1.y+rect1.height, rect2.y+rect2.height); // 如果重叠区域的左右或上下边界无效(即没有重叠),则返回0 if (overlapLeft >= overlapRight || overlapTop >= overlapBottom) { return 0; } // 计算重叠区域的宽度和高度 int overlapWidth = overlapRight - overlapLeft; int overlapHeight = overlapBottom - overlapTop; // 计算重叠面积 return overlapWidth * overlapHeight; } |
数据存储利用关系型数据SQLite。SQLite是一个功能强大且易于使用的数据库系统,特别适用于那些需要轻量级、嵌入式和跨平台数据库解决方案的场景。这个表tim字段是一个INTEGER类型,可转为long long,用于存储UNIX时间戳。num字段是一个整型,用于存储车辆数量。可以从数据库中拿到历史数据,也可以将新扫描的数据存放到数据库中。
建表语句
CREATE TABKE car_table ( tim INTEGER PRIMARY KEY, num INT NOT NULL); |
我们首先想到简单线性回归分析,但是通过观察发现车流量变化的函数曲线接近于正弦曲线,因此采用梯度下降拟合曲线。
使用梯度下降法来拟合y = a * sin(b * X + c) + d形式的模型是一个迭代的过程,其中我们逐步调整模型的参数a, b, c, d,以最小化预测值和实际值之间的误差。
5.4.5.1梯度下降
14梯度下降求极值示意图
5.4.5.1.1算法简介
梯度下降法(Gradient Descent)是一种优化算法,常用于机器学习和深度学习的训练过程中,特别是用于求解损失函数的最小值。该算法的基本思想是通过迭代的方式调整模型参数,以使得损失函数逐渐减小,从而逼近最小值。
梯度下降法的工作原理如下:
1.选择初始参数:首先,需要为模型的参数选择一个初始值。这些参数可以是随机选择的,也可以是基于某种启发式方法选择的。
2.计算梯度:在每一次迭代中,计算损失函数关于模型参数的梯度。梯度表示了损失函数在各个方向上的变化率,因此指向了损失函数减小最快的方向。
3.更新参数:根据计算得到的梯度,按照一定的学习率(learning rate)来更新模型的参数。学习率是一个超参数,它决定了每次参数更新的步长。过大的学习率可能导致算法不稳定,而过小的学习率则可能导致算法收敛速度过慢。
4.重复迭代:重复执行步骤2和步骤3,直到满足某种停止条件,如损失函数值小于某个阈值,或者达到预设的最大迭代次数。
1.为什么按负梯度下降
《数学分析》中,负梯度方向是函数下降最快的方向,即x=[2,3],如果梯度为[1,2],则x往[1,2]方向调整,能令函数f(x)下降最快的方向(所谓最快,即调整同样的步长,该方向能令函数下降最快)按负梯度下降,保证了调整方向的正确性。
2.为什么要设置学习率
目的是为了保证按梯度方向调整一定能下降。梯度方向能下降是瞬时的,如果调整步长过大,则不一定能保证函数能下降,但只要调整步长足够小,函数就能下降(前提是梯度不为0)。所以,我们在调整时,加入学习率lr,以控制步长:
3、学习率的设置与自适应学习率
要保证能下降,学习率就不能过大,但学习率很小,每次迭代调整都很小,就需要迭代很多次。为此,我们可以设定一个较中肯的学习率(例如,lr = 0.1)。如果更智能一些,在程序中把学习率改为自适应学习率: 函数能下降,我们把学习率调大些,如果函数本次迭代不能下降,我们就把学习率调小些。
5.4.5.1.2初始化参数
A = -100.0; // 振幅初始估计 B = 2.0 * std::acos(-1.0) / (7680/2); // 频率初始估计,假设周期为12h C = 0.0; // 相位初始估计 D = 150.0; // 偏移量初始估计 |
5.4.5.1.3正弦函数拟合模型
double myData::sine_model(long long x, double A, double B, double C, double D) { return A * std::sin(B * x + C) + D; } |
5.4.5.1.4计算误差平方和
double myData:: compute_error(const std::vector<long long>& x_data, const std::vector<int>& y_data, double A, double B, double C, double D) { double error = 0.0; for (size_t i = 0; i < x_data.size(); ++i) { double y_pred = sine_model(x_data[i], A, B, C, D); error += std::pow(y_data[i] - y_pred, 2); } return error; } |
5.4.5.1.5梯度下降法优化参数
void myData::gradient_descent(const std::vector<long long>& x_data, const std::vector<int>& y_data,double& A, double& B, double& C, double& D,double learning_rate, int iterations) { for (int iter = 0; iter < iterations; ++iter) { double dA = 0.0, dD = 0.0; double dB = 0.0, dC = 0.0; //double error = compute_error(x_data, y_data, A, B, C, D); // 计算梯度 for (size_t i = 0; i < x_data.size(); ++i) { long long x = x_data[i]; double y = y_data[i]; //qDebug()<<x<<"---------"<<y; double y_pred = sine_model(x,A,B,C,D); dA += (y - y_pred) * std::sin(B * x + C); dB += (y - y_pred) * A * x * std::cos(B * x + C); dC += (y - y_pred) * A * std::cos(B * x + C); dD += (y - y_pred); } // 更新参数 A -= learning_rate * dA / x_data.size(); B -= learning_rate * dB / x_data.size(); C -= learning_rate * dC / x_data.size(); D-= learning_rate * dD / x_data.size(); // 输出迭代信息(可选) if (iter % 100 == 0) { qDebug() << "Iteration " << iter << ": A=" << A << ", B=" << B << ", C=" << C << ", D=" << D<< "; } } } |
5.4.5.2线性回归
线性回归是一种统计学上分析数据的方法,用来确定两种或两种以上变量之间关系的强度和方向。它通常用于预测一个因变量(或响应变量)基于一个或多个自变量(或预测变量)的变化。这种方法得名于它使用一个或多个独立变量(或特征)的线性组合来预测因变量(或目标变量)。
通过将时间转化为时间戳:X;汽车流量:Y之后就可以进行线性回归分析,Y=K*X+b;
通过最小二乘法计算线性回归的系数:
pair<double,double>calculateLinearRegression(vector<double>& x, vector<double>& y) { int n = x.size(); // 计算x和y的均值 double mean_x = accumulate(x.begin(), x.end(), 0.0) / n; double mean_y = accumulate(y.begin(), y.end(), 0.0) / n; // 计算x和y的乘积之和,以及x的平方和 double sum_xy = 0.0; double sum_x2 = 0.0; for (int i = 0; i < n; ++i) { sum_xy += x[i] * y[i]; sum_x2 += pow(x[i], 2); } // 计算斜率m和截距b double m = (n * sum_xy - accumulate(x.begin(), x.end(), 0.0) * accumulate(y.begin(), y.end(), 0.0)) /(n * sum_x2 - pow(accumulate(x.begin(), x.end(), 0.0), 2)); double b = mean_y - m * mean_x; return {m, b}; } |
将 OpenCV 的图像格式转换为 Qt 的图像格式。函数检查输入的 cv::Mat 对象的类型。OpenCV 支持多种图像类型,但此函数主要处理三种类型:CV_8UC1(单通道8位无符号整数,通常用于灰度图像)、CV_8UC3(三通道8位无符号整数,通常用于彩色图像)和 CV_8UC4(四通道8位无符号整数,通常用于带有 alpha 通道的彩色图像)。
QImage Run:: getQImage(const cv::Mat& mat) { if (mat.type() == CV_8UC1) { QImage image(mat.cols, mat.rows, QImage::Format_Indexed8); image.setColorCount(256); for (int i = 0; i < 256; i++) { image.setColor(i, qRgb(i, i, i)); } uchar* pSrc = mat.data; for (int row = 0; row < mat.rows; row++) { uchar* pDest = image.scanLine(row); memcpy(pDest, pSrc, mat.cols); pSrc += mat.step; } return image; } else if (mat.type() == CV_8UC3) { const uchar* pSrc = (const uchar*)mat.data; QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); return image.rgbSwapped(); } else if (mat.type() == CV_8UC4) { const uchar* pSrc = (const uchar*)mat.data; QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_ARGB32); return image.copy(); } return QImage(); } |
QT事件循环是Qt框架中处理事件的核心机制,它确保了应用程序能够响应并处理各种异步事件,事件循环通过一个事件队列来管理和调度事件。当队列中有事件时,事件循环会从队列中依次取出事件并处理,直到队列为空或者事件循环被中断。
QT信号槽是Qt框架中一种独特且强大的对象间通信机制。信号(signal)是对象发出的一种特定事件,而槽(slot)则是用于响应这些信号的特定函数或方法。当某个对象发出信号时,与之相关联的槽函数会被自动调用,从而实现了对象间的通信和协作。
信号和槽的连接可以是一对一、多对一、一对多或者多对多,这使得对象间的通信非常灵活。同时,信号和槽机制还具有解耦和灵活性的优点,因为对象之间通过信号和槽进行通信,彼此之间不需要显式的引用,从而实现了松耦合的设计。此外,这种机制还具有可扩展性,因为可以动态地连接和断开信号和槽,使得系统更容易扩展和维护。
在程序开始就新建一个线程,用于专门处理图像并计数,这样异步的设计将鼠标事件,绘图事件以及视频处理异步解耦,这种设计方式不仅提高了程序运行速度,而且使得人机交互更加灵活。
SqLite,QT-5.15.2,OpenCV-4.5.2
8.运行指导
在Windows下直接解压压缩包,双击exe文件即可执行;启动之初会加载一段时间,这是机器学习的过程;由于没有外接摄像头,因此只在可执行程序目录下放了一段模拟视频,视频较短,视频播放结束程序就停止运行。