使用 C++/OpenCV 和 libevent 构建远程智能停车场管理系统
使用 C++/OpenCV 和 libevent 构建远程智能停车场管理系统
在之前的文章中,我们构建了一个本地运行的停车场视觉系统。本文将进行一次重大升级,通过集成高性能网络库 libevent
,将其改造为一个可以通过网络 API 远程查询的后端服务。
最终,我们将得到一个 C++ 服务程序,它能:
- 使用 OpenCV 持续分析摄像头视频,判断车位占用情况。
- 利用
libevent
启动一个轻量级的 HTTP 服务器。 - 提供一个 JSON API 接口,任何客户端(如浏览器、手机 App、命令行工具)都可以通过网络请求,实时获取停车场车位的详细状态。
核心架构
🏗️ 核心技术栈
- OpenCV: 负责所有核心的计算机视觉任务,包括图像处理和占用分析。
- libevent: 一个轻量级、高性能的开源事件通知库。我们将使用它的 HTTP 模块,以极少的代码快速构建一个稳定、异步的 HTTP 服务器来提供我们的 API。
- nlohmann/json: 一个非常流行的 C++ JSON 库。它以单个头文件的形式存在,使用简单,能轻松地将我们的数据结构序列化为 JSON 字符串。
- 多线程 (C++
std::thread
): 我们会将耗时的 OpenCV 视觉处理放在一个独立的工作线程中,而libevent
的网络事件处理则在主线程中运行。这可以确保网络请求的响应不会被视频处理任务阻塞。
🛠️ 环境与准备
除了 C++ 编译器和 OpenCV 之外,你还需要准备:
-
libevent 开发库:
- 在 Debian/Ubuntu 系统上:
sudo apt-get install libevent-dev
- 在 macOS 上 (使用 Homebrew):
brew install libevent
- 在 Debian/Ubuntu 系统上:
-
nlohmann/json 头文件:
- 这是一个仅头文件的库,无需编译。
- 从 GitHub 下载最新的
json.hpp
文件,并将其放在你的项目目录中。
-
前期准备:
- 你需要一个由上一篇文章中的校准程序生成的
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 头,告诉客户端我们返回的是 JSONevhttp_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-printingevbuffer_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. 初始化 libeventstruct 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;
}
🚀 编译与运行
-
编译:
这个命令比之前复杂,因为它需要链接libevent
和pthread
。g++ -o parking_server parking_server.cpp $(pkg-config --cflags --libs opencv4 libevent) -lpthread -std=c++17
确保你使用的是支持 C++17 的编译器。
-
运行:
- 第一步: 确保你的
parking_spots.xml
文件在同一目录下。 - 第二步: 启动服务器,指定视频文件和端口号。
服务器启动后,你会在终端看到 “Server started. Listening on http://0.0.0.0:8080”。./parking_server parking_video.mp4 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 将此服务容器化,以便在任何服务器上轻松部署。