Update app.py
Browse files
app.py
CHANGED
|
@@ -57,6 +57,9 @@ MIN_HAND_PRESENCE_RATIO = 1 / 3
|
|
| 57 |
# Fraction of processed frames with 2 hands to trigger automatic LSTM
|
| 58 |
TWO_HAND_RATIO = 1 / 3
|
| 59 |
|
|
|
|
|
|
|
|
|
|
| 60 |
# ============================================================
|
| 61 |
# Model Architectures
|
| 62 |
# ============================================================
|
|
@@ -196,10 +199,10 @@ download_if_missing(
|
|
| 196 |
'pose_landmarker.task'
|
| 197 |
)
|
| 198 |
|
| 199 |
-
#
|
| 200 |
hand_options_alphabet = mp_vision.HandLandmarkerOptions(
|
| 201 |
base_options=mp_python.BaseOptions(model_asset_path='hand_landmarker.task'),
|
| 202 |
-
running_mode=mp_vision.RunningMode.
|
| 203 |
num_hands=1,
|
| 204 |
min_hand_detection_confidence=0.7,
|
| 205 |
min_hand_presence_confidence=0.6,
|
|
@@ -207,7 +210,7 @@ hand_options_alphabet = mp_vision.HandLandmarkerOptions(
|
|
| 207 |
)
|
| 208 |
hand_options_phrase = mp_vision.HandLandmarkerOptions(
|
| 209 |
base_options=mp_python.BaseOptions(model_asset_path='hand_landmarker.task'),
|
| 210 |
-
running_mode=mp_vision.RunningMode.
|
| 211 |
num_hands=2,
|
| 212 |
min_hand_detection_confidence=0.5,
|
| 213 |
min_hand_presence_confidence=0.5,
|
|
@@ -215,7 +218,7 @@ hand_options_phrase = mp_vision.HandLandmarkerOptions(
|
|
| 215 |
)
|
| 216 |
pose_options = mp_vision.PoseLandmarkerOptions(
|
| 217 |
base_options=mp_python.BaseOptions(model_asset_path='pose_landmarker.task'),
|
| 218 |
-
running_mode=mp_vision.RunningMode.
|
| 219 |
num_poses=1,
|
| 220 |
min_pose_detection_confidence=0.3,
|
| 221 |
min_pose_presence_confidence=0.3,
|
|
@@ -244,7 +247,7 @@ def bytes_to_mp_image(data: bytes):
|
|
| 244 |
arr = np.frombuffer(data, np.uint8)
|
| 245 |
frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 246 |
frame = cv2.flip(frame, 1)
|
| 247 |
-
|
| 248 |
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 249 |
return mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
|
| 250 |
|
|
@@ -382,7 +385,7 @@ def health():
|
|
| 382 |
async def predict_alphabet(file: UploadFile = File(...)):
|
| 383 |
data = await file.read()
|
| 384 |
mp_img = bytes_to_mp_image(data)
|
| 385 |
-
result
|
| 386 |
hand_lms = result.hand_landmarks[0] if result.hand_landmarks else None
|
| 387 |
|
| 388 |
if hand_lms is None:
|
|
@@ -408,10 +411,10 @@ async def predict_alphabet_batch(files: List[UploadFile] = File(...)):
|
|
| 408 |
raise HTTPException(status_code=400, detail="No frames provided")
|
| 409 |
|
| 410 |
votes = {}
|
| 411 |
-
for file in files:
|
| 412 |
data = await file.read()
|
| 413 |
mp_img = bytes_to_mp_image(data)
|
| 414 |
-
result
|
| 415 |
hand_lms = result.hand_landmarks[0] if result.hand_landmarks else None
|
| 416 |
if hand_lms is None:
|
| 417 |
continue
|
|
@@ -435,13 +438,14 @@ async def predict_alphabet_batch(files: List[UploadFile] = File(...)):
|
|
| 435 |
async def predict_auto(files: List[UploadFile] = File(...)):
|
| 436 |
"""
|
| 437 |
Accepts 60 JPEG frames.
|
| 438 |
-
|
| 439 |
|
| 440 |
Decision logic:
|
| 441 |
1. If hand presence < 1/3 of processed frames β not detected
|
| 442 |
2. If 2 hands detected in >= 1/3 of processed frames β skip CNN, go straight to LSTM
|
| 443 |
-
3. If
|
| 444 |
-
4.
|
|
|
|
| 445 |
"""
|
| 446 |
if not files:
|
| 447 |
raise HTTPException(status_code=400, detail="No frames provided")
|
|
@@ -452,7 +456,7 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 452 |
frame_data.append(data)
|
| 453 |
|
| 454 |
votes = {}
|
| 455 |
-
total_processed_frames =
|
| 456 |
total_hand_frames = 0
|
| 457 |
two_hand_frames = 0
|
| 458 |
|
|
@@ -465,43 +469,42 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 465 |
last_right_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 466 |
|
| 467 |
for i, data in enumerate(frame_data):
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
two_hand_frames += 1
|
| 505 |
|
| 506 |
vec = np.concatenate([last_pose_vec, last_left_hand_vec, last_right_hand_vec])
|
| 507 |
coord_buffer.append(vec)
|
|
@@ -535,7 +538,6 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 535 |
|
| 536 |
print(f"[auto] Wrist movement (midβend): {movement:.4f}")
|
| 537 |
|
| 538 |
-
MOVEMENT_THRESHOLD = 0.70 # ~20% of frame width, tunable
|
| 539 |
if movement >= MOVEMENT_THRESHOLD:
|
| 540 |
print(f"[auto] Significant movement ({movement:.4f}) β LSTM")
|
| 541 |
return run_lstm(coord_buffer, vote_ratio=0.0)
|
|
@@ -562,7 +564,7 @@ async def predict_auto(files: List[UploadFile] = File(...)):
|
|
| 562 |
detected=True
|
| 563 |
)
|
| 564 |
|
| 565 |
-
# ββ Step
|
| 566 |
return run_lstm(coord_buffer, vote_ratio)
|
| 567 |
|
| 568 |
|
|
@@ -576,17 +578,19 @@ async def predict_phrase(files: List[UploadFile] = File(...)):
|
|
| 576 |
last_left_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 577 |
last_right_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 578 |
|
| 579 |
-
for file in files:
|
| 580 |
data = await file.read()
|
| 581 |
mp_img = bytes_to_mp_image(data)
|
| 582 |
-
|
| 583 |
-
|
|
|
|
|
|
|
| 584 |
pose_lms = pose_result.pose_landmarks[0] \
|
| 585 |
if pose_result.pose_landmarks else None
|
| 586 |
if pose_lms:
|
| 587 |
last_pose_vec = normalize_pose(pose_lms)
|
| 588 |
|
| 589 |
-
hand_result
|
| 590 |
left_lms, right_lms = get_hands_by_side(hand_result)
|
| 591 |
if left_lms is not None: last_left_hand_vec = normalize_hand(left_lms)
|
| 592 |
if right_lms is not None: last_right_hand_vec = normalize_hand(right_lms)
|
|
|
|
| 57 |
# Fraction of processed frames with 2 hands to trigger automatic LSTM
|
| 58 |
TWO_HAND_RATIO = 1 / 3
|
| 59 |
|
| 60 |
+
# Movement threshold for wrist position change (normalized coordinates)
|
| 61 |
+
MOVEMENT_THRESHOLD = 0.03 # ~3% of frame width
|
| 62 |
+
|
| 63 |
# ============================================================
|
| 64 |
# Model Architectures
|
| 65 |
# ============================================================
|
|
|
|
| 199 |
'pose_landmarker.task'
|
| 200 |
)
|
| 201 |
|
| 202 |
+
# VIDEO mode detectors
|
| 203 |
hand_options_alphabet = mp_vision.HandLandmarkerOptions(
|
| 204 |
base_options=mp_python.BaseOptions(model_asset_path='hand_landmarker.task'),
|
| 205 |
+
running_mode=mp_vision.RunningMode.VIDEO,
|
| 206 |
num_hands=1,
|
| 207 |
min_hand_detection_confidence=0.7,
|
| 208 |
min_hand_presence_confidence=0.6,
|
|
|
|
| 210 |
)
|
| 211 |
hand_options_phrase = mp_vision.HandLandmarkerOptions(
|
| 212 |
base_options=mp_python.BaseOptions(model_asset_path='hand_landmarker.task'),
|
| 213 |
+
running_mode=mp_vision.RunningMode.VIDEO,
|
| 214 |
num_hands=2,
|
| 215 |
min_hand_detection_confidence=0.5,
|
| 216 |
min_hand_presence_confidence=0.5,
|
|
|
|
| 218 |
)
|
| 219 |
pose_options = mp_vision.PoseLandmarkerOptions(
|
| 220 |
base_options=mp_python.BaseOptions(model_asset_path='pose_landmarker.task'),
|
| 221 |
+
running_mode=mp_vision.RunningMode.VIDEO,
|
| 222 |
num_poses=1,
|
| 223 |
min_pose_detection_confidence=0.3,
|
| 224 |
min_pose_presence_confidence=0.3,
|
|
|
|
| 247 |
arr = np.frombuffer(data, np.uint8)
|
| 248 |
frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 249 |
frame = cv2.flip(frame, 1)
|
| 250 |
+
frame = cv2.convertScaleAbs(frame, alpha=1.3, beta=20)
|
| 251 |
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 252 |
return mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
|
| 253 |
|
|
|
|
| 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:
|
|
|
|
| 411 |
raise HTTPException(status_code=400, detail="No frames provided")
|
| 412 |
|
| 413 |
votes = {}
|
| 414 |
+
for i, file in enumerate(files):
|
| 415 |
data = await file.read()
|
| 416 |
mp_img = bytes_to_mp_image(data)
|
| 417 |
+
result = hand_detector_alpha.detect_for_video(mp_img, i * 33)
|
| 418 |
hand_lms = result.hand_landmarks[0] if result.hand_landmarks else None
|
| 419 |
if hand_lms is None:
|
| 420 |
continue
|
|
|
|
| 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")
|
|
|
|
| 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 |
|
|
|
|
| 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 β single hand for alphabet
|
| 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 |
+
# Track wrist position for movement detection
|
| 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 |
+
if conf.item() >= 0.6: # only count confident predictions
|
| 491 |
+
letter = ALPHA_CLASSES[idx.item()]
|
| 492 |
+
votes[letter] = votes.get(letter, 0) + 1
|
| 493 |
+
|
| 494 |
+
# LSTM β pose + both hands
|
| 495 |
+
pose_result = pose_detector.detect_for_video(mp_img, timestamp_ms)
|
| 496 |
+
pose_lms = pose_result.pose_landmarks[0] \
|
| 497 |
+
if pose_result.pose_landmarks else None
|
| 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 = get_hands_by_side(lstm_result)
|
| 503 |
+
if left_lms is not None: last_left_hand_vec = normalize_hand(left_lms)
|
| 504 |
+
if right_lms is not None: 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
|
|
|
|
| 508 |
|
| 509 |
vec = np.concatenate([last_pose_vec, last_left_hand_vec, last_right_hand_vec])
|
| 510 |
coord_buffer.append(vec)
|
|
|
|
| 538 |
|
| 539 |
print(f"[auto] Wrist movement (midβend): {movement:.4f}")
|
| 540 |
|
|
|
|
| 541 |
if movement >= MOVEMENT_THRESHOLD:
|
| 542 |
print(f"[auto] Significant movement ({movement:.4f}) β LSTM")
|
| 543 |
return run_lstm(coord_buffer, vote_ratio=0.0)
|
|
|
|
| 564 |
detected=True
|
| 565 |
)
|
| 566 |
|
| 567 |
+
# ββ Step 5: LSTM βββββββββββββββββββββββββββββββββββββββββ
|
| 568 |
return run_lstm(coord_buffer, vote_ratio)
|
| 569 |
|
| 570 |
|
|
|
|
| 578 |
last_left_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 579 |
last_right_hand_vec = np.zeros(HAND_SIZE, dtype=np.float32)
|
| 580 |
|
| 581 |
+
for i, file in enumerate(files):
|
| 582 |
data = await file.read()
|
| 583 |
mp_img = bytes_to_mp_image(data)
|
| 584 |
+
|
| 585 |
+
timestamp_ms = int(i * (1000 / 30))
|
| 586 |
+
|
| 587 |
+
pose_result = pose_detector.detect_for_video(mp_img, timestamp_ms)
|
| 588 |
pose_lms = pose_result.pose_landmarks[0] \
|
| 589 |
if pose_result.pose_landmarks else None
|
| 590 |
if pose_lms:
|
| 591 |
last_pose_vec = normalize_pose(pose_lms)
|
| 592 |
|
| 593 |
+
hand_result = hand_detector_phrase.detect_for_video(mp_img, timestamp_ms)
|
| 594 |
left_lms, right_lms = get_hands_by_side(hand_result)
|
| 595 |
if left_lms is not None: last_left_hand_vec = normalize_hand(left_lms)
|
| 596 |
if right_lms is not None: last_right_hand_vec = normalize_hand(right_lms)
|