Gesture_Control / app.py
zlf18's picture
Update app.py
1b46f52 verified
# 1. Import all the libraries
import gradio as gr
import cv2
import mediapipe as mp
import numpy as np
import math
import os # Import os to check for file
# --- This dictionary now points to YOUR uploaded presets ---
PRELOADED_MODELS = {
"Preset 1": "preset_1.stl",
"Preset 2": "preset_2.stl",
"Preset 3": "preset_3.stl"
}
# 4. Set up our MediaPipe objects
mp_drawing = mp.solutions.drawing_utils
mp_hands = mp.solutions.hands
# We are detecting ONE hand
hands = mp_hands.Hands(
max_num_hands=1,
min_detection_confidence=0.7,
min_tracking_confidence=0.5
)
# 5. --- GESTURE RECOGNIZER CLASS (Same as before) ---
class GestureRecognizer:
def __init__(self):
self.PINCH_THRESHOLD = 0.08
def _calculate_distance(self, lm1, lm2):
return math.sqrt((lm1.x - lm2.x)**2 + (lm1.y - lm2.y)**2)
def recognize(self, hand_landmarks):
landmarks = hand_landmarks.landmark
wrist_pos = (landmarks[mp_hands.HandLandmark.WRIST].x, landmarks[mp_hands.HandLandmark.WRIST].y)
# --- PRE-CALCULATE FINGER STATES ---
# Note: In screen coordinates, Y increases downwards.
# TIP < PIP means the finger is pointing UP (Extended)
# TIP > PIP means the finger is curled DOWN (Curled)
index_tip_y = landmarks[mp_hands.HandLandmark.INDEX_FINGER_TIP].y
index_pip_y = landmarks[mp_hands.HandLandmark.INDEX_FINGER_PIP].y
middle_tip_y = landmarks[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].y
middle_pip_y = landmarks[mp_hands.HandLandmark.MIDDLE_FINGER_PIP].y
ring_tip_y = landmarks[mp_hands.HandLandmark.RING_FINGER_TIP].y
ring_pip_y = landmarks[mp_hands.HandLandmark.RING_FINGER_PIP].y
pinky_tip_y = landmarks[mp_hands.HandLandmark.PINKY_TIP].y
pinky_pip_y = landmarks[mp_hands.HandLandmark.PINKY_PIP].y
# Determine states
index_extended = index_tip_y < index_pip_y
middle_extended = middle_tip_y < middle_pip_y
ring_curled = ring_tip_y > ring_pip_y
pinky_curled = pinky_tip_y > pinky_pip_y
# 1. Check for "PEACE SIGN"
# Index UP, Middle UP, Ring DOWN, Pinky DOWN
if index_extended and middle_extended and ring_curled and pinky_curled:
return "**PEACE**", wrist_pos
# 2. Check for "FIST"
# Index DOWN, Middle DOWN, Ring DOWN, Pinky DOWN
# (Your previous code checked if Index was Extended)
index_curled = not index_extended # TIP > PIP
middle_curled = not middle_extended # TIP > PIP
if index_curled and middle_curled and ring_curled and pinky_curled:
return "**FIST**", wrist_pos # Return wrist position
# 3. Check for "PINCH"
thumb_tip = landmarks[mp_hands.HandLandmark.THUMB_TIP]
index_tip = landmarks[mp_hands.HandLandmark.INDEX_FINGER_TIP]
pinch_distance = self._calculate_distance(thumb_tip, index_tip)
if pinch_distance < self.PINCH_THRESHOLD:
return "**PINCH**", wrist_pos
# 4. If no other gesture, it's "OPEN"
return "**OPEN**", None
# 6. --- Create an instance of our recognizer ---
recognizer = GestureRecognizer()
# 7. --- GESTURE PROCESSING FUNCTION (OPTIMIZED) ---
DEFAULT_CAMERA = (0, 90, 150)
ROTATION_SENSITIVITY = 360
ZOOM_SENSITIVITY = 75
def process_image(webcam_frame, camera_state, prev_hand_pos_state):
gesture_text = "No hand detected"
new_camera_state = camera_state
new_prev_hand_pos = None
(old_azimuth, old_polar, old_zoom) = camera_state
if webcam_frame is None:
return "Waiting...", DEFAULT_CAMERA, None, gr.update(camera_position=DEFAULT_CAMERA)
image = webcam_frame
image.flags.writeable = False
results = hands.process(image)
if results.multi_hand_landmarks:
hand_landmarks = results.multi_hand_landmarks[0]
current_gesture, current_hand_pos = recognizer.recognize(hand_landmarks)
gesture_text = current_gesture.replace("**", "")
if current_gesture == "**PEACE**":
new_camera_state = DEFAULT_CAMERA
gesture_text = "Resetting view!"
elif current_hand_pos and prev_hand_pos_state:
delta_x = current_hand_pos[0] - prev_hand_pos_state[0]
delta_y = current_hand_pos[1] - prev_hand_pos_state[1]
if current_gesture == "**PINCH**":
new_azimuth = old_azimuth + (delta_x * ROTATION_SENSITIVITY)
new_polar = old_polar - (delta_y * ROTATION_SENSITIVITY)
new_camera_state = (new_azimuth, new_polar, old_zoom)
gesture_text = "Rotating..."
elif current_gesture == "**FIST**":
new_zoom = old_zoom + (delta_x * ZOOM_SENSITIVITY)
new_zoom = max(0.5, min(new_zoom, 400.0))
new_camera_state = (old_azimuth, old_polar, new_zoom)
gesture_text = "Zooming..."
if current_hand_pos:
new_prev_hand_pos = current_hand_pos
else:
new_prev_hand_pos = None
# Return 4 values
return gesture_text, new_camera_state, new_prev_hand_pos, gr.update(camera_position=new_camera_state)
# 8. --- HELPER FUNCTIONS ---
def load_preloaded_model(model_name):
file_path = PRELOADED_MODELS[model_name]
return gr.update(value=file_path), gr.update(value=DEFAULT_CAMERA)
def load_uploaded_model(temp_file):
return gr.update(value=temp_file.name), gr.update(value=DEFAULT_CAMERA)
# --- 9. NEW: LOGIN FUNCTION ---
def login_function(password):
# --- THIS IS THE FIX ---
# It now reads the SECURE password from the environment
correct_password = os.environ.get("APP_PASSWORD")
if password == correct_password:
# Return updates to hide login and show app
return gr.update(visible=False), gr.update(visible=True), gr.update(value="")
else:
# Return updates to show error
return gr.update(visible=True), gr.update(visible=False), gr.update(value="Incorrect Password")
# --- 10. Create and launch the Gradio Interface ---
with gr.Blocks(theme=gr.themes.Glass()) as demo:
# --- This row is the LOGIN SCREEN (visible by default) ---
with gr.Row(visible=True) as login_page:
with gr.Column(scale=1):
pass # Empty spacer
with gr.Column(scale=2):
gr.Markdown("# ๐Ÿ–๏ธ Gesture-Controlled 3D Viewer")
gr.Markdown("Please enter the password to continue.")
password_input = gr.Textbox(label="Password", type="password")
login_button = gr.Button("Login")
error_output = gr.Textbox(label="Error", interactive=False)
with gr.Column(scale=1):
pass # Empty spacer
# --- This column is the MAIN APP (hidden by default) ---
with gr.Column(visible=False) as main_app:
camera_state = gr.State(value=DEFAULT_CAMERA)
prev_hand_pos_state = gr.State(value=None)
with gr.Row():
gr.Markdown("# ๐Ÿ–๏ธ Gesture-Controlled 3D Viewer")
with gr.Row():
# --- UI Column for controls ---
with gr.Column(scale=1):
gr.Markdown("### 1. Select a Model")
with gr.Tabs():
with gr.Tab("Your Presets"):
radio_picker = gr.Radio(
choices=list(PRELOADED_MODELS.keys()),
label="Select your preset model",
value="Preset 1"
)
with gr.Tab("Upload"):
file_uploader = gr.File(
label="Upload .stl, .obj, .glb, or .3mf",
file_types=['.stl', '.obj', '.glb', '.gltf', '.3mf']
)
gr.Markdown("### 2. Control with Webcam")
webcam_input = gr.Image(sources=["webcam"], streaming=True, label="Webcam Feed")
gr.Markdown("### 3. Gesture Status")
text_output = gr.Text(label="Status")
# --- MAIN COLUMN for 3D viewer ---
with gr.Column(scale=3):
default_model_path = PRELOADED_MODELS.get("Preset 1", None)
if not os.path.exists(default_model_path):
print(f"Warning: Default preset '{default_model_path}' not found.")
default_model_path = None
model_3d = gr.Model3D(
value=default_model_path,
label=None,
camera_position=DEFAULT_CAMERA,
interactive=False
)
gr.Markdown(
"""
<div style="width: 100%; text-align: center;">
<b>Gesture Controls:</b> โœŒ๏ธ <b>Peace Sign:</b> Reset/Home | ๐Ÿ‘Œ <b>Pinch & Move:</b> Rotate | โœŠ <b>Fist & Move Left/Right:</b> Zoom In / Zoom Out
</div>
"""
)
# --- 11. WIRE UP ALL CONTROLS ---
# 1. Wire up the Login Button
login_button.click(
fn=login_function,
inputs=password_input,
outputs=[login_page, main_app, error_output]
)
# 2. Wire up the Radio buttons
radio_picker.change(
fn=load_preloaded_model,
inputs=radio_picker,
outputs=[model_3d, camera_state]
)
# 3. Wire up the File uploader
file_uploader.upload(
fn=load_uploaded_model,
inputs=file_uploader,
outputs=[model_3d, camera_state]
)
# 4. Wire up the webcam gesture controls
webcam_input.stream(
fn=process_image,
inputs=[
webcam_input,
camera_state,
prev_hand_pos_state
],
outputs=[
text_output,
camera_state,
prev_hand_pos_state,
model_3d
]
)
# ---
# THIS IS THE LAUNCH COMMAND FOR HUGGING FACE
# ---
# We use share=True to fix the ValueError
demo.launch(share=True)