專題技術說明

讀書監督系統

Python 電腦視覺 + ESP32 TFT LCD 即時顯示

架構

系統架構總覽

攝影機
webcam / iPhone
Python
OpenCV 人臉 / 眼睛偵測
Serial (USB)
STATUS,seconds\n
ESP32
TFT LCD 顯示
核心概念

Python 負責「看」(偵測人臉與眼睛),ESP32 負責「顯示」(讀書時間與狀態警告)。
兩者透過 USB Serial 溝通,格式簡單:READING,125\n

狀態設計

三種偵測狀態

READING

  • 偵測到人臉
  • 眼睛張開
  • 累積讀書時間
  • ESP32 顯示綠色主題

SLEEPING

  • 偵測到人臉
  • 閉眼超過 3 秒
  • 暫停計時
  • ESP32 橘色閃爍警告

AWAY

  • 未偵測到人臉
  • 離座超過 5 秒
  • 暫停計時
  • ESP32 紅色閃爍警告

Part 1
Python 程式說明

camera_project/test.py

環境

Python 環境建置

安裝所需套件:

pip install opencv-python pyserial
套件用途
opencv-python攝影機擷取、人臉偵測、眼睛偵測
pyserialPython ↔ ESP32 Serial 通訊
Mac 額外套件

pip install pyobjc-framework-AVFoundation
用於自動偵測 iPhone Continuity Camera

偵測原理

Haar Cascade 偵測器

什麼是 Haar Cascade?

  • OpenCV 內建的物件偵測演算法
  • 使用預訓練的 XML 模型檔
  • 速度快,不需要 GPU
  • 適合即時偵測人臉與眼睛

本專案使用的模型

  • haarcascade_frontalface_default.xml
    → 偵測正面人臉
  • haarcascade_eye.xml
    → 偵測眼睛(睜眼/閉眼)
  • 兩者皆 OpenCV 內建,不需下載
# 載入偵測器(OpenCV 內建路徑)
face_cascade = cv2.CascadeClassifier(
    cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
eye_cascade  = cv2.CascadeClassifier(
    cv2.data.haarcascades + 'haarcascade_eye.xml')
流程

偵測流程

1. 擷取攝影機畫面 → 轉灰階
2. face_cascade.detectMultiScale() 偵測人臉
3. 在臉的上半部eye_cascade 偵測眼睛
4. 根據結果判斷狀態:READING / SLEEPING / AWAY
5. 累積讀書時間 → 透過 Serial 傳送給 ESP32
為什麼只偵測臉的上半部?

避免嘴巴被誤判為眼睛,只取 yy + 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 時暫停計時,但不會歸零。

通訊

Serial 通訊(Python 端)

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 — 離座,累積時間不增加

Part 2
Arduino IDE 使用

環境建置與燒錄設定

安裝

Arduino IDE 安裝

  1. arduino.cc/en/software 下載 Arduino IDE 2.x
  2. 安裝完成後開啟 Arduino IDE
  3. Mac 使用者可能需要安裝 USB 驅動(CH340 或 CP210x)
常見問題

Mac 找不到 ESP32?安裝 CH340 驅動:
brew install --cask wch-ch34x-usb-serial-driver
或到 wch-ic.com 下載安裝。

設定

加入 ESP32 開發板支援

  1. 開啟 Arduino IDE → 偏好設定(Mac: Cmd+,
  2. 在「額外開發板管理員網址」填入:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  1. 工具 → 開發板開發板管理員
  2. 搜尋 esp32,安裝 esp32 by Espressif Systems
安裝完成後

工具 → 開發板 → 應該能看到 ESP32 Dev Module

函式庫

安裝 TFT_eSPI 函式庫

  1. Arduino IDE → 工具 → 程式庫管理員(或 Ctrl+Shift+I
  2. 搜尋 TFT_eSPI,安裝 by Bodmer
  3. 關鍵步驟:將專案的 User_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
不覆蓋 User_Setup.h 會怎樣?

螢幕不會顯示任何東西!因為預設腳位跟 ESP32-2432S028 不同。

設定檔

User_Setup.h 重點

這個檔案告訴 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 預設不同。

燒錄

燒錄到 ESP32

  1. Arduino IDE 開啟 Arduino/study_monitor/study_monitor.ino
  2. 工具 → 開發板 → 選擇 ESP32 Dev Module
  3. 工具 → Port → 選擇 ESP32 的序列埠
    Mac: /dev/cu.usbserial-XXXX Windows: COM3(依實際而定)
  4. 工具 → Upload Speed → 921600
  5. 按下 上傳 按鈕(→ 箭頭圖示)
  6. 等待編譯 + 上傳完成,螢幕顯示 "Waiting for PC..."
上傳失敗?

試試按住 ESP32 上的 BOOT 按鈕再點上傳,出現 "Connecting..." 後放開。

Part 3
Arduino 程式碼

Arduino/study_monitor/study_monitor.ino

架構

Arduino 程式架構

setup() — 初始化螢幕、開啟 Serial、顯示等待畫面
loop() — 持續執行

loop() 做的事

  • 讀取 Serial 資料
  • 解析 STATUS,seconds
  • 狀態改變 → 完整重繪
  • 秒數改變 → 局部更新
  • 警告畫面 → 閃爍效果

主要函式

  • fullRedraw() — 整頁重繪
  • updateTimeOnly() — 只更新時間
  • drawReadingScreen()
  • drawSleepingScreen()
  • drawAwayScreen()
程式碼

接收 Serial 資料

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();
  }
}
程式碼

READING 畫面 — 綠色主題

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);
}
Reading
12:34
Keep going!
20%
程式碼

警告畫面 — 閃爍效果

// 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";
}
!! SLEEPING !!
Wake up!
Open your eyes!
Total: 12:34
!! AWAY !!
Come back
and study!
Total: 12:34
程式碼

目標進度條

#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 分鐘。

操作

實際操作步驟

  1. 安裝 TFT_eSPI 函式庫,覆蓋 User_Setup.h
  2. Arduino IDE 開啟 study_monitor.ino,燒錄到 ESP32
  3. ESP32 螢幕顯示 "Waiting for PC..."
  4. 終端機執行 python3 camera_project/test.py
  5. Python 自動偵測 ESP32,開始傳送資料
  6. ESP32 螢幕即時顯示讀書狀態與累積時間
測試小技巧

可先用 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 套件清單

Thank You!

讀書監督系統 — 技術說明完畢