You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 00:37:05