Alex Vega commited on
Commit ·
f1acbf1
1
Parent(s): 8b441e0
init
Browse files- Dockerfile +13 -0
- main.py +108 -0
- requirements.txt +8 -0
Dockerfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
RUN chmod -R 777 /app
|
| 12 |
+
|
| 13 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
main.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
import numpy as np
|
| 3 |
+
import io
|
| 4 |
+
import math
|
| 5 |
+
import cv2
|
| 6 |
+
from skimage.feature import hog
|
| 7 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
app = FastAPI(
|
| 11 |
+
title="Enhanced Peruvian Sign Language (LSP) Recognition API",
|
| 12 |
+
description="Upload an image of a Peruvian Sign Language alphabet sign to predict the corresponding letter using an enhanced Self-Organizing Map (SOM) with HOG features.",
|
| 13 |
+
version="2.0.0"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
MODEL_FILENAME = 'lsp_som_model_enhanced.pkl'
|
| 17 |
+
try:
|
| 18 |
+
with open(MODEL_FILENAME, 'rb') as f:
|
| 19 |
+
model_data = pickle.load(f)
|
| 20 |
+
som = model_data['som']
|
| 21 |
+
label_map = model_data['label_map']
|
| 22 |
+
CLASSES = model_data['classes']
|
| 23 |
+
IMG_SIZE = model_data['img_size']
|
| 24 |
+
HOG_PARAMS = model_data['feature_extraction_params'] # Load HOG parameters
|
| 25 |
+
print(f"✅ Model '{MODEL_FILENAME}' and assets loaded successfully.")
|
| 26 |
+
print(f" - Classes: {CLASSES}")
|
| 27 |
+
print(f" - Expected Image Size for Processing: {IMG_SIZE}x{IMG_SIZE}")
|
| 28 |
+
print(f" - Feature Extractor: {HOG_PARAMS['type']}")
|
| 29 |
+
except FileNotFoundError:
|
| 30 |
+
print(f"❌ FATAL ERROR: Model file '{MODEL_FILENAME}' not found.")
|
| 31 |
+
som = None
|
| 32 |
+
|
| 33 |
+
def preprocess_and_extract_features_from_bytes(image_bytes: bytes):
|
| 34 |
+
try:
|
| 35 |
+
nparr = np.frombuffer(image_bytes, np.uint8)
|
| 36 |
+
img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 37 |
+
if img_bgr is None:
|
| 38 |
+
raise ValueError("Could not decode image bytes. The file may be corrupt or not an image.")
|
| 39 |
+
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
| 40 |
+
|
| 41 |
+
ycrcb = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2YCrCb)
|
| 42 |
+
skin_mask = cv2.inRange(ycrcb, np.array([0, 135, 85]), np.array([255, 180, 135]))
|
| 43 |
+
|
| 44 |
+
contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 45 |
+
if not contours:
|
| 46 |
+
raise ValueError("No contours found in the image. The hand sign may not be clear enough.")
|
| 47 |
+
|
| 48 |
+
largest_contour = max(contours, key=cv2.contourArea)
|
| 49 |
+
x, y, w, h = cv2.boundingRect(largest_contour)
|
| 50 |
+
cropped_hand = img_rgb[y:y+h, x:x+w]
|
| 51 |
+
|
| 52 |
+
resized_hand = cv2.resize(cropped_hand, (IMG_SIZE, IMG_SIZE))
|
| 53 |
+
gray_hand = cv2.cvtColor(resized_hand, cv2.COLOR_RGB2GRAY)
|
| 54 |
+
|
| 55 |
+
hog_features = hog(gray_hand,
|
| 56 |
+
orientations=HOG_PARAMS['orientations'],
|
| 57 |
+
pixels_per_cell=HOG_PARAMS['pixels_per_cell'],
|
| 58 |
+
cells_per_block=HOG_PARAMS['cells_per_block'],
|
| 59 |
+
transform_sqrt=HOG_PARAMS['transform_sqrt'],
|
| 60 |
+
block_norm=HOG_PARAMS['block_norm'])
|
| 61 |
+
|
| 62 |
+
return hog_features
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
raise HTTPException(status_code=400, detail=f"Image processing failed. Error: {e}")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@app.get("/", tags=["Status"])
|
| 69 |
+
def read_root():
|
| 70 |
+
return {"status": "ok", "message": "Welcome to the Enhanced LSP Recognition API!"}
|
| 71 |
+
|
| 72 |
+
@app.post("/predict", tags=["Prediction"])
|
| 73 |
+
async def predict_sign(file: UploadFile = File(..., description="An image file of a Peruvian Sign Language sign.")):
|
| 74 |
+
if not som:
|
| 75 |
+
raise HTTPException(status_code=503, detail="Model is not available. API cannot process requests.")
|
| 76 |
+
|
| 77 |
+
image_bytes = await file.read()
|
| 78 |
+
|
| 79 |
+
feature_vector = preprocess_and_extract_features_from_bytes(image_bytes)
|
| 80 |
+
|
| 81 |
+
winner_neuron = som.winner(feature_vector)
|
| 82 |
+
predicted_index = label_map.get(winner_neuron, -1)
|
| 83 |
+
|
| 84 |
+
is_best_guess = False
|
| 85 |
+
if predicted_index == -1:
|
| 86 |
+
is_best_guess = True
|
| 87 |
+
min_dist = float('inf')
|
| 88 |
+
for mapped_pos, mapped_label in label_map.items():
|
| 89 |
+
dist = math.sqrt((winner_neuron[0] - mapped_pos[0])**2 + (winner_neuron[1] - mapped_pos[1])**2)
|
| 90 |
+
if dist < min_dist:
|
| 91 |
+
min_dist = dist
|
| 92 |
+
predicted_index = mapped_label
|
| 93 |
+
|
| 94 |
+
if predicted_index != -1:
|
| 95 |
+
predicted_letter = CLASSES[predicted_index]
|
| 96 |
+
prediction_type = "Nearest Neighbor Guess" if is_best_guess else "Direct Match"
|
| 97 |
+
else:
|
| 98 |
+
predicted_letter = "Unknown"
|
| 99 |
+
prediction_type = "Critical Error: No mapped neurons found on the entire map."
|
| 100 |
+
|
| 101 |
+
response = {
|
| 102 |
+
"filename": file.filename,
|
| 103 |
+
"predicted_letter": predicted_letter,
|
| 104 |
+
"prediction_type": prediction_type,
|
| 105 |
+
"winner_neuron_on_map": [int(coord) for coord in winner_neuron]
|
| 106 |
+
}
|
| 107 |
+
print(f"[LOG] Prediction successful. Response: {response}")
|
| 108 |
+
return response
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
python-multipart
|
| 4 |
+
minisom
|
| 5 |
+
numpy
|
| 6 |
+
Pillow
|
| 7 |
+
scikit-image
|
| 8 |
+
opencv-python-headless
|