使用 OpenCV 处理 esp32-cam 推流
使用 Arduino 刷入 esp32-cam 的 CameraWebServer 示例,然后就可以通过访问 http://ip:80/ 进入控制页,或者访问 http://ip:81/stream 获取“实时视频推流”。
但是这个视频推流并不是常见的视频格式,而是 jpg 图片流,这个可以抓包分析出来。
我在浏览器使用 F12 没法查看具体的响应,所以我用 Fiddler 进行抓包分析的。
我的 esp32-cam 响应的内容如下:
1 2 3 4 5 6 7
| \r\n --123456789000000000000987654321\r\n Content-Type: image/jpeg\r\n Content-Length: 5123 \r\n \r\n /* 这里是jpg图片的二进制内容 */
|
为了方便查看,我把响应中的\r\n都保留了,没有删去。
其中 Content-Length 的内容就是 jpg 图片的长度,所以要先把它的值取出来,然后才能往后取出 jpg 图片的内容。
我的需求是将这一张张 jpg 图片转化为 OpenCV 的 Mat 格式,这样就能使用 OpenCV 作进一步处理了。
因为这个推流是源源不断地进行的,OpenCV 的处理也是不断在进行的,这就需要用两个线程同时进行处理。一个线程不断获取 esp32 推流,另一个线程不断解析响应,并且用 OpenCV 处理响应中的 jpg 图片。另外,两个线程都要访问响应数据,如果同时访问响应数据,可能会出现 race condition,所以还要考虑多线程同步。
我用 libcurl 库发起 HTTP 请求,这是使用 libcurl 发起请求部分的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| static size_t reWriter(char* buffer, size_t size, size_t nmemb, string* content) { unsigned long sizes = size * nmemb; content->append(buffer, sizes); return sizes; }
int main() { curl_global_init(CURL_GLOBAL_ALL); CURL* curl; curl = curl_easy_init();
string url = "http://192.168.1.123:81/stream"; string buffer;
thread th = std::thread([&]() { while (true) { curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)5LL); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false); curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, true); curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 120L); curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 60L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, reWriter); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&(buffer)); curl_easy_setopt(curl, CURLOPT_URL, url.data()); cout << "正在发起请求..." << endl; curl_easy_perform(curl); cout << "连接已经断开!" << endl; } }); th.detach();
return 0; }
|
其中 reWriter 函数是响应数据的处理函数,将接收到的响应数据存起来并返回其长度就好了。
响应的解析部分我直接在主线程进行,先使用正则表达式取出 Content-Length,然后再取出 jpg 图片的内容。
使用 string 作为 buffer 的好处就是可以用 substr 比较方便地取出其中的部分内容。
具体代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| while (true) { int64_t buffer_length = buffer.size(); cout << "buffer_length: " << buffer.size() << endl; regex pattern("\r\n--123456789000000000000987654321\r\nContent-Type: image\/jpeg\r\nContent-Length: ([\\d]+)\r\n\r\n"); smatch result; if (!regex_search(buffer, result, pattern)) continue; if (result.size() <= 1) continue; int64_t image_length = stoll(result[1].str()); int64_t header_length = result.length(0); if (header_length + image_length <= buffer_length) { string image_buffer = buffer.substr(header_length, image_length); buffer.clear(); Mat tmp = Mat(1, image_length, CV_8UC1, (void*)image_buffer.data()); cv::Mat decodedImage = imdecode(tmp, 1); if (decodedImage.data != NULL) { Mat filped; flip(decodedImage, filped, -1); imshow("display", filped); waitKey(10); } } }
|
这里面有一个步骤非常重要,就是每解析一张图片就清除 buffer。
我们接收到的 buffer 里,可能不止含有一份响应。一份响应里包含一个 HTTP 头部和一张 jpg 图片。
变量 buffer 里储存的内容,可能包含一份完整的响应和半份响应(半份响应:比如完整的 HTTP 头部和 jpg 图片的一部分内容,甚至只有一部分 HTTP 头部)。
正常来说,那半份响应应该保留在 buffer 中。等到接收到更多内容时再尝试解析,因为buffer中旧的内容加上新接收的内容可能就凑出了一份完整的响应。如果我们清除掉未解析的数据,会导致丢失掉一些数据!
但是我们的需求有点特殊,因为视频推流对实时性要求很高。我们不在意过去的每一帧内容,只在意当前 esp32-cam 看到的那一帧内容。
如果 esp32-cam 推流的速度大于我们解析的速度,那么 esp32-cam 推送的内容就会堆积在 buffer 变量中(因为 TCP 是可靠的协议,TCP 保证将 esp32-cam 发送到内容送达)。占用大量的内存不说,还会遇到累积延迟的问题。也就是说程序当前解析的一帧,其实发生在过去,表现就是画面的延迟。而且这种延迟是会累积的,随着时间的推移,延迟会越来越大。
所以如果我们解析的时候遇到了很多份响应(说明此时 esp32-cam 推流速度大于解析速度),那么就应该只取其中的一份,其他的响应都丢弃,这样就不会有内容堆积在 buffer 中。
最后,主线程和 libcurl 的线程同时访问了变量 buffer,需要加锁控制。
考虑到只需要在 buffer 被修改后才需要解析 buffer,所以我还用上了条件变量(condition_variable)。
条件变量通常和 lock_guard/unique_lock 一起使用。它会释放锁并阻塞,直到另一个线程将 flag 变为 true,并发出通知后才继续执行。
修改后的完整程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| #include <iostream> #include <string> #include <regex> #include <future> #include <curl/curl.h> #include <opencv2/opencv.hpp> using namespace std; using namespace cv;
std::mutex mutex_buffer; std::condition_variable cv_buffer;
bool arrived = false;
static size_t reWriter(char* buffer, size_t size, size_t nmemb, string* content) { unsigned long sizes = size * nmemb; { std::lock_guard<std::mutex> lock(mutex_buffer); content->append(buffer, sizes); } arrived = true; cv_buffer.notify_one(); return sizes; }
int main() { curl_global_init(CURL_GLOBAL_ALL); CURL* curl; curl = curl_easy_init();
string url = "http://192.168.1.123:81/stream"; string buffer;
thread th = std::thread([&]() { while (true) { curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)5LL); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false); curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, true); curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 120L); curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 60L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, reWriter); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&(buffer)); curl_easy_setopt(curl, CURLOPT_URL, url.data()); cout << "正在发起请求..." << endl; curl_easy_perform(curl); cout << "连接已经断开!" << endl; } }); th.detach();
while (true) { std::unique_lock<std::mutex> lock(mutex_buffer); cv_buffer.wait(lock, [] { return arrived; }); arrived = false;
int64_t buffer_length = buffer.size(); cout << "buffer_length: " << buffer.size() << endl; regex pattern("\r\n--123456789000000000000987654321\r\nContent-Type: image\/jpeg\r\nContent-Length: ([\\d]+)\r\n\r\n"); smatch result; if (!regex_search(buffer, result, pattern)) continue; if (result.size() <= 1) continue; int64_t image_length = stoll(result[1].str()); int64_t header_length = result.length(0); if (header_length + image_length <= buffer_length) { string image_buffer = buffer.substr(header_length, image_length); buffer.clear(); Mat tmp = Mat(1, image_length, CV_8UC1, (void*)image_buffer.data()); cv::Mat decodedImage = imdecode(tmp, 1); if (decodedImage.data != NULL) { Mat filped; flip(decodedImage, filped, -1); imshow("display", filped); waitKey(10); } } }
return 0; }
|