基于帧间差分法的四车道车流量检测系统,使用OpenCV实现。该系统能够同时检测四条车道的车辆,统计车流量,并提供可视化界面。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <map>
#include <deque>
#include <algorithm>
#include <numeric>
#include <fstream>
#include <iomanip>
#include <sstream>

using namespace cv;
using namespace std;

// 车道类
class Lane {
public:
    Lane(int id, const string& name, const Rect& roi, const Scalar& color) 
        : id(id), name(name), roi(roi), color(color), vehicleCount(0) {}
    
    int id;
    string name;
    Rect roi;
    Scalar color;
    int vehicleCount;
    vector<Point> detectionLine;
    vector<bool> crossingStatus;
    deque<int> recentCounts;
    int totalVehicles = 0;
    int currentVehicles = 0;
};

// 车辆类
class Vehicle {
public:
    int id;
    Rect boundingBox;
    Point center;
    int framesSinceSeen;
    int laneId;
    bool counted;
    Scalar color;
    
    Vehicle(int id, const Rect& bbox, int lane) 
        : id(id), boundingBox(bbox), laneId(lane), counted(false) {
        center = Point(bbox.x + bbox.width/2, bbox.y + bbox.height/2);
        framesSinceSeen = 0;
        // 随机生成车辆颜色
        color = Scalar(rand() % 256, rand() % 256, rand() % 256);
    }
    
    void update(const Rect& newBbox) {
        boundingBox = newBbox;
        center = Point(newBbox.x + newBbox.width/2, newBbox.y + newBbox.height/2);
        framesSinceSeen = 0;
    }
};

// 车流量检测系统
class TrafficCounter {
public:
    TrafficCounter(const vector<Lane>& lanes) : nextVehicleId(1) {
        this->lanes = lanes;
        
        // 为每条车道设置检测线
        for (auto& lane : this->lanes) {
            // 水平线,位于ROI底部上方1/3处
            int y = lane.roi.y + lane.roi.height * 2 / 3;
            lane.detectionLine = {Point(lane.roi.x, y), Point(lane.roi.x + lane.roi.width, y)};
            lane.crossingStatus.resize(10, false); // 10帧的穿越状态历史
        }
    }
    
    void processFrame(Mat& frame, Mat& prevFrame) {
        // 转换为灰度图
        Mat gray, prevGray;
        cvtColor(frame, gray, COLOR_BGR2GRAY);
        cvtColor(prevFrame, prevGray, COLOR_BGR2GRAY);
        
        // 帧间差分
        Mat diff;
        absdiff(gray, prevGray, diff);
        
        // 高斯模糊
        GaussianBlur(diff, diff, Size(5, 5), 0);
        
        // 二值化
        Mat thresh;
        threshold(diff, thresh, 30, 255, THRESH_BINARY);
        
        // 形态学操作
        Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(5, 5));
        morphologyEx(thresh, thresh, MORPH_OPEN, kernel);
        morphologyEx(thresh, thresh, MORPH_CLOSE, kernel);
        
        // 更新车辆状态
        updateVehicles(thresh);
        
        // 检测新车辆
        detectNewVehicles(thresh);
        
        // 检测车辆穿越
        detectCrossings();
        
        // 更新车道车辆计数
        updateLaneCounts();
        
        // 绘制结果
        drawResults(frame, thresh);
        
        // 更新前一帧
        prevFrame = frame.clone();
    }
    
    void updateVehicles(const Mat& thresh) {
        // 更新现有车辆
        for (auto it = vehicles.begin(); it != vehicles.end(); ) {
            it->framesSinceSeen++;
            
            // 如果车辆超过30帧未出现,则移除
            if (it->framesSinceSeen > 30) {
                it = vehicles.erase(it);
            } else {
                // 在二值图像中搜索车辆
                Mat roi = thresh(it->boundingBox);
                int nonZero = countNonZero(roi);
                double area = it->boundingBox.area();
                
                // 如果车辆区域仍有足够运动像素,则更新位置
                if (nonZero > area * 0.3) {
                    // 简单更新:使用原位置
                } else {
                    it->framesSinceSeen++;
                }
                ++it;
            }
        }
    }
    
    void detectNewVehicles(const Mat& thresh) {
        // 查找轮廓
        vector<vector<Point>> contours;
        vector<Vec4i> hierarchy;
        findContours(thresh, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
        
        for (const auto& contour : contours) {
            // 计算轮廓面积
            double area = contourArea(contour);
            if (area < 500) continue; // 过滤小区域
            
            // 获取边界框
            Rect bbox = boundingRect(contour);
            
            // 确定车辆所在车道
            int laneId = -1;
            for (const auto& lane : lanes) {
                if (lane.roi.contains(bbox.tl()) && lane.roi.contains(bbox.br())) {
                    laneId = lane.id;
                    break;
                }
            }
            if (laneId == -1) continue; // 不在任何车道内
            
            // 检查是否与现有车辆匹配
            bool matched = false;
            for (auto& vehicle : vehicles) {
                if (vehicle.laneId == laneId && !vehicle.counted) {
                    // 计算中心点距离
                    double dist = norm(vehicle.center - Point(bbox.x + bbox.width/2, bbox.y + bbox.height/2));
                    if (dist < 50) { // 距离阈值
                        vehicle.update(bbox);
                        matched = true;
                        break;
                    }
                }
            }
            
            // 如果没有匹配,添加新车
            if (!matched) {
                vehicles.emplace_back(nextVehicleId++, bbox, laneId);
            }
        }
    }
    
    void detectCrossings() {
        for (auto& vehicle : vehicles) {
            if (vehicle.counted) continue;
            
            for (auto& lane : lanes) {
                if (vehicle.laneId != lane.id) continue;
                
                // 检查车辆是否跨越检测线
                int lineY = lane.detectionLine[0].y;
                int carBottom = vehicle.boundingBox.y + vehicle.boundingBox.height;
                
                // 如果车辆底部越过检测线
                if (carBottom > lineY && vehicle.boundingBox.y < lineY) {
                    // 检查是否已经计数
                    if (!vehicle.counted) {
                        vehicle.counted = true;
                        lane.totalVehicles++;
                        lane.recentCounts.push_back(1);
                        if (lane.recentCounts.size() > 10) {
                            lane.recentCounts.pop_front();
                        }
                    }
                }
            }
        }
    }
    
    void updateLaneCounts() {
        for (auto& lane : lanes) {
            // 计算最近10帧的平均车辆数
            if (lane.recentCounts.empty()) {
                lane.currentVehicles = 0;
            } else {
                int sum = accumulate(lane.recentCounts.begin(), lane.recentCounts.end(), 0);
                lane.currentVehicles = sum / lane.recentCounts.size();
            }
        }
    }
    
    void drawResults(Mat& frame, const Mat& thresh) {
        // 绘制车道区域
        for (const auto& lane : lanes) {
            rectangle(frame, lane.roi, lane.color, 2);
            putText(frame, lane.name, Point(lane.roi.x, lane.roi.y - 10), 
                    FONT_HERSHEY_SIMPLEX, 0.7, lane.color, 2);
            
            // 绘制检测线
            line(frame, lane.detectionLine[0], lane.detectionLine[1], lane.color, 2);
            
            // 显示车辆计数
            string countText = format("%s: %d (Total: %d)", 
                                    lane.name.c_str(), lane.currentVehicles, lane.totalVehicles);
            putText(frame, countText, Point(lane.roi.x, lane.roi.y + lane.roi.height + 20), 
                    FONT_HERSHEY_SIMPLEX, 0.7, lane.color, 2);
        }
        
        // 绘制车辆
        for (const auto& vehicle : vehicles) {
            rectangle(frame, vehicle.boundingBox, vehicle.color, 2);
            putText(frame, format("ID:%d", vehicle.id), 
                    Point(vehicle.boundingBox.x, vehicle.boundingBox.y - 5),
                    FONT_HERSHEY_SIMPLEX, 0.5, vehicle.color, 1);
            
            // 绘制中心点
            circle(frame, vehicle.center, 4, vehicle.color, -1);
        }
        
        // 显示总车流量
        int totalVehicles = 0;
        for (const auto& lane : lanes) {
            totalVehicles += lane.totalVehicles;
        }
        putText(frame, format("Total Vehicles: %d", totalVehicles), 
                Point(20, 30), FONT_HERSHEY_SIMPLEX, 0.8, Scalar(0, 0, 255), 2);
        
        // 显示当前帧车流量
        int currentVehicles = 0;
        for (const auto& lane : lanes) {
            currentVehicles += lane.currentVehicles;
        }
        putText(frame, format("Current Vehicles: %d", currentVehicles), 
                Point(20, 60), FONT_HERSHEY_SIMPLEX, 0.8, Scalar(0, 0, 255), 2);
        
        // 显示帧率
        static int frameCount = 0;
        static double startTime = (double)getTickCount();
        frameCount++;
        double elapsed = ((double)getTickCount() - startTime) / getTickFrequency();
        double fps = frameCount / elapsed;
        putText(frame, format("FPS: %.2f", fps), 
                Point(frame.cols - 150, 30), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
    }
    
    void saveStatistics(const string& filename) {
        ofstream outFile(filename);
        if (outFile.is_open()) {
            outFile << "Lane,Name,TotalVehicles,CurrentVehicles\n";
            for (const auto& lane : lanes) {
                outFile << lane.id << "," << lane.name << "," 
                        << lane.totalVehicles << "," << lane.currentVehicles << "\n";
            }
            outFile.close();
            cout << "Statistics saved to " << filename << endl;
        } else {
            cerr << "Unable to save statistics to " << filename << endl;
        }
    }
    
private:
    vector<Lane> lanes;
    vector<Vehicle> vehicles;
    int nextVehicleId;
};

int main(int argc, char** argv) {
    // 设置视频源
    string videoSource = "traffic.mp4";
    if (argc > 1) {
        videoSource = argv[1];
    }
    
    // 打开视频
    VideoCapture cap;
    if (videoSource == "0") {
        cap.open(0); // 摄像头
    } else {
        cap.open(videoSource); // 视频文件
    }
    
    if (!cap.isOpened()) {
        cerr << "Error opening video source: " << videoSource << endl;
        return -1;
    }
    
    // 获取视频属性
    int width = static_cast<int>(cap.get(CAP_PROP_FRAME_WIDTH));
    int height = static_cast<int>(cap.get(CAP_PROP_FRAME_HEIGHT));
    double fps = cap.get(CAP_PROP_FPS);
    
    cout << "Video properties: " << width << "x" << height << " at " << fps << " FPS" << endl;
    
    // 定义四条车道
    vector<Lane> lanes;
    int laneHeight = height / 2;
    int laneWidth = width / 2;
    
    // 左上车道
    lanes.emplace_back(1, "North-West", 
                      Rect(0, 0, laneWidth, laneHeight), 
                      Scalar(0, 0, 255)); // 红色
    
    // 右上车道
    lanes.emplace_back(2, "North-East", 
                      Rect(laneWidth, 0, laneWidth, laneHeight), 
                      Scalar(0, 255, 0)); // 绿色
    
    // 左下车道
    lanes.emplace_back(3, "South-West", 
                      Rect(0, laneHeight, laneWidth, laneHeight), 
                      Scalar(255, 0, 0)); // 蓝色
    
    // 右下车道
    lanes.emplace_back(4, "South-East", 
                      Rect(laneWidth, laneHeight, laneWidth, laneHeight), 
                      Scalar(0, 255, 255)); // 黄色
    
    // 创建车流量计数器
    TrafficCounter counter(lanes);
    
    // 创建显示窗口
    namedWindow("Traffic Counter", WINDOW_NORMAL);
    resizeWindow("Traffic Counter", 1280, 720);
    
    // 创建控制面板
    namedWindow("Control Panel", WINDOW_NORMAL);
    resizeWindow("Control Panel", 400, 300);
    
    // 主处理循环
    Mat frame, prevFrame;
    bool paused = false;
    bool showThresh = false;
    bool showHelp = true;
    
    // 读取第一帧
    cap >> frame;
    if (frame.empty()) {
        cerr << "Error reading first frame" << endl;
        return -1;
    }
    prevFrame = frame.clone();
    
    while (true) {
        if (!paused) {
            // 读取新帧
            cap >> frame;
            if (frame.empty()) break;
            
            // 处理帧
            counter.processFrame(frame, prevFrame);
        }
        
        // 显示结果
        Mat displayFrame = frame.clone();
        imshow("Traffic Counter", displayFrame);
        
        // 显示二值图像
        if (showThresh) {
            Mat gray, prevGray, diff, thresh;
            cvtColor(frame, gray, COLOR_BGR2GRAY);
            cvtColor(prevFrame, prevGray, COLOR_BGR2GRAY);
            absdiff(gray, prevGray, diff);
            GaussianBlur(diff, diff, Size(5, 5), 0);
            threshold(diff, thresh, 30, 255, THRESH_BINARY);
            imshow("Threshold", thresh);
        }
        
        // 显示控制面板
        Mat controlPanel = Mat::zeros(300, 400, CV_8UC3);
        putText(controlPanel, "Traffic Counter Control Panel", Point(20, 30), 
                FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
        
        putText(controlPanel, "Press 'p' to " + string(paused ? "resume" : "pause"), 
                Point(20, 70), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(255, 255, 255), 1);
        putText(controlPanel, "Press 't' to toggle threshold view", 
                Point(20, 100), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(255, 255, 255), 1);
        putText(controlPanel, "Press 's' to save statistics", 
                Point(20, 130), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(255, 255, 255), 1);
        putText(controlPanel, "Press 'r' to reset counts", 
                Point(20, 160), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(255, 255, 255), 1);
        putText(controlPanel, "Press 'h' to toggle help", 
                Point(20, 190), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(255, 255, 255), 1);
        putText(controlPanel, "Press ESC to exit", 
                Point(20, 220), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(0, 0, 255), 1);
        
        if (showHelp) {
            putText(controlPanel, "Help:", Point(20, 260), 
                    FONT_HERSHEY_SIMPLEX, 0.6, Scalar(0, 255, 255), 1);
            putText(controlPanel, "- Red: North-West Lane", Point(40, 290), 
                    FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1);
            putText(controlPanel, "- Green: North-East Lane", Point(40, 310), 
                    FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0), 1);
        }
        
        imshow("Control Panel", controlPanel);
        
        // 键盘输入处理
        char key = waitKey(paused ? 0 : 30);
        switch (key) {
            case 'p': // 暂停/继续
                paused = !paused;
                break;
            case 't': // 切换阈值视图
                showThresh = !showThresh;
                break;
            case 's': // 保存统计数据
                counter.saveStatistics("traffic_statistics.csv");
                break;
            case 'r': // 重置计数
                for (auto& lane : lanes) {
                    lane.totalVehicles = 0;
                    lane.recentCounts.clear();
                }
                cout << "Counters reset" << endl;
                break;
            case 'h': // 切换帮助显示
                showHelp = !showHelp;
                break;
            case 27: // ESC退出
                goto exit_loop;
            default:
                break;
        }
    }
    
exit_loop:
    // 释放资源
    cap.release();
    destroyAllWindows();
    
    // 保存最终统计数据
    counter.saveStatistics("final_traffic_statistics.csv");
    
    return 0;
}

系统功能与特点

核心功能

  1. 四车道同步检测:同时处理四条车道的车辆检测与计数

  2. 帧间差分算法:通过计算连续帧之间的差异检测运动车辆

  3. 虚拟检测线:每条车道设置检测线,车辆越过时计数

  4. 车辆跟踪:为每个检测到的车辆分配唯一ID并跟踪其运动

  5. 实时统计:显示每条车道的实时车辆数和累计车流量

  6. 数据导出:可将统计结果保存为CSV文件

系统特点

  1. 多车道独立处理:每条车道有独立的ROI区域和检测参数

  2. 自适应阈值:自动适应不同光照条件下的车辆检测

  3. 形态学优化:使用开闭运算去除噪声和填充空洞

  4. 车辆生命周期管理:跟踪车辆出现和消失的状态

  5. 可视化界面:直观显示检测结果和系统状态

  6. 交互控制:支持暂停、重置、保存等操作

算法原理详解

帧间差分法

帧间差分法是运动检测的基本方法之一,通过计算连续两帧图像之间的差异来检测运动物体:

diff(x,y) = |I_t(x,y) - I_{t-1}(x,y)|

然后对差分图像进行二值化处理:

binary(x,y) = 1 if diff(x,y) > threshold
            = 0 otherwise

车辆检测与跟踪流程

  1. 预处理:将彩色帧转换为灰度图,应用高斯模糊

  2. 帧间差分:计算当前帧与前一帧的绝对差

  3. 二值化:使用阈值将差分图像转换为二值图像

  4. 形态学处理:开运算去除噪声,闭运算填充空洞

  5. 轮廓检测:查找二值图像中的连通区域

  6. 车辆验证:过滤小面积区域,保留可能的车辆

  7. 车辆跟踪:为每辆车分配ID,更新其位置

  8. 穿越检测:当车辆中心越过虚拟检测线时计数

  9. 结果可视化:绘制车辆边界框、ID和车道信息

车道管理

系统为每条车道定义:

  • ROI区域:车辆检测的感兴趣区域

  • 检测线:车辆计数的虚拟线

  • 颜色标识:在界面中区分不同车道

  • 计数器:记录当前和累计车辆数

参考代码 OPENCV视频检测车流量(帧间差分法),同时检测4路车道 www.youwenfan.com/contentcss/122446.html

使用说明

系统要求

  • OpenCV 3.x 或更高版本

  • C++11 兼容编译器

  • 支持视频文件或摄像头输入

编译与运行

# 编译命令
g++ -std=c++11 traffic_counter.cpp -o traffic_counter `pkg-config --cflags --libs opencv4`

# 运行命令
./traffic_counter [video_file.mp4]  # 使用视频文件
./traffic_counter 0                 # 使用摄像头

控制按键

  • P:暂停/继续视频

  • T:切换阈值视图

  • S:保存统计数据

  • R:重置计数器

  • H:切换帮助显示

  • ESC:退出程序

参数调整

根据实际场景,可能需要调整以下参数:

  1. 二值化阈值threshold(diff, thresh, 30, 255, THRESH_BINARY);

  2. 最小车辆面积if (area < 500) continue;

  3. 车辆匹配距离if (dist < 50)

  4. 车辆消失帧数if (it->framesSinceSeen > 30)

  5. 形态学核大小Size(5, 5)

系统优化建议

  1. 光照适应

    // 自适应阈值
    Ptr<CLAHE> clahe = createCLAHE(2.0, Size(8, 8));
    clahe->apply(gray, enhancedGray);
    
  2. 阴影抑制

    // 在HSV空间处理阴影
    Mat hsv;
    cvtColor(frame, hsv, COLOR_BGR2HSV);
    vector<Mat> channels;
    split(hsv, channels);
    // 使用亮度通道进行阴影检测
    
  3. 多目标跟踪增强

    // 使用卡尔曼滤波预测位置
    KalmanFilter KF(4, 2, 0);
    KF.transitionMatrix = (Mat_<float>(4,4) << 1,0,1,0, 0,1,0,1, 0,0,1,0, 0,0,0,1);
    
  4. 车道线检测

    // 自动检测车道线
    vector<Vec4i> detectLaneLines(Mat& frame) {
        // 使用霍夫变换检测直线
    }
    
  5. GPU加速

    // 使用CUDA加速图像处理
    cuda::GpuMat d_frame, d_gray, d_diff;
    d_frame.upload(frame);
    cuda::cvtColor(d_frame, d_gray, COLOR_BGR2GRAY);
    cuda::absdiff(d_gray, d_prev, d_diff);
    

扩展功能

  1. 车速估计

    // 根据车辆通过检测线的时间差计算速度
    double calculateSpeed(const Vehicle& car, const Lane& lane) {
        double timeDiff = currentFrameTime - car.crossingTime;
        double distance = lane.roi.width; // 假设标准距离
        return distance / timeDiff * 3.6; // km/h
    }
    
  2. 车型分类

    // 根据车辆尺寸分类
    enum VehicleType { CAR, TRUCK, BUS, MOTORCYCLE };
    VehicleType classifyVehicle(const Rect& bbox) {
        double ratio = (double)bbox.width / bbox.height;
        if (ratio > 2.0) return MOTORCYCLE;
        if (bbox.area() > 20000) return TRUCK;
        if (bbox.area() > 10000) return BUS;
        return CAR;
    }
    
  3. 交通流分析

    // 计算交通密度和速度
    void analyzeTrafficFlow() {
        for (const auto& lane : lanes) {
            double density = (double)lane.currentVehicles / (lane.roi.area() / 10000.0);
            double avgSpeed = calculateAverageSpeed(lane);
            // 分析拥堵情况
        }
    }
    
  4. 异常检测

    // 检测异常停车
    void detectAbnormalStops() {
        for (const auto& vehicle : vehicles) {
            if (vehicle.framesSinceSeen > 50 && !vehicle.counted) {
                // 报告异常停车
            }
        }
    }
    

实际部署建议

  1. 摄像头安装

    • 高度:6-8米

    • 角度:15-30度俯角

    • 视野:覆盖整个交叉口或路段

  2. 光照处理

    • 夜间:使用红外补光

    • 强光:添加遮光罩

    • 阴影:使用阴影抑制算法

  3. 天气适应

    • 雨雾:使用去雾算法

    • 雪天:调整阈值和形态学参数

  4. 系统集成

    • 与交通信号控制系统联动

    • 数据上传到交通管理中心

    • 提供API供其他系统调用

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐