Receiving and Decoding HD Images From ESP32-CAM With Node-Red (and MQTTPubSubClient)

by tnowroz in Circuits > Microcontrollers

86 Views, 0 Favorites, 0 Comments

Receiving and Decoding HD Images From ESP32-CAM With Node-Red (and MQTTPubSubClient)

ImageOverMQTT.drawio (1).png

I've found many unsolved threads of people failing to transmit HD or large sized image files over MQTT. Passing the Framebuffer and buffer length just works for most resolutions until you select the HD ones and starts resetting due to exceeding memory budget.

In this writeup, I'll walk through how to transmit HD images from ESP32 CAM over MQTT using the MQTTPubSubClient library, by dividing it in multiple payloads/chunks. Then decode the image back again.


If you are someone experienced, start from step 4

Supplies

  1. ESP32-CAM module
  2. Node-Red
  3. Arduino Framework

Start by Configuring the Image Sensor With Highest Resolution

Start by creating a Arduino Sketch or code example of ESP32-CAM module for template.

Uncomment definitions to select the AI-Thinker ESP32 CAM module in needed.


void setup() {

Serial.begin(115200);
Serial.setDebugOutput(true);

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;

config.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 4;
config.fb_count = 1;

esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}

sensor_t * s = esp_camera_sensor_get();
// initial sensors are flipped vertically and colors are a bit saturated

if (s->id.PID == OV2640_PID) {
Serial.println("Detected Camera Lens: OV2640");
}


Connect to WiFi and MQTT

WiFi.begin(ssid, password);
WiFi.setSleep(false);

while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");

// startCameraServer();
mqtt.begin(client);

// connect to wifi, host and mqtt broker
MQTTconnect();

Subscribe to Necessary Topics

// subscribe topic and callback
mqtt.subscribe("ESP32/Cam/Result", [](const String& payload, const size_t size) {
// Print the incoming Result
Serial.print("ESP32/Cam/Result :: ");
Serial.println(payload);

});

Publish an Image

void loop() {
mqtt.update(); // should be called
publishImageWithMarkers(); // Publish the image
}


How does publishImageWithMarkers() Work?

  1. Get the FrameBuffer from the image sensor
  2. Calculates the image size
  3. Define Maximum Allowable payload size.
  4. Calculates how many payloads needed to transmit the image.
  5. Start Transmission by sending "start" signal.
  6. Compose and send payload of pieces of FrameBuffer/Image
  7. End Transmission by sending "end" signal.


void publishImageWithMarkers() {
// Gets the FrameBuffer from the image sensor
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed! Restarting ESP...");
ESP.restart();
return;
}
// Calculates the image size
Serial.printf("Captured Image, size: %d bytes\n", fb->len);
// Define Maximum Allowable payload size.
int chunkSize = 1024*8; // 8KB per chunk
int totalSize = fb->len;

// Calculates how many payloads needed to transmit the image.
int NoI = totalSize/chunkSize;
char mqtt_topic[50];

// Start Transmission by sending "start" signal.
mqtt.publish("ESP32/Cam/ImagePart", "START", false, 1);

Serial.println("START Marker Sent");
// Print Buffer Start Address
Serial.print("Starting Frame buffer address - ");
Serial.println((uintptr_t)(fb->buf), HEX);

// Print Buffer Length
Serial.print("Frame length - ");
Serial.println(fb->len);
// Print Buffer End Address
Serial.print("End Address is - ");
Serial.println((uintptr_t)(fb->buf + fb->len), HEX);
// Compose and send payload of pieces of FrameBuffer/Image
for(int i = 0; i<=NoI; i++){
snprintf(mqtt_topic, sizeof(mqtt_topic), "ESP32/Cam/ImagePart");
Serial.print("Transmitting from Address - ");
Serial.print((uintptr_t)(fb->buf + (i * chunkSize)), HEX); // Print address in HEX
Serial.print(" to ");
Serial.println((uintptr_t)(fb->buf + (i * chunkSize) + chunkSize), HEX);

bool success = mqtt.publish(mqtt_topic, fb->buf + (i*chunkSize), chunkSize, false, 1);
if (!success) {
Serial.println("Failed to send image chunk!");
break;
}

Serial.printf("Sent chunk %d/%d (%d bytes)\n", i, totalSize / chunkSize, chunkSize);
delay(1); // Small delay to prevent buffer overflow
}

Serial.print("Transmitting from Address - ");
Serial.print((uintptr_t)(fb->buf + (NoI * chunkSize)), HEX); // Print address in HEX
Serial.print(" to ");
Serial.println((uintptr_t)(fb->buf + (NoI * chunkSize) + (totalSize-(NoI*chunkSize)) ), HEX);

bool success = mqtt.publish(mqtt_topic, fb->buf + ((NoI*chunkSize)), (totalSize-(NoI*chunkSize)), false, 1);
Serial.println("Sent last bytes");


// End Transmission by sending "end" signal.
mqtt.publish("ESP32/Cam/ImagePart", "END", false, 1);
Serial.println("Sent END marker");

esp_camera_fb_return(fb); // Free memory
}

Receive and Decode on Node-Red

vivaldi_Axfy1LlcPJ.png

Parts of FrameBuffer needs to be sent to same MQTT Topic.

Connect the incoming payload to a function. Once an entire image is sent, it will write it to a file with jpg format.


Function to receive, compile and decode to image -

  1. Initialization: Get value of Global Variable - imageData. Define it as Buffer in undefined.
  2. Edge Case: "START" signal received - empty buffer for new image reception.
  3. Edge Case: "End" signal received - compile Buffer to image, and send it to File Write Block.
  4. Otherwise, none of the edge cases are true - keep adding image pieces/chunks into the image buffer and return nothing.
// Get existing buffer or initialize it
var imageData = global.get("imageData");

// If no previous data exists, create an empty buffer
if (!imageData) {
imageData = Buffer.alloc(0);
}

// Check if message is a "START" marker
if (msg.payload.toString() === "START") {
node.status({ text: "Started Reception" });
global.set("imageData", Buffer.alloc(0)); // Reset buffer
return null;
}

// Check if message is an "END" marker
if (msg.payload.toString() === "END") {
node.status({ text: "Reception Complete!" });

// Retrieve full image data
msg.payload = global.get("imageData");
// Change this path for your OS
msg.filename = "C:\\Users\\Sayed Nowroz\\Documents\\received_image.jpg";
msg.contentType = "image/jpeg"; // Ensure correct MIME type
node.warn("Saving image, size: " + msg.payload.length + " bytes");

// Reset buffer for next image
global.set("imageData", Buffer.alloc(0));

return msg; // Send complete image to file node
}

// Ensure incoming payload is a Buffer
if (!Buffer.isBuffer(msg.payload)) {
node.warn("Received non-buffer data, converting to buffer...");
msg.payload = Buffer.from(msg.payload, "binary");
}

// Append new image chunk to buffer
var newChunk = msg.payload;
imageData = Buffer.concat([imageData, newChunk]);

global.set("imageData", imageData);
node.status({ text: "Receiving: " + imageData.length + " bytes" });

return null; // Keep collecting until "END" is received


I've connected it to a file write

Look at My Baby Yoda and Arduino Sticker :)

vivaldi_OjIgeK8E22.png
received_image - Copy.jpg

Picture 1: Image Decoded at Node-Red canvas using Image Preview block (node-red-contrib-image-output)

Picture 2: A saved JPG file from ESP32-CAM, Decoded by Node-Red

Check My GitHub for Full Project Files

Full Project files (including Node-Red Flow) can be found at my GitHub:

https://github.com/TNeutron/Publishing-High-Resolution-Images-from-ESP32-CAM-to-Node-Red