1+ import cv2
2+ import mediapipe as mp
3+ import numpy as np
4+ import uuid
5+ import os
6+
7+ resized = None
8+ crop_size = (83 , 84 )
9+
10+ def restart_canvas ():
11+ # black canvas, contour_canvas
12+ # 3 is for channel
13+ return np .zeros ((480 , 640 , 3 ), dtype = np .uint8 ), np .zeros ((480 , 640 ), dtype = np .uint8 )
14+
15+ vc = cv2 .VideoCapture (index = 0 )
16+
17+ canvas , contour_canvas = restart_canvas ()
18+
19+ # distance threshold between thumb tip and finger tip
20+ threshold = 50
21+
22+ # bursh size of ink
23+ brush_size = 5
24+
25+ # Stores previous finger position
26+ prev_x , prev_y = None , None
27+
28+ # draw mode
29+ brush_mode = True
30+
31+ # Solutions API
32+
33+ # Inititalie MediaPipe FaceMesh
34+ mp_face_mesh = mp .solutions .face_mesh
35+
36+ # Initialize MediaPipe Hands
37+ mp_hands = mp .solutions .hands
38+
39+ # Instantiate the Hands class
40+ hands = mp_hands .Hands (
41+ static_image_mode = False , # False for video streams (better performance)
42+ max_num_hands = 1 , # Max number of hands to detect
43+ min_detection_confidence = 0.5 , # Confidence threshold to start tracking
44+ min_tracking_confidence = 0.5 # Confidence threshold to continue tracking
45+ )
46+
47+ # For drawing landmarks
48+ mp_drawing = mp .solutions .drawing_utils
49+
50+ while vc .isOpened ():
51+ # read each frame
52+ success , frame = vc .read ()
53+
54+ # flip for mirror effect
55+ frame = cv2 .flip (src = frame , flipCode = 1 )
56+
57+ # media pipe requires RGB
58+ # works with RGB, but renders BGR
59+ rgb_frame = cv2 .cvtColor (src = frame , code = cv2 .COLOR_BGR2RGB )
60+
61+ # process with model
62+ detect = hands .process (rgb_frame )
63+
64+ # frame height, width and number of channels (3)
65+ h , w , ch = frame .shape
66+
67+ # if detected
68+ if detect .multi_hand_landmarks :
69+ for hand_landmarks in detect .multi_hand_landmarks :
70+ mp_drawing .draw_landmarks (
71+ frame ,
72+ hand_landmarks ,
73+ mp_hands .HAND_CONNECTIONS ,
74+ mp_drawing .DrawingSpec (color = (0 , 255 , 0 ), thickness = 2 ), # Landmark color
75+ mp_drawing .DrawingSpec (color = (0 , 0 , 255 ), thickness = 2 ) # Connection color
76+ )
77+
78+ # landmark node (x, y) is standarized to be [0, 1],
79+ # so to get multiply it with frame values (finger.x * frame.x)
80+ finger_tip = hand_landmarks .landmark [8 ]
81+ thumb_tip = hand_landmarks .landmark [12 ]
82+
83+ (finger_x , finger_y ) = (int (finger_tip .x * w ), int (finger_tip .y * h ))
84+ (thumb_x , thumb_y ) = (int (thumb_tip .x * w ), int (thumb_tip .y * h ))
85+
86+ # Compute Euclidean distance between index and thumb tips
87+ distance = np .linalg .norm (np .array ([finger_x , finger_y ]) - np .array ([thumb_x , thumb_y ]))
88+
89+ if distance > threshold :
90+
91+ cv2 .circle (frame , (finger_x , finger_y ), 5 , (0 , 255 , 255 ), - 1 ) # Yellow preview circle
92+
93+ # Draw on canvas if finger is moving
94+ if prev_x and prev_y :
95+ cv2 .line (canvas , (prev_x , prev_y ), (finger_x , finger_y ), (255 , 255 , 255 ), brush_size )
96+
97+ prev_x , prev_y = finger_x , finger_y
98+
99+ else :
100+ # red circle to show that currently not drawing
101+ cv2 .circle (frame , (finger_x , finger_y ), 5 , (0 , 0 , 255 ), - 1 )
102+
103+ if brush_mode :
104+ prev_x , prev_y = None , None
105+
106+ # draw contours
107+ contours , _ = cv2 .findContours (cv2 .cvtColor (canvas , cv2 .COLOR_BGR2GRAY ), cv2 .RETR_EXTERNAL , cv2 .CHAIN_APPROX_SIMPLE )
108+ cv2 .drawContours (contour_canvas , contours , - 1 , 255 )
109+
110+ if contours :
111+ x_min = min (cv2 .boundingRect (c )[0 ] for c in contours )
112+ y_min = min (cv2 .boundingRect (c )[1 ] for c in contours )
113+ x_max = max (cv2 .boundingRect (c )[0 ] + cv2 .boundingRect (c )[2 ] for c in contours )
114+ y_max = max (cv2 .boundingRect (c )[1 ] + cv2 .boundingRect (c )[3 ] for c in contours )
115+
116+ # crop according to contour
117+ cropped = canvas .copy ()[y_min :y_max , x_min :x_max ]
118+
119+ _ , contour_canvas = restart_canvas ()
120+
121+ # cv2.imshow('cropped', cropped)
122+
123+ # invert colors
124+ # cropped = cv2.bitwise_not(cropped)
125+
126+ # resize to fit the desired size
127+ resized = cv2 .resize (cropped , crop_size )
128+
129+ # if brush_mode:
130+ # cropped = cv2.GaussianBlur(cropped, (15, 15), 0)
131+
132+ # cv2.imshow('resized', resized)
133+
134+ # overlay two images
135+ frame = cv2 .addWeighted (frame , 0.6 , canvas , 0.4 , 0 )
136+
137+ cv2 .putText (frame , f'threshold:{ threshold } ' , (10 ,30 ), cv2 .FONT_HERSHEY_SIMPLEX , 1 , (255 ,255 ,255 ), 1 , cv2 .LINE_AA )
138+
139+ if brush_mode :
140+ mode = 'brush'
141+ else :
142+ mode = 'line'
143+ cv2 .putText (frame , f'Mode:{ mode } ' , (10 ,60 ), cv2 .FONT_HERSHEY_SIMPLEX , 1 , (255 ,255 ,255 ), 1 , cv2 .LINE_AA )
144+
145+ cv2 .imshow ('frame' , frame )
146+ # cv2.imshow('canvas', canvas)
147+ # cv2.imshow('contour canvas', contour_canvas)
148+
149+ key = cv2 .waitKey (1 )
150+ if key == 27 : # esc
151+ break
152+ elif key == ord ('m' ): # m - 109
153+ threshold += 1
154+ elif key == ord ('n' ): # n - 110
155+ threshold -= 1
156+ elif key == ord ('d' ):
157+ canvas , contour_canvas = restart_canvas ()
158+ elif key == ord ('p' ):
159+ brush_mode = not brush_mode
160+ elif key == ord ('c' ):
161+ # Generate a UUID and convert it to a string for use as a filename
162+ if 'saved' not in os .listdir ('./' ):
163+ os .makedirs ('./saved' , exist_ok = True )
164+ random_filename = str (uuid .uuid4 ())
165+ cv2 .imwrite (f'./saved/{ random_filename } .jpg' , resized )
166+
167+ vc .release ()
168+ cv2 .destroyAllWindows ()
0 commit comments