update
@@ -1,222 +1,65 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
# Python virtual environment
|
||||
venv/
|
||||
env/
|
||||
.env/
|
||||
.venv/
|
||||
|
||||
# Python cache files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Django stuff:
|
||||
# IDE specific files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# YOLO specific
|
||||
runs/
|
||||
*.pt
|
||||
weights/
|
||||
|
||||
# Project specific
|
||||
torch_compile_debug/
|
||||
training/train/
|
||||
training/val/
|
||||
dataset.yaml
|
||||
|
||||
# Logs and temporary files
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
.DS_Store
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# Debug directories
|
||||
torchinductor_*/
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
# Cache directories
|
||||
.cache/
|
||||
*.cache
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
# Test coverage
|
||||
coverage_html_report/
|
||||
.coverage
|
||||
htmlcov/
|
||||
test_results/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
# Compiled files
|
||||
*.so
|
||||
*.dll
|
||||
*.dylib
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
*.ipynb
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
# Environment variables
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
myenv/
|
||||
memory_detection_env/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Machine Learning specific
|
||||
# Model weights and checkpoints
|
||||
*.pt
|
||||
*.pth
|
||||
*.ckpt
|
||||
*.h5
|
||||
*.pkl
|
||||
*.joblib
|
||||
runs/
|
||||
wandb/
|
||||
mlruns/
|
||||
.neptune/
|
||||
|
||||
# YOLOv8 specific
|
||||
yolov8*.pt
|
||||
best.pt
|
||||
last.pt
|
||||
exp*/
|
||||
|
||||
# Dataset cache
|
||||
*.cache
|
||||
.fiftyone/
|
||||
|
||||
# Tensorboard logs
|
||||
logs/
|
||||
tb_logs/
|
||||
|
||||
# Jupyter notebook checkpoints
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# IDE specific
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
uploads/
|
||||
temp/
|
||||
tmp/
|
||||
annotated_*.png
|
||||
test_*_result.png
|
||||
|
||||
# Large files that shouldn't be committed
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# API keys and secrets
|
||||
.env
|
||||
config.ini
|
||||
secrets.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Coverage reports
|
||||
htmlcov/
|
||||
.coverage
|
||||
|
||||
# Virtual environment (additional patterns)
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
|
||||
# PyTorch specific
|
||||
*.pth.tar
|
||||
|
||||
# Conda
|
||||
.conda/
|
||||
|
||||
# Local configuration
|
||||
local_config.py
|
||||
config_local.py
|
||||
.env.local
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
# Memory Module Detection API Documentation
|
||||
|
||||
## Base URL
|
||||
```
|
||||
http://localhost:5002
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Health Check
|
||||
**GET** `/health`
|
||||
|
||||
Check if the API is running and model is loaded.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"model_loaded": true,
|
||||
"timestamp": "2025-07-12T07:41:46.123456"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. API Information
|
||||
**GET** `/api`
|
||||
|
||||
Get basic API information and available endpoints.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"name": "Memory Module Detection API",
|
||||
"version": "1.0",
|
||||
"description": "AI-powered memory module detection using YOLOv8",
|
||||
"model_loaded": true,
|
||||
"endpoints": ["/health", "/api", "/detect", "/detect/hardcoded", "/detect/base64"]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Upload Image Detection
|
||||
**POST** `/detect`
|
||||
|
||||
Upload an image file for memory module detection.
|
||||
|
||||
**Request:**
|
||||
- Content-Type: `multipart/form-data`
|
||||
- Body: Form data with `file` field containing image
|
||||
- Optional: `confidence` parameter (default: 0.8)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"detections": [
|
||||
{
|
||||
"x1": 100.5,
|
||||
"y1": 150.2,
|
||||
"x2": 200.8,
|
||||
"y2": 250.6,
|
||||
"confidence": 0.95,
|
||||
"class": "memory_module"
|
||||
}
|
||||
],
|
||||
"num_detections": 1,
|
||||
"annotated_image": "base64_encoded_image_string",
|
||||
"confidence_threshold": 0.8
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Hardcoded Test Image
|
||||
**GET** `/detect/hardcoded`
|
||||
|
||||
Process the hardcoded test image for detection.
|
||||
|
||||
**Query Parameters:**
|
||||
- `confidence` (optional): Confidence threshold (default: 0.8)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET /detect/hardcoded?confidence=0.9
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"detections": [...],
|
||||
"num_detections": 2,
|
||||
"annotated_image": "base64_encoded_image_string",
|
||||
"confidence_threshold": 0.9,
|
||||
"test_image_path": "training/memory/out1.png"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Base64 Image Detection
|
||||
**POST** `/detect/base64`
|
||||
|
||||
Process a base64 encoded image for detection.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"image_data": "base64_encoded_image_string",
|
||||
"confidence": 0.8
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"detections": [...],
|
||||
"num_detections": 1,
|
||||
"annotated_image": "base64_encoded_image_string",
|
||||
"confidence_threshold": 0.8
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
All endpoints return error responses in this format:
|
||||
```json
|
||||
{
|
||||
"error": "Error message description",
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
Common HTTP status codes:
|
||||
- `200` - Success
|
||||
- `400` - Bad Request (invalid file, missing parameters)
|
||||
- `404` - Not Found (endpoint or file not found)
|
||||
- `413` - File Too Large (max 16MB)
|
||||
- `500` - Internal Server Error (model not loaded, processing error)
|
||||
|
||||
## Supported Image Formats
|
||||
- PNG
|
||||
- JPG/JPEG
|
||||
- GIF
|
||||
- BMP
|
||||
|
||||
## File Size Limits
|
||||
- Maximum upload size: 16MB
|
||||
|
||||
## Detection Response Format
|
||||
|
||||
Each detection object contains:
|
||||
- `x1, y1`: Top-left corner coordinates
|
||||
- `x2, y2`: Bottom-right corner coordinates
|
||||
- `confidence`: Detection confidence score (0.0-1.0)
|
||||
- `class`: Detected object class ("memory_module")
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### cURL Examples
|
||||
|
||||
**Health Check:**
|
||||
```bash
|
||||
curl http://localhost:5002/health
|
||||
```
|
||||
|
||||
**Upload Image:**
|
||||
```bash
|
||||
curl -X POST -F "file=@image.jpg" -F "confidence=0.8" http://localhost:5002/detect
|
||||
```
|
||||
|
||||
**Hardcoded Test:**
|
||||
```bash
|
||||
curl "http://localhost:5002/detect/hardcoded?confidence=0.9"
|
||||
```
|
||||
|
||||
### Python Examples
|
||||
|
||||
**Health Check:**
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.get('http://localhost:5002/health')
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
**Upload Image:**
|
||||
```python
|
||||
import requests
|
||||
|
||||
with open('image.jpg', 'rb') as f:
|
||||
files = {'file': f}
|
||||
data = {'confidence': 0.8}
|
||||
response = requests.post('http://localhost:5002/detect', files=files, data=data)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
**Base64 Detection:**
|
||||
```python
|
||||
import requests
|
||||
import base64
|
||||
|
||||
with open('image.jpg', 'rb') as f:
|
||||
image_data = base64.b64encode(f.read()).decode()
|
||||
|
||||
payload = {
|
||||
'image_data': image_data,
|
||||
'confidence': 0.8
|
||||
}
|
||||
response = requests.post('http://localhost:5002/detect/base64', json=payload)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
## Model Information
|
||||
- **Architecture:** YOLOv8 Nano
|
||||
- **Classes:** memory_module
|
||||
- **Input Size:** 640x640
|
||||
- **Accuracy:** 99.5% mAP50
|
||||
- **Inference Time:** ~37ms on CPU
|
||||
@@ -1,545 +1,194 @@
|
||||
# DS Task Recycling Project - Memory Module Detection
|
||||
# DS Task Recycling Project
|
||||
|
||||
This project is a complete implementation of a Flask API that processes motherboard images and detects memory modules using YOLOv8. The API returns annotated images with bounding boxes drawn around each detected memory module.
|
||||
This project is a Flask API that processes images of motherboards to detect memory modules. It uses computer vision to identify and draw bounding boxes around memory modules present in the input images.
|
||||
|
||||
## 🚀 Quick Start
|
||||
## Project Overview
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Train the Model
|
||||
```bash
|
||||
python3 train.py --epochs 100 --batch 16
|
||||
```
|
||||
|
||||
### 3. Start the API
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
### 4. Test the API
|
||||
```bash
|
||||
# Option 1: Use the Web Interface (Recommended for QA)
|
||||
# Open browser and go to: http://localhost:5000
|
||||
|
||||
# Option 2: Use command line
|
||||
# Test with hardcoded image
|
||||
curl http://localhost:5000/detect/hardcoded
|
||||
|
||||
# Upload an image
|
||||
curl -X POST -F "image=@your_image.png" http://localhost:5000/detect
|
||||
|
||||
# Option 3: Run automated tests
|
||||
python3 test_api.py
|
||||
```
|
||||
|
||||
## 📋 Project Overview
|
||||
|
||||
- **Algorithm Used:** YOLOv8 Nano (ultralytics)
|
||||
- **Input Types:**
|
||||
- Image upload via Flask API
|
||||
- Base64 encoded images
|
||||
- Hardcoded test image
|
||||
- **Dataset:** 40 images (20 with memory modules, 20 without)
|
||||
- **Output:** Annotated images with bounding boxes and confidence scores
|
||||
- Image upload via the Flask API
|
||||
- A hardcoded test image (memory_out19.png) for testing purposes
|
||||
|
||||
## 🏗️ Project Structure
|
||||
- **Dataset:**
|
||||
- 20 pictures of motherboards with memory
|
||||
- 20 pictures of motherboards without memory
|
||||
|
||||
```
|
||||
ds_task_recycling_project/
|
||||
├── main.py # Flask API application (main interface)
|
||||
├── api_docs.py # Swagger UI API documentation (developer only)
|
||||
├── train.py # YOLOv8 training script
|
||||
├── inference_utils.py # Detection and visualization utilities
|
||||
├── prepare_dataset.py # Dataset preparation script
|
||||
├── test_api.py # API testing script
|
||||
├── setup.py # Automated setup script
|
||||
├── requirements.txt # Python dependencies
|
||||
├── dataset.yaml # YOLO dataset configuration
|
||||
├── .gitignore # Git ignore file for ML projects
|
||||
├── VALIDATION_CHECKLIST.md # Project validation checklist
|
||||
├── templates/ # Frontend templates
|
||||
│ └── index.html # QA testing web interface
|
||||
├── static/ # Frontend assets
|
||||
│ ├── style.css # Styling for web interface
|
||||
│ └── script.js # JavaScript for web interface
|
||||
├── venv/ # Virtual environment (created by user)
|
||||
├── training/ # Dataset directory
|
||||
│ ├── memory/ # Images with memory modules + YOLO labels
|
||||
│ │ ├── out1.png # Sample motherboard image with memory
|
||||
│ │ ├── out1.txt # YOLO format annotation file
|
||||
│ │ └── ... # 19 more image/label pairs
|
||||
│ ├── no_memory/ # Images without memory modules
|
||||
│ │ ├── out21.png # Sample motherboard image without memory
|
||||
│ │ └── ... # 19 more images (no labels needed)
|
||||
│ ├── train/ # Training split (80% = 32 images)
|
||||
│ │ ├── images/ # Training images
|
||||
│ │ └── labels/ # Training labels
|
||||
│ └── val/ # Validation split (20% = 8 images)
|
||||
│ ├── images/ # Validation images
|
||||
│ └── labels/ # Validation labels
|
||||
├── uploads/ # Temporary upload directory (created at runtime)
|
||||
└── runs/ # Training outputs (created after training)
|
||||
└── detect/
|
||||
└── memory_module_detection/
|
||||
├── weights/
|
||||
│ ├── best.pt # Best model weights
|
||||
│ └── last.pt # Last epoch weights
|
||||
├── train_batch*.jpg # Training visualization
|
||||
├── val_batch*.jpg # Validation visualization
|
||||
├── confusion_matrix.png # Model performance metrics
|
||||
├── results.png # Training curves
|
||||
└── args.yaml # Training arguments
|
||||
```
|
||||
- **Output:**
|
||||
- An annotated image with bounding boxes around each detected memory module
|
||||
- For example, if there are two memory modules, two boxes are drawn; if only one is detected, then one box is drawn
|
||||
|
||||
### **📁 Key Files Description**
|
||||
- **Annotation Tool:**
|
||||
- [makesense.ai](https://www.makesense.ai/) was used for manual annotation
|
||||
|
||||
| File/Directory | Purpose | Usage |
|
||||
|----------------|---------|-------|
|
||||
| `main.py` | Main Flask API application | `python3 main.py` |
|
||||
| `api_docs.py` | Swagger UI documentation (developer only) | `python3 api_docs.py` |
|
||||
| `train.py` | YOLOv8 model training | `python3 train.py` |
|
||||
| `inference_utils.py` | Detection utilities and classes | Imported by other scripts |
|
||||
| `test_api.py` | Comprehensive API testing | `python3 test_api.py` |
|
||||
| `setup.py` | Automated project setup | `python3 setup.py` |
|
||||
| `templates/index.html` | Web interface for QA testing | Served by Flask |
|
||||
| `static/` | CSS, JavaScript, and assets | Served by Flask |
|
||||
| `training/` | Complete dataset with annotations | Used by training script |
|
||||
| `runs/` | Model training outputs | Created after training |
|
||||
| `venv/` | Python virtual environment | Created by user |
|
||||
## Implementation Details
|
||||
|
||||
## 🤖 Algorithm Choice & Technical Decisions
|
||||
### Algorithm Choice & Rationale
|
||||
|
||||
### 1. **Algorithm Choice: YOLOv8 Nano**
|
||||
1. **Which algorithm was chosen?**
|
||||
- YOLOv8 (specifically YOLOv8n - the nano version) was selected for this task
|
||||
|
||||
2. **Why this algorithm?**
|
||||
- Fast inference speed suitable for real-time applications
|
||||
- Good balance between accuracy and computational requirements
|
||||
- Built-in support for transfer learning
|
||||
- Excellent performance on object detection tasks
|
||||
- Easy integration with Python/Flask applications
|
||||
- Robust community support and documentation
|
||||
|
||||
**Which algorithm will you use for detecting the memory modules?**
|
||||
- **Answer:** YOLOv8 Nano (You Only Look Once version 8, Nano variant)
|
||||
### Hardware Considerations
|
||||
|
||||
**Why do you choose this particular algorithm?**
|
||||
3. **CPU/GPU Impact:**
|
||||
- The current implementation runs on CPU for broader accessibility
|
||||
- Model parameters were optimized for CPU performance:
|
||||
- Reduced batch size (8)
|
||||
- Lightweight augmentation
|
||||
- Early stopping with patience=15
|
||||
- GPU support is available through YOLO if needed for scaling
|
||||
- Current performance is suitable for the demo nature of the project
|
||||
|
||||
**Primary Reasons:**
|
||||
- **State-of-the-art performance:** Latest evolution of YOLO family with superior accuracy
|
||||
- **Real-time inference:** 37ms processing time, single-stage detector
|
||||
- **Small object detection:** Excellent at detecting memory modules on motherboards
|
||||
- **Pre-trained weights:** Leverages COCO dataset for transfer learning
|
||||
- **Easy integration:** Ultralytics library with excellent Python API
|
||||
- **Model efficiency:** Nano variant balances 99.5% mAP50 accuracy with speed
|
||||
- **Production ready:** Proven architecture used in industrial applications
|
||||
### Video Processing Approach
|
||||
|
||||
**Technical Advantages:**
|
||||
- **Anchor-free design:** Eliminates anchor box tuning complexity
|
||||
- **Advanced augmentation:** Built-in data augmentation strategies
|
||||
- **Multi-scale detection:** Handles objects of different sizes effectively
|
||||
- **Export flexibility:** ONNX, TensorRT support for deployment optimization
|
||||
- **Active community:** Regular updates and extensive documentation
|
||||
4. **Handling Video Input:**
|
||||
- While not currently implemented, video processing would involve:
|
||||
- Frame extraction
|
||||
- Batch processing of frames
|
||||
- Real-time detection using YOLO's video processing capabilities
|
||||
- Optional frame skipping for performance optimization
|
||||
- The current architecture can be extended for video by:
|
||||
- Adding a video upload endpoint
|
||||
- Implementing frame-by-frame processing
|
||||
- Returning annotated video or real-time stream
|
||||
|
||||
### 2. **Hardware Considerations**
|
||||
|
||||
**Does CPU or GPU have an impact on your decision? Please explain.**
|
||||
|
||||
**Yes, hardware significantly impacts the implementation strategy:**
|
||||
|
||||
**Training Phase:**
|
||||
- **GPU Impact:** Critical for training efficiency
|
||||
- **GPU Training:** 5-10 minutes for 50 epochs (recommended)
|
||||
- **CPU Training:** 30-60 minutes for same epochs
|
||||
- **Memory Requirements:** 4GB+ GPU memory recommended
|
||||
- **Batch Size:** GPU allows larger batches (16-32) vs CPU (4-8)
|
||||
|
||||
**Inference Phase:**
|
||||
- **CPU Performance:** 37ms per image on modern CPU (Intel i5/i7, M1/M2)
|
||||
- **GPU Performance:** 10-15ms per image, better for batch processing
|
||||
- **Memory Usage:** CPU: 2-4GB RAM, GPU: 1-2GB VRAM
|
||||
- **Edge Deployment:** Model runs efficiently on CPU-only devices
|
||||
|
||||
**Decision Impact:**
|
||||
- **Algorithm Choice:** YOLOv8 Nano chosen specifically for CPU compatibility
|
||||
- **Deployment Flexibility:** No expensive GPU required for production
|
||||
- **Cost Efficiency:** Reduces infrastructure costs
|
||||
- **Scalability:** GPU enables high-throughput batch processing
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
# Auto-detection with fallback in train.py
|
||||
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
print(f"Using device: {device}")
|
||||
```
|
||||
|
||||
### 3. **Video Input Approach**
|
||||
|
||||
**What if a video is provided instead of single images?**
|
||||
**Does your approach change when processing videos? Please describe your approach.**
|
||||
|
||||
**Yes, the approach would change significantly for video processing:**
|
||||
|
||||
**Video Processing Strategy:**
|
||||
|
||||
**1. Frame Extraction & Sampling**
|
||||
```python
|
||||
def process_video(video_path, fps_sample=5):
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
frame_rate = cap.get(cv2.CAP_PROP_FPS)
|
||||
frame_interval = int(frame_rate / fps_sample) # Sample every N frames
|
||||
|
||||
frames = []
|
||||
frame_count = 0
|
||||
while cap.isOpened():
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
if frame_count % frame_interval == 0:
|
||||
frames.append(frame)
|
||||
frame_count += 1
|
||||
return frames
|
||||
```
|
||||
|
||||
**2. Batch Processing for Efficiency**
|
||||
```python
|
||||
def batch_detect_video(frames, batch_size=8):
|
||||
results = []
|
||||
for i in range(0, len(frames), batch_size):
|
||||
batch = frames[i:i+batch_size]
|
||||
batch_results = model(batch) # Process multiple frames at once
|
||||
results.extend(batch_results)
|
||||
return results
|
||||
```
|
||||
|
||||
**3. Temporal Consistency & Tracking**
|
||||
```python
|
||||
def apply_temporal_tracking(detections, frames):
|
||||
tracker = DeepSORT() # Or ByteTrack for better performance
|
||||
tracked_results = []
|
||||
|
||||
for frame_detections, frame in zip(detections, frames):
|
||||
tracked_objects = tracker.update(frame_detections)
|
||||
tracked_results.append(tracked_objects)
|
||||
|
||||
return tracked_results
|
||||
```
|
||||
|
||||
**4. Optimization Strategies**
|
||||
- **Motion Detection:** Skip frames with no significant changes
|
||||
- **Optical Flow:** Track objects between frames to reduce processing
|
||||
- **Keyframe Selection:** Process only important frames
|
||||
- **Parallel Processing:** Use multiple CPU cores/GPU streams
|
||||
- **Memory Management:** Process in chunks to avoid overflow
|
||||
|
||||
**5. Video-Specific Considerations**
|
||||
- **Temporal Smoothing:** Apply filters to reduce detection jitter
|
||||
- **Performance Scaling:** GPU becomes more critical for video processing
|
||||
- **Storage Requirements:** Annotated videos require significant storage
|
||||
- **Real-time Processing:** Streaming vs batch processing trade-offs
|
||||
|
||||
**Potential API Endpoint:**
|
||||
```python
|
||||
@app.route('/detect/video', methods=['POST'])
|
||||
def detect_video():
|
||||
# Upload video file
|
||||
# Extract frames at specified FPS
|
||||
# Batch process frames with YOLOv8
|
||||
# Apply temporal tracking for consistency
|
||||
# Return annotated video or frame-by-frame results
|
||||
```
|
||||
|
||||
## **Technical Questions Summary**
|
||||
|
||||
The project successfully addresses all required technical questions:
|
||||
|
||||
1. **✅ Algorithm Choice:** YOLOv8 Nano selected for optimal balance of accuracy (99.5% mAP50), speed (37ms), and deployment flexibility
|
||||
2. **✅ Hardware Considerations:** Comprehensive CPU/GPU analysis with auto-detection and fallback strategies for maximum compatibility
|
||||
3. **✅ Video Processing:** Complete video processing strategy with frame extraction, batch processing, temporal tracking, and optimization techniques
|
||||
|
||||
All technical decisions are implemented and validated in the working system.
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.8+
|
||||
- pip or conda
|
||||
|
||||
### Step-by-Step Installation
|
||||
|
||||
1. **Clone/Download the project**
|
||||
```bash
|
||||
cd ds_task_recycling_project
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Prepare dataset (if not already done)**
|
||||
```bash
|
||||
python3 prepare_dataset.py
|
||||
```
|
||||
|
||||
4. **Train the model**
|
||||
```bash
|
||||
# Basic training (recommended)
|
||||
python3 train.py
|
||||
|
||||
# Custom training parameters
|
||||
python3 train.py --epochs 150 --batch 8 --device cuda
|
||||
```
|
||||
|
||||
5. **Start the Flask API**
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
The API will be available at `http://localhost:5000`
|
||||
|
||||
## 🌐 Web Interface for QA Testing
|
||||
|
||||
We've included a comprehensive web interface for easy QA testing:
|
||||
|
||||
### Features:
|
||||
- **Drag & Drop Image Upload** - Easy image selection
|
||||
- **Real-time API Status** - Shows if API and model are loaded
|
||||
- **Multiple Test Options:**
|
||||
- Test hardcoded image
|
||||
- Upload custom images
|
||||
- Run comprehensive API tests
|
||||
- **Interactive Results** - View annotated images with detection details
|
||||
- **Confidence Threshold Control** - Adjust detection sensitivity
|
||||
- **Responsive Design** - Works on desktop and mobile
|
||||
|
||||
### Access:
|
||||
1. Start the API: `python3 main.py`
|
||||
2. Open browser: `http://localhost:5000`
|
||||
3. Use the interface to test detection functionality
|
||||
|
||||
### QA Testing Workflow:
|
||||
1. **Check API Status** - Verify green "API Online" indicator
|
||||
2. **Test Hardcoded Image** - Click "Test Hardcoded Image" button
|
||||
3. **Upload Custom Images** - Drag/drop or select motherboard images
|
||||
4. **Adjust Confidence** - Use slider to test different thresholds
|
||||
5. **Run All Tests** - Comprehensive API endpoint testing
|
||||
6. **Review Results** - Check detection accuracy and annotations
|
||||
|
||||
## 📡 API Documentation
|
||||
|
||||
### Base URL
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
## API Implementation
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### 1. **GET /** - API Information
|
||||
1. **Image Upload (`/detect`):**
|
||||
```http
|
||||
POST /detect
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
- Accepts image uploads
|
||||
- Returns annotated image with detection boxes
|
||||
|
||||
2. **Test Detection (`/detect/test`):**
|
||||
```http
|
||||
GET /detect/test
|
||||
```
|
||||
- Uses a hardcoded test image (memory_out19.png)
|
||||
- Returns annotated image with detection boxes
|
||||
|
||||
### Processing Workflow
|
||||
|
||||
1. Image Reception:
|
||||
- Via file upload or hardcoded test image
|
||||
2. Detection:
|
||||
- YOLOv8 processes the image
|
||||
- Confidence threshold: 0.25
|
||||
- IoU threshold: 0.45
|
||||
3. Annotation:
|
||||
- Bounding boxes drawn around detected modules
|
||||
4. Response:
|
||||
- Annotated image returned in PNG format
|
||||
|
||||
## Model Training
|
||||
|
||||
The model was trained with the following parameters:
|
||||
- 50 epochs
|
||||
- Image size: 640x640
|
||||
- Batch size: 8
|
||||
- Early stopping patience: 15
|
||||
- Augmentations:
|
||||
- Rotation (±5°)
|
||||
- Scale (0.5)
|
||||
- Translation (0.1)
|
||||
- Horizontal flip (0.5)
|
||||
- Mosaic (1.0)
|
||||
|
||||
## Dataset Preparation
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/
|
||||
training/
|
||||
├── memory/
|
||||
│ └── (images with memory modules) #You have this
|
||||
├── no_memory/
|
||||
│ └── (images without memory modules) #You have this as well
|
||||
├── train/
|
||||
│ ├── images/
|
||||
│ │ ├── memory_*.png
|
||||
│ │ └── no_memory_*.png
|
||||
│ └── labels/
|
||||
│ ├── memory_*.txt
|
||||
│ └── no_memory_*.txt
|
||||
└── val/
|
||||
├── images/
|
||||
│ ├── memory_*.png
|
||||
│ └── no_memory_*.png
|
||||
└── labels/
|
||||
├── memory_*.txt
|
||||
└── no_memory_*.txt
|
||||
|
||||
dataset.yaml
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Memory Module Detection API",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {...},
|
||||
"model_loaded": true,
|
||||
"supported_formats": ["png", "jpg", "jpeg", "gif", "bmp"]
|
||||
}
|
||||
The dataset is organized as follows:
|
||||
- `training/memory/`: Source directory for images with memory modules
|
||||
- `training/no_memory/`: Source directory for images without memory modules
|
||||
- `training/train/`: Training dataset
|
||||
- `images/`: Contains both memory and no-memory images with appropriate prefixes
|
||||
- `labels/`: Contains YOLO format annotation files
|
||||
- `training/val/`: Validation dataset
|
||||
- `images/`: Contains both memory and no-memory images with appropriate prefixes
|
||||
- `labels/`: Contains YOLO format annotation files
|
||||
|
||||
The `dataset.yaml` file contains:
|
||||
```yaml
|
||||
path: training # dataset root dir
|
||||
train: train/images # train images
|
||||
val: val/images # validation images
|
||||
nc: 1 # number of classes
|
||||
names: ['memory_module'] # class names
|
||||
```
|
||||
|
||||
#### 2. **GET /health** - Health Check
|
||||
## Getting Started
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone http://23.29.118.76:3000/michael/ds_task_recycling_project.git
|
||||
```
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Prepare the dataset:
|
||||
```bash
|
||||
python prepare_dataset.py
|
||||
```
|
||||
4. Train the model (if not already trained):
|
||||
```bash
|
||||
python train.py
|
||||
```
|
||||
5. Run the Flask application:
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
6. Access the web interface at `http://localhost:5000`
|
||||
|
||||
## Testing
|
||||
|
||||
The project includes comprehensive tests for the detector:
|
||||
- Batch detection testing
|
||||
- Threshold optimization
|
||||
- Various confidence/IoU threshold combinations
|
||||
|
||||
Run tests with:
|
||||
```bash
|
||||
curl http://localhost:5000/health
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
#### 3. **POST /detect** - Upload Image Detection
|
||||
```bash
|
||||
curl -X POST -F "image=@motherboard.png" -F "confidence=0.5" http://localhost:5000/detect
|
||||
```
|
||||
## Future Improvements
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"detections": [
|
||||
{
|
||||
"bbox": [100, 150, 200, 250],
|
||||
"confidence": 0.85,
|
||||
"class": 0,
|
||||
"class_name": "memory_module"
|
||||
}
|
||||
],
|
||||
"num_detections": 1,
|
||||
"annotated_image": "base64_encoded_image...",
|
||||
"confidence_threshold": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. **GET /detect/hardcoded** - Test with Hardcoded Image
|
||||
```bash
|
||||
curl "http://localhost:5000/detect/hardcoded?confidence=0.5"
|
||||
```
|
||||
|
||||
#### 5. **POST /detect/base64** - Base64 Image Detection
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"image": "base64_string", "confidence": 0.5}' \
|
||||
http://localhost:5000/detect/base64
|
||||
```
|
||||
|
||||
## 🧪 Testing & Usage Examples
|
||||
|
||||
### 1. **Test with Python requests**
|
||||
```python
|
||||
import requests
|
||||
import base64
|
||||
|
||||
# Test hardcoded image
|
||||
response = requests.get('http://localhost:5000/detect/hardcoded')
|
||||
result = response.json()
|
||||
print(f"Found {result['num_detections']} memory modules")
|
||||
|
||||
# Upload image
|
||||
with open('test_image.png', 'rb') as f:
|
||||
files = {'image': f}
|
||||
response = requests.post('http://localhost:5000/detect', files=files)
|
||||
result = response.json()
|
||||
```
|
||||
|
||||
### 2. **Test with curl**
|
||||
```bash
|
||||
# Basic detection
|
||||
curl -X POST -F "image=@training/memory/out1.png" http://localhost:5000/detect
|
||||
|
||||
# With custom confidence
|
||||
curl -X POST -F "image=@training/memory/out1.png" -F "confidence=0.3" http://localhost:5000/detect
|
||||
```
|
||||
|
||||
### 3. **Command Line Inference**
|
||||
```bash
|
||||
# Test single image
|
||||
python3 inference_utils.py --image training/memory/out1.png --conf 0.5
|
||||
|
||||
# Validate trained model
|
||||
python3 train.py --validate --model runs/detect/memory_module_detection/weights/best.pt
|
||||
```
|
||||
|
||||
## 📊 Training Details
|
||||
|
||||
### Dataset Statistics
|
||||
- **Total Images:** 40 (20 with memory, 20 without)
|
||||
- **Training Split:** 32 images (80%)
|
||||
- **Validation Split:** 8 images (20%)
|
||||
- **Classes:** 1 (memory_module)
|
||||
- **Annotation Format:** YOLO (normalized coordinates)
|
||||
|
||||
### Training Configuration
|
||||
```python
|
||||
# Default training parameters
|
||||
epochs = 100
|
||||
batch_size = 16
|
||||
image_size = 640
|
||||
confidence_threshold = 0.5
|
||||
iou_threshold = 0.45
|
||||
```
|
||||
|
||||
### Expected Training Time
|
||||
- **GPU (RTX 3060+):** 5-10 minutes
|
||||
- **CPU (Modern):** 30-60 minutes
|
||||
- **Memory Usage:** 2-4GB RAM
|
||||
|
||||
### Model Performance
|
||||
After training, you should see:
|
||||
- **mAP50:** >0.8 (80%+ accuracy at 50% IoU)
|
||||
- **Precision:** >0.85
|
||||
- **Recall:** >0.80
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. **Model Not Found Error**
|
||||
```
|
||||
Error: Model not found at runs/detect/memory_module_detection/weights/best.pt
|
||||
```
|
||||
**Solution:** Train the model first
|
||||
```bash
|
||||
python3 train.py
|
||||
```
|
||||
|
||||
#### 2. **CUDA Out of Memory**
|
||||
```
|
||||
RuntimeError: CUDA out of memory
|
||||
```
|
||||
**Solutions:**
|
||||
- Reduce batch size: `python3 train.py --batch 8`
|
||||
- Use CPU: `python3 train.py --device cpu`
|
||||
- Close other GPU applications
|
||||
|
||||
#### 3. **Import Error: ultralytics**
|
||||
```
|
||||
ModuleNotFoundError: No module named 'ultralytics'
|
||||
```
|
||||
**Solution:**
|
||||
```bash
|
||||
pip install ultralytics
|
||||
```
|
||||
|
||||
#### 4. **Flask Port Already in Use**
|
||||
```
|
||||
OSError: [Errno 48] Address already in use
|
||||
```
|
||||
**Solution:**
|
||||
```bash
|
||||
# Kill process using port 5000
|
||||
lsof -ti:5000 | xargs kill -9
|
||||
|
||||
# Or use different port
|
||||
python3 main.py # Edit main.py to change port
|
||||
```
|
||||
|
||||
#### 5. **Low Detection Accuracy**
|
||||
**Solutions:**
|
||||
- Increase training epochs: `python3 train.py --epochs 200`
|
||||
- Lower confidence threshold: `confidence=0.3`
|
||||
- Check image quality and lighting
|
||||
- Verify annotations are correct
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
#### For Better Accuracy:
|
||||
1. **More Training Data:** Add more annotated images
|
||||
2. **Data Augmentation:** Already included in YOLOv8
|
||||
3. **Hyperparameter Tuning:** Adjust learning rate, batch size
|
||||
4. **Model Size:** Use YOLOv8s or YOLOv8m for better accuracy
|
||||
|
||||
#### For Faster Inference:
|
||||
1. **Model Quantization:** Convert to TensorRT or ONNX
|
||||
2. **Batch Processing:** Process multiple images together
|
||||
3. **Image Resizing:** Use smaller input size (320x320)
|
||||
|
||||
## 📁 File Descriptions
|
||||
|
||||
- **`main.py`** - Flask API with all endpoints
|
||||
- **`train.py`** - YOLOv8 training script with validation
|
||||
- **`inference_utils.py`** - Detection utilities and visualization
|
||||
- **`prepare_dataset.py`** - Dataset preparation and splitting
|
||||
- **`requirements.txt`** - Python dependencies
|
||||
- **`dataset.yaml`** - YOLO dataset configuration
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
1. **Video Processing:** Add video upload and processing endpoints
|
||||
2. **Model Ensemble:** Combine multiple models for better accuracy
|
||||
3. **Real-time Streaming:** WebSocket support for live camera feeds
|
||||
4. **Database Integration:** Store detection results and statistics
|
||||
5. **Web Interface:** HTML frontend for easier testing
|
||||
6. **Docker Deployment:** Containerized deployment
|
||||
7. **Model Versioning:** Support multiple model versions
|
||||
8. **Batch Processing:** Process multiple images simultaneously
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is for educational and training purposes.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This is a toy project for training purposes. Feel free to experiment and improve!
|
||||
1. GPU support for faster processing
|
||||
2. Video input support
|
||||
3. Real-time streaming capabilities
|
||||
4. More sophisticated augmentation techniques
|
||||
5. Model quantization for improved CPU performance
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
# Project Validation Checklist
|
||||
|
||||
## ✅ README Requirements Validation
|
||||
|
||||
### Original Requirements from README:
|
||||
1. **Flask API that processes motherboard images** ✅
|
||||
2. **Detects memory modules present on motherboards** ✅
|
||||
3. **Returns image with bounding boxes around detected memory modules** ✅
|
||||
4. **Image upload via Flask API** ✅
|
||||
5. **Hardcoded image for testing purposes** ✅
|
||||
6. **Dataset: 20 pictures with memory, 20 without memory** ✅
|
||||
7. **Annotation tool suggestion: makesense.ai** ✅ (Already annotated)
|
||||
|
||||
### Additional Features Implemented:
|
||||
- ✅ **Web Frontend for QA Testing** (Beyond requirements)
|
||||
- ✅ **Base64 image processing endpoint**
|
||||
- ✅ **Comprehensive API testing suite**
|
||||
- ✅ **Automated setup script**
|
||||
- ✅ **Complete documentation**
|
||||
|
||||
## 🔧 Technical Implementation Validation
|
||||
|
||||
### Algorithm Choice Questions Answered:
|
||||
1. **Which algorithm for detecting memory modules?**
|
||||
- ✅ **Answer: YOLOv8 Nano**
|
||||
- ✅ **Reasoning: State-of-the-art performance, real-time inference, pre-trained weights, easy integration**
|
||||
|
||||
2. **Hardware considerations (CPU vs GPU impact)?**
|
||||
- ✅ **Training: GPU recommended (5-10 min vs 30-60 min CPU)**
|
||||
- ✅ **Inference: CPU sufficient for real-time, GPU better for batch processing**
|
||||
- ✅ **Implementation: Auto-detection with fallback**
|
||||
|
||||
3. **Video input approach?**
|
||||
- ✅ **Approach described: Frame extraction + batch processing + temporal tracking**
|
||||
- ✅ **Implementation strategy provided with pseudo-code**
|
||||
|
||||
## 📁 File Structure Validation
|
||||
|
||||
### Required Files:
|
||||
- ✅ `main.py` - Flask API application
|
||||
- ✅ `train.py` - YOLOv8 training script
|
||||
- ✅ `inference_utils.py` - Detection and visualization utilities
|
||||
- ✅ `prepare_dataset.py` - Dataset preparation script
|
||||
- ✅ `requirements.txt` - Python dependencies
|
||||
- ✅ `dataset.yaml` - YOLO dataset configuration
|
||||
- ✅ `README.md` - Complete documentation
|
||||
|
||||
### Additional Files Created:
|
||||
- ✅ `test_api.py` - API testing script
|
||||
- ✅ `setup.py` - Automated setup script
|
||||
- ✅ `templates/index.html` - Web interface
|
||||
- ✅ `static/style.css` - Frontend styling
|
||||
- ✅ `static/script.js` - Frontend functionality
|
||||
- ✅ `VALIDATION_CHECKLIST.md` - This validation document
|
||||
|
||||
### Dataset Structure:
|
||||
- ✅ `training/memory/` - 20 images with memory modules + YOLO labels
|
||||
- ✅ `training/no_memory/` - 20 images without memory modules
|
||||
- ✅ `training/train/` - Training split (80% = 32 images)
|
||||
- ✅ `training/val/` - Validation split (20% = 8 images)
|
||||
|
||||
## 🚀 API Endpoints Validation
|
||||
|
||||
### Required Endpoints:
|
||||
1. ✅ **Image upload endpoint** - `POST /detect`
|
||||
2. ✅ **Hardcoded image endpoint** - `GET /detect/hardcoded`
|
||||
|
||||
### Additional Endpoints:
|
||||
3. ✅ **API information** - `GET /` (serves frontend) & `GET /api` (JSON)
|
||||
4. ✅ **Health check** - `GET /health`
|
||||
5. ✅ **Base64 processing** - `POST /detect/base64`
|
||||
6. ✅ **Error handlers** - 404, 413, 500
|
||||
|
||||
## 🧪 Testing Validation
|
||||
|
||||
### Test Coverage:
|
||||
- ✅ **API health check testing**
|
||||
- ✅ **Hardcoded image detection testing**
|
||||
- ✅ **File upload testing**
|
||||
- ✅ **Base64 image testing**
|
||||
- ✅ **Error handling testing**
|
||||
- ✅ **Web interface testing**
|
||||
|
||||
### Test Scripts:
|
||||
- ✅ `test_api.py` - Comprehensive API testing
|
||||
- ✅ Web interface - Interactive QA testing
|
||||
- ✅ `setup.py` - Automated setup validation
|
||||
|
||||
## 📦 Dependencies Validation
|
||||
|
||||
### Core Dependencies:
|
||||
- ✅ `ultralytics` - YOLOv8 implementation
|
||||
- ✅ `torch` & `torchvision` - PyTorch for ML
|
||||
- ✅ `opencv-python` - Image processing
|
||||
- ✅ `Pillow` - Image handling
|
||||
- ✅ `Flask` & `Flask-CORS` - Web framework
|
||||
- ✅ `numpy` - Numerical operations
|
||||
- ✅ `PyYAML` - Configuration files
|
||||
|
||||
### Additional Dependencies:
|
||||
- ✅ `Werkzeug` - Flask utilities
|
||||
- ✅ `requests` - HTTP testing
|
||||
- ✅ `tqdm` - Progress bars
|
||||
- ✅ `matplotlib` & `seaborn` - Visualization (optional)
|
||||
|
||||
## 🎯 Functional Requirements Validation
|
||||
|
||||
### Input Processing:
|
||||
- ✅ **Accepts PNG, JPG, JPEG, GIF, BMP formats**
|
||||
- ✅ **File size limit: 16MB**
|
||||
- ✅ **Drag & drop support in web interface**
|
||||
- ✅ **Base64 encoding support**
|
||||
- ✅ **Confidence threshold adjustment**
|
||||
|
||||
### Output Generation:
|
||||
- ✅ **Bounding boxes around detected memory modules**
|
||||
- ✅ **Confidence scores for each detection**
|
||||
- ✅ **Annotated images returned as base64**
|
||||
- ✅ **JSON response with detection details**
|
||||
- ✅ **Visual feedback in web interface**
|
||||
|
||||
### Model Performance:
|
||||
- ✅ **Single class detection: 'memory_module'**
|
||||
- ✅ **YOLO format annotations**
|
||||
- ✅ **Transfer learning from COCO dataset**
|
||||
- ✅ **Configurable confidence and IoU thresholds**
|
||||
|
||||
## 🌐 Web Interface Validation
|
||||
|
||||
### QA Testing Features:
|
||||
- ✅ **Real-time API status indicator**
|
||||
- ✅ **Drag & drop image upload**
|
||||
- ✅ **Confidence threshold slider**
|
||||
- ✅ **Multiple testing options**
|
||||
- ✅ **Interactive results display**
|
||||
- ✅ **Responsive design**
|
||||
- ✅ **Error handling and feedback**
|
||||
|
||||
### User Experience:
|
||||
- ✅ **Intuitive interface design**
|
||||
- ✅ **Clear visual feedback**
|
||||
- ✅ **Loading indicators**
|
||||
- ✅ **Result visualization**
|
||||
- ✅ **Mobile compatibility**
|
||||
|
||||
## 📚 Documentation Validation
|
||||
|
||||
### README Completeness:
|
||||
- ✅ **Quick start guide**
|
||||
- ✅ **Installation instructions**
|
||||
- ✅ **API documentation**
|
||||
- ✅ **Usage examples**
|
||||
- ✅ **Troubleshooting guide**
|
||||
- ✅ **Technical decisions explained**
|
||||
- ✅ **Project structure documented**
|
||||
|
||||
### Code Documentation:
|
||||
- ✅ **Docstrings in all functions**
|
||||
- ✅ **Inline comments for complex logic**
|
||||
- ✅ **Type hints where appropriate**
|
||||
- ✅ **Error handling documented**
|
||||
|
||||
## 🔄 Setup & Deployment Validation
|
||||
|
||||
### Setup Options:
|
||||
- ✅ **Manual setup with step-by-step instructions**
|
||||
- ✅ **Automated setup script (`setup.py`)**
|
||||
- ✅ **Requirements file for dependencies**
|
||||
- ✅ **Dataset preparation script**
|
||||
|
||||
### Deployment Readiness:
|
||||
- ✅ **Production-ready Flask configuration**
|
||||
- ✅ **Error handling and logging**
|
||||
- ✅ **CORS support for frontend**
|
||||
- ✅ **File upload security**
|
||||
- ✅ **Model loading validation**
|
||||
|
||||
## 🎉 Final Validation Summary
|
||||
|
||||
### ✅ **ALL ORIGINAL REQUIREMENTS MET**
|
||||
### ✅ **ADDITIONAL FEATURES IMPLEMENTED**
|
||||
### ✅ **COMPREHENSIVE TESTING SUITE**
|
||||
### ✅ **PRODUCTION-READY CODE**
|
||||
### ✅ **EXCELLENT DOCUMENTATION**
|
||||
### ✅ **QA-FRIENDLY WEB INTERFACE**
|
||||
|
||||
## 🚀 Ready for QA Testing!
|
||||
|
||||
The project is complete and ready for quality assurance testing. All original requirements have been met and exceeded with additional features for better usability and testing.
|
||||
@@ -0,0 +1,14 @@
|
||||
from flask import Flask
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
# Register blueprints
|
||||
from app.routes import main_bp
|
||||
app.register_blueprint(main_bp)
|
||||
|
||||
# Ensure the static folder is properly set
|
||||
app.static_folder = 'static'
|
||||
app.template_folder = 'templates'
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,57 @@
|
||||
from flask import Blueprint, request, jsonify, send_file, render_template
|
||||
from app.utils.detector import MemoryDetector
|
||||
import os
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
detector = MemoryDetector()
|
||||
|
||||
@main_bp.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@main_bp.route('/detect', methods=['POST'])
|
||||
def detect_memory():
|
||||
if 'image' not in request.files:
|
||||
return jsonify({'error': 'No image provided'}), 400
|
||||
|
||||
file = request.files['image']
|
||||
|
||||
# Read the image
|
||||
img = Image.open(file.stream)
|
||||
|
||||
# Process the image and get annotated image and detections
|
||||
annotated_img, detections = detector.detect(img)
|
||||
|
||||
# Convert PIL image to bytes
|
||||
img_byte_arr = io.BytesIO()
|
||||
annotated_img.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr.seek(0)
|
||||
|
||||
return send_file(
|
||||
img_byte_arr,
|
||||
mimetype='image/png'
|
||||
)
|
||||
|
||||
@main_bp.route('/detect/test', methods=['GET'])
|
||||
def detect_test():
|
||||
"""Endpoint for testing with a hardcoded image"""
|
||||
# Using an existing image from the validation set
|
||||
test_image_path = os.path.join('training', 'val', 'images', 'memory_out19.png')
|
||||
|
||||
if not os.path.exists(test_image_path):
|
||||
return jsonify({'error': f'Test image not found at {test_image_path}'}), 404
|
||||
|
||||
img = Image.open(test_image_path)
|
||||
# Get both the annotated image and detections
|
||||
annotated_img, detections = detector.detect(img)
|
||||
|
||||
img_byte_arr = io.BytesIO()
|
||||
annotated_img.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr.seek(0)
|
||||
|
||||
return send_file(
|
||||
img_byte_arr,
|
||||
mimetype='image/png'
|
||||
)
|
||||
@@ -0,0 +1,159 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 200px;
|
||||
border: 2px dashed #3498db;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.upload-box:hover {
|
||||
border-color: #2980b9;
|
||||
}
|
||||
|
||||
.upload-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.browse-text {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.8rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#detectButton {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#detectButton:disabled {
|
||||
background-color: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#testButton {
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#detectButton:hover:not(:disabled) {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
#testButton:hover {
|
||||
background-color: #27ae60;
|
||||
}
|
||||
|
||||
.results-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-box {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.image-box h3 {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-box img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #f3f3f3;
|
||||
border-top: 5px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 2.9 MiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#3498db" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,100 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const detectButton = document.getElementById('detectButton');
|
||||
const testButton = document.getElementById('testButton');
|
||||
const originalImage = document.getElementById('originalImage');
|
||||
const resultImage = document.getElementById('resultImage');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
// Handle drag and drop
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.style.borderColor = '#2980b9';
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.style.borderColor = '#3498db';
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.style.borderColor = '#3498db';
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
handleImageSelection(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle click to upload
|
||||
dropZone.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
handleImageSelection(file);
|
||||
}
|
||||
});
|
||||
|
||||
function handleImageSelection(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
originalImage.src = e.target.result;
|
||||
originalImage.style.display = 'block';
|
||||
detectButton.disabled = false;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// Handle detect button click
|
||||
detectButton.addEventListener('click', async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', fileInput.files[0]);
|
||||
|
||||
try {
|
||||
loading.style.display = 'flex';
|
||||
const response = await fetch('/detect', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
resultImage.src = URL.createObjectURL(blob);
|
||||
resultImage.style.display = 'block';
|
||||
} else {
|
||||
alert('Error processing image');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error processing image');
|
||||
} finally {
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Handle test button click
|
||||
testButton.addEventListener('click', async () => {
|
||||
try {
|
||||
loading.style.display = 'flex';
|
||||
const response = await fetch('/detect/test');
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
resultImage.src = URL.createObjectURL(blob);
|
||||
resultImage.style.display = 'block';
|
||||
} else {
|
||||
alert('Error running test detection');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error running test detection');
|
||||
} finally {
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Memory Module Detector</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Memory Module Detector</h1>
|
||||
|
||||
<div class="upload-section">
|
||||
<div class="upload-box" id="dropZone">
|
||||
<input type="file" id="fileInput" accept="image/*" hidden>
|
||||
<div class="upload-content">
|
||||
<img src="{{ url_for('static', filename='images/upload-icon.svg') }}" alt="Upload" class="upload-icon">
|
||||
<p>Drag and drop an image or <span class="browse-text">browse</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="detectButton" disabled>Detect Memory Modules</button>
|
||||
<button id="testButton">Run Test Detection</button>
|
||||
</div>
|
||||
|
||||
<div class="results-section">
|
||||
<div class="image-container">
|
||||
<div class="image-box">
|
||||
<h3>Original Image</h3>
|
||||
<img id="originalImage" src="" alt="Original image will appear here">
|
||||
</div>
|
||||
<div class="image-box">
|
||||
<h3>Detected Results</h3>
|
||||
<img id="resultImage" src="" alt="Detection results will appear here">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="loading-spinner" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p>Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
from ultralytics import YOLO
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from typing import Tuple, List, Dict
|
||||
|
||||
class MemoryDetector:
|
||||
def __init__(self,
|
||||
model_path='model/weights/best.pt',
|
||||
conf_threshold=0.25,
|
||||
iou_threshold=0.45):
|
||||
"""
|
||||
Initialize the detector with the trained model.
|
||||
|
||||
Args:
|
||||
model_path (str): Path to the trained model weights
|
||||
conf_threshold (float): Confidence threshold for detections
|
||||
iou_threshold (float): IoU threshold for NMS
|
||||
"""
|
||||
self.model = YOLO(model_path)
|
||||
self.conf_threshold = conf_threshold
|
||||
self.iou_threshold = iou_threshold
|
||||
|
||||
def detect(self,
|
||||
image: Image.Image,
|
||||
conf_threshold: float = None,
|
||||
iou_threshold: float = None) -> Tuple[Image.Image, List[Dict]]:
|
||||
"""
|
||||
Detect memory modules in the given image.
|
||||
|
||||
Args:
|
||||
image (PIL.Image): Input image to process
|
||||
conf_threshold (float, optional): Override default confidence threshold
|
||||
iou_threshold (float, optional): Override default IoU threshold
|
||||
|
||||
Returns:
|
||||
Tuple[PIL.Image, List[Dict]]: Annotated image and list of detections
|
||||
"""
|
||||
# Use provided thresholds or defaults
|
||||
conf = conf_threshold if conf_threshold is not None else self.conf_threshold
|
||||
iou = iou_threshold if iou_threshold is not None else self.iou_threshold
|
||||
|
||||
# Run inference
|
||||
results = self.model.predict(
|
||||
source=image,
|
||||
conf=conf,
|
||||
iou=iou,
|
||||
max_det=10,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Get the annotated image
|
||||
annotated_img = results[0].plot()
|
||||
|
||||
# Extract detection information
|
||||
detections = []
|
||||
for box in results[0].boxes:
|
||||
detection = {
|
||||
'xyxy': box.xyxy[0].tolist(), # Bounding box coordinates
|
||||
'confidence': float(box.conf[0]), # Detection confidence
|
||||
'class': int(box.cls[0]) # Class ID
|
||||
}
|
||||
detections.append(detection)
|
||||
|
||||
return Image.fromarray(annotated_img), detections
|
||||
|
||||
def optimize_thresholds(self, validation_images: List[Image.Image]) -> Tuple[float, float]:
|
||||
"""
|
||||
Find optimal confidence and IoU thresholds using validation images.
|
||||
|
||||
Args:
|
||||
validation_images (List[Image.Image]): List of validation images
|
||||
|
||||
Returns:
|
||||
Tuple[float, float]: Optimal confidence and IoU thresholds
|
||||
"""
|
||||
best_conf = 0.25
|
||||
best_iou = 0.45
|
||||
|
||||
# Grid search for best parameters
|
||||
conf_range = [0.15, 0.2, 0.25, 0.3, 0.35]
|
||||
iou_range = [0.35, 0.4, 0.45, 0.5, 0.55]
|
||||
|
||||
best_score = 0
|
||||
|
||||
for conf in conf_range:
|
||||
for iou in iou_range:
|
||||
total_score = 0
|
||||
for img in validation_images:
|
||||
_, detections = self.detect(img, conf, iou)
|
||||
# Score based on number of detections and confidence
|
||||
score = sum([d['confidence'] for d in detections])
|
||||
total_score += score
|
||||
|
||||
if total_score > best_score:
|
||||
best_score = total_score
|
||||
best_conf = conf
|
||||
best_iou = iou
|
||||
|
||||
return best_conf, best_iou
|
||||
@@ -1,5 +0,0 @@
|
||||
path: training # dataset root dir
|
||||
train: train/images # train images
|
||||
val: val/images # validation images
|
||||
nc: 1 # number of classes
|
||||
names: ['memory_module'] # class names
|
||||
@@ -1,287 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Inference utilities for memory module detection.
|
||||
Contains functions for model loading, inference, and visualization.
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
from ultralytics import YOLO
|
||||
import torch
|
||||
|
||||
class MemoryModuleDetector:
|
||||
"""Memory module detector using YOLOv8."""
|
||||
|
||||
def __init__(self, model_path='runs/detect/memory_module_detection/weights/best.pt'):
|
||||
"""
|
||||
Initialize the detector.
|
||||
|
||||
Args:
|
||||
model_path (str): Path to the trained YOLOv8 model
|
||||
"""
|
||||
self.model_path = model_path
|
||||
self.model = None
|
||||
self.class_names = ['memory_module']
|
||||
self.colors = [(0, 255, 0)] # Green for memory modules
|
||||
|
||||
# Load model if it exists
|
||||
if os.path.exists(model_path):
|
||||
self.load_model()
|
||||
else:
|
||||
print(f"Warning: Model not found at {model_path}")
|
||||
print("Please train the model first using train.py")
|
||||
|
||||
def load_model(self):
|
||||
"""Load the trained YOLOv8 model."""
|
||||
try:
|
||||
# Fix for PyTorch 2.6+ weights_only issue
|
||||
import torch
|
||||
# Use weights_only=False for compatibility
|
||||
with torch.serialization.safe_globals(['ultralytics.nn.tasks.DetectionModel']):
|
||||
self.model = YOLO(self.model_path)
|
||||
print(f"Model loaded successfully from {self.model_path}")
|
||||
except Exception as e:
|
||||
try:
|
||||
# Fallback: try loading with weights_only=False
|
||||
import torch
|
||||
original_load = torch.load
|
||||
torch.load = lambda *args, **kwargs: original_load(*args, **kwargs, weights_only=False)
|
||||
self.model = YOLO(self.model_path)
|
||||
torch.load = original_load
|
||||
print(f"Model loaded successfully from {self.model_path} (fallback method)")
|
||||
except Exception as e2:
|
||||
print(f"Error loading model: {e2}")
|
||||
self.model = None
|
||||
|
||||
def detect(self, image_path, conf_threshold=0.5, iou_threshold=0.45):
|
||||
"""
|
||||
Detect memory modules in an image.
|
||||
|
||||
Args:
|
||||
image_path (str): Path to the input image
|
||||
conf_threshold (float): Confidence threshold for detections
|
||||
iou_threshold (float): IoU threshold for NMS
|
||||
|
||||
Returns:
|
||||
tuple: (detections, annotated_image)
|
||||
"""
|
||||
if self.model is None:
|
||||
raise ValueError("Model not loaded. Please check model path.")
|
||||
|
||||
# Run inference
|
||||
results = self.model(image_path, conf=conf_threshold, iou=iou_threshold)
|
||||
|
||||
# Extract detections
|
||||
detections = []
|
||||
if len(results) > 0 and results[0].boxes is not None:
|
||||
boxes = results[0].boxes
|
||||
for i in range(len(boxes)):
|
||||
box = boxes.xyxy[i].cpu().numpy() # x1, y1, x2, y2
|
||||
conf = boxes.conf[i].cpu().numpy()
|
||||
cls = int(boxes.cls[i].cpu().numpy())
|
||||
|
||||
detection = {
|
||||
'bbox': box.tolist(),
|
||||
'confidence': float(conf),
|
||||
'class': int(cls),
|
||||
'class_name': self.class_names[cls] if cls < len(self.class_names) else 'unknown'
|
||||
}
|
||||
detections.append(detection)
|
||||
|
||||
# Create annotated image
|
||||
annotated_image = self.draw_detections(image_path, detections)
|
||||
|
||||
return detections, annotated_image
|
||||
|
||||
def draw_detections(self, image_path, detections):
|
||||
"""
|
||||
Draw bounding boxes on the image.
|
||||
|
||||
Args:
|
||||
image_path (str): Path to the input image
|
||||
detections (list): List of detection dictionaries
|
||||
|
||||
Returns:
|
||||
PIL.Image: Annotated image
|
||||
"""
|
||||
# Load image
|
||||
image = Image.open(image_path).convert('RGB')
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Try to load a font
|
||||
try:
|
||||
font = ImageFont.truetype("arial.ttf", 16)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Draw each detection
|
||||
for detection in detections:
|
||||
bbox = detection['bbox']
|
||||
confidence = detection['confidence']
|
||||
class_name = detection['class_name']
|
||||
|
||||
# Extract coordinates
|
||||
x1, y1, x2, y2 = bbox
|
||||
|
||||
# Draw bounding box
|
||||
color = self.colors[0] # Green for memory modules
|
||||
draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
|
||||
|
||||
# Draw label
|
||||
label = f"{class_name}: {confidence:.2f}"
|
||||
|
||||
# Get text size for background
|
||||
bbox_text = draw.textbbox((0, 0), label, font=font)
|
||||
text_width = bbox_text[2] - bbox_text[0]
|
||||
text_height = bbox_text[3] - bbox_text[1]
|
||||
|
||||
# Draw background for text
|
||||
draw.rectangle([x1, y1 - text_height - 4, x1 + text_width + 4, y1],
|
||||
fill=color, outline=color)
|
||||
|
||||
# Draw text
|
||||
draw.text((x1 + 2, y1 - text_height - 2), label, fill=(255, 255, 255), font=font)
|
||||
|
||||
return image
|
||||
|
||||
def detect_from_array(self, image_array, conf_threshold=0.5, iou_threshold=0.45):
|
||||
"""
|
||||
Detect memory modules from a numpy array.
|
||||
|
||||
Args:
|
||||
image_array (np.ndarray): Input image as numpy array
|
||||
conf_threshold (float): Confidence threshold for detections
|
||||
iou_threshold (float): IoU threshold for NMS
|
||||
|
||||
Returns:
|
||||
tuple: (detections, annotated_image)
|
||||
"""
|
||||
if self.model is None:
|
||||
raise ValueError("Model not loaded. Please check model path.")
|
||||
|
||||
# Convert numpy array to PIL Image if needed
|
||||
if isinstance(image_array, np.ndarray):
|
||||
if image_array.dtype != np.uint8:
|
||||
image_array = (image_array * 255).astype(np.uint8)
|
||||
image = Image.fromarray(image_array)
|
||||
else:
|
||||
image = image_array
|
||||
|
||||
# Run inference
|
||||
results = self.model(image, conf=conf_threshold, iou=iou_threshold)
|
||||
|
||||
# Extract detections
|
||||
detections = []
|
||||
if len(results) > 0 and results[0].boxes is not None:
|
||||
boxes = results[0].boxes
|
||||
for i in range(len(boxes)):
|
||||
box = boxes.xyxy[i].cpu().numpy() # x1, y1, x2, y2
|
||||
conf = boxes.conf[i].cpu().numpy()
|
||||
cls = int(boxes.cls[i].cpu().numpy())
|
||||
|
||||
detection = {
|
||||
'bbox': box.tolist(),
|
||||
'confidence': float(conf),
|
||||
'class': int(cls),
|
||||
'class_name': self.class_names[cls] if cls < len(self.class_names) else 'unknown'
|
||||
}
|
||||
detections.append(detection)
|
||||
|
||||
# Create annotated image
|
||||
annotated_image = self.draw_detections_on_image(image, detections)
|
||||
|
||||
return detections, annotated_image
|
||||
|
||||
def draw_detections_on_image(self, image, detections):
|
||||
"""
|
||||
Draw bounding boxes on a PIL Image.
|
||||
|
||||
Args:
|
||||
image (PIL.Image): Input image
|
||||
detections (list): List of detection dictionaries
|
||||
|
||||
Returns:
|
||||
PIL.Image: Annotated image
|
||||
"""
|
||||
# Make a copy to avoid modifying the original
|
||||
annotated_image = image.copy()
|
||||
draw = ImageDraw.Draw(annotated_image)
|
||||
|
||||
# Try to load a font
|
||||
try:
|
||||
font = ImageFont.truetype("arial.ttf", 16)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Draw each detection
|
||||
for detection in detections:
|
||||
bbox = detection['bbox']
|
||||
confidence = detection['confidence']
|
||||
class_name = detection['class_name']
|
||||
|
||||
# Extract coordinates
|
||||
x1, y1, x2, y2 = bbox
|
||||
|
||||
# Draw bounding box
|
||||
color = self.colors[0] # Green for memory modules
|
||||
draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
|
||||
|
||||
# Draw label
|
||||
label = f"{class_name}: {confidence:.2f}"
|
||||
|
||||
# Get text size for background
|
||||
bbox_text = draw.textbbox((0, 0), label, font=font)
|
||||
text_width = bbox_text[2] - bbox_text[0]
|
||||
text_height = bbox_text[3] - bbox_text[1]
|
||||
|
||||
# Draw background for text
|
||||
draw.rectangle([x1, y1 - text_height - 4, x1 + text_width + 4, y1],
|
||||
fill=color, outline=color)
|
||||
|
||||
# Draw text
|
||||
draw.text((x1 + 2, y1 - text_height - 2), label, fill=(255, 255, 255), font=font)
|
||||
|
||||
return annotated_image
|
||||
|
||||
def test_inference(image_path, model_path='runs/detect/memory_module_detection/weights/best.pt'):
|
||||
"""
|
||||
Test inference on a single image.
|
||||
|
||||
Args:
|
||||
image_path (str): Path to test image
|
||||
model_path (str): Path to trained model
|
||||
"""
|
||||
detector = MemoryModuleDetector(model_path)
|
||||
|
||||
if detector.model is None:
|
||||
print("Cannot run inference without a trained model.")
|
||||
return
|
||||
|
||||
print(f"Running inference on: {image_path}")
|
||||
detections, annotated_image = detector.detect(image_path)
|
||||
|
||||
print(f"Found {len(detections)} memory modules:")
|
||||
for i, detection in enumerate(detections):
|
||||
print(f" {i+1}. {detection['class_name']} (confidence: {detection['confidence']:.3f})")
|
||||
|
||||
# Save annotated image
|
||||
output_path = f"annotated_{os.path.basename(image_path)}"
|
||||
annotated_image.save(output_path)
|
||||
print(f"Annotated image saved as: {output_path}")
|
||||
|
||||
return detections, annotated_image
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Test memory module detection')
|
||||
parser.add_argument('--image', type=str, required=True, help='Path to test image')
|
||||
parser.add_argument('--model', type=str, default='runs/detect/memory_module_detection/weights/best.pt',
|
||||
help='Path to trained model')
|
||||
parser.add_argument('--conf', type=float, default=0.5, help='Confidence threshold')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
test_inference(args.image, args.model)
|
||||
@@ -1,408 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Flask API for Memory Module Detection
|
||||
This API processes motherboard images and detects memory modules using YOLOv8.
|
||||
"""
|
||||
|
||||
import os
|
||||
import io
|
||||
import base64
|
||||
from flask import Flask, request, jsonify, send_file, render_template
|
||||
from flask_cors import CORS
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from werkzeug.utils import secure_filename
|
||||
import tempfile
|
||||
import logging
|
||||
from inference_utils import MemoryModuleDetector
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# Configuration
|
||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
|
||||
UPLOAD_FOLDER = 'uploads'
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}
|
||||
|
||||
# Create upload folder if it doesn't exist
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
|
||||
# Initialize detector
|
||||
MODEL_PATH = 'runs/detect/memory_module_detection/weights/best.pt'
|
||||
detector = MemoryModuleDetector(MODEL_PATH)
|
||||
|
||||
# Hardcoded test image path
|
||||
HARDCODED_IMAGE_PATH = 'training/memory/out1.png'
|
||||
|
||||
def allowed_file(filename):
|
||||
"""Check if file extension is allowed."""
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def image_to_base64(image):
|
||||
"""Convert PIL Image to base64 string."""
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format='PNG')
|
||||
img_str = base64.b64encode(buffer.getvalue()).decode()
|
||||
return img_str
|
||||
|
||||
def base64_to_image(base64_string):
|
||||
"""Convert base64 string to PIL Image."""
|
||||
img_data = base64.b64decode(base64_string)
|
||||
image = Image.open(io.BytesIO(img_data))
|
||||
return image
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def home():
|
||||
"""Home endpoint - serve frontend or API information based on Accept header."""
|
||||
# Check if request is from a browser (wants HTML)
|
||||
if 'text/html' in request.headers.get('Accept', ''):
|
||||
return render_template('index.html')
|
||||
|
||||
# Otherwise return JSON API information
|
||||
return jsonify({
|
||||
'message': 'Memory Module Detection API',
|
||||
'version': '1.0.0',
|
||||
'endpoints': {
|
||||
'/': 'GET - Frontend interface or API information',
|
||||
'/api': 'GET - API information (JSON)',
|
||||
'/detect': 'POST - Upload image for memory module detection',
|
||||
'/detect/hardcoded': 'GET - Process hardcoded test image',
|
||||
'/detect/base64': 'POST - Process base64 encoded image',
|
||||
'/health': 'GET - Health check'
|
||||
},
|
||||
'model_loaded': detector.model is not None,
|
||||
'supported_formats': list(ALLOWED_EXTENSIONS)
|
||||
})
|
||||
|
||||
@app.route('/api', methods=['GET'])
|
||||
def api_info():
|
||||
"""API information endpoint (always returns JSON)."""
|
||||
return jsonify({
|
||||
'message': 'Memory Module Detection API',
|
||||
'version': '1.0.0',
|
||||
'endpoints': {
|
||||
'/': 'GET - Frontend interface or API information',
|
||||
'/api': 'GET - API information (JSON)',
|
||||
'/docs': 'GET - Detailed API documentation',
|
||||
'/detect': 'POST - Upload image for memory module detection',
|
||||
'/detect/hardcoded': 'GET - Process hardcoded test image',
|
||||
'/detect/base64': 'POST - Process base64 encoded image',
|
||||
'/health': 'GET - Health check'
|
||||
},
|
||||
'model_loaded': detector.model is not None,
|
||||
'supported_formats': list(ALLOWED_EXTENSIONS),
|
||||
'documentation': 'Visit /docs for detailed API documentation'
|
||||
})
|
||||
|
||||
@app.route('/docs')
|
||||
def api_docs():
|
||||
"""Serve API documentation."""
|
||||
try:
|
||||
with open('API_DOCS.md', 'r') as f:
|
||||
docs_content = f.read()
|
||||
|
||||
# Convert markdown to HTML for better display
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Memory Module Detection API - Documentation</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; line-height: 1.6; }}
|
||||
pre {{ background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }}
|
||||
code {{ background: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
|
||||
h1, h2, h3 {{ color: #333; }}
|
||||
.nav {{ background: #e8f5e8; padding: 10px; margin-bottom: 20px; border-radius: 5px; }}
|
||||
.nav a {{ margin-right: 15px; text-decoration: none; color: #0066cc; }}
|
||||
.nav a:hover {{ text-decoration: underline; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nav">
|
||||
<a href="/">🏠 Web Interface</a>
|
||||
<a href="/api">📊 API Info</a>
|
||||
<a href="/health">💚 Health Check</a>
|
||||
</div>
|
||||
<pre>{docs_content}</pre>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html_content
|
||||
except FileNotFoundError:
|
||||
return jsonify({
|
||||
'error': 'API documentation file not found',
|
||||
'message': 'Please ensure API_DOCS.md exists in the project directory'
|
||||
}), 404
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'model_loaded': detector.model is not None,
|
||||
'model_path': MODEL_PATH
|
||||
})
|
||||
|
||||
@app.route('/detect', methods=['POST'])
|
||||
def detect_memory_modules():
|
||||
"""
|
||||
Detect memory modules in uploaded image.
|
||||
|
||||
Expected input:
|
||||
- File upload with key 'image'
|
||||
- Optional: confidence threshold as form data
|
||||
|
||||
Returns:
|
||||
- JSON with detections and annotated image (base64)
|
||||
"""
|
||||
try:
|
||||
# Check if model is loaded
|
||||
if detector.model is None:
|
||||
return jsonify({
|
||||
'error': 'Model not loaded. Please train the model first.',
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
# Check if file is present
|
||||
if 'image' not in request.files:
|
||||
return jsonify({
|
||||
'error': 'No image file provided',
|
||||
'success': False
|
||||
}), 400
|
||||
|
||||
file = request.files['image']
|
||||
|
||||
# Check if file is selected
|
||||
if file.filename == '':
|
||||
return jsonify({
|
||||
'error': 'No file selected',
|
||||
'success': False
|
||||
}), 400
|
||||
|
||||
# Check file extension
|
||||
if not allowed_file(file.filename):
|
||||
return jsonify({
|
||||
'error': f'File type not allowed. Supported formats: {ALLOWED_EXTENSIONS}',
|
||||
'success': False
|
||||
}), 400
|
||||
|
||||
# Get confidence threshold from form data (default 80%)
|
||||
conf_threshold = float(request.form.get('confidence', 0.8))
|
||||
|
||||
# Save uploaded file temporarily
|
||||
filename = secure_filename(file.filename)
|
||||
temp_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
file.save(temp_path)
|
||||
|
||||
try:
|
||||
# Run detection
|
||||
detections, annotated_image = detector.detect(
|
||||
temp_path,
|
||||
conf_threshold=conf_threshold
|
||||
)
|
||||
|
||||
# Convert annotated image to base64
|
||||
annotated_base64 = image_to_base64(annotated_image)
|
||||
|
||||
# Prepare response
|
||||
response_data = {
|
||||
'success': True,
|
||||
'detections': detections,
|
||||
'num_detections': len(detections),
|
||||
'annotated_image': annotated_base64,
|
||||
'confidence_threshold': conf_threshold,
|
||||
'original_filename': filename
|
||||
}
|
||||
|
||||
logger.info(f"Processed {filename}: found {len(detections)} memory modules")
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing image: {str(e)}")
|
||||
return jsonify({
|
||||
'error': f'Error processing image: {str(e)}',
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
@app.route('/detect/hardcoded', methods=['GET'])
|
||||
def detect_hardcoded_image():
|
||||
"""
|
||||
Process hardcoded test image for memory module detection.
|
||||
|
||||
Optional query parameters:
|
||||
- confidence: confidence threshold (default: 0.8)
|
||||
|
||||
Returns:
|
||||
- JSON with detections and annotated image (base64)
|
||||
"""
|
||||
try:
|
||||
# Check if model is loaded
|
||||
if detector.model is None:
|
||||
return jsonify({
|
||||
'error': 'Model not loaded. Please train the model first.',
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
# Check if hardcoded image exists
|
||||
if not os.path.exists(HARDCODED_IMAGE_PATH):
|
||||
return jsonify({
|
||||
'error': f'Hardcoded test image not found at {HARDCODED_IMAGE_PATH}',
|
||||
'success': False
|
||||
}), 404
|
||||
|
||||
# Get confidence threshold from query parameters (default 80%)
|
||||
conf_threshold = float(request.args.get('confidence', 0.8))
|
||||
|
||||
# Run detection
|
||||
detections, annotated_image = detector.detect(
|
||||
HARDCODED_IMAGE_PATH,
|
||||
conf_threshold=conf_threshold
|
||||
)
|
||||
|
||||
# Convert annotated image to base64
|
||||
annotated_base64 = image_to_base64(annotated_image)
|
||||
|
||||
# Prepare response
|
||||
response_data = {
|
||||
'success': True,
|
||||
'detections': detections,
|
||||
'num_detections': len(detections),
|
||||
'annotated_image': annotated_base64,
|
||||
'confidence_threshold': conf_threshold,
|
||||
'test_image_path': HARDCODED_IMAGE_PATH
|
||||
}
|
||||
|
||||
logger.info(f"Processed hardcoded image: found {len(detections)} memory modules")
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing hardcoded image: {str(e)}")
|
||||
return jsonify({
|
||||
'error': f'Error processing hardcoded image: {str(e)}',
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
|
||||
|
||||
@app.route('/detect/base64', methods=['POST'])
|
||||
def detect_base64_image():
|
||||
"""
|
||||
Detect memory modules in base64 encoded image.
|
||||
|
||||
Expected JSON input:
|
||||
{
|
||||
"image": "base64_encoded_image_string",
|
||||
"confidence": 0.5 // optional
|
||||
}
|
||||
|
||||
Returns:
|
||||
- JSON with detections and annotated image (base64)
|
||||
"""
|
||||
try:
|
||||
# Check if model is loaded
|
||||
if detector.model is None:
|
||||
return jsonify({
|
||||
'error': 'Model not loaded. Please train the model first.',
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
# Get JSON data
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'image' not in data:
|
||||
return jsonify({
|
||||
'error': 'No base64 image data provided',
|
||||
'success': False
|
||||
}), 400
|
||||
|
||||
# Get confidence threshold (default 80%)
|
||||
conf_threshold = float(data.get('confidence', 0.8))
|
||||
|
||||
# Decode base64 image
|
||||
try:
|
||||
image = base64_to_image(data['image'])
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': f'Invalid base64 image data: {str(e)}',
|
||||
'success': False
|
||||
}), 400
|
||||
|
||||
# Run detection
|
||||
detections, annotated_image = detector.detect_from_array(
|
||||
np.array(image),
|
||||
conf_threshold=conf_threshold
|
||||
)
|
||||
|
||||
# Convert annotated image to base64
|
||||
annotated_base64 = image_to_base64(annotated_image)
|
||||
|
||||
# Prepare response
|
||||
response_data = {
|
||||
'success': True,
|
||||
'detections': detections,
|
||||
'num_detections': len(detections),
|
||||
'annotated_image': annotated_base64,
|
||||
'confidence_threshold': conf_threshold
|
||||
}
|
||||
|
||||
logger.info(f"Processed base64 image: found {len(detections)} memory modules")
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing base64 image: {str(e)}")
|
||||
return jsonify({
|
||||
'error': f'Error processing base64 image: {str(e)}',
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
@app.errorhandler(413)
|
||||
def too_large(e):
|
||||
"""Handle file too large error."""
|
||||
return jsonify({
|
||||
'error': 'File too large. Maximum size is 16MB.',
|
||||
'success': False
|
||||
}), 413
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
"""Handle 404 errors."""
|
||||
return jsonify({
|
||||
'error': 'Endpoint not found',
|
||||
'success': False
|
||||
}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(e):
|
||||
"""Handle internal server errors."""
|
||||
return jsonify({
|
||||
'error': 'Internal server error',
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Check if model exists
|
||||
if not os.path.exists(MODEL_PATH):
|
||||
print(f"Warning: Model not found at {MODEL_PATH}")
|
||||
print("Please train the model first using: python3 train.py")
|
||||
print("The API will still start but detection endpoints will return errors.")
|
||||
|
||||
# Start the Flask app
|
||||
print("Starting Memory Module Detection API...")
|
||||
print(f"Model path: {MODEL_PATH}")
|
||||
print(f"Model loaded: {detector.model is not None}")
|
||||
print(f"Hardcoded test image: {HARDCODED_IMAGE_PATH}")
|
||||
|
||||
app.run(host='0.0.0.0', port=5002, debug=True)
|
||||
@@ -1,30 +1,5 @@
|
||||
# Core ML and Computer Vision
|
||||
ultralytics==8.0.196
|
||||
torch>=1.9.0
|
||||
torchvision>=0.10.0
|
||||
opencv-python==4.8.1.78
|
||||
Pillow==10.0.1
|
||||
|
||||
# Web Framework
|
||||
Flask==2.3.3
|
||||
Flask-CORS==4.0.0
|
||||
Flask-RESTX==1.3.0
|
||||
Werkzeug==2.3.7
|
||||
|
||||
# Data Processing
|
||||
numpy==1.24.3
|
||||
pandas==2.0.3
|
||||
|
||||
# Image Processing and Visualization
|
||||
matplotlib==3.7.2
|
||||
seaborn==0.12.2
|
||||
|
||||
# Utilities
|
||||
PyYAML==6.0.1
|
||||
requests==2.31.0
|
||||
tqdm==4.66.1
|
||||
pathlib2>=2.3.0;python_version<"3.4"
|
||||
|
||||
# Optional GPU support (uncomment if using CUDA)
|
||||
# torch==2.0.1+cu118 --index-url https://download.pytorch.org/whl/cu118
|
||||
# torchvision==0.15.2+cu118 --index-url https://download.pytorch.org/whl/cu118
|
||||
flask
|
||||
ultralytics
|
||||
pillow
|
||||
numpy
|
||||
python-multipart
|
||||
@@ -0,0 +1,6 @@
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Setup script for Memory Module Detection Project
|
||||
This script helps users set up the project quickly.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
def run_command(command, description):
|
||||
"""Run a command and handle errors."""
|
||||
print(f"🔄 {description}...")
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||
print(f"✅ {description} completed successfully")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ {description} failed:")
|
||||
print(f" Command: {command}")
|
||||
print(f" Error: {e.stderr}")
|
||||
return False
|
||||
|
||||
def check_python_version():
|
||||
"""Check if Python version is compatible."""
|
||||
print("🐍 Checking Python version...")
|
||||
version = sys.version_info
|
||||
if version.major == 3 and version.minor >= 8:
|
||||
print(f"✅ Python {version.major}.{version.minor}.{version.micro} is compatible")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Python {version.major}.{version.minor}.{version.micro} is not compatible")
|
||||
print(" Please use Python 3.8 or higher")
|
||||
return False
|
||||
|
||||
def check_files():
|
||||
"""Check if required files exist."""
|
||||
print("📁 Checking project files...")
|
||||
required_files = [
|
||||
'requirements.txt',
|
||||
'main.py',
|
||||
'train.py',
|
||||
'inference_utils.py',
|
||||
'prepare_dataset.py',
|
||||
'dataset.yaml',
|
||||
'training/memory',
|
||||
'training/no_memory'
|
||||
]
|
||||
|
||||
missing_files = []
|
||||
for file in required_files:
|
||||
if not os.path.exists(file):
|
||||
missing_files.append(file)
|
||||
|
||||
if missing_files:
|
||||
print(f"❌ Missing files: {missing_files}")
|
||||
return False
|
||||
else:
|
||||
print("✅ All required files found")
|
||||
return True
|
||||
|
||||
def install_dependencies():
|
||||
"""Install Python dependencies."""
|
||||
if not run_command("pip install -r requirements.txt", "Installing dependencies"):
|
||||
print(" Try using: pip3 install -r requirements.txt")
|
||||
return run_command("pip3 install -r requirements.txt", "Installing dependencies with pip3")
|
||||
return True
|
||||
|
||||
def prepare_dataset():
|
||||
"""Prepare the dataset structure."""
|
||||
if os.path.exists('training/train/images') and os.path.exists('training/val/images'):
|
||||
print("✅ Dataset already prepared")
|
||||
return True
|
||||
|
||||
return run_command("python3 prepare_dataset.py", "Preparing dataset structure")
|
||||
|
||||
def train_model():
|
||||
"""Train the YOLOv8 model."""
|
||||
model_path = 'runs/detect/memory_module_detection/weights/best.pt'
|
||||
if os.path.exists(model_path):
|
||||
print("✅ Model already trained")
|
||||
return True
|
||||
|
||||
print("🤖 Training YOLOv8 model...")
|
||||
print(" This may take 5-60 minutes depending on your hardware...")
|
||||
return run_command("python3 train.py --epochs 50 --batch 8", "Training YOLOv8 model")
|
||||
|
||||
def test_setup():
|
||||
"""Test the setup by running a quick inference."""
|
||||
print("🧪 Testing setup...")
|
||||
return run_command("python3 test_api.py", "Running API tests")
|
||||
|
||||
def main():
|
||||
"""Main setup function."""
|
||||
print("🚀 Memory Module Detection Project Setup")
|
||||
print("=" * 50)
|
||||
|
||||
# Check prerequisites
|
||||
if not check_python_version():
|
||||
return False
|
||||
|
||||
if not check_files():
|
||||
return False
|
||||
|
||||
# Setup steps
|
||||
steps = [
|
||||
("Install Dependencies", install_dependencies),
|
||||
("Prepare Dataset", prepare_dataset),
|
||||
("Train Model", train_model)
|
||||
]
|
||||
|
||||
for step_name, step_func in steps:
|
||||
print(f"\n📋 Step: {step_name}")
|
||||
if not step_func():
|
||||
print(f"❌ Setup failed at step: {step_name}")
|
||||
return False
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("🎉 Setup completed successfully!")
|
||||
print("\n📖 Next steps:")
|
||||
print("1. Start the API:")
|
||||
print(" python3 main.py")
|
||||
print("\n2. Test the API (in another terminal):")
|
||||
print(" python3 test_api.py")
|
||||
print("\n3. Or test manually:")
|
||||
print(" curl http://localhost:5000/detect/hardcoded")
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -1,505 +0,0 @@
|
||||
// Memory Module Detection QA Interface JavaScript
|
||||
|
||||
const API_BASE_URL = 'http://localhost:5002';
|
||||
let uploadedFile = null;
|
||||
let lastDetectionResult = null;
|
||||
|
||||
// Initialize the application
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeApp();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function initializeApp() {
|
||||
checkApiStatus();
|
||||
loadApiInfo();
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// File input change
|
||||
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
|
||||
|
||||
// Drag and drop
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
uploadArea.addEventListener('dragover', handleDragOver);
|
||||
uploadArea.addEventListener('dragleave', handleDragLeave);
|
||||
uploadArea.addEventListener('drop', handleDrop);
|
||||
|
||||
// Click to upload (only on the upload area, not buttons inside it)
|
||||
uploadArea.addEventListener('click', function(event) {
|
||||
// Only trigger file input if clicking on the upload area itself, not buttons
|
||||
if (event.target === uploadArea || (event.target.closest('.upload-content') && !event.target.closest('button'))) {
|
||||
console.log('Upload area clicked, triggering file input');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkApiStatus() {
|
||||
const statusElement = document.getElementById('apiStatus');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/health`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
statusElement.className = 'status-indicator online';
|
||||
statusElement.innerHTML = '<i class="fas fa-circle"></i> <span>API Online</span>';
|
||||
} else {
|
||||
throw new Error('API not responding');
|
||||
}
|
||||
} catch (error) {
|
||||
statusElement.className = 'status-indicator offline';
|
||||
statusElement.innerHTML = '<i class="fas fa-circle"></i> <span>API Offline</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApiInfo() {
|
||||
const apiInfoElement = document.getElementById('apiInfo');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
displayApiInfo(data);
|
||||
} else {
|
||||
throw new Error('Failed to load API info');
|
||||
}
|
||||
} catch (error) {
|
||||
apiInfoElement.innerHTML = '<div class="error">Failed to load API information</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayApiInfo(data) {
|
||||
const apiInfoElement = document.getElementById('apiInfo');
|
||||
apiInfoElement.innerHTML = `
|
||||
<div class="info-item">
|
||||
<h3>API Status</h3>
|
||||
<p>${data.message}</p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h3>Version</h3>
|
||||
<p>${data.version}</p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h3>Model Status</h3>
|
||||
<p>${data.model_loaded ? 'Loaded ✅' : 'Not Loaded ❌'}</p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h3>Supported Formats</h3>
|
||||
<p>${data.supported_formats.join(', ')}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function showUploadSection() {
|
||||
document.getElementById('uploadSection').style.display = 'block';
|
||||
document.getElementById('uploadSection').scrollIntoView({ behavior: 'smooth' });
|
||||
|
||||
// Ensure the upload area is properly initialized
|
||||
initializeUploadArea();
|
||||
}
|
||||
|
||||
function initializeUploadArea() {
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
let fileInput = document.getElementById('fileInput');
|
||||
|
||||
// Completely recreate the file input element
|
||||
if (fileInput) {
|
||||
fileInput.remove();
|
||||
}
|
||||
|
||||
// Create a brand new file input
|
||||
const newFileInput = document.createElement('input');
|
||||
newFileInput.type = 'file';
|
||||
newFileInput.id = 'fileInput';
|
||||
newFileInput.accept = 'image/*';
|
||||
newFileInput.style.display = 'none';
|
||||
newFileInput.multiple = false;
|
||||
|
||||
// Insert the new file input into the DOM
|
||||
uploadArea.parentNode.insertBefore(newFileInput, uploadArea);
|
||||
|
||||
// Clear any existing event listeners on upload area by cloning
|
||||
const newUploadArea = uploadArea.cloneNode(true);
|
||||
uploadArea.parentNode.replaceChild(newUploadArea, uploadArea);
|
||||
|
||||
// Re-attach all event listeners to the new elements
|
||||
newFileInput.addEventListener('change', handleFileSelect);
|
||||
|
||||
newUploadArea.addEventListener('dragover', handleDragOver);
|
||||
newUploadArea.addEventListener('dragleave', handleDragLeave);
|
||||
newUploadArea.addEventListener('drop', handleDrop);
|
||||
|
||||
newUploadArea.addEventListener('click', function(event) {
|
||||
if (event.target === newUploadArea || (event.target.closest('.upload-content') && !event.target.closest('button'))) {
|
||||
console.log('Upload area clicked, triggering file input');
|
||||
const currentFileInput = document.getElementById('fileInput');
|
||||
if (currentFileInput) {
|
||||
currentFileInput.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Upload area completely reinitialized with fresh file input');
|
||||
}
|
||||
|
||||
function handleFileSelect(event) {
|
||||
console.log('File select event triggered');
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
console.log('File selected:', file.name);
|
||||
handleFile(file);
|
||||
} else {
|
||||
console.log('No file selected');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.classList.add('dragover');
|
||||
}
|
||||
|
||||
function handleDragLeave(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.classList.remove('dragover');
|
||||
}
|
||||
|
||||
function handleDrop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.classList.remove('dragover');
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleFile(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
uploadedFile = file;
|
||||
|
||||
// Hide test results when new image is uploaded
|
||||
const testResultsSection = document.getElementById('testResultsSection');
|
||||
if (testResultsSection) {
|
||||
testResultsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show file info with change file option
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
uploadArea.innerHTML = `
|
||||
<div class="upload-content">
|
||||
<i class="fas fa-check-circle upload-icon" style="color: #28a745;"></i>
|
||||
<p><strong>File selected:</strong> ${file.name}</p>
|
||||
<p class="upload-hint">Size: ${(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
||||
<button class="btn btn-outline" onclick="resetFileUpload()" style="margin-top: 10px;">
|
||||
<i class="fas fa-sync-alt"></i> Change File
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show controls
|
||||
document.getElementById('uploadControls').style.display = 'block';
|
||||
}
|
||||
|
||||
function resetFileUpload() {
|
||||
uploadedFile = null;
|
||||
lastDetectionResult = null; // Reset last detection result
|
||||
|
||||
// Reset upload area HTML
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
uploadArea.innerHTML = `
|
||||
<div class="upload-content">
|
||||
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
||||
<p>Drag and drop an image here or click to select</p>
|
||||
<p class="upload-hint">Supported formats: PNG, JPG, JPEG, GIF, BMP</p>
|
||||
<button class="btn btn-outline" onclick="document.getElementById('fileInput').click()">
|
||||
Select Image
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Hide controls
|
||||
const uploadControls = document.getElementById('uploadControls');
|
||||
uploadControls.style.display = 'none';
|
||||
|
||||
// Remove the "Upload Another" button if it exists
|
||||
const uploadAnotherBtn = uploadControls.querySelector('.upload-another');
|
||||
if (uploadAnotherBtn) {
|
||||
uploadAnotherBtn.remove();
|
||||
}
|
||||
|
||||
// Hide results if showing
|
||||
document.getElementById('resultsSection').style.display = 'none';
|
||||
|
||||
// Hide test results when file is reset
|
||||
const testResultsSection = document.getElementById('testResultsSection');
|
||||
if (testResultsSection) {
|
||||
testResultsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Reinitialize the upload area with fresh event listeners
|
||||
initializeUploadArea();
|
||||
|
||||
console.log('File upload reset completed');
|
||||
}
|
||||
|
||||
async function processUploadedImage() {
|
||||
if (!uploadedFile) {
|
||||
alert('Please select an image first');
|
||||
return;
|
||||
}
|
||||
|
||||
const confidence = 0.8; // Fixed 80% threshold
|
||||
showLoading('Processing uploaded image...');
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', uploadedFile);
|
||||
formData.append('confidence', confidence);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/detect`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
hideLoading();
|
||||
|
||||
if (result.success) {
|
||||
// Store the last detection result for Run All Tests
|
||||
lastDetectionResult = result;
|
||||
displayResults(result, 'Uploaded Image Detection');
|
||||
// Add option to upload another file
|
||||
addUploadAnotherOption();
|
||||
} else {
|
||||
alert(`Detection failed: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
alert(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function addUploadAnotherOption() {
|
||||
const uploadControls = document.getElementById('uploadControls');
|
||||
if (!uploadControls.querySelector('.upload-another')) {
|
||||
const uploadAnotherBtn = document.createElement('button');
|
||||
uploadAnotherBtn.className = 'btn btn-secondary upload-another';
|
||||
uploadAnotherBtn.style.marginLeft = '10px';
|
||||
uploadAnotherBtn.innerHTML = '<i class="fas fa-plus"></i> Upload Another Image';
|
||||
uploadAnotherBtn.onclick = resetFileUpload;
|
||||
uploadControls.appendChild(uploadAnotherBtn);
|
||||
}
|
||||
}
|
||||
|
||||
async function testHardcodedImage() {
|
||||
showLoading('Testing hardcoded image...');
|
||||
|
||||
try {
|
||||
console.log(`Making request to: ${API_BASE_URL}/detect/hardcoded?confidence=0.8`);
|
||||
const response = await fetch(`${API_BASE_URL}/detect/hardcoded?confidence=0.8`);
|
||||
console.log('Response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Response data:', result);
|
||||
hideLoading();
|
||||
|
||||
if (result.success) {
|
||||
// Store the last detection result for Run All Tests
|
||||
lastDetectionResult = result;
|
||||
displayResults(result, 'Hardcoded Image Test');
|
||||
} else {
|
||||
alert(`Test failed: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('Hardcoded test error:', error);
|
||||
alert(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function displayResults(result, title) {
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
const resultsContent = document.getElementById('resultsContent');
|
||||
|
||||
let detectionsHtml = '';
|
||||
if (result.detections && result.detections.length > 0) {
|
||||
detectionsHtml = result.detections.map((detection, index) => `
|
||||
<div class="detection-item">
|
||||
<span>Detection ${index + 1}: ${detection.class_name}</span>
|
||||
<span class="detection-confidence">${(detection.confidence * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
detectionsHtml = '<div class="detection-item">No memory modules detected</div>';
|
||||
}
|
||||
|
||||
resultsContent.innerHTML = `
|
||||
<div class="result-item">
|
||||
<div class="result-header">
|
||||
<h3>${title}</h3>
|
||||
<span class="badge">${new Date().toLocaleTimeString()}</span>
|
||||
</div>
|
||||
|
||||
<div class="result-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">${result.num_detections}</div>
|
||||
<div class="stat-label">Detections</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">${(result.confidence_threshold * 100).toFixed(0)}%</div>
|
||||
<div class="stat-label">Confidence</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">${result.success ? 'Success' : 'Failed'}</div>
|
||||
<div class="stat-label">Status</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detection-list">
|
||||
<h4>Detected Memory Modules:</h4>
|
||||
${detectionsHtml}
|
||||
</div>
|
||||
|
||||
${result.annotated_image ? `
|
||||
<div class="image-container">
|
||||
<h4>Annotated Image:</h4>
|
||||
<img src="data:image/png;base64,${result.annotated_image}"
|
||||
alt="Annotated Result" class="result-image">
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultsSection.style.display = 'block';
|
||||
resultsSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
showLoading('Running comprehensive tests...');
|
||||
const testResults = [];
|
||||
|
||||
// Test 1: API Health
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/health`);
|
||||
const result = await response.json();
|
||||
testResults.push({
|
||||
name: 'API Health Check',
|
||||
success: response.ok && result.status === 'healthy',
|
||||
message: response.ok ? 'API is healthy' : 'API health check failed'
|
||||
});
|
||||
} catch (error) {
|
||||
testResults.push({
|
||||
name: 'API Health Check',
|
||||
success: false,
|
||||
message: `Error: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
// Test 2: Image with Memory Modules (use last detection result if available)
|
||||
if (lastDetectionResult) {
|
||||
// Use the last detection result from uploaded/tested image
|
||||
testResults.push({
|
||||
name: 'Image with Memory Modules',
|
||||
success: lastDetectionResult.success,
|
||||
message: lastDetectionResult.success ?
|
||||
(lastDetectionResult.num_detections > 0 ?
|
||||
`✅ Found ${lastDetectionResult.num_detections} memory modules` :
|
||||
`❌ No memory modules`) :
|
||||
`❌ Error: ${lastDetectionResult.error}`
|
||||
});
|
||||
} else {
|
||||
// Fallback to hardcoded test if no previous detection
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/detect/hardcoded`);
|
||||
const result = await response.json();
|
||||
lastDetectionResult = result; // Store for future use
|
||||
testResults.push({
|
||||
name: 'Image with Memory Modules',
|
||||
success: result.success,
|
||||
message: result.success ?
|
||||
(result.num_detections > 0 ?
|
||||
`✅ Found ${result.num_detections} memory modules` :
|
||||
`❌ No memory modules`) :
|
||||
`❌ Error: ${result.error}`
|
||||
});
|
||||
} catch (error) {
|
||||
testResults.push({
|
||||
name: 'Image with Memory Modules',
|
||||
success: false,
|
||||
message: `❌ Error: ${error.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: API Information
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api`);
|
||||
const result = await response.json();
|
||||
testResults.push({
|
||||
name: 'API Information',
|
||||
success: response.ok && result.message,
|
||||
message: response.ok ? 'API info loaded successfully' : 'Failed to load API info'
|
||||
});
|
||||
} catch (error) {
|
||||
testResults.push({
|
||||
name: 'API Information',
|
||||
success: false,
|
||||
message: `Error: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
displayTestResults(testResults);
|
||||
}
|
||||
|
||||
function displayTestResults(testResults) {
|
||||
const testResultsSection = document.getElementById('testResultsSection');
|
||||
const testResultsContent = document.getElementById('testResults');
|
||||
|
||||
const successCount = testResults.filter(test => test.success).length;
|
||||
const totalTests = testResults.length;
|
||||
|
||||
const testsHtml = testResults.map(test => `
|
||||
<div class="test-item ${test.success ? 'success' : 'error'}">
|
||||
<h3>
|
||||
<i class="fas ${test.success ? 'fa-check-circle' : 'fa-times-circle'}"></i>
|
||||
${test.name}
|
||||
</h3>
|
||||
<p>${test.message}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
testResultsContent.innerHTML = `
|
||||
<div class="test-summary">
|
||||
<h3>Test Summary: ${successCount}/${totalTests} tests passed</h3>
|
||||
</div>
|
||||
${testsHtml}
|
||||
`;
|
||||
|
||||
testResultsSection.style.display = 'block';
|
||||
testResultsSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function showLoading(message) {
|
||||
document.getElementById('loadingText').textContent = message;
|
||||
document.getElementById('loadingOverlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loadingOverlay').style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
/* Memory Module Detection QA Interface Styles */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel h2 i {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.api-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.info-item h3 {
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-item p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.test-options {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #667eea;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 3px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-area:hover,
|
||||
.upload-area.dragover {
|
||||
border-color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 3rem;
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.upload-controls {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.confidence-info {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: #e8f5e8;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.confidence-info p {
|
||||
margin: 0;
|
||||
color: #155724;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.results-content {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.detection-list {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.detection-item {
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detection-confidence {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.test-item {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
|
||||
.test-item.success {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
|
||||
.test-item.error {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
|
||||
.test-item h3 {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-content i {
|
||||
font-size: 2rem;
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-message {
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.summary-message.no-memory {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.summary-message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: auto;
|
||||
padding: 20px 0;
|
||||
color: rgba(255,255,255,0.8);
|
||||
background: rgba(0,0,0,0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.test-options {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.api-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Memory Module Detection - QA Testing Interface</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><i class="fas fa-microchip"></i> Memory Module Detection</h1>
|
||||
<p>AI-Powered Motherboard Memory Module Detection</p>
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- API Information Panel -->
|
||||
<section class="panel" id="apiInfoPanel">
|
||||
<h2><i class="fas fa-info-circle"></i> API Information</h2>
|
||||
<div class="api-info" id="apiInfo">
|
||||
<div class="loading">Loading API information...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Test Options -->
|
||||
<section class="panel">
|
||||
<h2><i class="fas fa-vial"></i> Test Options</h2>
|
||||
<div class="test-options">
|
||||
<button class="btn btn-primary" onclick="testHardcodedImage()">
|
||||
<i class="fas fa-image"></i> Test Hardcoded Image
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="showUploadSection()">
|
||||
<i class="fas fa-upload"></i> Upload Custom Image
|
||||
</button>
|
||||
|
||||
<button class="btn btn-info" onclick="runAllTests()">
|
||||
<i class="fas fa-play"></i> Run All Tests
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Image Upload Section -->
|
||||
<section class="panel" id="uploadSection" style="display: none;">
|
||||
<h2><i class="fas fa-cloud-upload-alt"></i> Upload Image</h2>
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<div class="upload-content">
|
||||
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
||||
<p>Drag and drop an image here or click to select</p>
|
||||
<p class="upload-hint">Supported formats: PNG, JPG, JPEG, GIF, BMP</p>
|
||||
<input type="file" id="fileInput" accept="image/*" style="display: none;" multiple="false">
|
||||
<button class="btn btn-outline" onclick="document.getElementById('fileInput').click()">
|
||||
Select Image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-controls" style="display: none;" id="uploadControls">
|
||||
<div class="confidence-info">
|
||||
<p><strong>Confidence Threshold:</strong> 80% (High Precision Mode)</p>
|
||||
</div>
|
||||
<button class="btn btn-success" onclick="processUploadedImage()">
|
||||
<i class="fas fa-search"></i> Detect Memory Modules
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Results Section -->
|
||||
<section class="panel" id="resultsSection" style="display: none;">
|
||||
<h2><i class="fas fa-chart-bar"></i> Detection Results</h2>
|
||||
<div class="results-content" id="resultsContent">
|
||||
<!-- Results will be populated here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Test Results Section -->
|
||||
<section class="panel" id="testResultsSection" style="display: none;">
|
||||
<h2><i class="fas fa-clipboard-check"></i> Test Results</h2>
|
||||
<div class="test-results" id="testResults">
|
||||
<!-- Test results will be populated here -->
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2025 Memory Module Detection Project</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
|
||||
<div class="loading-content">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<p id="loadingText">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,257 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Memory Module Detection API
|
||||
This script tests all API endpoints and provides usage examples.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
# API base URL
|
||||
BASE_URL = "http://localhost:5002"
|
||||
|
||||
def test_api_info():
|
||||
"""Test the API info endpoint."""
|
||||
print("🔍 Testing API Info...")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ API Info: {data['message']}")
|
||||
print(f" Model loaded: {data['model_loaded']}")
|
||||
print(f" Supported formats: {data['supported_formats']}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ API Info failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ API Info error: {e}")
|
||||
return False
|
||||
|
||||
def test_health_check():
|
||||
"""Test the health check endpoint."""
|
||||
print("\n🏥 Testing Health Check...")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/health")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ Health: {data['status']}")
|
||||
print(f" Model loaded: {data['model_loaded']}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Health check failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Health check error: {e}")
|
||||
return False
|
||||
|
||||
def test_hardcoded_detection():
|
||||
"""Test detection with hardcoded image."""
|
||||
print("\n🖼️ Testing Hardcoded Image Detection...")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/detect/hardcoded?confidence=0.8")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data['success']:
|
||||
print(f"✅ Hardcoded detection successful!")
|
||||
print(f" Found {data['num_detections']} memory modules")
|
||||
for i, detection in enumerate(data['detections']):
|
||||
print(f" Detection {i+1}: {detection['class_name']} "
|
||||
f"(confidence: {detection['confidence']:.3f})")
|
||||
|
||||
# Save annotated image
|
||||
if 'annotated_image' in data:
|
||||
save_base64_image(data['annotated_image'], 'test_hardcoded_result.png')
|
||||
print(" Annotated image saved as: test_hardcoded_result.png")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Hardcoded detection failed: {data.get('error', 'Unknown error')}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ Hardcoded detection failed: {response.status_code}")
|
||||
if response.text:
|
||||
print(f" Response: {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Hardcoded detection error: {e}")
|
||||
return False
|
||||
|
||||
def test_file_upload():
|
||||
"""Test detection with file upload."""
|
||||
print("\n📤 Testing File Upload Detection...")
|
||||
|
||||
# Find a test image
|
||||
test_image_path = None
|
||||
possible_paths = [
|
||||
'training/memory/out1.png',
|
||||
'training/memory/out2.png',
|
||||
'training/val/images/memory_out8.png'
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
test_image_path = path
|
||||
break
|
||||
|
||||
if not test_image_path:
|
||||
print("❌ No test image found. Skipping file upload test.")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(test_image_path, 'rb') as f:
|
||||
files = {'image': f}
|
||||
data = {'confidence': '0.8'}
|
||||
response = requests.post(f"{BASE_URL}/detect", files=files, data=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result['success']:
|
||||
print(f"✅ File upload detection successful!")
|
||||
print(f" Test image: {test_image_path}")
|
||||
print(f" Found {result['num_detections']} memory modules")
|
||||
for i, detection in enumerate(result['detections']):
|
||||
print(f" Detection {i+1}: {detection['class_name']} "
|
||||
f"(confidence: {detection['confidence']:.3f})")
|
||||
|
||||
# Save annotated image
|
||||
if 'annotated_image' in result:
|
||||
save_base64_image(result['annotated_image'], 'test_upload_result.png')
|
||||
print(" Annotated image saved as: test_upload_result.png")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"❌ File upload detection failed: {result.get('error', 'Unknown error')}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ File upload detection failed: {response.status_code}")
|
||||
if response.text:
|
||||
print(f" Response: {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ File upload detection error: {e}")
|
||||
return False
|
||||
|
||||
def test_base64_detection():
|
||||
"""Test detection with base64 encoded image."""
|
||||
print("\n🔢 Testing Base64 Detection...")
|
||||
|
||||
# Find a test image
|
||||
test_image_path = None
|
||||
possible_paths = [
|
||||
'training/memory/out1.png',
|
||||
'training/memory/out2.png'
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
test_image_path = path
|
||||
break
|
||||
|
||||
if not test_image_path:
|
||||
print("❌ No test image found. Skipping base64 test.")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Convert image to base64
|
||||
with open(test_image_path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
base64_string = base64.b64encode(image_data).decode('utf-8')
|
||||
|
||||
# Send request
|
||||
payload = {
|
||||
'image': base64_string,
|
||||
'confidence': 0.8
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/detect/base64",
|
||||
json=payload,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result['success']:
|
||||
print(f"✅ Base64 detection successful!")
|
||||
print(f" Test image: {test_image_path}")
|
||||
print(f" Found {result['num_detections']} memory modules")
|
||||
for i, detection in enumerate(result['detections']):
|
||||
print(f" Detection {i+1}: {detection['class_name']} "
|
||||
f"(confidence: {detection['confidence']:.3f})")
|
||||
|
||||
# Save annotated image
|
||||
if 'annotated_image' in result:
|
||||
save_base64_image(result['annotated_image'], 'test_base64_result.png')
|
||||
print(" Annotated image saved as: test_base64_result.png")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Base64 detection failed: {result.get('error', 'Unknown error')}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ Base64 detection failed: {response.status_code}")
|
||||
if response.text:
|
||||
print(f" Response: {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Base64 detection error: {e}")
|
||||
return False
|
||||
|
||||
def save_base64_image(base64_string, filename):
|
||||
"""Save base64 encoded image to file."""
|
||||
try:
|
||||
image_data = base64.b64decode(base64_string)
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
image.save(filename)
|
||||
except Exception as e:
|
||||
print(f" Warning: Could not save image {filename}: {e}")
|
||||
|
||||
def main():
|
||||
"""Run all API tests."""
|
||||
print("🧪 Memory Module Detection API Test Suite")
|
||||
print("=" * 50)
|
||||
|
||||
# Check if API is running
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/health", timeout=5)
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ API is not running!")
|
||||
print(" Please start the API first: python3 main.py")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"❌ Cannot connect to API: {e}")
|
||||
return
|
||||
|
||||
# Run tests
|
||||
tests = [
|
||||
test_api_info,
|
||||
test_health_check,
|
||||
test_hardcoded_detection,
|
||||
test_file_upload,
|
||||
test_base64_detection
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test in tests:
|
||||
if test():
|
||||
passed += 1
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"🏁 Test Results: {passed}/{total} tests passed")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 All tests passed! The API is working correctly.")
|
||||
else:
|
||||
print("⚠️ Some tests failed. Check the output above for details.")
|
||||
if passed == 0:
|
||||
print(" Make sure the model is trained: python3 train.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,182 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import matplotlib.pyplot as plt
|
||||
from app.utils.detector import MemoryDetector
|
||||
import os
|
||||
import json
|
||||
from typing import List, Dict, Tuple
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TestMemoryDetector:
|
||||
@pytest.fixture(scope="class")
|
||||
def results_dir(self):
|
||||
"""Create and return results directory"""
|
||||
dir_path = Path("test_results")
|
||||
dir_path.mkdir(exist_ok=True)
|
||||
logger.info(f"Created results directory: {dir_path}")
|
||||
return dir_path
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def detector(self):
|
||||
"""Initialize detector once for all tests"""
|
||||
logger.info("Initializing MemoryDetector...")
|
||||
return MemoryDetector()
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def test_images(self):
|
||||
"""Load test images from validation directory"""
|
||||
val_dir = Path('training/val/images')
|
||||
assert val_dir.exists(), f"Validation directory not found: {val_dir}"
|
||||
|
||||
logger.info(f"Loading test images from {val_dir}")
|
||||
images = []
|
||||
for img_path in val_dir.glob('memory_*.png'):
|
||||
images.append({
|
||||
'path': str(img_path),
|
||||
'image': Image.open(img_path)
|
||||
})
|
||||
logger.info(f"Loaded {len(images)} test images")
|
||||
assert len(images) > 0, "No test images found"
|
||||
return images
|
||||
|
||||
def test_detector_initialization(self, detector):
|
||||
"""Test detector initialization and default parameters"""
|
||||
logger.info("Testing detector initialization...")
|
||||
assert detector.conf_threshold == 0.25
|
||||
assert detector.iou_threshold == 0.45
|
||||
assert detector.model is not None
|
||||
logger.info("Detector initialization test passed")
|
||||
|
||||
def test_single_image_detection(self, detector, test_images, results_dir):
|
||||
"""Test detection on a single image"""
|
||||
logger.info("Testing single image detection...")
|
||||
test_case = test_images[0]
|
||||
result_img, detections = detector.detect(test_case['image'])
|
||||
|
||||
# Save the result
|
||||
output_path = results_dir / "single_detection_test.png"
|
||||
result_img.save(output_path)
|
||||
logger.info(f"Saved detection result to {output_path}")
|
||||
|
||||
# Verify result type and content
|
||||
assert isinstance(result_img, Image.Image)
|
||||
assert isinstance(detections, list)
|
||||
assert all(isinstance(d, dict) for d in detections)
|
||||
|
||||
# Log detection results
|
||||
logger.info(f"Number of detections: {len(detections)}")
|
||||
if len(detections) > 0:
|
||||
for i, det in enumerate(detections):
|
||||
logger.info(f"Detection {i+1}: confidence={det['confidence']:.3f}")
|
||||
|
||||
def test_batch_detection(self, detector, test_images, results_dir):
|
||||
"""Test detection on multiple images"""
|
||||
logger.info("Testing batch detection...")
|
||||
results = []
|
||||
for i, test_case in enumerate(test_images):
|
||||
logger.info(f"Processing image {i+1}/{len(test_images)}")
|
||||
result_img, detections = detector.detect(test_case['image'])
|
||||
|
||||
# Save each result
|
||||
output_path = results_dir / f"batch_detection_{i}.png"
|
||||
result_img.save(output_path)
|
||||
|
||||
results.append({
|
||||
'path': test_case['path'],
|
||||
'detections': len(detections),
|
||||
'confidences': [d['confidence'] for d in detections]
|
||||
})
|
||||
|
||||
# Save detailed results
|
||||
results_path = results_dir / "batch_results.json"
|
||||
with open(results_path, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
logger.info(f"Saved batch results to {results_path}")
|
||||
|
||||
# Log statistics
|
||||
total_detections = sum(r['detections'] for r in results)
|
||||
avg_confidence = np.mean([conf for r in results for conf in r['confidences']]) if total_detections > 0 else 0
|
||||
|
||||
logger.info("\nBatch Detection Statistics:")
|
||||
logger.info(f"Total images processed: {len(results)}")
|
||||
logger.info(f"Total detections: {total_detections}")
|
||||
logger.info(f"Average confidence: {avg_confidence:.3f}")
|
||||
|
||||
assert total_detections > 0, "No detections found in any test image"
|
||||
|
||||
def test_threshold_optimization(self, detector, test_images):
|
||||
"""Test threshold optimization functionality"""
|
||||
images = [tc['image'] for tc in test_images]
|
||||
best_conf, best_iou = detector.optimize_thresholds(images)
|
||||
|
||||
# Verify threshold bounds
|
||||
assert 0 <= best_conf <= 1, f"Invalid confidence threshold: {best_conf}"
|
||||
assert 0 <= best_iou <= 1, f"Invalid IoU threshold: {best_iou}"
|
||||
|
||||
# Test detection with optimized thresholds
|
||||
test_case = test_images[0]
|
||||
result_img, detections = detector.detect(
|
||||
test_case['image'],
|
||||
conf_threshold=best_conf,
|
||||
iou_threshold=best_iou
|
||||
)
|
||||
|
||||
print(f"\nOptimized Thresholds:")
|
||||
print(f"Confidence: {best_conf:.3f}")
|
||||
print(f"IoU: {best_iou:.3f}")
|
||||
|
||||
@pytest.mark.parametrize("conf_threshold,iou_threshold", [
|
||||
(0.1, 0.1),
|
||||
(0.5, 0.5),
|
||||
(0.9, 0.9)
|
||||
])
|
||||
def test_different_thresholds(self, detector, test_images, conf_threshold, iou_threshold):
|
||||
"""Test detection with different threshold combinations"""
|
||||
test_case = test_images[0]
|
||||
result_img, detections = detector.detect(
|
||||
test_case['image'],
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold
|
||||
)
|
||||
|
||||
print(f"\nThreshold Test (conf={conf_threshold}, iou={iou_threshold}):")
|
||||
print(f"Detections found: {len(detections)}")
|
||||
|
||||
def test_visualization(self, detector, test_images, results_dir):
|
||||
"""Test detection visualization and save results"""
|
||||
logger.info("Testing visualization...")
|
||||
|
||||
# Process and visualize a batch of images
|
||||
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
|
||||
axes = axes.ravel()
|
||||
|
||||
for idx, test_case in enumerate(test_images[:4]):
|
||||
logger.info(f"Processing image {idx+1}/4 for visualization")
|
||||
result_img, detections = detector.detect(test_case['image'])
|
||||
|
||||
# Save individual result
|
||||
result_path = results_dir / f"visualization_{idx}.png"
|
||||
result_img.save(result_path)
|
||||
logger.info(f"Saved individual result to {result_path}")
|
||||
|
||||
# Plot result
|
||||
axes[idx].imshow(result_img)
|
||||
axes[idx].set_title(f"Detections: {len(detections)}")
|
||||
axes[idx].axis('off')
|
||||
|
||||
# Save summary plot
|
||||
summary_path = results_dir / "summary.png"
|
||||
plt.tight_layout()
|
||||
plt.savefig(summary_path)
|
||||
plt.close()
|
||||
logger.info(f"Saved summary visualization to {summary_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run with output capture disabled
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
@@ -1,158 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
YOLOv8 Training Script for Memory Module Detection
|
||||
This script trains a YOLOv8 nano model to detect memory modules in motherboard images.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from ultralytics import YOLO
|
||||
import torch
|
||||
|
||||
def check_dataset_structure():
|
||||
"""Verify that the dataset structure is correct."""
|
||||
required_paths = [
|
||||
'training/train/images',
|
||||
'training/train/labels',
|
||||
'training/val/images',
|
||||
'training/val/labels',
|
||||
'dataset.yaml'
|
||||
]
|
||||
def train_model():
|
||||
# Load YOLOv8n (nano) for faster training with decent accuracy
|
||||
model = YOLO('yolov8n.pt')
|
||||
|
||||
for path in required_paths:
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"Required path not found: {path}")
|
||||
# Train with optimized parameters for speed and quality
|
||||
results = model.train(
|
||||
data='dataset.yaml',
|
||||
epochs=50, # Reduced number of epochs
|
||||
imgsz=640, # Standard image size for faster processing
|
||||
batch=8, # Smaller batch size for less memory usage
|
||||
name='memory_detector_fast',
|
||||
save=True,
|
||||
device='cpu',
|
||||
patience=15, # Shorter patience for earlier stopping
|
||||
save_period=5, # Save every 5 epochs
|
||||
verbose=True,
|
||||
|
||||
# Effective but lightweight augmentation
|
||||
degrees=5.0, # Less rotation for speed
|
||||
scale=0.5,
|
||||
translate=0.1,
|
||||
fliplr=0.5,
|
||||
mosaic=1.0, # Keep mosaic as it's very effective
|
||||
|
||||
# Speed-optimized optimization parameters
|
||||
lr0=0.01,
|
||||
lrf=0.01,
|
||||
momentum=0.937,
|
||||
weight_decay=0.0005,
|
||||
warmup_epochs=1.0, # Shorter warmup
|
||||
|
||||
# Performance parameters
|
||||
workers=0, # Fewer workers for CPU training
|
||||
cache='disk', # Changed to disk caching for deterministic results
|
||||
)
|
||||
|
||||
# Check if we have images and labels
|
||||
train_images = len([f for f in os.listdir('training/train/images') if f.endswith('.png')])
|
||||
train_labels = len([f for f in os.listdir('training/train/labels') if f.endswith('.txt')])
|
||||
val_images = len([f for f in os.listdir('training/val/images') if f.endswith('.png')])
|
||||
val_labels = len([f for f in os.listdir('training/val/labels') if f.endswith('.txt')])
|
||||
|
||||
print(f"Dataset structure verified:")
|
||||
print(f" Training: {train_images} images, {train_labels} labels")
|
||||
print(f" Validation: {val_images} images, {val_labels} labels")
|
||||
|
||||
return True
|
||||
# Save the trained model
|
||||
model.save('model/weights/best.pt')
|
||||
|
||||
def train_model(epochs=100, imgsz=640, batch_size=16, device='auto'):
|
||||
"""
|
||||
Train YOLOv8 nano model on memory module dataset.
|
||||
|
||||
Args:
|
||||
epochs (int): Number of training epochs
|
||||
imgsz (int): Image size for training
|
||||
batch_size (int): Batch size for training
|
||||
device (str): Device to use ('auto', 'cpu', 'cuda', or specific GPU id)
|
||||
"""
|
||||
|
||||
# Check dataset structure
|
||||
check_dataset_structure()
|
||||
|
||||
# Initialize YOLOv8 nano model
|
||||
print("Initializing YOLOv8 nano model...")
|
||||
model = YOLO('yolov8n.pt') # Load pretrained YOLOv8 nano model
|
||||
|
||||
# Check available device
|
||||
if device == 'auto':
|
||||
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
|
||||
print(f"Using device: {device}")
|
||||
print(f"CUDA available: {torch.cuda.is_available()}")
|
||||
if torch.cuda.is_available():
|
||||
print(f"CUDA device: {torch.cuda.get_device_name()}")
|
||||
|
||||
# Training configuration
|
||||
train_config = {
|
||||
'data': 'dataset.yaml',
|
||||
'epochs': epochs,
|
||||
'imgsz': imgsz,
|
||||
'batch': batch_size,
|
||||
'device': device,
|
||||
'project': 'runs/detect',
|
||||
'name': 'memory_module_detection',
|
||||
'save': True,
|
||||
'save_period': 10, # Save checkpoint every 10 epochs
|
||||
'cache': False, # Don't cache images (saves RAM)
|
||||
'workers': 4,
|
||||
'patience': 50, # Early stopping patience
|
||||
'optimizer': 'AdamW',
|
||||
'lr0': 0.01, # Initial learning rate
|
||||
'lrf': 0.01, # Final learning rate factor
|
||||
'momentum': 0.937,
|
||||
'weight_decay': 0.0005,
|
||||
'warmup_epochs': 3,
|
||||
'warmup_momentum': 0.8,
|
||||
'warmup_bias_lr': 0.1,
|
||||
'box': 7.5, # Box loss gain
|
||||
'cls': 0.5, # Class loss gain
|
||||
'dfl': 1.5, # DFL loss gain
|
||||
'pose': 12.0, # Pose loss gain
|
||||
'kobj': 1.0, # Keypoint obj loss gain
|
||||
'label_smoothing': 0.0,
|
||||
'nbs': 64, # Nominal batch size
|
||||
'hsv_h': 0.015, # Image HSV-Hue augmentation
|
||||
'hsv_s': 0.7, # Image HSV-Saturation augmentation
|
||||
'hsv_v': 0.4, # Image HSV-Value augmentation
|
||||
'degrees': 0.0, # Image rotation (+/- deg)
|
||||
'translate': 0.1, # Image translation (+/- fraction)
|
||||
'scale': 0.5, # Image scale (+/- gain)
|
||||
'shear': 0.0, # Image shear (+/- deg)
|
||||
'perspective': 0.0, # Image perspective (+/- fraction)
|
||||
'flipud': 0.0, # Image flip up-down (probability)
|
||||
'fliplr': 0.5, # Image flip left-right (probability)
|
||||
'mosaic': 1.0, # Image mosaic (probability)
|
||||
'mixup': 0.0, # Image mixup (probability)
|
||||
'copy_paste': 0.0, # Segment copy-paste (probability)
|
||||
}
|
||||
|
||||
print("Starting training...")
|
||||
print(f"Configuration: {train_config}")
|
||||
|
||||
# Train the model
|
||||
results = model.train(**train_config)
|
||||
|
||||
# Print training results
|
||||
print("\nTraining completed!")
|
||||
print(f"Best model saved at: runs/detect/memory_module_detection/weights/best.pt")
|
||||
print(f"Last model saved at: runs/detect/memory_module_detection/weights/last.pt")
|
||||
|
||||
return results
|
||||
|
||||
def validate_model(model_path='runs/detect/memory_module_detection/weights/best.pt'):
|
||||
"""Validate the trained model."""
|
||||
if not os.path.exists(model_path):
|
||||
print(f"Model not found at {model_path}")
|
||||
return None
|
||||
|
||||
print(f"Validating model: {model_path}")
|
||||
model = YOLO(model_path)
|
||||
|
||||
# Run validation
|
||||
results = model.val(data='dataset.yaml')
|
||||
|
||||
print("Validation completed!")
|
||||
return results
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Train YOLOv8 for memory module detection')
|
||||
parser.add_argument('--epochs', type=int, default=100, help='Number of training epochs')
|
||||
parser.add_argument('--imgsz', type=int, default=640, help='Image size for training')
|
||||
parser.add_argument('--batch', type=int, default=16, help='Batch size')
|
||||
parser.add_argument('--device', type=str, default='auto', help='Device to use (auto, cpu, cuda)')
|
||||
parser.add_argument('--validate', action='store_true', help='Only run validation')
|
||||
parser.add_argument('--model', type=str, default='runs/detect/memory_module_detection/weights/best.pt',
|
||||
help='Model path for validation')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.validate:
|
||||
validate_model(args.model)
|
||||
else:
|
||||
train_model(epochs=args.epochs, imgsz=args.imgsz, batch_size=args.batch, device=args.device)
|
||||
# Also run validation after training
|
||||
validate_model()
|
||||
if __name__ == '__main__':
|
||||
train_model()
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.353333 0.415062 0.164444 0.549136
|
||||
0 0.574444 0.426914 0.180000 0.557037
|
||||
0 0.331616 0.424054 0.113032 0.395981
|
||||
0 0.569149 0.459811 0.093085 0.446809
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.353333 0.387407 0.253333 0.454321
|
||||
0 0.568889 0.509877 0.244444 0.564938
|
||||
0 0.557488 0.563739 0.214812 0.472813
|
||||
0 0.372852 0.415530 0.203560 0.361884
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.383333 0.359753 0.233333 0.438519
|
||||
0 0.557778 0.559259 0.324444 0.481975
|
||||
0 0.557561 0.587639 0.226064 0.445795
|
||||
0 0.373290 0.415400 0.212766 0.351233
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.368889 0.395309 0.271111 0.391111
|
||||
0 0.568889 0.567160 0.324444 0.560988
|
||||
0 0.552812 0.583418 0.252660 0.437352
|
||||
0 0.378989 0.402736 0.226064 0.366430
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.402222 0.316296 0.253333 0.470123
|
||||
0 0.550000 0.541481 0.273333 0.509630
|
||||
0 0.552527 0.595745 0.299202 0.463357
|
||||
0 0.380319 0.388889 0.247340 0.338061
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.365556 0.381481 0.304444 0.355556
|
||||
0 0.554444 0.571111 0.313333 0.410864
|
||||
0 0.397606 0.407801 0.239362 0.342790
|
||||
0 0.556516 0.606383 0.299202 0.437352
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.412222 0.369630 0.317778 0.292346
|
||||
0 0.572222 0.573086 0.322222 0.454321
|
||||
0 0.424867 0.382979 0.238032 0.321513
|
||||
0 0.571144 0.601655 0.267287 0.432624
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.413333 0.340000 0.315556 0.375309
|
||||
0 0.553333 0.594815 0.368889 0.497778
|
||||
0 0.417553 0.373522 0.252660 0.335697
|
||||
0 0.561170 0.613475 0.284574 0.427896
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.404444 0.320247 0.368889 0.383210
|
||||
0 0.526667 0.616543 0.386667 0.383210
|
||||
0 0.404920 0.359338 0.264628 0.373522
|
||||
0 0.541223 0.613475 0.303191 0.404255
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.400000 0.365679 0.368889 0.363457
|
||||
0 0.513333 0.644198 0.520000 0.454321
|
||||
0 0.533245 0.682033 0.372340 0.446809
|
||||
0 0.410904 0.375887 0.287234 0.321513
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.423333 0.369630 0.393333 0.308148
|
||||
0 0.540000 0.673827 0.435556 0.402963
|
||||
0 0.443484 0.401891 0.291223 0.293144
|
||||
0 0.547872 0.708038 0.356383 0.408983
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.332222 0.401235 0.171111 0.513580
|
||||
0 0.580000 0.419012 0.217778 0.533333
|
||||
0 0.572473 0.470449 0.070479 0.453901
|
||||
0 0.336436 0.470449 0.117021 0.458629
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.450000 0.343951 0.371111 0.304198
|
||||
0 0.535556 0.648148 0.426667 0.414815
|
||||
0 0.545878 0.667849 0.341755 0.404255
|
||||
0 0.444149 0.390071 0.297872 0.260047
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.327778 0.401235 0.162222 0.553086
|
||||
0 0.552222 0.432840 0.162222 0.616296
|
||||
0 0.331782 0.440898 0.126330 0.437352
|
||||
0 0.569149 0.471631 0.079787 0.442080
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.338889 0.424938 0.171111 0.576790
|
||||
0 0.541111 0.432840 0.193333 0.553086
|
||||
0 0.571809 0.486998 0.079787 0.463357
|
||||
0 0.333112 0.456265 0.128989 0.425532
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.315556 0.411111 0.151111 0.557037
|
||||
0 0.550000 0.436790 0.157778 0.553086
|
||||
0 0.555851 0.515366 0.095745 0.505910
|
||||
0 0.310505 0.465721 0.158245 0.406619
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.324444 0.460494 0.200000 0.513580
|
||||
0 0.550000 0.478272 0.180000 0.541235
|
||||
0 0.543218 0.547281 0.139628 0.517730
|
||||
0 0.310505 0.491726 0.168883 0.505910
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.321111 0.417037 0.197778 0.529383
|
||||
0 0.537778 0.474321 0.177778 0.588642
|
||||
0 0.533245 0.539007 0.162234 0.520095
|
||||
0 0.318484 0.438534 0.174202 0.465721
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.307778 0.438765 0.286667 0.414815
|
||||
0 0.538889 0.531605 0.273333 0.529383
|
||||
0 0.542553 0.554374 0.183511 0.508274
|
||||
0 0.327128 0.446809 0.194149 0.444444
|
||||
@@ -1,2 +1,2 @@
|
||||
0 0.562222 0.541481 0.235556 0.525432
|
||||
0 0.346667 0.381481 0.262222 0.489877
|
||||
0 0.559840 0.528369 0.210106 0.527187
|
||||
0 0.345745 0.407801 0.226064 0.475177
|
||||
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
@@ -1,2 +0,0 @@
|
||||
0 0.353333 0.415062 0.164444 0.549136
|
||||
0 0.574444 0.426914 0.180000 0.557037
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.353333 0.387407 0.253333 0.454321
|
||||
0 0.568889 0.509877 0.244444 0.564938
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.383333 0.359753 0.233333 0.438519
|
||||
0 0.557778 0.559259 0.324444 0.481975
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.365556 0.381481 0.304444 0.355556
|
||||
0 0.554444 0.571111 0.313333 0.410864
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.412222 0.369630 0.317778 0.292346
|
||||
0 0.572222 0.573086 0.322222 0.454321
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.413333 0.340000 0.315556 0.375309
|
||||
0 0.553333 0.594815 0.368889 0.497778
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.404444 0.320247 0.368889 0.383210
|
||||
0 0.526667 0.616543 0.386667 0.383210
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.400000 0.365679 0.368889 0.363457
|
||||
0 0.513333 0.644198 0.520000 0.454321
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.332222 0.401235 0.171111 0.513580
|
||||
0 0.580000 0.419012 0.217778 0.533333
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.450000 0.343951 0.371111 0.304198
|
||||
0 0.535556 0.648148 0.426667 0.414815
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.327778 0.401235 0.162222 0.553086
|
||||
0 0.552222 0.432840 0.162222 0.616296
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.338889 0.424938 0.171111 0.576790
|
||||
0 0.541111 0.432840 0.193333 0.553086
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.315556 0.411111 0.151111 0.557037
|
||||
0 0.550000 0.436790 0.157778 0.553086
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.324444 0.460494 0.200000 0.513580
|
||||
0 0.550000 0.478272 0.180000 0.541235
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.321111 0.417037 0.197778 0.529383
|
||||
0 0.537778 0.474321 0.177778 0.588642
|
||||
@@ -1,2 +0,0 @@
|
||||
0 0.562222 0.541481 0.235556 0.525432
|
||||
0 0.346667 0.381481 0.262222 0.489877
|
||||