Update app.py
Browse files
app.py
CHANGED
|
@@ -385,7 +385,7 @@ def health():
|
|
| 385 |
async def predict_alphabet(file: UploadFile = File(...)):
|
| 386 |
data = await file.read()
|
| 387 |
mp_img = bytes_to_mp_image(data)
|
| 388 |
-
result = hand_detector_alpha.detect_for_video(mp_img, 0)
|
| 389 |
hand_lms = result.hand_landmarks[0] if result.hand_landmarks else None
|
| 390 |
|
| 391 |
if hand_lms is None:
|
|
@@ -414,7 +414,8 @@ async def predict_alphabet_batch(files: List[UploadFile] = File(...)):
|
|
| 414 |
for i, file in enumerate(files):
|
| 415 |
data = await file.read()
|
| 416 |
mp_img = bytes_to_mp_image(data)
|
| 417 |
-
|
|
|
|
| 418 |
hand_lms = result.hand_landmarks[0] if result.hand_landmarks else None
|
| 419 |
if hand_lms is None:
|
| 420 |
continue
|
|
@@ -436,31 +437,16 @@ async def predict_alphabet_batch(files: List[UploadFile] = File(...)):
|
|
| 436 |
|
| 437 |
@app.post("/predict/auto", response_model=AutoResponse)
|
| 438 |
async def predict_auto(files: List[UploadFile] = File(...)):
|
| 439 |
-
|
| 440 |
-
Accepts 60 JPEG frames.
|
| 441 |
-
Processes all frames (no skipping).
|
| 442 |
-
|
| 443 |
-
Decision logic:
|
| 444 |
-
1. If hand presence < 1/3 of processed frames β not detected
|
| 445 |
-
2. If 2 hands detected in >= 1/3 of processed frames β skip CNN, go straight to LSTM
|
| 446 |
-
3. If wrist movement >= threshold β skip CNN, go straight to LSTM
|
| 447 |
-
4. If CNN vote_ratio >= 0.65 β return letter
|
| 448 |
-
5. Else β run LSTM β return phrase
|
| 449 |
-
"""
|
| 450 |
if not files:
|
| 451 |
raise HTTPException(status_code=400, detail="No frames provided")
|
| 452 |
|
| 453 |
-
frame_data = []
|
| 454 |
-
for file in files:
|
| 455 |
-
data = await file.read()
|
| 456 |
-
frame_data.append(data)
|
| 457 |
|
| 458 |
votes = {}
|
| 459 |
total_processed_frames = len(frame_data)
|
| 460 |
total_hand_frames = 0
|
| 461 |
two_hand_frames = 0
|
| 462 |
-
|
| 463 |
-
# Movement tracking β store wrist position of each processed frame
|
| 464 |
wrist_positions = []
|
| 465 |
|
| 466 |
coord_buffer = []
|
|
@@ -468,40 +454,54 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 468 |
last_left_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 469 |
last_right_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 470 |
|
|
|
|
|
|
|
|
|
|
| 471 |
for i, data in enumerate(frame_data):
|
|
|
|
|
|
|
| 472 |
timestamp_ms = int(i * (1000 / 30))
|
|
|
|
| 473 |
mp_img = bytes_to_mp_image(data)
|
| 474 |
|
| 475 |
-
# CNN
|
| 476 |
cnn_result = hand_detector_alpha.detect_for_video(mp_img, timestamp_ms)
|
| 477 |
-
hand_lms = cnn_result.hand_landmarks[0]
|
| 478 |
-
if cnn_result.hand_landmarks else None
|
| 479 |
|
| 480 |
if hand_lms is not None:
|
| 481 |
total_hand_frames += 1
|
| 482 |
-
|
|
|
|
| 483 |
wrist_positions.append((hand_lms[WRIST].x, hand_lms[WRIST].y))
|
| 484 |
|
| 485 |
skel = render_hand_skeleton(hand_lms)
|
| 486 |
tensor = infer_transform(skel).unsqueeze(0).to(device)
|
|
|
|
| 487 |
with torch.no_grad():
|
| 488 |
probs = torch.softmax(alphabet_model(tensor), dim=1)[0]
|
|
|
|
| 489 |
conf, idx = probs.max(dim=0)
|
| 490 |
-
|
|
|
|
| 491 |
letter = ALPHA_CLASSES[idx.item()]
|
| 492 |
votes[letter] = votes.get(letter, 0) + 1
|
| 493 |
|
| 494 |
-
#
|
| 495 |
pose_result = pose_detector.detect_for_video(mp_img, timestamp_ms)
|
| 496 |
-
pose_lms = pose_result.pose_landmarks[0]
|
| 497 |
-
|
| 498 |
if pose_lms:
|
| 499 |
last_pose_vec = normalize_pose(pose_lms)
|
| 500 |
|
|
|
|
| 501 |
lstm_result = hand_detector_phrase.detect_for_video(mp_img, timestamp_ms)
|
| 502 |
-
left_lms, right_lms
|
| 503 |
-
|
| 504 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
|
| 506 |
if left_lms is not None and right_lms is not None:
|
| 507 |
two_hand_frames += 1
|
|
@@ -509,43 +509,40 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 509 |
vec = np.concatenate([last_pose_vec, last_left_hand_vec, last_right_hand_vec])
|
| 510 |
coord_buffer.append(vec)
|
| 511 |
|
| 512 |
-
#
|
|
|
|
|
|
|
| 513 |
min_required = total_processed_frames * MIN_HAND_PRESENCE_RATIO
|
|
|
|
| 514 |
if total_hand_frames < min_required:
|
| 515 |
-
print(f"[auto] Hand presence too low: {total_hand_frames}/{total_processed_frames} "
|
| 516 |
-
f"(need >= {min_required:.1f}) β skipping")
|
| 517 |
return AutoResponse(
|
| 518 |
result="", result_type="none",
|
| 519 |
confidence=0.0, vote_ratio=0.0, detected=False
|
| 520 |
)
|
| 521 |
|
| 522 |
-
#
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
return run_lstm(coord_buffer, vote_ratio=0.0)
|
| 527 |
|
| 528 |
-
#
|
| 529 |
-
#
|
| 530 |
-
#
|
| 531 |
-
|
| 532 |
-
mid_idx = len(wrist_positions) // 2
|
| 533 |
-
mid_pos = wrist_positions[mid_idx]
|
| 534 |
-
end_pos = wrist_positions[-1]
|
| 535 |
-
dx = end_pos[0] - mid_pos[0]
|
| 536 |
-
dy = end_pos[1] - mid_pos[1]
|
| 537 |
-
movement = (dx**2 + dy**2) ** 0.5
|
| 538 |
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
return run_lstm(coord_buffer, vote_ratio=0.0)
|
| 544 |
-
|
| 545 |
-
# ββ Step 4: CNN vote decision βββββββββββββββββββββββββββββ
|
| 546 |
-
vote_ratio = 0.0
|
| 547 |
-
best_letter = ""
|
| 548 |
|
|
|
|
|
|
|
|
|
|
| 549 |
if not votes:
|
| 550 |
return AutoResponse(
|
| 551 |
result="", result_type="none",
|
|
@@ -555,7 +552,13 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 555 |
best_letter = max(votes, key=votes.get)
|
| 556 |
vote_ratio = votes[best_letter] / sum(votes.values())
|
| 557 |
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
return AutoResponse(
|
| 560 |
result=best_letter,
|
| 561 |
result_type="letter",
|
|
@@ -564,7 +567,9 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 564 |
detected=True
|
| 565 |
)
|
| 566 |
|
| 567 |
-
#
|
|
|
|
|
|
|
| 568 |
return run_lstm(coord_buffer, vote_ratio)
|
| 569 |
|
| 570 |
|
|
|
|
| 385 |
async def predict_alphabet(file: UploadFile = File(...)):
|
| 386 |
data = await file.read()
|
| 387 |
mp_img = bytes_to_mp_image(data)
|
| 388 |
+
result = hand_detector_alpha.detect_for_video(mp_img, int(0))
|
| 389 |
hand_lms = result.hand_landmarks[0] if result.hand_landmarks else None
|
| 390 |
|
| 391 |
if hand_lms is None:
|
|
|
|
| 414 |
for i, file in enumerate(files):
|
| 415 |
data = await file.read()
|
| 416 |
mp_img = bytes_to_mp_image(data)
|
| 417 |
+
timestamp_ms = int(i * (1000 / 30))
|
| 418 |
+
result = hand_detector_alpha.detect_for_video(mp_img, timestamp_ms)
|
| 419 |
hand_lms = result.hand_landmarks[0] if result.hand_landmarks else None
|
| 420 |
if hand_lms is None:
|
| 421 |
continue
|
|
|
|
| 437 |
|
| 438 |
@app.post("/predict/auto", response_model=AutoResponse)
|
| 439 |
async def predict_auto(files: List[UploadFile] = File(...)):
|
| 440 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
if not files:
|
| 442 |
raise HTTPException(status_code=400, detail="No frames provided")
|
| 443 |
|
| 444 |
+
frame_data = [await f.read() for f in files]
|
|
|
|
|
|
|
|
|
|
| 445 |
|
| 446 |
votes = {}
|
| 447 |
total_processed_frames = len(frame_data)
|
| 448 |
total_hand_frames = 0
|
| 449 |
two_hand_frames = 0
|
|
|
|
|
|
|
| 450 |
wrist_positions = []
|
| 451 |
|
| 452 |
coord_buffer = []
|
|
|
|
| 454 |
last_left_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 455 |
last_right_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 456 |
|
| 457 |
+
# ===============================
|
| 458 |
+
# Frame Processing Loop
|
| 459 |
+
# ===============================
|
| 460 |
for i, data in enumerate(frame_data):
|
| 461 |
+
|
| 462 |
+
# β
FIXED timestamp
|
| 463 |
timestamp_ms = int(i * (1000 / 30))
|
| 464 |
+
|
| 465 |
mp_img = bytes_to_mp_image(data)
|
| 466 |
|
| 467 |
+
# ββ CNN (alphabet) ββ
|
| 468 |
cnn_result = hand_detector_alpha.detect_for_video(mp_img, timestamp_ms)
|
| 469 |
+
hand_lms = cnn_result.hand_landmarks[0] if cnn_result.hand_landmarks else None
|
|
|
|
| 470 |
|
| 471 |
if hand_lms is not None:
|
| 472 |
total_hand_frames += 1
|
| 473 |
+
|
| 474 |
+
# Track wrist position
|
| 475 |
wrist_positions.append((hand_lms[WRIST].x, hand_lms[WRIST].y))
|
| 476 |
|
| 477 |
skel = render_hand_skeleton(hand_lms)
|
| 478 |
tensor = infer_transform(skel).unsqueeze(0).to(device)
|
| 479 |
+
|
| 480 |
with torch.no_grad():
|
| 481 |
probs = torch.softmax(alphabet_model(tensor), dim=1)[0]
|
| 482 |
+
|
| 483 |
conf, idx = probs.max(dim=0)
|
| 484 |
+
|
| 485 |
+
if conf.item() >= 0.6:
|
| 486 |
letter = ALPHA_CLASSES[idx.item()]
|
| 487 |
votes[letter] = votes.get(letter, 0) + 1
|
| 488 |
|
| 489 |
+
# ββ Pose ββ
|
| 490 |
pose_result = pose_detector.detect_for_video(mp_img, timestamp_ms)
|
| 491 |
+
pose_lms = pose_result.pose_landmarks[0] if pose_result.pose_landmarks else None
|
| 492 |
+
|
| 493 |
if pose_lms:
|
| 494 |
last_pose_vec = normalize_pose(pose_lms)
|
| 495 |
|
| 496 |
+
# ββ Hands (LSTM) ββ
|
| 497 |
lstm_result = hand_detector_phrase.detect_for_video(mp_img, timestamp_ms)
|
| 498 |
+
left_lms, right_lms = get_hands_by_side(lstm_result)
|
| 499 |
+
|
| 500 |
+
if left_lms is not None:
|
| 501 |
+
last_left_hand_vec = normalize_hand(left_lms)
|
| 502 |
+
|
| 503 |
+
if right_lms is not None:
|
| 504 |
+
last_right_hand_vec = normalize_hand(right_lms)
|
| 505 |
|
| 506 |
if left_lms is not None and right_lms is not None:
|
| 507 |
two_hand_frames += 1
|
|
|
|
| 509 |
vec = np.concatenate([last_pose_vec, last_left_hand_vec, last_right_hand_vec])
|
| 510 |
coord_buffer.append(vec)
|
| 511 |
|
| 512 |
+
# ===============================
|
| 513 |
+
# Step 1: Hand Presence Check
|
| 514 |
+
# ===============================
|
| 515 |
min_required = total_processed_frames * MIN_HAND_PRESENCE_RATIO
|
| 516 |
+
|
| 517 |
if total_hand_frames < min_required:
|
|
|
|
|
|
|
| 518 |
return AutoResponse(
|
| 519 |
result="", result_type="none",
|
| 520 |
confidence=0.0, vote_ratio=0.0, detected=False
|
| 521 |
)
|
| 522 |
|
| 523 |
+
# ===============================
|
| 524 |
+
# Step 2: Two Hands β LSTM
|
| 525 |
+
# ===============================
|
| 526 |
+
if two_hand_frames >= total_processed_frames * TWO_HAND_RATIO:
|
| 527 |
return run_lstm(coord_buffer, vote_ratio=0.0)
|
| 528 |
|
| 529 |
+
# ===============================
|
| 530 |
+
# Step 3: Movement Detection (FIXED)
|
| 531 |
+
# ===============================
|
| 532 |
+
movement = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
+
if len(wrist_positions) >= 2:
|
| 535 |
+
for j in range(1, len(wrist_positions)):
|
| 536 |
+
dx = wrist_positions[j][0] - wrist_positions[j-1][0]
|
| 537 |
+
dy = wrist_positions[j][1] - wrist_positions[j-1][1]
|
| 538 |
+
movement += (dx**2 + dy**2) ** 0.5
|
| 539 |
|
| 540 |
+
# β
Normalize (important)
|
| 541 |
+
movement /= len(wrist_positions)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
|
| 543 |
+
# ===============================
|
| 544 |
+
# Step 4: CNN Voting
|
| 545 |
+
# ===============================
|
| 546 |
if not votes:
|
| 547 |
return AutoResponse(
|
| 548 |
result="", result_type="none",
|
|
|
|
| 552 |
best_letter = max(votes, key=votes.get)
|
| 553 |
vote_ratio = votes[best_letter] / sum(votes.values())
|
| 554 |
|
| 555 |
+
# ===============================
|
| 556 |
+
# Step 5: Smart Decision Logic
|
| 557 |
+
# ===============================
|
| 558 |
+
if movement >= MOVEMENT_THRESHOLD and vote_ratio < 0.75:
|
| 559 |
+
return run_lstm(coord_buffer, vote_ratio)
|
| 560 |
+
|
| 561 |
+
if vote_ratio >= 0.65:
|
| 562 |
return AutoResponse(
|
| 563 |
result=best_letter,
|
| 564 |
result_type="letter",
|
|
|
|
| 567 |
detected=True
|
| 568 |
)
|
| 569 |
|
| 570 |
+
# ===============================
|
| 571 |
+
# Step 6: Default β LSTM
|
| 572 |
+
# ===============================
|
| 573 |
return run_lstm(coord_buffer, vote_ratio)
|
| 574 |
|
| 575 |
|