目標:把 ESP32-CAM → 雲端比對(向量/人臉)→ 看板/APP 推播 → 數據回寫 的骨架一次搭好,並給你能用的最小程式骨架。
[ESP32-CAM] --HTTP/JPEG--> [Flask 後端(GCP)] --API--> [比對引擎]
| | ├─ Amazon Rekognition (人臉)
| | └─ Multimodal Embedding (向量)
| | ↑ 距離門檻: high=0.447 / med=0.632
| | (餘弦相似輔助)
| └──────────┬──────────┘
| │
└──(時間戳/點位)──→ [SQLite/日誌] └→ [推播建議/素材URL] → [數位看板/APP]
└→ [Dashboard 指標回寫]
向量門檻(報告摘錄):
high=0.447、medium=0.632;實測 Pair A1-A2 距離 0.408(高信心),A1-B1 距離 0.634(傾向不同個體)。
Arduino(ESP32-CAM)→ Flask /upload:
#include <WiFi.h>
#include "esp_camera.h"
#include <HTTPClient.h>
const char* ssid="YOUR_WIFI";
const char* pwd ="YOUR_PASS";
String server = "http://<YOUR_SERVER>:8000/upload";
void setup(){
WiFi.begin(ssid, pwd);
while(WiFi.status()!=WL_CONNECTED) delay(500);
// 省略:camera_config_t 初始化(OV2640, JPEG, SVGA/VGA)
// 建議:事件觸發時再拍照,或每 X 秒拍一張
}
void loop(){
camera_fb_t* fb = esp_camera_fb_get();
if(!fb) return;
HTTPClient http;
http.begin(server);
String boundary = "----esp32form";
http.addHeader("Content-Type","multipart/form-data; boundary="+boundary);
WiFiClient *s = http.getStreamPtr();
String head = "--"+boundary+"\r\nContent-Disposition: form-data; name=\"cam_id\"\r\n\r\ncam-001\r\n";
head += "--"+boundary+"\r\nContent-Disposition: form-data; name=\"image\"; filename=\"snap.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
String tail = "\r\n--"+boundary+"--\r\n";
int size = head.length() + fb->len + tail.length();
http.addHeader("Content-Length", String(size));
http.collectHeaders(nullptr,0);
s->print(head); s->write(fb->buf, fb->len); s->print(tail);
int code = http.POST(nullptr,0);
http.end();
esp_camera_fb_return(fb);
delay(3000); // 節流,實務上依場景調整
}
為什麼選 ESP32-CAM? 價格數百元、內建 OV2640、支援 Wi-Fi,HTTP 直傳到伺服器即可。
# server.py
from flask import Flask, request, jsonify
from datetime import datetime
import sqlite3, os
app = Flask(__name__)
def save_event(cam_id, path, decision, score):
conn = sqlite3.connect("events.db"); cur = conn.cursor()
cur.execute("""CREATE TABLE IF NOT EXISTS events(
ts TEXT, cam TEXT, path TEXT, decision TEXT, score REAL)""")
cur.execute("INSERT INTO events VALUES (?,?,?,?,?)",
(datetime.utcnow().isoformat(), cam_id, path, decision, score))
conn.commit(); conn.close()
@app.post("/upload")
def upload():
img = request.files["image"]
cam = request.form.get("cam_id","cam-001")
ts = datetime.utcnow().strftime("%Y%m%dT%H%M%S")
os.makedirs("uploads", exist_ok=True)
path = f"uploads/{cam}_{ts}.jpg"; img.save(path)
# 1) 呼叫 Rekognition 或 向量 API(此處以偽代碼示意)
# score, decision = face_or_vector_match(path) # decision: 'match' / 'unknown'
# 先用假資料跑通流程:
score, decision = 0.41, "match" # 門檻 0.447 以內視為高信心
save_event(cam, path, decision, score)
# 2) 回傳看板素材 URL(或直接 WebSocket 推送)
return jsonify({
"ok": True,
"decision": decision,
"score": score,
"asset_url": "https://your-cdn/ad_pages/berry_tart_85.html"
})
方案選擇:雲端 Amazon Rekognition(免自訓、boto3 易串接)或 多模態向量(延展性高)。報告內亦比較 Azure Face vs Rekognition 的授權/便利性。
ASCII:日報儀表模板
[今日轉換小結]
曝光 4,320 | 看板互動 372 (8.6%) | 推播 310 | 轉換 47 (15.2%)
高峰時段:12:00–14:00、18:00–21:00
熱門品類:甜點 / 健康飲品
向量門檻
distance < 0.447:高信心;< 0.632:中信心;超過則視為不同。建議中信心段落保守處理或人工複核。通知漏斗
[有影像] → [距離過門檻?] → [偏好/品類匹配?] → [冷卻&AB Test] → ✅ 推播
↑ ↑
0.447/0.632 避免洗頻
退避重試
抓取/上傳失敗 → 等 1s → 再試
若仍失敗 → 等 2s → 再試
再失敗 → 等 4s → 記錄 error_log 後暫停 1min
最小相依(節錄)
Flask==3.0.3, gunicorn==21.2.0
requests==2.32.3, beautifulsoup4==4.12.3, lxml>=5.3
line-bot-sdk>=3.11,<4
google-cloud-firestore==2.16.0, grpcio>=1.74
Cloud Run 重要環境變數
LINE_CHANNEL_ACCESS_TOKEN, LINE_CHANNEL_SECRET
DEFAULT_PERIOD_SEC (預設監看間隔, e.g., 60)
ALWAYS_NOTIFY (0/1) | MAX_PER_TICK | TICK_SOFT_DEADLINE_SEC
A. 甜點自由日
B. 運動後補給
C. 回購喚醒
0.447–0.632 屬中信心,建議人工覆核或退回匿名分群邏輯。decision/score/asset_url
明天(Day 3)把「監控儀表+A/B 測試+ROI 觀測」一次補齊,幫你做出可長跑的營運版。