Xây dựng AI Đọc Nháy Mắt và Giao Tiếp Bằng Morse 👁️
Giới thiệu
Bạn có bao giờ nghĩ rằng mình có thể giao tiếp bằng đôi mắt? Đó là một ý tưởng mà nghe có vẻ như từ một bộ phim khoa học viễn tưởng. Khi tôi lần đầu tiên thấy một AI vẽ 478 điểm trên khuôn mặt của tôi trong thời gian thực, ngay trong trình duyệt, thì ý tưởng đó bỗng nhiên trở nên khả thi. Chiếc mạng trắng, bám sát từng đường nét, biết chính xác vị trí của đôi môi, má và đầu mũi của tôi. Nó biết khi nào tôi nháy mắt.
Tôi quyết định sẽ sử dụng nó để thực hiện một giấc mơ từ lâu: thực sự giao tiếp bằng đôi mắt. Kế hoạch của tôi là chuyển đổi những cái nháy mắt của mình thành mã Morse. Điều này hóa ra khó khăn và thú vị hơn tôi từng tưởng tượng!
Dự án này cũng được truyền cảm hứng sâu sắc từ câu chuyện tuyệt vời của Jeremiah Denton, một phi công Mỹ bị bắt giữ, người đã nháy mắt để truyền đạt từ "TORTURE" trong một video tuyên truyền. Tôi muốn xem liệu mình có thể xây dựng một phiên bản kỹ thuật số, mã nguồn mở của sự thông minh con người đáng kinh ngạc đó.
Dưới đây là phân tích về những thách thức kỹ thuật thú vị nhất và cách tôi đã giải quyết chúng.
Công nghệ sử dụng
Công nghệ mà tôi sử dụng khá đơn giản:
- JavaScript (ES6 Modules): Để xử lý tất cả logic.
- MediaPipe Face Landmarker: Để phát hiện điểm đặc trưng và hình dạng khuôn mặt trong thời gian thực. Đây là phần cốt lõi của dự án.
- HTML5 & CSS3: Để tạo giao diện. Không cần framework!
Thách thức #1: Phân biệt dấu chấm và dấu gạch
Vấn đề đầu tiên là phát hiện nháy mắt. MediaPipe cung cấp điểm số theo thời gian thực cho 52 biểu cảm khuôn mặt khác nhau, gọi là "blendshapes," theo dõi mọi thứ từ nụ cười đến lông mày nâng cao. May mắn thay, hai trong số các blendshapes đó chính xác là những gì tôi cần: eyeBlinkLeft và eyeBlinkRight.
Cách chúng hoạt động rất đơn giản: Điểm số gần 0 có nghĩa là mắt đang mở, và điểm số gần 1 có nghĩa là mắt đang đóng.
Suy nghĩ đầu tiên của tôi là, "Dễ thôi, tôi chỉ cần sử dụng setTimeout để xem mắt đóng bao lâu." Đây là một ý tưởng tồi. Nó rối và không đồng bộ với vòng lặp requestAnimationFrame của video.
Giải pháp thực sự đơn giản hơn: đếm số khung hình.
Vì AI phân tích từng khung hình, tôi chỉ cần tăng một bộ đếm cho mỗi khung hình liên tiếp mà mắt đóng lại.
Logic:
- Nếu điểm số nháy mắt vượt ngưỡng (ví dụ,
0.5), tăngblinkFrameCounter. - Nếu điểm số giảm dưới ngưỡng, kiểm tra giá trị của bộ đếm.
- Nếu
blinkFrameCounternằm trong khoảng2-14khung hình, đó là một nháy ngắn (dấu chấm). - Nếu là
15khung hình trở lên, đó là một nháy dài (dấu gạch).
Đây là đoạn mã cho logic đó:
javascript
// Từ js/detection.js
// Kiểm tra xem điểm số nháy mắt trung bình có vượt qua ngưỡng không
if ((rightEyeBlink + leftEyeBlink) / 2.0 > CONST.BLINK_THRESHOLD) {
appState.blinkFrameCounter++;
return null; // Vẫn đang nháy mắt, chưa làm gì cả
}
// Nếu chúng ta đã ngừng nháy mắt, kiểm tra thời gian nháy
if (appState.blinkFrameCounter >= CONST.BLINK_CONSECUTIVE_FRAMES) {
const isLong = appState.blinkFrameCounter >= CONST.LONG_BLINK_FRAMES;
appState.blinkFrameCounter = 0; // Đặt lại cho lần sau
return isLong ? 'long' : 'short'; // Thành công!
}
// Nếu chúng ta không nháy đủ lâu, chỉ cần đặt lại
appState.blinkFrameCounter = 0;
return null;
Thách thức #2: Phát hiện gật đầu không có độ rung
Để phân tách các chữ cái và từ, tôi muốn sử dụng việc gật đầu. Nỗ lực đầu tiên của tôi là theo dõi vị trí y của đầu mũi (điểm đặc trưng #4) và so sánh nó theo từng khung hình. Điều này đã thất bại thảm hại. Hệ thống nhạy cảm đến nỗi nó kích hoạt chỉ từ một hơi thở nhẹ!
Chìa khóa là nhận ra rằng một cái gật đầu là một hành trình, không phải là một bức ảnh. Giải pháp: một thuật toán "cửa sổ trượt".
Logic:
- Giữ một lịch sử liên tục của vị trí
ycủa mũi trong một mảng (noseYHistory) cho 15 khung hình cuối. - Trong mỗi khung hình, tìm
minvàmaxY-position trong lịch sử đó. - Nếu
(max - min)lớn hơn ngưỡng chuyển động của chúng ta, thì chúng ta có một cái gật đầu rõ ràng, có chủ ý.
Đoạn mã cho logic đó:
javascript
// Từ js/detection.js
export function processNod(landmarks) {
const noseY = landmarks[CONST.NOSE_TIP_INDEX].y * DOM.video.videoHeight;
appState.noseYHistory.push(noseY);
// Giữ lịch sử ở một độ dài cố định
if (appState.noseYHistory.length > CONST.NOD_HISTORY_LENGTH) {
appState.noseYHistory.shift();
}
// Không phát hiện gật đầu ngay lập tức
if (appState.nodCooldownCounter > 0) {
appState.nodCooldownCounter--;
return false;
}
if (appState.noseYHistory.length === CONST.NOD_HISTORY_LENGTH) {
const minY = Math.min(...appState.noseYHistory);
const maxY = Math.max(...appState.noseYHistory);
if ((maxY - minY) > CONST.NOD_MOVEMENT_THRESHOLD) {
appState.nodCooldownCounter = CONST.NOD_COOLDOWN_FRAMES; // Đặt cooldown
appState.noseYHistory = []; // Xóa lịch sử sau khi phát hiện
return true; // Chúng ta đã có một cái gật đầu!
}
}
return false;
}
Thách thức #3: Tỷ lệ má và mũi cho việc quay đầu
Phát hiện việc quay đầu để thực hiện backspace và khoảng trắng cũng gặp vấn đề "rung" tương tự. Giải pháp đến từ việc nhận thấy tỷ lệ khuôn mặt của tôi thay đổi khi tôi quay đầu.
Khi nhìn thẳng, khoảng cách từ mũi đến má trái gần như bằng với má phải. Khi tôi quay sang trái, má phải của tôi trở nên "rộng" hơn so với góc nhìn của mũi, và má trái trở nên "hẹp" hơn.
Logic:
- Lấy tọa độ
xcủa má trái (#454), má phải (#234), và mũi (#4). - Tính tổng chiều rộng:
leftCheek.x - rightCheek.x. - Tính tỷ lệ:
(nose.x - rightCheek.x) / totalCheekWidth. - Khi nhìn thẳng, tỷ lệ này là
~0.5. Khi tôi quay sang trái, nó tăng lên (> 0.65). Khi tôi quay sang phải, nó giảm xuống (< 0.35).
Điều này hoạt động rất tốt đã làm tôi rất hài lòng!
Đoạn mã cho logic đó:
javascript
// Từ js/detection.js
export function processTurn(landmarks) {
const nose = landmarks[CONST.NOSE_TIP_INDEX];
const leftCheek = landmarks[CONST.LEFT_CHEEK_INDEX];
const rightCheek = landmarks[CONST.RIGHT_CHEEK_INDEX];
const totalCheekWidth = leftCheek.x - rightCheek.x;
const noseToRightCheekDist = nose.x - rightCheek.x;
if (totalCheekWidth > 0.1) { // Tránh chia cho 0
const turnRatio = noseToRightCheekDist / totalCheekWidth;
if (turnRatio > CONST.TURN_RIGHT_RATIO_THRESHOLD) {
return 'left';
} else if (turnRatio < CONST.TURN_LEFT_RATIO_THRESHOLD) {
return 'right';
}
}
return null; // Đầu đang ở giữa
}
Kết luận
Dự án này là một trong những dự án thú vị nhất mà tôi từng thực hiện. Đây là một ví dụ tuyệt vời về cách mà AI dựa trên trình duyệt đã trở nên dễ tiếp cận và mạnh mẽ như thế nào. Những gì trước đây yêu cầu phần cứng chuyên dụng giờ đây có thể được xây dựng chỉ với một chút sáng tạo và các công cụ mã nguồn mở.
Nếu bạn có bất kỳ câu hỏi nào, hãy để lại trong phần bình luận bên dưới. Tôi rất muốn nghe ý kiến của bạn! Bạn sẽ xây dựng gì với công nghệ này?
Video đầy đủ
- Demo Trực Tiếp: Nhấn vào đây!
- Video Đầy Đủ: Xem trên YouTube
- GitHub Repo: Nhấn vào đây! Chúc bạn lập trình vui vẻ! ✨