pca grayscale handler cannot run below UXGA resolution. Lowering the resolution will restart
// 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 sebelum capture
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);
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;
// BMP conversion based on frame size
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 (dynamic frame size support)
size_t out_width = out_buf[18] + (out_buf[19] << 8); // Dynamic width
size_t out_height = out_buf[22] + (out_buf[23] << 8); // Dynamic height
// 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;
}
full code
#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;
// Define the RGB structure
struct RGB {
uint8_t r, g, b;
};
#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;
}
// 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 len = 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] / len;
}
}
// Project each pixel onto the principal component
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]);
grayscale[i / 2] = vec[0] * rgb.r + vec[1] * rgb.g + vec[2] * rgb.b;
}
return grayscale;
}
// Otsu's thresholding method
uint8_t* otsu_threshold(uint8_t* grayscale, size_t pixel_count) {
int histogram[256] = {0};
for (size_t i = 0; i < pixel_count; i++) {
histogram[grayscale[i]]++;
}
int total = pixel_count;
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;
int 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;
}
}
uint8_t* binary = (uint8_t*)malloc(pixel_count);
if (!binary) return nullptr;
for (size_t i = 0; i < pixel_count; i++) {
binary[i] = (grayscale[i] > threshold) ? 255 : 0;
}
return binary;
}
// 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 sebelum capture
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);
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;
// BMP conversion based on frame size
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 (dynamic frame size support)
size_t out_width = out_buf[18] + (out_buf[19] << 8); // Dynamic width
size_t out_height = out_buf[22] + (out_buf[23] << 8); // Dynamic height
// 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;
}
// 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 Camera</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: 30px;
border-radius: 15px;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
width: 90%;
max-width: 600px;
}
h1 {
color: #333;
margin-bottom: 30px;
}
.camera-container {
position: relative;
width: 100%;
max-width: 400px;
aspect-ratio: 1 / 1;
overflow: hidden;
margin: 0 auto 30px;
border-radius: 50%;
border: 5px solid #333;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
#cameraFeed {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
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: 8px;
transition: background-color 0.3s;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
button:hover {
background-color: #45a049;
}
/* Media queries for responsiveness */
@media (max-width: 768px) {
body {
padding: 20px;
}
.container {
width: 100%;
max-width: 500px;
padding: 20px;
}
.camera-container {
max-width: 300px;
}
button {
padding: 12px 24px;
font-size: 14px;
}
}
@media (max-width: 480px) {
body {
padding: 10px;
}
.container {
padding: 15px;
}
.camera-container {
max-width: 250px;
}
button {
padding: 10px 20px;
font-size: 14px;
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 Camera</h1>
<div class="camera-container">
<img id="cameraFeed" src="/stream" alt="Camera Stream">
</div>
<button onclick="saveImage()">Save Image</button>
</div>
<script>
function saveImage() {
const video = document.getElementById('cameraFeed');
const canvas = document.createElement('canvas');
canvas.width = 1600;
canvas.height = 1600;
const context = canvas.getContext('2d');
// Draw white background
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
// Calculate new position for the circular camera feed
const centerY = 800;
const radius = 600;
const videoSize = radius * 2;
const videoY = centerY - radius;
// Draw circular camera feed
context.save();
context.beginPath();
context.arc(800, centerY, radius, 0, Math.PI * 2);
context.clip();
context.drawImage(video, 200, videoY, videoSize, videoSize);
context.restore();
// Draw border
context.strokeStyle = '#333';
context.lineWidth = 20;
context.beginPath();
context.arc(800, centerY, radius, 0, Math.PI * 2);
context.stroke();
canvas.toBlob(function(blob) {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'esp32_camera.jpg';
link.click();
}, 'image/jpeg');
}
</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 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, &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);
#ifdef RGB_BUILTIN
digitalWrite(RGB_BUILTIN, HIGH); // Turn the RGB LED white
delay(1000);
digitalWrite(RGB_BUILTIN, LOW); // Turn the RGB LED off
delay(1000);
neopixelWrite(RGB_BUILTIN,RGB_BRIGHTNESS,0,0); // Red
delay(1000);
neopixelWrite(RGB_BUILTIN,0,RGB_BRIGHTNESS,0); // Green
delay(1000);
neopixelWrite(RGB_BUILTIN,0,0,RGB_BRIGHTNESS); // Blue
delay(1000);
neopixelWrite(RGB_BUILTIN,0,0,0); // Off / black
delay(1000);
#endif
// 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
}