Spaces:
Sleeping
Sleeping
Upload 30 files
Browse files- .gitignore +47 -0
- .python-version +1 -0
- Dockerfile +11 -15
- INSTALLATION.md +215 -0
- QUICKSTART.md +108 -0
- README.md +126 -20
- README_NEW.md +113 -0
- app.py +163 -0
- app_enhanced.py +473 -0
- app_new.py +357 -0
- create_sample_model.py +52 -0
- create_simple_model.py +25 -0
- data_collection.py +136 -0
- final_app.py +292 -0
- requirements.txt +0 -0
- requirements_new.txt +10 -0
- requirements_training.txt +8 -0
- run_app.py +96 -0
- run_simple_app.py +59 -0
- server.py +51 -0
- setup.py +68 -0
- sign_utils.py +200 -0
- simple_app.py +195 -0
- simple_sign_interpreter.py +307 -0
- test_system.py +155 -0
- train_digits_model.py +330 -0
- train_gesture_dataset.py +427 -0
- train_model.py +176 -0
- train_platform.py +70 -0
- train_simple.py +176 -0
.gitignore
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
build/
|
| 9 |
+
develop-eggs/
|
| 10 |
+
dist/
|
| 11 |
+
downloads/
|
| 12 |
+
eggs/
|
| 13 |
+
.eggs/
|
| 14 |
+
lib/
|
| 15 |
+
lib64/
|
| 16 |
+
parts/
|
| 17 |
+
sdist/
|
| 18 |
+
var/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual Environment
|
| 24 |
+
venv/
|
| 25 |
+
ENV/
|
| 26 |
+
|
| 27 |
+
# IDE
|
| 28 |
+
.idea/
|
| 29 |
+
.vscode/
|
| 30 |
+
*.swp
|
| 31 |
+
*.swo
|
| 32 |
+
|
| 33 |
+
# Streamlit
|
| 34 |
+
.streamlit/
|
| 35 |
+
|
| 36 |
+
# Model files
|
| 37 |
+
*.h5
|
| 38 |
+
*.tflite
|
| 39 |
+
*.pb
|
| 40 |
+
|
| 41 |
+
# Misc
|
| 42 |
+
.DS_Store
|
| 43 |
+
.env
|
| 44 |
+
.env.local
|
| 45 |
+
.env.development.local
|
| 46 |
+
.env.test.local
|
| 47 |
+
.env.production.local
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.10.13
|
Dockerfile
CHANGED
|
@@ -1,21 +1,17 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
-
WORKDIR /
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
curl \
|
| 8 |
-
software-properties-common \
|
| 9 |
-
git \
|
| 10 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
| 12 |
-
|
| 13 |
-
COPY
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
EXPOSE
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
|
| 3 |
+
WORKDIR /code
|
| 4 |
|
| 5 |
+
# Install system dependencies for OpenCV and MediaPipe
|
| 6 |
+
RUN apt-get update && apt-get install -y libgl1 ffmpeg
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
# Copy all project files into the container
|
| 9 |
+
COPY . .
|
| 10 |
|
| 11 |
+
# Install Python dependencies
|
| 12 |
+
RUN pip install --upgrade pip
|
| 13 |
+
RUN pip install -r requirements.txt
|
| 14 |
|
| 15 |
+
EXPOSE 7860
|
| 16 |
|
| 17 |
+
CMD ["streamlit", "run", "simple_sign_interpreter.py", "--server.port=7860", "--server.headless=true", "--server.enableCORS=false"]
|
|
|
|
|
|
INSTALLATION.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📦 Installation Guide - Sign Language Interpreter
|
| 2 |
+
|
| 3 |
+
This guide provides detailed installation instructions for the Sign Language Interpreter system.
|
| 4 |
+
|
| 5 |
+
## 🔧 System Requirements
|
| 6 |
+
|
| 7 |
+
- **Python**: 3.8 - 3.11 (recommended: 3.9 or 3.10)
|
| 8 |
+
- **Operating System**: Windows 10/11, macOS 10.14+, or Linux
|
| 9 |
+
- **Hardware**:
|
| 10 |
+
- Webcam (built-in or external)
|
| 11 |
+
- 4GB RAM minimum (8GB recommended)
|
| 12 |
+
- 2GB free disk space
|
| 13 |
+
- **Internet**: Required for initial setup and package installation
|
| 14 |
+
|
| 15 |
+
## 🚀 Quick Installation
|
| 16 |
+
|
| 17 |
+
### Option 1: Automated Setup (Recommended)
|
| 18 |
+
|
| 19 |
+
1. **Download and Extract**
|
| 20 |
+
```bash
|
| 21 |
+
# If using git
|
| 22 |
+
git clone https://github.com/yourusername/sign-language-interpreter.git
|
| 23 |
+
cd sign-language-interpreter
|
| 24 |
+
|
| 25 |
+
# Or download and extract ZIP file
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
2. **Run Setup Script**
|
| 29 |
+
```bash
|
| 30 |
+
python setup.py
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
This will:
|
| 34 |
+
- Create necessary directories
|
| 35 |
+
- Install required packages
|
| 36 |
+
- Create a sample model
|
| 37 |
+
- Verify installation
|
| 38 |
+
|
| 39 |
+
### Option 2: Manual Installation
|
| 40 |
+
|
| 41 |
+
1. **Create Virtual Environment**
|
| 42 |
+
```bash
|
| 43 |
+
python -m venv venv
|
| 44 |
+
|
| 45 |
+
# Activate virtual environment
|
| 46 |
+
# Windows:
|
| 47 |
+
venv\Scripts\activate
|
| 48 |
+
# macOS/Linux:
|
| 49 |
+
source venv/bin/activate
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
2. **Install Dependencies**
|
| 53 |
+
```bash
|
| 54 |
+
pip install -r requirements.txt
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
3. **Create Directories**
|
| 58 |
+
```bash
|
| 59 |
+
mkdir models dataset logs examples
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
4. **Create Sample Model**
|
| 63 |
+
```bash
|
| 64 |
+
python create_simple_model.py
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## 🔍 Verification
|
| 68 |
+
|
| 69 |
+
Run the system test to verify everything is working:
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
python test_system.py
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Expected output:
|
| 76 |
+
```
|
| 77 |
+
🤟 Sign Language Interpreter - System Test
|
| 78 |
+
==================================================
|
| 79 |
+
|
| 80 |
+
Dependencies:
|
| 81 |
+
✅ All dependencies installed
|
| 82 |
+
|
| 83 |
+
Camera:
|
| 84 |
+
✅ Camera working
|
| 85 |
+
|
| 86 |
+
MediaPipe:
|
| 87 |
+
✅ MediaPipe working
|
| 88 |
+
|
| 89 |
+
Model Files:
|
| 90 |
+
✅ Model files found
|
| 91 |
+
|
| 92 |
+
Predictor:
|
| 93 |
+
✅ Predictor working - Sample prediction: hello (0.85)
|
| 94 |
+
|
| 95 |
+
==================================================
|
| 96 |
+
Test Results: 5/5 tests passed
|
| 97 |
+
🎉 All tests passed! System is ready to use.
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## 🚀 Running the Application
|
| 101 |
+
|
| 102 |
+
### Web Interface (Recommended)
|
| 103 |
+
```bash
|
| 104 |
+
streamlit run app.py
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### Command Line Interface
|
| 108 |
+
```bash
|
| 109 |
+
python examples/basic_usage.py
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## 🛠️ Troubleshooting
|
| 113 |
+
|
| 114 |
+
### Common Issues
|
| 115 |
+
|
| 116 |
+
1. **Python Version Error**
|
| 117 |
+
```
|
| 118 |
+
Error: This package requires Python 3.8-3.11
|
| 119 |
+
```
|
| 120 |
+
**Solution**: Install a compatible Python version
|
| 121 |
+
|
| 122 |
+
2. **Camera Access Error**
|
| 123 |
+
```
|
| 124 |
+
Error: Camera not accessible
|
| 125 |
+
```
|
| 126 |
+
**Solutions**:
|
| 127 |
+
- Close other applications using the camera
|
| 128 |
+
- Check camera permissions
|
| 129 |
+
- Try a different camera index
|
| 130 |
+
|
| 131 |
+
3. **Package Installation Error**
|
| 132 |
+
```
|
| 133 |
+
Error: Could not find a version that satisfies the requirement
|
| 134 |
+
```
|
| 135 |
+
**Solutions**:
|
| 136 |
+
- Update pip: `python -m pip install --upgrade pip`
|
| 137 |
+
- Try installing packages individually
|
| 138 |
+
- Check Python version compatibility
|
| 139 |
+
|
| 140 |
+
4. **TensorFlow Installation Issues**
|
| 141 |
+
```
|
| 142 |
+
Error: No matching distribution found for tensorflow
|
| 143 |
+
```
|
| 144 |
+
**Solutions**:
|
| 145 |
+
- Use Python 3.8-3.11
|
| 146 |
+
- Install TensorFlow separately: `pip install tensorflow`
|
| 147 |
+
- Use CPU-only version: `pip install tensorflow-cpu`
|
| 148 |
+
|
| 149 |
+
### Platform-Specific Issues
|
| 150 |
+
|
| 151 |
+
#### Windows
|
| 152 |
+
- **Visual C++ Error**: Install Microsoft Visual C++ Redistributable
|
| 153 |
+
- **Long Path Error**: Enable long path support in Windows settings
|
| 154 |
+
|
| 155 |
+
#### macOS
|
| 156 |
+
- **Permission Error**: Grant camera access in System Preferences
|
| 157 |
+
- **Homebrew Issues**: Update Homebrew and Python
|
| 158 |
+
|
| 159 |
+
#### Linux
|
| 160 |
+
- **Camera Error**: Install v4l-utils: `sudo apt install v4l-utils`
|
| 161 |
+
- **Permission Error**: Add user to video group: `sudo usermod -a -G video $USER`
|
| 162 |
+
|
| 163 |
+
## 🔄 Updates
|
| 164 |
+
|
| 165 |
+
To update the system:
|
| 166 |
+
|
| 167 |
+
1. **Update Code**
|
| 168 |
+
```bash
|
| 169 |
+
git pull # If using git
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
2. **Update Dependencies**
|
| 173 |
+
```bash
|
| 174 |
+
pip install -r requirements.txt --upgrade
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
3. **Test System**
|
| 178 |
+
```bash
|
| 179 |
+
python test_system.py
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
## 🆘 Getting Help
|
| 183 |
+
|
| 184 |
+
If you encounter issues:
|
| 185 |
+
|
| 186 |
+
1. **Check Documentation**
|
| 187 |
+
- Read `README.md` for overview
|
| 188 |
+
- Check `QUICKSTART.md` for basic usage
|
| 189 |
+
- Review this installation guide
|
| 190 |
+
|
| 191 |
+
2. **Run Diagnostics**
|
| 192 |
+
```bash
|
| 193 |
+
python test_system.py --verbose
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
3. **Community Support**
|
| 197 |
+
- GitHub Issues: Report bugs and feature requests
|
| 198 |
+
- Discord: Join our community chat
|
| 199 |
+
- Email: support@signlanguage.ai
|
| 200 |
+
|
| 201 |
+
## 📋 Next Steps
|
| 202 |
+
|
| 203 |
+
After successful installation:
|
| 204 |
+
|
| 205 |
+
1. **Try the Demo**: `streamlit run app.py`
|
| 206 |
+
2. **Collect Data**: `python data_collection.py`
|
| 207 |
+
3. **Train Model**: `python train_model.py`
|
| 208 |
+
4. **Read Documentation**: Check `docs/` folder for guides
|
| 209 |
+
|
| 210 |
+
## 🔐 Security Notes
|
| 211 |
+
|
| 212 |
+
- The application only accesses your camera locally
|
| 213 |
+
- No data is sent to external servers
|
| 214 |
+
- All processing happens on your device
|
| 215 |
+
- You can review the source code for transparency
|
QUICKSTART.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Quick Start Guide - Sign Language Interpreter
|
| 2 |
+
|
| 3 |
+
This guide will help you get started with the Sign Language Interpreter system quickly.
|
| 4 |
+
|
| 5 |
+
## 📋 Prerequisites
|
| 6 |
+
|
| 7 |
+
- Python 3.8 or higher
|
| 8 |
+
- Webcam
|
| 9 |
+
- Git (optional)
|
| 10 |
+
|
| 11 |
+
## 🛠️ Installation
|
| 12 |
+
|
| 13 |
+
1. **Setup Environment**
|
| 14 |
+
```bash
|
| 15 |
+
# Clone repository (if using git)
|
| 16 |
+
git clone https://github.com/yourusername/sign-language-interpreter.git
|
| 17 |
+
cd sign-language-interpreter
|
| 18 |
+
|
| 19 |
+
# Create virtual environment
|
| 20 |
+
python -m venv venv
|
| 21 |
+
|
| 22 |
+
# Activate virtual environment
|
| 23 |
+
# On Windows:
|
| 24 |
+
venv\Scripts\activate
|
| 25 |
+
# On macOS/Linux:
|
| 26 |
+
source venv/bin/activate
|
| 27 |
+
|
| 28 |
+
# Run setup script
|
| 29 |
+
python setup.py
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## 🎯 Using the Application
|
| 33 |
+
|
| 34 |
+
### 1. Quick Demo
|
| 35 |
+
To try the system with a pre-trained model:
|
| 36 |
+
```bash
|
| 37 |
+
streamlit run app.py
|
| 38 |
+
```
|
| 39 |
+
This will launch the web interface where you can:
|
| 40 |
+
- Adjust confidence threshold
|
| 41 |
+
- Toggle hand landmark visualization
|
| 42 |
+
- See real-time sign language interpretation
|
| 43 |
+
|
| 44 |
+
### 2. Collect Your Own Data
|
| 45 |
+
To create a custom dataset:
|
| 46 |
+
```bash
|
| 47 |
+
python data_collection.py
|
| 48 |
+
```
|
| 49 |
+
Follow the on-screen instructions to:
|
| 50 |
+
- Choose signs to collect
|
| 51 |
+
- Record sequences of hand gestures
|
| 52 |
+
- Save data for training
|
| 53 |
+
|
| 54 |
+
### 3. Train Custom Model
|
| 55 |
+
To train a model on your collected data:
|
| 56 |
+
```bash
|
| 57 |
+
python train_model.py
|
| 58 |
+
```
|
| 59 |
+
This will:
|
| 60 |
+
- Load your collected dataset
|
| 61 |
+
- Train a new model
|
| 62 |
+
- Save the trained model
|
| 63 |
+
- Generate performance metrics
|
| 64 |
+
|
| 65 |
+
## 🔍 Troubleshooting
|
| 66 |
+
|
| 67 |
+
1. **Camera not working?**
|
| 68 |
+
- Check if another application is using the camera
|
| 69 |
+
- Ensure camera permissions are granted
|
| 70 |
+
- Try closing and reopening the application
|
| 71 |
+
|
| 72 |
+
2. **Poor recognition?**
|
| 73 |
+
- Ensure good lighting
|
| 74 |
+
- Keep hands within camera frame
|
| 75 |
+
- Try adjusting the confidence threshold
|
| 76 |
+
- Collect more training data
|
| 77 |
+
|
| 78 |
+
3. **Installation issues?**
|
| 79 |
+
- Check Python version: `python --version`
|
| 80 |
+
- Update pip: `python -m pip install --upgrade pip`
|
| 81 |
+
- Install requirements manually: `pip install -r requirements.txt`
|
| 82 |
+
|
| 83 |
+
## 📚 Additional Resources
|
| 84 |
+
|
| 85 |
+
- Check `README.md` for detailed documentation
|
| 86 |
+
- View example signs in `examples/` directory
|
| 87 |
+
- See training tips in `docs/training_guide.md`
|
| 88 |
+
|
| 89 |
+
## 🆘 Need Help?
|
| 90 |
+
|
| 91 |
+
- Check the issues section on GitHub
|
| 92 |
+
- Join our Discord community
|
| 93 |
+
- Email support at: support@signlanguage.ai
|
| 94 |
+
|
| 95 |
+
## 🔄 Updates
|
| 96 |
+
|
| 97 |
+
To update the application:
|
| 98 |
+
```bash
|
| 99 |
+
git pull # If using git
|
| 100 |
+
pip install -r requirements.txt # Update dependencies
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
## 📝 Notes
|
| 104 |
+
|
| 105 |
+
- The system works best in well-lit environments
|
| 106 |
+
- Position yourself 2-3 feet from the camera
|
| 107 |
+
- Keep your hand gestures clear and deliberate
|
| 108 |
+
- Practice with the sample signs first
|
README.md
CHANGED
|
@@ -1,20 +1,126 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🤟 Sign Language Interpreter
|
| 2 |
+
|
| 3 |
+
A real-time sign language recognition system built with Python, MediaPipe, TensorFlow, and Streamlit. This application can detect and interpret sign language gestures in real-time using computer vision and machine learning.
|
| 4 |
+
|
| 5 |
+
## 🌟 Features
|
| 6 |
+
|
| 7 |
+
- **Real-time Hand Detection**: Uses MediaPipe for accurate hand landmark detection
|
| 8 |
+
- **Sign Language Recognition**: Supports 29 gestures including:
|
| 9 |
+
- Numbers: 1, 2, 4, 5, 6, 7, 8
|
| 10 |
+
- Letters: A-X (excluding E)
|
| 11 |
+
- **Live Webcam Processing**: Real-time gesture recognition through webcam
|
| 12 |
+
- **Interactive Web Interface**: Built with Streamlit for easy use
|
| 13 |
+
- **Adjustable Confidence**: Configurable detection confidence threshold
|
| 14 |
+
- **Optimized Performance**: Fast response with temporal prediction smoothing
|
| 15 |
+
|
| 16 |
+
## 🚀 Quick Start
|
| 17 |
+
|
| 18 |
+
### Prerequisites
|
| 19 |
+
- Python 3.8+
|
| 20 |
+
- Webcam
|
| 21 |
+
- Git
|
| 22 |
+
|
| 23 |
+
### Installation
|
| 24 |
+
|
| 25 |
+
1. **Clone the repository**
|
| 26 |
+
```bash
|
| 27 |
+
git clone https://github.com/yourusername/ai-sign-language-interpreter.git
|
| 28 |
+
cd ai-sign-language-interpreter
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
2. **Install dependencies**
|
| 32 |
+
```bash
|
| 33 |
+
pip install -r requirements.txt
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
3. **Run the application**
|
| 37 |
+
```bash
|
| 38 |
+
streamlit run simple_sign_interpreter.py
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
4. **Open your browser** and navigate to `http://localhost:8501`
|
| 42 |
+
|
| 43 |
+
## 🎯 Usage
|
| 44 |
+
|
| 45 |
+
1. **Start the application** using the command above
|
| 46 |
+
2. **Allow camera access** when prompted by your browser
|
| 47 |
+
3. **Click "START"** to begin webcam capture
|
| 48 |
+
4. **Show hand gestures** to the camera
|
| 49 |
+
5. **Adjust confidence threshold** using the sidebar slider for better detection
|
| 50 |
+
6. **View real-time predictions** in the "Current Sign" panel
|
| 51 |
+
|
| 52 |
+
## 📁 Project Structure
|
| 53 |
+
|
| 54 |
+
```
|
| 55 |
+
ai-sign-language-interpreter/
|
| 56 |
+
├── simple_sign_interpreter.py # Main Streamlit application
|
| 57 |
+
├── sign_utils.py # Core prediction and landmark detection
|
| 58 |
+
├── models/
|
| 59 |
+
│ ├── sign_model.tflite # Trained TensorFlow Lite model
|
| 60 |
+
│ └── labels.txt # Class labels for gestures
|
| 61 |
+
├── requirements.txt # Python dependencies
|
| 62 |
+
├── .gitignore # Git ignore file
|
| 63 |
+
└── README.md # Project documentation
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## 🔧 Technical Details
|
| 67 |
+
|
| 68 |
+
### Architecture
|
| 69 |
+
- **Frontend**: Streamlit web interface
|
| 70 |
+
- **Computer Vision**: MediaPipe for hand landmark detection
|
| 71 |
+
- **Machine Learning**: TensorFlow Lite for gesture classification
|
| 72 |
+
- **Real-time Processing**: Optimized prediction pipeline with temporal smoothing
|
| 73 |
+
|
| 74 |
+
### Model Information
|
| 75 |
+
- **Input**: 63-dimensional hand landmark features (21 landmarks × 3 coordinates)
|
| 76 |
+
- **Output**: 29 sign language gesture classes
|
| 77 |
+
- **Format**: TensorFlow Lite (.tflite) for optimized inference
|
| 78 |
+
- **Performance**: Real-time processing at 30+ FPS
|
| 79 |
+
|
| 80 |
+
### Key Components
|
| 81 |
+
|
| 82 |
+
1. **MediaPipeLandmarksModel**: Handles hand detection and landmark extraction
|
| 83 |
+
2. **SignLanguagePredictor**: Manages model inference and prediction smoothing
|
| 84 |
+
3. **Streamlit Interface**: Provides user-friendly web interface
|
| 85 |
+
|
| 86 |
+
## 🎛️ Configuration
|
| 87 |
+
|
| 88 |
+
### Adjustable Parameters
|
| 89 |
+
- **Detection Confidence**: Minimum confidence for hand detection (default: 0.5)
|
| 90 |
+
- **Tracking Confidence**: Minimum confidence for hand tracking (default: 0.5)
|
| 91 |
+
- **Prediction Threshold**: Minimum confidence for gesture prediction (default: 0.5)
|
| 92 |
+
- **Buffer Size**: Number of frames for prediction smoothing (default: 8)
|
| 93 |
+
|
| 94 |
+
## 🤝 Contributing
|
| 95 |
+
|
| 96 |
+
1. Fork the repository
|
| 97 |
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
| 98 |
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
| 99 |
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
| 100 |
+
5. Open a Pull Request
|
| 101 |
+
|
| 102 |
+
## 📝 License
|
| 103 |
+
|
| 104 |
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
| 105 |
+
|
| 106 |
+
## 🙏 Acknowledgments
|
| 107 |
+
|
| 108 |
+
- **MediaPipe** for hand landmark detection
|
| 109 |
+
- **TensorFlow** for machine learning framework
|
| 110 |
+
- **Streamlit** for the web interface
|
| 111 |
+
- **OpenCV** for computer vision utilities
|
| 112 |
+
|
| 113 |
+
## 📞 Support
|
| 114 |
+
|
| 115 |
+
If you encounter any issues or have questions:
|
| 116 |
+
1. Check the [Issues](https://github.com/kris07hna/SIGN-LANG-INTREPRETOR/issues) page
|
| 117 |
+
2. Create a new issue with detailed description
|
| 118 |
+
3. Include system information and error messages
|
| 119 |
+
|
| 120 |
+
## 🔮 Future Enhancements
|
| 121 |
+
|
| 122 |
+
- [ ] Support for more sign language gestures
|
| 123 |
+
- [ ] Multi-hand gesture recognition
|
| 124 |
+
- [ ] Sign language sentence formation
|
| 125 |
+
- [ ] Mobile app version
|
| 126 |
+
- [ ] Real-time translation to multiple languages
|
README_NEW.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🤟 AI Sign Language Interpreter
|
| 2 |
+
|
| 3 |
+
A comprehensive, real-time sign language interpretation system powered by AI. This application uses computer vision and machine learning to recognize and interpret sign language gestures, providing instant translations with natural language understanding through Google's Gemini AI.
|
| 4 |
+
|
| 5 |
+
## ✨ Features
|
| 6 |
+
|
| 7 |
+
- 🎯 Real-time sign language recognition
|
| 8 |
+
- 🤖 AI-powered gesture interpretation
|
| 9 |
+
- 📹 Live webcam processing
|
| 10 |
+
- 🔤 Support for letters (A-X) and numbers (1-8)
|
| 11 |
+
- 💡 Natural language interpretation using Gemini AI
|
| 12 |
+
- 🎨 Beautiful, responsive user interface
|
| 13 |
+
- 📱 Mobile-friendly design
|
| 14 |
+
|
| 15 |
+
## 🚀 Quick Start
|
| 16 |
+
|
| 17 |
+
1. **Install Dependencies**
|
| 18 |
+
```bash
|
| 19 |
+
pip install -r requirements_new.txt
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
2. **Set Up Gemini API**
|
| 23 |
+
- Create `.streamlit/secrets.toml` if not exists
|
| 24 |
+
- Add your Gemini API key:
|
| 25 |
+
```toml
|
| 26 |
+
GEMINI_API_KEY = "your-api-key-here"
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
3. **Run the Application**
|
| 30 |
+
```bash
|
| 31 |
+
python run_app.py
|
| 32 |
+
```
|
| 33 |
+
This will start both the landing page and the Streamlit application.
|
| 34 |
+
- Landing page: http://localhost:5000
|
| 35 |
+
- Streamlit app: http://localhost:8501
|
| 36 |
+
|
| 37 |
+
## 📖 Usage
|
| 38 |
+
|
| 39 |
+
1. Visit the landing page at http://localhost:5000
|
| 40 |
+
2. Click "Try it Out" to launch the interpreter
|
| 41 |
+
3. Allow camera access when prompted
|
| 42 |
+
4. Position your hand clearly in the camera view
|
| 43 |
+
5. Make sign language gestures
|
| 44 |
+
6. View real-time interpretations and AI-enhanced understanding
|
| 45 |
+
|
| 46 |
+
## 🎯 Supported Gestures
|
| 47 |
+
|
| 48 |
+
### Numbers
|
| 49 |
+
- 1, 2, 4, 5, 6, 7, 8
|
| 50 |
+
|
| 51 |
+
### Letters
|
| 52 |
+
- A through X (except E)
|
| 53 |
+
|
| 54 |
+
## 💡 Tips for Best Results
|
| 55 |
+
|
| 56 |
+
- Ensure good lighting conditions
|
| 57 |
+
- Keep your hand centered in the frame
|
| 58 |
+
- Make clear, distinct gestures
|
| 59 |
+
- Hold gestures steady for 1-2 seconds
|
| 60 |
+
- Maintain consistent hand orientation
|
| 61 |
+
- Avoid cluttered backgrounds
|
| 62 |
+
|
| 63 |
+
## 🛠️ Technical Details
|
| 64 |
+
|
| 65 |
+
### Components
|
| 66 |
+
|
| 67 |
+
- **Frontend**
|
| 68 |
+
- Landing page: HTML/CSS with Tailwind CSS
|
| 69 |
+
- Application UI: Streamlit
|
| 70 |
+
- Real-time video: streamlit-webrtc
|
| 71 |
+
|
| 72 |
+
- **Backend**
|
| 73 |
+
- Web Server: Flask
|
| 74 |
+
- ML Framework: TensorFlow Lite
|
| 75 |
+
- Hand Tracking: MediaPipe
|
| 76 |
+
- AI Interpretation: Google Gemini
|
| 77 |
+
|
| 78 |
+
### Model Architecture
|
| 79 |
+
|
| 80 |
+
- Custom TensorFlow model trained on hand gesture dataset
|
| 81 |
+
- MediaPipe hand landmark detection
|
| 82 |
+
- 63-dimensional feature vector for gesture recognition
|
| 83 |
+
- Real-time processing optimizations
|
| 84 |
+
|
| 85 |
+
## 📊 Performance
|
| 86 |
+
|
| 87 |
+
- Real-time processing at 30+ FPS
|
| 88 |
+
- Low latency gesture recognition
|
| 89 |
+
- High accuracy on supported gestures
|
| 90 |
+
- Efficient memory usage
|
| 91 |
+
|
| 92 |
+
## 🤝 Contributing
|
| 93 |
+
|
| 94 |
+
Contributions are welcome! Please feel free to submit pull requests.
|
| 95 |
+
|
| 96 |
+
## 📝 License
|
| 97 |
+
|
| 98 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
| 99 |
+
|
| 100 |
+
## 🙏 Acknowledgments
|
| 101 |
+
|
| 102 |
+
- MediaPipe team for hand tracking technology
|
| 103 |
+
- Google for Gemini AI API
|
| 104 |
+
- Streamlit team for the amazing framework
|
| 105 |
+
- Open source community for various tools and libraries
|
| 106 |
+
|
| 107 |
+
## 📞 Support
|
| 108 |
+
|
| 109 |
+
For issues, questions, or contributions, please open an issue on GitHub.
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
Made with ❤️ for breaking communication barriers
|
app.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from streamlit_webrtc import webrtc_streamer, VideoTransformerBase
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
from av import VideoFrame
|
| 6 |
+
import os
|
| 7 |
+
from sign_utils import SignLanguagePredictor, MediaPipeLandmarksModel
|
| 8 |
+
|
| 9 |
+
# Page config
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="Sign Language Digits Interpreter",
|
| 12 |
+
page_icon="🔢",
|
| 13 |
+
layout="wide"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Sidebar
|
| 17 |
+
st.sidebar.title("🔢 Controls")
|
| 18 |
+
confidence_threshold = st.sidebar.slider("Confidence Threshold", 0.0, 1.0, 0.5)
|
| 19 |
+
show_landmarks = st.sidebar.checkbox("Show Hand Landmarks", True)
|
| 20 |
+
|
| 21 |
+
# Main content
|
| 22 |
+
st.title("Sign Language Digits Interpreter")
|
| 23 |
+
st.markdown("""
|
| 24 |
+
This app recognizes hand signs for digits (0-9) in real-time.
|
| 25 |
+
Show your hand signs to the camera and see the interpretations!
|
| 26 |
+
|
| 27 |
+
Instructions:
|
| 28 |
+
1. Allow camera access when prompted
|
| 29 |
+
2. Show your hand sign for a digit (0-9)
|
| 30 |
+
3. Hold the sign steady for best results
|
| 31 |
+
4. Adjust the confidence threshold if needed
|
| 32 |
+
""")
|
| 33 |
+
|
| 34 |
+
# Initialize models
|
| 35 |
+
@st.cache_resource
|
| 36 |
+
def load_models():
|
| 37 |
+
try:
|
| 38 |
+
predictor = SignLanguagePredictor(
|
| 39 |
+
model_path="models/model.tflite",
|
| 40 |
+
labels_path="models/labels.txt"
|
| 41 |
+
)
|
| 42 |
+
return predictor
|
| 43 |
+
except Exception as e:
|
| 44 |
+
st.error(f"Error loading models: {str(e)}")
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
predictor = load_models()
|
| 48 |
+
|
| 49 |
+
class DigitInterpreter(VideoTransformerBase):
|
| 50 |
+
def __init__(self):
|
| 51 |
+
self.predictor = predictor
|
| 52 |
+
self.landmark_model = MediaPipeLandmarksModel()
|
| 53 |
+
self.result_text = "Show your hand sign"
|
| 54 |
+
self.confidence = 0.0
|
| 55 |
+
self.landmarks = None
|
| 56 |
+
self.frame_count = 0
|
| 57 |
+
self.last_prediction = None
|
| 58 |
+
self.prediction_counter = {}
|
| 59 |
+
|
| 60 |
+
def recv(self, frame: VideoFrame) -> VideoFrame:
|
| 61 |
+
image = frame.to_ndarray(format="bgr24")
|
| 62 |
+
self.frame_count += 1
|
| 63 |
+
|
| 64 |
+
if self.predictor:
|
| 65 |
+
# Get landmarks
|
| 66 |
+
self.landmarks = self.landmark_model.predict_landmarks(image)
|
| 67 |
+
|
| 68 |
+
# Get prediction
|
| 69 |
+
if self.landmarks:
|
| 70 |
+
try:
|
| 71 |
+
# Use static prediction mode
|
| 72 |
+
prediction, self.confidence = self.predictor.predict_static(image)
|
| 73 |
+
|
| 74 |
+
# Only update prediction if confidence is above threshold
|
| 75 |
+
if self.confidence > confidence_threshold:
|
| 76 |
+
# Count predictions to reduce flickering
|
| 77 |
+
if prediction not in self.prediction_counter:
|
| 78 |
+
self.prediction_counter = {prediction: 1}
|
| 79 |
+
else:
|
| 80 |
+
self.prediction_counter[prediction] += 1
|
| 81 |
+
|
| 82 |
+
# Update result if we have consistent predictions
|
| 83 |
+
max_pred = max(self.prediction_counter.items(), key=lambda x: x[1])
|
| 84 |
+
if max_pred[1] >= 3: # Require 3 consistent predictions
|
| 85 |
+
self.result_text = f"Digit: {prediction}"
|
| 86 |
+
self.last_prediction = prediction
|
| 87 |
+
else:
|
| 88 |
+
self.result_text = "Show your hand sign"
|
| 89 |
+
self.prediction_counter = {}
|
| 90 |
+
except Exception as e:
|
| 91 |
+
self.result_text = "Processing..."
|
| 92 |
+
self.confidence = 0.0
|
| 93 |
+
self.prediction_counter = {}
|
| 94 |
+
else:
|
| 95 |
+
self.result_text = "No hand detected"
|
| 96 |
+
self.confidence = 0.0
|
| 97 |
+
self.prediction_counter = {}
|
| 98 |
+
|
| 99 |
+
# Draw landmarks if enabled
|
| 100 |
+
if show_landmarks and self.landmarks:
|
| 101 |
+
image = self.landmark_model.draw_landmarks(image, self.landmarks)
|
| 102 |
+
|
| 103 |
+
# Add text overlay
|
| 104 |
+
cv2.putText(
|
| 105 |
+
image,
|
| 106 |
+
f"{self.result_text} ({self.confidence:.2f})",
|
| 107 |
+
(20, 40),
|
| 108 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 109 |
+
1.0,
|
| 110 |
+
(0, 255, 0) if self.confidence > confidence_threshold else (0, 0, 255),
|
| 111 |
+
2
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
return VideoFrame.from_ndarray(image, format="bgr24")
|
| 115 |
+
|
| 116 |
+
# Create two columns
|
| 117 |
+
col1, col2 = st.columns([2, 1])
|
| 118 |
+
|
| 119 |
+
with col1:
|
| 120 |
+
# Webcam component
|
| 121 |
+
ctx = webrtc_streamer(
|
| 122 |
+
key="digit_recognition",
|
| 123 |
+
video_processor_factory=DigitInterpreter,
|
| 124 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 125 |
+
async_processing=True,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
with col2:
|
| 129 |
+
# Interpretation display
|
| 130 |
+
st.markdown("### 🔢 Current Digit")
|
| 131 |
+
if ctx.video_transformer:
|
| 132 |
+
# Large digit display
|
| 133 |
+
digit_display = st.empty()
|
| 134 |
+
confidence_meter = st.progress(0)
|
| 135 |
+
|
| 136 |
+
# Update interpretation and confidence
|
| 137 |
+
if ctx.video_transformer.last_prediction is not None and ctx.video_transformer.confidence > confidence_threshold:
|
| 138 |
+
digit_display.markdown(f"# {ctx.video_transformer.last_prediction}")
|
| 139 |
+
else:
|
| 140 |
+
digit_display.markdown("# -")
|
| 141 |
+
confidence_meter.progress(int(ctx.video_transformer.confidence * 100))
|
| 142 |
+
|
| 143 |
+
# Show confidence value
|
| 144 |
+
st.text(f"Confidence: {ctx.video_transformer.confidence:.2f}")
|
| 145 |
+
else:
|
| 146 |
+
st.warning("📷 Turn on your webcam to start!")
|
| 147 |
+
|
| 148 |
+
# Instructions
|
| 149 |
+
st.markdown("---")
|
| 150 |
+
st.markdown("""
|
| 151 |
+
### How to Use
|
| 152 |
+
1. Make sure you're in a well-lit environment
|
| 153 |
+
2. Position your hand clearly in the camera view
|
| 154 |
+
3. Make hand signs for digits 0-9
|
| 155 |
+
4. Hold each sign steady for accurate recognition
|
| 156 |
+
5. Adjust the confidence threshold if needed:
|
| 157 |
+
- Higher threshold = more accurate but less sensitive
|
| 158 |
+
- Lower threshold = more sensitive but may have false positives
|
| 159 |
+
""")
|
| 160 |
+
|
| 161 |
+
# Footer
|
| 162 |
+
st.markdown("---")
|
| 163 |
+
st.markdown("Made with ❤️ using MediaPipe, TensorFlow, and Streamlit")
|
app_enhanced.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import tensorflow as tf
|
| 5 |
+
from streamlit_webrtc import webrtc_streamer, VideoTransformerBase, RTCConfiguration
|
| 6 |
+
import av
|
| 7 |
+
import mediapipe as mp
|
| 8 |
+
import google.generativeai as genai
|
| 9 |
+
from collections import deque
|
| 10 |
+
import time
|
| 11 |
+
import json
|
| 12 |
+
|
| 13 |
+
# Configure page
|
| 14 |
+
st.set_page_config(
|
| 15 |
+
page_title="🤟 AI Sign Language Interpreter",
|
| 16 |
+
page_icon="🤟",
|
| 17 |
+
layout="wide",
|
| 18 |
+
initial_sidebar_state="expanded"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Load custom CSS
|
| 22 |
+
st.markdown("""
|
| 23 |
+
<style>
|
| 24 |
+
.main-header {
|
| 25 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| 26 |
+
padding: 2rem;
|
| 27 |
+
border-radius: 10px;
|
| 28 |
+
color: white;
|
| 29 |
+
text-align: center;
|
| 30 |
+
margin-bottom: 2rem;
|
| 31 |
+
}
|
| 32 |
+
.feature-card {
|
| 33 |
+
background: #ffffff;
|
| 34 |
+
border: 1px solid #e0e0e0;
|
| 35 |
+
padding: 1.5rem;
|
| 36 |
+
border-radius: 10px;
|
| 37 |
+
border-left: 4px solid #667eea;
|
| 38 |
+
margin: 1rem 0;
|
| 39 |
+
color: #1a1a1a;
|
| 40 |
+
font-weight: 600;
|
| 41 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 42 |
+
}
|
| 43 |
+
.prediction-box {
|
| 44 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 45 |
+
color: white;
|
| 46 |
+
padding: 1.5rem;
|
| 47 |
+
border-radius: 10px;
|
| 48 |
+
text-align: center;
|
| 49 |
+
font-size: 1.2rem;
|
| 50 |
+
margin: 1rem 0;
|
| 51 |
+
}
|
| 52 |
+
.confidence-meter {
|
| 53 |
+
background: #e9ecef;
|
| 54 |
+
border-radius: 10px;
|
| 55 |
+
padding: 0.5rem;
|
| 56 |
+
margin: 0.5rem 0;
|
| 57 |
+
}
|
| 58 |
+
.sentence-builder {
|
| 59 |
+
background: #ffffff;
|
| 60 |
+
border: 2px solid #e0e0e0;
|
| 61 |
+
border-radius: 10px;
|
| 62 |
+
padding: 1rem;
|
| 63 |
+
margin: 1rem 0;
|
| 64 |
+
min-height: 100px;
|
| 65 |
+
color: #1a1a1a;
|
| 66 |
+
font-weight: 600;
|
| 67 |
+
font-size: 1.1rem;
|
| 68 |
+
}
|
| 69 |
+
.control-panel {
|
| 70 |
+
background: #ffffff;
|
| 71 |
+
border: 2px solid #dee2e6;
|
| 72 |
+
border-radius: 10px;
|
| 73 |
+
padding: 1rem;
|
| 74 |
+
margin: 1rem 0;
|
| 75 |
+
color: #1a1a1a;
|
| 76 |
+
font-weight: 600;
|
| 77 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 78 |
+
}
|
| 79 |
+
</style>
|
| 80 |
+
""", unsafe_allow_html=True)
|
| 81 |
+
|
| 82 |
+
# Initialize Gemini AI
|
| 83 |
+
@st.cache_resource
|
| 84 |
+
def initialize_gemini():
|
| 85 |
+
try:
|
| 86 |
+
genai.configure(api_key=st.secrets["GEMINI_API_KEY"])
|
| 87 |
+
model = genai.GenerativeModel('gemini-pro')
|
| 88 |
+
return model
|
| 89 |
+
except Exception as e:
|
| 90 |
+
st.error(f"Failed to initialize Gemini AI: {e}")
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
# Load model and labels
|
| 94 |
+
@st.cache_resource
|
| 95 |
+
def load_model_and_labels():
|
| 96 |
+
try:
|
| 97 |
+
interpreter = tf.lite.Interpreter(model_path="models/model.tflite")
|
| 98 |
+
interpreter.allocate_tensors()
|
| 99 |
+
|
| 100 |
+
with open("models/labels.txt", "r") as f:
|
| 101 |
+
labels = [line.strip() for line in f.readlines()]
|
| 102 |
+
|
| 103 |
+
return interpreter, labels
|
| 104 |
+
except Exception as e:
|
| 105 |
+
st.error(f"Error loading model: {e}")
|
| 106 |
+
return None, []
|
| 107 |
+
|
| 108 |
+
# Initialize MediaPipe
|
| 109 |
+
@st.cache_resource
|
| 110 |
+
def initialize_mediapipe():
|
| 111 |
+
mp_hands = mp.solutions.hands
|
| 112 |
+
hands = mp_hands.Hands(
|
| 113 |
+
static_image_mode=False,
|
| 114 |
+
max_num_hands=2,
|
| 115 |
+
min_detection_confidence=0.7,
|
| 116 |
+
min_tracking_confidence=0.5
|
| 117 |
+
)
|
| 118 |
+
mp_drawing = mp.solutions.drawing_utils
|
| 119 |
+
return hands, mp_drawing, mp_hands
|
| 120 |
+
|
| 121 |
+
class EnhancedSignInterpreter(VideoTransformerBase):
|
| 122 |
+
def __init__(self, confidence_threshold=0.7, sequence_length=10):
|
| 123 |
+
self.interpreter, self.labels = load_model_and_labels()
|
| 124 |
+
self.hands, self.mp_drawing, self.mp_hands = initialize_mediapipe()
|
| 125 |
+
self.gemini_model = initialize_gemini()
|
| 126 |
+
|
| 127 |
+
# Enhanced features
|
| 128 |
+
self.confidence_threshold = confidence_threshold
|
| 129 |
+
self.sequence_length = sequence_length
|
| 130 |
+
self.gesture_sequence = deque(maxlen=sequence_length)
|
| 131 |
+
self.last_prediction = ""
|
| 132 |
+
self.last_confidence = 0.0
|
| 133 |
+
self.sentence_builder = []
|
| 134 |
+
self.last_gesture_time = time.time()
|
| 135 |
+
self.gesture_hold_time = 1.5 # seconds to hold gesture for confirmation
|
| 136 |
+
|
| 137 |
+
# Prediction smoothing
|
| 138 |
+
self.prediction_buffer = deque(maxlen=5)
|
| 139 |
+
|
| 140 |
+
def extract_landmarks(self, image):
|
| 141 |
+
"""Extract hand landmarks using MediaPipe"""
|
| 142 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 143 |
+
results = self.hands.process(image_rgb)
|
| 144 |
+
|
| 145 |
+
if results.multi_hand_landmarks:
|
| 146 |
+
landmarks = []
|
| 147 |
+
for hand_landmarks in results.multi_hand_landmarks:
|
| 148 |
+
for landmark in hand_landmarks.landmark:
|
| 149 |
+
landmarks.extend([landmark.x, landmark.y, landmark.z])
|
| 150 |
+
|
| 151 |
+
# Pad or truncate to 63 features (21 landmarks * 3 coordinates)
|
| 152 |
+
if len(landmarks) < 63:
|
| 153 |
+
landmarks.extend([0.0] * (63 - len(landmarks)))
|
| 154 |
+
else:
|
| 155 |
+
landmarks = landmarks[:63]
|
| 156 |
+
|
| 157 |
+
return np.array(landmarks, dtype=np.float32)
|
| 158 |
+
return None
|
| 159 |
+
|
| 160 |
+
def predict_gesture(self, landmarks):
|
| 161 |
+
"""Predict gesture from landmarks"""
|
| 162 |
+
if self.interpreter is None or landmarks is None:
|
| 163 |
+
return "No gesture", 0.0
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
input_details = self.interpreter.get_input_details()
|
| 167 |
+
output_details = self.interpreter.get_output_details()
|
| 168 |
+
|
| 169 |
+
# Reshape landmarks for model input
|
| 170 |
+
input_data = landmarks.reshape(1, -1)
|
| 171 |
+
|
| 172 |
+
self.interpreter.set_tensor(input_details[0]['index'], input_data)
|
| 173 |
+
self.interpreter.invoke()
|
| 174 |
+
|
| 175 |
+
output_data = self.interpreter.get_tensor(output_details[0]['index'])
|
| 176 |
+
predicted_class = np.argmax(output_data)
|
| 177 |
+
confidence = float(np.max(output_data))
|
| 178 |
+
|
| 179 |
+
if predicted_class < len(self.labels):
|
| 180 |
+
return self.labels[predicted_class], confidence
|
| 181 |
+
else:
|
| 182 |
+
return "Unknown", confidence
|
| 183 |
+
|
| 184 |
+
except Exception as e:
|
| 185 |
+
return f"Error: {str(e)}", 0.0
|
| 186 |
+
|
| 187 |
+
def smooth_prediction(self, prediction, confidence):
|
| 188 |
+
"""Smooth predictions using buffer"""
|
| 189 |
+
self.prediction_buffer.append((prediction, confidence))
|
| 190 |
+
|
| 191 |
+
# Get most common prediction in buffer
|
| 192 |
+
predictions = [p[0] for p in self.prediction_buffer]
|
| 193 |
+
confidences = [p[1] for p in self.prediction_buffer]
|
| 194 |
+
|
| 195 |
+
# Find most frequent prediction
|
| 196 |
+
unique_predictions = list(set(predictions))
|
| 197 |
+
if unique_predictions:
|
| 198 |
+
most_common = max(unique_predictions, key=predictions.count)
|
| 199 |
+
avg_confidence = np.mean([c for p, c in self.prediction_buffer if p == most_common])
|
| 200 |
+
return most_common, avg_confidence
|
| 201 |
+
|
| 202 |
+
return prediction, confidence
|
| 203 |
+
|
| 204 |
+
def update_sentence_builder(self, gesture, confidence):
|
| 205 |
+
"""Update sentence builder with confirmed gestures"""
|
| 206 |
+
current_time = time.time()
|
| 207 |
+
cooldown_period = 0.5 # Time to wait before accepting new gestures
|
| 208 |
+
|
| 209 |
+
# Check if the gesture is valid and meets confidence threshold
|
| 210 |
+
is_valid_gesture = (gesture not in ["No gesture", "Unknown"] and
|
| 211 |
+
confidence > self.confidence_threshold)
|
| 212 |
+
|
| 213 |
+
# Time elapsed since last gesture
|
| 214 |
+
time_elapsed = current_time - self.last_gesture_time
|
| 215 |
+
|
| 216 |
+
if is_valid_gesture:
|
| 217 |
+
# Case 1: New gesture detected
|
| 218 |
+
if gesture != self.last_prediction:
|
| 219 |
+
self.last_prediction = gesture
|
| 220 |
+
self.last_confidence = confidence
|
| 221 |
+
self.last_gesture_time = current_time
|
| 222 |
+
|
| 223 |
+
# Case 2: Gesture held long enough and cooldown period passed
|
| 224 |
+
elif (time_elapsed > self.gesture_hold_time and
|
| 225 |
+
(not self.sentence_builder or
|
| 226 |
+
time_elapsed > self.gesture_hold_time + cooldown_period)):
|
| 227 |
+
if not self.sentence_builder or self.sentence_builder[-1] != gesture:
|
| 228 |
+
self.sentence_builder.append(gesture)
|
| 229 |
+
self.last_gesture_time = current_time
|
| 230 |
+
self.last_prediction = "" # Reset prediction to allow new gestures
|
| 231 |
+
else:
|
| 232 |
+
# Reset prediction if gesture is invalid or confidence is low
|
| 233 |
+
if time_elapsed > cooldown_period:
|
| 234 |
+
self.last_prediction = ""
|
| 235 |
+
|
| 236 |
+
def generate_ai_interpretation(self, gestures):
|
| 237 |
+
"""Generate AI interpretation using Gemini"""
|
| 238 |
+
if not gestures or not self.gemini_model:
|
| 239 |
+
return "No interpretation available"
|
| 240 |
+
|
| 241 |
+
try:
|
| 242 |
+
gesture_text = " ".join(gestures)
|
| 243 |
+
prompt = f"""
|
| 244 |
+
Convert these sign language gestures into a natural, meaningful sentence: {gesture_text}
|
| 245 |
+
|
| 246 |
+
Guidelines:
|
| 247 |
+
- Form complete, grammatically correct sentences
|
| 248 |
+
- Add appropriate articles, prepositions, and conjunctions
|
| 249 |
+
- Consider context and common sign language patterns
|
| 250 |
+
- If it's a sequence of letters, try to form words
|
| 251 |
+
- If it's numbers, consider if they represent quantities, dates, or codes
|
| 252 |
+
- Make the output conversational and natural
|
| 253 |
+
|
| 254 |
+
Gestures: {gesture_text}
|
| 255 |
+
|
| 256 |
+
Natural interpretation:
|
| 257 |
+
"""
|
| 258 |
+
|
| 259 |
+
response = self.gemini_model.generate_content(prompt)
|
| 260 |
+
return response.text.strip()
|
| 261 |
+
except Exception as e:
|
| 262 |
+
return f"AI interpretation error: {str(e)}"
|
| 263 |
+
|
| 264 |
+
def recv(self, frame):
|
| 265 |
+
image = frame.to_ndarray(format="bgr24")
|
| 266 |
+
|
| 267 |
+
# Extract landmarks
|
| 268 |
+
landmarks = self.extract_landmarks(image)
|
| 269 |
+
|
| 270 |
+
if landmarks is not None:
|
| 271 |
+
# Predict gesture
|
| 272 |
+
gesture, confidence = self.predict_gesture(landmarks)
|
| 273 |
+
|
| 274 |
+
# Smooth prediction
|
| 275 |
+
gesture, confidence = self.smooth_prediction(gesture, confidence)
|
| 276 |
+
|
| 277 |
+
# Update sentence builder
|
| 278 |
+
self.update_sentence_builder(gesture, confidence)
|
| 279 |
+
|
| 280 |
+
# Draw hand landmarks
|
| 281 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 282 |
+
results = self.hands.process(image_rgb)
|
| 283 |
+
|
| 284 |
+
if results.multi_hand_landmarks:
|
| 285 |
+
for hand_landmarks in results.multi_hand_landmarks:
|
| 286 |
+
self.mp_drawing.draw_landmarks(
|
| 287 |
+
image, hand_landmarks, self.mp_hands.HAND_CONNECTIONS)
|
| 288 |
+
|
| 289 |
+
# Draw prediction info
|
| 290 |
+
color = (0, 255, 0) if confidence > self.confidence_threshold else (0, 165, 255)
|
| 291 |
+
cv2.putText(image, f"{gesture} ({confidence:.2f})",
|
| 292 |
+
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2)
|
| 293 |
+
|
| 294 |
+
# Store current prediction for UI
|
| 295 |
+
st.session_state.current_gesture = gesture
|
| 296 |
+
st.session_state.current_confidence = confidence
|
| 297 |
+
st.session_state.sentence_builder = self.sentence_builder.copy()
|
| 298 |
+
else:
|
| 299 |
+
st.session_state.current_gesture = "No hand detected"
|
| 300 |
+
st.session_state.current_confidence = 0.0
|
| 301 |
+
|
| 302 |
+
return av.VideoFrame.from_ndarray(image, format="bgr24")
|
| 303 |
+
|
| 304 |
+
def main():
|
| 305 |
+
# Header
|
| 306 |
+
st.markdown("""
|
| 307 |
+
<div class="main-header">
|
| 308 |
+
<h1>🤟 Enhanced AI Sign Language Interpreter</h1>
|
| 309 |
+
<p>Advanced real-time interpretation with AI-powered sentence formation</p>
|
| 310 |
+
</div>
|
| 311 |
+
""", unsafe_allow_html=True)
|
| 312 |
+
|
| 313 |
+
# Initialize session state
|
| 314 |
+
if 'current_gesture' not in st.session_state:
|
| 315 |
+
st.session_state.current_gesture = "Ready to start"
|
| 316 |
+
st.session_state.current_confidence = 0.0
|
| 317 |
+
st.session_state.sentence_builder = []
|
| 318 |
+
st.session_state.ai_interpretation = ""
|
| 319 |
+
|
| 320 |
+
# Sidebar controls
|
| 321 |
+
with st.sidebar:
|
| 322 |
+
st.markdown("### 🎛️ Control Panel")
|
| 323 |
+
|
| 324 |
+
confidence_threshold = st.slider(
|
| 325 |
+
"Confidence Threshold",
|
| 326 |
+
min_value=0.1,
|
| 327 |
+
max_value=1.0,
|
| 328 |
+
value=0.7,
|
| 329 |
+
step=0.1,
|
| 330 |
+
help="Minimum confidence required for gesture recognition"
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
sequence_length = st.slider(
|
| 334 |
+
"Sequence Memory",
|
| 335 |
+
min_value=5,
|
| 336 |
+
max_value=20,
|
| 337 |
+
value=10,
|
| 338 |
+
help="Number of gestures to remember for sentence building"
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
gesture_hold_time = st.slider(
|
| 342 |
+
"Gesture Hold Time (seconds)",
|
| 343 |
+
min_value=0.5,
|
| 344 |
+
max_value=3.0,
|
| 345 |
+
value=1.5,
|
| 346 |
+
step=0.1,
|
| 347 |
+
help="Time to hold gesture for confirmation"
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
st.markdown("### 🎯 Quick Actions")
|
| 351 |
+
if st.button("🗑️ Clear Sentence", use_container_width=True):
|
| 352 |
+
st.session_state.sentence_builder = []
|
| 353 |
+
st.session_state.ai_interpretation = ""
|
| 354 |
+
|
| 355 |
+
if st.button("🤖 Generate AI Interpretation", use_container_width=True):
|
| 356 |
+
if st.session_state.sentence_builder:
|
| 357 |
+
interpreter = EnhancedSignInterpreter()
|
| 358 |
+
st.session_state.ai_interpretation = interpreter.generate_ai_interpretation(
|
| 359 |
+
st.session_state.sentence_builder
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
# Display supported gestures
|
| 363 |
+
st.markdown("### 📋 Supported Gestures")
|
| 364 |
+
st.markdown("**Numbers:** 1, 2, 4, 5, 6, 7, 8")
|
| 365 |
+
st.markdown("**Letters:** A-X (except E)")
|
| 366 |
+
|
| 367 |
+
# Main content
|
| 368 |
+
col1, col2 = st.columns([2, 1])
|
| 369 |
+
|
| 370 |
+
with col1:
|
| 371 |
+
st.markdown("### 📹 Live Camera Feed")
|
| 372 |
+
|
| 373 |
+
# WebRTC configuration
|
| 374 |
+
rtc_configuration = RTCConfiguration({
|
| 375 |
+
"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
|
| 376 |
+
})
|
| 377 |
+
|
| 378 |
+
# Video streamer
|
| 379 |
+
webrtc_ctx = webrtc_streamer(
|
| 380 |
+
key="enhanced-sign-interpreter",
|
| 381 |
+
video_transformer_factory=lambda: EnhancedSignInterpreter(
|
| 382 |
+
confidence_threshold=confidence_threshold,
|
| 383 |
+
sequence_length=sequence_length
|
| 384 |
+
),
|
| 385 |
+
rtc_configuration=rtc_configuration,
|
| 386 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 387 |
+
async_processing=True,
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
with col2:
|
| 391 |
+
st.markdown("### 🎯 Recognition Results")
|
| 392 |
+
|
| 393 |
+
# Current prediction
|
| 394 |
+
st.markdown(f"""
|
| 395 |
+
<div class="prediction-box">
|
| 396 |
+
<h3>{st.session_state.current_gesture}</h3>
|
| 397 |
+
<div class="confidence-meter">
|
| 398 |
+
<div style="background: {'#28a745' if st.session_state.current_confidence > confidence_threshold else '#ffc107'};
|
| 399 |
+
width: {st.session_state.current_confidence * 100}%;
|
| 400 |
+
height: 20px;
|
| 401 |
+
border-radius: 10px;
|
| 402 |
+
transition: all 0.3s ease;">
|
| 403 |
+
</div>
|
| 404 |
+
<small>Confidence: {st.session_state.current_confidence:.1%}</small>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
""", unsafe_allow_html=True)
|
| 408 |
+
|
| 409 |
+
# Sentence builder
|
| 410 |
+
st.markdown("### 📝 Sentence Builder")
|
| 411 |
+
sentence_text = " → ".join(st.session_state.sentence_builder) if st.session_state.sentence_builder else "Start making gestures..."
|
| 412 |
+
|
| 413 |
+
st.markdown(f"""
|
| 414 |
+
<div class="sentence-builder">
|
| 415 |
+
<strong>Gestures:</strong><br>
|
| 416 |
+
{sentence_text}
|
| 417 |
+
</div>
|
| 418 |
+
""", unsafe_allow_html=True)
|
| 419 |
+
|
| 420 |
+
# AI Interpretation
|
| 421 |
+
if st.session_state.ai_interpretation:
|
| 422 |
+
st.markdown("### 🤖 AI Interpretation")
|
| 423 |
+
st.markdown(f"""
|
| 424 |
+
<div class="feature-card">
|
| 425 |
+
<strong>Natural Language:</strong><br>
|
| 426 |
+
{st.session_state.ai_interpretation}
|
| 427 |
+
</div>
|
| 428 |
+
""", unsafe_allow_html=True)
|
| 429 |
+
|
| 430 |
+
# Statistics and tips
|
| 431 |
+
st.markdown("---")
|
| 432 |
+
col1, col2, col3 = st.columns(3)
|
| 433 |
+
|
| 434 |
+
with col1:
|
| 435 |
+
st.markdown("""
|
| 436 |
+
<div class="feature-card">
|
| 437 |
+
<h4>💡 Tips for Better Recognition</h4>
|
| 438 |
+
<ul>
|
| 439 |
+
<li>Ensure good lighting</li>
|
| 440 |
+
<li>Keep hand centered in frame</li>
|
| 441 |
+
<li>Hold gestures steady</li>
|
| 442 |
+
<li>Avoid cluttered backgrounds</li>
|
| 443 |
+
</ul>
|
| 444 |
+
</div>
|
| 445 |
+
""", unsafe_allow_html=True)
|
| 446 |
+
|
| 447 |
+
with col2:
|
| 448 |
+
st.markdown(f"""
|
| 449 |
+
<div class="feature-card">
|
| 450 |
+
<h4>📊 Session Statistics</h4>
|
| 451 |
+
<ul>
|
| 452 |
+
<li>Gestures Captured: {len(st.session_state.sentence_builder)}</li>
|
| 453 |
+
<li>Current Confidence: {st.session_state.current_confidence:.1%}</li>
|
| 454 |
+
<li>Threshold: {confidence_threshold:.1%}</li>
|
| 455 |
+
</ul>
|
| 456 |
+
</div>
|
| 457 |
+
""", unsafe_allow_html=True)
|
| 458 |
+
|
| 459 |
+
with col3:
|
| 460 |
+
st.markdown("""
|
| 461 |
+
<div class="feature-card">
|
| 462 |
+
<h4>🚀 Enhanced Features</h4>
|
| 463 |
+
<ul>
|
| 464 |
+
<li>Confidence threshold control</li>
|
| 465 |
+
<li>Gesture sequence memory</li>
|
| 466 |
+
<li>AI sentence formation</li>
|
| 467 |
+
<li>Real-time smoothing</li>
|
| 468 |
+
</ul>
|
| 469 |
+
</div>
|
| 470 |
+
""", unsafe_allow_html=True)
|
| 471 |
+
|
| 472 |
+
if __name__ == "__main__":
|
| 473 |
+
main()
|
app_new.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from streamlit_webrtc import webrtc_streamer, VideoTransformerBase, RTCConfiguration
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
import mediapipe as mp
|
| 6 |
+
import tensorflow as tf
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
import google.generativeai as genai
|
| 10 |
+
from av import VideoFrame
|
| 11 |
+
|
| 12 |
+
# Page config
|
| 13 |
+
st.set_page_config(
|
| 14 |
+
page_title="AI Sign Language Interpreter",
|
| 15 |
+
page_icon="🤟",
|
| 16 |
+
layout="wide",
|
| 17 |
+
initial_sidebar_state="expanded"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Custom CSS
|
| 21 |
+
st.markdown("""
|
| 22 |
+
<style>
|
| 23 |
+
.main {
|
| 24 |
+
background-color: #f8f9fa;
|
| 25 |
+
}
|
| 26 |
+
.stApp {
|
| 27 |
+
max-width: 1200px;
|
| 28 |
+
margin: 0 auto;
|
| 29 |
+
}
|
| 30 |
+
.prediction-box {
|
| 31 |
+
background-color: white;
|
| 32 |
+
padding: 2rem;
|
| 33 |
+
border-radius: 10px;
|
| 34 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 35 |
+
margin-bottom: 1rem;
|
| 36 |
+
}
|
| 37 |
+
.interpretation-box {
|
| 38 |
+
background-color: #f0f7ff;
|
| 39 |
+
padding: 2rem;
|
| 40 |
+
border-radius: 10px;
|
| 41 |
+
border: 1px solid #cce5ff;
|
| 42 |
+
margin-bottom: 1rem;
|
| 43 |
+
}
|
| 44 |
+
.stButton>button {
|
| 45 |
+
background-color: #4c51bf;
|
| 46 |
+
color: white;
|
| 47 |
+
font-weight: bold;
|
| 48 |
+
}
|
| 49 |
+
.stButton>button:hover {
|
| 50 |
+
background-color: #434190;
|
| 51 |
+
}
|
| 52 |
+
</style>
|
| 53 |
+
""", unsafe_allow_html=True)
|
| 54 |
+
|
| 55 |
+
# Initialize Gemini AI
|
| 56 |
+
@st.cache_resource
|
| 57 |
+
def initialize_gemini():
|
| 58 |
+
try:
|
| 59 |
+
genai.configure(api_key=st.secrets["GEMINI_API_KEY"])
|
| 60 |
+
model = genai.GenerativeModel('gemini-pro')
|
| 61 |
+
return model
|
| 62 |
+
except Exception as e:
|
| 63 |
+
st.error(f"Error initializing Gemini AI: {e}")
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
# RTC Configuration
|
| 67 |
+
RTC_CONFIGURATION = RTCConfiguration({
|
| 68 |
+
"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
# Global variables for thread-safe prediction sharing
|
| 72 |
+
prediction_lock = threading.Lock()
|
| 73 |
+
current_prediction = {
|
| 74 |
+
"gesture": "Show your hand",
|
| 75 |
+
"confidence": 0.0,
|
| 76 |
+
"interpretation": ""
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
# Load model and labels
|
| 80 |
+
@st.cache_resource
|
| 81 |
+
def load_model():
|
| 82 |
+
try:
|
| 83 |
+
interpreter = tf.lite.Interpreter(model_path="models/model.tflite")
|
| 84 |
+
interpreter.allocate_tensors()
|
| 85 |
+
|
| 86 |
+
with open("models/labels.txt", 'r') as f:
|
| 87 |
+
labels = [line.strip() for line in f.readlines()]
|
| 88 |
+
|
| 89 |
+
return interpreter, labels
|
| 90 |
+
except Exception as e:
|
| 91 |
+
st.error(f"Error loading model: {e}")
|
| 92 |
+
return None, []
|
| 93 |
+
|
| 94 |
+
interpreter, labels = load_model()
|
| 95 |
+
gemini_model = initialize_gemini()
|
| 96 |
+
|
| 97 |
+
# MediaPipe setup
|
| 98 |
+
@st.cache_resource
|
| 99 |
+
def get_mediapipe_hands():
|
| 100 |
+
mp_hands = mp.solutions.hands
|
| 101 |
+
mp_drawing = mp.solutions.drawing_utils
|
| 102 |
+
hands = mp_hands.Hands(
|
| 103 |
+
static_image_mode=False,
|
| 104 |
+
max_num_hands=1,
|
| 105 |
+
min_detection_confidence=0.7,
|
| 106 |
+
min_tracking_confidence=0.5
|
| 107 |
+
)
|
| 108 |
+
return mp_hands, mp_drawing, hands
|
| 109 |
+
|
| 110 |
+
mp_hands, mp_drawing, hands = get_mediapipe_hands()
|
| 111 |
+
|
| 112 |
+
def extract_hand_features(landmarks):
|
| 113 |
+
"""Extract features from hand landmarks."""
|
| 114 |
+
try:
|
| 115 |
+
landmarks_array = np.array([(lm.x, lm.y, lm.z) for lm in landmarks.landmark])
|
| 116 |
+
if len(landmarks_array) != 21:
|
| 117 |
+
return np.zeros(63)
|
| 118 |
+
|
| 119 |
+
wrist = landmarks_array[0]
|
| 120 |
+
normalized = landmarks_array - wrist
|
| 121 |
+
hand_size = np.linalg.norm(landmarks_array[12] - wrist)
|
| 122 |
+
|
| 123 |
+
if hand_size > 0:
|
| 124 |
+
normalized = normalized / hand_size
|
| 125 |
+
|
| 126 |
+
features = normalized.flatten()
|
| 127 |
+
if len(features) != 63:
|
| 128 |
+
features = np.pad(features, (0, max(0, 63 - len(features))))[:63]
|
| 129 |
+
|
| 130 |
+
return features
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"Error extracting features: {e}")
|
| 133 |
+
return np.zeros(63)
|
| 134 |
+
|
| 135 |
+
def predict_gesture(features):
|
| 136 |
+
"""Predict gesture from features."""
|
| 137 |
+
if interpreter is None:
|
| 138 |
+
return "Model not loaded", 0.0
|
| 139 |
+
|
| 140 |
+
try:
|
| 141 |
+
input_data = np.expand_dims(features, axis=0).astype(np.float32)
|
| 142 |
+
input_details = interpreter.get_input_details()
|
| 143 |
+
output_details = interpreter.get_output_details()
|
| 144 |
+
|
| 145 |
+
interpreter.set_tensor(input_details[0]['index'], input_data)
|
| 146 |
+
interpreter.invoke()
|
| 147 |
+
|
| 148 |
+
output = interpreter.get_tensor(output_details[0]['index'])
|
| 149 |
+
predicted_class = np.argmax(output[0])
|
| 150 |
+
|
| 151 |
+
exp_output = np.exp(output[0] - np.max(output[0]))
|
| 152 |
+
softmax_output = exp_output / np.sum(exp_output)
|
| 153 |
+
confidence = float(softmax_output[predicted_class])
|
| 154 |
+
|
| 155 |
+
gesture = labels[predicted_class] if predicted_class < len(labels) else str(predicted_class)
|
| 156 |
+
return gesture, confidence
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"Prediction error: {e}")
|
| 159 |
+
return "Error", 0.0
|
| 160 |
+
|
| 161 |
+
def get_gesture_interpretation(gesture_sequence):
|
| 162 |
+
"""Get interpretation from Gemini AI."""
|
| 163 |
+
if not gemini_model or not gesture_sequence:
|
| 164 |
+
return ""
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
prompt = f"""
|
| 168 |
+
As a sign language interpreter, analyze this sequence of gestures and provide a natural interpretation:
|
| 169 |
+
Gestures detected: {', '.join(gesture_sequence)}
|
| 170 |
+
|
| 171 |
+
Provide a clear and concise interpretation. If the gestures form a word or phrase, explain its meaning.
|
| 172 |
+
If they're individual letters/numbers, format them clearly.
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
response = gemini_model.generate_content(prompt)
|
| 176 |
+
return response.text
|
| 177 |
+
except Exception as e:
|
| 178 |
+
print(f"Gemini AI error: {e}")
|
| 179 |
+
return ""
|
| 180 |
+
|
| 181 |
+
class GestureRecognizer(VideoTransformerBase):
|
| 182 |
+
def __init__(self):
|
| 183 |
+
self.frame_count = 0
|
| 184 |
+
self.last_prediction_time = 0
|
| 185 |
+
self.prediction_interval = 0.2
|
| 186 |
+
self.gesture_history = []
|
| 187 |
+
self.last_interpretation_time = 0
|
| 188 |
+
self.interpretation_interval = 2.0
|
| 189 |
+
|
| 190 |
+
def recv(self, frame: VideoFrame) -> VideoFrame:
|
| 191 |
+
image = frame.to_ndarray(format="bgr24")
|
| 192 |
+
self.frame_count += 1
|
| 193 |
+
current_time = time.time()
|
| 194 |
+
|
| 195 |
+
if current_time - self.last_prediction_time > self.prediction_interval:
|
| 196 |
+
self.last_prediction_time = current_time
|
| 197 |
+
|
| 198 |
+
try:
|
| 199 |
+
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 200 |
+
results = hands.process(rgb_image)
|
| 201 |
+
|
| 202 |
+
if results.multi_hand_landmarks:
|
| 203 |
+
hand_landmarks = results.multi_hand_landmarks[0]
|
| 204 |
+
|
| 205 |
+
mp_drawing.draw_landmarks(
|
| 206 |
+
image, hand_landmarks, mp_hands.HAND_CONNECTIONS,
|
| 207 |
+
mp_drawing.DrawingSpec(color=(0, 0, 255), thickness=2, circle_radius=2),
|
| 208 |
+
mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2)
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
features = extract_hand_features(hand_landmarks)
|
| 212 |
+
gesture, confidence = predict_gesture(features)
|
| 213 |
+
|
| 214 |
+
if confidence > 0.5:
|
| 215 |
+
self.gesture_history.append(gesture)
|
| 216 |
+
self.gesture_history = self.gesture_history[-10:] # Keep last 10 gestures
|
| 217 |
+
|
| 218 |
+
with prediction_lock:
|
| 219 |
+
current_prediction["gesture"] = gesture
|
| 220 |
+
current_prediction["confidence"] = confidence
|
| 221 |
+
|
| 222 |
+
# Update interpretation periodically
|
| 223 |
+
if current_time - self.last_interpretation_time > self.interpretation_interval:
|
| 224 |
+
self.last_interpretation_time = current_time
|
| 225 |
+
interpretation = get_gesture_interpretation(self.gesture_history)
|
| 226 |
+
if interpretation:
|
| 227 |
+
current_prediction["interpretation"] = interpretation
|
| 228 |
+
else:
|
| 229 |
+
with prediction_lock:
|
| 230 |
+
current_prediction["gesture"] = "No hand detected"
|
| 231 |
+
current_prediction["confidence"] = 0.0
|
| 232 |
+
except Exception as e:
|
| 233 |
+
print(f"Processing error: {e}")
|
| 234 |
+
with prediction_lock:
|
| 235 |
+
current_prediction["gesture"] = "Processing error"
|
| 236 |
+
current_prediction["confidence"] = 0.0
|
| 237 |
+
|
| 238 |
+
# Add text overlay
|
| 239 |
+
with prediction_lock:
|
| 240 |
+
display_text = current_prediction["gesture"]
|
| 241 |
+
confidence = current_prediction["confidence"]
|
| 242 |
+
|
| 243 |
+
color = (0, 255, 0) if confidence > 0.5 else (0, 0, 255)
|
| 244 |
+
cv2.putText(
|
| 245 |
+
image,
|
| 246 |
+
f"{display_text} ({confidence:.2f})",
|
| 247 |
+
(20, 40),
|
| 248 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 249 |
+
1.0,
|
| 250 |
+
color,
|
| 251 |
+
2
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
return VideoFrame.from_ndarray(image, format="bgr24")
|
| 255 |
+
|
| 256 |
+
def main():
|
| 257 |
+
st.title("🤟 AI Sign Language Interpreter")
|
| 258 |
+
st.markdown("Transform sign language into text with real-time AI interpretation")
|
| 259 |
+
|
| 260 |
+
# Create columns for layout
|
| 261 |
+
col1, col2 = st.columns([2, 1])
|
| 262 |
+
|
| 263 |
+
with col1:
|
| 264 |
+
st.markdown("### 📹 Live Camera Feed")
|
| 265 |
+
ctx = webrtc_streamer(
|
| 266 |
+
key="gesture_recognition",
|
| 267 |
+
video_processor_factory=GestureRecognizer,
|
| 268 |
+
rtc_configuration=RTC_CONFIGURATION,
|
| 269 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 270 |
+
async_processing=True,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
with col2:
|
| 274 |
+
st.markdown("### 🎯 Recognition Results")
|
| 275 |
+
|
| 276 |
+
# Create placeholders for dynamic updates
|
| 277 |
+
prediction_placeholder = st.empty()
|
| 278 |
+
confidence_bar = st.empty()
|
| 279 |
+
interpretation_placeholder = st.empty()
|
| 280 |
+
|
| 281 |
+
if ctx.state.playing:
|
| 282 |
+
while ctx.state.playing:
|
| 283 |
+
with prediction_lock:
|
| 284 |
+
gesture = current_prediction["gesture"]
|
| 285 |
+
confidence = current_prediction["confidence"]
|
| 286 |
+
interpretation = current_prediction["interpretation"]
|
| 287 |
+
|
| 288 |
+
# Display current gesture
|
| 289 |
+
with prediction_placeholder.container():
|
| 290 |
+
st.markdown("""
|
| 291 |
+
<div class="prediction-box">
|
| 292 |
+
<h3 style="font-size: 1.5rem; margin-bottom: 0.5rem;">Current Gesture</h3>
|
| 293 |
+
<p style="font-size: 2rem; font-weight: bold; color: #4c51bf;">{}</p>
|
| 294 |
+
</div>
|
| 295 |
+
""".format(gesture), unsafe_allow_html=True)
|
| 296 |
+
|
| 297 |
+
# Display confidence bar
|
| 298 |
+
if confidence > 0.5 and gesture not in ["No hand detected", "Processing error", "Error"]:
|
| 299 |
+
confidence_bar.progress(confidence)
|
| 300 |
+
|
| 301 |
+
# Display interpretation
|
| 302 |
+
if interpretation:
|
| 303 |
+
with interpretation_placeholder.container():
|
| 304 |
+
st.markdown("""
|
| 305 |
+
<div class="interpretation-box">
|
| 306 |
+
<h3 style="font-size: 1.5rem; margin-bottom: 0.5rem;">AI Interpretation</h3>
|
| 307 |
+
<p style="font-size: 1.2rem;">{}</p>
|
| 308 |
+
</div>
|
| 309 |
+
""".format(interpretation), unsafe_allow_html=True)
|
| 310 |
+
else:
|
| 311 |
+
confidence_bar.progress(0)
|
| 312 |
+
if gesture == "No hand detected":
|
| 313 |
+
interpretation_placeholder.info("Show your hand to begin")
|
| 314 |
+
else:
|
| 315 |
+
interpretation_placeholder.warning("Gesture not recognized clearly")
|
| 316 |
+
|
| 317 |
+
time.sleep(0.1)
|
| 318 |
+
else:
|
| 319 |
+
st.warning("📷 Turn on your webcam to start!")
|
| 320 |
+
|
| 321 |
+
# Instructions and supported gestures
|
| 322 |
+
st.markdown("---")
|
| 323 |
+
|
| 324 |
+
# Display supported gestures in an organized grid
|
| 325 |
+
st.markdown("### 🎯 Supported Gestures")
|
| 326 |
+
|
| 327 |
+
# Numbers
|
| 328 |
+
st.markdown("#### 🔢 Numbers")
|
| 329 |
+
number_labels = [label for label in labels if label.isdigit()]
|
| 330 |
+
if number_labels:
|
| 331 |
+
cols = st.columns(len(number_labels))
|
| 332 |
+
for i, label in enumerate(number_labels):
|
| 333 |
+
with cols[i]:
|
| 334 |
+
st.markdown(f"""
|
| 335 |
+
<div style="text-align: center; padding: 1rem; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
| 336 |
+
<h3 style="font-size: 1.5rem; color: #4c51bf;">{label}</h3>
|
| 337 |
+
</div>
|
| 338 |
+
""", unsafe_allow_html=True)
|
| 339 |
+
|
| 340 |
+
# Letters
|
| 341 |
+
st.markdown("#### 🔤 Letters")
|
| 342 |
+
letter_labels = [label for label in labels if label.isalpha()]
|
| 343 |
+
if letter_labels:
|
| 344 |
+
# Display letters in rows of 8
|
| 345 |
+
for i in range(0, len(letter_labels), 8):
|
| 346 |
+
row_labels = letter_labels[i:i+8]
|
| 347 |
+
cols = st.columns(len(row_labels))
|
| 348 |
+
for j, label in enumerate(row_labels):
|
| 349 |
+
with cols[j]:
|
| 350 |
+
st.markdown(f"""
|
| 351 |
+
<div style="text-align: center; padding: 1rem; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
| 352 |
+
<h3 style="font-size: 1.5rem; color: #4c51bf;">{label}</h3>
|
| 353 |
+
</div>
|
| 354 |
+
""", unsafe_allow_html=True)
|
| 355 |
+
|
| 356 |
+
if __name__ == "__main__":
|
| 357 |
+
main()
|
create_sample_model.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
import numpy as np
|
| 3 |
+
from utils.model_utils import create_model, convert_to_tflite
|
| 4 |
+
|
| 5 |
+
def create_sample_model():
|
| 6 |
+
"""Create a sample model for demonstration purposes."""
|
| 7 |
+
print("Creating sample sign language model...")
|
| 8 |
+
|
| 9 |
+
# Model parameters
|
| 10 |
+
num_classes = 10 # Number of signs in labels.txt
|
| 11 |
+
sequence_length = 30
|
| 12 |
+
num_features = 126 # 63 landmarks + 63 velocity features
|
| 13 |
+
|
| 14 |
+
# Create model
|
| 15 |
+
model = create_model(num_classes, sequence_length, num_features)
|
| 16 |
+
|
| 17 |
+
# Generate dummy training data
|
| 18 |
+
X_train = np.random.random((1000, sequence_length, num_features)).astype(np.float32)
|
| 19 |
+
y_train = np.random.randint(0, num_classes, (1000,))
|
| 20 |
+
|
| 21 |
+
# Generate dummy validation data
|
| 22 |
+
X_val = np.random.random((200, sequence_length, num_features)).astype(np.float32)
|
| 23 |
+
y_val = np.random.randint(0, num_classes, (200,))
|
| 24 |
+
|
| 25 |
+
print("Training sample model...")
|
| 26 |
+
# Train for a few epochs
|
| 27 |
+
history = model.fit(
|
| 28 |
+
X_train, y_train,
|
| 29 |
+
validation_data=(X_val, y_val),
|
| 30 |
+
epochs=5,
|
| 31 |
+
batch_size=32,
|
| 32 |
+
verbose=1
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Save the model
|
| 36 |
+
model.save('models/sign_language_model.h5')
|
| 37 |
+
print("Model saved as 'models/sign_language_model.h5'")
|
| 38 |
+
|
| 39 |
+
# Convert to TFLite
|
| 40 |
+
print("Converting to TensorFlow Lite...")
|
| 41 |
+
converter = tf.lite.TFLiteConverter.from_keras_model(model)
|
| 42 |
+
converter.optimizations = [tf.lite.Optimize.DEFAULT]
|
| 43 |
+
tflite_model = converter.convert()
|
| 44 |
+
|
| 45 |
+
with open('models/model.tflite', 'wb') as f:
|
| 46 |
+
f.write(tflite_model)
|
| 47 |
+
|
| 48 |
+
print("TFLite model saved as 'models/model.tflite'")
|
| 49 |
+
print("Sample model creation completed!")
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
create_sample_model()
|
create_simple_model.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def create_simple_tflite_model():
|
| 5 |
+
"""Create a simple dummy TFLite model for testing."""
|
| 6 |
+
|
| 7 |
+
# Create a minimal TFLite model structure
|
| 8 |
+
# This is a placeholder that will work for basic testing
|
| 9 |
+
|
| 10 |
+
# For now, we'll create a simple file that can be loaded
|
| 11 |
+
# In a real scenario, you'd use TensorFlow to create this
|
| 12 |
+
|
| 13 |
+
print("Creating simple model structure...")
|
| 14 |
+
|
| 15 |
+
# Create a dummy model file (this is just for structure)
|
| 16 |
+
model_data = b'\x18\x00\x00\x00TFL3\x00\x00\x00\x00\x14\x00\x18\x00\x04\x00\x08\x00\x0c\x00\x10\x00\x14\x00\x00\x00'
|
| 17 |
+
|
| 18 |
+
with open('models/model.tflite', 'wb') as f:
|
| 19 |
+
f.write(model_data)
|
| 20 |
+
|
| 21 |
+
print("Simple model structure created!")
|
| 22 |
+
print("Note: This is a placeholder. For a real model, install TensorFlow and run create_sample_model.py")
|
| 23 |
+
|
| 24 |
+
if __name__ == "__main__":
|
| 25 |
+
create_simple_tflite_model()
|
data_collection.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import mediapipe as mp
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
import time
|
| 6 |
+
from sign_utils import MediaPipeLandmarksModel
|
| 7 |
+
|
| 8 |
+
class DataCollector:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.landmark_model = MediaPipeLandmarksModel()
|
| 11 |
+
self.mp_drawing = mp.solutions.drawing_utils
|
| 12 |
+
self.mp_hands = mp.solutions.hands
|
| 13 |
+
|
| 14 |
+
def collect_sign_data(self, sign_name, num_sequences=30, sequence_length=30):
|
| 15 |
+
"""Collect data for a specific sign."""
|
| 16 |
+
|
| 17 |
+
# Create directory for the sign
|
| 18 |
+
data_dir = f"dataset/{sign_name}"
|
| 19 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 20 |
+
|
| 21 |
+
cap = cv2.VideoCapture(0)
|
| 22 |
+
|
| 23 |
+
# Arrays to store sequences
|
| 24 |
+
sequences = []
|
| 25 |
+
|
| 26 |
+
print(f"Collecting data for sign: {sign_name}")
|
| 27 |
+
print(f"Will collect {num_sequences} sequences of {sequence_length} frames each")
|
| 28 |
+
print("Press 's' to start collecting, 'q' to quit")
|
| 29 |
+
|
| 30 |
+
collecting = False
|
| 31 |
+
sequence = []
|
| 32 |
+
sequence_count = 0
|
| 33 |
+
frame_count = 0
|
| 34 |
+
|
| 35 |
+
while True:
|
| 36 |
+
ret, frame = cap.read()
|
| 37 |
+
if not ret:
|
| 38 |
+
break
|
| 39 |
+
|
| 40 |
+
# Flip frame horizontally for mirror effect
|
| 41 |
+
frame = cv2.flip(frame, 1)
|
| 42 |
+
|
| 43 |
+
# Extract landmarks
|
| 44 |
+
landmarks = self.landmark_model.predict_landmarks(frame)
|
| 45 |
+
|
| 46 |
+
# Draw landmarks
|
| 47 |
+
if landmarks:
|
| 48 |
+
frame = self.landmark_model.draw_landmarks(frame, landmarks)
|
| 49 |
+
|
| 50 |
+
# Display status
|
| 51 |
+
if collecting:
|
| 52 |
+
cv2.putText(frame, f'Collecting: {sign_name}', (20, 50),
|
| 53 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
| 54 |
+
cv2.putText(frame, f'Sequence: {sequence_count + 1}/{num_sequences}', (20, 100),
|
| 55 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
| 56 |
+
cv2.putText(frame, f'Frame: {frame_count}/{sequence_length}', (20, 150),
|
| 57 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
| 58 |
+
|
| 59 |
+
if landmarks:
|
| 60 |
+
# Extract features
|
| 61 |
+
features = self.landmark_model.extract_features(landmarks)
|
| 62 |
+
sequence.append(features)
|
| 63 |
+
frame_count += 1
|
| 64 |
+
|
| 65 |
+
if frame_count >= sequence_length:
|
| 66 |
+
# Save sequence
|
| 67 |
+
sequences.append(sequence.copy())
|
| 68 |
+
sequence = []
|
| 69 |
+
frame_count = 0
|
| 70 |
+
sequence_count += 1
|
| 71 |
+
|
| 72 |
+
if sequence_count >= num_sequences:
|
| 73 |
+
break
|
| 74 |
+
|
| 75 |
+
# Wait before next sequence
|
| 76 |
+
time.sleep(2)
|
| 77 |
+
else:
|
| 78 |
+
cv2.putText(frame, f'Ready to collect: {sign_name}', (20, 50),
|
| 79 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
|
| 80 |
+
cv2.putText(frame, "Press 's' to start", (20, 100),
|
| 81 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
|
| 82 |
+
|
| 83 |
+
cv2.imshow('Data Collection', frame)
|
| 84 |
+
|
| 85 |
+
key = cv2.waitKey(1) & 0xFF
|
| 86 |
+
if key == ord('s') and not collecting:
|
| 87 |
+
collecting = True
|
| 88 |
+
print("Started collecting...")
|
| 89 |
+
elif key == ord('q'):
|
| 90 |
+
break
|
| 91 |
+
|
| 92 |
+
cap.release()
|
| 93 |
+
cv2.destroyAllWindows()
|
| 94 |
+
|
| 95 |
+
# Save collected data
|
| 96 |
+
if sequences:
|
| 97 |
+
np.save(f"{data_dir}/sequences.npy", np.array(sequences))
|
| 98 |
+
print(f"Saved {len(sequences)} sequences for {sign_name}")
|
| 99 |
+
|
| 100 |
+
return sequences
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
collector = DataCollector()
|
| 104 |
+
|
| 105 |
+
# List of signs to collect
|
| 106 |
+
signs = ['hello', 'thank_you', 'please', 'sorry', 'yes', 'no', 'good', 'bad', 'help', 'stop']
|
| 107 |
+
|
| 108 |
+
print("Sign Language Data Collection Tool")
|
| 109 |
+
print("Available signs:", signs)
|
| 110 |
+
|
| 111 |
+
while True:
|
| 112 |
+
print("\nOptions:")
|
| 113 |
+
print("1. Collect data for a specific sign")
|
| 114 |
+
print("2. Collect data for all signs")
|
| 115 |
+
print("3. Exit")
|
| 116 |
+
|
| 117 |
+
choice = input("Enter your choice (1-3): ")
|
| 118 |
+
|
| 119 |
+
if choice == '1':
|
| 120 |
+
sign_name = input("Enter sign name: ").lower().replace(' ', '_')
|
| 121 |
+
num_sequences = int(input("Number of sequences (default 30): ") or 30)
|
| 122 |
+
collector.collect_sign_data(sign_name, num_sequences)
|
| 123 |
+
|
| 124 |
+
elif choice == '2':
|
| 125 |
+
for sign in signs:
|
| 126 |
+
print(f"\nPrepare to collect data for: {sign}")
|
| 127 |
+
input("Press Enter when ready...")
|
| 128 |
+
collector.collect_sign_data(sign, 30)
|
| 129 |
+
|
| 130 |
+
elif choice == '3':
|
| 131 |
+
break
|
| 132 |
+
else:
|
| 133 |
+
print("Invalid choice!")
|
| 134 |
+
|
| 135 |
+
if __name__ == "__main__":
|
| 136 |
+
main()
|
final_app.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from streamlit_webrtc import webrtc_streamer, VideoTransformerBase, RTCConfiguration
|
| 5 |
+
from av import VideoFrame
|
| 6 |
+
import mediapipe as mp
|
| 7 |
+
import tensorflow as tf
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
# Page config
|
| 12 |
+
st.set_page_config(
|
| 13 |
+
page_title="Sign Language Digits Interpreter",
|
| 14 |
+
page_icon="🔢",
|
| 15 |
+
layout="wide"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# RTC Configuration for better WebRTC performance
|
| 19 |
+
RTC_CONFIGURATION = RTCConfiguration({
|
| 20 |
+
"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
# Global variables for thread-safe prediction sharing
|
| 24 |
+
prediction_lock = threading.Lock()
|
| 25 |
+
current_prediction = {"digit": "Show your hand", "confidence": 0.0}
|
| 26 |
+
|
| 27 |
+
# Load model and labels
|
| 28 |
+
@st.cache_resource
|
| 29 |
+
def load_model():
|
| 30 |
+
try:
|
| 31 |
+
interpreter = tf.lite.Interpreter(model_path="models/model.tflite")
|
| 32 |
+
interpreter.allocate_tensors()
|
| 33 |
+
|
| 34 |
+
with open("models/labels.txt", 'r') as f:
|
| 35 |
+
labels = [line.strip() for line in f.readlines()]
|
| 36 |
+
|
| 37 |
+
return interpreter, labels
|
| 38 |
+
except Exception as e:
|
| 39 |
+
st.error(f"Error loading model: {e}")
|
| 40 |
+
return None, []
|
| 41 |
+
|
| 42 |
+
interpreter, labels = load_model()
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@st.cache_resource
|
| 46 |
+
def get_mediapipe_hands():
|
| 47 |
+
mp_hands = mp.solutions.hands
|
| 48 |
+
mp_drawing = mp.solutions.drawing_utils
|
| 49 |
+
hands = mp_hands.Hands(
|
| 50 |
+
static_image_mode=False,
|
| 51 |
+
max_num_hands=1,
|
| 52 |
+
min_detection_confidence=0.7,
|
| 53 |
+
min_tracking_confidence=0.5
|
| 54 |
+
)
|
| 55 |
+
return mp_hands, mp_drawing, hands
|
| 56 |
+
|
| 57 |
+
mp_hands, mp_drawing, hands = get_mediapipe_hands()
|
| 58 |
+
|
| 59 |
+
def extract_hand_features(landmarks):
|
| 60 |
+
"""Extract 63 correct features from hand landmarks."""
|
| 61 |
+
try:
|
| 62 |
+
# Convert landmarks to numpy array
|
| 63 |
+
landmarks_array = np.array([(lm.x, lm.y, lm.z) for lm in landmarks.landmark])
|
| 64 |
+
|
| 65 |
+
# Ensure we have exactly 21 landmarks
|
| 66 |
+
if len(landmarks_array) != 21:
|
| 67 |
+
return np.zeros(63)
|
| 68 |
+
|
| 69 |
+
# Normalize relative to wrist (first landmark)
|
| 70 |
+
wrist = landmarks_array[0]
|
| 71 |
+
normalized = landmarks_array - wrist
|
| 72 |
+
|
| 73 |
+
# Scale by hand size (distance from wrist to middle finger tip)
|
| 74 |
+
hand_size = np.linalg.norm(landmarks_array[12] - wrist)
|
| 75 |
+
if hand_size > 0:
|
| 76 |
+
normalized = normalized / hand_size
|
| 77 |
+
|
| 78 |
+
# Flatten to 63 features
|
| 79 |
+
features = normalized.flatten()
|
| 80 |
+
|
| 81 |
+
# Ensure exactly 63 features
|
| 82 |
+
if len(features) != 63:
|
| 83 |
+
features = np.pad(features, (0, max(0, 63 - len(features))))[:63]
|
| 84 |
+
|
| 85 |
+
return features
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"Error extracting features: {e}")
|
| 88 |
+
return np.zeros(63)
|
| 89 |
+
|
| 90 |
+
def predict_digit(features):
|
| 91 |
+
"""Predict digit from features."""
|
| 92 |
+
if interpreter is None:
|
| 93 |
+
return "Model not loaded", 0.0
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
# Prepare input
|
| 97 |
+
input_data = np.expand_dims(features, axis=0).astype(np.float32)
|
| 98 |
+
|
| 99 |
+
# Run inference
|
| 100 |
+
input_details = interpreter.get_input_details()
|
| 101 |
+
output_details = interpreter.get_output_details()
|
| 102 |
+
|
| 103 |
+
interpreter.set_tensor(input_details[0]['index'], input_data)
|
| 104 |
+
interpreter.invoke()
|
| 105 |
+
|
| 106 |
+
# Get prediction
|
| 107 |
+
output = interpreter.get_tensor(output_details[0]['index'])
|
| 108 |
+
predicted_class = np.argmax(output[0])
|
| 109 |
+
confidence = float(np.max(output[0]))
|
| 110 |
+
|
| 111 |
+
# Apply softmax to get better confidence scores
|
| 112 |
+
exp_output = np.exp(output[0] - np.max(output[0]))
|
| 113 |
+
softmax_output = exp_output / np.sum(exp_output)
|
| 114 |
+
confidence = float(softmax_output[predicted_class])
|
| 115 |
+
|
| 116 |
+
digit = labels[predicted_class] if predicted_class < len(labels) else str(predicted_class)
|
| 117 |
+
return digit, confidence
|
| 118 |
+
except Exception as e:
|
| 119 |
+
print(f"Prediction error: {e}")
|
| 120 |
+
return "Error", 0.0
|
| 121 |
+
|
| 122 |
+
class DigitRecognizer(VideoTransformerBase):
|
| 123 |
+
def __init__(self):
|
| 124 |
+
self.frame_count = 0
|
| 125 |
+
self.last_prediction_time = 0
|
| 126 |
+
self.prediction_interval = 0.2 # Predict every 200ms
|
| 127 |
+
|
| 128 |
+
def recv(self, frame: VideoFrame) -> VideoFrame:
|
| 129 |
+
image = frame.to_ndarray(format="bgr24")
|
| 130 |
+
self.frame_count += 1
|
| 131 |
+
|
| 132 |
+
current_time = time.time()
|
| 133 |
+
|
| 134 |
+
# Process prediction at regular intervals
|
| 135 |
+
if current_time - self.last_prediction_time > self.prediction_interval:
|
| 136 |
+
self.last_prediction_time = current_time
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
# Convert to RGB for MediaPipe
|
| 140 |
+
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 141 |
+
results = hands.process(rgb_image)
|
| 142 |
+
|
| 143 |
+
if results.multi_hand_landmarks:
|
| 144 |
+
# Get first hand
|
| 145 |
+
hand_landmarks = results.multi_hand_landmarks[0]
|
| 146 |
+
|
| 147 |
+
# Draw landmarks
|
| 148 |
+
mp_drawing.draw_landmarks(
|
| 149 |
+
image, hand_landmarks, mp_hands.HAND_CONNECTIONS,
|
| 150 |
+
mp_drawing.DrawingSpec(color=(0, 0, 255), thickness=2, circle_radius=2),
|
| 151 |
+
mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2)
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# Extract features and predict
|
| 155 |
+
features = extract_hand_features(hand_landmarks)
|
| 156 |
+
digit, confidence = predict_digit(features)
|
| 157 |
+
|
| 158 |
+
# Update global prediction
|
| 159 |
+
with prediction_lock:
|
| 160 |
+
current_prediction["digit"] = digit
|
| 161 |
+
current_prediction["confidence"] = confidence
|
| 162 |
+
else:
|
| 163 |
+
with prediction_lock:
|
| 164 |
+
current_prediction["digit"] = "No hand detected"
|
| 165 |
+
current_prediction["confidence"] = 0.0
|
| 166 |
+
except Exception as e:
|
| 167 |
+
print(f"Processing error: {e}")
|
| 168 |
+
with prediction_lock:
|
| 169 |
+
current_prediction["digit"] = "Processing error"
|
| 170 |
+
current_prediction["confidence"] = 0.0
|
| 171 |
+
|
| 172 |
+
# Add text overlay
|
| 173 |
+
with prediction_lock:
|
| 174 |
+
display_text = current_prediction["digit"]
|
| 175 |
+
confidence = current_prediction["confidence"]
|
| 176 |
+
|
| 177 |
+
color = (0, 255, 0) if confidence > 0.5 else (0, 0, 255)
|
| 178 |
+
cv2.putText(
|
| 179 |
+
image,
|
| 180 |
+
f"{display_text} ({confidence:.2f})",
|
| 181 |
+
(20, 40),
|
| 182 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 183 |
+
1.0,
|
| 184 |
+
color,
|
| 185 |
+
2
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
return VideoFrame.from_ndarray(image, format="bgr24")
|
| 189 |
+
|
| 190 |
+
# Main app
|
| 191 |
+
st.title("🤟 AI Sign Language Interpreter")
|
| 192 |
+
st.markdown("Show hand signs for numbers and letters to the camera!")
|
| 193 |
+
|
| 194 |
+
# Sidebar controls
|
| 195 |
+
st.sidebar.title("🎛️ Controls")
|
| 196 |
+
confidence_threshold = st.sidebar.slider("Confidence Threshold", 0.0, 1.0, 0.5, 0.1)
|
| 197 |
+
st.sidebar.markdown("---")
|
| 198 |
+
st.sidebar.markdown("### Model Info")
|
| 199 |
+
if interpreter:
|
| 200 |
+
st.sidebar.success("✅ Model loaded successfully")
|
| 201 |
+
st.sidebar.info(f"📊 Classes: {len(labels)}")
|
| 202 |
+
st.sidebar.info("🔢 Numbers: 1,2,4,5,6,7,8")
|
| 203 |
+
st.sidebar.info("🔤 Letters: A-X (except E)")
|
| 204 |
+
else:
|
| 205 |
+
st.sidebar.error("❌ Model not loaded")
|
| 206 |
+
|
| 207 |
+
# Create columns
|
| 208 |
+
col1, col2 = st.columns([2, 1])
|
| 209 |
+
|
| 210 |
+
with col1:
|
| 211 |
+
st.markdown("### 📹 Live Camera Feed")
|
| 212 |
+
# Webcam component
|
| 213 |
+
ctx = webrtc_streamer(
|
| 214 |
+
key="digit_recognition",
|
| 215 |
+
video_processor_factory=DigitRecognizer,
|
| 216 |
+
rtc_configuration=RTC_CONFIGURATION,
|
| 217 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 218 |
+
async_processing=True,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
with col2:
|
| 222 |
+
st.markdown("### 🎯 Current Prediction")
|
| 223 |
+
|
| 224 |
+
# Create placeholders for dynamic updates
|
| 225 |
+
prediction_placeholder = st.empty()
|
| 226 |
+
confidence_bar = st.empty()
|
| 227 |
+
confidence_text = st.empty()
|
| 228 |
+
|
| 229 |
+
# Update display in real-time
|
| 230 |
+
if ctx.state.playing:
|
| 231 |
+
while ctx.state.playing:
|
| 232 |
+
with prediction_lock:
|
| 233 |
+
digit = current_prediction["digit"]
|
| 234 |
+
confidence = current_prediction["confidence"]
|
| 235 |
+
|
| 236 |
+
# Display prediction
|
| 237 |
+
if confidence > confidence_threshold and digit not in ["No hand detected", "Processing error", "Error"]:
|
| 238 |
+
prediction_placeholder.markdown(f"# {digit}")
|
| 239 |
+
confidence_bar.progress(confidence)
|
| 240 |
+
confidence_text.success(f"Confidence: {confidence:.2f}")
|
| 241 |
+
else:
|
| 242 |
+
prediction_placeholder.markdown("# -")
|
| 243 |
+
confidence_bar.progress(0)
|
| 244 |
+
if digit == "No hand detected":
|
| 245 |
+
confidence_text.info("Show your hand to the camera")
|
| 246 |
+
else:
|
| 247 |
+
confidence_text.warning(f"Low confidence: {confidence:.2f}")
|
| 248 |
+
|
| 249 |
+
time.sleep(0.1) # Update every 100ms
|
| 250 |
+
else:
|
| 251 |
+
st.warning("📷 Turn on your webcam to start!")
|
| 252 |
+
|
| 253 |
+
# Instructions
|
| 254 |
+
st.markdown("---")
|
| 255 |
+
st.markdown("""
|
| 256 |
+
### 📋 Instructions:
|
| 257 |
+
1. **Allow camera access** when prompted
|
| 258 |
+
2. **Position your hand** clearly in the camera view
|
| 259 |
+
3. **Make gesture signs** for numbers or letters
|
| 260 |
+
4. **Hold the sign steady** for best results
|
| 261 |
+
5. **Adjust confidence threshold** if needed
|
| 262 |
+
|
| 263 |
+
### 💡 Tips for Better Recognition:
|
| 264 |
+
- ✅ Ensure **good lighting**
|
| 265 |
+
- ✅ Keep your **hand centered** in the frame
|
| 266 |
+
- ✅ Make **clear, distinct** gesture signs
|
| 267 |
+
- ✅ **Hold steady** for 1-2 seconds
|
| 268 |
+
- ✅ Try different **hand orientations** if needed
|
| 269 |
+
- ✅ **Avoid background clutter** behind your hand
|
| 270 |
+
|
| 271 |
+
### 🎯 Supported Gestures:
|
| 272 |
+
""")
|
| 273 |
+
|
| 274 |
+
# Display supported gestures
|
| 275 |
+
st.markdown("#### 🔢 Numbers:")
|
| 276 |
+
number_labels = [label for label in labels if label.isdigit()]
|
| 277 |
+
if number_labels:
|
| 278 |
+
cols = st.columns(min(len(number_labels), 8))
|
| 279 |
+
for i, label in enumerate(number_labels):
|
| 280 |
+
with cols[i % len(cols)]:
|
| 281 |
+
st.info(f"**{label}**")
|
| 282 |
+
|
| 283 |
+
st.markdown("#### 🔤 Letters:")
|
| 284 |
+
letter_labels = [label for label in labels if label.isalpha()]
|
| 285 |
+
if letter_labels:
|
| 286 |
+
# Display letters in rows of 8
|
| 287 |
+
for row_start in range(0, len(letter_labels), 8):
|
| 288 |
+
row_labels = letter_labels[row_start:row_start + 8]
|
| 289 |
+
cols = st.columns(len(row_labels))
|
| 290 |
+
for i, label in enumerate(row_labels):
|
| 291 |
+
with cols[i]:
|
| 292 |
+
st.info(f"**{label}**")
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|
requirements_new.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.28.0
|
| 2 |
+
streamlit-webrtc==0.47.1
|
| 3 |
+
opencv-python==4.8.1.78
|
| 4 |
+
mediapipe==0.10.7
|
| 5 |
+
tensorflow==2.13.0
|
| 6 |
+
numpy==1.24.3
|
| 7 |
+
google-generativeai==0.3.2
|
| 8 |
+
flask==2.3.3
|
| 9 |
+
av==10.0.0
|
| 10 |
+
protobuf==3.20.3
|
requirements_training.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tensorflow>=2.0.0
|
| 2 |
+
opencv-python>=4.5.0
|
| 3 |
+
mediapipe>=0.8.0
|
| 4 |
+
scikit-learn>=0.24.0
|
| 5 |
+
matplotlib>=3.3.0
|
| 6 |
+
seaborn>=0.11.0
|
| 7 |
+
tqdm>=4.60.0
|
| 8 |
+
numpy>=1.19.0
|
run_app.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Startup script for AI Sign Language Interpreter
|
| 4 |
+
This script starts both the landing page (Flask) and the main app (Streamlit)
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import subprocess
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
import webbrowser
|
| 11 |
+
import sys
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
def start_streamlit():
|
| 15 |
+
"""Start Streamlit app"""
|
| 16 |
+
print("🚀 Starting Streamlit app...")
|
| 17 |
+
try:
|
| 18 |
+
subprocess.run([
|
| 19 |
+
sys.executable, '-m', 'streamlit', 'run', 'app_enhanced.py',
|
| 20 |
+
'--server.port=8502',
|
| 21 |
+
'--server.headless=true',
|
| 22 |
+
'--server.runOnSave=true'
|
| 23 |
+
])
|
| 24 |
+
except KeyboardInterrupt:
|
| 25 |
+
print("Streamlit app stopped.")
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"Error starting Streamlit: {e}")
|
| 28 |
+
|
| 29 |
+
def start_flask():
|
| 30 |
+
"""Start Flask server for landing page"""
|
| 31 |
+
print("🌐 Starting Flask server...")
|
| 32 |
+
try:
|
| 33 |
+
subprocess.run([sys.executable, 'server.py'])
|
| 34 |
+
except KeyboardInterrupt:
|
| 35 |
+
print("Flask server stopped.")
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"Error starting Flask: {e}")
|
| 38 |
+
|
| 39 |
+
def main():
|
| 40 |
+
print("=" * 60)
|
| 41 |
+
print("🤟 AI Sign Language Interpreter")
|
| 42 |
+
print("=" * 60)
|
| 43 |
+
print("Starting application servers...")
|
| 44 |
+
print()
|
| 45 |
+
|
| 46 |
+
# Check if required files exist
|
| 47 |
+
required_files = ['app_enhanced.py', 'server.py', 'models/model.tflite', 'models/labels.txt']
|
| 48 |
+
missing_files = [f for f in required_files if not os.path.exists(f)]
|
| 49 |
+
|
| 50 |
+
if missing_files:
|
| 51 |
+
print("❌ Missing required files:")
|
| 52 |
+
for file in missing_files:
|
| 53 |
+
print(f" - {file}")
|
| 54 |
+
print("\nPlease ensure all required files are present.")
|
| 55 |
+
return
|
| 56 |
+
|
| 57 |
+
print("✅ All required files found.")
|
| 58 |
+
print()
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
# Start Streamlit in a separate thread
|
| 62 |
+
streamlit_thread = threading.Thread(target=start_streamlit, daemon=True)
|
| 63 |
+
streamlit_thread.start()
|
| 64 |
+
|
| 65 |
+
# Give Streamlit time to start
|
| 66 |
+
print("⏳ Waiting for Streamlit to initialize...")
|
| 67 |
+
time.sleep(5)
|
| 68 |
+
|
| 69 |
+
# Start Flask (this will block)
|
| 70 |
+
print("🌐 Flask server starting on http://localhost:5000")
|
| 71 |
+
print("🤖 Streamlit app available on http://localhost:8501")
|
| 72 |
+
print()
|
| 73 |
+
print("📖 Instructions:")
|
| 74 |
+
print(" 1. Visit http://localhost:5000 for the landing page")
|
| 75 |
+
print(" 2. Click 'Try it Out' to access the AI interpreter")
|
| 76 |
+
print(" 3. Allow camera access when prompted")
|
| 77 |
+
print(" 4. Start making sign language gestures!")
|
| 78 |
+
print()
|
| 79 |
+
print("Press Ctrl+C to stop all servers")
|
| 80 |
+
print("=" * 60)
|
| 81 |
+
|
| 82 |
+
# Open browser automatically
|
| 83 |
+
time.sleep(2)
|
| 84 |
+
webbrowser.open('http://localhost:5000')
|
| 85 |
+
|
| 86 |
+
# Start Flask server
|
| 87 |
+
start_flask()
|
| 88 |
+
|
| 89 |
+
except KeyboardInterrupt:
|
| 90 |
+
print("\n🛑 Shutting down servers...")
|
| 91 |
+
print("👋 Thank you for using AI Sign Language Interpreter!")
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"❌ Error: {e}")
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
main()
|
run_simple_app.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple startup script for the streamlined Sign Language Interpreter
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
def check_requirements():
|
| 12 |
+
"""Check if required files exist"""
|
| 13 |
+
required_files = ['simple_sign_interpreter.py', 'sign_utils.py', 'models/model.tflite', 'models/labels.txt']
|
| 14 |
+
missing_files = [f for f in required_files if not os.path.exists(f)]
|
| 15 |
+
|
| 16 |
+
if missing_files:
|
| 17 |
+
print("❌ Missing required files:")
|
| 18 |
+
for file in missing_files:
|
| 19 |
+
print(f" - {file}")
|
| 20 |
+
print("\nPlease ensure all required files are present.")
|
| 21 |
+
return False
|
| 22 |
+
|
| 23 |
+
print("✅ All required files found.")
|
| 24 |
+
return True
|
| 25 |
+
|
| 26 |
+
def start_streamlit():
|
| 27 |
+
"""Start the simple sign interpreter app"""
|
| 28 |
+
print("🤟 Starting Simple Sign Language Interpreter...")
|
| 29 |
+
print("=" * 50)
|
| 30 |
+
|
| 31 |
+
if not check_requirements():
|
| 32 |
+
return
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
print("🚀 Starting Streamlit app on http://localhost:8501")
|
| 36 |
+
print("📖 Instructions:")
|
| 37 |
+
print(" 1. Allow camera access when prompted")
|
| 38 |
+
print(" 2. Make sign language gestures for numbers (1,2,4,5,6,7,8) or letters (A-X)")
|
| 39 |
+
print(" 3. Hold gestures steady for best recognition")
|
| 40 |
+
print(" 4. Adjust confidence threshold if needed")
|
| 41 |
+
print()
|
| 42 |
+
print("Press Ctrl+C to stop the application")
|
| 43 |
+
print("=" * 50)
|
| 44 |
+
|
| 45 |
+
# Start Streamlit
|
| 46 |
+
subprocess.run([
|
| 47 |
+
sys.executable, '-m', 'streamlit', 'run', 'simple_sign_interpreter.py',
|
| 48 |
+
'--server.port=8501',
|
| 49 |
+
'--server.headless=false'
|
| 50 |
+
])
|
| 51 |
+
|
| 52 |
+
except KeyboardInterrupt:
|
| 53 |
+
print("\n🛑 Application stopped.")
|
| 54 |
+
print("👋 Thank you for using the Sign Language Interpreter!")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"❌ Error starting application: {e}")
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
start_streamlit()
|
server.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, redirect, url_for
|
| 2 |
+
import subprocess
|
| 3 |
+
import threading
|
| 4 |
+
import time
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
app = Flask(__name__)
|
| 8 |
+
|
| 9 |
+
# Global variable to track Streamlit process
|
| 10 |
+
streamlit_process = None
|
| 11 |
+
|
| 12 |
+
def start_streamlit():
|
| 13 |
+
"""Start Streamlit app in background"""
|
| 14 |
+
global streamlit_process
|
| 15 |
+
try:
|
| 16 |
+
streamlit_process = subprocess.Popen([
|
| 17 |
+
'streamlit', 'run', 'app_enhanced.py',
|
| 18 |
+
'--server.port=8502',
|
| 19 |
+
'--server.headless=true'
|
| 20 |
+
])
|
| 21 |
+
print("Streamlit app started on port 8502")
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"Error starting Streamlit: {e}")
|
| 24 |
+
|
| 25 |
+
@app.route('/')
|
| 26 |
+
def landing_page():
|
| 27 |
+
"""Serve the landing page"""
|
| 28 |
+
return render_template('index.html')
|
| 29 |
+
|
| 30 |
+
@app.route('/app')
|
| 31 |
+
def launch_app():
|
| 32 |
+
"""Redirect to Streamlit app"""
|
| 33 |
+
return redirect('http://localhost:8502')
|
| 34 |
+
|
| 35 |
+
@app.route('/start-streamlit')
|
| 36 |
+
def start_streamlit_endpoint():
|
| 37 |
+
"""Start Streamlit if not running"""
|
| 38 |
+
global streamlit_process
|
| 39 |
+
if streamlit_process is None or streamlit_process.poll() is not None:
|
| 40 |
+
threading.Thread(target=start_streamlit, daemon=True).start()
|
| 41 |
+
time.sleep(3) # Give Streamlit time to start
|
| 42 |
+
return redirect('http://localhost:8502')
|
| 43 |
+
|
| 44 |
+
if __name__ == '__main__':
|
| 45 |
+
# Start Streamlit in background
|
| 46 |
+
threading.Thread(target=start_streamlit, daemon=True).start()
|
| 47 |
+
|
| 48 |
+
# Start Flask server
|
| 49 |
+
print("Starting Flask server on http://localhost:5000")
|
| 50 |
+
print("Streamlit app will be available on http://localhost:8502")
|
| 51 |
+
app.run(host='0.0.0.0', port=5000, debug=True)
|
setup.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
def install_requirements():
|
| 6 |
+
"""Install required packages."""
|
| 7 |
+
print("Installing required packages...")
|
| 8 |
+
try:
|
| 9 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
|
| 10 |
+
print("✅ Requirements installed successfully!")
|
| 11 |
+
except subprocess.CalledProcessError as e:
|
| 12 |
+
print(f"❌ Error installing requirements: {e}")
|
| 13 |
+
return False
|
| 14 |
+
return True
|
| 15 |
+
|
| 16 |
+
def create_directories():
|
| 17 |
+
"""Create necessary directories."""
|
| 18 |
+
directories = [
|
| 19 |
+
"models",
|
| 20 |
+
"dataset",
|
| 21 |
+
"static/css",
|
| 22 |
+
"templates",
|
| 23 |
+
"utils",
|
| 24 |
+
"logs"
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
for directory in directories:
|
| 28 |
+
os.makedirs(directory, exist_ok=True)
|
| 29 |
+
print(f"✅ Created directory: {directory}")
|
| 30 |
+
|
| 31 |
+
def create_sample_model():
|
| 32 |
+
"""Create a sample model for testing."""
|
| 33 |
+
print("Creating sample model...")
|
| 34 |
+
try:
|
| 35 |
+
subprocess.check_call([sys.executable, "create_sample_model.py"])
|
| 36 |
+
print("✅ Sample model created successfully!")
|
| 37 |
+
except subprocess.CalledProcessError as e:
|
| 38 |
+
print(f"❌ Error creating sample model: {e}")
|
| 39 |
+
return False
|
| 40 |
+
return True
|
| 41 |
+
|
| 42 |
+
def main():
|
| 43 |
+
print("🤟 Sign Language Interpreter Setup")
|
| 44 |
+
print("=" * 40)
|
| 45 |
+
|
| 46 |
+
# Create directories
|
| 47 |
+
print("\n1. Creating directories...")
|
| 48 |
+
create_directories()
|
| 49 |
+
|
| 50 |
+
# Install requirements
|
| 51 |
+
print("\n2. Installing requirements...")
|
| 52 |
+
if not install_requirements():
|
| 53 |
+
print("Setup failed. Please install requirements manually.")
|
| 54 |
+
return
|
| 55 |
+
|
| 56 |
+
# Create sample model
|
| 57 |
+
print("\n3. Creating sample model...")
|
| 58 |
+
if not create_sample_model():
|
| 59 |
+
print("Warning: Sample model creation failed. You can create it later.")
|
| 60 |
+
|
| 61 |
+
print("\n🎉 Setup completed successfully!")
|
| 62 |
+
print("\nNext steps:")
|
| 63 |
+
print("1. Run 'python data_collection.py' to collect your own sign data")
|
| 64 |
+
print("2. Run 'python train_model.py' to train a custom model")
|
| 65 |
+
print("3. Run 'streamlit run app.py' to start the application")
|
| 66 |
+
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
main()
|
sign_utils.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import mediapipe as mp
|
| 3 |
+
import numpy as np
|
| 4 |
+
from typing import List, Tuple, Optional
|
| 5 |
+
|
| 6 |
+
# Try to import TensorFlow, handle gracefully if not available
|
| 7 |
+
try:
|
| 8 |
+
import tensorflow as tf
|
| 9 |
+
TF_AVAILABLE = True
|
| 10 |
+
except ImportError:
|
| 11 |
+
TF_AVAILABLE = False
|
| 12 |
+
print("Warning: TensorFlow not available. Some features may be limited.")
|
| 13 |
+
|
| 14 |
+
class MediaPipeLandmarksModel:
|
| 15 |
+
def __init__(self):
|
| 16 |
+
self.mp_hands = mp.solutions.hands
|
| 17 |
+
self.mp_drawing = mp.solutions.drawing_utils
|
| 18 |
+
self.hands = self.mp_hands.Hands(
|
| 19 |
+
static_image_mode=False,
|
| 20 |
+
max_num_hands=2,
|
| 21 |
+
min_detection_confidence=0.5,
|
| 22 |
+
min_tracking_confidence=0.5
|
| 23 |
+
)
|
| 24 |
+
self.sequence = []
|
| 25 |
+
self.sequence_length = 15 # Reduced for faster response
|
| 26 |
+
|
| 27 |
+
def predict_landmarks(self, image: np.ndarray) -> Optional[List[Tuple[float, float, float]]]:
|
| 28 |
+
"""Extract hand landmarks from image."""
|
| 29 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 30 |
+
results = self.hands.process(image_rgb)
|
| 31 |
+
|
| 32 |
+
if results.multi_hand_landmarks:
|
| 33 |
+
landmarks = []
|
| 34 |
+
for hand_landmarks in results.multi_hand_landmarks:
|
| 35 |
+
for landmark in hand_landmarks.landmark:
|
| 36 |
+
landmarks.append((landmark.x, landmark.y, landmark.z))
|
| 37 |
+
return landmarks
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
def draw_landmarks(self, image: np.ndarray, landmarks) -> np.ndarray:
|
| 41 |
+
"""Draw hand landmarks on image."""
|
| 42 |
+
if landmarks:
|
| 43 |
+
results = self.hands.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
| 44 |
+
if results.multi_hand_landmarks:
|
| 45 |
+
for hand_landmarks in results.multi_hand_landmarks:
|
| 46 |
+
self.mp_drawing.draw_landmarks(
|
| 47 |
+
image, hand_landmarks, self.mp_hands.HAND_CONNECTIONS)
|
| 48 |
+
return image
|
| 49 |
+
|
| 50 |
+
def normalize_landmarks(self, landmarks: List[Tuple[float, float, float]]) -> np.ndarray:
|
| 51 |
+
"""Normalize landmarks relative to wrist position."""
|
| 52 |
+
if not landmarks:
|
| 53 |
+
return np.zeros((21, 3))
|
| 54 |
+
|
| 55 |
+
landmarks_array = np.array(landmarks)
|
| 56 |
+
|
| 57 |
+
# Get wrist position (first landmark)
|
| 58 |
+
wrist = landmarks_array[0]
|
| 59 |
+
|
| 60 |
+
# Normalize relative to wrist
|
| 61 |
+
normalized = landmarks_array - wrist
|
| 62 |
+
|
| 63 |
+
# Scale by hand size (distance from wrist to middle finger tip)
|
| 64 |
+
if len(landmarks_array) >= 12:
|
| 65 |
+
hand_size = np.linalg.norm(landmarks_array[12] - wrist)
|
| 66 |
+
if hand_size > 0:
|
| 67 |
+
normalized = normalized / hand_size
|
| 68 |
+
|
| 69 |
+
return normalized.flatten()
|
| 70 |
+
|
| 71 |
+
def extract_features(self, landmarks: List[Tuple[float, float, float]]) -> np.ndarray:
|
| 72 |
+
"""Extract features from landmarks for model input."""
|
| 73 |
+
if not landmarks:
|
| 74 |
+
return np.zeros(63) # 21 landmarks * 3 coordinates
|
| 75 |
+
|
| 76 |
+
# Ensure we have exactly 21 landmarks (one hand)
|
| 77 |
+
if len(landmarks) > 21:
|
| 78 |
+
landmarks = landmarks[:21] # Take first hand only
|
| 79 |
+
elif len(landmarks) < 21:
|
| 80 |
+
# Pad with zeros if we have fewer landmarks
|
| 81 |
+
while len(landmarks) < 21:
|
| 82 |
+
landmarks.append((0.0, 0.0, 0.0))
|
| 83 |
+
|
| 84 |
+
# Convert to numpy array
|
| 85 |
+
landmarks_array = np.array(landmarks)
|
| 86 |
+
|
| 87 |
+
# Get wrist position (first landmark)
|
| 88 |
+
wrist = landmarks_array[0]
|
| 89 |
+
|
| 90 |
+
# Normalize relative to wrist
|
| 91 |
+
normalized = landmarks_array - wrist
|
| 92 |
+
|
| 93 |
+
# Scale by hand size (distance from wrist to middle finger tip)
|
| 94 |
+
hand_size = np.linalg.norm(landmarks_array[12] - wrist) if len(landmarks_array) >= 12 else 1.0
|
| 95 |
+
if hand_size > 0:
|
| 96 |
+
normalized = normalized / hand_size
|
| 97 |
+
|
| 98 |
+
# Flatten to 63 dimensions (21 landmarks * 3 coordinates)
|
| 99 |
+
features = normalized.flatten()
|
| 100 |
+
|
| 101 |
+
# Ensure we have exactly 63 dimensions
|
| 102 |
+
if len(features) > 63:
|
| 103 |
+
features = features[:63]
|
| 104 |
+
elif len(features) < 63:
|
| 105 |
+
features = np.pad(features, (0, 63 - len(features)))
|
| 106 |
+
|
| 107 |
+
return features
|
| 108 |
+
|
| 109 |
+
def update_sequence(self, landmarks: List[Tuple[float, float, float]]) -> np.ndarray:
|
| 110 |
+
"""Update sequence for temporal modeling."""
|
| 111 |
+
features = self.extract_features(landmarks)
|
| 112 |
+
self.sequence.append(features)
|
| 113 |
+
|
| 114 |
+
# Keep only last sequence_length frames
|
| 115 |
+
if len(self.sequence) > self.sequence_length:
|
| 116 |
+
self.sequence.pop(0)
|
| 117 |
+
|
| 118 |
+
# Pad sequence if needed
|
| 119 |
+
if len(self.sequence) < self.sequence_length:
|
| 120 |
+
padding = np.zeros((self.sequence_length - len(self.sequence), features.shape[0]))
|
| 121 |
+
sequence_array = np.vstack([padding, np.array(self.sequence)])
|
| 122 |
+
else:
|
| 123 |
+
sequence_array = np.array(self.sequence)
|
| 124 |
+
|
| 125 |
+
return sequence_array
|
| 126 |
+
|
| 127 |
+
def reset_sequence(self):
|
| 128 |
+
"""Reset the sequence buffer."""
|
| 129 |
+
self.sequence = []
|
| 130 |
+
|
| 131 |
+
class SignLanguagePredictor:
|
| 132 |
+
def __init__(self, model_path: str, labels_path: str):
|
| 133 |
+
self.landmark_model = MediaPipeLandmarksModel()
|
| 134 |
+
|
| 135 |
+
# Load labels
|
| 136 |
+
try:
|
| 137 |
+
with open(labels_path, 'r') as f:
|
| 138 |
+
self.labels = [line.strip() for line in f.readlines()]
|
| 139 |
+
except FileNotFoundError:
|
| 140 |
+
print(f"Warning: Labels file not found at {labels_path}")
|
| 141 |
+
self.labels = ["Unknown"]
|
| 142 |
+
|
| 143 |
+
# Initialize TFLite interpreter if available
|
| 144 |
+
if TF_AVAILABLE:
|
| 145 |
+
try:
|
| 146 |
+
self.interpreter = tf.lite.Interpreter(model_path=model_path)
|
| 147 |
+
self.interpreter.allocate_tensors()
|
| 148 |
+
self.input_details = self.interpreter.get_input_details()
|
| 149 |
+
self.output_details = self.interpreter.get_output_details()
|
| 150 |
+
self.model_loaded = True
|
| 151 |
+
except Exception as e:
|
| 152 |
+
print(f"Warning: Could not load model: {e}")
|
| 153 |
+
self.model_loaded = False
|
| 154 |
+
else:
|
| 155 |
+
print("Warning: TensorFlow not available. Running in landmark-only mode.")
|
| 156 |
+
self.model_loaded = False
|
| 157 |
+
|
| 158 |
+
def predict(self, image: np.ndarray) -> Tuple[str, float]:
|
| 159 |
+
"""Predict sign from image using static prediction for compatibility."""
|
| 160 |
+
# Use static prediction instead of temporal for now to avoid dimension issues
|
| 161 |
+
return self.predict_static(image)
|
| 162 |
+
|
| 163 |
+
def predict_static(self, image: np.ndarray) -> Tuple[str, float]:
|
| 164 |
+
"""Predict sign from static image (single frame)."""
|
| 165 |
+
landmarks = self.landmark_model.predict_landmarks(image)
|
| 166 |
+
|
| 167 |
+
if not landmarks:
|
| 168 |
+
return "No hand detected", 0.0
|
| 169 |
+
|
| 170 |
+
if not self.model_loaded:
|
| 171 |
+
return "Hand detected (Model not available)", 0.5
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
# Extract features
|
| 175 |
+
features = self.landmark_model.extract_features(landmarks)
|
| 176 |
+
|
| 177 |
+
# Get expected input shape from model
|
| 178 |
+
input_shape = self.input_details[0]['shape']
|
| 179 |
+
|
| 180 |
+
# Reshape features to match expected input shape
|
| 181 |
+
if len(input_shape) == 2: # If model expects 2D input (batch_size, features)
|
| 182 |
+
input_data = features.reshape(1, -1).astype(np.float32)
|
| 183 |
+
elif len(input_shape) == 3: # If model expects 3D input (batch_size, time_steps, features)
|
| 184 |
+
input_data = features.reshape(1, 1, -1).astype(np.float32)
|
| 185 |
+
else:
|
| 186 |
+
raise ValueError(f"Unexpected input shape: {input_shape}")
|
| 187 |
+
|
| 188 |
+
# Run inference
|
| 189 |
+
self.interpreter.set_tensor(self.input_details[0]['index'], input_data)
|
| 190 |
+
self.interpreter.invoke()
|
| 191 |
+
|
| 192 |
+
# Get prediction
|
| 193 |
+
output = self.interpreter.get_tensor(self.output_details[0]['index'])
|
| 194 |
+
predicted_class = np.argmax(output[0])
|
| 195 |
+
confidence = float(np.max(output[0]))
|
| 196 |
+
|
| 197 |
+
return self.labels[predicted_class], confidence
|
| 198 |
+
except Exception as e:
|
| 199 |
+
print(f"Error during static prediction: {e}")
|
| 200 |
+
return "Error during prediction", 0.0
|
simple_app.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from streamlit_webrtc import webrtc_streamer, VideoTransformerBase
|
| 5 |
+
from av import VideoFrame
|
| 6 |
+
import mediapipe as mp
|
| 7 |
+
import tensorflow as tf
|
| 8 |
+
|
| 9 |
+
# Page config
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="Sign Language Digits Interpreter",
|
| 12 |
+
page_icon="🔢",
|
| 13 |
+
layout="wide"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Load model and labels
|
| 17 |
+
@st.cache_resource
|
| 18 |
+
def load_model():
|
| 19 |
+
try:
|
| 20 |
+
interpreter = tf.lite.Interpreter(model_path="models/model.tflite")
|
| 21 |
+
interpreter.allocate_tensors()
|
| 22 |
+
|
| 23 |
+
with open("models/labels.txt", 'r') as f:
|
| 24 |
+
labels = [line.strip() for line in f.readlines()]
|
| 25 |
+
|
| 26 |
+
return interpreter, labels
|
| 27 |
+
except Exception as e:
|
| 28 |
+
st.error(f"Error loading model: {e}")
|
| 29 |
+
return None, []
|
| 30 |
+
|
| 31 |
+
interpreter, labels = load_model()
|
| 32 |
+
|
| 33 |
+
# MediaPipe setup
|
| 34 |
+
mp_hands = mp.solutions.hands
|
| 35 |
+
mp_drawing = mp.solutions.drawing_utils
|
| 36 |
+
hands = mp_hands.Hands(
|
| 37 |
+
static_image_mode=False,
|
| 38 |
+
max_num_hands=1,
|
| 39 |
+
min_detection_confidence=0.5,
|
| 40 |
+
min_tracking_confidence=0.5
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
def extract_hand_features(landmarks):
|
| 44 |
+
"""Extract 63 features from hand landmarks."""
|
| 45 |
+
if not landmarks:
|
| 46 |
+
return np.zeros(63)
|
| 47 |
+
|
| 48 |
+
# Convert landmarks to numpy array
|
| 49 |
+
landmarks_array = np.array([(lm.x, lm.y, lm.z) for lm in landmarks.landmark])
|
| 50 |
+
|
| 51 |
+
# Normalize relative to wrist (first landmark)
|
| 52 |
+
wrist = landmarks_array[0]
|
| 53 |
+
normalized = landmarks_array - wrist
|
| 54 |
+
|
| 55 |
+
# Scale by hand size
|
| 56 |
+
hand_size = np.linalg.norm(landmarks_array[12] - wrist)
|
| 57 |
+
if hand_size > 0:
|
| 58 |
+
normalized = normalized / hand_size
|
| 59 |
+
|
| 60 |
+
# Flatten to 63 features
|
| 61 |
+
features = normalized.flatten()
|
| 62 |
+
|
| 63 |
+
# Ensure exactly 63 features
|
| 64 |
+
if len(features) > 63:
|
| 65 |
+
features = features[:63]
|
| 66 |
+
elif len(features) < 63:
|
| 67 |
+
features = np.pad(features, (0, 63 - len(features)))
|
| 68 |
+
|
| 69 |
+
return features
|
| 70 |
+
|
| 71 |
+
def predict_digit(features):
|
| 72 |
+
"""Predict digit from features."""
|
| 73 |
+
if interpreter is None:
|
| 74 |
+
return "Model not loaded", 0.0
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
# Prepare input
|
| 78 |
+
input_data = np.expand_dims(features, axis=0).astype(np.float32)
|
| 79 |
+
|
| 80 |
+
# Run inference
|
| 81 |
+
input_details = interpreter.get_input_details()
|
| 82 |
+
output_details = interpreter.get_output_details()
|
| 83 |
+
|
| 84 |
+
interpreter.set_tensor(input_details[0]['index'], input_data)
|
| 85 |
+
interpreter.invoke()
|
| 86 |
+
|
| 87 |
+
# Get prediction
|
| 88 |
+
output = interpreter.get_tensor(output_details[0]['index'])
|
| 89 |
+
predicted_class = np.argmax(output[0])
|
| 90 |
+
confidence = float(np.max(output[0]))
|
| 91 |
+
|
| 92 |
+
return labels[predicted_class] if predicted_class < len(labels) else str(predicted_class), confidence
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return f"Error: {str(e)}", 0.0
|
| 95 |
+
|
| 96 |
+
class DigitRecognizer(VideoTransformerBase):
|
| 97 |
+
def __init__(self):
|
| 98 |
+
self.prediction = "Show your hand"
|
| 99 |
+
self.confidence = 0.0
|
| 100 |
+
self.frame_count = 0
|
| 101 |
+
|
| 102 |
+
def recv(self, frame: VideoFrame) -> VideoFrame:
|
| 103 |
+
image = frame.to_ndarray(format="bgr24")
|
| 104 |
+
self.frame_count += 1
|
| 105 |
+
|
| 106 |
+
# Process every 5th frame to reduce computation
|
| 107 |
+
if self.frame_count % 5 == 0:
|
| 108 |
+
# Convert to RGB for MediaPipe
|
| 109 |
+
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 110 |
+
results = hands.process(rgb_image)
|
| 111 |
+
|
| 112 |
+
if results.multi_hand_landmarks:
|
| 113 |
+
# Get first hand
|
| 114 |
+
hand_landmarks = results.multi_hand_landmarks[0]
|
| 115 |
+
|
| 116 |
+
# Draw landmarks
|
| 117 |
+
mp_drawing.draw_landmarks(
|
| 118 |
+
image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
|
| 119 |
+
|
| 120 |
+
# Extract features and predict
|
| 121 |
+
features = extract_hand_features(hand_landmarks)
|
| 122 |
+
self.prediction, self.confidence = predict_digit(features)
|
| 123 |
+
else:
|
| 124 |
+
self.prediction = "No hand detected"
|
| 125 |
+
self.confidence = 0.0
|
| 126 |
+
|
| 127 |
+
# Add text overlay
|
| 128 |
+
color = (0, 255, 0) if self.confidence > 0.5 else (0, 0, 255)
|
| 129 |
+
cv2.putText(
|
| 130 |
+
image,
|
| 131 |
+
f"{self.prediction} ({self.confidence:.2f})",
|
| 132 |
+
(20, 40),
|
| 133 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 134 |
+
1.0,
|
| 135 |
+
color,
|
| 136 |
+
2
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
return VideoFrame.from_ndarray(image, format="bgr24")
|
| 140 |
+
|
| 141 |
+
# Main app
|
| 142 |
+
st.title("🔢 Sign Language Digits Interpreter")
|
| 143 |
+
st.markdown("Show hand signs for digits 0-9 to the camera!")
|
| 144 |
+
|
| 145 |
+
# Sidebar controls
|
| 146 |
+
st.sidebar.title("Controls")
|
| 147 |
+
confidence_threshold = st.sidebar.slider("Confidence Threshold", 0.0, 1.0, 0.5)
|
| 148 |
+
|
| 149 |
+
# Create columns
|
| 150 |
+
col1, col2 = st.columns([2, 1])
|
| 151 |
+
|
| 152 |
+
with col1:
|
| 153 |
+
# Webcam component
|
| 154 |
+
ctx = webrtc_streamer(
|
| 155 |
+
key="digit_recognition",
|
| 156 |
+
video_processor_factory=DigitRecognizer,
|
| 157 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 158 |
+
async_processing=True,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
with col2:
|
| 162 |
+
st.markdown("### Current Prediction")
|
| 163 |
+
|
| 164 |
+
if ctx.video_processor:
|
| 165 |
+
# Display current prediction
|
| 166 |
+
prediction_placeholder = st.empty()
|
| 167 |
+
confidence_placeholder = st.empty()
|
| 168 |
+
|
| 169 |
+
if hasattr(ctx.video_processor, 'prediction'):
|
| 170 |
+
if ctx.video_processor.confidence > confidence_threshold:
|
| 171 |
+
prediction_placeholder.markdown(f"# {ctx.video_processor.prediction}")
|
| 172 |
+
else:
|
| 173 |
+
prediction_placeholder.markdown("# -")
|
| 174 |
+
|
| 175 |
+
confidence_placeholder.progress(int(ctx.video_processor.confidence * 100))
|
| 176 |
+
st.text(f"Confidence: {ctx.video_processor.confidence:.2f}")
|
| 177 |
+
else:
|
| 178 |
+
st.warning("Turn on your webcam to start!")
|
| 179 |
+
|
| 180 |
+
# Instructions
|
| 181 |
+
st.markdown("---")
|
| 182 |
+
st.markdown("""
|
| 183 |
+
### Instructions:
|
| 184 |
+
1. Allow camera access when prompted
|
| 185 |
+
2. Show your hand clearly in the camera view
|
| 186 |
+
3. Make digit signs (0-9) with your hand
|
| 187 |
+
4. Hold the sign steady for best results
|
| 188 |
+
5. Adjust confidence threshold if needed
|
| 189 |
+
|
| 190 |
+
### Tips:
|
| 191 |
+
- Ensure good lighting
|
| 192 |
+
- Keep your hand in the center of the frame
|
| 193 |
+
- Make clear, distinct digit signs
|
| 194 |
+
- Wait a moment between different signs
|
| 195 |
+
""")
|
simple_sign_interpreter.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from streamlit_webrtc import webrtc_streamer, VideoTransformerBase
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
from av import VideoFrame
|
| 6 |
+
from sign_utils import SignLanguagePredictor, MediaPipeLandmarksModel
|
| 7 |
+
|
| 8 |
+
# Page config
|
| 9 |
+
st.set_page_config(
|
| 10 |
+
page_title="Sign Language Interpreter",
|
| 11 |
+
page_icon="🤟",
|
| 12 |
+
layout="wide"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# Custom CSS for better UI
|
| 16 |
+
st.markdown("""
|
| 17 |
+
<style>
|
| 18 |
+
.main-header {
|
| 19 |
+
padding: 1rem;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
margin-bottom: 2rem;
|
| 22 |
+
text-align: center;
|
| 23 |
+
}
|
| 24 |
+
.prediction-box {
|
| 25 |
+
background-color: #f0f2f6;
|
| 26 |
+
border-radius: 10px;
|
| 27 |
+
padding: 1rem;
|
| 28 |
+
margin: 1rem 0;
|
| 29 |
+
text-align: center;
|
| 30 |
+
}
|
| 31 |
+
.instruction-box {
|
| 32 |
+
background-color: #ffffff;
|
| 33 |
+
border: 1px solid #e0e0e0;
|
| 34 |
+
border-radius: 10px;
|
| 35 |
+
padding: 1rem;
|
| 36 |
+
margin: 1rem 0;
|
| 37 |
+
}
|
| 38 |
+
</style>
|
| 39 |
+
""", unsafe_allow_html=True)
|
| 40 |
+
|
| 41 |
+
# Initialize models
|
| 42 |
+
@st.cache_resource
|
| 43 |
+
def load_models():
|
| 44 |
+
try:
|
| 45 |
+
predictor = SignLanguagePredictor(
|
| 46 |
+
model_path="models/model.tflite",
|
| 47 |
+
labels_path="models/labels.txt"
|
| 48 |
+
)
|
| 49 |
+
return predictor
|
| 50 |
+
except Exception as e:
|
| 51 |
+
st.error(f"Error loading models: {str(e)}")
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
predictor = load_models()
|
| 55 |
+
|
| 56 |
+
class SignInterpreter(VideoTransformerBase):
|
| 57 |
+
def __init__(self, confidence_threshold=0.7):
|
| 58 |
+
self.predictor = predictor
|
| 59 |
+
self.landmark_model = MediaPipeLandmarksModel()
|
| 60 |
+
self.result_text = "Show your hand sign"
|
| 61 |
+
self.confidence = 0.0
|
| 62 |
+
self.landmarks = None
|
| 63 |
+
self.prediction_counter = {}
|
| 64 |
+
self.last_prediction = None
|
| 65 |
+
self.confidence_threshold = confidence_threshold
|
| 66 |
+
self.frame_count = 0
|
| 67 |
+
self.prediction_history = []
|
| 68 |
+
self.stable_prediction = None
|
| 69 |
+
self.stable_confidence = 0.0
|
| 70 |
+
|
| 71 |
+
# Enhanced prediction smoothing
|
| 72 |
+
self.prediction_buffer = []
|
| 73 |
+
self.buffer_size = 8 # Reduced for faster response
|
| 74 |
+
self.min_consistent_frames = 3 # Reduced for faster response
|
| 75 |
+
|
| 76 |
+
def recv(self, frame: VideoFrame) -> VideoFrame:
|
| 77 |
+
image = frame.to_ndarray(format="bgr24")
|
| 78 |
+
self.frame_count += 1
|
| 79 |
+
|
| 80 |
+
if self.predictor:
|
| 81 |
+
# Get landmarks
|
| 82 |
+
self.landmarks = self.landmark_model.predict_landmarks(image)
|
| 83 |
+
|
| 84 |
+
# Get prediction if hand is detected
|
| 85 |
+
if self.landmarks:
|
| 86 |
+
try:
|
| 87 |
+
# Use temporal prediction instead of static
|
| 88 |
+
prediction, raw_confidence = self.predictor.predict(image)
|
| 89 |
+
|
| 90 |
+
# Add to prediction buffer with higher threshold for confidence
|
| 91 |
+
if raw_confidence > 0.4: # Only add predictions with decent confidence
|
| 92 |
+
self.prediction_buffer.append((prediction, raw_confidence))
|
| 93 |
+
if len(self.prediction_buffer) > self.buffer_size:
|
| 94 |
+
self.prediction_buffer.pop(0)
|
| 95 |
+
|
| 96 |
+
# Get smoothed prediction
|
| 97 |
+
smoothed_prediction, smoothed_confidence = self._get_smoothed_prediction()
|
| 98 |
+
|
| 99 |
+
# Update current values
|
| 100 |
+
self.confidence = smoothed_confidence
|
| 101 |
+
|
| 102 |
+
# Check if prediction meets confidence threshold
|
| 103 |
+
if smoothed_confidence > self.confidence_threshold:
|
| 104 |
+
# Count consecutive predictions
|
| 105 |
+
if smoothed_prediction not in self.prediction_counter:
|
| 106 |
+
self.prediction_counter = {smoothed_prediction: 1}
|
| 107 |
+
else:
|
| 108 |
+
self.prediction_counter[smoothed_prediction] += 1
|
| 109 |
+
|
| 110 |
+
# Update stable prediction if we have enough consistent frames
|
| 111 |
+
max_pred = max(self.prediction_counter.items(), key=lambda x: x[1])
|
| 112 |
+
if max_pred[1] >= self.min_consistent_frames:
|
| 113 |
+
self.stable_prediction = smoothed_prediction
|
| 114 |
+
self.stable_confidence = smoothed_confidence
|
| 115 |
+
self.result_text = f"Sign: {smoothed_prediction}"
|
| 116 |
+
self.last_prediction = smoothed_prediction
|
| 117 |
+
else:
|
| 118 |
+
# Reset if confidence is too low
|
| 119 |
+
if self.frame_count % 30 == 0: # Reset every 30 frames
|
| 120 |
+
self.prediction_counter = {}
|
| 121 |
+
self.result_text = "Show your hand sign"
|
| 122 |
+
|
| 123 |
+
except Exception as e:
|
| 124 |
+
self.result_text = "Processing..."
|
| 125 |
+
self.confidence = 0.0
|
| 126 |
+
self.prediction_counter = {}
|
| 127 |
+
else:
|
| 128 |
+
self.result_text = "No hand detected"
|
| 129 |
+
self.confidence = 0.0
|
| 130 |
+
self.prediction_counter = {}
|
| 131 |
+
self.prediction_buffer = []
|
| 132 |
+
|
| 133 |
+
# Draw landmarks with better visualization
|
| 134 |
+
if self.landmarks:
|
| 135 |
+
image = self.landmark_model.draw_landmarks(image, self.landmarks)
|
| 136 |
+
|
| 137 |
+
# Enhanced text overlay with better positioning
|
| 138 |
+
self._draw_prediction_overlay(image)
|
| 139 |
+
|
| 140 |
+
return VideoFrame.from_ndarray(image, format="bgr24")
|
| 141 |
+
|
| 142 |
+
def _get_smoothed_prediction(self):
|
| 143 |
+
"""Get smoothed prediction from buffer"""
|
| 144 |
+
if not self.prediction_buffer:
|
| 145 |
+
return "No prediction", 0.0
|
| 146 |
+
|
| 147 |
+
# Get predictions from last few frames
|
| 148 |
+
recent_predictions = self.prediction_buffer[-5:] if len(self.prediction_buffer) >= 5 else self.prediction_buffer
|
| 149 |
+
|
| 150 |
+
# Count occurrences of each prediction
|
| 151 |
+
prediction_counts = {}
|
| 152 |
+
confidence_sums = {}
|
| 153 |
+
|
| 154 |
+
for pred, conf in recent_predictions:
|
| 155 |
+
if pred not in prediction_counts:
|
| 156 |
+
prediction_counts[pred] = 0
|
| 157 |
+
confidence_sums[pred] = 0.0
|
| 158 |
+
prediction_counts[pred] += 1
|
| 159 |
+
confidence_sums[pred] += conf
|
| 160 |
+
|
| 161 |
+
# Get most frequent prediction
|
| 162 |
+
if prediction_counts:
|
| 163 |
+
most_frequent = max(prediction_counts.items(), key=lambda x: x[1])
|
| 164 |
+
avg_confidence = confidence_sums[most_frequent[0]] / prediction_counts[most_frequent[0]]
|
| 165 |
+
return most_frequent[0], avg_confidence
|
| 166 |
+
|
| 167 |
+
return "No prediction", 0.0
|
| 168 |
+
|
| 169 |
+
def _draw_prediction_overlay(self, image):
|
| 170 |
+
"""Draw enhanced prediction overlay"""
|
| 171 |
+
height, width = image.shape[:2]
|
| 172 |
+
|
| 173 |
+
# Background rectangle for better text visibility
|
| 174 |
+
overlay = image.copy()
|
| 175 |
+
cv2.rectangle(overlay, (10, 10), (width-10, 100), (0, 0, 0), -1)
|
| 176 |
+
cv2.addWeighted(overlay, 0.7, image, 0.3, 0, image)
|
| 177 |
+
|
| 178 |
+
# Main prediction text
|
| 179 |
+
color = (0, 255, 0) if self.confidence > self.confidence_threshold else (0, 165, 255)
|
| 180 |
+
cv2.putText(
|
| 181 |
+
image,
|
| 182 |
+
self.result_text,
|
| 183 |
+
(20, 40),
|
| 184 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 185 |
+
1.0,
|
| 186 |
+
color,
|
| 187 |
+
2
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
# Confidence text
|
| 191 |
+
cv2.putText(
|
| 192 |
+
image,
|
| 193 |
+
f"Confidence: {self.confidence:.2f}",
|
| 194 |
+
(20, 70),
|
| 195 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 196 |
+
0.6,
|
| 197 |
+
(255, 255, 255),
|
| 198 |
+
1
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
# Threshold indicator
|
| 202 |
+
threshold_color = (0, 255, 0) if self.confidence > self.confidence_threshold else (0, 0, 255)
|
| 203 |
+
cv2.putText(
|
| 204 |
+
image,
|
| 205 |
+
f"Threshold: {self.confidence_threshold:.2f}",
|
| 206 |
+
(20, 90),
|
| 207 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 208 |
+
0.5,
|
| 209 |
+
threshold_color,
|
| 210 |
+
1
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
def main():
|
| 214 |
+
# Header
|
| 215 |
+
st.markdown("""
|
| 216 |
+
<div class="main-header">
|
| 217 |
+
<h1>🤟 Sign Language Interpreter</h1>
|
| 218 |
+
<p>Real-time sign language recognition using computer vision</p>
|
| 219 |
+
</div>
|
| 220 |
+
""", unsafe_allow_html=True)
|
| 221 |
+
|
| 222 |
+
# Sidebar controls
|
| 223 |
+
st.sidebar.title("⚙️ Controls")
|
| 224 |
+
confidence_threshold = st.sidebar.slider(
|
| 225 |
+
"Detection Confidence",
|
| 226 |
+
min_value=0.0,
|
| 227 |
+
max_value=1.0,
|
| 228 |
+
value=0.5, # Lowered for better live detection
|
| 229 |
+
help="Adjust the confidence threshold for sign detection"
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
# Create two columns for layout
|
| 233 |
+
col1, col2 = st.columns([2, 1])
|
| 234 |
+
|
| 235 |
+
with col1:
|
| 236 |
+
# Webcam component
|
| 237 |
+
st.markdown("### 📹 Camera Feed")
|
| 238 |
+
ctx = webrtc_streamer(
|
| 239 |
+
key="sign_language",
|
| 240 |
+
video_processor_factory=lambda: SignInterpreter(confidence_threshold),
|
| 241 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 242 |
+
async_processing=True,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
with col2:
|
| 246 |
+
# Current prediction display
|
| 247 |
+
st.markdown("### 🎯 Current Sign")
|
| 248 |
+
prediction_placeholder = st.empty()
|
| 249 |
+
confidence_meter = st.progress(0)
|
| 250 |
+
|
| 251 |
+
# Update prediction and confidence
|
| 252 |
+
if ctx.video_transformer:
|
| 253 |
+
if ctx.video_transformer.last_prediction and ctx.video_transformer.confidence > confidence_threshold:
|
| 254 |
+
prediction_placeholder.markdown(f"""
|
| 255 |
+
<div class="prediction-box">
|
| 256 |
+
<h2>{ctx.video_transformer.last_prediction}</h2>
|
| 257 |
+
</div>
|
| 258 |
+
""", unsafe_allow_html=True)
|
| 259 |
+
else:
|
| 260 |
+
prediction_placeholder.markdown("""
|
| 261 |
+
<div class="prediction-box">
|
| 262 |
+
<h2>-</h2>
|
| 263 |
+
</div>
|
| 264 |
+
""", unsafe_allow_html=True)
|
| 265 |
+
|
| 266 |
+
confidence_meter.progress(int(ctx.video_transformer.confidence * 100))
|
| 267 |
+
else:
|
| 268 |
+
st.info("📷 Start the webcam to begin recognition")
|
| 269 |
+
|
| 270 |
+
# Instructions
|
| 271 |
+
st.markdown("---")
|
| 272 |
+
|
| 273 |
+
# Create three columns for better layout
|
| 274 |
+
col1, col2, col3 = st.columns(3)
|
| 275 |
+
|
| 276 |
+
with col1:
|
| 277 |
+
st.markdown("### 📝 Instructions")
|
| 278 |
+
st.markdown("""
|
| 279 |
+
1. Allow camera access when prompted
|
| 280 |
+
2. Position your hand clearly in the camera view
|
| 281 |
+
3. Make hand signs for:
|
| 282 |
+
- **Numbers:** 1, 2, 4, 5, 6, 7, 8
|
| 283 |
+
- **Letters:** A through X (except E)
|
| 284 |
+
4. Hold each sign steady for best recognition
|
| 285 |
+
5. Adjust the confidence threshold if needed
|
| 286 |
+
""")
|
| 287 |
+
|
| 288 |
+
with col2:
|
| 289 |
+
st.markdown("### 💡 Tips for Better Recognition")
|
| 290 |
+
st.markdown("""
|
| 291 |
+
- Ensure good lighting on your hands
|
| 292 |
+
- Keep your hand within the camera frame
|
| 293 |
+
- Make clear, distinct gestures
|
| 294 |
+
- Avoid rapid movements
|
| 295 |
+
- Use a plain background
|
| 296 |
+
- Keep hand at comfortable distance from camera
|
| 297 |
+
""")
|
| 298 |
+
|
| 299 |
+
with col3:
|
| 300 |
+
st.markdown("### 📊 Supported Gestures")
|
| 301 |
+
st.markdown("**Numbers:**")
|
| 302 |
+
st.markdown("1, 2, 4, 5, 6, 7, 8")
|
| 303 |
+
st.markdown("**Letters:**")
|
| 304 |
+
st.markdown("A, B, C, D, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X")
|
| 305 |
+
|
| 306 |
+
if __name__ == "__main__":
|
| 307 |
+
main()
|
test_system.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from sign_utils import MediaPipeLandmarksModel, SignLanguagePredictor
|
| 6 |
+
|
| 7 |
+
def test_camera():
|
| 8 |
+
"""Test if camera is working."""
|
| 9 |
+
print("Testing camera...")
|
| 10 |
+
cap = cv2.VideoCapture(0)
|
| 11 |
+
|
| 12 |
+
if not cap.isOpened():
|
| 13 |
+
print("❌ Camera not accessible")
|
| 14 |
+
return False
|
| 15 |
+
|
| 16 |
+
ret, frame = cap.read()
|
| 17 |
+
if not ret:
|
| 18 |
+
print("❌ Cannot read from camera")
|
| 19 |
+
cap.release()
|
| 20 |
+
return False
|
| 21 |
+
|
| 22 |
+
print("✅ Camera working")
|
| 23 |
+
cap.release()
|
| 24 |
+
return True
|
| 25 |
+
|
| 26 |
+
def test_mediapipe():
|
| 27 |
+
"""Test MediaPipe hand detection."""
|
| 28 |
+
print("Testing MediaPipe...")
|
| 29 |
+
try:
|
| 30 |
+
landmark_model = MediaPipeLandmarksModel()
|
| 31 |
+
|
| 32 |
+
# Create a dummy image
|
| 33 |
+
dummy_image = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 34 |
+
landmarks = landmark_model.predict_landmarks(dummy_image)
|
| 35 |
+
|
| 36 |
+
print("✅ MediaPipe working")
|
| 37 |
+
return True
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"❌ MediaPipe error: {e}")
|
| 40 |
+
return False
|
| 41 |
+
|
| 42 |
+
def test_model_files():
|
| 43 |
+
"""Test if model files exist."""
|
| 44 |
+
print("Testing model files...")
|
| 45 |
+
|
| 46 |
+
model_path = "models/model.tflite"
|
| 47 |
+
labels_path = "models/labels.txt"
|
| 48 |
+
|
| 49 |
+
if not os.path.exists(model_path):
|
| 50 |
+
print(f"❌ Model file not found: {model_path}")
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
if not os.path.exists(labels_path):
|
| 54 |
+
print(f"❌ Labels file not found: {labels_path}")
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
print("✅ Model files found")
|
| 58 |
+
return True
|
| 59 |
+
|
| 60 |
+
def test_predictor():
|
| 61 |
+
"""Test the sign language predictor."""
|
| 62 |
+
print("Testing predictor...")
|
| 63 |
+
|
| 64 |
+
if not test_model_files():
|
| 65 |
+
return False
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
predictor = SignLanguagePredictor(
|
| 69 |
+
model_path="models/model.tflite",
|
| 70 |
+
labels_path="models/labels.txt"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# Create a dummy image
|
| 74 |
+
dummy_image = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 75 |
+
prediction, confidence = predictor.predict(dummy_image)
|
| 76 |
+
|
| 77 |
+
print(f"✅ Predictor working - Sample prediction: {prediction} ({confidence:.2f})")
|
| 78 |
+
return True
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"❌ Predictor error: {e}")
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
def test_dependencies():
|
| 84 |
+
"""Test if all dependencies are installed."""
|
| 85 |
+
print("Testing dependencies...")
|
| 86 |
+
|
| 87 |
+
required_modules = [
|
| 88 |
+
'cv2', 'numpy', 'tensorflow', 'mediapipe',
|
| 89 |
+
'streamlit', 'sklearn'
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
missing_modules = []
|
| 93 |
+
|
| 94 |
+
for module in required_modules:
|
| 95 |
+
try:
|
| 96 |
+
__import__(module)
|
| 97 |
+
except ImportError:
|
| 98 |
+
missing_modules.append(module)
|
| 99 |
+
|
| 100 |
+
if missing_modules:
|
| 101 |
+
print(f"❌ Missing modules: {missing_modules}")
|
| 102 |
+
return False
|
| 103 |
+
|
| 104 |
+
print("✅ All dependencies installed")
|
| 105 |
+
return True
|
| 106 |
+
|
| 107 |
+
def run_full_test():
|
| 108 |
+
"""Run complete system test."""
|
| 109 |
+
print("🤟 Sign Language Interpreter - System Test")
|
| 110 |
+
print("=" * 50)
|
| 111 |
+
|
| 112 |
+
tests = [
|
| 113 |
+
("Dependencies", test_dependencies),
|
| 114 |
+
("Camera", test_camera),
|
| 115 |
+
("MediaPipe", test_mediapipe),
|
| 116 |
+
("Model Files", test_model_files),
|
| 117 |
+
("Predictor", test_predictor)
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
passed = 0
|
| 121 |
+
total = len(tests)
|
| 122 |
+
|
| 123 |
+
for test_name, test_func in tests:
|
| 124 |
+
print(f"\n{test_name}:")
|
| 125 |
+
if test_func():
|
| 126 |
+
passed += 1
|
| 127 |
+
else:
|
| 128 |
+
print(f" Fix this issue before proceeding")
|
| 129 |
+
|
| 130 |
+
print(f"\n{'='*50}")
|
| 131 |
+
print(f"Test Results: {passed}/{total} tests passed")
|
| 132 |
+
|
| 133 |
+
if passed == total:
|
| 134 |
+
print("🎉 All tests passed! System is ready to use.")
|
| 135 |
+
print("\nNext steps:")
|
| 136 |
+
print("1. Run 'streamlit run app.py' to start the application")
|
| 137 |
+
print("2. Or run 'python data_collection.py' to collect custom data")
|
| 138 |
+
else:
|
| 139 |
+
print("❌ Some tests failed. Please fix the issues above.")
|
| 140 |
+
|
| 141 |
+
if not test_model_files():
|
| 142 |
+
print("\n💡 Tip: Run 'python create_sample_model.py' to create a sample model")
|
| 143 |
+
|
| 144 |
+
def main():
|
| 145 |
+
if len(sys.argv) > 1 and sys.argv[1] == "--quick":
|
| 146 |
+
# Quick test - just check if basic components work
|
| 147 |
+
if test_dependencies() and test_mediapipe():
|
| 148 |
+
print("✅ Quick test passed!")
|
| 149 |
+
else:
|
| 150 |
+
print("❌ Quick test failed!")
|
| 151 |
+
else:
|
| 152 |
+
run_full_test()
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
main()
|
train_digits_model.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import tensorflow as tf
|
| 3 |
+
from tensorflow.keras.models import Sequential
|
| 4 |
+
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization
|
| 5 |
+
from tensorflow.keras.preprocessing.image import ImageDataGenerator
|
| 6 |
+
from sklearn.model_selection import train_test_split
|
| 7 |
+
import cv2
|
| 8 |
+
import os
|
| 9 |
+
import glob
|
| 10 |
+
from sign_utils import MediaPipeLandmarksModel
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
|
| 13 |
+
class DigitSignTrainer:
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self.landmark_model = MediaPipeLandmarksModel()
|
| 16 |
+
self.img_size = (224, 224)
|
| 17 |
+
|
| 18 |
+
def load_image_dataset(self, dataset_path="dataset"):
|
| 19 |
+
"""Load digit images and extract landmarks."""
|
| 20 |
+
X_images = []
|
| 21 |
+
X_landmarks = []
|
| 22 |
+
y = []
|
| 23 |
+
|
| 24 |
+
print("Loading and processing images...")
|
| 25 |
+
|
| 26 |
+
# Process each digit folder
|
| 27 |
+
for digit in range(10):
|
| 28 |
+
digit_path = os.path.join(dataset_path, str(digit))
|
| 29 |
+
if not os.path.exists(digit_path):
|
| 30 |
+
continue
|
| 31 |
+
|
| 32 |
+
image_files = glob.glob(os.path.join(digit_path, "*.JPG"))
|
| 33 |
+
print(f"Processing {len(image_files)} images for digit {digit}")
|
| 34 |
+
|
| 35 |
+
for img_file in image_files:
|
| 36 |
+
# Load and preprocess image
|
| 37 |
+
img = cv2.imread(img_file)
|
| 38 |
+
if img is None:
|
| 39 |
+
continue
|
| 40 |
+
|
| 41 |
+
# Resize image
|
| 42 |
+
img_resized = cv2.resize(img, self.img_size)
|
| 43 |
+
|
| 44 |
+
# Extract landmarks
|
| 45 |
+
landmarks = self.landmark_model.predict_landmarks(img)
|
| 46 |
+
|
| 47 |
+
if landmarks is not None:
|
| 48 |
+
# Normalize landmarks
|
| 49 |
+
landmarks_normalized = self.landmark_model.normalize_landmarks(landmarks)
|
| 50 |
+
|
| 51 |
+
X_images.append(img_resized)
|
| 52 |
+
X_landmarks.append(landmarks_normalized)
|
| 53 |
+
y.append(digit)
|
| 54 |
+
|
| 55 |
+
print(f"Successfully processed {len(X_images)} images with landmarks")
|
| 56 |
+
|
| 57 |
+
# Convert to numpy arrays
|
| 58 |
+
X_images = np.array(X_images) / 255.0 # Normalize pixel values
|
| 59 |
+
X_landmarks = np.array(X_landmarks)
|
| 60 |
+
y = np.array(y)
|
| 61 |
+
|
| 62 |
+
return X_images, X_landmarks, y
|
| 63 |
+
|
| 64 |
+
def create_cnn_model(self, num_classes=10):
|
| 65 |
+
"""Create CNN model for image classification."""
|
| 66 |
+
model = Sequential([
|
| 67 |
+
Conv2D(32, (3, 3), activation='relu', input_shape=(224, 224, 3)),
|
| 68 |
+
BatchNormalization(),
|
| 69 |
+
MaxPooling2D(2, 2),
|
| 70 |
+
|
| 71 |
+
Conv2D(64, (3, 3), activation='relu'),
|
| 72 |
+
BatchNormalization(),
|
| 73 |
+
MaxPooling2D(2, 2),
|
| 74 |
+
|
| 75 |
+
Conv2D(128, (3, 3), activation='relu'),
|
| 76 |
+
BatchNormalization(),
|
| 77 |
+
MaxPooling2D(2, 2),
|
| 78 |
+
|
| 79 |
+
Conv2D(256, (3, 3), activation='relu'),
|
| 80 |
+
BatchNormalization(),
|
| 81 |
+
MaxPooling2D(2, 2),
|
| 82 |
+
|
| 83 |
+
Flatten(),
|
| 84 |
+
Dense(512, activation='relu'),
|
| 85 |
+
Dropout(0.5),
|
| 86 |
+
Dense(256, activation='relu'),
|
| 87 |
+
Dropout(0.3),
|
| 88 |
+
Dense(num_classes, activation='softmax')
|
| 89 |
+
])
|
| 90 |
+
|
| 91 |
+
model.compile(
|
| 92 |
+
optimizer='adam',
|
| 93 |
+
loss='sparse_categorical_crossentropy',
|
| 94 |
+
metrics=['accuracy']
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
return model
|
| 98 |
+
|
| 99 |
+
def create_landmark_model(self, num_classes=10):
|
| 100 |
+
"""Create model for landmark-based classification."""
|
| 101 |
+
model = Sequential([
|
| 102 |
+
Dense(128, activation='relu', input_shape=(63,)), # 21 landmarks * 3 coordinates
|
| 103 |
+
BatchNormalization(),
|
| 104 |
+
Dropout(0.3),
|
| 105 |
+
|
| 106 |
+
Dense(64, activation='relu'),
|
| 107 |
+
BatchNormalization(),
|
| 108 |
+
Dropout(0.3),
|
| 109 |
+
|
| 110 |
+
Dense(32, activation='relu'),
|
| 111 |
+
Dropout(0.2),
|
| 112 |
+
|
| 113 |
+
Dense(num_classes, activation='softmax')
|
| 114 |
+
])
|
| 115 |
+
|
| 116 |
+
model.compile(
|
| 117 |
+
optimizer='adam',
|
| 118 |
+
loss='sparse_categorical_crossentropy',
|
| 119 |
+
metrics=['accuracy']
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
return model
|
| 123 |
+
|
| 124 |
+
def create_combined_model(self, num_classes=10):
|
| 125 |
+
"""Create combined model using both images and landmarks."""
|
| 126 |
+
# Image branch
|
| 127 |
+
image_input = tf.keras.Input(shape=(224, 224, 3), name='image_input')
|
| 128 |
+
x1 = Conv2D(32, (3, 3), activation='relu')(image_input)
|
| 129 |
+
x1 = BatchNormalization()(x1)
|
| 130 |
+
x1 = MaxPooling2D(2, 2)(x1)
|
| 131 |
+
x1 = Conv2D(64, (3, 3), activation='relu')(x1)
|
| 132 |
+
x1 = BatchNormalization()(x1)
|
| 133 |
+
x1 = MaxPooling2D(2, 2)(x1)
|
| 134 |
+
x1 = Conv2D(128, (3, 3), activation='relu')(x1)
|
| 135 |
+
x1 = BatchNormalization()(x1)
|
| 136 |
+
x1 = MaxPooling2D(2, 2)(x1)
|
| 137 |
+
x1 = Flatten()(x1)
|
| 138 |
+
x1 = Dense(256, activation='relu')(x1)
|
| 139 |
+
x1 = Dropout(0.5)(x1)
|
| 140 |
+
|
| 141 |
+
# Landmark branch
|
| 142 |
+
landmark_input = tf.keras.Input(shape=(63,), name='landmark_input')
|
| 143 |
+
x2 = Dense(128, activation='relu')(landmark_input)
|
| 144 |
+
x2 = BatchNormalization()(x2)
|
| 145 |
+
x2 = Dropout(0.3)(x2)
|
| 146 |
+
x2 = Dense(64, activation='relu')(x2)
|
| 147 |
+
x2 = Dropout(0.3)(x2)
|
| 148 |
+
|
| 149 |
+
# Combine branches
|
| 150 |
+
combined = tf.keras.layers.concatenate([x1, x2])
|
| 151 |
+
combined = Dense(128, activation='relu')(combined)
|
| 152 |
+
combined = Dropout(0.3)(combined)
|
| 153 |
+
output = Dense(num_classes, activation='softmax')(combined)
|
| 154 |
+
|
| 155 |
+
model = tf.keras.Model(inputs=[image_input, landmark_input], outputs=output)
|
| 156 |
+
model.compile(
|
| 157 |
+
optimizer='adam',
|
| 158 |
+
loss='sparse_categorical_crossentropy',
|
| 159 |
+
metrics=['accuracy']
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
return model
|
| 163 |
+
|
| 164 |
+
def train_model(self, model_type='combined'):
|
| 165 |
+
"""Train the model."""
|
| 166 |
+
# Load dataset
|
| 167 |
+
X_images, X_landmarks, y = self.load_image_dataset()
|
| 168 |
+
|
| 169 |
+
if len(X_images) == 0:
|
| 170 |
+
print("No valid images found with hand landmarks!")
|
| 171 |
+
return None
|
| 172 |
+
|
| 173 |
+
# Split data
|
| 174 |
+
if model_type == 'combined':
|
| 175 |
+
X_img_train, X_img_test, X_land_train, X_land_test, y_train, y_test = train_test_split(
|
| 176 |
+
X_images, X_landmarks, y, test_size=0.2, random_state=42, stratify=y
|
| 177 |
+
)
|
| 178 |
+
X_img_train, X_img_val, X_land_train, X_land_val, y_train, y_val = train_test_split(
|
| 179 |
+
X_img_train, X_land_train, y_train, test_size=0.2, random_state=42, stratify=y_train
|
| 180 |
+
)
|
| 181 |
+
else:
|
| 182 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 183 |
+
X_images if model_type == 'cnn' else X_landmarks, y,
|
| 184 |
+
test_size=0.2, random_state=42, stratify=y
|
| 185 |
+
)
|
| 186 |
+
X_train, X_val, y_train, y_val = train_test_split(
|
| 187 |
+
X_train, y_train, test_size=0.2, random_state=42, stratify=y_train
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
print(f"Training set: {len(y_train)} samples")
|
| 191 |
+
print(f"Validation set: {len(y_val)} samples")
|
| 192 |
+
print(f"Test set: {len(y_test)} samples")
|
| 193 |
+
|
| 194 |
+
# Create model
|
| 195 |
+
if model_type == 'cnn':
|
| 196 |
+
model = self.create_cnn_model()
|
| 197 |
+
train_data = (X_train, y_train)
|
| 198 |
+
val_data = (X_val, y_val)
|
| 199 |
+
test_data = (X_test, y_test)
|
| 200 |
+
elif model_type == 'landmark':
|
| 201 |
+
model = self.create_landmark_model()
|
| 202 |
+
train_data = (X_train, y_train)
|
| 203 |
+
val_data = (X_val, y_val)
|
| 204 |
+
test_data = (X_test, y_test)
|
| 205 |
+
else: # combined
|
| 206 |
+
model = self.create_combined_model()
|
| 207 |
+
train_data = ([X_img_train, X_land_train], y_train)
|
| 208 |
+
val_data = ([X_img_val, X_land_val], y_val)
|
| 209 |
+
test_data = ([X_img_test, X_land_test], y_test)
|
| 210 |
+
|
| 211 |
+
print(f"\nModel Summary ({model_type}):")
|
| 212 |
+
model.summary()
|
| 213 |
+
|
| 214 |
+
# Callbacks
|
| 215 |
+
callbacks = [
|
| 216 |
+
tf.keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True),
|
| 217 |
+
tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5),
|
| 218 |
+
tf.keras.callbacks.ModelCheckpoint(
|
| 219 |
+
f'models/best_{model_type}_model.h5',
|
| 220 |
+
save_best_only=True
|
| 221 |
+
)
|
| 222 |
+
]
|
| 223 |
+
|
| 224 |
+
# Train model
|
| 225 |
+
print(f"\nTraining {model_type} model...")
|
| 226 |
+
history = model.fit(
|
| 227 |
+
train_data[0], train_data[1],
|
| 228 |
+
validation_data=val_data,
|
| 229 |
+
epochs=50,
|
| 230 |
+
batch_size=32,
|
| 231 |
+
callbacks=callbacks,
|
| 232 |
+
verbose=1
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# Evaluate model
|
| 236 |
+
print(f"\nEvaluating {model_type} model...")
|
| 237 |
+
test_loss, test_accuracy = model.evaluate(test_data[0], test_data[1], verbose=0)
|
| 238 |
+
print(f"Test Accuracy: {test_accuracy:.4f}")
|
| 239 |
+
|
| 240 |
+
# Save model
|
| 241 |
+
model.save(f'models/{model_type}_digit_model.h5')
|
| 242 |
+
|
| 243 |
+
# Convert to TFLite
|
| 244 |
+
self.convert_to_tflite(model, model_type)
|
| 245 |
+
|
| 246 |
+
# Save labels
|
| 247 |
+
labels = [str(i) for i in range(10)]
|
| 248 |
+
with open('models/labels.txt', 'w') as f:
|
| 249 |
+
f.write('\n'.join(labels))
|
| 250 |
+
|
| 251 |
+
return model, history
|
| 252 |
+
|
| 253 |
+
def convert_to_tflite(self, model, model_type):
|
| 254 |
+
"""Convert model to TensorFlow Lite."""
|
| 255 |
+
try:
|
| 256 |
+
converter = tf.lite.TFLiteConverter.from_keras_model(model)
|
| 257 |
+
converter.optimizations = [tf.lite.Optimize.DEFAULT]
|
| 258 |
+
|
| 259 |
+
tflite_model = converter.convert()
|
| 260 |
+
|
| 261 |
+
with open(f'models/{model_type}_model.tflite', 'wb') as f:
|
| 262 |
+
f.write(tflite_model)
|
| 263 |
+
|
| 264 |
+
# Also save as the default model.tflite
|
| 265 |
+
with open('models/model.tflite', 'wb') as f:
|
| 266 |
+
f.write(tflite_model)
|
| 267 |
+
|
| 268 |
+
print(f"Model converted to TFLite: {model_type}_model.tflite")
|
| 269 |
+
|
| 270 |
+
except Exception as e:
|
| 271 |
+
print(f"TFLite conversion failed: {e}")
|
| 272 |
+
|
| 273 |
+
def plot_training_history(self, history, model_type):
|
| 274 |
+
"""Plot training history."""
|
| 275 |
+
plt.figure(figsize=(12, 4))
|
| 276 |
+
|
| 277 |
+
plt.subplot(1, 2, 1)
|
| 278 |
+
plt.plot(history.history['accuracy'], label='Training Accuracy')
|
| 279 |
+
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
|
| 280 |
+
plt.title(f'{model_type.title()} Model Accuracy')
|
| 281 |
+
plt.xlabel('Epoch')
|
| 282 |
+
plt.ylabel('Accuracy')
|
| 283 |
+
plt.legend()
|
| 284 |
+
|
| 285 |
+
plt.subplot(1, 2, 2)
|
| 286 |
+
plt.plot(history.history['loss'], label='Training Loss')
|
| 287 |
+
plt.plot(history.history['val_loss'], label='Validation Loss')
|
| 288 |
+
plt.title(f'{model_type.title()} Model Loss')
|
| 289 |
+
plt.xlabel('Epoch')
|
| 290 |
+
plt.ylabel('Loss')
|
| 291 |
+
plt.legend()
|
| 292 |
+
|
| 293 |
+
plt.tight_layout()
|
| 294 |
+
plt.savefig(f'logs/{model_type}_training_history.png')
|
| 295 |
+
plt.show()
|
| 296 |
+
|
| 297 |
+
def main():
|
| 298 |
+
trainer = DigitSignTrainer()
|
| 299 |
+
|
| 300 |
+
print("Sign Language Digits Model Training")
|
| 301 |
+
print("=" * 40)
|
| 302 |
+
|
| 303 |
+
# Check if dataset exists
|
| 304 |
+
if not os.path.exists("dataset"):
|
| 305 |
+
print("Dataset not found!")
|
| 306 |
+
return
|
| 307 |
+
|
| 308 |
+
# Train different models
|
| 309 |
+
models_to_train = ['landmark', 'cnn', 'combined']
|
| 310 |
+
|
| 311 |
+
for model_type in models_to_train:
|
| 312 |
+
print(f"\n{'='*50}")
|
| 313 |
+
print(f"Training {model_type.upper()} model")
|
| 314 |
+
print(f"{'='*50}")
|
| 315 |
+
|
| 316 |
+
try:
|
| 317 |
+
model, history = trainer.train_model(model_type)
|
| 318 |
+
if model is not None:
|
| 319 |
+
trainer.plot_training_history(history, model_type)
|
| 320 |
+
print(f"{model_type.title()} model training completed!")
|
| 321 |
+
else:
|
| 322 |
+
print(f"Failed to train {model_type} model")
|
| 323 |
+
except Exception as e:
|
| 324 |
+
print(f"Error training {model_type} model: {e}")
|
| 325 |
+
|
| 326 |
+
print("\nTraining completed!")
|
| 327 |
+
print("You can now run the main application: streamlit run app.py")
|
| 328 |
+
|
| 329 |
+
if __name__ == "__main__":
|
| 330 |
+
main()
|
train_gesture_dataset.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import mediapipe as mp
|
| 5 |
+
import tensorflow as tf
|
| 6 |
+
from tensorflow import keras
|
| 7 |
+
from sklearn.model_selection import train_test_split
|
| 8 |
+
from sklearn.preprocessing import LabelEncoder
|
| 9 |
+
import matplotlib.pyplot as plt
|
| 10 |
+
import seaborn as sns
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
import json
|
| 13 |
+
import pickle
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
class GestureDatasetTrainer:
|
| 17 |
+
def __init__(self, dataset_path, output_dir="models"):
|
| 18 |
+
self.dataset_path = dataset_path
|
| 19 |
+
self.output_dir = output_dir
|
| 20 |
+
self.mp_hands = mp.solutions.hands
|
| 21 |
+
self.hands = self.mp_hands.Hands(
|
| 22 |
+
static_image_mode=True,
|
| 23 |
+
max_num_hands=1,
|
| 24 |
+
min_detection_confidence=0.7,
|
| 25 |
+
min_tracking_confidence=0.5
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Create output directory
|
| 29 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 30 |
+
|
| 31 |
+
# Initialize data storage
|
| 32 |
+
self.features = []
|
| 33 |
+
self.labels = []
|
| 34 |
+
self.class_names = []
|
| 35 |
+
self.label_encoder = LabelEncoder()
|
| 36 |
+
|
| 37 |
+
def extract_hand_landmarks(self, image_path):
|
| 38 |
+
"""Extract hand landmarks from image."""
|
| 39 |
+
try:
|
| 40 |
+
# Read image
|
| 41 |
+
image = cv2.imread(image_path)
|
| 42 |
+
if image is None:
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
# Convert to RGB
|
| 46 |
+
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 47 |
+
|
| 48 |
+
# Process with MediaPipe
|
| 49 |
+
results = self.hands.process(rgb_image)
|
| 50 |
+
|
| 51 |
+
if results.multi_hand_landmarks:
|
| 52 |
+
# Get first hand landmarks
|
| 53 |
+
hand_landmarks = results.multi_hand_landmarks[0]
|
| 54 |
+
|
| 55 |
+
# Convert to numpy array
|
| 56 |
+
landmarks_array = np.array([(lm.x, lm.y, lm.z) for lm in hand_landmarks.landmark])
|
| 57 |
+
|
| 58 |
+
# Normalize relative to wrist (first landmark)
|
| 59 |
+
wrist = landmarks_array[0]
|
| 60 |
+
normalized = landmarks_array - wrist
|
| 61 |
+
|
| 62 |
+
# Scale by hand size (distance from wrist to middle finger tip)
|
| 63 |
+
hand_size = np.linalg.norm(landmarks_array[12] - wrist)
|
| 64 |
+
if hand_size > 0:
|
| 65 |
+
normalized = normalized / hand_size
|
| 66 |
+
|
| 67 |
+
# Flatten to 63 features (21 landmarks * 3 coordinates)
|
| 68 |
+
features = normalized.flatten()
|
| 69 |
+
|
| 70 |
+
# Ensure exactly 63 features
|
| 71 |
+
if len(features) != 63:
|
| 72 |
+
features = np.pad(features, (0, max(0, 63 - len(features))))[:63]
|
| 73 |
+
|
| 74 |
+
return features
|
| 75 |
+
else:
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"Error processing {image_path}: {e}")
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
def load_dataset(self, max_samples_per_class=500, sample_ratio=0.1):
|
| 83 |
+
"""Load and process the dataset."""
|
| 84 |
+
print("Loading dataset...")
|
| 85 |
+
|
| 86 |
+
# Get all class directories
|
| 87 |
+
class_dirs = [d for d in os.listdir(self.dataset_path)
|
| 88 |
+
if os.path.isdir(os.path.join(self.dataset_path, d))]
|
| 89 |
+
class_dirs.sort() # Ensure consistent ordering
|
| 90 |
+
|
| 91 |
+
self.class_names = class_dirs
|
| 92 |
+
print(f"Found {len(class_dirs)} classes: {class_dirs}")
|
| 93 |
+
|
| 94 |
+
# Process each class
|
| 95 |
+
for class_name in tqdm(class_dirs, desc="Processing classes"):
|
| 96 |
+
class_path = os.path.join(self.dataset_path, class_name)
|
| 97 |
+
|
| 98 |
+
# Get all image files
|
| 99 |
+
image_files = [f for f in os.listdir(class_path)
|
| 100 |
+
if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
|
| 101 |
+
|
| 102 |
+
# Sample images if there are too many
|
| 103 |
+
if len(image_files) > max_samples_per_class:
|
| 104 |
+
# Take every nth image to get a good distribution
|
| 105 |
+
step = len(image_files) // max_samples_per_class
|
| 106 |
+
image_files = image_files[::step][:max_samples_per_class]
|
| 107 |
+
else:
|
| 108 |
+
# If fewer images, sample a percentage
|
| 109 |
+
sample_size = max(1, int(len(image_files) * sample_ratio))
|
| 110 |
+
image_files = image_files[:sample_size]
|
| 111 |
+
|
| 112 |
+
print(f"Processing {len(image_files)} images for class '{class_name}'")
|
| 113 |
+
|
| 114 |
+
# Process images
|
| 115 |
+
class_features = []
|
| 116 |
+
for image_file in tqdm(image_files, desc=f"Class {class_name}", leave=False):
|
| 117 |
+
image_path = os.path.join(class_path, image_file)
|
| 118 |
+
features = self.extract_hand_landmarks(image_path)
|
| 119 |
+
|
| 120 |
+
if features is not None:
|
| 121 |
+
class_features.append(features)
|
| 122 |
+
|
| 123 |
+
# Add to dataset
|
| 124 |
+
if class_features:
|
| 125 |
+
self.features.extend(class_features)
|
| 126 |
+
self.labels.extend([class_name] * len(class_features))
|
| 127 |
+
print(f"Added {len(class_features)} samples for class '{class_name}'")
|
| 128 |
+
else:
|
| 129 |
+
print(f"Warning: No valid samples found for class '{class_name}'")
|
| 130 |
+
|
| 131 |
+
# Convert to numpy arrays
|
| 132 |
+
self.features = np.array(self.features)
|
| 133 |
+
self.labels = np.array(self.labels)
|
| 134 |
+
|
| 135 |
+
# Encode labels
|
| 136 |
+
self.labels_encoded = self.label_encoder.fit_transform(self.labels)
|
| 137 |
+
|
| 138 |
+
print(f"\nDataset loaded successfully!")
|
| 139 |
+
print(f"Total samples: {len(self.features)}")
|
| 140 |
+
print(f"Feature shape: {self.features.shape}")
|
| 141 |
+
print(f"Number of classes: {len(self.class_names)}")
|
| 142 |
+
|
| 143 |
+
# Print class distribution
|
| 144 |
+
unique, counts = np.unique(self.labels, return_counts=True)
|
| 145 |
+
print("\nClass distribution:")
|
| 146 |
+
for class_name, count in zip(unique, counts):
|
| 147 |
+
print(f" {class_name}: {count} samples")
|
| 148 |
+
|
| 149 |
+
def create_model(self, input_shape=63, num_classes=36):
|
| 150 |
+
"""Create the neural network model with improved architecture."""
|
| 151 |
+
model = keras.Sequential([
|
| 152 |
+
# Input layer with data augmentation
|
| 153 |
+
keras.layers.Dense(512, activation='relu', input_shape=(input_shape,)),
|
| 154 |
+
keras.layers.Dropout(0.4),
|
| 155 |
+
keras.layers.BatchNormalization(),
|
| 156 |
+
|
| 157 |
+
keras.layers.Dense(256, activation='relu'),
|
| 158 |
+
keras.layers.Dropout(0.3),
|
| 159 |
+
keras.layers.BatchNormalization(),
|
| 160 |
+
|
| 161 |
+
keras.layers.Dense(128, activation='relu'),
|
| 162 |
+
keras.layers.Dropout(0.3),
|
| 163 |
+
keras.layers.BatchNormalization(),
|
| 164 |
+
|
| 165 |
+
keras.layers.Dense(64, activation='relu'),
|
| 166 |
+
keras.layers.Dropout(0.2),
|
| 167 |
+
keras.layers.BatchNormalization(),
|
| 168 |
+
|
| 169 |
+
keras.layers.Dense(32, activation='relu'),
|
| 170 |
+
keras.layers.Dropout(0.2),
|
| 171 |
+
|
| 172 |
+
keras.layers.Dense(num_classes, activation='softmax')
|
| 173 |
+
])
|
| 174 |
+
|
| 175 |
+
model.compile(
|
| 176 |
+
optimizer=keras.optimizers.Adam(learning_rate=0.001),
|
| 177 |
+
loss='sparse_categorical_crossentropy',
|
| 178 |
+
metrics=['accuracy']
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
return model
|
| 182 |
+
|
| 183 |
+
def train_model(self, test_size=0.2, validation_split=0.2, epochs=50, batch_size=64):
|
| 184 |
+
"""Train the model with improved configuration."""
|
| 185 |
+
print("\nPreparing data for training...")
|
| 186 |
+
|
| 187 |
+
# Filter out classes with insufficient samples for stratified split
|
| 188 |
+
unique_labels, label_counts = np.unique(self.labels_encoded, return_counts=True)
|
| 189 |
+
min_samples_required = max(2, int(1 / test_size)) # At least 2 samples, or enough for test split
|
| 190 |
+
|
| 191 |
+
# Find classes with sufficient samples
|
| 192 |
+
valid_classes = unique_labels[label_counts >= min_samples_required]
|
| 193 |
+
|
| 194 |
+
if len(valid_classes) < len(unique_labels):
|
| 195 |
+
print(f"Warning: Removing {len(unique_labels) - len(valid_classes)} classes with insufficient samples")
|
| 196 |
+
print("Classes with insufficient samples:")
|
| 197 |
+
for label, count in zip(unique_labels, label_counts):
|
| 198 |
+
if count < min_samples_required:
|
| 199 |
+
class_name = self.class_names[label] if label < len(self.class_names) else f"Class_{label}"
|
| 200 |
+
print(f" {class_name}: {count} samples (minimum required: {min_samples_required})")
|
| 201 |
+
|
| 202 |
+
# Filter data to only include valid classes
|
| 203 |
+
valid_indices = np.isin(self.labels_encoded, valid_classes)
|
| 204 |
+
filtered_features = self.features[valid_indices]
|
| 205 |
+
filtered_labels = self.labels_encoded[valid_indices]
|
| 206 |
+
|
| 207 |
+
# Update class names to only include valid classes
|
| 208 |
+
valid_class_names = [self.class_names[i] for i in valid_classes if i < len(self.class_names)]
|
| 209 |
+
|
| 210 |
+
# Remap labels to be continuous (0, 1, 2, ...)
|
| 211 |
+
label_mapping = {old_label: new_label for new_label, old_label in enumerate(valid_classes)}
|
| 212 |
+
remapped_labels = np.array([label_mapping[label] for label in filtered_labels])
|
| 213 |
+
|
| 214 |
+
print(f"Training with {len(valid_classes)} classes and {len(filtered_features)} samples")
|
| 215 |
+
|
| 216 |
+
# Calculate class weights for handling imbalance
|
| 217 |
+
from sklearn.utils.class_weight import compute_class_weight
|
| 218 |
+
class_weights = compute_class_weight(
|
| 219 |
+
'balanced',
|
| 220 |
+
classes=np.unique(remapped_labels),
|
| 221 |
+
y=remapped_labels
|
| 222 |
+
)
|
| 223 |
+
class_weight_dict = dict(enumerate(class_weights))
|
| 224 |
+
|
| 225 |
+
# Split data - use stratify only if we have enough samples
|
| 226 |
+
try:
|
| 227 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 228 |
+
filtered_features, remapped_labels,
|
| 229 |
+
test_size=test_size,
|
| 230 |
+
random_state=42,
|
| 231 |
+
stratify=remapped_labels
|
| 232 |
+
)
|
| 233 |
+
except ValueError as e:
|
| 234 |
+
print(f"Warning: Stratified split failed ({e}), using random split instead")
|
| 235 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 236 |
+
filtered_features, remapped_labels,
|
| 237 |
+
test_size=test_size,
|
| 238 |
+
random_state=42
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Update class names for saving
|
| 242 |
+
self.class_names = valid_class_names
|
| 243 |
+
|
| 244 |
+
print(f"Training samples: {len(X_train)}")
|
| 245 |
+
print(f"Testing samples: {len(X_test)}")
|
| 246 |
+
|
| 247 |
+
# Create model
|
| 248 |
+
num_classes = len(self.class_names)
|
| 249 |
+
model = self.create_model(num_classes=num_classes)
|
| 250 |
+
|
| 251 |
+
print("\nModel architecture:")
|
| 252 |
+
model.summary()
|
| 253 |
+
|
| 254 |
+
# Enhanced callbacks
|
| 255 |
+
callbacks = [
|
| 256 |
+
keras.callbacks.EarlyStopping(
|
| 257 |
+
monitor='val_accuracy',
|
| 258 |
+
patience=15,
|
| 259 |
+
restore_best_weights=True,
|
| 260 |
+
verbose=1
|
| 261 |
+
),
|
| 262 |
+
keras.callbacks.ReduceLROnPlateau(
|
| 263 |
+
monitor='val_loss',
|
| 264 |
+
factor=0.3,
|
| 265 |
+
patience=7,
|
| 266 |
+
min_lr=1e-7,
|
| 267 |
+
verbose=1
|
| 268 |
+
),
|
| 269 |
+
keras.callbacks.ModelCheckpoint(
|
| 270 |
+
os.path.join(self.output_dir, 'best_model.h5'),
|
| 271 |
+
monitor='val_accuracy',
|
| 272 |
+
save_best_only=True,
|
| 273 |
+
verbose=1
|
| 274 |
+
)
|
| 275 |
+
]
|
| 276 |
+
|
| 277 |
+
# Train model with class weights
|
| 278 |
+
print("\nStarting training...")
|
| 279 |
+
history = model.fit(
|
| 280 |
+
X_train, y_train,
|
| 281 |
+
validation_split=validation_split,
|
| 282 |
+
epochs=epochs,
|
| 283 |
+
batch_size=batch_size,
|
| 284 |
+
class_weight=class_weight_dict,
|
| 285 |
+
callbacks=callbacks,
|
| 286 |
+
verbose=1
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# Evaluate on test set
|
| 290 |
+
print("\nEvaluating on test set...")
|
| 291 |
+
test_loss, test_accuracy = model.evaluate(X_test, y_test, verbose=0)
|
| 292 |
+
print(f"Test accuracy: {test_accuracy:.4f}")
|
| 293 |
+
print(f"Test loss: {test_loss:.4f}")
|
| 294 |
+
|
| 295 |
+
# Save model and metadata
|
| 296 |
+
self.save_model_and_metadata(model, history, test_accuracy)
|
| 297 |
+
|
| 298 |
+
# Plot training history
|
| 299 |
+
self.plot_training_history(history)
|
| 300 |
+
|
| 301 |
+
return model, history
|
| 302 |
+
|
| 303 |
+
def save_model_and_metadata(self, model, history, test_accuracy):
|
| 304 |
+
"""Save the trained model and associated metadata."""
|
| 305 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 306 |
+
|
| 307 |
+
# Save Keras model
|
| 308 |
+
model_path = os.path.join(self.output_dir, f'gesture_model_{timestamp}.h5')
|
| 309 |
+
model.save(model_path)
|
| 310 |
+
print(f"Model saved to: {model_path}")
|
| 311 |
+
|
| 312 |
+
# Convert to TensorFlow Lite
|
| 313 |
+
converter = tf.lite.TFLiteConverter.from_keras_model(model)
|
| 314 |
+
converter.optimizations = [tf.lite.Optimize.DEFAULT]
|
| 315 |
+
tflite_model = converter.convert()
|
| 316 |
+
|
| 317 |
+
tflite_path = os.path.join(self.output_dir, f'gesture_model_{timestamp}.tflite')
|
| 318 |
+
with open(tflite_path, 'wb') as f:
|
| 319 |
+
f.write(tflite_model)
|
| 320 |
+
print(f"TFLite model saved to: {tflite_path}")
|
| 321 |
+
|
| 322 |
+
# Save labels
|
| 323 |
+
labels_path = os.path.join(self.output_dir, f'labels_{timestamp}.txt')
|
| 324 |
+
with open(labels_path, 'w') as f:
|
| 325 |
+
for label in self.class_names:
|
| 326 |
+
f.write(f"{label}\n")
|
| 327 |
+
print(f"Labels saved to: {labels_path}")
|
| 328 |
+
|
| 329 |
+
# Save label encoder
|
| 330 |
+
encoder_path = os.path.join(self.output_dir, f'label_encoder_{timestamp}.pkl')
|
| 331 |
+
with open(encoder_path, 'wb') as f:
|
| 332 |
+
pickle.dump(self.label_encoder, f)
|
| 333 |
+
|
| 334 |
+
# Save training metadata
|
| 335 |
+
metadata = {
|
| 336 |
+
'timestamp': timestamp,
|
| 337 |
+
'test_accuracy': float(test_accuracy),
|
| 338 |
+
'num_classes': len(self.class_names),
|
| 339 |
+
'class_names': self.class_names,
|
| 340 |
+
'total_samples': len(self.features),
|
| 341 |
+
'feature_shape': self.features.shape,
|
| 342 |
+
'model_path': model_path,
|
| 343 |
+
'tflite_path': tflite_path,
|
| 344 |
+
'labels_path': labels_path
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
metadata_path = os.path.join(self.output_dir, f'training_metadata_{timestamp}.json')
|
| 348 |
+
with open(metadata_path, 'w') as f:
|
| 349 |
+
json.dump(metadata, f, indent=2)
|
| 350 |
+
print(f"Metadata saved to: {metadata_path}")
|
| 351 |
+
|
| 352 |
+
def plot_training_history(self, history):
|
| 353 |
+
"""Plot training history."""
|
| 354 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
| 355 |
+
|
| 356 |
+
# Plot accuracy
|
| 357 |
+
ax1.plot(history.history['accuracy'], label='Training Accuracy')
|
| 358 |
+
ax1.plot(history.history['val_accuracy'], label='Validation Accuracy')
|
| 359 |
+
ax1.set_title('Model Accuracy')
|
| 360 |
+
ax1.set_xlabel('Epoch')
|
| 361 |
+
ax1.set_ylabel('Accuracy')
|
| 362 |
+
ax1.legend()
|
| 363 |
+
ax1.grid(True)
|
| 364 |
+
|
| 365 |
+
# Plot loss
|
| 366 |
+
ax2.plot(history.history['loss'], label='Training Loss')
|
| 367 |
+
ax2.plot(history.history['val_loss'], label='Validation Loss')
|
| 368 |
+
ax2.set_title('Model Loss')
|
| 369 |
+
ax2.set_xlabel('Epoch')
|
| 370 |
+
ax2.set_ylabel('Loss')
|
| 371 |
+
ax2.legend()
|
| 372 |
+
ax2.grid(True)
|
| 373 |
+
|
| 374 |
+
plt.tight_layout()
|
| 375 |
+
|
| 376 |
+
# Save plot
|
| 377 |
+
plot_path = os.path.join(self.output_dir, 'training_history.png')
|
| 378 |
+
plt.savefig(plot_path, dpi=300, bbox_inches='tight')
|
| 379 |
+
print(f"Training history plot saved to: {plot_path}")
|
| 380 |
+
plt.show()
|
| 381 |
+
|
| 382 |
+
def main():
|
| 383 |
+
# Configuration
|
| 384 |
+
DATASET_PATH = r"C:\Users\krishna\Downloads\archive (2)\Gesture Image Data"
|
| 385 |
+
OUTPUT_DIR = "models"
|
| 386 |
+
|
| 387 |
+
# Training parameters - Optimized for better performance
|
| 388 |
+
MAX_SAMPLES_PER_CLASS = 500 # Increased for better generalization
|
| 389 |
+
SAMPLE_RATIO = 0.1 # Use 10% of available images if less than max
|
| 390 |
+
TEST_SIZE = 0.2
|
| 391 |
+
VALIDATION_SPLIT = 0.2
|
| 392 |
+
EPOCHS = 50
|
| 393 |
+
BATCH_SIZE = 64 # Increased for faster training
|
| 394 |
+
|
| 395 |
+
print("=== Sign Language Gesture Dataset Training ===")
|
| 396 |
+
print(f"Dataset path: {DATASET_PATH}")
|
| 397 |
+
print(f"Output directory: {OUTPUT_DIR}")
|
| 398 |
+
print(f"Max samples per class: {MAX_SAMPLES_PER_CLASS}")
|
| 399 |
+
print(f"Sample ratio: {SAMPLE_RATIO}")
|
| 400 |
+
|
| 401 |
+
# Initialize trainer
|
| 402 |
+
trainer = GestureDatasetTrainer(DATASET_PATH, OUTPUT_DIR)
|
| 403 |
+
|
| 404 |
+
# Load dataset
|
| 405 |
+
trainer.load_dataset(
|
| 406 |
+
max_samples_per_class=MAX_SAMPLES_PER_CLASS,
|
| 407 |
+
sample_ratio=SAMPLE_RATIO
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
# Train model
|
| 411 |
+
model, history = trainer.train_model(
|
| 412 |
+
test_size=TEST_SIZE,
|
| 413 |
+
validation_split=VALIDATION_SPLIT,
|
| 414 |
+
epochs=EPOCHS,
|
| 415 |
+
batch_size=BATCH_SIZE
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
print("\n=== Training Complete ===")
|
| 419 |
+
print("Check the 'models' directory for saved files:")
|
| 420 |
+
print("- .h5 file: Keras model")
|
| 421 |
+
print("- .tflite file: TensorFlow Lite model")
|
| 422 |
+
print("- labels.txt: Class labels")
|
| 423 |
+
print("- training_metadata.json: Training information")
|
| 424 |
+
print("- training_history.png: Training plots")
|
| 425 |
+
|
| 426 |
+
if __name__ == "__main__":
|
| 427 |
+
main()
|
train_model.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import tensorflow as tf
|
| 3 |
+
from sklearn.model_selection import train_test_split
|
| 4 |
+
from sklearn.preprocessing import LabelEncoder
|
| 5 |
+
import os
|
| 6 |
+
import glob
|
| 7 |
+
from utils.model_utils import create_model, get_training_callbacks, evaluate_model
|
| 8 |
+
from utils.data_utils import DataProcessor
|
| 9 |
+
|
| 10 |
+
class SignLanguageTrainer:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.data_processor = DataProcessor()
|
| 13 |
+
self.label_encoder = LabelEncoder()
|
| 14 |
+
|
| 15 |
+
def load_dataset(self, dataset_path="dataset"):
|
| 16 |
+
"""Load dataset from collected data."""
|
| 17 |
+
X, y = [], []
|
| 18 |
+
labels = []
|
| 19 |
+
|
| 20 |
+
# Get all sign directories
|
| 21 |
+
sign_dirs = [d for d in os.listdir(dataset_path) if os.path.isdir(os.path.join(dataset_path, d))]
|
| 22 |
+
|
| 23 |
+
for sign_name in sign_dirs:
|
| 24 |
+
sign_path = os.path.join(dataset_path, sign_name)
|
| 25 |
+
sequence_file = os.path.join(sign_path, "sequences.npy")
|
| 26 |
+
|
| 27 |
+
if os.path.exists(sequence_file):
|
| 28 |
+
sequences = np.load(sequence_file)
|
| 29 |
+
|
| 30 |
+
for sequence in sequences:
|
| 31 |
+
X.append(sequence)
|
| 32 |
+
y.append(sign_name)
|
| 33 |
+
|
| 34 |
+
if sign_name not in labels:
|
| 35 |
+
labels.append(sign_name)
|
| 36 |
+
|
| 37 |
+
print(f"Loaded {len(sequences)} sequences for {sign_name}")
|
| 38 |
+
|
| 39 |
+
# Save labels
|
| 40 |
+
with open('models/labels.txt', 'w') as f:
|
| 41 |
+
f.write('\n'.join(labels))
|
| 42 |
+
|
| 43 |
+
# Encode labels
|
| 44 |
+
y_encoded = self.label_encoder.fit_transform(y)
|
| 45 |
+
|
| 46 |
+
return np.array(X), y_encoded, labels
|
| 47 |
+
|
| 48 |
+
def prepare_data(self, X, y, test_size=0.2, val_size=0.2):
|
| 49 |
+
"""Prepare data for training."""
|
| 50 |
+
# Split into train and test
|
| 51 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 52 |
+
X, y, test_size=test_size, random_state=42, stratify=y
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Split train into train and validation
|
| 56 |
+
X_train, X_val, y_train, y_val = train_test_split(
|
| 57 |
+
X_train, y_train, test_size=val_size, random_state=42, stratify=y_train
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
return X_train, X_val, X_test, y_train, y_val, y_test
|
| 61 |
+
|
| 62 |
+
def train_model(self, X_train, y_train, X_val, y_val,
|
| 63 |
+
num_classes, epochs=50, batch_size=32):
|
| 64 |
+
"""Train the sign language model."""
|
| 65 |
+
|
| 66 |
+
# Get input shape
|
| 67 |
+
sequence_length, num_features = X_train.shape[1], X_train.shape[2]
|
| 68 |
+
|
| 69 |
+
# Create model
|
| 70 |
+
model = create_model(num_classes, sequence_length, num_features)
|
| 71 |
+
|
| 72 |
+
print("Model Summary:")
|
| 73 |
+
model.summary()
|
| 74 |
+
|
| 75 |
+
# Get callbacks
|
| 76 |
+
callbacks = get_training_callbacks()
|
| 77 |
+
|
| 78 |
+
# Train model
|
| 79 |
+
history = model.fit(
|
| 80 |
+
X_train, y_train,
|
| 81 |
+
validation_data=(X_val, y_val),
|
| 82 |
+
epochs=epochs,
|
| 83 |
+
batch_size=batch_size,
|
| 84 |
+
callbacks=callbacks,
|
| 85 |
+
verbose=1
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
return model, history
|
| 89 |
+
|
| 90 |
+
def save_model(self, model, model_name="sign_language_model"):
|
| 91 |
+
"""Save the trained model."""
|
| 92 |
+
# Save Keras model
|
| 93 |
+
model.save(f'models/{model_name}.h5')
|
| 94 |
+
|
| 95 |
+
# Convert to TFLite
|
| 96 |
+
converter = tf.lite.TFLiteConverter.from_keras_model(model)
|
| 97 |
+
converter.optimizations = [tf.lite.Optimize.DEFAULT]
|
| 98 |
+
|
| 99 |
+
# Quantization for smaller model size
|
| 100 |
+
def representative_dataset():
|
| 101 |
+
for _ in range(100):
|
| 102 |
+
yield [np.random.random((1, 30, 126)).astype(np.float32)]
|
| 103 |
+
|
| 104 |
+
converter.representative_dataset = representative_dataset
|
| 105 |
+
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
|
| 106 |
+
converter.inference_input_type = tf.uint8
|
| 107 |
+
converter.inference_output_type = tf.uint8
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
tflite_model = converter.convert()
|
| 111 |
+
with open('models/model.tflite', 'wb') as f:
|
| 112 |
+
f.write(tflite_model)
|
| 113 |
+
print("Model converted to TFLite successfully!")
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"TFLite conversion failed: {e}")
|
| 116 |
+
# Fallback to float32 model
|
| 117 |
+
converter = tf.lite.TFLiteConverter.from_keras_model(model)
|
| 118 |
+
converter.optimizations = [tf.lite.Optimize.DEFAULT]
|
| 119 |
+
tflite_model = converter.convert()
|
| 120 |
+
with open('models/model.tflite', 'wb') as f:
|
| 121 |
+
f.write(tflite_model)
|
| 122 |
+
print("Model converted to TFLite (float32) successfully!")
|
| 123 |
+
|
| 124 |
+
def main():
|
| 125 |
+
trainer = SignLanguageTrainer()
|
| 126 |
+
|
| 127 |
+
print("Sign Language Model Training")
|
| 128 |
+
print("=" * 40)
|
| 129 |
+
|
| 130 |
+
# Check if dataset exists
|
| 131 |
+
if not os.path.exists("dataset"):
|
| 132 |
+
print("Dataset not found! Please run data_collection.py first.")
|
| 133 |
+
return
|
| 134 |
+
|
| 135 |
+
# Load dataset
|
| 136 |
+
print("Loading dataset...")
|
| 137 |
+
X, y, labels = trainer.load_dataset()
|
| 138 |
+
|
| 139 |
+
if len(X) == 0:
|
| 140 |
+
print("No data found! Please collect data first using data_collection.py")
|
| 141 |
+
return
|
| 142 |
+
|
| 143 |
+
print(f"Loaded {len(X)} sequences for {len(labels)} signs")
|
| 144 |
+
print(f"Signs: {labels}")
|
| 145 |
+
|
| 146 |
+
# Prepare data
|
| 147 |
+
print("Preparing data...")
|
| 148 |
+
X_train, X_val, X_test, y_train, y_val, y_test = trainer.prepare_data(X, y)
|
| 149 |
+
|
| 150 |
+
print(f"Training set: {len(X_train)} sequences")
|
| 151 |
+
print(f"Validation set: {len(X_val)} sequences")
|
| 152 |
+
print(f"Test set: {len(X_test)} sequences")
|
| 153 |
+
|
| 154 |
+
# Train model
|
| 155 |
+
print("Training model...")
|
| 156 |
+
model, history = trainer.train_model(
|
| 157 |
+
X_train, y_train, X_val, y_val,
|
| 158 |
+
num_classes=len(labels),
|
| 159 |
+
epochs=50,
|
| 160 |
+
batch_size=32
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# Evaluate model
|
| 164 |
+
print("Evaluating model...")
|
| 165 |
+
results = evaluate_model(model, X_test, y_test)
|
| 166 |
+
print(f"Test Accuracy: {results['accuracy']:.4f}")
|
| 167 |
+
|
| 168 |
+
# Save model
|
| 169 |
+
print("Saving model...")
|
| 170 |
+
trainer.save_model(model)
|
| 171 |
+
|
| 172 |
+
print("Training completed successfully!")
|
| 173 |
+
print("You can now run the main application: streamlit run app.py")
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
main()
|
train_platform.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
import tensorflow as tf
|
| 6 |
+
from tensorflow.keras.models import Sequential
|
| 7 |
+
from tensorflow.keras.layers import LSTM, Dense, Dropout
|
| 8 |
+
from tensorflow.keras.callbacks import ModelCheckpoint, TensorBoard
|
| 9 |
+
import mediapipe as mp
|
| 10 |
+
from utils.data_utils import process_video, extract_landmarks
|
| 11 |
+
from utils.model_utils import create_model, convert_to_tflite
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
st.title("Sign Language Model Training Platform")
|
| 15 |
+
|
| 16 |
+
# Sidebar for training configuration
|
| 17 |
+
st.sidebar.header("Training Configuration")
|
| 18 |
+
|
| 19 |
+
# Model parameters
|
| 20 |
+
num_classes = st.sidebar.number_input("Number of Sign Classes", min_value=2, value=10)
|
| 21 |
+
epochs = st.sidebar.number_input("Training Epochs", min_value=1, value=50)
|
| 22 |
+
batch_size = st.sidebar.number_input("Batch Size", min_value=1, value=32)
|
| 23 |
+
|
| 24 |
+
# Dataset upload
|
| 25 |
+
st.header("Dataset Upload")
|
| 26 |
+
dataset_path = st.file_uploader("Upload Dataset (ZIP file containing video folders)", type=['zip'])
|
| 27 |
+
|
| 28 |
+
if dataset_path:
|
| 29 |
+
# Create progress bar
|
| 30 |
+
progress_bar = st.progress(0)
|
| 31 |
+
status_text = st.empty()
|
| 32 |
+
|
| 33 |
+
# Extract dataset
|
| 34 |
+
status_text.text("Extracting dataset...")
|
| 35 |
+
# TODO: Add dataset extraction logic
|
| 36 |
+
|
| 37 |
+
# Process videos and extract landmarks
|
| 38 |
+
status_text.text("Processing videos and extracting landmarks...")
|
| 39 |
+
X_train, y_train = [], []
|
| 40 |
+
# TODO: Add video processing logic using MediaPipe
|
| 41 |
+
|
| 42 |
+
# Create and train model
|
| 43 |
+
if st.button("Start Training"):
|
| 44 |
+
status_text.text("Creating model...")
|
| 45 |
+
model = create_model(num_classes)
|
| 46 |
+
|
| 47 |
+
status_text.text("Training model...")
|
| 48 |
+
history = model.fit(
|
| 49 |
+
X_train, y_train,
|
| 50 |
+
epochs=epochs,
|
| 51 |
+
batch_size=batch_size,
|
| 52 |
+
validation_split=0.2,
|
| 53 |
+
callbacks=[
|
| 54 |
+
ModelCheckpoint('models/best_model.h5', save_best_only=True),
|
| 55 |
+
TensorBoard(log_dir='logs')
|
| 56 |
+
]
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Convert to TFLite
|
| 60 |
+
status_text.text("Converting model to TFLite...")
|
| 61 |
+
convert_to_tflite(model, 'models/model.tflite')
|
| 62 |
+
|
| 63 |
+
st.success("Training completed! Model saved as 'model.tflite'")
|
| 64 |
+
|
| 65 |
+
# Display training metrics
|
| 66 |
+
st.header("Training Results")
|
| 67 |
+
st.line_chart(history.history)
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
main()
|
train_simple.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import tensorflow as tf
|
| 3 |
+
from tensorflow.keras.models import Sequential
|
| 4 |
+
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization
|
| 5 |
+
import cv2
|
| 6 |
+
import os
|
| 7 |
+
import glob
|
| 8 |
+
from sign_utils import MediaPipeLandmarksModel
|
| 9 |
+
|
| 10 |
+
def load_and_process_dataset():
|
| 11 |
+
"""Load digit images and extract landmarks."""
|
| 12 |
+
landmark_model = MediaPipeLandmarksModel()
|
| 13 |
+
|
| 14 |
+
X_landmarks = []
|
| 15 |
+
y = []
|
| 16 |
+
|
| 17 |
+
print("Loading and processing images...")
|
| 18 |
+
|
| 19 |
+
# Process each digit folder
|
| 20 |
+
for digit in range(10):
|
| 21 |
+
digit_path = os.path.join("dataset", str(digit))
|
| 22 |
+
if not os.path.exists(digit_path):
|
| 23 |
+
continue
|
| 24 |
+
|
| 25 |
+
image_files = glob.glob(os.path.join(digit_path, "*.JPG"))
|
| 26 |
+
print(f"Processing {len(image_files)} images for digit {digit}")
|
| 27 |
+
|
| 28 |
+
count = 0
|
| 29 |
+
for img_file in image_files:
|
| 30 |
+
# Load image
|
| 31 |
+
img = cv2.imread(img_file)
|
| 32 |
+
if img is None:
|
| 33 |
+
continue
|
| 34 |
+
|
| 35 |
+
# Extract landmarks
|
| 36 |
+
landmarks = landmark_model.predict_landmarks(img)
|
| 37 |
+
|
| 38 |
+
if landmarks is not None:
|
| 39 |
+
# Normalize landmarks
|
| 40 |
+
landmarks_normalized = landmark_model.normalize_landmarks(landmarks)
|
| 41 |
+
|
| 42 |
+
X_landmarks.append(landmarks_normalized)
|
| 43 |
+
y.append(digit)
|
| 44 |
+
count += 1
|
| 45 |
+
|
| 46 |
+
# Limit to 100 images per digit for faster training
|
| 47 |
+
if count >= 100:
|
| 48 |
+
break
|
| 49 |
+
|
| 50 |
+
print(f"Successfully processed {count} images for digit {digit}")
|
| 51 |
+
|
| 52 |
+
print(f"Total processed: {len(X_landmarks)} images with landmarks")
|
| 53 |
+
|
| 54 |
+
# Check if we have any data
|
| 55 |
+
if len(X_landmarks) == 0:
|
| 56 |
+
return np.array([]), np.array([])
|
| 57 |
+
|
| 58 |
+
# Ensure all landmark arrays have the same shape
|
| 59 |
+
landmark_shape = X_landmarks[0].shape[0]
|
| 60 |
+
filtered_landmarks = []
|
| 61 |
+
filtered_y = []
|
| 62 |
+
|
| 63 |
+
for i, landmarks in enumerate(X_landmarks):
|
| 64 |
+
if landmarks.shape[0] == landmark_shape:
|
| 65 |
+
filtered_landmarks.append(landmarks)
|
| 66 |
+
filtered_y.append(y[i])
|
| 67 |
+
|
| 68 |
+
print(f"After filtering: {len(filtered_landmarks)} valid samples")
|
| 69 |
+
|
| 70 |
+
return np.array(filtered_landmarks), np.array(filtered_y)
|
| 71 |
+
|
| 72 |
+
def create_simple_model(num_classes=10):
|
| 73 |
+
"""Create a simple neural network for landmark classification."""
|
| 74 |
+
model = Sequential([
|
| 75 |
+
Dense(128, activation='relu', input_shape=(63,)), # 21 landmarks * 3 coordinates
|
| 76 |
+
BatchNormalization(),
|
| 77 |
+
Dropout(0.3),
|
| 78 |
+
|
| 79 |
+
Dense(64, activation='relu'),
|
| 80 |
+
BatchNormalization(),
|
| 81 |
+
Dropout(0.3),
|
| 82 |
+
|
| 83 |
+
Dense(32, activation='relu'),
|
| 84 |
+
Dropout(0.2),
|
| 85 |
+
|
| 86 |
+
Dense(num_classes, activation='softmax')
|
| 87 |
+
])
|
| 88 |
+
|
| 89 |
+
model.compile(
|
| 90 |
+
optimizer='adam',
|
| 91 |
+
loss='sparse_categorical_crossentropy',
|
| 92 |
+
metrics=['accuracy']
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
return model
|
| 96 |
+
|
| 97 |
+
def main():
|
| 98 |
+
print("Simple Sign Language Digits Training")
|
| 99 |
+
print("=" * 40)
|
| 100 |
+
|
| 101 |
+
# Load dataset
|
| 102 |
+
X, y = load_and_process_dataset()
|
| 103 |
+
|
| 104 |
+
if len(X) == 0:
|
| 105 |
+
print("No valid images found with hand landmarks!")
|
| 106 |
+
return
|
| 107 |
+
|
| 108 |
+
# Split data (simple split without sklearn)
|
| 109 |
+
n_samples = len(X)
|
| 110 |
+
n_train = int(0.8 * n_samples)
|
| 111 |
+
|
| 112 |
+
# Shuffle data
|
| 113 |
+
indices = np.random.permutation(n_samples)
|
| 114 |
+
train_indices = indices[:n_train]
|
| 115 |
+
test_indices = indices[n_train:]
|
| 116 |
+
|
| 117 |
+
X_train, X_test = X[train_indices], X[test_indices]
|
| 118 |
+
y_train, y_test = y[train_indices], y[test_indices]
|
| 119 |
+
|
| 120 |
+
print(f"Training set: {len(X_train)} samples")
|
| 121 |
+
print(f"Test set: {len(X_test)} samples")
|
| 122 |
+
|
| 123 |
+
# Create model
|
| 124 |
+
model = create_simple_model()
|
| 125 |
+
print("\nModel Summary:")
|
| 126 |
+
model.summary()
|
| 127 |
+
|
| 128 |
+
# Train model
|
| 129 |
+
print("\nTraining model...")
|
| 130 |
+
history = model.fit(
|
| 131 |
+
X_train, y_train,
|
| 132 |
+
validation_data=(X_test, y_test),
|
| 133 |
+
epochs=30,
|
| 134 |
+
batch_size=32,
|
| 135 |
+
verbose=1
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Evaluate model
|
| 139 |
+
print("\nEvaluating model...")
|
| 140 |
+
test_loss, test_accuracy = model.evaluate(X_test, y_test, verbose=0)
|
| 141 |
+
print(f"Test Accuracy: {test_accuracy:.4f}")
|
| 142 |
+
|
| 143 |
+
# Save model
|
| 144 |
+
print("\nSaving model...")
|
| 145 |
+
|
| 146 |
+
# Create models directory if it doesn't exist
|
| 147 |
+
os.makedirs('models', exist_ok=True)
|
| 148 |
+
|
| 149 |
+
# Save Keras model
|
| 150 |
+
model.save('models/digit_model.h5')
|
| 151 |
+
|
| 152 |
+
# Convert to TFLite
|
| 153 |
+
try:
|
| 154 |
+
converter = tf.lite.TFLiteConverter.from_keras_model(model)
|
| 155 |
+
converter.optimizations = [tf.lite.Optimize.DEFAULT]
|
| 156 |
+
|
| 157 |
+
tflite_model = converter.convert()
|
| 158 |
+
|
| 159 |
+
with open('models/model.tflite', 'wb') as f:
|
| 160 |
+
f.write(tflite_model)
|
| 161 |
+
|
| 162 |
+
print("Model converted to TFLite successfully!")
|
| 163 |
+
|
| 164 |
+
except Exception as e:
|
| 165 |
+
print(f"TFLite conversion failed: {e}")
|
| 166 |
+
|
| 167 |
+
# Save labels
|
| 168 |
+
labels = [str(i) for i in range(10)]
|
| 169 |
+
with open('models/labels.txt', 'w') as f:
|
| 170 |
+
f.write('\n'.join(labels))
|
| 171 |
+
|
| 172 |
+
print("Training completed successfully!")
|
| 173 |
+
print("You can now run the main application: streamlit run app.py")
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
main()
|