PieMeter : Calculating Pi From a Real Pie With ESP32-CAM
by jules-24 in Workshop > 3D Printing
91 Views, 0 Favorites, 0 Comments
PieMeter : Calculating Pi From a Real Pie With ESP32-CAM
Hello! My name is Jules. I am a 15-year-old maker, and I share my first project on Instructables!
As soon as I saw the 'All Things Pi' contest, I started brainstorming ideas. I immediately saw the connection between 'Pi' (the mathematical constant), 'Pie' (the delicious dessert), and the geometric formulas we use to understand circles.
I decided to create a smart tool that bridges the gap between the kitchen and the math lab. My project uses an ESP32-CAM and a custom web interface to analyze a real pie in real-time. By using computer vision, the software calculates:
- The real-world Diameter : Using calibrated reference markers.
- The Circumference : Applying the classic formula C = pi * d
- The Surface Area : To understand the total size of the treat.
- The Optimal Number of Slices: Automatically calculating how to cut the pie based on its area, ensuring everyone gets a fair (and mathematically perfect) share!
I designed this to be simple, fun, and educational. I hope you enjoy it!
Supplies
Hardware & Electronics:
- ESP32-CAM Module: The brain of the project, including the camera sensor.
- External Antenna: To ensure a stable Wi-Fi connection while streaming video.
- ESP32-CAM Shield: This makes it easier to power the board and flash the code via USB.
- USB-C Cable: To connect the device to your computer or a power bank.
- FTDI Adapter (USB-to-Serial): Essential if you are not using a shield to upload the firmware.
3D Printing & Aesthetics:
- 3D Printer & PLA Filament: I used this specific ESP32-CAM Case to protect the electronics.
- Pi Symbol pi: A 3D printed Pi logo to decorate the case and stick to the theme of the contest.
Computer Vision & Setup:
- ArUco Markers: You need to print the first four markers from this document. Make sure to scale them so they are exactly 15cm wide when printed include a 3 cm with margin .
- A Ruler: To double-check your physical measurements and calibrate the initial setup.
The Most Important Ingredient:
- A Real Pie: Any round dessert (cherry, apple, or even a pizza!) to test the Smart PieMeter.
Flashing the Firmware
First Steps: Flashing the Firmware To get started, you need to flash the ESP32-CAM module using the Arduino IDE:
- Open Arduino IDE and go to the Board Manager.
- Select "AI Thinker ESP32-CAM" (or "ESP32 Dev Module") from the ESP32 Arduino library.
- Match your settings to the provided photo.
- To enter Flash Mode: Hold the BOOT button, press and release the RESET button, then keep holding BOOT until you see "Connecting..." on the screen.
- Wait for the flashing process to complete until every line is written.
Connecting to the Device Once flashed, press the RESET button to start the module. Check your phone or computer for a new Wi-Fi network:
- SSID: PieMeter_V3
- Password: piday2026 (These lines in the code define your network credentials).
After connecting, open your web browser and go to: 192.168.4.1
Step: Measurement and Calibration
Positioning the ArUco Markers
- Place the two ArUco markers on the surface, one on each side of the pie.
- Important: Ensure there is a 3cm margin between the physical black square of the marker and the edge of the pie (use the white border of the printout as a guide).
- Hold your camera about 1 meter (or more) above the pie.
- For the best precision: Keep your phone parallel to the imaginary line connecting the centers of the two markers.
Results & Accuracy Click on "Analyze Pie" to see the results. If your angle, orientation, and height are correct, the values will be very close to reality.
- Accuracy: The maximum margin of error is approximately 1.6 cm.
- Lighting: This sensor works outdoors, but ensure you aren't too far from the markers for the best detection.
Now you can enjoy your dessert! Cut your pie with mathematical perfection, thanks to Pi and this "Raspberry Pi-like" module.
Here Is the Code
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
// ================= Config Caméra (AI Thinker) =================
#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
const char* ssid = "PieMeter_2026";
const char* password = "piday2025";
WebServer server(80);
// Page HTML / JS embarquée
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>PieMeter V3 - PiDay 2026</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; text-align: center; background: #121212; color: white; margin: 0; }
.header { background: #ff9800; padding: 15px; font-weight: bold; font-size: 1.2em; box-shadow: 0 2px 10px rgba(0,0,0,0.5); }
#container { position: relative; display: inline-block; margin-top: 20px; border: 3px solid #333; border-radius: 10px; overflow: hidden; }
canvas { display: block; max-width: 100%; height: auto; background: #000; }
.controls { margin: 20px; }
button { background: #ff9800; border: none; color: white; padding: 12px 24px; font-size: 16px; border-radius: 5px; cursor: pointer; transition: 0.3s; margin: 5px; }
button:hover { background: #e68a00; transform: scale(1.05); }
button:active { transform: scale(0.95); }
#results { background: #1e1e1e; padding: 15px; margin: 10px auto; max-width: 400px; border-radius: 8px; border-left: 5px solid #ff9800; display: none; }
.stat { font-size: 1.2em; margin: 5px 0; }
.highlight { color: #ff9800; font-weight: bold; }
</style>
</head>
<body>
<div class="header">🥧 PIE METER - Pi Day 2026</div>
<div id="container">
<canvas id="display"></canvas>
</div>
<div class="controls">
<button id="btnStream" onclick="toggleStream()">📷 LIVE STREAM</button>
<button id="btnAnalyze" onclick="analyzeFrame()">🔍 ANALYSER LA TARTE</button>
</div>
<div id="results">
<div class="stat">Diamètre : <span id="txtDiam" class="highlight">-</span> px</div>
<div class="stat">Circonférence : <span id="txtCirc" class="highlight">-</span> px</div>
<div style="margin-top:10px; font-size:0.8em; color:#888;">(Basé sur marqueurs ArUco 4x4)</div>
</div>
<script>
const canvas = document.getElementById('display');
const ctx = canvas.getContext('2d');
const btnStream = document.getElementById('btnStream');
const results = document.getElementById('results');
let streaming = true;
const img = new Image();
// --- BOUCLE DE STREAM ---
function updateFrame() {
if (!streaming) return;
img.src = "/capture?t=" + Date.now();
}
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
if(streaming) setTimeout(updateFrame, 50);
};
function toggleStream() {
streaming = true;
results.style.display = "none";
updateFrame();
}
// --- LOGIQUE D'ANALYSE ---
function analyzeFrame() {
streaming = false;
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
const markers = detectArUco(data);
if (markers.length >= 2) {
// On calcule la distance entre les deux premiers marqueurs trouvés
const d = Math.hypot(markers[1].x - markers[0].x, markers[1].y - markers[0].y).toFixed(1);
const c = (d * Math.PI).toFixed(1);
document.getElementById('txtDiam').innerText = d;
document.getElementById('txtCirc').innerText = c;
results.style.display = "block";
// Dessiner les résultats
ctx.strokeStyle = "#00ff00";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(markers[0].x, markers[0].y);
ctx.lineTo(markers[1].x, markers[1].y);
ctx.stroke();
} else {
alert("Montrez au moins 2 marqueurs ArUco (les carrés noirs) !");
streaming = true;
updateFrame();
}
}
// Détecteur ArUco simplifié (Recherche de carrés noirs contrastés)
function detectArUco(pixels) {
const width = pixels.width;
const height = pixels.height;
const data = pixels.data;
const found = [];
// Analyse simplifiée : recherche de blocs sombres entourés de clair
// On scanne l'image par grille pour gagner du temps
for (let y = 20; y < height - 20; y += 15) {
for (let x = 20; x < width - 20; x += 15) {
let idx = (y * width + x) * 4;
let gray = (data[idx] + data[idx+1] + data[idx+2]) / 3;
if (gray < 50) { // Un point noir trouvé
// On vérifie si c'est un carré (simplifié)
if (!found.some(m => Math.hypot(m.x - x, m.y - y) < 40)) {
found.push({x: x, y: y});
}
}
}
}
return found;
}
// Lancer au démarrage
updateFrame();
</script>
</body>
</html>
)rawliteral";
void handleCapture() {
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) {
server.send(500, "text/plain", "Cam Error");
return;
}
server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
server.send_P(200, "image/jpeg", (const char
V1 and V2
many fails with the detection :(