目录
前言
在嵌入式视觉项目中,ESP32-CAM 凭借其低成本、集成 WiFi/BLE、支持 OV2640/OV7670 摄像头,成为轻量级图像采集的首选。
本文分享一个实时图像流传输项目,使用 VS Code + PlatformIO 开发环境,搭建一个基于 ESP32-CAM 的实时视频流传输系统:ESP32-CAM 采集 JPEG 图像,通过 WiFi TCP 发送至 PC,PC 端用 Python + OpenCV 显示。代码极简,拷贝即用,适合初学者和项目快速原型。

推荐教程
Python+ESP32-CAM 摄像头 拍照 录像 有课件(持续更新)
一、整体流程与协议设计
1.1 TCP 协议
TCP 是一种面向连接的可靠传输协议,广泛用于需要数据完整性的场景。它的核心特性与我们的图像传输项目密切相关:
面向连接与三次握手
通信前,客户端与服务端通过“三次握手”建立连接(SYN → SYN+ACK → ACK),确保双方就绪。代码中的 client.connect() 即触发该过程。
流式传输与粘包
TCP 是字节流,不保留消息边界。多次发送可能合并或拆分,导致接收端无法识别帧边界。为此我们在数据前添加 4字节长度字段,接收端先读长度,再循环读取完整帧,实现消息分割。
可靠传输与超时重传
TCP 使用确认应答(ACK)和超时重传保证数据无损。若发送方未收到 ACK,会重复发送。但重传可能造成长时间阻塞(默认超时可达数分钟),所以我们在项目中用 setTimeout(300) 缩短超时,防止因网络波动导致系统卡死。
1.2 整体流程
TCP 连接的建立与维护
ESP32 作为 TCP 客户端,在 loop() 中通过 client.connect(pc_ip, pc_port) 主动发起连接。TCP 三次握手成功后,连接进入 ESTABLISHED 状态。此后,只要连接不断开,双方即可通过该 socket 进行双向数据传输。代码中,每次循环都会检查 client.connected(),若连接断开(比如 PC 端重启、网络中断),则会触发重新连接,保证系统具备断线重连能力。
数据帧格式设计
我们采用 长度 + 数据 的经典 TLV(Type-Length-Value)变体:
-
长度(4字节):使用
htonl()转换为大端序(网络字节序),确保跨平台解析一致。接收端通过int.from_bytes(raw_len, byteorder='big')还原。 -
数据(变长):JPEG 压缩后的图像原始字节流。
这种设计的优点:接收端可以首先获取帧长度,然后循环接收指定字节数,完整拼合一帧,避免了 TCP 流式传输中的“粘包”问题。同时,由于 JPEG 是自包含的格式,解码时只需将完整字节流交给 cv2.imdecode() 即可,无需额外解析。
分块发送的必要性
TCP 虽然提供可靠的字节流服务,但底层发送缓冲区大小有限(通常约 1.5KB~8KB)。如果一次 write() 写入几十 KB 的数据,可能导致底层分包,或者因缓冲区满而阻塞。为了提高效率和避免单次阻塞时间过长,我们将数据拆分为 1024 字节的小块进行循环发送:
-
每一块独立调用
write(),若某一块发送失败(超时或返回值不足),可立即定位并中断本次传输。 -
1024 字节远小于常见 MTU(1500 字节),能更好地适应网络层分片,减少 IP 层重组开销。
二、ESP32 端代码实现
platformio.ini
[env:esp32cam]
platform = espressif32
board = esp32cam
framework = arduino
main.c
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include "mywifi.h"
#include "camera.h"
const char* pc_ip = "你电脑的IP地址";
const int pc_port = 9091;
WiFiClient tcpClient;
void setup()
{
Serial.begin(115200);
Serial.setDebugOutput(true);
camera_init();
wifi_connect();
}
void loop()
{
// 检查并建立 TCP 连接
if (!tcpClient.connected())
{
if (tcpClient.connect(pc_ip, pc_port))
{
Serial.println("TCP connected");
}
else
{
delay(1000);
return;
}
}
// 抓取一帧
camera_fb_t *fb = esp_camera_fb_get();
if (!fb)
{
delay(10);
return;
}
// 设置发送超时为 500ms(超过则放弃)
tcpClient.setTimeout(500);
// 发送长度(4字节)
uint32_t len = htonl(fb->len);
size_t written = tcpClient.write((uint8_t*)&len, 4);
if (written != 4)
{
// 发送长度失败,断开连接,释放帧,退出本次循环
tcpClient.stop();
esp_camera_fb_return(fb);
Serial.println("Send length timeout, reconnect");
return;
}
// 分块发送图像数据(每次 1024 字节)
uint8_t *buf = fb->buf;
size_t remaining = fb->len;
const size_t chunk_size = 1024;
bool send_ok = true;
while (remaining > 0)
{
size_t to_send = (remaining > chunk_size) ? chunk_size : remaining;
size_t w = tcpClient.write(buf, to_send);
if (w != to_send)
{
// 某一分块未发送完,连接已坏
send_ok = false;
break;
}
buf += w;
remaining -= w;
}
// 释放帧缓冲区
esp_camera_fb_return(fb);
// 如果发送失败,断开连接,下次循环会重连
if (!send_ok)
{
tcpClient.stop();
Serial.println("Send data timeout, reconnect");
return;
}
// 可以保留一个小延时来控制帧率
delay(1);
}
camera.c
#include "camera.h"
void camera_init()
{
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
// ====================== 核心优化参数 ======================
config.xclk_freq_hz = 20000000; // 恢复20M,速度更快
config.frame_size = FRAMESIZE_HVGA; // 480x320(不卡的关键)
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12; // 降低一点画质,大幅提升速度
config.fb_count = 1; // 单缓冲,不占内存
// 无PSRAM自动降级
if (!psramFound())
{
config.frame_size = FRAMESIZE_QVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 15;
}
// 初始化摄像头
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
Serial.printf("Camera init failed with error 0x%x\n", err);
ESP.restart();
}
sensor_t *s = esp_camera_sensor_get();
s->set_vflip(s, 1); // 垂直翻转
s->set_hmirror(s, 1); // 水平镜像
s->set_brightness(s, 1);
s->set_contrast(s, 1);
s->set_saturation(s, -1);
// 特殊优化:锐化、降噪、曝光
s->set_sharpness(s, 2);
s->set_denoise(s, 1);
s->set_exposure_ctrl(s, 1);
s->set_gain_ctrl(s, 1);
s->set_whitebal(s, 1);
}
camera.h
#ifndef CAMERA_H
#define CAMERA_H
#include <Arduino.h>
#include "esp_camera.h"
#include "camera_pins.h"
void camera_init();
#endif
camera_pins.h
#ifndef CAMERA_PINS_H
#define CAMERA_PINS_H
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
// 4 for flash led or 33 for normal led
#define LED_GPIO_NUM 4
#endif
三、PC 端代码实现
import socket
import cv2
import numpy as np
# 服务器监听地址和端口(与 ESP32 代码中的 pc_ip 和 pc_port 一致)
SERVER_IP = '0.0.0.0' # 监听所有网络接口
SERVER_PORT = 9091
def main():
# 创建 TCP 服务器
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((SERVER_IP, SERVER_PORT))
server_socket.listen(1)
print(f"等待 ESP32-CAM 连接(端口 {SERVER_PORT})...")
client_socket, addr = server_socket.accept()
print(f"已连接:{addr}")
try:
while True:
# 1. 读取 4 字节的帧长度(大端序)
raw_len = client_socket.recv(4)
if not raw_len:
break
frame_len = int.from_bytes(raw_len, byteorder='big')
# 2. 根据长度读取完整的 JPEG 数据
img_data = b''
while len(img_data) < frame_len:
chunk = client_socket.recv(frame_len - len(img_data))
if not chunk:
break
img_data += chunk
if len(img_data) < frame_len:
break # 数据不完整,断开连接
# 3. 将 JPEG 数据解码为 OpenCV 图像
nparr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is not None:
cv2.imshow("ESP32-CAM", img)
# 按 'q' 退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except Exception as e:
print(f"接收错误:{e}")
finally:
client_socket.close()
server_socket.close()
cv2.destroyAllWindows()
print("程序已退出")
if __name__ == "__main__":
main()
四、运行效果与调试
烧录与运行
-
在 VS Code 中打开 PlatformIO 项目,修改
ssid/password和pc_ip。 -
连接 ESP32-CAM 到电脑,点击 PlatformIO 的“上传”按钮进行编译烧录。
-
先运行 PC 端的 show.py(确保防火墙允许 9091 端口)。
-
给 ESP32-CAM 复位,等待连接成功。
-
屏幕上会弹出 OpenCV 窗口,显示实时视频流。
画面效果

结语
通过本项目的实战开发,我们从零搭建了一套基于 ESP32-CAM 的实时视频流传输系统,完整走过了硬件配置、嵌入式固件开发、TCP 通信协议设计以及 PC 端图像解码显示的全链路流程。
同时,这个项目的视频流有时会有小几秒卡顿的问题,如果你有相关优化思路或解决方案,欢迎随时交流探讨。

158

被折叠的 条评论
为什么被折叠?



