ESP32 Cam向Python客户端传图:灰度图转彩色/JPG问题求助
问题:ESP32 Cam WebSocket传输彩色图像/JPG失败,仅能显示灰度图
现有一套ESP32 Cam作为WebSocket服务端、Python作为客户端的图像传输代码,可正常运行但仅显示灰度图。尝试修改ESP32代码发送JPG图像、Python端改用3通道RGB矩阵、注释灰度相关设置均无效,推测问题出在Python代码中。
ESP32 Cam 服务端代码
#include "esp_camera.h" #include <WiFi.h> #include <WebSocketsServer.h> // Select camera model #define CAMERA_MODEL_WROVER_KIT // Has PSRAM #include "esp_camera.h" const char* ssid = "Wifi"; // 输入你的WiFi名称 const char* password = "password"; // 输入你的WiFi密码 #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 // 全局变量 WebSocketsServer webSocket = WebSocketsServer(80); // 接收到WebSocket消息时调用 void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { // 判断WebSocket事件类型 switch(type) { // 客户端已断开连接 case WStype_DISCONNECTED: Serial.printf("[%u] Disconnected!\n", num); break; // 新客户端已连接 case WStype_CONNECTED: { IPAddress ip = webSocket.remoteIP(num); Serial.printf("[%u] Connection from ", num); Serial.println(ip.toString()); } break; // 将文本消息回传给客户端 case WStype_TEXT: Serial.printf("[%u] Text: %s\n", num, payload); Serial.println((char*)payload); if (String((char*)payload) == "capture") { Serial.println("Capture Command Received - capturing frame"); camera_fb_t * fb = NULL; fb = esp_camera_fb_get(); // 获取图像... 获取最新图像的临时解决方案 esp_camera_fb_return(fb); // 返回帧缓存... 获取最新图像的临时解决方案 fb = NULL; fb = esp_camera_fb_get(); // 获取新图像 size_t fbsize = fb->len; Serial.println(fbsize); Serial.println("Image captured. Returning frame buffer data."); webSocket.sendBIN(num, fb->buf, fbsize); esp_camera_fb_return(fb); Serial.println("Done"); } else { webSocket.sendTXT(num, payload); } break; // 其他事件:不做处理 case WStype_BIN: // Serial.printf("[%u] get binary length: %u\n", num, length); // hexdump(payload, length); // 向客户端发送消息 // webSocket.sendBIN(num, payload, length); // break; case WStype_ERROR: case WStype_FRAGMENT_TEXT_START: case WStype_FRAGMENT_BIN_START: case WStype_FRAGMENT: case WStype_FRAGMENT_FIN: default: break; } } void setup() { Serial.begin(115200); Serial.setDebugOutput(true); Serial.println("Connecting..."); 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_sscb_sda = SIOD_GPIO_NUM; config.pin_sscb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_GRAYSCALE; // 若存在PSRAM芯片,使用UXGA分辨率和更高JPEG质量初始化 // 用于更大的预分配帧缓存。 //if(psramFound()){ // config.frame_size = FRAMESIZE_UXGA; // config.jpeg_quality = 10; // config.fb_count = 2; //} else { config.frame_size = FRAMESIZE_QVGA; //FRAMESIZE_96X96; //FRAMESIZE_QQVGA; //FRAMESIZE_SVGA; config.jpeg_quality = 12; config.fb_count = 1; //} // 相机初始化 esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Msg: Camera init failed with error 0x%x", err); return; } sensor_t * s = esp_camera_sensor_get(); // 降低帧尺寸以提高初始帧率 s->set_framesize(s, FRAMESIZE_QVGA); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.print("Msg: Camera Ready! Use 'http://"); Serial.print(WiFi.localIP()); Serial.println("' to connect"); // 启动WebSocket服务器并分配回调函数 webSocket.begin(); webSocket.onEvent(onWebSocketEvent); } void loop() { // 查找并处理WebSocket数据 webSocket.loop(); }
Python 客户端代码
import websocket # pip install websocket-client import numpy as np import matplotlib.pyplot as plt # 图像尺寸 - 需与ESP32 CAM板上的设置匹配 IMG_X = 320 # QVGA 320x240 IMG_Y = 240 ws = websocket.WebSocket() ws.connect("ws://192.168.1.3") # 使用ESP32板的IP地址 - 可从串口监视器查看 #192.168.1.3 ESP32的IP地址 while(1): # 请求用户输入并发送 str = input("Say something: ") if (str == "exit"): break # 通过套接字发送输入字符串 ws.send(str) # 等待服务器响应并打印 if (str == "capture"): binResp = ws.recv_frame() # 从相机接收二进制图像数据(默认灰度图) # 将接收到的二进制图像数据存储到数组中... img_array = np.zeros((IMG_Y,IMG_X)) x = 0 y = 0 count = 0 # 逐个字节遍历捕获的数据并填充图像数组 for byte in bytearray(binResp.data): #print (byte) y = count // IMG_X x = count % IMG_X img_array[y,x] = byte count = count + 1 # 显示捕获的图像 plt.figure('Capture Image 1') plt.imshow(img_array) plt.set_cmap('gray') plt.show(block=False) else: result = ws.recv() print("Received: " + result) # 优雅关闭WebSocket连接 ws.close()
解决方案
步骤1:修改ESP32代码,切换到JPEG格式输出
ESP32当前配置为PIXFORMAT_GRAYSCALE,需改成JPEG格式以传输彩色压缩数据:
// 在camera_config_t配置中修改像素格式 config.pixel_format = PIXFORMAT_JPEG; // 启用PSRAM(若模块支持),确保JPEG缓存足够 if(psramFound()){ config.frame_size = FRAMESIZE_QVGA; config.jpeg_quality = 12; config.fb_count = 2; } else { config.frame_size = FRAMESIZE_QVGA; config.jpeg_quality = 12; config.fb_count = 1; }
步骤2:修改Python客户端代码,解析JPG二进制数据
原代码按单字节灰度图解析,无法处理JPG数据,改用PIL.Image直接加载二进制流:
import websocket import numpy as np import matplotlib.pyplot as plt from PIL import Image import io IMG_X = 320 IMG_Y = 240 ws = websocket.WebSocket() ws.connect("ws://192.168.1.3") while(1): str_input = input("Say something: ") if str_input == "exit": break ws.send(str_input) if str_input == "capture": binResp = ws.recv_frame() # 直接加载JPG二进制流 img = Image.open(io.BytesIO(binResp.data)) img_array = np.array(img) # 显示彩色图像,无需灰度映射 plt.figure('Capture Image 1') plt.imshow(img_array) plt.show(block=False) else: result = ws.recv() print("Received: " + result) ws.close()
关键说明
- ESP32发送JPG压缩数据,既节省带宽又天然支持彩色。
- Python端通过
PIL.Image直接解析JPG流,避免了手动拼接像素的复杂逻辑,解决了灰度解析不兼容彩色数据的问题。 - 确保ESP32与Python的图像尺寸配置匹配,JPG格式会自动处理尺寸信息。
内容的提问来源于stack exchange,提问作者Diego Ulises
相关产品推荐
相关产品推荐

