使用 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 头,告诉客户端我们返回的是 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;
}
🚀 编译与运行
编译:
这个命令比之前复杂,因为它需要链接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 将此服务容器化,以便在任何服务器上轻松部署。