XiaoBai1221 commited on
Commit
44292e3
·
1 Parent(s): 62595e3

🚀 Deploy SignView: 完整手語辨識系統部署到HuggingFace Spaces - 整合所有功能到單一app.py, 支援即時攝像頭辨識+影片上傳+Messenger Bot, PyTorch LSTM+Attention模型, MediaPipe特徵提取+OpenAI GPT-4o-mini, 支援4種手語: eat/fish/like/want

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. Dockerfile +24 -22
  3. Procfile +0 -1
  4. README.md +177 -14
  5. app.py +907 -123
  6. app_config.py +41 -0
  7. {features → data/features}/keypoints/eat_001_aug_rotate_keypoints.npy +0 -0
  8. {features → data/features}/keypoints/eat_001_aug_shift_keypoints.npy +0 -0
  9. {features → data/features}/keypoints/eat_001_keypoints.npy +0 -0
  10. {features → data/features}/keypoints/eat_002_aug_rotate_keypoints.npy +0 -0
  11. {features → data/features}/keypoints/eat_002_aug_shift_keypoints.npy +0 -0
  12. {features → data/features}/keypoints/eat_002_keypoints.npy +0 -0
  13. {features → data/features}/keypoints/eat_003_aug_rotate_keypoints.npy +0 -0
  14. {features → data/features}/keypoints/eat_003_keypoints.npy +0 -0
  15. {features → data/features}/keypoints/eat_004_aug_flip_keypoints.npy +0 -0
  16. {features → data/features}/keypoints/eat_004_aug_shift_keypoints.npy +0 -0
  17. {features → data/features}/keypoints/eat_004_keypoints.npy +0 -0
  18. {features → data/features}/keypoints/eat_005_aug_flip_keypoints.npy +0 -0
  19. {features → data/features}/keypoints/eat_005_keypoints.npy +0 -0
  20. {features → data/features}/keypoints/eat_006_keypoints.npy +0 -0
  21. {features → data/features}/keypoints/eat_007_aug_flip_keypoints.npy +0 -0
  22. {features → data/features}/keypoints/eat_007_keypoints.npy +0 -0
  23. {features → data/features}/keypoints/eat_008_aug_flip_keypoints.npy +0 -0
  24. {features → data/features}/keypoints/eat_008_keypoints.npy +0 -0
  25. {features → data/features}/keypoints/eat_009_aug_flip_keypoints.npy +0 -0
  26. {features → data/features}/keypoints/eat_009_aug_rotate_keypoints.npy +0 -0
  27. {features → data/features}/keypoints/eat_009_aug_shift_keypoints.npy +0 -0
  28. {features → data/features}/keypoints/eat_009_keypoints.npy +0 -0
  29. {features → data/features}/keypoints/eat_010_aug_flip_keypoints.npy +0 -0
  30. {features → data/features}/keypoints/eat_010_aug_rotate_keypoints.npy +0 -0
  31. {features → data/features}/keypoints/eat_010_aug_shift_keypoints.npy +0 -0
  32. {features → data/features}/keypoints/eat_010_keypoints.npy +0 -0
  33. {features → data/features}/keypoints/eat_011_aug_shift_keypoints.npy +0 -0
  34. {features → data/features}/keypoints/eat_011_keypoints.npy +0 -0
  35. {features → data/features}/keypoints/eat_012_aug_shift_keypoints.npy +0 -0
  36. {features → data/features}/keypoints/eat_012_keypoints.npy +0 -0
  37. {features → data/features}/keypoints/eat_013_keypoints.npy +0 -0
  38. {features → data/features}/keypoints/eat_014_aug_shift_keypoints.npy +0 -0
  39. {features → data/features}/keypoints/eat_014_keypoints.npy +0 -0
  40. {features → data/features}/keypoints/eat_015_aug_flip_keypoints.npy +0 -0
  41. {features → data/features}/keypoints/eat_015_aug_shift_keypoints.npy +0 -0
  42. {features → data/features}/keypoints/eat_015_keypoints.npy +0 -0
  43. {features → data/features}/keypoints/eat_016_aug_flip_keypoints.npy +0 -0
  44. {features → data/features}/keypoints/eat_016_aug_shift_keypoints.npy +0 -0
  45. {features → data/features}/keypoints/eat_016_keypoints.npy +0 -0
  46. {features → data/features}/keypoints/eat_017_aug_rotate_keypoints.npy +0 -0
  47. {features → data/features}/keypoints/eat_017_aug_shift_keypoints.npy +0 -0
  48. {features → data/features}/keypoints/eat_017_keypoints.npy +0 -0
  49. {features → data/features}/keypoints/eat_018_keypoints.npy +0 -0
  50. {features → data/features}/keypoints/eat_019_aug_flip_keypoints.npy +0 -0
.DS_Store DELETED
Binary file (8.2 kB)
 
Dockerfile CHANGED
@@ -1,32 +1,34 @@
1
- # 使用官方 Python 映像檔作為基礎
2
- FROM python:3.12.3-slim
3
 
4
- # 設定工作目錄
5
  WORKDIR /app
6
 
7
- # 將 requirements.txt 複製到工作目錄中
 
 
 
 
 
 
 
 
 
 
8
  COPY requirements.txt .
9
 
10
- # 更新 pip 並安裝所需的套件
11
- # 我們需要 git 來安裝特定版本的套件 (如果有的話)
12
- # 以及 build-essential 來編譯某些相依套件
13
- RUN apt-get update && apt-get install -y git build-essential && \
14
- pip install --no-cache-dir --upgrade pip && \
15
- pip install --no-cache-dir -r requirements.txt
16
 
17
- # 將專案中的所有檔案複製到工作目錄中
18
  COPY . .
19
 
20
- # 設定環境變數 (這些應該在 Hugging Face Space 的 secrets 中設定)
21
- # ENV VERIFY_TOKEN="your_verify_token"
22
- # ENV PAGE_ACCESS_TOKEN="your_page_access_token"
23
- # ENV OPENAI_API_KEY="your_openai_api_key"
 
24
 
25
- # 開放應用程式運行的埠口
26
- EXPOSE 8000
27
 
28
- # 使用 Gunicorn 啟動應用程式
29
- # --bind 0.0.0.0:8000 讓它可以從外部被存取
30
- # --workers 1 對於免費方案,一個 worker 通常是比較穩定的選擇
31
- # --timeout 120 增加超時時間,以應對可能的模型載入或長時間的請求
32
- CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "1", "--timeout", "120", "app:app"]
 
1
+ FROM python:3.10-slim
 
2
 
 
3
  WORKDIR /app
4
 
5
+ # 安裝系統依賴
6
+ RUN apt-get update && apt-get install -y \
7
+ libglib2.0-0 \
8
+ libsm6 \
9
+ libxext6 \
10
+ libxrender-dev \
11
+ libgomp1 \
12
+ libglib2.0-0 \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # 複製依賴檔案
16
  COPY requirements.txt .
17
 
18
+ # 安裝 Python 依賴
19
+ RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
20
 
21
+ # 複製應用程式檔案
22
  COPY . .
23
 
24
+ # 建立必要目錄
25
+ RUN mkdir -p uploads data/models data/features/keypoints
26
+
27
+ # 暴露端口
28
+ EXPOSE 7860
29
 
30
+ # 設定環境變數
31
+ ENV PYTHONUNBUFFERED=1
32
 
33
+ # 啟動命令
34
+ CMD ["python", "app.py"]
 
 
 
Procfile DELETED
@@ -1 +0,0 @@
1
- web: gunicorn app:app
 
 
README.md CHANGED
@@ -1,24 +1,187 @@
1
  ---
2
- title: SignView
3
- emoji: 👋
4
  colorFrom: blue
5
- colorTo: indigo
6
  sdk: docker
7
- app_file: app.py
8
- secrets:
9
- - MESSENGER_ACCESS_TOKEN
10
- - MESSENGER_VERIFY_TOKEN
11
  ---
12
 
13
- # SignView - 手語辨識機器人
14
 
15
- 這是一個基於 Flask Messenger 機器人,用於即時辨識手語影片。
16
 
17
- ## Hugging Face Space 設定
18
 
19
- 為了讓這個應用程式正常運作,您需要在 Space 的設定頁面中新增以下 Secrets(環境變數):
20
 
21
- 1. `MESSENGER_ACCESS_TOKEN`: 你的 Facebook 粉絲專頁存取權杖。
22
- 2. `MESSENGER_VERIFY_TOKEN`: 你在設定 Webhook 時自訂的驗證權杖。
 
 
 
 
 
 
23
 
24
- 設定完成後,Hugging Face 會自動根據 `Dockerfile` 建置並啟動應用程式。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SignView - 手語辨識整合系統
3
+ emoji: 🤟
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
+ pinned: boolean
9
+ duplicated_from: XiaoBai1221/SignView
 
10
  ---
11
 
12
+ # 手語辨識整合系統 (Sign Language Recognition System)
13
 
14
+ 一個整合的手語辨識系統,支援即時攝像頭辨識、影片上傳處理和 Facebook Messenger Bot 功能。使用 PyTorch 深度學習模型、MediaPipe 特徵提取和 OpenAI GPT 生成自然語句。
15
 
16
+ ## 🚀 快速開始
17
 
18
+ ### HuggingFace Spaces 部署 (推薦)
19
 
20
+ 1. **Fork 此專案到你的 HuggingFace Spaces**
21
+ 2. **設定環境變數**:
22
+ ```
23
+ OPENAI_API_KEY=你的OpenAI_API金鑰
24
+ VERIFY_TOKEN=你的Messenger驗證Token
25
+ PAGE_ACCESS_TOKEN=你的Facebook頁面存取Token
26
+ ```
27
+ 3. **自動部署** - HuggingFace 會自動建置和部署
28
 
29
+ ### 本地開發
30
+
31
+ ```bash
32
+ # 1. 安裝依賴
33
+ pip install -r requirements.txt
34
+
35
+ # 2. 設定環境變數
36
+ export OPENAI_API_KEY="你的OpenAI_API金鑰"
37
+ export VERIFY_TOKEN="你的Messenger驗證Token"
38
+ export PAGE_ACCESS_TOKEN="你的Facebook頁面存取Token"
39
+
40
+ # 3. 啟動應用
41
+ python3 app.py
42
+ ```
43
+
44
+ ## 📁 專案結構
45
+
46
+ ```
47
+ Sign-bot/
48
+ ├── app.py # 🎯 主應用程式 (整合所有功能)
49
+ ├── app_config.py # ⚙️ 配置管理
50
+ ├── requirements.txt # 📦 Python依賴套件
51
+ ├── Dockerfile # 🐳 Docker容器配置
52
+ ├── README.md # 📖 專案文檔
53
+ ├── final_review_gate.py # 🔍 測試腳本
54
+ ├── data/ # 📊 資料目錄
55
+ │ ├── models/ # 🤖 訓練好的模型檔案
56
+ │ │ └── sign_language_model.pth
57
+ │ ├── labels.csv # 🏷️ 標籤映射檔案
58
+ │ └── features/ # 🎬 訓練特徵資料
59
+ │ ├── keypoints/ # ✋ 關鍵點特徵檔案
60
+ │ └── optical_flow/ # 🌊 光流特徵檔案
61
+ ├── templates/ # 🌐 網頁範本
62
+ │ └── index.html # 首頁範本
63
+ └── uploads/ # 📁 暫時檔案上傳目錄
64
+ ```
65
+
66
+ ## ✨ 功能特色
67
+
68
+ ### 🎯 **整合設計**
69
+ - **統一入口**: 所有功能整合在 `app.py` 單一檔案
70
+ - **環境適配**: 自動檢測本地/雲端環境並調整功能
71
+ - **模組化**: 清晰的類別結構,易於維護
72
+
73
+ ### 🤖 **AI 手語辨識**
74
+ - **深度學習模型**: PyTorch LSTM + Attention 機制
75
+ - **特徵提取**: MediaPipe 提取手部、姿態關鍵點
76
+ - **自然語句生成**: OpenAI GPT-4o-mini 生成流暢句子
77
+ - **支援手語**: 目前支援 eat, fish, like, want 四個手語
78
+
79
+ ### 🌐 **多平台支援**
80
+ - **Web 介面**: 即時攝像頭辨識 + 影片上傳處理
81
+ - **Messenger Bot**: Facebook 整合,自動處理使用者影片
82
+ - **RESTful API**: 提供第三方整合接口
83
+ - **WebSocket**: 即時雙向通訊
84
+
85
+ ### 📱 **使用方式**
86
+
87
+ #### Web 介面 (本地環境)
88
+ 1. 造訪 `http://localhost:7860`
89
+ 2. 點擊「開始辨識」使用攝像頭
90
+ 3. 或上傳 MP4 影片檔案
91
+
92
+ #### Messenger Bot
93
+ 1. 找到你的 Facebook 頁面
94
+ 2. 發送手語影片
95
+ 3. 系統自動辨識並回傳結果
96
+
97
+ #### API 呼叫
98
+ ```bash
99
+ # 上傳影片進行辨識
100
+ curl -X POST http://localhost:7860/process_video \
101
+ -F "video=@your_video.mp4" \
102
+ -F "sender_id=test_user"
103
+ ```
104
+
105
+ ## 🔧 技術架構
106
+
107
+ ### 核心類別
108
+ - **FeatureExtractor**: MediaPipe 特徵提取器
109
+ - **SignLanguageModel**: PyTorch LSTM 神經網絡
110
+ - **VideoSignLanguageRecognizer**: 影片手語辨識器
111
+ - **SignLanguageRecognizer**: 即時手語辨識器
112
+
113
+ ### 技術棧
114
+ - **後端**: Flask + SocketIO
115
+ - **AI框架**: PyTorch + MediaPipe
116
+ - **自然語言**: OpenAI GPT-4o-mini
117
+ - **前端**: HTML5 + WebSocket
118
+ - **部署**: HuggingFace Spaces + Docker
119
+
120
+ ## 🌍 環境變數
121
+
122
+ | 變數名稱 | 說明 | 必須 |
123
+ |---------|------|------|
124
+ | `OPENAI_API_KEY` | OpenAI API 金鑰 | ✅ |
125
+ | `VERIFY_TOKEN` | Messenger 驗證 Token | Messenger功能需要 |
126
+ | `PAGE_ACCESS_TOKEN` | Facebook 頁面存取 Token | Messenger功能需要 |
127
+ | `SPACE_ID` | HuggingFace Space ID | 自動設定 |
128
+ | `PORT` | 服務埠號 | 預設 7860 |
129
+
130
+ ## 🎮 API 端點
131
+
132
+ ### Web 路由
133
+ - `GET /` - 主頁面
134
+ - `GET /health` - 健康檢查
135
+ - `POST /process_video` - 影片處理
136
+
137
+ ### Messenger 整合
138
+ - `GET /webhook` - Webhook ��證
139
+ - `POST /webhook` - 訊息處理
140
+
141
+ ### WebSocket 事件
142
+ - `start_stream` - 開始視頻流
143
+ - `stop_stream` - 停止視頻流
144
+
145
+ ## 🚀 部署指南
146
+
147
+ ### HuggingFace Spaces
148
+ 1. 建立新的 Space (Gradio/Docker)
149
+ 2. 上傳所有檔案
150
+ 3. 設定環境變數
151
+ 4. 自動部署完成
152
+
153
+ ### Docker 部署
154
+ ```bash
155
+ # 建置映像
156
+ docker build -t sign-language-recognition .
157
+
158
+ # 執行容器
159
+ docker run -p 7860:7860 \
160
+ -e OPENAI_API_KEY="你的金鑰" \
161
+ sign-language-recognition
162
+ ```
163
+
164
+ ## 🎯 使用限制
165
+
166
+ - **模型準確度**: 目前為測試版本,準確度可能有限
167
+ - **支援手語**: 僅支援 4 個基礎手語詞彙
168
+ - **攝像頭功能**: 雲端環境不支援,請使用影片上傳
169
+ - **檔案大小**: 影片檔案限制 100MB
170
+
171
+ ## 🔄 未來規劃
172
+
173
+ - [ ] 增加更多手語詞彙支援
174
+ - [ ] 提升模型準確度
175
+ - [ ] 支援手語語法結構
176
+ - [ ] 加入使用者自訓練功能
177
+ - [ ] 支援多語言介面
178
+
179
+ ## 📞 技術支援
180
+
181
+ 如有問題請透過以下方式聯絡:
182
+ - GitHub Issues
183
+ - 或直接在 HuggingFace Space 留言
184
+
185
+ ---
186
+
187
+ > **🎉 這是一個整合型手語辨識系統,將所有功能統一整合在 `app.py` 中,提供最佳的使用體驗和部署便利性!**
app.py CHANGED
@@ -1,37 +1,750 @@
 
 
 
1
  import os
2
  import json
3
  import requests
4
- from flask import Flask, request, jsonify
5
- from datetime import datetime
 
 
 
 
 
6
  import threading
7
  import time
 
 
 
 
 
 
 
8
 
9
- # 匯入我們的手語處理器
10
- from sign_language_processor import VideoSignLanguageRecognizer
11
 
 
 
 
 
 
12
  app = Flask(__name__)
 
 
 
13
 
14
- # 從環境變數取得設定
15
  VERIFY_TOKEN = os.environ.get('VERIFY_TOKEN', 'your_verify_token')
16
  PAGE_ACCESS_TOKEN = os.environ.get('PAGE_ACCESS_TOKEN', 'your_page_access_token')
17
  FACEBOOK_API_URL = 'https://graph.facebook.com/v18.0/me/messages'
18
 
19
- # --- Initializations ---
20
- # 初始化手語辨識器
21
- # 注意:你需要確保 'models/sign_language_model.pth' 'labels.csv' 存在
22
- try:
23
- recognizer = VideoSignLanguageRecognizer(
24
- model_path='models/sign_language_model.pth'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  )
26
- print("✅ 手語辨識模型載入成功")
27
- except Exception as e:
28
- recognizer = None
29
- print(f"❌ 載入模型失敗: {e}")
30
- print("👉 請確保 'models/sign_language_model.pth' 和 'labels.csv' 檔案存在於專案根目錄")
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  @app.route('/', methods=['GET'])
33
  def home():
34
- return "統一手語辨識服務正在運行中!🚀"
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  @app.route('/webhook', methods=['GET'])
37
  def verify_webhook():
@@ -72,7 +785,7 @@ def handle_webhook():
72
 
73
  @app.route('/receive_recognition_result', methods=['POST'])
74
  def receive_recognition_result():
75
- """接收來自本地手語辨識服務的結果"""
76
  try:
77
  data = request.get_json()
78
 
@@ -82,7 +795,6 @@ def receive_recognition_result():
82
  sender_id = data.get('sender_id')
83
  recognition_result = data.get('recognition_result', '無法辨識')
84
  confidence = data.get('confidence', 0)
85
- timestamp = data.get('timestamp', '')
86
 
87
  if not sender_id:
88
  return jsonify({"status": "error", "message": "缺少 sender_id"}), 400
@@ -91,7 +803,7 @@ def receive_recognition_result():
91
  print(f"🎯 辨識結果:{recognition_result}")
92
  print(f"📊 信心度:{confidence}")
93
 
94
- # 只發送純粹的辨識結果句子
95
  send_message(sender_id, recognition_result)
96
 
97
  return jsonify({
@@ -103,6 +815,70 @@ def receive_recognition_result():
103
  print(f"處理辨識結果時發生錯誤:{e}")
104
  return jsonify({"status": "error", "message": str(e)}), 500
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  def handle_message(messaging_event):
107
  """處理一般訊息"""
108
  sender_id = messaging_event['sender']['id']
@@ -112,29 +888,23 @@ def handle_message(messaging_event):
112
 
113
  print(f"收到訊息 from {sender_id}: {message_text}")
114
 
115
- # 檢查是否有影片附件
116
  if attachments:
117
  for attachment in attachments:
118
  if attachment.get('type') == 'video':
119
  video_url = attachment.get('payload', {}).get('url')
120
  if video_url:
121
- # 提示用戶我們已收到影片並開始處理
122
- send_message(sender_id, "收到您的影片了,正在為您處理手語辨識,請稍候...")
123
-
124
- # 在背景處理影片,避免阻塞
125
- thread = threading.Thread(
126
- target=process_video_and_reply,
127
- args=(video_url, sender_id)
128
- )
129
- thread.start()
130
  return
131
  else:
132
- send_message(sender_id, f"抱歉,目前只支援影片格式的手語辨識喔!")
133
  return
134
 
135
  # 處理文字訊息
136
  if message_text:
137
- send_message(sender_id, f"你好!請傳送一段手語影片,我會試著為您翻譯。")
 
138
 
139
  def handle_postback(messaging_event):
140
  """處理 postback 事件(按鈕點擊等)"""
@@ -160,120 +930,134 @@ def send_message(recipient_id, message_text):
160
  'access_token': PAGE_ACCESS_TOKEN
161
  }
162
 
163
- try:
164
- response = requests.post(
165
- FACEBOOK_API_URL,
166
- headers=headers,
167
- params=params,
168
- json=data,
169
- timeout=30
170
- )
171
- response.raise_for_status()
172
- print(f"訊息發送成功給 {recipient_id}")
173
- except requests.exceptions.RequestException as e:
174
- print(f"發送訊息失敗 to {recipient_id}: {e}")
175
-
176
- def send_quick_reply(recipient_id, message_text, quick_replies):
177
- """發送快速回覆選項"""
178
- headers = {
179
- 'Content-Type': 'application/json'
180
- }
181
-
182
- data = {
183
- 'recipient': {'id': recipient_id},
184
- 'message': {
185
- 'text': message_text,
186
- 'quick_replies': quick_replies
187
- }
188
- }
189
-
190
- params = {
191
- 'access_token': PAGE_ACCESS_TOKEN
192
- }
193
-
194
- requests.post(
195
  FACEBOOK_API_URL,
196
  headers=headers,
197
  params=params,
198
  json=data
199
  )
 
 
 
 
 
200
 
201
- def download_video_local(video_url, sender_id):
202
- """下載影片到本地"""
203
  try:
204
- # 生成檔案名稱
205
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
206
- filename = f"video_{sender_id}_{timestamp}.mp4"
207
- file_path = os.path.join('received_videos', filename)
208
-
209
- print(f"開始下載影片到本地:{video_url}")
210
 
211
  # 下載影片
212
  response = requests.get(video_url, stream=True, timeout=30)
213
  response.raise_for_status()
214
 
 
 
 
 
 
215
  # 寫入檔案
216
  with open(file_path, 'wb') as f:
217
  for chunk in response.iter_content(chunk_size=8192):
218
  if chunk:
219
  f.write(chunk)
220
 
221
- file_size = os.path.getsize(file_path)
222
- print(f"🎬 影片下載完成:{file_path} ({file_size} bytes)")
223
- return file_path
224
-
225
- except Exception as e:
226
- print(f"下載影片失敗:{e}")
227
- return None
228
-
229
- def process_video_and_reply(video_url, sender_id):
230
- """下載、處理影片,並回傳結果"""
231
- if not recognizer:
232
- print("❌ 辨識器未初始化,無法處理影片。")
233
- send_message(sender_id, "抱歉,後端辨識服務目前無法使用,請稍後再試。")
234
- return
235
-
236
- # 1. 下載影片
237
- print(f"🎬 開始下載影片 for user {sender_id} from {video_url}")
238
- video_path = download_video_local(video_url, sender_id)
239
-
240
- if not video_path:
241
- print(f"❌ 影片下載失敗 for user {sender_id}")
242
- send_message(sender_id, "抱歉,無法順利下載您的影片,請再試一次。")
243
- return
244
-
245
- # 2. 處理影片並進行手語辨識
246
- try:
247
- print(f"🤖 開始進行手語辨識 for {video_path}")
248
- result = recognizer.process_video(video_path)
249
 
250
- recognition_result = result.get('recognition_result', '無法辨識')
251
- confidence = result.get('confidence', 0)
 
252
 
253
- print(f"✅ 辨識完成 for user {sender_id} - 結果: {recognition_result} (信心度: {confidence:.2f})")
 
254
 
255
- # 3. 將結果發送給用戶
256
- reply_text = f"辨識結果:\n{recognition_result}"
257
- send_message(sender_id, reply_text)
 
 
258
 
 
 
 
 
 
 
 
 
 
 
259
  except Exception as e:
260
- print(f" 辨識過程中發生錯誤 for {video_path}: {e}")
261
- send_message(sender_id, "抱歉,在辨識過程中發生了未預期的錯誤。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
- finally:
264
- # 4. 刪除本地影片檔案
265
- if os.path.exists(video_path):
266
- try:
267
- os.remove(video_path)
268
- print(f"🗑️ 已刪除暫存影片:{video_path}")
269
- except Exception as e:
270
- print(f"❌ 刪除影片失敗 {video_path}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  if __name__ == '__main__':
273
- # 建立影片接收資料夾
274
- if not os.path.exists('received_videos'):
275
- os.makedirs('received_videos')
276
-
277
- port = int(os.environ.get('PORT', 8000))
278
- print(f"🚀 伺服器將在 http://localhost:{port} 上啟動")
279
- app.run(host='0.0.0.0', port=port, debug=True)
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
  import os
5
  import json
6
  import requests
7
+ import cv2
8
+ import numpy as np
9
+ import pandas as pd
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+ import base64
14
  import threading
15
  import time
16
+ import mediapipe as mp
17
+ import collections
18
+ from flask import Flask, request, jsonify, render_template, Response
19
+ from werkzeug.utils import secure_filename
20
+ from datetime import datetime
21
+ from flask_socketio import SocketIO, emit
22
+ from openai import OpenAI
23
 
24
+ # 環境變數設定
25
+ os.environ.setdefault("OPENAI_API_KEY", "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")
26
 
27
+ # 環境檢測
28
+ IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
29
+ IS_LOCAL_DEV = not IS_HUGGINGFACE
30
+
31
+ # Flask 應用初始化
32
  app = Flask(__name__)
33
+ app.config['SECRET_KEY'] = 'sign_language_secret_key'
34
+ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB max file size
35
+ socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
36
 
37
+ # Messenger Bot 設定
38
  VERIFY_TOKEN = os.environ.get('VERIFY_TOKEN', 'your_verify_token')
39
  PAGE_ACCESS_TOKEN = os.environ.get('PAGE_ACCESS_TOKEN', 'your_page_access_token')
40
  FACEBOOK_API_URL = 'https://graph.facebook.com/v18.0/me/messages'
41
 
42
+ # 路徑設定 - 適應不同環境
43
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
44
+ DATA_DIR = os.path.join(BASE_DIR, 'data')
45
+ MODEL_PATH = os.path.join(DATA_DIR, 'models', 'sign_language_model.pth')
46
+ LABELS_PATH = os.path.join(DATA_DIR, 'labels.csv')
47
+ UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
48
+
49
+ # 建立必要資料夾
50
+ for folder in [UPLOAD_FOLDER, os.path.join(DATA_DIR, 'models'), os.path.join(DATA_DIR, 'features', 'keypoints')]:
51
+ os.makedirs(folder, exist_ok=True)
52
+
53
+ # 全域變數
54
+ camera = None
55
+ recognizer = None
56
+ is_running = False
57
+ frame_lock = threading.Lock()
58
+ current_frame = None
59
+
60
+ print(f"🌍 運行環境: {'HuggingFace Spaces' if IS_HUGGINGFACE else '本地開發'}")
61
+ print(f"📁 基礎目錄: {BASE_DIR}")
62
+ print(f"🤖 模型路徑: {MODEL_PATH}")
63
+ print(f"📊 標籤路徑: {LABELS_PATH}")
64
+
65
+ #--------------------
66
+ # AI 模型類別
67
+ #--------------------
68
+ class FeatureExtractor:
69
+ def __init__(self):
70
+ # 初始化MediaPipe模型
71
+ self.mp_holistic = mp.solutions.holistic
72
+ self.mp_drawing = mp.solutions.drawing_utils
73
+ self.mp_drawing_styles = mp.solutions.drawing_styles
74
+
75
+ def extract_pose_keypoints(self, frame, holistic_results):
76
+ """提取骨架關鍵點"""
77
+ keypoints = []
78
+
79
+ # 提取手部關鍵點 (如果檢測到)
80
+ if holistic_results.left_hand_landmarks:
81
+ for landmark in holistic_results.left_hand_landmarks.landmark:
82
+ keypoints.extend([landmark.x, landmark.y, landmark.z])
83
+ else:
84
+ # 如果沒有檢測到手,填充0
85
+ keypoints.extend([0] * (21 * 3))
86
+
87
+ if holistic_results.right_hand_landmarks:
88
+ for landmark in holistic_results.right_hand_landmarks.landmark:
89
+ keypoints.extend([landmark.x, landmark.y, landmark.z])
90
+ else:
91
+ keypoints.extend([0] * (21 * 3))
92
+
93
+ # 提取姿勢關鍵點
94
+ if holistic_results.pose_landmarks:
95
+ for landmark in holistic_results.pose_landmarks.landmark:
96
+ keypoints.extend([landmark.x, landmark.y, landmark.z])
97
+ else:
98
+ keypoints.extend([0] * (33 * 3))
99
+
100
+ return np.array(keypoints)
101
+
102
+ class SignLanguageModel(nn.Module):
103
+ """
104
+ 手語辨識模型,使用雙向LSTM和注意力機制,加入批量標準化和殘差連接
105
+ """
106
+ def __init__(self, input_dim, hidden_dim, num_layers, num_classes, dropout=0.5):
107
+ super(SignLanguageModel, self).__init__()
108
+ self.hidden_dim = hidden_dim
109
+ self.num_layers = num_layers
110
+ self.num_classes = num_classes
111
+
112
+ # 特徵投影層,將輸入映射到統一維度
113
+ self.feature_projection = nn.Sequential(
114
+ nn.Linear(input_dim, hidden_dim),
115
+ nn.BatchNorm1d(hidden_dim),
116
+ nn.ReLU(),
117
+ nn.Dropout(dropout/2) # 較輕的dropout
118
+ )
119
+
120
+ # 雙向LSTM層
121
+ self.lstm = nn.LSTM(
122
+ input_size=hidden_dim,
123
+ hidden_size=hidden_dim,
124
+ num_layers=num_layers,
125
+ batch_first=True,
126
+ dropout=dropout if num_layers > 1 else 0,
127
+ bidirectional=True
128
+ )
129
+
130
+ # 批量標準化層(用於規範化LSTM輸出)
131
+ self.lstm_bn = nn.BatchNorm1d(hidden_dim * 2)
132
+
133
+ # 注意力機制
134
+ self.attention = nn.Sequential(
135
+ nn.Linear(hidden_dim * 2, hidden_dim),
136
+ nn.Tanh(),
137
+ nn.Linear(hidden_dim, 1),
138
+ nn.Softmax(dim=1)
139
+ )
140
+
141
+ # 分類器
142
+ self.classifier = nn.Sequential(
143
+ nn.Linear(hidden_dim * 2, hidden_dim),
144
+ nn.BatchNorm1d(hidden_dim),
145
+ nn.ReLU(),
146
+ nn.Dropout(dropout),
147
+ nn.Linear(hidden_dim, hidden_dim // 2),
148
+ nn.ReLU(),
149
+ nn.Dropout(dropout/2),
150
+ nn.Linear(hidden_dim // 2, num_classes)
151
+ )
152
+
153
+ # L2正則化
154
+ self.l2_reg_alpha = 0.001
155
+
156
+ # 初始化權重
157
+ self._init_weights()
158
+
159
+ def _init_weights(self):
160
+ """初始化模型權重"""
161
+ for m in self.modules():
162
+ if isinstance(m, nn.Linear):
163
+ nn.init.xavier_uniform_(m.weight)
164
+ if m.bias is not None:
165
+ nn.init.zeros_(m.bias)
166
+ elif isinstance(m, nn.LSTM):
167
+ for name, param in m.named_parameters():
168
+ if 'weight' in name:
169
+ nn.init.orthogonal_(param) # 正交初始化對RNN很有效
170
+ elif 'bias' in name:
171
+ nn.init.zeros_(param)
172
+
173
+ def forward(self, x):
174
+ """前向傳播"""
175
+ # x的形狀: [batch_size, seq_len, feature_dim]
176
+ batch_size, seq_len, _ = x.size()
177
+
178
+ # 特徵投影 - 需要調整維度以適應BatchNorm1d
179
+ x_reshaped = x.reshape(-1, x.size(-1)) # [batch_size*seq_len, feature_dim]
180
+ x_projected = self.feature_projection[0](x_reshaped) # Linear層
181
+ x_projected = x_projected.reshape(batch_size, seq_len, -1) # 恢復形狀 [batch_size, seq_len, hidden_dim]
182
+ x_projected = x_projected.transpose(1, 2) # [batch_size, hidden_dim, seq_len] 用於BatchNorm
183
+ x_projected = self.feature_projection[1](x_projected) # BatchNorm層
184
+ x_projected = x_projected.transpose(1, 2) # 恢復形狀 [batch_size, seq_len, hidden_dim]
185
+ x_projected = self.feature_projection[2](x_projected) # ReLU
186
+ x_projected = self.feature_projection[3](x_projected) # Dropout
187
+
188
+ # 保存輸入特徵,用於殘差連接
189
+ x_residual = x_projected
190
+
191
+ # LSTM處理
192
+ lstm_out, _ = self.lstm(x_projected)
193
+ # lstm_out的形狀: [batch_size, seq_len, hidden_dim*2]
194
+
195
+ # 對LSTM輸出應用BatchNorm
196
+ lstm_out_bn = lstm_out.transpose(1, 2) # [batch_size, hidden_dim*2, seq_len]
197
+ lstm_out_bn = self.lstm_bn(lstm_out_bn)
198
+ lstm_out = lstm_out_bn.transpose(1, 2) # [batch_size, seq_len, hidden_dim*2]
199
+
200
+ # 注意力權重計算
201
+ attention_weights = self.attention(lstm_out)
202
+ # attention_weights的形狀: [batch_size, seq_len, 1]
203
+
204
+ # 應用注意力機制
205
+ context = torch.bmm(lstm_out.transpose(1, 2), attention_weights)
206
+ # context的形狀: [batch_size, hidden_dim*2, 1]
207
+ context = context.squeeze(-1)
208
+
209
+ # 最終分類
210
+ output = self.classifier(context)
211
+ # output的形狀: [batch_size, num_classes]
212
+
213
+ return output
214
+
215
+ #--------------------
216
+ # 手語辨識器類別
217
+ #--------------------
218
+ class VideoSignLanguageRecognizer:
219
+ """影片手語辨識器 - 專門處理影片檔案"""
220
+ def __init__(self, model_path, threshold=0.7):
221
+ self.model_path = model_path
222
+ self.threshold = threshold
223
+
224
+ # 初始化特徵提取器
225
+ self.feature_extractor = FeatureExtractor()
226
+
227
+ # 加載標籤映射
228
+ self.label_map = self._load_label_mapping()
229
+
230
+ # 加載模型
231
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
232
+ self.model = self._load_model()
233
+
234
+ # GPT整合
235
+ try:
236
+ self.openai_client = OpenAI()
237
+ except Exception as e:
238
+ print(f"初始化OpenAI客户端出錯: {e}")
239
+ self.openai_client = None
240
+
241
+ print(f"影片辨識器初始化完成!使用設備: {self.device}")
242
+
243
+ def _load_label_mapping(self):
244
+ """加載標籤映射"""
245
+ label_map = {}
246
+
247
+ # 嘗試從 labels.csv 讀取
248
+ labels_file = LABELS_PATH
249
+ print(f"🔍 嘗試載入標籤檔案: {labels_file}")
250
+ print(f"📂 當前工作目錄: {os.getcwd()}")
251
+
252
+ if os.path.exists(labels_file):
253
+ try:
254
+ df = pd.read_csv(labels_file)
255
+ print(f"📄 標籤檔案內容:")
256
+ print(df)
257
+
258
+ for _, row in df.iterrows():
259
+ label_map[int(row['index'])] = row['label']
260
+ print(f"✅ 從 {labels_file} 載入了 {len(label_map)} 個類別標籤")
261
+ print(f"📊 標籤映射: {label_map}")
262
+ except Exception as e:
263
+ print(f"❌ 讀取 labels.csv 出錯: {e}")
264
+ # 使用默認映射
265
+ label_map = {0: "eat", 1: "fish", 2: "like", 3: "want"}
266
+ else:
267
+ print(f"❌ 標籤檔案不存在: {labels_file}")
268
+
269
+ if not label_map:
270
+ # 使用默認映射
271
+ label_map = {0: "eat", 1: "fish", 2: "like", 3: "want"}
272
+ print(f"⚠️ 使用預設標籤映射: {label_map}")
273
+
274
+ return label_map
275
+
276
+ def _load_model(self):
277
+ """加載訓練好的模型"""
278
+ input_dim = 225 # (21+21+33) * 3 = 225
279
+
280
+ model = SignLanguageModel(
281
+ input_dim=input_dim,
282
+ hidden_dim=96,
283
+ num_layers=2,
284
+ num_classes=len(self.label_map),
285
+ dropout=0.5
286
+ )
287
+
288
+ # 檢查模型檔案是否存在
289
+ if not os.path.exists(self.model_path):
290
+ print(f"⚠️ 警告:模型檔案不存在 {self.model_path}")
291
+ print("🔧 將使用隨機初始化的模型(僅供測試)")
292
+ # 隨機初始化權重用於測試
293
+ model.to(self.device)
294
+ model.eval()
295
+ return model
296
+
297
+ try:
298
+ # 載入權重
299
+ model.load_state_dict(torch.load(self.model_path, map_location=self.device))
300
+ model.to(self.device)
301
+ model.eval()
302
+ print(f"✅ 模型載入成功:{self.model_path}")
303
+ except Exception as e:
304
+ print(f"❌ 模型載入失敗:{e}")
305
+ print("🔧 使用隨機初始化的模型")
306
+ model.to(self.device)
307
+ model.eval()
308
+
309
+ return model
310
+
311
+ def process_video(self, video_path):
312
+ """處理整個影片檔案"""
313
+ print(f"🎬 開始處理影片:{video_path}")
314
+
315
+ # 開啟影片
316
+ cap = cv2.VideoCapture(video_path)
317
+ if not cap.isOpened():
318
+ print(f"❌ 無法開啟影片檔:{video_path}")
319
+ return None, 0
320
+
321
+ # 提取特徵序列
322
+ keypoints_sequence = []
323
+ frame_count = 0
324
+
325
+ while True:
326
+ ret, frame = cap.read()
327
+ if not ret:
328
+ break
329
+
330
+ # 跳幀處理
331
+ if frame_count % 5 == 0: # 每5幀處理一次
332
+ keypoints, _ = self._extract_features(frame)
333
+ if keypoints is not None:
334
+ keypoints_sequence.append(keypoints)
335
+
336
+ frame_count += 1
337
+
338
+ # 限制處理幀數
339
+ if len(keypoints_sequence) >= 60:
340
+ break
341
+
342
+ cap.release()
343
+
344
+ if len(keypoints_sequence) < 3:
345
+ print(f"❌ 有效幀數不足,無法進行辨識")
346
+ return None, 0
347
+
348
+ # 進行預測
349
+ prediction, confidence, word_sequence = self._predict_from_sequence(keypoints_sequence)
350
+
351
+ # 使用GPT生成完整句子
352
+ generated_sentence = self._generate_sentence_with_gpt(word_sequence)
353
+
354
+ print(f"🎯 辨識結果:{generated_sentence}")
355
+ print(f"📈 信心度:{confidence:.2f}")
356
+
357
+ return generated_sentence, confidence
358
+
359
+ def _extract_features(self, frame):
360
+ """從單一幀提取手部和姿勢特徵"""
361
+ with self.feature_extractor.mp_holistic.Holistic(
362
+ static_image_mode=False,
363
+ model_complexity=1,
364
+ smooth_landmarks=True,
365
+ enable_segmentation=False,
366
+ min_detection_confidence=0.1,
367
+ min_tracking_confidence=0.1
368
+ ) as holistic:
369
+ # 轉為RGB
370
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
371
+
372
+ # 處理圖像
373
+ results = holistic.process(frame_rgb)
374
+
375
+ # 檢查是否有手部被檢測到
376
+ hands_detected = (results.left_hand_landmarks is not None or
377
+ results.right_hand_landmarks is not None)
378
+
379
+ try:
380
+ keypoints = self.feature_extractor.extract_pose_keypoints(frame, results)
381
+ return keypoints, hands_detected
382
+ except Exception as e:
383
+ return None, hands_detected
384
+
385
+ def _predict_from_sequence(self, keypoints_sequence):
386
+ """從關鍵點序列進行預測"""
387
+ # 簡化版預測 - 直接使用整個序列
388
+ sequence_tensor = torch.FloatTensor(keypoints_sequence).unsqueeze(0).to(self.device)
389
+
390
+ with torch.no_grad():
391
+ outputs = self.model(sequence_tensor)
392
+ probabilities = torch.nn.functional.softmax(outputs, dim=1)
393
+ max_prob, predicted_class = torch.max(probabilities, 1)
394
+
395
+ predicted_class = predicted_class.item()
396
+ confidence = max_prob.item()
397
+
398
+ if confidence >= self.threshold:
399
+ predicted_word = self.label_map.get(predicted_class, f"類別{predicted_class}")
400
+ word_sequence = [predicted_word]
401
+ else:
402
+ word_sequence = []
403
+
404
+ return predicted_class, confidence, word_sequence
405
+
406
+ def _generate_sentence_with_gpt(self, word_sequence):
407
+ """使用GPT根據單詞序列生成一個完整句子"""
408
+ if not word_sequence:
409
+ return "無法辨識手語內容"
410
+
411
+ if not self.openai_client:
412
+ return " ".join(word_sequence)
413
+
414
+ try:
415
+ prompt = f"我使用手語表達了以下單詞序列: {', '.join(word_sequence)}。請將這些單詞組織成一個有意義、通順的完整句子。"
416
+
417
+ response = self.openai_client.chat.completions.create(
418
+ model="gpt-4o-mini",
419
+ messages=[
420
+ {"role": "system", "content": "你是一個專業的手語翻譯助手。"},
421
+ {"role": "user", "content": prompt}
422
+ ],
423
+ max_tokens=100
424
+ )
425
+
426
+ return response.choices[0].message.content.strip()
427
+
428
+ except Exception as e:
429
+ print(f"調用GPT API時出錯: {e}")
430
+ return " ".join(word_sequence)
431
+
432
+ class SignLanguageRecognizer:
433
+ """即時手語辨識器 - 用於攝像頭流"""
434
+ def __init__(self, model_path, frame_buffer_size=30, prediction_interval=15, threshold=0.7):
435
+ self.model_path = model_path
436
+ self.threshold = threshold
437
+ self.max_buffer_size = frame_buffer_size
438
+ self.prediction_interval = prediction_interval
439
+
440
+ # 初始化特徵提取器
441
+ self.feature_extractor = FeatureExtractor()
442
+
443
+ # 加載標籤映射
444
+ self.label_map = self._load_label_mapping()
445
+
446
+ # 加載模型
447
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
448
+ self.model = self._load_model()
449
+
450
+ # 緩衝區和狀態
451
+ self.keypoints_buffer = collections.deque(maxlen=frame_buffer_size)
452
+ self.frame_count = 0
453
+ self.current_prediction = None
454
+ self.prediction_probabilities = None
455
+
456
+ # 手部存在檢測
457
+ self.hand_present = False
458
+ self.hand_absent_frames = 0
459
+ self.hand_absent_threshold = 30
460
+
461
+ # 單詞序列
462
+ self.word_sequence = []
463
+ self.last_added_word = None
464
+ self.word_cooldown = 0
465
+
466
+ # 生成的句子
467
+ self.generated_sentence = ""
468
+ self.display_sentence_time = 0
469
+
470
+ # GPT整合
471
+ try:
472
+ self.openai_client = OpenAI()
473
+ except Exception as e:
474
+ print(f"初始化OpenAI客户端出錯: {e}")
475
+ self.openai_client = None
476
+
477
+ print(f"即時辨識器初始化完成!使用設備: {self.device}")
478
+
479
+ def _load_label_mapping(self):
480
+ """加載標籤映射"""
481
+ label_map = {}
482
+ # 嘗試從特徵目錄推斷類別標籤
483
+ features_dir = os.path.join(DATA_DIR, 'features', 'keypoints')
484
+ if os.path.exists(features_dir):
485
+ unique_labels = set()
486
+ for file_name in os.listdir(features_dir):
487
+ if file_name.endswith('_keypoints.npy'):
488
+ parts = file_name.split('_')
489
+ if len(parts) >= 2:
490
+ label = parts[0]
491
+ if label not in unique_labels and not (label.startswith("aug") or "aug_" in label):
492
+ unique_labels.add(label)
493
+
494
+ if unique_labels:
495
+ label_map = {i: label for i, label in enumerate(sorted(unique_labels))}
496
+ print(f"從特徵目錄推斷了 {len(label_map)} 個類別標籤")
497
+ else:
498
+ label_map = {0: "eat", 1: "fish", 2: "like", 3: "want"}
499
+ else:
500
+ label_map = {0: "eat", 1: "fish", 2: "like", 3: "want"}
501
+
502
+ return label_map
503
+
504
+ def _load_model(self):
505
+ """加載訓練好的模型"""
506
+ input_dim = 225
507
+
508
+ model = SignLanguageModel(
509
+ input_dim=input_dim,
510
+ hidden_dim=96,
511
+ num_layers=2,
512
+ num_classes=len(self.label_map),
513
+ dropout=0.5
514
+ )
515
+
516
+ # 檢查模型檔案是否存在
517
+ if not os.path.exists(self.model_path):
518
+ print(f"⚠️ 警告:模型檔案不存在 {self.model_path}")
519
+ print("🔧 將使用隨機初始化的模型(僅供測試)")
520
+ model.to(self.device)
521
+ model.eval()
522
+ return model
523
+
524
+ try:
525
+ model.load_state_dict(torch.load(self.model_path, map_location=self.device))
526
+ model.to(self.device)
527
+ model.eval()
528
+ print(f"✅ 即時辨識模型載入成功:{self.model_path}")
529
+ except Exception as e:
530
+ print(f"❌ 即時辨識模型載入失敗:{e}")
531
+ print("🔧 使用隨機初始化的模型")
532
+ model.to(self.device)
533
+ model.eval()
534
+
535
+ return model
536
+
537
+ def process_frame(self, frame):
538
+ """處理單個視頻幀"""
539
+ # 提取特徵和檢測手部
540
+ keypoint_features, hands_detected = self._extract_features(frame)
541
+
542
+ # 更新手部存在狀態
543
+ self._update_hand_presence(hands_detected)
544
+
545
+ # 僅當成功提取特徵時才繼續
546
+ if keypoint_features is not None:
547
+ self.keypoints_buffer.append(keypoint_features)
548
+
549
+ # 定期進行預測
550
+ if self.hand_present and self.frame_count % self.prediction_interval == 0 and len(self.keypoints_buffer) > 5:
551
+ self._make_prediction()
552
+ self._update_word_sequence()
553
+
554
+ # 手部離開時生成句子
555
+ if self.hand_present == False and self.hand_absent_frames == self.hand_absent_threshold and self.word_sequence:
556
+ self._generate_sentence_with_gpt()
557
+
558
+ self.frame_count += 1
559
+
560
+ if self.word_cooldown > 0:
561
+ self.word_cooldown -= 1
562
+
563
+ # 回傳狀態
564
+ status = {
565
+ "hand_present": self.hand_present,
566
+ "frame_count": self.frame_count,
567
+ "current_prediction": None,
568
+ "word_sequence": self.word_sequence.copy(),
569
+ "generated_sentence": self.generated_sentence,
570
+ "display_sentence": (time.time() - self.display_sentence_time < 10)
571
+ }
572
+
573
+ if self.current_prediction is not None:
574
+ if self.current_prediction == -1:
575
+ status["current_prediction"] = {"label": "未知", "confidence": 0}
576
+ else:
577
+ label = self.label_map.get(self.current_prediction, f"類別{self.current_prediction}")
578
+ confidence = float(self.prediction_probabilities[self.current_prediction]) if self.prediction_probabilities is not None else 0
579
+ status["current_prediction"] = {"label": label, "confidence": confidence}
580
+
581
+ if self.prediction_probabilities is not None:
582
+ status["probabilities"] = []
583
+ sorted_indices = np.argsort(self.prediction_probabilities)[::-1][:4]
584
+ for idx in sorted_indices:
585
+ prob = float(self.prediction_probabilities[idx])
586
+ class_label = self.label_map.get(idx, f"類別{idx}")
587
+ status["probabilities"].append({"label": class_label, "probability": prob})
588
+
589
+ return status
590
+
591
+ def _update_hand_presence(self, hands_detected):
592
+ """更新手部存在狀態"""
593
+ if hands_detected:
594
+ self.hand_present = True
595
+ self.hand_absent_frames = 0
596
+ else:
597
+ self.hand_absent_frames += 1
598
+ if self.hand_absent_frames >= self.hand_absent_threshold:
599
+ if self.hand_present:
600
+ self.hand_present = False
601
+
602
+ def _update_word_sequence(self):
603
+ """根據當前預測更新單詞序列"""
604
+ if self.current_prediction is not None and self.current_prediction >= 0:
605
+ word = self.label_map.get(self.current_prediction, f"類別{self.current_prediction}")
606
+
607
+ if word != self.last_added_word or self.word_cooldown == 0:
608
+ self.word_sequence.append(word)
609
+ self.last_added_word = word
610
+ self.word_cooldown = 20
611
+
612
+ def _generate_sentence_with_gpt(self):
613
+ """使用GPT根據單詞序列生成一個完整句子"""
614
+ if not self.word_sequence:
615
+ return
616
+
617
+ if not self.openai_client:
618
+ self.generated_sentence = " ".join(self.word_sequence)
619
+ self.display_sentence_time = time.time()
620
+ print(f"生成句子: {self.generated_sentence}")
621
+ self.word_sequence = []
622
+ return
623
+
624
+ try:
625
+ prompt = f"我使用手語表達了以下單詞序列: {', '.join(self.word_sequence)}。請將這些單詞組織成一個有意義、通順的完整句子。"
626
+
627
+ response = self.openai_client.chat.completions.create(
628
+ model="gpt-4o-mini",
629
+ messages=[
630
+ {"role": "system", "content": "你是一個專業的手語翻譯助手。"},
631
+ {"role": "user", "content": prompt}
632
+ ],
633
+ max_tokens=100
634
+ )
635
+
636
+ self.generated_sentence = response.choices[0].message.content.strip()
637
+ self.display_sentence_time = time.time()
638
+ print(f"GPT生成句子: {self.generated_sentence}")
639
+
640
+ except Exception as e:
641
+ print(f"調用GPT API時出錯: {e}")
642
+ self.generated_sentence = " ".join(self.word_sequence)
643
+ self.display_sentence_time = time.time()
644
+
645
+ self.word_sequence = []
646
+
647
+ def _extract_features(self, frame):
648
+ """從單一幀提取手部和姿勢特徵"""
649
+ with self.feature_extractor.mp_holistic.Holistic(
650
+ static_image_mode=False,
651
+ model_complexity=1,
652
+ smooth_landmarks=True,
653
+ enable_segmentation=False,
654
+ min_detection_confidence=0.1,
655
+ min_tracking_confidence=0.1
656
+ ) as holistic:
657
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
658
+ results = holistic.process(frame_rgb)
659
+
660
+ hands_detected = (results.left_hand_landmarks is not None or
661
+ results.right_hand_landmarks is not None)
662
+
663
+ try:
664
+ keypoints = self.feature_extractor.extract_pose_keypoints(frame, results)
665
+ return keypoints, hands_detected
666
+ except Exception as e:
667
+ return None, hands_detected
668
+
669
+ def _make_prediction(self):
670
+ """使用緩衝區中的特徵進行預測"""
671
+ if len(self.keypoints_buffer) < 2:
672
+ return
673
+
674
+ keypoints_array = np.array(list(self.keypoints_buffer))
675
+ keypoints_tensor = torch.FloatTensor(keypoints_array).unsqueeze(0).to(self.device)
676
+
677
+ with torch.no_grad():
678
+ outputs = self.model(keypoints_tensor)
679
+ probabilities = torch.nn.functional.softmax(outputs, dim=1)
680
+
681
+ max_prob, predicted_class = torch.max(probabilities, 1)
682
+ predicted_class = predicted_class.item()
683
+ max_prob = max_prob.item()
684
+
685
+ probs = probabilities[0].cpu().numpy()
686
+
687
+ if max_prob >= self.threshold:
688
+ self.current_prediction = predicted_class
689
+ self.prediction_probabilities = probs
690
+ else:
691
+ self.current_prediction = -1
692
+ self.prediction_probabilities = probs
693
+
694
+ def initialize_recognizer():
695
+ global recognizer
696
+
697
+ model_path = MODEL_PATH
698
+
699
+ recognizer = SignLanguageRecognizer(
700
+ model_path=model_path,
701
+ frame_buffer_size=30,
702
+ prediction_interval=10,
703
+ threshold=0.6
704
  )
 
 
 
 
 
705
 
706
+ def gen_frames():
707
+ global camera, recognizer, is_running, current_frame, frame_lock
708
+
709
+ while is_running:
710
+ success, frame = camera.read()
711
+ if not success:
712
+ break
713
+ else:
714
+ status = recognizer.process_frame(frame)
715
+
716
+ ret, buffer = cv2.imencode('.jpg', frame)
717
+ if not ret:
718
+ continue
719
+
720
+ frame_data = base64.b64encode(buffer).decode('utf-8')
721
+
722
+ with frame_lock:
723
+ current_frame = {'image': frame_data, 'status': status}
724
+
725
+ socketio.emit('update_frame', {'image': frame_data, 'status': status})
726
+
727
+ time.sleep(0.03) # 約30 FPS
728
+
729
+ #--------------------
730
+ # 路由定義
731
+ #--------------------
732
+
733
+ # Messenger Bot 路由
734
  @app.route('/', methods=['GET'])
735
  def home():
736
+ """主頁 - 提供Web介面和Messenger Bot狀態"""
737
+ return render_template('index.html')
738
+
739
+ @app.route('/health')
740
+ def health_check():
741
+ """健康檢查"""
742
+ return {
743
+ 'status': 'healthy',
744
+ 'environment': 'HuggingFace Spaces' if IS_HUGGINGFACE else 'Local Development',
745
+ 'model_loaded': os.path.exists(MODEL_PATH),
746
+ 'labels_loaded': os.path.exists(LABELS_PATH)
747
+ }
748
 
749
  @app.route('/webhook', methods=['GET'])
750
  def verify_webhook():
 
785
 
786
  @app.route('/receive_recognition_result', methods=['POST'])
787
  def receive_recognition_result():
788
+ """接收手語辨識結果(內部呼叫)"""
789
  try:
790
  data = request.get_json()
791
 
 
795
  sender_id = data.get('sender_id')
796
  recognition_result = data.get('recognition_result', '無法辨識')
797
  confidence = data.get('confidence', 0)
 
798
 
799
  if not sender_id:
800
  return jsonify({"status": "error", "message": "缺少 sender_id"}), 400
 
803
  print(f"🎯 辨識結果:{recognition_result}")
804
  print(f"📊 信心度:{confidence}")
805
 
806
+ # 發送結果給用戶
807
  send_message(sender_id, recognition_result)
808
 
809
  return jsonify({
 
815
  print(f"處理辨識結果時發生錯誤:{e}")
816
  return jsonify({"status": "error", "message": str(e)}), 500
817
 
818
+ @app.route('/process_video', methods=['POST'])
819
+ def process_video():
820
+ """處理上傳的影片檔案(整合版本)"""
821
+ try:
822
+ # 檢查是否有上傳檔案
823
+ if 'video' not in request.files:
824
+ return jsonify({"status": "error", "message": "沒有上傳影片檔案"}), 400
825
+
826
+ video_file = request.files['video']
827
+ sender_id = request.form.get('sender_id', 'unknown')
828
+
829
+ if video_file.filename == '':
830
+ return jsonify({"status": "error", "message": "沒有選擇檔案"}), 400
831
+
832
+ # 儲存檔案
833
+ filename = secure_filename(video_file.filename)
834
+ timestamp = int(time.time())
835
+ filename = f"{timestamp}_{sender_id}_{filename}"
836
+ video_path = os.path.join(UPLOAD_FOLDER, filename)
837
+
838
+ video_file.save(video_path)
839
+ print(f"📁 影片已儲存:{video_path}")
840
+
841
+ # 初始化影片辨識器
842
+ model_path = MODEL_PATH
843
+ print(f"🔍 模型路徑: {model_path}")
844
+ print(f"🔍 模型檔案是否存在: {os.path.exists(model_path)}")
845
+
846
+ video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
847
+
848
+ # 處理影片
849
+ recognition_result, confidence = video_recognizer.process_video(video_path)
850
+
851
+ # 清理臨時檔案
852
+ try:
853
+ os.remove(video_path)
854
+ except:
855
+ pass
856
+
857
+ if recognition_result is not None:
858
+ # 如果是來自 Messenger 的請求,直接回傳結果給用戶
859
+ if sender_id != 'unknown':
860
+ send_message(sender_id, recognition_result)
861
+
862
+ return jsonify({
863
+ "status": "success",
864
+ "recognition_result": recognition_result,
865
+ "confidence": float(confidence),
866
+ "sender_id": sender_id
867
+ })
868
+ else:
869
+ return jsonify({
870
+ "status": "error",
871
+ "message": "無法辨識手語內容",
872
+ "sender_id": sender_id
873
+ }), 400
874
+
875
+ except Exception as e:
876
+ print(f"處理影片時發生錯誤:{e}")
877
+ return jsonify({"status": "error", "message": str(e)}), 500
878
+
879
+ #--------------------
880
+ # Messenger Bot 輔助函數
881
+ #--------------------
882
  def handle_message(messaging_event):
883
  """處理一般訊息"""
884
  sender_id = messaging_event['sender']['id']
 
888
 
889
  print(f"收到訊息 from {sender_id}: {message_text}")
890
 
891
+ # 檢查是否有附件
892
  if attachments:
893
  for attachment in attachments:
894
  if attachment.get('type') == 'video':
895
  video_url = attachment.get('payload', {}).get('url')
896
  if video_url:
897
+ # 直接處理影片(HuggingFace 整合版本)
898
+ process_messenger_video(video_url, sender_id)
 
 
 
 
 
 
 
899
  return
900
  else:
901
+ send_message(sender_id, f"收到 {attachment.get('type')} 附件")
902
  return
903
 
904
  # 處理文字訊息
905
  if message_text:
906
+ response_text = f"您好!請發送手語影片給我,我會幫您辨識手語內容。"
907
+ send_message(sender_id, response_text)
908
 
909
  def handle_postback(messaging_event):
910
  """處理 postback 事件(按鈕點擊等)"""
 
930
  'access_token': PAGE_ACCESS_TOKEN
931
  }
932
 
933
+ response = requests.post(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
934
  FACEBOOK_API_URL,
935
  headers=headers,
936
  params=params,
937
  json=data
938
  )
939
+
940
+ if response.status_code != 200:
941
+ print(f"發送訊息失敗: {response.status_code} - {response.text}")
942
+ else:
943
+ print(f"訊息發送成功給 {recipient_id}")
944
 
945
+ def process_messenger_video(video_url, sender_id):
946
+ """處理來自 Messenger 的影片(HuggingFace 整合版本)"""
947
  try:
948
+ print(f"🎬 開始處理 Messenger 影片:{video_url}")
 
 
 
 
 
949
 
950
  # 下載影片
951
  response = requests.get(video_url, stream=True, timeout=30)
952
  response.raise_for_status()
953
 
954
+ # 生成檔案名稱
955
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
956
+ filename = f"messenger_video_{sender_id}_{timestamp}.mp4"
957
+ file_path = os.path.join(UPLOAD_FOLDER, filename)
958
+
959
  # 寫入檔案
960
  with open(file_path, 'wb') as f:
961
  for chunk in response.iter_content(chunk_size=8192):
962
  if chunk:
963
  f.write(chunk)
964
 
965
+ print(f"✅ 影片下載完成:{file_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
 
967
+ # 初始化影片辨識器
968
+ model_path = MODEL_PATH
969
+ video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
970
 
971
+ # 處理影片
972
+ recognition_result, confidence = video_recognizer.process_video(file_path)
973
 
974
+ # 清理臨時檔案
975
+ try:
976
+ os.remove(file_path)
977
+ except:
978
+ pass
979
 
980
+ if recognition_result:
981
+ print(f"✅ 手語辨識完成 - 用戶:{sender_id}")
982
+ print(f"📝 辨識結果:{recognition_result}")
983
+ print(f"🎯 信心度:{confidence:.2f}")
984
+
985
+ # 發送結果給用戶
986
+ send_message(sender_id, recognition_result)
987
+ else:
988
+ send_message(sender_id, "抱歉,無法辨識您的手語內容,請再試一次。")
989
+
990
  except Exception as e:
991
+ print(f"處理 Messenger 影片時發生錯誤:{e}")
992
+ send_message(sender_id, "處理影片時發生錯誤,請稍後再試。")
993
+
994
+ #--------------------
995
+ # WebSocket 路由 (即時手語辨識)
996
+ #--------------------
997
+ @socketio.on('connect')
998
+ def handle_connect():
999
+ """處理WebSocket連接"""
1000
+ print('客戶端已連接')
1001
+
1002
+ @socketio.on('disconnect')
1003
+ def handle_disconnect():
1004
+ """處理WebSocket斷開連接"""
1005
+ print('客戶端已斷開連接')
1006
+
1007
+ @socketio.on('start_stream')
1008
+ def handle_start_stream(data):
1009
+ """開始視頻流"""
1010
+ global camera, is_running
1011
 
1012
+ # 雲端環境檢查
1013
+ if IS_HUGGINGFACE:
1014
+ return {'status': 'error', 'message': '雲端環境不支援攝像頭功能,請使用影片上傳功能'}
1015
+
1016
+ if is_running:
1017
+ return {'status': 'already_running'}
1018
+
1019
+ # 初始化攝像頭
1020
+ camera = cv2.VideoCapture(0)
1021
+ camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
1022
+ camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
1023
+
1024
+ if not camera.isOpened():
1025
+ return {'status': 'error', 'message': '無法打開攝像頭'}
1026
+
1027
+ # 初始化手語辨識器
1028
+ if recognizer is None:
1029
+ initialize_recognizer()
1030
+
1031
+ # 啟動處理線程
1032
+ is_running = True
1033
+ threading.Thread(target=gen_frames, daemon=True).start()
1034
+
1035
+ return {'status': 'success'}
1036
 
1037
+ @socketio.on('stop_stream')
1038
+ def handle_stop_stream(data):
1039
+ """停止視頻流"""
1040
+ global camera, is_running
1041
+
1042
+ is_running = False
1043
+
1044
+ # 釋放攝像頭
1045
+ if camera is not None:
1046
+ camera.release()
1047
+ camera = None
1048
+
1049
+ return {'status': 'success'}
1050
+
1051
+ #--------------------
1052
+ # 應用程式啟動
1053
+ #--------------------
1054
  if __name__ == '__main__':
1055
+ # HuggingFace Spaces 環境檢測
1056
+ port = int(os.environ.get('PORT', 7860)) # HuggingFace 預設端口
1057
+
1058
+ print("🚀 手語辨識整合系統啟動中...")
1059
+ print(f"📱 Messenger Bot: {'已配置' if PAGE_ACCESS_TOKEN != 'your_page_access_token' else '未配置'}")
1060
+ print(f"🤖 OpenAI API: {'已配置' if os.environ.get('OPENAI_API_KEY') else '未配置'}")
1061
+ print(f"🔧 運行模式: {'HuggingFace Spaces' if port == 7860 else '本地開發'}")
1062
+
1063
+ socketio.run(app, host='0.0.0.0', port=port, debug=False, allow_unsafe_werkzeug=True)
app_config.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+
6
+ # HuggingFace Spaces 配置
7
+ APP_TITLE = "手語辨識整合系統"
8
+ APP_DESCRIPTION = "AI驅動的手語辨識系統,支援Web介面、Messenger Bot和API"
9
+
10
+ # 預設配置
11
+ DEFAULT_CONFIG = {
12
+ "MODEL_PATH": "data/models/sign_language_model.pth",
13
+ "LABELS_PATH": "data/labels.csv",
14
+ "UPLOAD_FOLDER": "uploads",
15
+ "MAX_FILE_SIZE": 100 * 1024 * 1024, # 100MB
16
+ "FRAME_SKIP": 5, # 每5幀處理一次
17
+ "CONFIDENCE_THRESHOLD": 0.5,
18
+ "FRAME_BUFFER_SIZE": 30,
19
+ "PREDICTION_INTERVAL": 10
20
+ }
21
+
22
+ # 環境變數配置
23
+ def get_config():
24
+ return {
25
+ "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY"),
26
+ "VERIFY_TOKEN": os.environ.get("VERIFY_TOKEN", "your_verify_token"),
27
+ "PAGE_ACCESS_TOKEN": os.environ.get("PAGE_ACCESS_TOKEN", "your_page_access_token"),
28
+ "PORT": int(os.environ.get("PORT", 7860)),
29
+ "DEBUG": os.environ.get("DEBUG", "False").lower() == "true",
30
+ **DEFAULT_CONFIG
31
+ }
32
+
33
+ # HuggingFace Spaces 專用設定
34
+ HUGGINGFACE_CONFIG = {
35
+ "title": APP_TITLE,
36
+ "description": APP_DESCRIPTION,
37
+ "tags": ["computer-vision", "sign-language", "pytorch", "mediapipe", "openai"],
38
+ "license": "mit",
39
+ "sdk": "docker",
40
+ "app_port": 7860
41
+ }
{features → data/features}/keypoints/eat_001_aug_rotate_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_001_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_001_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_002_aug_rotate_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_002_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_002_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_003_aug_rotate_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_003_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_004_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_004_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_004_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_005_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_005_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_006_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_007_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_007_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_008_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_008_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_009_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_009_aug_rotate_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_009_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_009_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_010_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_010_aug_rotate_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_010_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_010_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_011_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_011_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_012_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_012_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_013_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_014_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_014_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_015_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_015_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_015_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_016_aug_flip_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_016_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_016_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_017_aug_rotate_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_017_aug_shift_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_017_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_018_keypoints.npy RENAMED
File without changes
{features → data/features}/keypoints/eat_019_aug_flip_keypoints.npy RENAMED
File without changes