Python 電腦視覺 + ESP32 TFT LCD 即時顯示
Python 負責「看」(偵測人臉與眼睛),ESP32 負責「顯示」(讀書時間與狀態警告)。
兩者透過 USB Serial 溝通,格式簡單:READING,125\n
camera_project/test.py
安裝所需套件:
pip install opencv-python pyserial
| 套件 | 用途 |
|---|---|
opencv-python | 攝影機擷取、人臉偵測、眼睛偵測 |
pyserial | Python ↔ ESP32 Serial 通訊 |
pip install pyobjc-framework-AVFoundation
用於自動偵測 iPhone Continuity Camera
haarcascade_frontalface_default.xmlhaarcascade_eye.xml# 載入偵測器(OpenCV 內建路徑)
face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_eye.xml')
face_cascade.detectMultiScale() 偵測人臉eye_cascade 偵測眼睛避免嘴巴被誤判為眼睛,只取 y 到 y + h//2 的區域。
# 擷取畫面並轉灰階
ret, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 偵測人臉
faces = face_cascade.detectMultiScale(
gray,
scaleFactor=1.1, # 每次縮小 10%,偵測不同大小的臉
minNeighbors=5, # 至少 5 個重疊區域才算偵測到
minSize=(80, 80) # 忽略太小的區域
)
| 參數 | 說明 | 建議值 |
|---|---|---|
scaleFactor | 影像縮放比例 | 1.1(每層縮 10%) |
minNeighbors | 門檻值,越高越嚴格 | 5(減少誤判) |
minSize | 最小偵測尺寸 | (80, 80) |
# 只取臉的上半部來偵測眼睛
eye_roi_gray = gray[y : y + h // 2, x : x + w]
eyes = eye_cascade.detectMultiScale(
eye_roi_gray, scaleFactor=1.1,
minNeighbors=5, minSize=(20, 20)
)
eyes_open = len(eyes) >= 1 # 偵測到至少 1 隻眼睛 → 睜眼
if eyes_open:
status = "READING" # 張眼 → 讀書中
elif closed_secs >= 3:
status = "SLEEPING" # 閉眼超過 3 秒 → 打瞌睡
len(eyes) >= 1:偵測到至少一隻眼睛就算睜眼。
閉眼不一定馬上警告,需要持續 3 秒才觸發 SLEEPING。
study_seconds = 0
last_study_tick = None
# 在主迴圈中:
if status == "READING":
if last_study_tick is None:
last_study_tick = now # 開始計時
else:
study_seconds += int(now - last_study_tick)
last_study_tick = now # 累積秒數
else:
last_study_tick = None # 非讀書狀態 → 暫停計時
只有 READING 狀態才累積時間。SLEEPING 和 AWAY 時暫停計時,但不會歸零。
import serial
import serial.tools.list_ports
# 自動偵測 ESP32 的 USB 序列埠
def find_esp32_port():
keywords = ['usbserial', 'CP210', 'CH340']
for p in serial.tools.list_ports.comports():
if any(k.lower() in p.device.lower() for k in keywords):
return p.device
return None
# 連線(連不上也能繼續用,只是不顯示在螢幕上)
port = find_esp32_port()
esp32 = serial.Serial(port, 9600, timeout=1)
# 每秒傳送一次狀態
message = f'{status},{study_seconds}\n'
esp32.write(message.encode())
READING,125\n — 讀書中,累積 125 秒
SLEEPING,125\n — 打瞌睡,累積時間不增加
AWAY,125\n — 離座,累積時間不增加
環境建置與燒錄設定
Mac 找不到 ESP32?安裝 CH340 驅動:brew install --cask wch-ch34x-usb-serial-driver
或到 wch-ic.com 下載安裝。
Cmd+,)https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
esp32 by Espressif Systems工具 → 開發板 → 應該能看到 ESP32 Dev Module
Ctrl+Shift+I)by BodmerUser_Setup.h 複製到函式庫資料夾# 複製 User_Setup.h(覆蓋原檔)
cp camera_project/esp32/User_Setup.h \
~/Documents/Arduino/libraries/TFT_eSPI/User_Setup.h
:: 複製 User_Setup.h(覆蓋原檔)
copy camera_project\esp32\User_Setup.h ^
Documents\Arduino\libraries\TFT_eSPI\User_Setup.h
螢幕不會顯示任何東西!因為預設腳位跟 ESP32-2432S028 不同。
這個檔案告訴 TFT_eSPI 函式庫怎麼跟螢幕溝通:
// 驅動晶片 — ESP32-2432S028 使用 ILI9341
#define ILI9341_DRIVER
// 螢幕尺寸
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
// SPI 腳位(ESP32-2432S028 內建接線,不可更改)
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST -1 // 連到 EN,不需獨立腳位
#define TFT_BL 21 // 背光
ESP32-2432S028 的 SPI 腳位是固定的(13, 14, 15, 2),與 TFT_eSPI 預設不同。
Arduino/study_monitor/study_monitor.ino/dev/cu.usbserial-XXXX Windows: COM3(依實際而定)試試按住 ESP32 上的 BOOT 按鈕再點上傳,出現 "Connecting..." 後放開。
Arduino/study_monitor/study_monitor.ino
STATUS,secondsfullRedraw() — 整頁重繪updateTimeOnly() — 只更新時間drawReadingScreen()drawSleepingScreen()drawAwayScreen()void loop() {
// 格式:STATUS,seconds\n
if (Serial.available()) {
String data = Serial.readStringUntil('\n');
data.trim();
int comma = data.indexOf(',');
if (comma > 0) {
currentStatus = data.substring(0, comma); // "READING"
studySeconds = data.substring(comma + 1).toInt(); // 125
lastReceive = millis();
}
}
// 狀態改變 → 完整重繪
if (currentStatus != prevStatus) {
fullRedraw();
}
// 同狀態但秒數更新 → 局部更新(減少閃爍)
else if (studySeconds != prevSeconds && currentStatus == "READING") {
updateTimeOnly();
}
}
void drawReadingScreen() {
drawHeader("Reading", C_DARKGREEN); // 綠色橫幅
// 大時鐘(使用 Font 7,最大字體)
char timeBuf[12];
formatTime(studySeconds, timeBuf); // "12:34" 或 "1:23:45"
tft.setTextColor(C_GREEN, C_BG);
tft.drawString(timeBuf, x, 60, 7);
drawCentered("Keep going!", 145, C_GRAY, 2);
// 60 分鐘目標進度條
drawProgressBar(studySeconds);
}
// SLEEPING 和 AWAY 每 500ms 閃爍一次
if ((currentStatus == "SLEEPING" || currentStatus == "AWAY") &&
millis() - blinkTimer > 500) {
blinkTimer = millis();
blinkOn = !blinkOn; // 切換顯示/隱藏文字
fullRedraw();
}
// 10 秒沒收到資料 → 自動回到等待畫面
if (lastReceive > 0 && millis() - lastReceive > 10000) {
currentStatus = "WAIT";
}
#define GOAL_MIN 60 // 目標讀書 60 分鐘
void drawProgressBar(int seconds) {
int goalSec = GOAL_MIN * 60;
int progress = constrain(seconds, 0, goalSec);
int fillW = (long)BAR_W * progress / goalSec;
// 顏色隨進度改變
uint16_t barColor =
(progress < goalSec / 3) ? C_ORANGE : // 0~33%:橘色
(progress < goalSec * 2 / 3) ? C_YELLOW : // 33~66%:黃色
C_GREEN; // 66~100%:綠色
tft.fillRect(BAR_X, BAR_Y, fillW, BAR_H, barColor);
// 百分比文字
int pct = (long)progress * 100 / goalSec;
sprintf(pctBuf, "%d%%", pct);
}
修改 GOAL_MIN 的值即可調整目標時間,例如改成 30 分鐘或 90 分鐘。
User_Setup.hstudy_monitor.ino,燒錄到 ESP32python3 camera_project/test.py可先用 camera_project/esp32/serial_test.py 測試 Serial 通訊,
不需要攝影機就能確認 ESP32 螢幕是否正常。
張順捷/
├── camera_project/
│ ├── test.py ← Python 主程式(人臉偵測 + Serial 傳送)
│ └── esp32/
│ ├── User_Setup.h ← TFT_eSPI 設定檔(必須覆蓋)
│ ├── serial_test.py ← Serial 通訊測試(假資料)
│ └── display.ino ← 舊版顯示程式(參考用)
│
├── Arduino/
│ └── study_monitor/
│ └── study_monitor.ino ← ESP32 顯示程式(新版,本次新增)
│
└── requirements.txt ← Python 套件清單
讀書監督系統 — 技術說明完畢