├── README.md ├── hand_tracking.py ├── packages.txt ├── requirements.txt └── web_gesture_app_V3(final).py /README.md: -------------------------------------------------------------------------------- 1 | 🎛️ Volume Control Gesture 🎯 2 | 3 | A modern and interactive application that allows users to control the system volume using simple hand gestures. 🖐️🎚️ Built using computer vision techniques with OpenCV and Python, this project leverages hand-tracking to create a seamless and intuitive volume control experience—no more reaching for the volume buttons! 🚀 4 | 5 | 6 | --- 7 | 8 | 🌟 Features 9 | 10 | 🎥 Real-Time Hand Tracking: Track hand movements in real-time using OpenCV. 11 | 12 | 🖐️ Gesture-Based Volume Control: Adjust volume by moving your hand closer or farther. 13 | 14 | 🔊 Dynamic Volume Feedback: Displays the current volume level on the screen. 15 | 16 | ⚙️ Easy Integration: Simple and lightweight with minimal dependencies. 17 | 18 | 🎛️ User-Friendly Interface: Clear and intuitive UI for easy operation. 19 | 20 | 21 | 22 | --- 23 | 24 | 🛠️ Tech Stack 25 | 26 | 🐍 Python 27 | 28 | 🎯 OpenCV 29 | 30 | 🤖 MediaPipe (for hand tracking) 31 | 32 | 🔊 Pycaw (for volume control) 33 | 34 | 35 | 36 | --- 37 | 38 | 🚀 Installation 39 | 40 | 1. Clone the repository: 41 | 42 | git clone https://github.com/machinelearningprodigy/Volume_Control_Gesture.git 43 | cd Volume_Control_Gesture 44 | 45 | 46 | 2. Install dependencies: 47 | 48 | pip install opencv-python mediapipe pycaw numpy 49 | 50 | 51 | 52 | 53 | --- 54 | 55 | ▶️ Usage 56 | 57 | 1. Run the application: 58 | 59 | python volume_control.py 60 | 61 | 62 | 2. How it works: 63 | 64 | Show your hand 🖐️ in front of the camera. 65 | 66 | Adjust volume by moving your thumb and index finger: 67 | 68 | Bring fingers closer → 🔉 Decrease volume. 69 | 70 | Move fingers apart → 🔊 Increase volume. 71 | 72 | 73 | Current volume level is displayed in real-time. 74 | 75 | 76 | 77 | 78 | 79 | --- 80 | 81 | ⚙️ Configuration 82 | 83 | You can modify the settings in the code: 84 | 85 | Camera Index: Adjust cv2.VideoCapture(0) for external cameras. 86 | 87 | Volume Range: Customize the volume limits. 88 | 89 | 90 | 91 | --- 92 | 93 | 📸 Demo 94 | 95 | https://github.com/machinelearningprodigy/Volume_Control_Gesture/assets/demo.mp4 96 | 97 | 98 | --- 99 | 100 | 🧠 How It Works 101 | 102 | 1. Hand Detection: Uses MediaPipe to detect and track hands in real-time. 103 | 104 | 105 | 2. Distance Measurement: Calculates the distance between thumb and index finger. 106 | 107 | 108 | 3. Volume Adjustment: Maps the distance to system volume using Pycaw. 109 | 110 | 111 | 4. Feedback Display: Shows the volume level dynamically on the video feed. 112 | 113 | 114 | 115 | 116 | --- 117 | 118 | 💡 Troubleshooting 119 | 120 | Ensure the camera is working properly. 🎥 121 | 122 | Run the script with Admin permissions if the volume control doesn't work. 123 | 124 | Update dependencies if the hand-tracking is inaccurate. 125 | 126 | 127 | 128 | --- 129 | 130 | 🚀 Future Improvements 131 | 132 | 🤖 Enhanced Gesture Controls: Add play/pause functionality. 133 | 134 | 🔊 Voice Feedback: Provide audio cues for volume changes. 135 | 136 | 🧠 AI Integration: Improve hand tracking with AI-based enhancements. 137 | 138 | 139 | 140 | --- 141 | 142 | 🤝 Contributing 143 | 144 | Contributions are welcome! 🌱 145 | 146 | 1. Fork the repo 🍴 147 | 148 | 149 | 2. Create a new branch 🌿 150 | 151 | 152 | 3. Make your changes 💡 153 | 154 | 155 | 4. Submit a pull request 🔔 156 | 157 | 158 | 159 | 160 | --- 161 | 162 | 🛡️ License 163 | 164 | ⚖️ Licensed under the MIT License. 165 | 166 | 167 | --- 168 | 169 | 🎯 Acknowledgments 170 | 171 | Special thanks to the developers of OpenCV, MediaPipe, and Pycaw for their amazing libraries. 🙌 172 | 173 | 174 | --- 175 | 176 | 🔍 Let's make interaction with computers more intuitive! 🌐🚀 177 | 178 | -------------------------------------------------------------------------------- /hand_tracking.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import mediapipe as mp 3 | import time 4 | import threading 5 | 6 | class handDetector: 7 | def __init__(self, mode=False, maxHands=2, detectionCon=0.5, trackCon=0.5): 8 | self.mode = mode 9 | self.maxHands = maxHands 10 | self.detectionCon = detectionCon 11 | self.trackCon = trackCon 12 | 13 | self.mpHands = mp.solutions.hands 14 | self.hands = self.mpHands.Hands( 15 | static_image_mode=self.mode, 16 | max_num_hands=self.maxHands, 17 | min_detection_confidence=self.detectionCon, 18 | min_tracking_confidence=self.trackCon 19 | ) 20 | self.mpDraw = mp.solutions.drawing_utils 21 | self.tipIds = [4, 8, 12, 16, 20] 22 | self.hand_colors = [(0, 255, 0), (255, 0, 0)] # Colors for different hands 23 | self.part_colors = [(255, 0, 255), (255, 255, 0), (0, 255, 255), (255, 165, 0), (255, 192, 203)] # Colors for different parts 24 | 25 | def findHands(self, img, draw=True): 26 | imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 27 | self.results = self.hands.process(imgRGB) 28 | 29 | if self.results.multi_hand_landmarks: 30 | for hand_no, handLms in enumerate(self.results.multi_hand_landmarks): 31 | hand_color = self.hand_colors[hand_no % len(self.hand_colors)] 32 | if draw: 33 | for id, lm in enumerate(handLms.landmark): 34 | h, w, c = img.shape 35 | cx, cy = int(lm.x * w), int(lm.y * h) 36 | part_color = self.part_colors[id % len(self.part_colors)] 37 | cv2.circle(img, (cx, cy), 5, part_color, cv2.FILLED) 38 | self.mpDraw.draw_landmarks(img, handLms, self.mpHands.HAND_CONNECTIONS, 39 | self.mpDraw.DrawingSpec(color=hand_color, thickness=2, circle_radius=2), 40 | self.mpDraw.DrawingSpec(color=hand_color, thickness=2, circle_radius=2)) 41 | return img 42 | 43 | def findPosition(self, img, handNo=0, draw=True): 44 | self.lmList = [] 45 | if self.results.multi_hand_landmarks: 46 | myHand = self.results.multi_hand_landmarks[handNo] 47 | for id, lm in enumerate(myHand.landmark): 48 | h, w, c = img.shape 49 | cx, cy = int(lm.x * w), int(lm.y * h) 50 | self.lmList.append([id, cx, cy]) 51 | if draw: 52 | part_color = self.part_colors[id % len(self.part_colors)] 53 | cv2.circle(img, (cx, cy), 5, part_color, cv2.FILLED) 54 | return self.lmList 55 | 56 | def fingersUp(self): 57 | fingers = [] 58 | if self.lmList[self.tipIds[0]][1] < self.lmList[self.tipIds[0] - 1][1]: 59 | fingers.append(1) 60 | else: 61 | fingers.append(0) 62 | 63 | for id in range(1, 5): 64 | if self.lmList[self.tipIds[id]][2] < self.lmList[self.tipIds[id] - 2][2]: 65 | fingers.append(1) 66 | else: 67 | fingers.append(0) 68 | return fingers 69 | 70 | def process_frames(cap, detector, frame_queue): 71 | while cap.isOpened(): 72 | success, img = cap.read() 73 | if not success: 74 | break 75 | 76 | img = detector.findHands(img, draw=True) 77 | frame_queue.append(img) 78 | if len(frame_queue) > 2: 79 | frame_queue.pop(0) 80 | 81 | def main(): 82 | pTime = 0 83 | cap = cv2.VideoCapture(0) 84 | cap.set(3, 1080) 85 | cap.set(4, 720) 86 | cap.set(cv2.CAP_PROP_FPS, 30) 87 | 88 | detector = handDetector() 89 | frame_queue = [] 90 | fps_list = [] 91 | 92 | capture_thread = threading.Thread(target=process_frames, args=(cap, detector, frame_queue)) 93 | capture_thread.start() 94 | 95 | while True: 96 | if len(frame_queue) == 0: 97 | time.sleep(0.01) 98 | continue 99 | 100 | img = frame_queue[0] 101 | lmList = detector.findPosition(img, draw=True) 102 | if lmList: 103 | print(lmList[4]) 104 | 105 | cTime = time.time() 106 | fps = 1 / (cTime - pTime) 107 | pTime = cTime 108 | 109 | # Maintain a rolling average of the last 10 FPS values 110 | fps_list.append(fps) 111 | if len(fps_list) > 10: 112 | fps_list.pop(0) 113 | avg_fps = sum(fps_list) / len(fps_list) 114 | 115 | cv2.putText(img, f'FPS: {int(avg_fps)}', (10, 70), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 255), 2) 116 | 117 | cv2.imshow("Image", img) 118 | if cv2.waitKey(1) & 0xFF == ord('q'): 119 | break 120 | 121 | # Introduce sleep to control FPS 122 | time.sleep(0.025) # Approx 40 FPS 123 | 124 | capture_thread.join() 125 | cap.release() 126 | cv2.destroyAllWindows() 127 | 128 | if __name__ == "__main__": 129 | main() 130 | -------------------------------------------------------------------------------- /packages.txt: -------------------------------------------------------------------------------- 1 | libgl1 2 | libpulse0 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # opencv-python==4.5.3.56 2 | # mediapipe==0.8.6 3 | # streamlit==0.89.0 4 | # pulsectl==21.5.17 5 | # pygame==2.0.1 6 | # numpy==1.21.0 7 | 8 | opencv-python==4.9.0.80 9 | mediapipe==0.10.11 10 | streamlit==1.30.0 11 | pulsectl==21.5.17 12 | pygame==2.6.0 13 | numpy==1.26.4 14 | -------------------------------------------------------------------------------- /web_gesture_app_V3(final).py: -------------------------------------------------------------------------------- 1 | # import cv2 2 | # import numpy as np 3 | # import streamlit as st 4 | # import threading 5 | # import time 6 | # from hand_tracking import handDetector, process_frames 7 | # import pulsectl 8 | # import pygame 9 | # from io import BytesIO 10 | 11 | # # Initialize the volume control 12 | # pulse = pulsectl.Pulse('volume-control') 13 | # sinks = pulse.sink_list() 14 | # sink = sinks[0] # Use the first sink for volume control 15 | 16 | # def set_volume(volume): 17 | # pulse.volume_set_all_chans(sink, volume) 18 | 19 | # def get_volume_range(): 20 | # return (0.0, 1.0) 21 | 22 | # minVol, maxVol = get_volume_range() 23 | 24 | # # Initialize Pygame mixer 25 | # pygame.mixer.init() 26 | 27 | # # Music control variables 28 | # music_files = [] 29 | # music_titles = [] 30 | # current_track_index = 0 31 | 32 | # def play_music(): 33 | # if music_files: 34 | # pygame.mixer.music.load(music_files[current_track_index]) 35 | # pygame.mixer.music.play() 36 | 37 | # def pause_music(): 38 | # if pygame.mixer.music.get_busy(): 39 | # pygame.mixer.music.pause() 40 | 41 | # def unpause_music(): 42 | # if pygame.mixer.music.get_busy(): 43 | # pygame.mixer.music.unpause() 44 | 45 | # def next_track(): 46 | # global current_track_index 47 | # if music_files: 48 | # current_track_index = (current_track_index + 1) % len(music_files) 49 | # play_music() 50 | 51 | # def previous_track(): 52 | # global current_track_index 53 | # if music_files: 54 | # current_track_index = (current_track_index - 1) % len(music_files) 55 | # play_music() 56 | 57 | # def main(): 58 | # st.title("Hand Gesture Music Control") 59 | 60 | # # Upload button 61 | # uploaded_files = st.file_uploader("Upload Music", type=["mp3", "wav"], accept_multiple_files=True) 62 | # if uploaded_files: 63 | # for uploaded_file in uploaded_files: 64 | # file_bytes = uploaded_file.read() 65 | # music_files.append(BytesIO(file_bytes)) 66 | # music_titles.append(uploaded_file.name) 67 | # play_music() # Start playing the first uploaded song 68 | 69 | # # Control buttons 70 | # col1, col2, col3, col4, col5 = st.columns(5) 71 | # with col1: 72 | # if st.button("Previous"): 73 | # previous_track() 74 | # with col2: 75 | # if st.button("Play"): 76 | # play_music() 77 | # with col3: 78 | # if st.button("Pause"): 79 | # pause_music() 80 | # with col4: 81 | # if st.button("Unpause"): 82 | # unpause_music() 83 | # with col5: 84 | # if st.button("Next"): 85 | # next_track() 86 | 87 | # # Display uploaded music files in an album-like structure 88 | # st.subheader("Music Album") 89 | # if music_files: 90 | # for index, music_title in enumerate(music_titles): 91 | # with st.container(): 92 | # st.write(f"Track {index + 1}: {music_title}") 93 | 94 | # cap = cv2.VideoCapture(0) 95 | # cap.set(3, 1080) 96 | # cap.set(4, 720) 97 | # cap.set(cv2.CAP_PROP_FPS, 30) 98 | 99 | # detector = handDetector() 100 | # frame_queue = [] 101 | # fps_list = [] 102 | 103 | # capture_thread = threading.Thread(target=process_frames, args=(cap, detector, frame_queue)) 104 | # capture_thread.start() 105 | 106 | # volBar = 400 107 | # volPer = 0 108 | 109 | # pTime = time.time() 110 | 111 | # while True: 112 | # if len(frame_queue) == 0: 113 | # time.sleep(0.01) 114 | # continue 115 | 116 | # img = frame_queue[0] 117 | # lmList = detector.findPosition(img, draw=False) 118 | 119 | # if len(lmList) != 0: 120 | # x1, y1 = lmList[4][1], lmList[4][2] 121 | # x2, y2 = lmList[8][1], lmList[8][2] 122 | # cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 123 | 124 | # length = np.hypot(x2 - x1, y2 - y1) 125 | 126 | # # Reduce hand range for more sensitivity 127 | # vol = np.interp(length, [20, 200], [minVol, maxVol]) 128 | # volBar = np.interp(length, [20, 200], [400, 150]) 129 | # volPer = np.interp(length, [20, 200], [0, 100]) 130 | 131 | # set_volume(vol) 132 | 133 | # if length < 20: 134 | # cv2.circle(img, (cx, cy), 15, (0, 255, 0), cv2.FILLED) 135 | 136 | # cTime = time.time() 137 | # fps = 1 / (cTime - pTime) 138 | # pTime = cTime 139 | 140 | # # Maintain a rolling average of the last 10 FPS values 141 | # fps_list.append(fps) 142 | # if len(fps_list) > 10: 143 | # fps_list.pop(0) 144 | # avg_fps = sum(fps_list) / len(fps_list) 145 | 146 | # if cv2.waitKey(1) & 0xFF == ord('q'): 147 | # break 148 | 149 | # # Introduce sleep to control FPS 150 | # time.sleep(0.025) # Approx 40 FPS 151 | 152 | # capture_thread.join() 153 | # cap.release() 154 | # cv2.destroyAllWindows() 155 | 156 | # if __name__ == "__main__": 157 | # main() 158 | 159 | import cv2 160 | import numpy as np 161 | import streamlit as st 162 | import threading 163 | import time 164 | from hand_tracking import handDetector, process_frames 165 | import pulsectl 166 | import pygame 167 | from io import BytesIO 168 | 169 | # Initialize the volume control 170 | pulse = pulsectl.Pulse('volume-control') 171 | sinks = pulse.sink_list() 172 | sink = sinks[0] # Use the first sink for volume control 173 | 174 | def set_volume(volume): 175 | pulse.volume_set_all_chans(sink, volume) 176 | 177 | def get_volume_range(): 178 | return (0.0, 1.0) 179 | 180 | minVol, maxVol = get_volume_range() 181 | 182 | # Initialize Pygame mixer 183 | pygame.mixer.init() 184 | 185 | # Music control variables 186 | music_files = [] 187 | music_titles = [] 188 | current_track_index = 0 189 | 190 | def play_music(): 191 | if music_files: 192 | pygame.mixer.music.load(music_files[current_track_index]) 193 | pygame.mixer.music.play() 194 | 195 | def pause_music(): 196 | if pygame.mixer.music.get_busy(): 197 | pygame.mixer.music.pause() 198 | 199 | def unpause_music(): 200 | if pygame.mixer.music.get_busy(): 201 | pygame.mixer.music.unpause() 202 | 203 | def next_track(): 204 | global current_track_index 205 | if music_files: 206 | current_track_index = (current_track_index + 1) % len(music_files) 207 | play_music() 208 | 209 | def previous_track(): 210 | global current_track_index 211 | if music_files: 212 | current_track_index = (current_track_index - 1) % len(music_files) 213 | play_music() 214 | 215 | def main(): 216 | st.title("Hand Gesture Music Control") 217 | 218 | # Upload button 219 | uploaded_files = st.file_uploader("Upload Music", type=["mp3", "wav"], accept_multiple_files=True) 220 | if uploaded_files: 221 | for uploaded_file in uploaded_files: 222 | file_bytes = uploaded_file.read() 223 | music_files.append(BytesIO(file_bytes)) 224 | music_titles.append(uploaded_file.name) 225 | play_music() # Start playing the first uploaded song 226 | 227 | # Control buttons 228 | col1, col2, col3, col4, col5 = st.columns(5) 229 | with col1: 230 | if st.button("Previous"): 231 | previous_track() 232 | with col2: 233 | if st.button("Play"): 234 | play_music() 235 | with col3: 236 | if st.button("Pause"): 237 | pause_music() 238 | with col4: 239 | if st.button("Unpause"): 240 | unpause_music() 241 | with col5: 242 | if st.button("Next"): 243 | next_track() 244 | 245 | # Display uploaded music files in an album-like structure 246 | st.subheader("Music Album") 247 | if music_files: 248 | for index, music_title in enumerate(music_titles): 249 | with st.container(): 250 | st.write(f"Track {index + 1}: {music_title}") 251 | 252 | cap = cv2.VideoCapture(0) 253 | cap.set(3, 1080) 254 | cap.set(4, 720) 255 | cap.set(cv2.CAP_PROP_FPS, 30) 256 | 257 | detector = handDetector() 258 | frame_queue = [] 259 | fps_list = [] 260 | 261 | capture_thread = threading.Thread(target=process_frames, args=(cap, detector, frame_queue)) 262 | capture_thread.start() 263 | 264 | volBar = 400 265 | volPer = 0 266 | 267 | pTime = time.time() 268 | gesture_start_time = None 269 | gesture_held = False 270 | 271 | while True: 272 | if len(frame_queue) == 0: 273 | time.sleep(0.01) 274 | continue 275 | 276 | img = frame_queue[0] 277 | lmList = detector.findPosition(img, draw=False) 278 | 279 | if len(lmList) != 0: 280 | x1, y1 = lmList[4][1], lmList[4][2] 281 | x2, y2 = lmList[8][1], lmList[8][2] 282 | cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 283 | 284 | length = np.hypot(x2 - x1, y2 - y1) 285 | 286 | # Reduce hand range for more sensitivity 287 | vol = np.interp(length, [20, 200], [minVol, maxVol]) 288 | volBar = np.interp(length, [20, 200], [400, 150]) 289 | volPer = np.interp(length, [20, 200], [0, 100]) 290 | 291 | if length < 20: 292 | cv2.circle(img, (cx, cy), 15, (0, 255, 0), cv2.FILLED) 293 | 294 | if gesture_start_time is None: 295 | gesture_start_time = time.time() 296 | elif time.time() - gesture_start_time >= 3: 297 | gesture_held = True 298 | set_volume(vol) 299 | 300 | else: 301 | gesture_start_time = None 302 | 303 | if gesture_held: 304 | set_volume(vol) 305 | 306 | cTime = time.time() 307 | fps = 1 / (cTime - pTime) 308 | pTime = cTime 309 | 310 | # Maintain a rolling average of the last 10 FPS values 311 | fps_list.append(fps) 312 | if len(fps_list) > 10: 313 | fps_list.pop(0) 314 | avg_fps = sum(fps_list) / len(fps_list) 315 | 316 | if cv2.waitKey(1) & 0xFF == ord('q'): 317 | break 318 | 319 | # Introduce sleep to control FPS 320 | time.sleep(0.025) # Approx 40 FPS 321 | 322 | capture_thread.join() 323 | cap.release() 324 | cv2.destroyAllWindows() 325 | 326 | if __name__ == "__main__": 327 | main() 328 | --------------------------------------------------------------------------------