使用 C++/OpenCV 与 Libevent 打造实时 AI 视觉对话应用
🚀 使用 C++/OpenCV 与 Libevent 打造实时 AI 视觉对话应用
本文将详细介绍如何构建一个高性能的后端服务,该服务能够接收来自浏览器的实时视频流,并将其与用户提问一起发送给多模态大语言模型(如 Google Gemini),最终将 AI 的回答实时返回给浏览器。这是一个融合了 Web 技术、高性能网络编程和人工智能的有趣项目。
(图片来源: Google I/O 2024)
核心架构设计 🏛️
我们的系统分为两大部分:前端(浏览器)和后端(C++ 服务器)。
-
前端 (Browser):
- 使用
HTML和CSS构建基本的用户界面,包括一个视频播放区、聊天记录区和输入框。 - 通过
JavaScript调用navigator.mediaDevices.getUserMediaAPI 来访问用户的摄像头。 - 使用
WebSocket与后端服务器建立持久的双向连接。 - 定期将摄像头画面绘制到
<canvas>上,并将其编码为 Base64 格式的 JPEG 图像,通过 WebSocket 发送给后端。 - 发送用户的文本提问,并接收和显示来自后端的 AI 回答。
- 使用
-
后端 (C++ Server):
- 网络层: 使用
Libevent构建一个异步、事件驱动的 HTTP 和 WebSocket 服务器。Libevent的高性能 I/O 模型非常适合处理大量并发连接和数据。 - 图像处理层: 使用
OpenCV库来解码从前端接收到的 Base64 图像数据,将其转换成标准的图像格式(cv::Mat),以便后续处理。 - AI 通信层: 使用一个 HTTP 客户端库(如
libcurl)来调用大模型的 REST API。它负责构建请求(包含图像和文本),并解析返回的 JSON 结果。 - 数据解析: 使用
nlohmann/json库来方便地处理与大模型 API 交互的 JSON 数据。
- 网络层: 使用
数据流转路径:
浏览器摄像头 -> JS 获取帧 -> Base64 编码 -> WebSocket 发送 -> C++ Libevent 接收 -> Base64 解码 -> OpenCV 处理 -> libcurl 提交到 LLM API -> LLM 返回 JSON -> C++ 解析 -> WebSocket 发送 -> JS 接收并显示
🛠️ 环境准备与依赖
在开始编码之前,请确保你已安装以下库:
- GCC/Clang: C++ 编译器。
- CMake: 用于项目构建。
- Libevent:
sudo apt-get install libevent-dev或通过源码编译。 - OpenCV:
sudo apt-get install libopencv-dev或通过源码编译。 - Libcurl:
sudo apt-get install libcurl4-openssl-dev。 - nlohmann/json: 这是一个头文件库,直接将其
json.hpp文件包含到你的项目中即可。 - libb64: 一个简单的 Base64 编解码库。
👨💻 后端 C++ 实现
后端代码是整个项目的核心,我们将分步讲解关键部分。
1. Libevent 服务器基础
我们首先用 Libevent 的 evhttp 模块创建一个简单的 HTTP 服务器,它能响应 WebSocket 的升级请求。
#include <iostream>
#include <string>
#include <vector>
#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>
#include <event2/keyvalq_struct.h>
void generic_handler(struct evhttp_request *req, void *arg) {
// 默认处理,比如提供 index.html
// ...
}
void websocket_handler(struct evhttp_request *req, void *arg) {
// 这里将处理 WebSocket 连接
// ...
}
int main() {
struct event_base *base = event_base_new();
struct evhttp *http = evhttp_new(base);
evhttp_set_cb(http, "/ws", websocket_handler, nullptr);
evhttp_set_gencb(http, generic_handler, nullptr);
if (evhttp_bind_socket(http, "0.0.0.0", 8088) != 0) {
std::cerr << "Couldn't bind to port 8088. Exiting." << std::endl;
return 1;
}
std::cout << "Server listening on http://0.0.0.0:8088" << std::endl;
event_base_dispatch(base);
evhttp_free(http);
event_base_free(base);
return 0;
}
2. WebSocket 握手与数据处理
Libevent 本身不直接支持 WebSocket 协议,但我们可以手动实现握手过程。当收到 /ws 请求时,检查 Upgrade 头。
// 伪代码,放在 websocket_handler 中
void websocket_handler(struct evhttp_request *req, void *arg) {
// 1. 检查请求头是否包含 "Upgrade: websocket"
const char* upgrade_header = evhttp_find_header(request_get_input_headers(req), "Upgrade");
if (!upgrade_header || strsecmp(upgrade_header, "websocket") != 0) {
evhttp_send_error(req, HTTP_BADREQUEST, "Not a websocket request");
return;
}
// 2. 实现 WebSocket 握手
// a. 获取 Sec-WebSocket-Key
const char* key = evhttp_find_header(evhttp_request_get_input_headers(req), "Sec-WebSocket-Key");
// b. 拼接 Magic String: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
std::string accept_key = std::string(key) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
// c. 计算 SHA1 哈希
// d. 进行 Base64 编码
// e. 设置响应头 Sec-WebSocket-Accept
struct evkeyvalq* headers = evhttp_request_get_output_headers(req);
evhttp_add_header(headers, "Upgrade", "websocket");
evhttp_add_header(headers, "Connection", "Upgrade");
evhttp_add_header(headers, "WebSocket-Accept", /* [Encoded Accept Key] */);
// 3. 发送 101 Switching Protocols 响应
evhttp_send_reply(req, HTTP_SWITCHING_PROTOCOLS, "Switching Protocols", nullptr);
// 4. 将连接的 bufferevent 从 http 中解放出来,用于原始数据读写
struct evhttp_connection *evcon = evhttp_request_get_connection(req);
struct bufferevent *bev = evhttp_connection_get_bufferevent(evcon);
// 设置读写回调函数来处理 WebSocket 数据帧
// bufferevent_setcb(bev, websocket_read_cb, nullptr, websocket_event_cb, nullptr);
}
在 websocket_read_cb 中,你需要解析 WebSocket 数据帧(这部分比较复杂,需要处理掩码、opcode 等),提取出负载数据。
3. 解码图像并调用 LLM API
当从 WebSocket 帧中成功提取出 Base64 编码的图像数据后:
#include <opencv2/opencv.hpp>
#include <nlohmann/json.hpp>
#include "b64/decode.h" // Base64 解码库
// 假设从 WebSocket 收到 JSON: { "type": "image", "data": "base64_string...", "prompt": "user_question" }
void process_incoming_data(const std::string& jsonData) {
using json = nlohmann::json;
auto parsed = json::parse(jsonData);
if (parsed["type"] == "image") {
std::string prompt = parsed["prompt"];
std::string b64_image = parsed["data"]; // data:image/jpeg;base64,.....
// 移除 data URL 头部
std::string b64_data = b64_image.substr(b64_image.find(",") + 1);
// Base64 解码
base64::decoder D;
std::vector<char> decoded_bytes(b64_data.size());
int len = D.decode(b64_data.c_str(), b64_data.size(), decoded_bytes.data());
decoded_bytes.resize(len);
// 使用 OpenCV 从内存解码
cv::Mat frame = cv::imdecode(decoded_bytes, cv::IMREAD_COLOR);
if (frame.empty()) {
std::cerr << "Failed to decode image" << std::endl;
return;
}
// 可以在这里对 frame 做一些预处理,如缩放
// cv::resize(frame, frame, cv::Size(512, 512));
// 调用大模型 API (使用 libcurl)
std::string llm_response = call_gemini_vision_api(prompt, decoded_bytes);
// 将 llm_response 通过 WebSocket 发回客户端
// ...
}
}
call_gemini_vision_api 函数大致如下(使用 libcurl):
#include <curl/curl.h>
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
((std::string*)userp)->append((char*)contents, size * nmemb);
return size * nmemb;
}
std::string call_gemini_vision_api(const std::string& prompt, const std::vector<char>& image_bytes) {
CURL *curl;
CURLcode res;
std::string readBuffer;
std::string api_key = "YOUR_GEMINI_API_KEY";
std::string url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=" + api_key;
// 使用 nlohmann/json 构建请求体
using json = nlohmann::json;
json request_body = {
{"contents", {{
{"parts", {
{{"text", prompt}},
{{"inline_data", {
{"mime_type", "image/jpeg"},
{"data", base64::encoder::encode(image_bytes.data(), image_bytes.size())} // 重新编码
}}}
}}
}}}
};
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl) {
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, ur.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEAD, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.dump().c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
curl_easy_cleanup(curl);
}
curl_global_cleanup();
// 解析 readBuffer (JSON),提取模型回答的文本
// ...
return extracted_text;
}
🌐 前端 HTML 与 JavaScript 实现
前端部分相对直接。我们创建一个 index.html。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AI Vision Chat</title>
<style>
body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; }
#container { display: flex; gap: 20px; }
#video-container { border: 1px solid black; }
#chat-box { width: 400px; height: 480px; border: 1px solid #ccc; overflow-y: scroll; padding: 10px; }
#input-area { display: flex; width: 100%; margin-top: 10px; }
#prompt { flex-grow: 1; }
</style>
</head>
<body>
<h1>C++ AI Vision Chat 🤖</h1>
<div id="container">
<div id="video-container">
<video id="video" width="640" height="480" autoplay playsinline></video>
</div>
<div id="chat-box"></div>
</div>
<div id="input-area">
<input type="text" id="prompt" placeholder="Ask something about the video...">
<button id="send-btn">Send</button>
</div>
<canvas id="canvas" width="640" height="480" style="display:none;"></canvas>
<script>
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const chatBox = document.getElementById('chat-box');
const promptInput = document.getElementById('prompt');
const sendBtn = document.getElementById('send-btn');
const ws = new WebSocket('ws://' + window.location.host + '/ws');
ws.onopen = () => {
console.log('Connected to server.');
};
ws.onmessage = (event) => {
appendMessage('AI', event.data);
};
ws.onclose = () => {
console.log('Disconnected from server.');
};
// 获取摄像头权限
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
video.srcObject = stream;
}).catch(err => {
console.error("Error accessing camera: ", err);
});
function appendMessage(sender, message) {
msgElem.innerHTML = `<strong>${sender}:</strong> ${message}`;
chatBox.appendChild(msgElem);
chatBox.scrollTop = chatBox.scrollHeight;
}
function sendData() {
if (ws.readyState !== WebSocket.OPEN) return;
const prompt = promptInput.value;
if (!prompt) {
alert('Please enter a prompt.');
return;
}
appendMessage('You', prompt);
// 绘制当前视频帧到 canvas
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// 获取 Base64 编码的图像数据
const imageData = canvas.toDataURL('image/jpeg', 0.7);
const payload = JSON.stringify({
type: 'image',
prompt: prompt,
data: imageData
});
ws.send(payload);
promptInput.value = '';
}
promptInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendData();
});
</script>
</body>
</html>
🚀 整合与运行
-
编译后端:
g++ main.cpp -o server -std=c++17 -levent -lopencv_core -lopencv_imgcodecs -lcurl -ljsoncpp # (或链接你的json库)(注意:实际编译命令可能更复杂,建议使用 CMake)
-
运行服务器:
./server -
访问前端:
在浏览器中打开http://localhost:8088。你需要一个提供index.html的处理逻辑(在上面的generic_handler中实现)。
现在,你应该能看到自己的摄像头画面了。在输入框中提问(例如,“What do you see?”),点击发送,稍等片刻,AI 的回答就会出现在聊天框中。
总结与展望
我们成功地使用 C++、Libevent 和 OpenCV 构建了一个高性能的 AI 视觉应用后端。这种技术栈虽然比 Node.js 或 Python 实现起来更复杂,但它提供了无与伦比的性能和对内存、线程的精细控制能力,非常适合对性能要求极高的生产环境。
未来的改进方向:
- 安全性: 使用 WSS (WebSocket Secure) 和 HTTPS。
- 错误处理: 增加更健壮的错误处理和重连机制。
- 流式响应: 对接支持流式输出的 LLM API,实现打字机效果,提升用户体验。
- 视频流: 替换单帧发送为真正的视频流处理(例如 WebRTC),但这将大大增加复杂性。
更多推荐


所有评论(0)