I just made a program to count objects using a camera, the results appear on the serial monitor but why does it still say 0 on the web?
#include "esp_camera.h"
#include "WiFi.h"
#include "esp_timer.h"
#include "img_converters.h"
#include <algorithm>
#include <numeric>
#include "Arduino.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_http_server.h"
// Select camera model
#define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM
#include "camera_pins.h"
// Set up Access Point credentials
const char* ap_ssid = "ESP32-CAM-AP";
const char* ap_password = "12345678";
// Stream content type and boundary definitions
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
void startCameraServer();
int countRandom(camera_fb_t *fb);
void addrandomCountOverlay(camera_fb_t *fb, int random_count);
httpd_handle_t stream_httpd = NULL;
httpd_handle_t server = NULL;
volatile int last_random_count = 0;
static esp_err_t stream_handler(httpd_req_t *req) {
camera_fb_t * fb = NULL;
esp_err_t res = ESP_OK;
size_t _jpg_buf_len = 0;
uint8_t * _jpg_buf = NULL;
char * part_buf[64];
static int64_t last_frame = 0;
if (!last_frame) {
last_frame = esp_timer_get_time();
}
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
if (res != ESP_OK) {
return res;
}
while (true) {
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
res = ESP_FAIL;
} else {
last_random_count = countRandom(fb); // Update last_random_count here
Serial.printf("Random counted: %d\n", last_random_count);
addrandomCountOverlay(fb, last_random_count);
if (fb->format != PIXFORMAT_JPEG) {
bool jpeg_converted = frame2jpg(fb, 15, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
fb = NULL;
if (!jpeg_converted) {
Serial.println("JPEG compression failed");
res = ESP_FAIL;
}
} else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
}
if (res == ESP_OK) {
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if (res == ESP_OK) {
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
}
if (res == ESP_OK) {
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
}
if (fb) {
esp_camera_fb_return(fb);
fb = NULL;
_jpg_buf = NULL;
} else if (_jpg_buf) {
free(_jpg_buf);
_jpg_buf = NULL;
}
if (res != ESP_OK) {
break;
}
int64_t fr_end = esp_timer_get_time();
int64_t frame_time = fr_end - last_frame;
last_frame = fr_end;
frame_time /= 1000;
Serial.printf("MJPG: %uB %ums (%.1ffps)\n",
(uint32_t)(_jpg_buf_len),
(uint32_t)frame_time, 1000.0 / (uint32_t)frame_time
);
delay(30);
}
last_frame = 0;
return res;
}
// Function to perform PCA-based grayscale conversion
void pca_grayscale(uint8_t* rgb_image, uint8_t* gray_image, int width, int height) {
// Calculate mean of each channel
float mean_r = 0, mean_g = 0, mean_b = 0;
for (int i = 0; i < width * height; i++) {
mean_r += rgb_image[i * 3];
mean_g += rgb_image[i * 3 + 1];
mean_b += rgb_image[i * 3 + 2];
}
mean_r /= (width * height);
mean_g /= (width * height);
mean_b /= (width * height);
// Calculate covariance matrix
float cov[3][3] = {{0}};
for (int i = 0; i < width * height; i++) {
float r = rgb_image[i * 3] - mean_r;
float g = rgb_image[i * 3 + 1] - mean_g;
float b = rgb_image[i * 3 + 2] - mean_b;
cov[0][0] += r * r; cov[0][1] += r * g; cov[0][2] += r * b;
cov[1][1] += g * g; cov[1][2] += g * b;
cov[2][2] += b * b;
}
cov[1][0] = cov[0][1]; cov[2][0] = cov[0][2]; cov[2][1] = cov[1][2];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
cov[i][j] /= (width * height - 1);
}
}
// Find eigenvector with largest eigenvalue (simplified approach)
float eigen_vector[3] = {cov[0][0], cov[1][0], cov[2][0]};
float mag = sqrt(eigen_vector[0] * eigen_vector[0] +
eigen_vector[1] * eigen_vector[1] +
eigen_vector[2] * eigen_vector[2]);
eigen_vector[0] /= mag;
eigen_vector[1] /= mag;
eigen_vector[2] /= mag;
// Project RGB values onto eigenvector
for (int i = 0; i < width * height; i++) {
float gray_value = eigen_vector[0] * rgb_image[i * 3] +
eigen_vector[1] * rgb_image[i * 3 + 1] +
eigen_vector[2] * rgb_image[i * 3 + 2];
gray_image[i] = (uint8_t)std::min(255.0f, std::max(0.0f, gray_value));
}
}
// Function to perform Otsu's thresholding
uint8_t otsu_threshold(uint8_t* gray_image, int width, int height) {
int histogram[256] = {0};
for (int i = 0; i < width * height; i++) {
histogram[gray_image[i]]++;
}
int total = width * height;
float sum = 0;
for (int i = 0; i < 256; i++) {
sum += i * histogram[i];
}
float sumB = 0;
int wB = 0;
int wF = 0;
float varMax = 0;
uint8_t threshold = 0;
for (int t = 0; t < 256; t++) {
wB += histogram[t];
if (wB == 0) continue;
wF = total - wB;
if (wF == 0) break;
sumB += t * histogram[t];
float mB = sumB / wB;
float mF = (sum - sumB) / wF;
float varBetween = wB * wF * (mB - mF) * (mB - mF);
if (varBetween > varMax) {
varMax = varBetween;
threshold = t;
}
}
return threshold;
}
// New global variables for calibration and object properties
int MIN_OBJECT_AREA = 100; // Minimum area of an object in pixels
int MAX_OBJECT_AREA = 10000; // Maximum area of an object in pixels
float PIXELS_PER_CM = 10; // Calibration factor: pixels per cm
// Function prototypes
void setupLighting();
void calibratePixelsPerCm();
int countObjects(camera_fb_t *fb);
void applyImageFilters(uint8_t* image, size_t width, size_t height);
void detectEdges(uint8_t* image, size_t width, size_t height);
void setupLighting() {
// Example: If you have a GPIO pin connected to LEDs
const int LED_PIN = 2; // Change this to your actual LED pin
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH); // Turn on the LED
}
void calibratePixelsPerCm() {
// This function should be called with a known object in the frame
// For example, you could place a 10cm ruler in the frame
// Then, detect the ruler and calculate pixels per cm
// This is a placeholder implementation
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
return;
}
// Assume we've detected a 10cm object that's 100 pixels long
PIXELS_PER_CM = 100 / 10.0;
esp_camera_fb_return(fb);
Serial.printf("Calibrated to %.2f pixels per cm\n", PIXELS_PER_CM);
}
void applyImageFilters(uint8_t* image, size_t width, size_t height) {
// Apply a simple Gaussian blur to reduce noise
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int sum = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
sum += image[(y + dy) * width + (x + dx)];
}
}
image[y * width + x] = sum / 9;
}
}
}
void detectEdges(uint8_t* image, size_t width, size_t height) {
// Simple Sobel edge detection
uint8_t* temp = (uint8_t*)malloc(width * height);
memcpy(temp, image, width * height);
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int gx = (-1 * temp[(y-1)*width + (x-1)]) + (-2 * temp[y*width + (x-1)]) + (-1 * temp[(y+1)*width + (x-1)]) +
(1 * temp[(y-1)*width + (x+1)]) + (2 * temp[y*width + (x+1)]) + (1 * temp[(y+1)*width + (x+1)]);
int gy = (-1 * temp[(y-1)*width + (x-1)]) + (-2 * temp[(y-1)*width + x]) + (-1 * temp[(y-1)*width + (x+1)]) +
(1 * temp[(y+1)*width + (x-1)]) + (2 * temp[(y+1)*width + x]) + (1 * temp[(y+1)*width + (x+1)]);
int mag = sqrt(gx*gx + gy*gy);
image[y*width + x] = (mag > 100) ? 255 : 0; // Threshold
}
}
free(temp);
}
// Modified Random counting function
int countRandom(camera_fb_t *fb) {
int width = fb->width;
int height = fb->height;
uint8_t *rgb_buffer = fb->buf;
// Convert to grayscale using PCA (from original code)
uint8_t *gray_buffer = (uint8_t*)malloc(width * height);
pca_grayscale(rgb_buffer, gray_buffer, width, height);
// Apply image filters
applyImageFilters(gray_buffer, width, height);
// Perform Otsu's thresholding (from original code)
uint8_t threshold = otsu_threshold(gray_buffer, width, height);
// Count Random (white pixels)
int random_pixels = 0;
for (int i = 0; i < width * height; i++) {
if (gray_buffer[i] > threshold) {
random_pixels++;
}
}
// Free allocated memory
free(gray_buffer);
// Convert pixel count to Random count (adjust this based on expected Random size)
int random_count = random_pixels / 1000; // Assume each Random is roughly 1000 pixels
return random_count;
}
int floodFill(uint8_t* image, uint8_t* labeled, int width, int height, int x, int y, uint8_t label) {
if (x < 0 || x >= width || y < 0 || y >= height || image[y*width + x] != 255 || labeled[y*width + x] != 0) {
return 0;
}
labeled[y*width + x] = label;
int area = 1;
area += floodFill(image, labeled, width, height, x+1, y, label);
area += floodFill(image, labeled, width, height, x-1, y, label);
area += floodFill(image, labeled, width, height, x, y+1, label);
area += floodFill(image, labeled, width, height, x, y-1, label);
return area;
}
// Function to add overlay text to the frame buffer
void addrandomCountOverlay(camera_fb_t *fb, int random_count) {
char overlay_text[32];
snprintf(overlay_text, sizeof(overlay_text), "Random Count: %d", random_count);
// For now, just print to serial, as fb_gfx or similar method needs the right library.
Serial.println(overlay_text);
}
// Return the Random count as JSON
static esp_err_t random_count_handler(httpd_req_t *req) {
char json_response[64];
snprintf(json_response, sizeof(json_response), "{\"randomCount\": %d}", last_random_count);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, json_response, strlen(json_response));
// Add this line for logging
Serial.printf("Sending random count to client: %d\n", last_random_count);
return ESP_OK;
}
// HTML for the web interface
static esp_err_t index_handler(httpd_req_t *req) {
const char* resp_str = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Random Counter</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.container {
text-align: center;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
.bg-container {
position: relative;
width: 500px;
height: auto;
overflow: hidden;
margin: 20px auto;
}
.camera-container {
position: relative;
width: 300px;
height: 300px;
overflow: hidden;
margin: 20px auto;
border-radius: 50%;
border: 5px solid #333;
}
#cameraFeed {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
#randomCountDisplay {
display: inline-block;
background-color: rgba(0,0,0,0.5);
border-radius: 20px;
padding: 10px 20px;
color: white;
margin-top: 10px;
margin-bottom: -30px;
font-size: 24px;
font-weight: bold;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 Random Counter</h1>
<div id="randomCountDisplay">Random Count: <span id="randomCount">0</span></div>
<div class="bg-container">
<div class="camera-container">
<img id="cameraFeed" src="/stream" alt="Camera Stream">
</div>
</div>
<button onclick="saverandomCount()">Save Random Count</button>
</div>
<script>
const url = "/randomCount";
function updaterandomCount() {
fetch(url)
.then(response => response.json())
.then(data => {
console.log("Received data from server:", data); // Add this line
document.getElementById("randomCount").textContent = data.randomCount;
console.log("Updated DOM with new count:", data.randomCount); // Add this line
})
.catch(error => console.error('Error fetching Random count:', error));
}
function saverandomCount() {
const randomCountDisplay = document.getElementById('randomCountDisplay');
const randomCount = document.getElementById('randomCount').textContent;
const video = document.getElementById('cameraFeed');
const canvas = document.createElement('canvas');
canvas.width = 500; // Match the width of bg-container
canvas.height = 500; // Make it square for simplicity
const context = canvas.getContext('2d');
// Draw white background
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
// Draw circular camera feed
context.save();
context.beginPath();
context.arc(250, 250, 150, 0, Math.PI * 2);
context.clip();
context.drawImage(video, 100, 100, 300, 300);
context.restore();
// Draw border
context.strokeStyle = '#333';
context.lineWidth = 5;
context.beginPath();
context.arc(250, 250, 150, 0, Math.PI * 2);
context.stroke();
// Draw Random count display with dynamic width
context.font = 'bold 24px Arial';
const text = "Random Count: " + randomCount;
const textMetrics = context.measureText(text);
const textWidth = textMetrics.width;
const paddingX = 20;
const rectWidth = textWidth + (paddingX * 2);
const rectHeight = 40;
const rectX = (canvas.width - rectWidth) / 2;
const rectY = 50;
context.fillStyle = 'rgba(0,0,0,0.5)';
context.beginPath();
context.roundRect(rectX, rectY, rectWidth, rectHeight, 20);
context.fill();
context.fillStyle = 'white';
context.textAlign = 'center';
context.fillText(text, canvas.width / 2, rectY + 28);
canvas.toBlob(function(blob) {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'random_count.jpg';
link.click();
}, 'image/jpeg');
}
setInterval(updaterandomCount, 1000); // Update every 1 second
</script>
</body>
</html>
)rawliteral";
httpd_resp_send(req, resp_str, strlen(resp_str));
return ESP_OK;
}
void startCameraServer() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 80;
config.max_open_sockets = 5;
config.backlog_conn = 10;
config.lru_purge_enable = true;
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
httpd_uri_t random_count_uri = {
.uri = "/randomCount",
.method = HTTP_GET,
.handler = random_count_handler,
.user_ctx = NULL
};
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};
Serial.printf("Starting web server on port: '%d'\n", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
httpd_register_uri_handler(server, &stream_uri);
httpd_register_uri_handler(server, &random_count_uri);
httpd_register_uri_handler(server, &index_uri);
}
}
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
Serial.println("Setting up camera...");
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 = 10000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_VGA; // Meningkatkan resolusi
config.jpeg_quality = 12; // Mengurangi kompresi untuk kualitas lebih baik
config.fb_count = 2;
if (psramFound()) {
config.jpeg_quality = 10;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}
// Initialize the camera
esp_err_t err = ESP_FAIL;
int retry = 0;
while (err != ESP_OK && retry < 5) {
err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
delay(500);
retry++;
}
}
if (err != ESP_OK) {
Serial.println("Camera init failed after 5 attempts");
return;
}
delay(100);
setupLighting();
calibratePixelsPerCm();
// Adjust camera settings
sensor_t * s = esp_camera_sensor_get();
if (s) {
s->set_brightness(s, 1); // Increase brightness for better visibility in closed bucket
s->set_contrast(s, 2); // Increase contrast to make objects more distinguishable
s->set_saturation(s, 1); // Slight increase in saturation
s->set_special_effect(s, 0); // No special effect
s->set_whitebal(s, 1); // Enable white balance
s->set_awb_gain(s, 1); // Enable auto white balance gain
s->set_wb_mode(s, 0); // Auto white balance mode
s->set_exposure_ctrl(s, 1); // Enable auto exposure
s->set_aec2(s, 1); // Enable auto exposure (DSP)
s->set_gain_ctrl(s, 1); // Enable auto gain control
s->set_bpc(s, 1); // Enable black pixel correction
s->set_wpc(s, 1); // Enable white pixel correction
s->set_raw_gma(s, 1); // Enable gamma correction
s->set_lenc(s, 1); // Enable lens correction
s->set_hmirror(s, 0); // Disable horizontal mirror
s->set_vflip(s, 0); // Disable vertical flip
s->set_dcw(s, 1); // Enable downsize EN
}
// Start the Access Point
WiFi.softAP(ap_ssid, ap_password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
// Start the Camera Server
startCameraServer();
}
void loop() {
delay(30);
}