使用 C++/OpenCV 和 libevent 构建远程智能停车场管理系统

发布于:2025-06-14 ⋅ 阅读:(26) ⋅ 点赞:(0)

使用 C++/OpenCV 和 libevent 构建远程智能停车场管理系统

在之前的文章中,我们构建了一个本地运行的停车场视觉系统。本文将进行一次重大升级,通过集成高性能网络库 libevent,将其改造为一个可以通过网络 API 远程查询的后端服务。

最终,我们将得到一个 C++ 服务程序,它能:

  1. 使用 OpenCV 持续分析摄像头视频,判断车位占用情况。
  2. 利用 libevent 启动一个轻量级的 HTTP 服务器。
  3. 提供一个 JSON API 接口,任何客户端(如浏览器、手机 App、命令行工具)都可以通过网络请求,实时获取停车场车位的详细状态。

核心架构


🏗️ 核心技术栈

  • OpenCV: 负责所有核心的计算机视觉任务,包括图像处理和占用分析。
  • libevent: 一个轻量级、高性能的开源事件通知库。我们将使用它的 HTTP 模块,以极少的代码快速构建一个稳定、异步的 HTTP 服务器来提供我们的 API。
  • nlohmann/json: 一个非常流行的 C++ JSON 库。它以单个头文件的形式存在,使用简单,能轻松地将我们的数据结构序列化为 JSON 字符串。
  • 多线程 (C++ std::thread): 我们会将耗时的 OpenCV 视觉处理放在一个独立的工作线程中,而 libevent 的网络事件处理则在主线程中运行。这可以确保网络请求的响应不会被视频处理任务阻塞。

🛠️ 环境与准备

除了 C++ 编译器和 OpenCV 之外,你还需要准备:

  1. libevent 开发库:

    • 在 Debian/Ubuntu 系统上: sudo apt-get install libevent-dev
    • 在 macOS 上 (使用 Homebrew): brew install libevent
  2. nlohmann/json 头文件:

    • 这是一个仅头文件的库,无需编译。
    • GitHub 下载最新的 json.hpp 文件,并将其放在你的项目目录中。
  3. 前期准备:

    • 你需要一个由上一篇文章中的校准程序生成的 parking_spots.xml 文件。
    • 一个用于测试的停车场视频文件,例如 parking_video.mp4

💻 代码实现 (parking_server.cpp)

我们将所有逻辑整合到一个名为 parking_server.cpp 的文件中。这个程序将同时负责视觉分析和网络服务。

1. 包含头文件与全局定义

#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <mutex>
#include <chrono>

// OpenCV
#include <opencv2/opencv.hpp>

// libevent
#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>

// JSON
#include "json.hpp" // 确保 json.hpp 在你的项目中
using json = nlohmann::json;

// --- 全局共享数据 ---

// 定义每个车位的状态
struct ParkingSpotStatus {
    int id;
    bool occupied;
    cv::RotatedRect position;
};

// 存储所有车位状态的容器和用于保护它的互斥锁
std::vector<ParkingSpotStatus> g_spot_statuses;
std::mutex g_status_mutex;

// 用于判断占用的边缘像素阈值
const int EDGE_PIXEL_THRESHOLD = 300;

2. 视觉处理工作线程

这个函数将在一个独立的线程中循环运行,不断处理视频帧并更新全局的车位状态。

void vision_processing_thread(const std::string& video_path) {
    // 加载预先校准的车位位置
    std::vector<cv::RotatedRect> parkingSpots;
    cv::FileStorage fs("parking_spots.xml", cv::FileStorage::READ);
    if (!fs.isOpened()) {
        std::cerr << "Vision Thread Error: Could not open parking_spots.xml." << std::endl;
        return;
    }
    fs["parking_spots"] >> parkingSpots;
    fs.release();

    // 初始化全局状态
    {
        std::lock_guard<std::mutex> lock(g_status_mutex);
        for (size_t i = 0; i < parkingSpots.size(); ++i) {
            g_spot_statuses.push_back({(int)i, false, parkingSpots[i]});
        }
    }

    cv::VideoCapture cap(video_path);
    if (!cap.isOpened()) {
        std::cerr << "Vision Thread Error: Could not open video file." << std::endl;
        return;
    }

    cv::Mat frame, gray, roi, edges;
    while (true) {
        if (!cap.read(frame)) {
            // 视频播放完毕后,重置到开头继续循环
            cap.set(cv::CAP_PROP_POS_FRAMES, 0);
            continue;
        }

        cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
        std::vector<ParkingSpotStatus> current_statuses;

        // 分析每个车位
        for (size_t i = 0; i < parkingSpots.size(); ++i) {
            cv::Rect br = parkingSpots[i].boundingRect();
            br &= cv::Rect(0, 0, frame.cols, frame.rows);
            if (br.width == 0 || br.height == 0) continue;
            
            roi = gray(br);
            cv::Canny(roi, edges, 100, 200);
            int edge_pixels = cv::countNonZero(edges);
            bool occupied = edge_pixels > EDGE_PIXEL_THRESHOLD;
            current_statuses.push_back({(int)i, occupied, parkingSpots[i]});
        }
        
        // 使用互斥锁安全地更新全局状态
        {
            std::lock_guard<std::mutex> lock(g_status_mutex);
            g_spot_statuses = current_statuses;
        }

        // 等待一小段时间,避免 CPU 100%
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

3. libevent HTTP 请求处理函数

这是我们的 API 核心。每当有 HTTP 请求进来,libevent 就会调用这个函数。

void http_request_handler(struct evhttp_request *req, void *arg) {
    json response_json;
    int available_spots = 0;

    // 使用互斥锁安全地读取全局状态
    {
        std::lock_guard<std::mutex> lock(g_status_mutex);
        json spots_array = json::array();
        for (const auto& status : g_spot_statuses) {
            if (!status.occupied) {
                available_spots++;
            }
            json spot_obj;
            spot_obj["id"] = status.id;
            spot_obj["occupied"] = status.occupied;
            spot_obj["center_x"] = status.position.center.x;
            spot_obj["center_y"] = status.position.center.y;
            spots_array.push_back(spot_obj);
        }
        response_json["total_spots"] = g_spot_statuses.size();
        response_json["available_spots"] = available_spots;
        response_json["spots"] = spots_array;
    }

    // 创建响应
    struct evbuffer *buf = evbuffer_new();
    if (!buf) {
        std::cerr << "Failed to create response buffer." << std::endl;
        return;
    }

    // 设置 HTTP 头,告诉客户端我们返回的是 JSON
    evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "application/json");

    // 将 JSON 字符串添加到响应体
    std::string json_str = response_json.dump(4); // dump(4) for pretty-printing
    evbuffer_add_printf(buf, "%s", json_str.c_str());

    // 发送响应
    evhttp_send_reply(req, HTTP_OK, "OK", buf);
    evbuffer_free(buf);
}

4. main 函数: 启动一切

主函数负责初始化 libevent,启动视觉线程,并进入 libevent 的事件循环。

int main(int argc, char **argv) {
    if (argc != 3) {
        std::cout << "Usage: ./parking_server <video_file> <port>" << std::endl;
        return -1;
    }
    std::string video_path = argv[1];
    int port = std::atoi(argv[2]);

    // 1. 在新线程中启动视觉处理
    std::thread vision_thread(vision_processing_thread, video_path);
    vision_thread.detach(); // 让视觉线程在后台自由运行

    // 2. 初始化 libevent
    struct event_base *base = event_base_new();
    struct evhttp *http = evhttp_new(base);
    
    // 3. 设置通用的请求处理回调函数
    evhttp_set_gencb(http, http_request_handler, NULL);
    
    // 4. 绑定端口并监听
    if (evhttp_bind_socket(http, "0.0.0.0", port) != 0) {
        std::cerr << "Error: Could not bind to port " << port << std::endl;
        return -1;
    }
    
    std::cout << "Server started. Listening on http://0.0.0.0:" << port << std::endl;
    std::cout << "Waiting for vision thread to initialize..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(5)); // 等待视觉线程完成第一次分析
    std::cout << "Ready to accept requests." << std::endl;

    // 5. 启动事件循环 (此函数会阻塞)
    event_base_dispatch(base);

    // 清理资源
    evhttp_free(http);
    event_base_free(base);

    return 0;
}

🚀 编译与运行

  1. 编译:
    这个命令比之前复杂,因为它需要链接 libeventpthread

    g++ -o parking_server parking_server.cpp $(pkg-config --cflags --libs opencv4 libevent) -lpthread -std=c++17
    

    确保你使用的是支持 C++17 的编译器。

  2. 运行:

    • 第一步: 确保你的 parking_spots.xml 文件在同一目录下。
    • 第二步: 启动服务器,指定视频文件和端口号。
      ./parking_server parking_video.mp4 8080
      
      服务器启动后,你会在终端看到 “Server started. Listening on http://0.0.0.0:8080”。
    • 第三步: 远程访问。打开一个新的终端(可以在另一台电脑上,只需将 127.0.0.1 替换为服务器的 IP 地址),使用 curl 工具来请求 API:
      curl http://127.0.0.1:8080/
      
    • 预期输出: 你将会看到一个格式化的 JSON 响应,实时反映了停车场的状况:
      {
          "available_spots": 18,
          "total_spots": 25,
          "spots": [
              {
                  "center_x": 150.5,
                  "center_y": 230.0,
                  "id": 0,
                  "occupied": true
              },
              {
                  "center_x": 250.8,
                  "center_y": 232.1,
                  "id": 1,
                  "occupied": false
              },
              // ... more spots
          ]
      }
      

总结与展望

我们成功地将一个本地的 OpenCV 应用改造成了一个功能强大的、基于网络的远程服务。通过 libevent,我们以非常高效和简洁的方式实现了网络通信。

下一步可以做什么?

  • Web 前端: 创建一个简单的 HTML 和 JavaScript 前端页面,使用 fetch API 定期调用这个后端接口,并在网页上以图形化方式动态展示停车场地图和状态。
  • 数据持久化: 将车位状态的变化记录到数据库(如 SQLite 或 PostgreSQL)中,用于历史数据分析。
  • 功能扩展: 增加更多的 API 端点,例如 /api/spot/{id} 来获取单个车位的详细信息,或者 /api/history 来查询历史占用率。
  • 部署: 使用 Docker 将此服务容器化,以便在任何服务器上轻松部署。