I just made a program to count fish in a bucket. The problem is the camera can't count the number of fish and it's always 0. Where is the error?
Board ESP32s3 Camera Eye
#include <Arduino.h>
#include "esp_camera.h"
#include <cmath>
#include <WiFi.h>
#include "esp_http_server.h"
#include <esp_system.h>
#include <esp_err.h>
// Camera pins and configurations
#define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM
#include "camera_pins.h"
// WiFi credentials
const char* ap_ssid = "ESP32_Object_Counter";
const char* ap_password = "12345678"; // Password must be at least 8 characters
#define PART_BOUNDARY "123456789000000000000987654321"
#define _STREAM_CONTENT_TYPE "multipart/x-mixed-replace;boundary=" PART_BOUNDARY
#define _STREAM_BOUNDARY "\r\n--" PART_BOUNDARY "\r\n"
#define _STREAM_PART "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"
// Web server
httpd_handle_t server = NULL;
httpd_handle_t stream_httpd = NULL;
// Number of detected objects
int objectCount = 0;
// Define the RGB structure
struct RGB {
uint8_t r, g, b;
};
// Improved object detection parameters
#define OBJECT_THRESHOLD 30 // Nilai threshold lebih rendah untuk objek dalam air
#define MIN_OBJECT_SIZE 50 // Ukuran minimal objek lebih kecil
#define MAX_OBJECTS 20 // Meningkatkan jumlah maksimum objek yang dapat dideteksi
#define FRAME_SIZE_STREAM FRAMESIZE_SVGA
#define FRAME_SIZE_CAPTURE FRAMESIZE_UXGA
// Function prototypes
void startCameraServer();
// Fungsi untuk inisialisasi kamera dengan resolusi dan format yang fleksibel
esp_err_t init_camera(framesize_t frame_size, pixformat_t pixel_format) {
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;
// Parameter input untuk resolusi dan format
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.pixel_format = pixel_format;
config.frame_size = frame_size;
config.jpeg_quality = 10; // Kualitas JPEG (semakin tinggi angka semakin rendah kualitas)
config.fb_count = 2; // Jumlah frame buffer
// Deinit kamera jika sebelumnya sudah diinisialisasi
esp_camera_deinit();
// Init ulang kamera dengan konfigurasi yang baru
return esp_camera_init(&config);
}
// HTTP handler untuk streaming video
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 {
if(fb->format != PIXFORMAT_JPEG){
bool jpeg_converted = frame2jpg(fb, 80, &_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
);
}
last_frame = 0;
return res;
}
// Gaussian blur kernel
int gaussianKernel[3][3] = {
{1, 2, 1},
{2, 4, 2},
{1, 2, 1}
};
// Function to apply Gaussian Blur
void applyGaussianBlur(uint8_t* buf, int width, int height) {
uint8_t tempBuf[width * height * 3]; // Temporary buffer for blurring
for (int i = 1; i < height - 1; i++) {
for (int j = 1; j < width - 1; j++) {
int sum = 0;
for (int m = -1; m <= 1; m++) {
for (int n = -1; n <= 1; n++) {
int neighborIndex = ((i + m) * width + (j + n)) * 3;
sum += buf[neighborIndex] * gaussianKernel[m + 1][n + 1];
}
}
int pixelIndex = (i * width + j) * 3;
tempBuf[pixelIndex] = sum / 16; // Gaussian blur normalization
}
}
// Copy blurred data back to original buffer
memcpy(buf, tempBuf, width * height * 3);
}
// Function to apply histogram equalization
void applyHistogramEqualization(uint8_t* buf, int len) {
int histogram[256] = {0};
int lut[256] = {0};
int totalPixels = len / 3;
// Compute histogram
for (int i = 0; i < len; i += 3) {
int intensity = buf[i]; // Grayscale intensity
histogram[intensity]++;
}
// Compute cumulative distribution function (CDF)
lut[0] = histogram[0];
for (int i = 1; i < 256; i++) {
lut[i] = lut[i - 1] + histogram[i];
}
// Normalize CDF
for (int i = 0; i < 256; i++) {
lut[i] = (lut[i] * 255) / totalPixels;
}
// Apply equalization
for (int i = 0; i < len; i += 3) {
int intensity = buf[i];
buf[i] = buf[i + 1] = buf[i + 2] = lut[intensity];
}
}
// Convert image to grayscale
void convertToGrayscale(uint8_t* buf, int len) {
for (int i = 0; i < len; i += 3) {
uint8_t r = buf[i];
uint8_t g = buf[i + 1];
uint8_t b = buf[i + 2];
uint8_t gray = (r + g + b) / 3; // Simple average grayscale conversion
buf[i] = buf[i + 1] = buf[i + 2] = gray;
}
}
// Function to convert YUV422 to RGB
RGB yuv422_to_rgb(uint8_t y, uint8_t u, uint8_t v) {
int r = y + 1.402f * (v - 128);
int g = y - 0.344f * (u - 128) - 0.714f * (v - 128);
int b = y + 1.772f * (u - 128);
r = r > 255 ? 255 : (r < 0 ? 0 : r);
g = g > 255 ? 255 : (g < 0 ? 0 : g);
b = b > 255 ? 255 : (b < 0 ? 0 : b);
return {static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b)};
}
// Function to perform PCA on RGB data
uint8_t* pca_grayscale(camera_fb_t* fb) {
size_t pixel_count = fb->width * fb->height;
uint8_t* grayscale = (uint8_t*)malloc(pixel_count);
if (!grayscale) return nullptr;
// Calculate mean of each channel
float mean_r = 0, mean_g = 0, mean_b = 0;
for (size_t i = 0; i < pixel_count * 2; i += 4) {
RGB rgb = yuv422_to_rgb(fb->buf[i], fb->buf[i+1], fb->buf[i+3]);
mean_r += rgb.r;
mean_g += rgb.g;
mean_b += rgb.b;
}
mean_r /= pixel_count;
mean_g /= pixel_count;
mean_b /= pixel_count;
// Calculate covariance matrix
float cov[3][3] = {{0}};
for (size_t i = 0; i < pixel_count * 2; i += 4) {
RGB rgb = yuv422_to_rgb(fb->buf[i], fb->buf[i+1], fb->buf[i+3]);
float r = rgb.r - mean_r;
float g = rgb.g - mean_g;
float b = rgb.b - 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] /= pixel_count - 1;
}
}
// Find eigenvector with largest eigenvalue (power iteration method)
float vec[3] = {1, 1, 1};
for (int iter = 0; iter < 10; iter++) {
float new_vec[3] = {0};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
new_vec[i] += cov[i][j] * vec[j];
}
}
float norm = sqrt(new_vec[0]*new_vec[0] + new_vec[1]*new_vec[1] + new_vec[2]*new_vec[2]);
for (int i = 0; i < 3; i++) vec[i] = new_vec[i] / norm;
}
// Project colors onto principal component
for (size_t i = 0, j = 0; i < pixel_count * 2; i += 4, j++) {
RGB rgb = yuv422_to_rgb(fb->buf[i], fb->buf[i+1], fb->buf[i+3]);
float val = rgb.r * vec[0] + rgb.g * vec[1] + rgb.b * vec[2];
grayscale[j] = static_cast<uint8_t>(fmin(fmax(val, 0), 255));
}
return grayscale;
}
// Function to perform Otsu thresholding
uint8_t* otsu_threshold(uint8_t* grayscale, size_t pixel_count) {
uint8_t* thresholded = (uint8_t*)malloc(pixel_count);
if (!thresholded) return nullptr;
// Calculate histogram
int histogram[256] = {0};
for (size_t i = 0; i < pixel_count; i++) {
histogram[grayscale[i]]++;
}
// Calculate total mean level
float total_mean = 0;
for (int i = 0; i < 256; i++) {
total_mean += i * histogram[i];
}
total_mean /= pixel_count;
// Find threshold that maximizes between-class variance
int best_threshold = 0;
float max_variance = 0;
int first_class_pixel_count = 0;
float first_class_mean = 0;
for (int threshold = 0; threshold < 256; threshold++) {
first_class_pixel_count += histogram[threshold];
first_class_mean += threshold * histogram[threshold];
if (first_class_pixel_count == 0 || first_class_pixel_count == pixel_count) continue;
float first_class_prob = first_class_pixel_count / (float)pixel_count;
float second_class_prob = 1.0f - first_class_prob;
float first_class_mean_level = first_class_mean / first_class_pixel_count;
float second_class_mean_level = (total_mean - first_class_mean) / (pixel_count - first_class_pixel_count);
float mean_diff = first_class_mean_level - second_class_mean_level;
float variance = first_class_prob * second_class_prob * mean_diff * mean_diff;
if (variance > max_variance) {
max_variance = variance;
best_threshold = threshold;
}
}
// Apply threshold
for (size_t i = 0; i < pixel_count; i++) {
thresholded[i] = grayscale[i] > best_threshold ? 255 : 0;
}
return thresholded;
}
// HTTP handler for PCA grayscale conversion and Otsu thresholding
esp_err_t pca_grayscale_handler(httpd_req_t *req) {
camera_fb_t * fb = NULL;
esp_err_t res = ESP_OK;
size_t fb_len = 0;
int64_t fr_start = esp_timer_get_time();
// Ubah resolusi ke FRAMESIZE_UXGA (1600x1200) sebelum capture
esp_err_t err = init_camera(FRAMESIZE_UXGA, PIXFORMAT_JPEG);
if (err != ESP_OK) {
Serial.printf("Failed to set high resolution: 0x%x\n", err);
return ESP_FAIL;
}
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
return ESP_FAIL;
}
fb_len = fb->len;
size_t out_len = 0;
uint8_t * out_buf = NULL;
bool s;
if(!frame2bmp(fb, &out_buf, &out_len)){
Serial.println("BMP conversion failed");
esp_camera_fb_return(fb);
return ESP_FAIL;
}
esp_camera_fb_return(fb);
// Extract width and height from BMP header
size_t out_width = out_buf[18] + (out_buf[19] << 8);
size_t out_height = out_buf[22] + (out_buf[23] << 8);
// Perform PCA-based grayscale conversion
uint8_t* pca_gray = pca_grayscale(fb);
if (pca_gray) {
// Copy PCA result to BMP buffer (starting from pixel data offset, typically 54 bytes)
memcpy(out_buf + 54, pca_gray, out_width * out_height);
free(pca_gray);
} else {
// If PCA fails, fall back to simple grayscale conversion
for (size_t i = 54; i < out_len; i += 3) {
uint8_t gray = (out_buf[i] + out_buf[i+1] + out_buf[i+2]) / 3;
out_buf[i] = out_buf[i+1] = out_buf[i+2] = gray;
}
}
httpd_resp_set_type(req, "image/bmp");
httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=pca_grayscale.bmp");
s = httpd_resp_send(req, (const char *)out_buf, out_len);
free(out_buf);
// Kembali ke resolusi rendah setelah capture
err = init_camera(FRAME_SIZE_STREAM, PIXFORMAT_JPEG);
if (err != ESP_OK) {
Serial.printf("Failed to set low resolution: 0x%x\n", err);
}
int64_t fr_end = esp_timer_get_time();
Serial.printf("BMP: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start)/1000));
return s ? ESP_OK : ESP_FAIL;
}
// Auto exposure control (adjust brightness)
void autoAdjustExposure(camera_fb_t * fb) {
int brightness = 0;
int pixelCount = 0;
// Compute average brightness
for (int i = 0; i < fb->len; i += 3) {
brightness += fb->buf[i]; // Grayscale value
pixelCount++;
}
brightness /= pixelCount;
// Adjust brightness (exposure)
if (brightness < 100) {
sensor_t * s = esp_camera_sensor_get();
if (s->set_aec_value) {
s->set_aec_value(s, s->status.aec_value + 10); // Increase exposure
}
if (s != NULL) {
s->set_exposure_ctrl(s, 1); // Enable auto exposure control
s->set_aec2(s, 1); // Set second AEC (auto exposure control)
}
} else if (brightness > 150) {
sensor_t * s = esp_camera_sensor_get();
if (s->set_aec_value) {
s->set_aec_value(s, s->status.aec_value - 10); // Decrease exposure
}
if (s != NULL) {
s->set_exposure_ctrl(s, 1); // Enable auto exposure control
s->set_aec2(s, 1); // Set second AEC (auto exposure control)
}
}
}
// Fungsi untuk menerapkan filter median
void applyMedianFilter(uint8_t* buf, int width, int height) {
uint8_t window[9];
uint8_t* tempBuf = (uint8_t*)malloc(width * height);
if (!tempBuf) return;
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int k = 0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
window[k++] = buf[(y+j)*width + (x+i)];
}
}
// Sort window
for (int i = 0; i < 9; i++) {
for (int j = i + 1; j < 9; j++) {
if (window[i] > window[j]) {
uint8_t temp = window[i];
window[i] = window[j];
window[j] = temp;
}
}
}
tempBuf[y*width + x] = window[4]; // Median value
}
}
memcpy(buf, tempBuf, width * height);
free(tempBuf);
}
// Fungsi untuk meningkatkan kontras
void enhanceContrast(uint8_t* buf, int len) {
int min = 255, max = 0;
for (int i = 0; i < len; i++) {
if (buf[i] < min) min = buf[i];
if (buf[i] > max) max = buf[i];
}
for (int i = 0; i < len; i++) {
buf[i] = ((buf[i] - min) * 255) / (max - min);
}
}
// Fungsi untuk menghitung threshold adaptif
int computeAdaptiveThreshold(uint8_t* buf, int width, int height) {
int sum = 0, count = 0;
for (int y = 0; y < height; y += 10) { // Sample every 10th row
for (int x = 0; x < width; x += 10) { // Sample every 10th column
sum += buf[y * width + x];
count++;
}
}
return (sum / count) - 10; // Slightly lower than average for better detection
}
// Improved object counting algorithm
int countObjectsInFrame(camera_fb_t* fb) {
if (!fb || !fb->buf) return 0;
int width = fb->width;
int height = fb->height;
uint8_t* buf = fb->buf;
// Ensure the frame buffer is large enough
if (fb->len < width * height * 3) {
Serial.println("Frame buffer is too small");
return 0;
}
// Convert YUV422 to RGB
uint8_t* rgb_buf = (uint8_t*)malloc(width * height * 3);
if (!rgb_buf) {
Serial.println("Failed to allocate memory for RGB conversion");
return 0;
}
for (int i = 0, j = 0; i < width * height * 2; i += 4, j += 6) {
RGB rgb = yuv422_to_rgb(fb->buf[i], fb->buf[i+1], fb->buf[i+3]);
rgb_buf[j] = rgb.r;
rgb_buf[j+1] = rgb.g;
rgb_buf[j+2] = rgb.b;
rgb_buf[j+3] = rgb.r;
rgb_buf[j+4] = rgb.g;
rgb_buf[j+5] = rgb.b;
}
// Convert to grayscale
uint8_t* gray_buf = (uint8_t*)malloc(width * height);
if (!gray_buf) {
Serial.println("Failed to allocate memory for grayscale conversion");
free(rgb_buf);
return 0;
}
convertToGrayscale(rgb_buf, width * height * 3);
for (int i = 0, j = 0; i < width * height; i++, j += 3) {
gray_buf[i] = rgb_buf[j];
}
free(rgb_buf);
// Apply image processing steps
applyGaussianBlur(gray_buf, width, height);
applyHistogramEqualization(gray_buf, width * height);
autoAdjustExposure(fb); // Note: This function might need modification to work with grayscale
applyMedianFilter(gray_buf, width, height);
enhanceContrast(gray_buf, width * height);
applyWaterDistortionFilter(gray_buf, width, height);
reduceSurfaceReflections(gray_buf, width, height);
// Perform PCA-based grayscale conversion
uint8_t* pca_gray = pca_grayscale(fb);
if (pca_gray) {
free(gray_buf);
gray_buf = pca_gray;
}
// Apply Otsu thresholding
uint8_t* thresholded = otsu_threshold(gray_buf, width * height);
if (thresholded) {
free(gray_buf);
gray_buf = thresholded;
} else {
// If Otsu thresholding fails, use adaptive thresholding
int threshold = computeAdaptiveThreshold(gray_buf, width, height);
for (int i = 0; i < width * height; i++) {
gray_buf[i] = (gray_buf[i] > threshold) ? 255 : 0;
}
}
// Perform color segmentation (Note: This step might not be very useful after thresholding)
int* labels = (int*)calloc(width * height, sizeof(int));
if (labels) {
colorSegmentation(fb->buf, width, height, labels);
free(labels);
}
// Count objects using connected component labeling
int objectCount = 0;
int label = 1;
int* ccl_labels = (int*)calloc(width * height, sizeof(int));
if (!ccl_labels) {
Serial.println("Failed to allocate memory for labels");
free(gray_buf);
return 0;
}
for (int y = 0; y < height && label <= MAX_OBJECTS; y++) {
for (int x = 0; x < width && label <= MAX_OBJECTS; x++) {
if (gray_buf[y * width + x] == 255 && ccl_labels[y * width + x] == 0) {
int pixelCount = floodFill(gray_buf, ccl_labels, width, height, x, y, label);
if (pixelCount >= MIN_OBJECT_SIZE) {
objectCount++;
}
label++;
}
}
}
free(ccl_labels);
free(gray_buf);
return objectCount;
}
// Function to apply water distortion correction
void applyWaterDistortionFilter(uint8_t* buf, int width, int height) {
// Create a temporary buffer for the filtered image
uint8_t* tempBuf = (uint8_t*)malloc(width * height);
if (!tempBuf) return;
// Parameters for the distortion correction
float A = 0.01; // Amplitude of the distortion
float T = 10.0; // Period of the distortion
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// Apply sinusoidal distortion
float dx = A * sin(2 * M_PI * y / T);
float dy = A * sin(2 * M_PI * x / T);
// Calculate the source pixel coordinates
int sx = x + (int)(dx * width);
int sy = y + (int)(dy * height);
// Ensure the coordinates are within bounds
sx = max(0, min(sx, width - 1));
sy = max(0, min(sy, height - 1));
// Copy the pixel from the source to the destination
tempBuf[y * width + x] = buf[sy * width + sx];
}
}
// Copy the filtered image back to the original buffer
memcpy(buf, tempBuf, width * height);
free(tempBuf);
}
// Function to reduce surface reflections
void reduceSurfaceReflections(uint8_t* buf, int width, int height) {
const int kernelSize = 5;
const float threshold = 0.8; // Adjust this value to control reflection detection sensitivity
uint8_t* tempBuf = (uint8_t*)malloc(width * height);
if (!tempBuf) return;
for (int y = kernelSize/2; y < height - kernelSize/2; y++) {
for (int x = kernelSize/2; x < width - kernelSize/2; x++) {
float sum = 0;
float maxVal = 0;
// Calculate local average and find maximum value
for (int ky = -kernelSize/2; ky <= kernelSize/2; ky++) {
for (int kx = -kernelSize/2; kx <= kernelSize/2; kx++) {
int idx = (y+ky)*width + (x+kx);
sum += buf[idx];
maxVal = max(maxVal, (float)buf[idx]);
}
}
float avg = sum / (kernelSize * kernelSize);
// If the pixel is significantly brighter than its surroundings, reduce its intensity
if (buf[y*width + x] > avg * threshold && buf[y*width + x] > maxVal * threshold) {
tempBuf[y*width + x] = avg;
} else {
tempBuf[y*width + x] = buf[y*width + x];
}
}
}
memcpy(buf, tempBuf, width * height);
free(tempBuf);
}
// Function to perform color segmentation
void colorSegmentation(uint8_t* buf, int width, int height, int* labels) {
// Define color ranges for different objects (in HSV space)
const int SHRIMP_HUE_MIN = 0, SHRIMP_HUE_MAX = 20;
const int FISH_HUE_MIN = 80, FISH_HUE_MAX = 150;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = (y * width + x) * 3;
uint8_t r = buf[idx], g = buf[idx+1], b = buf[idx+2];
// Convert RGB to HSV
float h, s, v;
rgb2hsv(r, g, b, &h, &s, &v);
// Classify pixels based on color
if (h >= SHRIMP_HUE_MIN && h <= SHRIMP_HUE_MAX) {
labels[y * width + x] = 1; // Shrimp
} else if (h >= FISH_HUE_MIN && h <= FISH_HUE_MAX) {
labels[y * width + x] = 2; // Fish
} else {
labels[y * width + x] = 0; // Background
}
}
}
}
// Helper function to convert RGB to HSV
void rgb2hsv(uint8_t r, uint8_t g, uint8_t b, float* h, float* s, float* v) {
float r_prime = r / 255.0f;
float g_prime = g / 255.0f;
float b_prime = b / 255.0f;
float c_max = max(max(r_prime, g_prime), b_prime);
float c_min = min(min(r_prime, g_prime), b_prime);
float delta = c_max - c_min;
// Calculate hue
if (delta == 0) {
*h = 0;
} else if (c_max == r_prime) {
*h = 60 * fmod(((g_prime - b_prime) / delta), 6);
} else if (c_max == g_prime) {
*h = 60 * (((b_prime - r_prime) / delta) + 2);
} else {
*h = 60 * (((r_prime - g_prime) / delta) + 4);
}
// Calculate saturation
*s = (c_max == 0) ? 0 : (delta / c_max);
// Calculate value
*v = c_max;
}
// Helper function for flood fill
int floodFill(uint8_t* buf, int* labels, int width, int height, int x, int y, int label) {
int pixelCount = 0;
int stack[width * height][2], top = 0;
stack[top][0] = x;
stack[top][1] = y;
top++;
while (top > 0) {
top--;
int cx = stack[top][0];
int cy = stack[top][1];
if (cx < 0 || cx >= width || cy < 0 || cy >= height) continue;
if (buf[cy * width + cx] != 255 || labels[cy * width + cx] != 0) continue;
labels[cy * width + cx] = label;
pixelCount++;
if (top < width * height - 4) {
stack[top][0] = cx + 1; stack[top][1] = cy; top++;
stack[top][0] = cx - 1; stack[top][1] = cy; top++;
stack[top][0] = cx; stack[top][1] = cy + 1; top++;
stack[top][0] = cx; stack[top][1] = cy - 1; top++;
}
}
return pixelCount;
}
// HTTP handler for object count using the countObjectsInFrame function
static esp_err_t count_handler(httpd_req_t *req) {
// Capture the current frame from the camera
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) {
// If capture fails, return an error
Serial.println("Failed to get camera frame buffer");
httpd_resp_send_500(req); // Return HTTP 500 Internal Server Error
return ESP_FAIL;
}
// Use the object counting function to count objects in the frame
int count = countObjectsInFrame(fb);
// Return the frame buffer after processing
esp_camera_fb_return(fb);
// Prepare the response JSON
char resp_str[100];
snprintf(resp_str, sizeof(resp_str), "{\"objectCount\": %d}", count);
// Set the response type as JSON and send the object count
httpd_resp_set_type(req, "application/json");
return httpd_resp_send(req, resp_str, strlen(resp_str));
}
// HTTP handler untuk menangkap gambar dengan resolusi tinggi
esp_err_t capture_handler(httpd_req_t *req) {
camera_fb_t *fb = NULL;
esp_err_t res = ESP_OK;
size_t fb_len = 0;
int64_t fr_start = esp_timer_get_time();
// Set high resolution
esp_err_t err = init_camera(FRAME_SIZE_CAPTURE, PIXFORMAT_JPEG);
if (err != ESP_OK) {
Serial.printf("Failed to set high resolution: 0x%x\n", err);
httpd_resp_send_500(req);
return ESP_FAIL;
}
vTaskDelay(500 / portTICK_PERIOD_MS);
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
httpd_resp_send_500(req);
return ESP_FAIL;
}
fb_len = fb->len;
res = httpd_resp_set_type(req, "image/jpeg");
if (res == ESP_OK) {
res = httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
}
if (res == ESP_OK) {
res = httpd_resp_send(req, (const char *)fb->buf, fb->len);
}
esp_camera_fb_return(fb);
// Set low resolution
err = init_camera(FRAME_SIZE_STREAM, PIXFORMAT_JPEG);
if (err != ESP_OK) {
Serial.printf("Failed to set low resolution: 0x%x\n", err);
}
int64_t fr_end = esp_timer_get_time();
Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start)/1000));
if (res != ESP_OK) {
Serial.printf("Error in capture_handler: 0x%x\n", res);
httpd_resp_send_500(req);
}
return res;
}
// 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);
width: 90%;
max-width: 600px;
}
h1 {
color: #333;
}
.bg-container {
position: relative;
width: 100%;
max-width: 500px;
height: auto;
overflow: hidden;
margin: 20px auto;
}
.camera-container {
position: relative;
width: 100%;
max-width: 300px;
aspect-ratio: 1 / 1; /* Ensures a perfect square, 1:1 ratio */
overflow: hidden;
margin: 20px auto;
border-radius: 50%; /* Keeps it circular */
border: 5px solid #333;
}
#cameraFeed {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%; /* Ensures image stays circular */
}
#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;
}
/* Media queries for responsiveness */
@media (max-width: 768px) { /* Tablet */
body {
padding: 20px;
}
.container {
width: 100%;
max-width: 500px;
}
.camera-container {
max-width: 250px;
}
#randomCountDisplay {
font-size: 20px;
padding: 8px 16px;
}
button {
padding: 12px 24px;
font-size: 14px;
}
}
@media (max-width: 480px) { /* Smartphone */
body {
padding: 10px;
}
.container {
width: 100%;
}
.camera-container {
max-width: 200px;
}
#randomCountDisplay {
font-size: 18px;
padding: 6px 12px;
}
button {
padding: 10px 20px;
font-size: 14px;
}
}
</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;
}
// Fungsi untuk memulai web server kamera
void startCameraServer() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.stack_size = 10240; // Increase stack size if needed
config.max_uri_handlers = 8; // Increase if you have many endpoints
config.max_resp_headers = 8; // Adjust as needed
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
httpd_uri_t capture_uri = {
.uri = "/capture",
.method = HTTP_GET,
.handler = capture_handler,
.user_ctx = NULL
};
httpd_uri_t count_uri = {
.uri = "/count",
.method = HTTP_GET,
.handler = count_handler,
.user_ctx = NULL
};
httpd_uri_t pca_uri = {
.uri = "/capture_grayscale",
.method = HTTP_GET,
.handler = pca_grayscale_handler,
.user_ctx = NULL
};
if (httpd_start(&server, &config) == ESP_OK) {
httpd_register_uri_handler(server, &index_uri);
httpd_register_uri_handler(server, &stream_uri);
httpd_register_uri_handler(server, &capture_uri);
httpd_register_uri_handler(server, &count_uri);
httpd_register_uri_handler(server, &pca_uri);
}
config.server_port += 1;
config.ctrl_port += 1;
if (httpd_start(&stream_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &stream_uri);
}
}
void handle_error(esp_err_t error) {
if (error != ESP_OK) {
Serial.printf("Error occurred: %s\n", esp_err_to_name(error));
// Tambahkan tindakan yang sesuai, seperti restart ESP32
// esp_restart();
}
}
// Fungsi setup utama
void setup() {
Serial.begin(115200);
// Setup WiFi dalam mode AP
WiFi.softAP(ap_ssid, ap_password);
IPAddress IP = WiFi.softAPIP();
Serial.print("Access Point IP: ");
Serial.println(IP);
// Inisialisasi kamera pertama kali dengan resolusi VGA
esp_err_t err = init_camera(FRAME_SIZE_STREAM, PIXFORMAT_JPEG);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
startCameraServer();
}
void loop() {
if (server == NULL || stream_httpd == NULL) {
Serial.println("Error: HTTP server not started. Restarting...");
esp_restart();
}
delay(10000); // Check every 10 seconds
}
