samuellimabraz commited on
Commit
ae1809b
·
unverified ·
1 Parent(s): 91f508f

Add initial project structure with OpenCV and Mediapipe integration

Browse files

- Created .gitignore to exclude environment and configuration files.
- Added requirements.txt for project dependencies including OpenCV, Mediapipe, and Streamlit.
- Implemented run.py to launch GUI applications using Tkinter or Streamlit.
- Added face and hand landmark detection models in the res directory.
- Developed face_mesh_tracker.py and hand_tracker.py for face and hand tracking functionalities.
- Introduced opencv_utils.py for various image processing utilities.
- Created streamlit_app.py for a web-based interface to explore OpenCV filters.
- Developed tkinter_app.py for a desktop application interface with real-time image processing capabilities.

.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .vscode
2
+ .env
3
+ .venv
4
+ .streamlit
5
+ .streamlit/secrets.toml
6
+ .streamlit/secrets.toml
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ opencv-python-headless==4.8.0.74
2
+ mediapipe==0.10.8
3
+ numpy==1.24.4
4
+ streamlit==1.41.1
5
+ streamlit-webrtc==0.62.4
6
+ av==12.3.0
7
+ Pillow==11.2.1
res/face_landmarker.task ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff
3
+ size 3758596
res/hand_landmarker.task ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1
3
+ size 7819105
run.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def main():
10
+ """
11
+ Main function to run the appropriate GUI application based on command-line arguments.
12
+ """
13
+ parser = argparse.ArgumentParser(description="OpenCV GUI Application Launcher")
14
+ parser.add_argument(
15
+ "-i",
16
+ "--interface",
17
+ choices=["tkinter", "streamlit"],
18
+ default="tkinter",
19
+ help="Choose the interface to run (tkinter or streamlit)",
20
+ )
21
+
22
+ args = parser.parse_args()
23
+
24
+ # Get the absolute path to the src directory
25
+ current_dir = Path(__file__).parent
26
+ src_dir = current_dir / "src"
27
+
28
+ if args.interface == "tkinter":
29
+ print("Starting Tkinter interface...")
30
+ # Run the tkinter application directly using Python
31
+ tkinter_path = src_dir / "tkinter_app.py"
32
+ subprocess.run([sys.executable, str(tkinter_path)])
33
+
34
+ elif args.interface == "streamlit":
35
+ print("Starting Streamlit interface...")
36
+ # Run the streamlit application using the streamlit CLI
37
+ streamlit_path = src_dir / "streamlit_app.py"
38
+ subprocess.run(["streamlit", "run", str(streamlit_path)])
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
src/face_mesh_tracker.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import urllib.request
3
+ import sys
4
+
5
+ import cv2
6
+
7
+ import mediapipe as mp
8
+ from mediapipe.tasks import python
9
+ from mediapipe.tasks.python import vision
10
+ from mediapipe.framework.formats import landmark_pb2
11
+
12
+ import time
13
+ import numpy as np
14
+
15
+ # import autopy
16
+
17
+
18
+ class FaceMeshTracker:
19
+ # face bounder indices
20
+ FACE_OVAL = [
21
+ 10,
22
+ 338,
23
+ 297,
24
+ 332,
25
+ 284,
26
+ 251,
27
+ 389,
28
+ 356,
29
+ 454,
30
+ 323,
31
+ 361,
32
+ 288,
33
+ 397,
34
+ 365,
35
+ 379,
36
+ 378,
37
+ 400,
38
+ 377,
39
+ 152,
40
+ 148,
41
+ 176,
42
+ 149,
43
+ 150,
44
+ 136,
45
+ 172,
46
+ 58,
47
+ 132,
48
+ 93,
49
+ 234,
50
+ 127,
51
+ 162,
52
+ 21,
53
+ 54,
54
+ 103,
55
+ 67,
56
+ 109,
57
+ ]
58
+
59
+ # lips indices for Landmarks
60
+ LIPS = [
61
+ 61,
62
+ 146,
63
+ 91,
64
+ 181,
65
+ 84,
66
+ 17,
67
+ 314,
68
+ 405,
69
+ 321,
70
+ 375,
71
+ 291,
72
+ 308,
73
+ 324,
74
+ 318,
75
+ 402,
76
+ 317,
77
+ 14,
78
+ 87,
79
+ 178,
80
+ 88,
81
+ 95,
82
+ 185,
83
+ 40,
84
+ 39,
85
+ 37,
86
+ 0,
87
+ 267,
88
+ 269,
89
+ 270,
90
+ 409,
91
+ 415,
92
+ 310,
93
+ 311,
94
+ 312,
95
+ 13,
96
+ 82,
97
+ 81,
98
+ 42,
99
+ 183,
100
+ 78,
101
+ ]
102
+ LOWER_LIPS = [
103
+ 61,
104
+ 146,
105
+ 91,
106
+ 181,
107
+ 84,
108
+ 17,
109
+ 314,
110
+ 405,
111
+ 321,
112
+ 375,
113
+ 291,
114
+ 308,
115
+ 324,
116
+ 318,
117
+ 402,
118
+ 317,
119
+ 14,
120
+ 87,
121
+ 178,
122
+ 88,
123
+ 95,
124
+ ]
125
+ UPPER_LIPS = [
126
+ 185,
127
+ 40,
128
+ 39,
129
+ 37,
130
+ 0,
131
+ 267,
132
+ 269,
133
+ 270,
134
+ 409,
135
+ 415,
136
+ 310,
137
+ 311,
138
+ 312,
139
+ 13,
140
+ 82,
141
+ 81,
142
+ 42,
143
+ 183,
144
+ 78,
145
+ ]
146
+ # Left eyes indices
147
+ LEFT_EYE = [
148
+ 362,
149
+ 382,
150
+ 381,
151
+ 380,
152
+ 374,
153
+ 373,
154
+ 390,
155
+ 249,
156
+ 263,
157
+ 466,
158
+ 388,
159
+ 387,
160
+ 386,
161
+ 385,
162
+ 384,
163
+ 398,
164
+ ]
165
+ LEFT_EYEBROW = [336, 296, 334, 293, 300, 276, 283, 282, 295, 285]
166
+ LEFT_CENTER_EYE = [473]
167
+
168
+ # right eyes indices
169
+ RIGHT_EYE = [
170
+ 33,
171
+ 7,
172
+ 163,
173
+ 144,
174
+ 145,
175
+ 153,
176
+ 154,
177
+ 155,
178
+ 133,
179
+ 173,
180
+ 157,
181
+ 158,
182
+ 159,
183
+ 160,
184
+ 161,
185
+ 246,
186
+ ]
187
+ RIGHT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
188
+ RIGHT_CENTER_EYE = [468]
189
+
190
+ def __init__(
191
+ self,
192
+ model: str = None,
193
+ num_faces: int = 1,
194
+ min_face_detection_confidence: float = 0.5,
195
+ min_face_presence_confidence: float = 0.5,
196
+ min_tracking_confidence: float = 0.5,
197
+ ):
198
+ """
199
+ Initialize a FaceTracker instance.
200
+
201
+ Args:
202
+ model (str): The path to the model for face tracking.
203
+ num_faces (int): Maximum number of faces to detect.
204
+ min_face_detection_confidence (float): Minimum confidence value ([0.0, 1.0]) for successful face detection.
205
+ min_face_presence_confidence (float): Minimum confidence value ([0.0, 1.0]) for presence of a face to be tracked.
206
+ min_tracking_confidence (float): Minimum confidence value ([0.0, 1.0]) for successful face landmark tracking.
207
+ """
208
+ self.model = model
209
+
210
+ if self.model == None:
211
+ self.model = self.download_model()
212
+
213
+ if self.model == None:
214
+ self.model = self.download_model()
215
+
216
+ self.detector = self.initialize_detector(
217
+ num_faces,
218
+ min_face_detection_confidence,
219
+ min_face_presence_confidence,
220
+ min_tracking_confidence,
221
+ )
222
+
223
+ self.mp_face_mesh = mp.solutions.face_mesh
224
+ self.mp_drawing = mp.solutions.drawing_utils
225
+ self.mp_drawing_styles = mp.solutions.drawing_styles
226
+
227
+ self.DETECTION_RESULT = None
228
+
229
+ def save_result(
230
+ self,
231
+ result: vision.FaceLandmarkerResult,
232
+ unused_output_image,
233
+ timestamp_ms: int,
234
+ fps: bool = False,
235
+ ):
236
+ """
237
+ Saves the result of the face detection.
238
+
239
+ Args:
240
+ result (vision.FaceLandmarkerResult): Result of the face detection.
241
+ unused_output_image (mp.Image): Unused.
242
+ timestamp_ms (int): Timestamp of the detection.
243
+
244
+ Returns:
245
+ None
246
+ """
247
+ self.DETECTION_RESULT = result
248
+
249
+ def initialize_detector(
250
+ self,
251
+ num_faces: int,
252
+ min_face_detection_confidence: float,
253
+ min_face_presence_confidence: float,
254
+ min_tracking_confidence: float,
255
+ ):
256
+ """
257
+ Initializes the FaceLandmarker instance.
258
+
259
+ Args:
260
+ num_faces (int): Maximum number of faces to detect.
261
+ min_face_detection_confidence (float): Minimum confidence value ([0.0, 1.0]) for face detection to be considered successful.
262
+ min_face_presence_confidence (float): Minimum confidence value ([0.0, 1.0]) for the presence of a face for the face landmarks to be considered tracked successfully.
263
+ min_tracking_confidence (float): Minimum confidence value ([0.0, 1.0]) for the face landmarks to be considered tracked successfully.
264
+
265
+ Returns:
266
+ vision.FaceLandmarker: FaceLandmarker instance.
267
+ """
268
+ base_options = python.BaseOptions(model_asset_path=self.model)
269
+ options = vision.FaceLandmarkerOptions(
270
+ base_options=base_options,
271
+ running_mode=vision.RunningMode.LIVE_STREAM,
272
+ num_faces=num_faces,
273
+ min_face_detection_confidence=min_face_detection_confidence,
274
+ min_face_presence_confidence=min_face_presence_confidence,
275
+ min_tracking_confidence=min_tracking_confidence,
276
+ output_face_blendshapes=True,
277
+ result_callback=self.save_result,
278
+ )
279
+ return vision.FaceLandmarker.create_from_options(options)
280
+
281
+ def draw_landmarks(
282
+ self,
283
+ image: np.ndarray,
284
+ text_color: tuple = (0, 0, 0),
285
+ font_size: int = 1,
286
+ font_thickness: int = 1,
287
+ ) -> np.ndarray:
288
+ """
289
+ Draws the face landmarks on the image.
290
+
291
+ Args:
292
+ image (numpy.ndarray): Image on which to draw the landmarks.
293
+ text_color (tuple, optional): Color of the text. Defaults to (0, 0, 0).
294
+ font_size (int, optional): Size of the font. Defaults to 1.
295
+ font_thickness (int, optional): Thickness of the font. Defaults to 1.
296
+
297
+ Returns:
298
+ numpy.ndarray: Image with the landmarks drawn.
299
+ """
300
+
301
+ if self.DETECTION_RESULT:
302
+ # Draw landmarks.
303
+ for face_landmarks in self.DETECTION_RESULT.face_landmarks:
304
+ face_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
305
+ face_landmarks_proto.landmark.extend(
306
+ [
307
+ landmark_pb2.NormalizedLandmark(
308
+ x=landmark.x, y=landmark.y, z=landmark.z
309
+ )
310
+ for landmark in face_landmarks
311
+ ]
312
+ )
313
+ self.mp_drawing.draw_landmarks(
314
+ image=image,
315
+ landmark_list=face_landmarks_proto,
316
+ connections=self.mp_face_mesh.FACEMESH_TESSELATION,
317
+ landmark_drawing_spec=None,
318
+ connection_drawing_spec=self.mp_drawing_styles.get_default_face_mesh_tesselation_style(),
319
+ )
320
+ self.mp_drawing.draw_landmarks(
321
+ image=image,
322
+ landmark_list=face_landmarks_proto,
323
+ connections=self.mp_face_mesh.FACEMESH_CONTOURS,
324
+ landmark_drawing_spec=None,
325
+ connection_drawing_spec=self.mp_drawing_styles.get_default_face_mesh_contours_style(),
326
+ )
327
+ self.mp_drawing.draw_landmarks(
328
+ image=image,
329
+ landmark_list=face_landmarks_proto,
330
+ connections=self.mp_face_mesh.FACEMESH_IRISES,
331
+ landmark_drawing_spec=None,
332
+ connection_drawing_spec=self.mp_drawing_styles.get_default_face_mesh_iris_connections_style(),
333
+ )
334
+
335
+ return image
336
+
337
+ def draw_landmark_circles(
338
+ self,
339
+ image: np.ndarray,
340
+ landmark_indices: list,
341
+ circle_radius: int = 1,
342
+ circle_color: tuple = (0, 255, 0),
343
+ circle_thickness: int = 1,
344
+ ) -> np.ndarray:
345
+ """
346
+ Draws circles on the specified face landmarks on the image.
347
+
348
+ Args:
349
+ image (numpy.ndarray): Image on which to draw the landmarks.
350
+ landmark_indices (list of int): Indices of the landmarks to draw.
351
+ circle_radius (int, optional): Radius of the circles. Defaults to 1.
352
+ circle_color (tuple, optional): Color of the circles. Defaults to (0, 255, 0).
353
+ circle_thickness (int, optional): Thickness of the circles. Defaults to 1.
354
+
355
+ Returns:
356
+ numpy.ndarray: Image with the landmarks drawn.
357
+ """
358
+ if self.DETECTION_RESULT:
359
+ # Draw landmarks.
360
+ for face_landmarks in self.DETECTION_RESULT.face_landmarks:
361
+ for i, landmark in enumerate(face_landmarks):
362
+ if i in landmark_indices:
363
+ # Convert the landmark position to image coordinates.
364
+ x = int(landmark.x * image.shape[1])
365
+ y = int(landmark.y * image.shape[0])
366
+ cv2.circle(
367
+ image,
368
+ (x, y),
369
+ circle_radius,
370
+ circle_color,
371
+ circle_thickness,
372
+ )
373
+
374
+ return image
375
+
376
+ def detect(self, frame: np.ndarray, draw: bool = False) -> np.ndarray:
377
+ """
378
+ Detects the face landmarks in the frame.
379
+
380
+ Args:
381
+ frame (numpy.ndarray): Frame in which to detect the landmarks.
382
+ draw (bool, optional): Whether to draw the landmarks on the frame. Defaults to False.
383
+
384
+ Returns:
385
+ numpy.ndarray: Frame with the landmarks drawn.
386
+ """
387
+ rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
388
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_image)
389
+ self.detector.detect_async(mp_image, time.time_ns() // 1_000_000)
390
+ return self.draw_landmarks(frame) if draw else frame
391
+
392
+ def get_face_landmarks(self, face_idx: int = 0, idxs: list = None) -> list:
393
+ """
394
+ Returns the face landmarks.
395
+
396
+ Args:
397
+ face_idx (int, optional): Index of the face for which to return the landmarks. Defaults to 0.
398
+ idxs (list, optional): List of indices of the landmarks to return. Defaults to None.
399
+
400
+ Returns:
401
+ list: List of face world landmarks.
402
+ """
403
+ if self.DETECTION_RESULT is not None:
404
+ if idxs is None:
405
+ return self.DETECTION_RESULT.face_landmarks[face_idx]
406
+ else:
407
+ return [
408
+ self.DETECTION_RESULT.face_landmarks[face_idx][idx] for idx in idxs
409
+ ]
410
+ else:
411
+ return []
412
+
413
+ @staticmethod
414
+ def download_model() -> str:
415
+ """
416
+ Download the face_landmarker task model from the mediapipe repository.
417
+ https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task
418
+
419
+ Returns:
420
+ str: Path to the downloaded model.
421
+ """
422
+ root = os.path.dirname(os.path.realpath(__file__))
423
+ # Unino to res folder
424
+ root = os.path.join(root, "..", "res")
425
+ filename = os.path.join(root, "face_landmarker.task")
426
+ if os.path.exists(filename):
427
+ print(f"O arquivo {filename} já existe, pulando o download.")
428
+ else:
429
+ base = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task"
430
+ urllib.request.urlretrieve(base, filename)
431
+
432
+ return filename
src/hand_tracker.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import os
4
+ import urllib.request
5
+
6
+ import cv2
7
+ import numpy as np
8
+ import mediapipe as mp
9
+
10
+ from mediapipe.tasks import python
11
+ from mediapipe.tasks.python import vision
12
+ from mediapipe.framework.formats import landmark_pb2
13
+
14
+
15
+ class HandTracker:
16
+ def __init__(
17
+ self,
18
+ model: str = None,
19
+ num_hands: int = 2,
20
+ min_hand_detection_confidence: float = 0.5,
21
+ min_hand_presence_confidence: float = 0.5,
22
+ min_tracking_confidence: float = 0.5,
23
+ ):
24
+ """
25
+ Initialize a HandTracker instance.
26
+
27
+ Args:
28
+ model (str): The path to the model for hand tracking.
29
+ num_hands (int): Maximum number of hands to detect.
30
+ min_hand_detection_confidence (float): Minimum confidence value ([0.0, 1.0]) for successful hand detection.
31
+ min_hand_presence_confidence (float): Minimum confidence value ([0.0, 1.0]) for presence of a hand to be tracked.
32
+ min_tracking_confidence (float): Minimum confidence value ([0.0, 1.0]) for successful hand landmark tracking.
33
+ """
34
+ self.model = model
35
+
36
+ if self.model is None:
37
+ self.model = self.download_model()
38
+
39
+ self.detector = self.initialize_detector(
40
+ num_hands,
41
+ min_hand_detection_confidence,
42
+ min_hand_presence_confidence,
43
+ min_tracking_confidence,
44
+ )
45
+
46
+ self.mp_hands = mp.solutions.hands
47
+ self.mp_drawing = mp.solutions.drawing_utils
48
+ self.mp_drawing_styles = mp.solutions.drawing_styles
49
+ self.DETECTION_RESULT = None
50
+
51
+ self.tipIds = [4, 8, 12, 16, 20]
52
+
53
+ self.MARGIN = 10 # pixels
54
+ self.FONT_SIZE = 1
55
+ self.FONT_THICKNESS = 1
56
+ self.HANDEDNESS_TEXT_COLOR = (88, 205, 54) # vibrant green
57
+
58
+ # x is the raw distance, y is the value in cm
59
+ # This values are used to calculate the approximate depth of the hand
60
+ x = (
61
+ np.array(
62
+ [
63
+ 300,
64
+ 245,
65
+ 200,
66
+ 170,
67
+ 145,
68
+ 130,
69
+ 112,
70
+ 103,
71
+ 93,
72
+ 87,
73
+ 80,
74
+ 75,
75
+ 70,
76
+ 67,
77
+ 62,
78
+ 59,
79
+ 57,
80
+ ]
81
+ )
82
+ / 1.5
83
+ )
84
+ y = [20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]
85
+ self.coff = np.polyfit(x, y, 2) # y = Ax^2 + Bx + C
86
+
87
+ def save_result(
88
+ self,
89
+ result: landmark_pb2.NormalizedLandmarkList,
90
+ unused_output_image,
91
+ timestamp_ms: int,
92
+ ):
93
+ """
94
+ Saves the result of the detection.
95
+
96
+ Args:
97
+ result (mediapipe.framework.formats.landmark_pb2.NormalizedLandmarkList): Result of the detection.
98
+ unused_output_image (mediapipe.framework.formats.image_frame.ImageFrame): Unused.
99
+ timestamp_ms (int): Timestamp of the detection.
100
+
101
+ Returns:
102
+ None
103
+ """
104
+ self.DETECTION_RESULT = result
105
+
106
+ def initialize_detector(
107
+ self,
108
+ num_hands: int,
109
+ min_hand_detection_confidence: float,
110
+ min_hand_presence_confidence: float,
111
+ min_tracking_confidence: float,
112
+ ):
113
+ """
114
+ Initializes the HandLandmarker instance.
115
+
116
+ Args:
117
+ num_hands (int): Maximum number of hands to detect.
118
+ min_hand_detection_confidence (float): Minimum confidence value ([0.0, 1.0]) for hand detection to be considered successful.
119
+ min_hand_presence_confidence (float): Minimum confidence value ([0.0, 1.0]) for the presence of a hand for the hand landmarks to be considered tracked successfully.
120
+ min_tracking_confidence (float): Minimum confidence value ([0.0, 1.0]) for the hand landmarks to be considered tracked successfully.
121
+
122
+ Returns:
123
+ mediapipe.HandLandmarker: HandLandmarker instance.
124
+ """
125
+ base_options = python.BaseOptions(model_asset_path=self.model)
126
+ options = vision.HandLandmarkerOptions(
127
+ base_options=base_options,
128
+ # running_mode=vision.RunningMode.LIVE_STREAM,
129
+ num_hands=num_hands,
130
+ min_hand_detection_confidence=min_hand_detection_confidence,
131
+ min_hand_presence_confidence=min_hand_presence_confidence,
132
+ min_tracking_confidence=min_tracking_confidence,
133
+ # result_callback=self.save_result,
134
+ )
135
+ return vision.HandLandmarker.create_from_options(options)
136
+
137
+ def draw_landmarks(
138
+ self,
139
+ image: np.ndarray,
140
+ text_color: tuple = (0, 0, 0),
141
+ font_size: int = 1,
142
+ font_thickness: int = 1,
143
+ ) -> np.ndarray:
144
+ """
145
+ Draws the landmarks and handedness on the image.
146
+
147
+ Args:
148
+ image (numpy.ndarray): Image on which to draw the landmarks.
149
+ text_color (tuple, optional): Color of the text. Defaults to (0, 0, 0).
150
+ font_size (int, optional): Size of the font. Defaults to 1.
151
+ font_thickness (int, optional): Thickness of the font. Defaults to 1.
152
+
153
+ Returns:
154
+ numpy.ndarray: Image with the landmarks drawn.
155
+ """
156
+
157
+ if self.DETECTION_RESULT:
158
+ # Landmark visualization parameters.
159
+
160
+ # Draw landmarks and indicate handedness.
161
+ for idx in range(len(self.DETECTION_RESULT.hand_landmarks)):
162
+ hand_landmarks = self.DETECTION_RESULT.hand_landmarks[idx]
163
+ handedness = self.DETECTION_RESULT.handedness[idx]
164
+
165
+ # Draw the hand landmarks.
166
+ hand_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
167
+ hand_landmarks_proto.landmark.extend(
168
+ [
169
+ landmark_pb2.NormalizedLandmark(
170
+ x=landmark.x, y=landmark.y, z=landmark.z
171
+ )
172
+ for landmark in hand_landmarks
173
+ ]
174
+ )
175
+ self.mp_drawing.draw_landmarks(
176
+ image,
177
+ hand_landmarks_proto,
178
+ self.mp_hands.HAND_CONNECTIONS,
179
+ self.mp_drawing_styles.get_default_hand_landmarks_style(),
180
+ self.mp_drawing_styles.get_default_hand_connections_style(),
181
+ )
182
+
183
+ # Get the top left corner of the detected hand's bounding box.
184
+ height, width, _ = image.shape
185
+ x_coordinates = [landmark.x for landmark in hand_landmarks]
186
+ y_coordinates = [landmark.y for landmark in hand_landmarks]
187
+ text_x = int(min(x_coordinates) * width)
188
+ text_y = int(min(y_coordinates) * height) - self.MARGIN
189
+
190
+ # Draw handedness (left or right hand) on the image.
191
+ cv2.putText(
192
+ image,
193
+ f"{handedness[0].category_name}",
194
+ (text_x, text_y),
195
+ cv2.FONT_HERSHEY_DUPLEX,
196
+ self.FONT_SIZE,
197
+ self.HANDEDNESS_TEXT_COLOR,
198
+ self.FONT_THICKNESS,
199
+ cv2.LINE_AA,
200
+ )
201
+
202
+ return image
203
+
204
+ def detect(self, frame: np.ndarray, draw: bool = True) -> np.ndarray:
205
+ """
206
+ Detects hands in the image.
207
+
208
+ Args:
209
+ frame (numpy.ndarray): Image in which to detect the hands.
210
+ draw (bool, optional): Whether to draw the landmarks on the image. Defaults to False.
211
+
212
+ Returns:
213
+ numpy.ndarray: Image with the landmarks drawn if draw is True, else the original image.
214
+ """
215
+
216
+ rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
217
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_image)
218
+ self.DETECTION_RESULT = self.detector.detect(mp_image)
219
+
220
+ return self.draw_landmarks(frame) if draw else frame
221
+
222
+ def raised_fingers(self):
223
+ """
224
+ Counts the number of raised fingers.
225
+
226
+ Returns:
227
+ list: List of 1s and 0s, where 1 indicates a raised finger and 0 indicates a lowered finger.
228
+ """
229
+ fingers = []
230
+ if self.DETECTION_RESULT:
231
+ for idx, hand_landmarks in enumerate(
232
+ self.DETECTION_RESULT.hand_world_landmarks
233
+ ):
234
+ if self.DETECTION_RESULT.handedness[idx][0].category_name == "Right":
235
+ if (
236
+ hand_landmarks[self.tipIds[0]].x
237
+ > hand_landmarks[self.tipIds[0] - 1].x
238
+ ):
239
+ fingers.append(1)
240
+ else:
241
+ fingers.append(0)
242
+ else:
243
+ if (
244
+ hand_landmarks[self.tipIds[0]].x
245
+ < hand_landmarks[self.tipIds[0] - 1].x
246
+ ):
247
+ fingers.append(1)
248
+ else:
249
+ fingers.append(0)
250
+
251
+ for id in range(1, 5):
252
+ if (
253
+ hand_landmarks[self.tipIds[id]].y
254
+ < hand_landmarks[self.tipIds[id] - 2].y
255
+ ):
256
+ fingers.append(1)
257
+ else:
258
+ fingers.append(0)
259
+ return fingers
260
+
261
+ def get_approximate_depth(
262
+ self, hand_idx: int = 0, width: int = 640, height: int = 480
263
+ ) -> float:
264
+ """
265
+ Calculates the depth of each finger landmark.
266
+
267
+ Returns:
268
+ numpy.ndarray: Mean of the depth of each finger landmark.
269
+ """
270
+ if self.DETECTION_RESULT is not None:
271
+ x1, y1 = (
272
+ self.DETECTION_RESULT.hand_landmarks[hand_idx][5].x * width,
273
+ self.DETECTION_RESULT.hand_landmarks[hand_idx][5].y * height,
274
+ )
275
+ x2, y2 = (
276
+ self.DETECTION_RESULT.hand_landmarks[hand_idx][17].x * width,
277
+ self.DETECTION_RESULT.hand_landmarks[hand_idx][17].y * height,
278
+ )
279
+
280
+ distance = math.sqrt((y2 - y1) ** 2 + (x2 - x1) ** 2)
281
+ A, B, C = self.coff
282
+
283
+ return A * distance**2 + B * distance + C
284
+ else:
285
+ 0
286
+
287
+ def get_hand_world_landmarks(self, hand_idx: int = 0):
288
+ """
289
+ Returns the hand world landmarks.
290
+
291
+ Args:
292
+ hand_idx (int, optional): Index of the hand for which to return the landmarks. Defaults to 0.
293
+ 0 = Right hand
294
+ 1 = Left hand
295
+
296
+ Returns:
297
+ list: List of hand world landmarks.
298
+ """
299
+ return (
300
+ self.DETECTION_RESULT.hand_world_landmarks[hand_idx]
301
+ if self.DETECTION_RESULT is not None
302
+ else []
303
+ )
304
+
305
+ def get_hand_landmarks(self, hand_idx: int = 0, idxs: list = None) -> list:
306
+ """
307
+ Returns the hand landmarks.
308
+
309
+ Args:
310
+ hand_idx (int, optional): Index of the hand for which to return the landmarks. Defaults to 0.
311
+ 0 = Right hand
312
+ 1 = Left hand
313
+ idxs (list, optional): List of indices of the landmarks to return. Defaults to None.
314
+
315
+ Returns:
316
+ list: List of hand world landmarks.
317
+ """
318
+ if self.DETECTION_RESULT is not None:
319
+ if idxs is None:
320
+ return self.DETECTION_RESULT.hand_landmarks[hand_idx]
321
+ else:
322
+ return [
323
+ self.DETECTION_RESULT.hand_landmarks[hand_idx][idx] for idx in idxs
324
+ ]
325
+
326
+ else:
327
+ return []
328
+
329
+ def find_distance(self, l1, l2, img, draw=True):
330
+ """
331
+ Finds the distance between two landmarks.
332
+
333
+ Args:
334
+ l1 (int): Index of the first landmark.
335
+ l2 (int): Index of the second landmark.
336
+ img (numpy.ndarray): Image on which to draw the landmarks.
337
+ draw (bool, optional): Whether to draw the landmarks on the image. Defaults to True.
338
+
339
+ Returns:
340
+ float: Distance between the two landmarks.
341
+ numpy.ndarray: Image with the landmarks drawn if draw is True, else the original image.
342
+ list: List of the coordinates of the two landmarks and the center of the line joining them.
343
+ """
344
+ ladnmarks = self.get_hand_landmarks(idxs=[l1, l2])
345
+ x1, y1 = ladnmarks[0].x * img.shape[1], ladnmarks[0].y * img.shape[0]
346
+ x2, y2 = ladnmarks[1].x * img.shape[1], ladnmarks[1].y * img.shape[0]
347
+ cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
348
+ length = math.hypot(x2 - x1, y2 - y1)
349
+
350
+ # Cast points to int
351
+ x1, y1, x2, y2, cx, cy = map(int, [x1, y1, x2, y2, cx, cy])
352
+
353
+ if draw:
354
+ cv2.circle(img, (x1, y1), 10, (255, 0, 255), cv2.FILLED)
355
+ cv2.circle(img, (x2, y2), 10, (255, 0, 255), cv2.FILLED)
356
+ cv2.line(img, (x1, y1), (x2, y2), (255, 0, 255), 3)
357
+ cv2.circle(img, (cx, cy), 10, (255, 0, 255), cv2.FILLED)
358
+
359
+ return length, img, [x1, y1, x2, y2, cx, cy]
360
+
361
+ @staticmethod
362
+ def download_model() -> str:
363
+ """
364
+ Downloads the hand landmark model in float16 format from the mediapipe website.
365
+ https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task
366
+
367
+ Returns:
368
+ str: Path to the downloaded model.
369
+ """
370
+ root = os.path.dirname(os.path.realpath(__file__))
371
+ # Unino to res folder
372
+ root = os.path.join(root, "..", "res")
373
+ filename = os.path.join(root, "hand_landmarker.task")
374
+ if os.path.exists(filename):
375
+ print(f"O arquivo {filename} já existe, pulando o download.")
376
+ else:
377
+ print(f"Baixando o arquivo {filename}...")
378
+ base = "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task"
379
+ urllib.request.urlretrieve(base, filename)
380
+
381
+ return filename
src/opencv_utils.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+
4
+ from hand_tracker import HandTracker
5
+ from face_mesh_tracker import FaceMeshTracker
6
+
7
+
8
+ class OpenCVUtils:
9
+
10
+ def __init__(self) -> None:
11
+ self.hand_tracker = HandTracker(
12
+ num_hands=2,
13
+ min_hand_detection_confidence=0.7,
14
+ min_hand_presence_confidence=0.7,
15
+ min_tracking_confidence=0.7,
16
+ )
17
+ self.face_mesh_tracker = FaceMeshTracker(
18
+ num_faces=1,
19
+ min_face_detection_confidence=0.7,
20
+ min_face_presence_confidence=0.7,
21
+ min_tracking_confidence=0.7,
22
+ )
23
+
24
+ def detect_faces(self, frame: np.ndarray, draw: bool = True) -> np.ndarray:
25
+ """
26
+ Detect a face in the frame with the face mesh tracker of mediapipe
27
+
28
+ :param frame: The frame to detect the face
29
+ :param draw: If the output should be drawn
30
+ """
31
+ return self.face_mesh_tracker.detect(frame, draw=draw)
32
+
33
+ def detect_hands(self, frame: np.ndarray, draw: bool = True) -> np.ndarray:
34
+ """
35
+ Detect a hand in the frame with the hand tracker of mediapipe
36
+
37
+ :param frame: The frame to detect the hand
38
+ :param draw: If the output should be drawn
39
+ """
40
+ result = self.hand_tracker.detect(frame, draw=draw)
41
+ return result
42
+
43
+ def apply_color_filter(
44
+ self, frame: np.ndarray, lower_bound: list, upper_bound: list
45
+ ) -> np.ndarray:
46
+ """
47
+ Apply a color filter to the frame
48
+
49
+ :param frame: The frame to apply the filter
50
+ :param lower_bound: The lower bound of the color filter in HSV
51
+ :param upper_bound: The upper bound of the color filter in HSV
52
+ """
53
+ hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
54
+ lower_bound = np.array([lower_bound[0], lower_bound[1], lower_bound[2]])
55
+ upper_bound = np.array([upper_bound[0], upper_bound[1], upper_bound[2]])
56
+ mask = cv2.inRange(hsv, lower_bound, upper_bound)
57
+ return cv2.bitwise_and(frame, frame, mask=mask)
58
+
59
+ def apply_edge_detection(
60
+ self, frame: np.ndarray, lower_canny: int = 100, upper_canny: int = 200
61
+ ) -> np.ndarray:
62
+ """
63
+ Apply a edge detection to the frame
64
+
65
+ :param frame: The frame to apply the filter
66
+ :param lower_canny: The lower bound of the canny edge detection
67
+ :param upper_canny: The upper bound of the canny edge detection
68
+ """
69
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
70
+ edges = cv2.Canny(gray, lower_canny, upper_canny)
71
+ return cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
72
+
73
+ def apply_contour_detection(self, frame: np.ndarray) -> np.ndarray:
74
+ """
75
+ Apply a contour detection to the frame
76
+
77
+ :param frame: The frame to apply the filter
78
+ """
79
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
80
+ ret, thresh = cv2.threshold(gray, 127, 255, 0)
81
+ contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
82
+ cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)
83
+ return frame
84
+
85
+ def blur_image(self, image: np.ndarray, kernel_size: int = 5) -> np.ndarray:
86
+ """
87
+ Apply a blur to the image
88
+
89
+ :param image: The image to apply the blur
90
+ :param kernel_size: The kernel size of the blur
91
+ """
92
+ if kernel_size % 2 == 0:
93
+ kernel_size += 1
94
+ return cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
95
+
96
+ def rotate_image(self, image: np.ndarray, angle: int = 0) -> np.ndarray:
97
+ """
98
+ Rotate the image
99
+
100
+ :param image: The image to rotate
101
+ :param angle: The angle to rotate the image
102
+ """
103
+ (h, w) = image.shape[:2]
104
+ center = (w / 2, h / 2)
105
+
106
+ M = cv2.getRotationMatrix2D(center, angle, 1.0)
107
+ return cv2.warpAffine(image, M, (w, h))
108
+
109
+ def resize_image(
110
+ self, image: np.ndarray, width: int = None, height: int = None
111
+ ) -> np.ndarray:
112
+ """
113
+ Resize the image
114
+
115
+ :param image: The image to resize
116
+ :param width: The width of the new image
117
+ :param height: The height of the new image
118
+ """
119
+ dim = None
120
+ (h, w) = image.shape[:2]
121
+
122
+ if width is None and height is None:
123
+ return image
124
+
125
+ if width is None:
126
+ r = height / float(h)
127
+ dim = (int(w * r), height)
128
+ else:
129
+ r = width / float(w)
130
+ dim = (width, int(h * r))
131
+
132
+ return cv2.resize(image, dim, interpolation=cv2.INTER_AREA)
133
+
134
+ def pencil_sketch(
135
+ self,
136
+ image: np.ndarray,
137
+ sigma_s: int = 60,
138
+ sigma_r: float = 0.07,
139
+ shade_factor: float = 0.05,
140
+ ) -> np.ndarray:
141
+ # Converte para sketch preto e branco
142
+ gray, sketch = cv2.pencilSketch(
143
+ image, sigma_s=sigma_s, sigma_r=sigma_r, shade_factor=shade_factor
144
+ )
145
+ return sketch
146
+
147
+ def stylization(
148
+ self, image: np.ndarray, sigma_s: int = 60, sigma_r: float = 0.45
149
+ ) -> np.ndarray:
150
+ # Efeito de pintura estilizada
151
+ return cv2.stylization(image, sigma_s=sigma_s, sigma_r=sigma_r)
152
+
153
+ def cartoonify(self, image: np.ndarray) -> np.ndarray:
154
+ # Cartoon: detecta bordas e aplica quantização de cores
155
+ # 1) Detecção de bordas
156
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
157
+ blur = cv2.medianBlur(gray, 7)
158
+ edges = cv2.adaptiveThreshold(
159
+ blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 2
160
+ )
161
+ # 2) Redução de cores
162
+ data = np.float32(image).reshape((-1, 3))
163
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 0.001)
164
+ _, label, center = cv2.kmeans(
165
+ data, 8, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS
166
+ )
167
+ center = np.uint8(center)
168
+ quant = center[label.flatten()].reshape(image.shape)
169
+ # Combina bordas e quantização
170
+ cartoon = cv2.bitwise_and(quant, quant, mask=edges)
171
+ return cartoon
172
+
173
+ def color_quantization(self, image: np.ndarray, k: int = 8) -> np.ndarray:
174
+ # Reduz o número de cores via k-means
175
+ data = np.float32(image).reshape((-1, 3))
176
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 0.001)
177
+ _, label, center = cv2.kmeans(
178
+ data, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS
179
+ )
180
+ center = np.uint8(center)
181
+ quant = center[label.flatten()].reshape(image.shape)
182
+ return quant
183
+
184
+ def equalize_histogram(self, image: np.ndarray) -> np.ndarray:
185
+ ycrcb = cv2.cvtColor(image, cv2.COLOR_BGR2YCrCb)
186
+ channels = cv2.split(ycrcb)
187
+ cv2.equalizeHist(channels[0], channels[0])
188
+ merged = cv2.merge(channels)
189
+ return cv2.cvtColor(merged, cv2.COLOR_YCrCb2BGR)
190
+
191
+ def adaptive_threshold(self, image: np.ndarray) -> np.ndarray:
192
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
193
+ return cv2.cvtColor(
194
+ cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
195
+ cv2.THRESH_BINARY, 11, 2),
196
+ cv2.COLOR_GRAY2BGR)
197
+
198
+ def morphology(self, image: np.ndarray, op: str = 'erode', ksize: int = 5) -> np.ndarray:
199
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (ksize, ksize))
200
+ ops = {
201
+ 'erode': cv2.erode,
202
+ 'dilate': cv2.dilate,
203
+ 'open': cv2.morphologyEx,
204
+ 'close': cv2.morphologyEx
205
+ }
206
+ if op in ['open', 'close']:
207
+ flag = cv2.MORPH_OPEN if op == 'open' else cv2.MORPH_CLOSE
208
+ return ops[op](image, flag, kernel)
209
+ return ops[op](image, kernel)
210
+
211
+ def sharpen(self, image: np.ndarray) -> np.ndarray:
212
+ kernel = np.array([[0, -1, 0],
213
+ [-1, 5, -1],
214
+ [0, -1, 0]])
215
+ return cv2.filter2D(image, -1, kernel)
216
+
217
+ def hough_lines(self, image: np.ndarray) -> np.ndarray:
218
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
219
+ edges = cv2.Canny(gray, 50, 150)
220
+ lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=50,
221
+ minLineLength=50, maxLineGap=10)
222
+ if lines is not None:
223
+ for x1, y1, x2, y2 in lines[:,0]:
224
+ cv2.line(image, (x1, y1), (x2, y2), (0, 0, 255), 2)
225
+ return image
226
+
227
+ def hough_circles(self, image: np.ndarray) -> np.ndarray:
228
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
229
+ circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1.2,
230
+ minDist=50, param1=50, param2=30,
231
+ minRadius=5, maxRadius=100)
232
+ if circles is not None:
233
+ circles = np.uint16(np.around(circles))
234
+ for x, y, r in circles[0, :]:
235
+ cv2.circle(image, (x, y), r, (0, 255, 0), 2)
236
+ return image
237
+
238
+ def optical_flow(self, prev_gray: np.ndarray, curr_gray: np.ndarray, image: np.ndarray) -> np.ndarray:
239
+ flow = cv2.calcOpticalFlowFarneback(prev_gray, curr_gray, None,
240
+ 0.5, 3, 15, 3, 5, 1.2, 0)
241
+ mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])
242
+ hsv = np.zeros_like(image)
243
+ hsv[...,1] = 255
244
+ hsv[...,0] = ang * 180 / np.pi / 2
245
+ hsv[...,2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
246
+ return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
src/streamlit_app.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import av
2
+ import cv2
3
+ import numpy as np
4
+ import streamlit as st
5
+ from streamlit_webrtc import webrtc_streamer
6
+ from opencv_utils import OpenCVUtils
7
+
8
+ st.set_page_config(page_title="OpenCV Explorer", page_icon="🎨", layout="wide")
9
+
10
+
11
+ @st.cache_resource
12
+ def get_app():
13
+ return OpenCVUtils()
14
+
15
+
16
+ app = get_app()
17
+
18
+ # --- HIDE STREAMLIT STYLE ---
19
+ hide_st_style = """
20
+ <style>
21
+ #MainMenu {visibility: hidden;}
22
+ footer {visibility: hidden;}
23
+ header {visibility: hidden;}
24
+ </style>
25
+ """
26
+ st.markdown(hide_st_style, unsafe_allow_html=True)
27
+ # ---------------------------
28
+
29
+ st.markdown("# 🎨 OpenCV Explorer")
30
+ st.markdown("Explore filters and transformations in real-time using your webcam.")
31
+
32
+ # Sidebar Controls
33
+ FUNCTIONS = [
34
+ "Color Filter",
35
+ "Canny",
36
+ "Blur",
37
+ "Rotation",
38
+ "Resize",
39
+ "Contour",
40
+ "Histogram Equalization",
41
+ "Adaptive Threshold",
42
+ "Morphology",
43
+ "Sharpen",
44
+ "Hough Lines",
45
+ "Optical Flow",
46
+ "Pencil Sketch",
47
+ "Color Quantization",
48
+ "Hand Tracker",
49
+ "Face Tracker",
50
+ ]
51
+ selected_functions = st.sidebar.multiselect(
52
+ "Select and order functions:", FUNCTIONS, default=[]
53
+ )
54
+ # Parameters
55
+ with st.sidebar.expander("Color Filter"):
56
+ lh = st.slider("Lower Hue", 0, 180, 0)
57
+ uh = st.slider("Upper Hue", 0, 180, 180)
58
+ ls = st.slider("Lower Sat", 0, 255, 0)
59
+ us = st.slider("Upper Sat", 0, 255, 255)
60
+ lv = st.slider("Lower Val", 0, 255, 0)
61
+ uv = st.slider("Upper Val", 0, 255, 255)
62
+ with st.sidebar.expander("Canny Edge"):
63
+ lc = st.slider("Lower Canny", 0, 255, 100)
64
+ uc = st.slider("Upper Canny", 0, 255, 200)
65
+ with st.sidebar.expander("Blur"):
66
+ bk = st.slider("Kernel Size (odd)", 1, 15, 5, step=2)
67
+ with st.sidebar.expander("Rotation"):
68
+ ang = st.slider("Angle", 0, 360, 0)
69
+ with st.sidebar.expander("Resize"):
70
+ w = st.slider("Width", 100, 1920, 640)
71
+ h = st.slider("Height", 100, 1080, 480)
72
+ with st.sidebar.expander("Morphology"):
73
+ morph_op = st.selectbox("Operation", ["erode", "dilate", "open", "close"])
74
+ morph_ks = st.slider("Kernel Size", 1, 31, 5, step=2)
75
+
76
+ prev_gray = None
77
+
78
+
79
+ def video_frame_callback(frame: av.VideoFrame) -> av.VideoFrame:
80
+ global prev_gray
81
+ img = frame.to_ndarray(format="bgr24")
82
+ curr_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
83
+
84
+ for fn in selected_functions:
85
+ if fn == "Color Filter":
86
+ img = app.apply_color_filter(img, (lh, ls, lv), (uh, us, uv))
87
+ elif fn == "Canny":
88
+ img = app.apply_edge_detection(img, lc, uc)
89
+ elif fn == "Blur":
90
+ img = app.blur_image(img, bk)
91
+ elif fn == "Rotation":
92
+ img = app.rotate_image(img, ang)
93
+ elif fn == "Resize":
94
+ img = app.resize_image(img, w, h)
95
+ elif fn == "Contour":
96
+ img = app.apply_contour_detection(img)
97
+ elif fn == "Histogram Equalization":
98
+ img = app.equalize_histogram(img)
99
+ elif fn == "Adaptive Threshold":
100
+ img = app.adaptive_threshold(img)
101
+ elif fn == "Morphology":
102
+ img = app.morphology(img, morph_op, morph_ks)
103
+ elif fn == "Sharpen":
104
+ img = app.sharpen(img)
105
+ elif fn == "Hough Lines":
106
+ img = app.hough_lines(img)
107
+ elif fn == "Optical Flow" and prev_gray is not None:
108
+ img = app.optical_flow(prev_gray, curr_gray, img)
109
+ elif fn == "Pencil Sketch":
110
+ img = app.pencil_sketch(img)
111
+ elif fn == "Color Quantization":
112
+ img = app.color_quantization(img)
113
+ elif fn == "Hand Tracker":
114
+ img = app.detect_hands(img)
115
+ elif fn == "Face Tracker":
116
+ img = app.detect_faces(img)
117
+
118
+ prev_gray = curr_gray
119
+ return av.VideoFrame.from_ndarray(img, format="bgr24")
120
+
121
+
122
+ webrtc_streamer(
123
+ key="opencv-explorer",
124
+ video_frame_callback=video_frame_callback,
125
+ media_stream_constraints={"video": True, "audio": False},
126
+ async_processing=True,
127
+ )
src/tkinter_app.py ADDED
@@ -0,0 +1,713 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import time
4
+
5
+ from tkinter import *
6
+ from tkinter import ttk
7
+
8
+ from PIL import Image, ImageTk
9
+
10
+ from opencv_utils import OpenCVUtils
11
+
12
+
13
+ class MainWindow:
14
+ def __init__(self, root: Tk) -> None:
15
+ self.root = root
16
+
17
+ self.font = ("Arial", 12, "bold")
18
+ self.font_small = ("Arial", 10, "bold")
19
+
20
+ self.colors = {
21
+ "yellow": "#FDCE01",
22
+ "black": "#1E1E1E",
23
+ "white": "#FEFEFE",
24
+ }
25
+
26
+ self.congig_interface()
27
+
28
+ self.root.bind("<q>", self.close_application)
29
+
30
+ self.functions = []
31
+ self.aplication = OpenCVUtils()
32
+ self.fps_avg_frame_count = 30
33
+
34
+ self.COUNTER, self.FPS = 0, 0
35
+ self.START_TIME = time.time()
36
+
37
+ # For optical flow
38
+ self.prev_gray = None
39
+
40
+ def close_application(self, event) -> None:
41
+ """
42
+ Close the application
43
+
44
+ :param event: The event that triggered the function
45
+ """
46
+ # Libera a webcam e destrói todas as janelas do OpenCV
47
+ self.cap.release()
48
+ cv2.destroyAllWindows()
49
+ self.root.destroy()
50
+
51
+ def congig_interface(self) -> None:
52
+ self.root.geometry("1500x1000")
53
+ self.root.title("OpenCV + Tkinter")
54
+ self.root.config(bg=self.colors["black"])
55
+
56
+ self.paned_window = PanedWindow(self.root, orient=HORIZONTAL)
57
+ self.paned_window.pack(fill=BOTH, expand=1)
58
+
59
+ # Cria a barra lateral com os sliders
60
+ self.sidebar = Frame(
61
+ self.paned_window,
62
+ width=700,
63
+ bg=self.colors["black"],
64
+ background=self.colors["black"],
65
+ padx=10,
66
+ pady=10,
67
+ )
68
+ self.paned_window.add(self.sidebar)
69
+
70
+ # Create a scrollbar for the sidebar
71
+ canvas = Canvas(self.sidebar, bg=self.colors["black"], highlightthickness=0)
72
+ scrollbar = Scrollbar(self.sidebar, orient="vertical", command=canvas.yview)
73
+ scrollable_frame = Frame(
74
+ canvas,
75
+ bg=self.colors["black"],
76
+ )
77
+
78
+ scrollable_frame.bind(
79
+ "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
80
+ )
81
+
82
+ canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
83
+ canvas.configure(yscrollcommand=scrollbar.set)
84
+
85
+ canvas.pack(side="left", fill="both", expand=True)
86
+ scrollbar.pack(side="right", fill="y")
87
+
88
+ # Cria as trackbars
89
+ self.color_filter_var = IntVar()
90
+ self.color_filter_var.trace_add(
91
+ "write",
92
+ lambda *args: self.add_function(
93
+ self.aplication.apply_color_filter, self.color_filter_var
94
+ ),
95
+ )
96
+ Checkbutton(
97
+ scrollable_frame,
98
+ text="Color Filter",
99
+ variable=self.color_filter_var,
100
+ font=self.font,
101
+ bg=self.colors["black"],
102
+ fg=self.colors["white"],
103
+ highlightbackground=self.colors["black"],
104
+ selectcolor=self.colors["black"],
105
+ ).pack()
106
+
107
+ self.lower_hue = Scale(
108
+ scrollable_frame,
109
+ from_=0,
110
+ to=180,
111
+ orient=HORIZONTAL,
112
+ label="Lower Hue",
113
+ bg=self.colors["black"],
114
+ fg=self.colors["white"],
115
+ highlightbackground=self.colors["black"],
116
+ )
117
+ self.lower_hue.pack(anchor="center")
118
+ self.upper_hue = Scale(
119
+ scrollable_frame,
120
+ from_=0,
121
+ to=180,
122
+ orient=HORIZONTAL,
123
+ label="Upper Hue",
124
+ bg=self.colors["black"],
125
+ fg=self.colors["white"],
126
+ highlightbackground=self.colors["black"],
127
+ )
128
+ self.upper_hue.pack(anchor="center")
129
+
130
+ self.lower_saturation = Scale(
131
+ scrollable_frame,
132
+ from_=0,
133
+ to=255,
134
+ orient=HORIZONTAL,
135
+ label="Lower Sat",
136
+ bg=self.colors["black"],
137
+ fg=self.colors["white"],
138
+ highlightbackground=self.colors["black"],
139
+ )
140
+ self.lower_saturation.pack(anchor="center")
141
+ self.upper_saturation = Scale(
142
+ scrollable_frame,
143
+ from_=0,
144
+ to=255,
145
+ orient=HORIZONTAL,
146
+ label="Upper Sat",
147
+ bg=self.colors["black"],
148
+ fg=self.colors["white"],
149
+ highlightbackground=self.colors["black"],
150
+ )
151
+ self.upper_saturation.pack(anchor="center")
152
+
153
+ self.lower_value = Scale(
154
+ scrollable_frame,
155
+ from_=0,
156
+ to=255,
157
+ orient=HORIZONTAL,
158
+ label="Lower Value",
159
+ bg=self.colors["black"],
160
+ fg=self.colors["white"],
161
+ highlightbackground=self.colors["black"],
162
+ )
163
+ self.lower_value.pack(anchor="center")
164
+ self.upper_value = Scale(
165
+ scrollable_frame,
166
+ from_=0,
167
+ to=255,
168
+ orient=HORIZONTAL,
169
+ label="Upper Value",
170
+ bg=self.colors["black"],
171
+ fg=self.colors["white"],
172
+ highlightbackground=self.colors["black"],
173
+ )
174
+ self.upper_value.pack(anchor="center")
175
+
176
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
177
+
178
+ self.canny_var = IntVar()
179
+ self.canny_var.trace_add(
180
+ "write",
181
+ lambda *args: self.add_function(
182
+ self.aplication.apply_edge_detection, self.canny_var
183
+ ),
184
+ )
185
+ Checkbutton(
186
+ scrollable_frame,
187
+ text="Canny",
188
+ variable=self.canny_var,
189
+ font=self.font,
190
+ bg=self.colors["black"],
191
+ fg=self.colors["white"],
192
+ highlightbackground=self.colors["black"],
193
+ selectcolor=self.colors["black"],
194
+ ).pack()
195
+
196
+ self.lower_canny = Scale(
197
+ scrollable_frame,
198
+ from_=0,
199
+ to=255,
200
+ orient=HORIZONTAL,
201
+ label="Lower Canny",
202
+ bg=self.colors["black"],
203
+ fg=self.colors["white"],
204
+ highlightbackground=self.colors["black"],
205
+ )
206
+ self.lower_canny.pack(anchor="center")
207
+ self.upper_canny = Scale(
208
+ scrollable_frame,
209
+ from_=0,
210
+ to=255,
211
+ orient=HORIZONTAL,
212
+ label="Upper Canny",
213
+ bg=self.colors["black"],
214
+ fg=self.colors["white"],
215
+ highlightbackground=self.colors["black"],
216
+ )
217
+ self.upper_canny.pack(anchor="center")
218
+
219
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
220
+
221
+ self.blur_var = IntVar()
222
+ self.blur_var.trace_add(
223
+ "write",
224
+ lambda *args: self.add_function(self.aplication.blur_image, self.blur_var),
225
+ )
226
+ Checkbutton(
227
+ scrollable_frame,
228
+ text="Blur",
229
+ variable=self.blur_var,
230
+ font=self.font,
231
+ bg=self.colors["black"],
232
+ fg=self.colors["white"],
233
+ highlightbackground=self.colors["black"],
234
+ selectcolor=self.colors["black"],
235
+ ).pack(anchor="center")
236
+
237
+ self.blur = Scale(
238
+ scrollable_frame,
239
+ from_=1,
240
+ to=15,
241
+ orient=HORIZONTAL,
242
+ bg=self.colors["black"],
243
+ fg=self.colors["white"],
244
+ highlightbackground=self.colors["black"],
245
+ )
246
+ self.blur.pack(anchor="center")
247
+
248
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
249
+
250
+ self.rotation_var = IntVar()
251
+ self.rotation_var.trace_add(
252
+ "write",
253
+ lambda *args: self.add_function(
254
+ self.aplication.rotate_image, self.rotation_var
255
+ ),
256
+ )
257
+ Checkbutton(
258
+ scrollable_frame,
259
+ text="Rotation",
260
+ variable=self.rotation_var,
261
+ font=self.font,
262
+ bg=self.colors["black"],
263
+ fg=self.colors["white"],
264
+ highlightbackground=self.colors["black"],
265
+ selectcolor=self.colors["black"],
266
+ ).pack(anchor="center")
267
+
268
+ self.rotation_angle = Scale(
269
+ scrollable_frame,
270
+ from_=0,
271
+ to=360,
272
+ orient=HORIZONTAL,
273
+ label="Rotation Angle",
274
+ bg=self.colors["black"],
275
+ fg=self.colors["white"],
276
+ highlightbackground=self.colors["black"],
277
+ )
278
+ self.rotation_angle.pack(anchor="center")
279
+
280
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
281
+
282
+ self.resize_var = IntVar()
283
+ self.resize_var.trace_add(
284
+ "write",
285
+ lambda *args: self.add_function(
286
+ self.aplication.resize_image, self.resize_var
287
+ ),
288
+ )
289
+ Checkbutton(
290
+ scrollable_frame,
291
+ text="Resize",
292
+ variable=self.resize_var,
293
+ font=self.font,
294
+ bg=self.colors["black"],
295
+ fg=self.colors["white"],
296
+ highlightbackground=self.colors["black"],
297
+ selectcolor=self.colors["black"],
298
+ ).pack()
299
+
300
+ Label(
301
+ scrollable_frame,
302
+ text="Height",
303
+ bg=self.colors["black"],
304
+ fg=self.colors["white"],
305
+ ).pack()
306
+ self.height = Scale(
307
+ scrollable_frame,
308
+ from_=100,
309
+ to=1080,
310
+ orient=HORIZONTAL,
311
+ bg=self.colors["black"],
312
+ fg=self.colors["white"],
313
+ highlightbackground=self.colors["black"],
314
+ )
315
+ self.height.pack(anchor="center")
316
+ self.width = Scale(
317
+ scrollable_frame,
318
+ from_=100,
319
+ to=1920,
320
+ orient=HORIZONTAL,
321
+ label="Width",
322
+ bg=self.colors["black"],
323
+ fg=self.colors["white"],
324
+ highlightbackground=self.colors["black"],
325
+ )
326
+ self.width.pack(anchor="center")
327
+
328
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
329
+
330
+ self.contour_var = IntVar()
331
+ self.contour_var.trace_add(
332
+ "write",
333
+ lambda *args: self.add_function(
334
+ self.aplication.apply_contour_detection, self.contour_var
335
+ ),
336
+ )
337
+ Checkbutton(
338
+ scrollable_frame,
339
+ text="Contour",
340
+ variable=self.contour_var,
341
+ font=self.font,
342
+ bg=self.colors["black"],
343
+ fg=self.colors["white"],
344
+ highlightbackground=self.colors["black"],
345
+ selectcolor=self.colors["black"],
346
+ ).pack()
347
+
348
+ # Add new OpenCV functions
349
+
350
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
351
+
352
+ self.hist_equal_var = IntVar()
353
+ self.hist_equal_var.trace_add(
354
+ "write",
355
+ lambda *args: self.add_function(
356
+ self.aplication.equalize_histogram, self.hist_equal_var
357
+ ),
358
+ )
359
+ Checkbutton(
360
+ scrollable_frame,
361
+ text="Histogram Equalization",
362
+ variable=self.hist_equal_var,
363
+ font=self.font,
364
+ bg=self.colors["black"],
365
+ fg=self.colors["white"],
366
+ highlightbackground=self.colors["black"],
367
+ selectcolor=self.colors["black"],
368
+ ).pack()
369
+
370
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
371
+
372
+ self.adaptive_threshold_var = IntVar()
373
+ self.adaptive_threshold_var.trace_add(
374
+ "write",
375
+ lambda *args: self.add_function(
376
+ self.aplication.adaptive_threshold, self.adaptive_threshold_var
377
+ ),
378
+ )
379
+ Checkbutton(
380
+ scrollable_frame,
381
+ text="Adaptive Threshold",
382
+ variable=self.adaptive_threshold_var,
383
+ font=self.font,
384
+ bg=self.colors["black"],
385
+ fg=self.colors["white"],
386
+ highlightbackground=self.colors["black"],
387
+ selectcolor=self.colors["black"],
388
+ ).pack()
389
+
390
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
391
+
392
+ self.morphology_var = IntVar()
393
+ self.morphology_var.trace_add(
394
+ "write",
395
+ lambda *args: self.add_function(
396
+ self.aplication.morphology, self.morphology_var
397
+ ),
398
+ )
399
+ Checkbutton(
400
+ scrollable_frame,
401
+ text="Morphology",
402
+ variable=self.morphology_var,
403
+ font=self.font,
404
+ bg=self.colors["black"],
405
+ fg=self.colors["white"],
406
+ highlightbackground=self.colors["black"],
407
+ selectcolor=self.colors["black"],
408
+ ).pack()
409
+
410
+ # Morphology operation options
411
+ self.morph_op_var = StringVar(value="erode")
412
+ Label(
413
+ scrollable_frame,
414
+ text="Operation",
415
+ bg=self.colors["black"],
416
+ fg=self.colors["white"],
417
+ ).pack()
418
+
419
+ for op in ["erode", "dilate", "open", "close"]:
420
+ Radiobutton(
421
+ scrollable_frame,
422
+ text=op.capitalize(),
423
+ variable=self.morph_op_var,
424
+ value=op,
425
+ bg=self.colors["black"],
426
+ fg=self.colors["white"],
427
+ selectcolor=self.colors["black"],
428
+ highlightbackground=self.colors["black"],
429
+ ).pack(anchor="w")
430
+
431
+ self.morph_kernel_size = Scale(
432
+ scrollable_frame,
433
+ from_=1,
434
+ to=31,
435
+ orient=HORIZONTAL,
436
+ label="Kernel Size",
437
+ bg=self.colors["black"],
438
+ fg=self.colors["white"],
439
+ highlightbackground=self.colors["black"],
440
+ )
441
+ self.morph_kernel_size.set(5)
442
+ self.morph_kernel_size.pack(anchor="center")
443
+
444
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
445
+
446
+ self.sharpen_var = IntVar()
447
+ self.sharpen_var.trace_add(
448
+ "write",
449
+ lambda *args: self.add_function(self.aplication.sharpen, self.sharpen_var),
450
+ )
451
+ Checkbutton(
452
+ scrollable_frame,
453
+ text="Sharpen",
454
+ variable=self.sharpen_var,
455
+ font=self.font,
456
+ bg=self.colors["black"],
457
+ fg=self.colors["white"],
458
+ highlightbackground=self.colors["black"],
459
+ selectcolor=self.colors["black"],
460
+ ).pack()
461
+
462
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
463
+
464
+ self.hough_lines_var = IntVar()
465
+ self.hough_lines_var.trace_add(
466
+ "write",
467
+ lambda *args: self.add_function(
468
+ self.aplication.hough_lines, self.hough_lines_var
469
+ ),
470
+ )
471
+ Checkbutton(
472
+ scrollable_frame,
473
+ text="Hough Lines",
474
+ variable=self.hough_lines_var,
475
+ font=self.font,
476
+ bg=self.colors["black"],
477
+ fg=self.colors["white"],
478
+ highlightbackground=self.colors["black"],
479
+ selectcolor=self.colors["black"],
480
+ ).pack()
481
+
482
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
483
+
484
+ self.optical_flow_var = IntVar()
485
+ self.optical_flow_var.trace_add(
486
+ "write",
487
+ lambda *args: self.add_function(
488
+ self.process_optical_flow, self.optical_flow_var
489
+ ),
490
+ )
491
+ Checkbutton(
492
+ scrollable_frame,
493
+ text="Optical Flow",
494
+ variable=self.optical_flow_var,
495
+ font=self.font,
496
+ bg=self.colors["black"],
497
+ fg=self.colors["white"],
498
+ highlightbackground=self.colors["black"],
499
+ selectcolor=self.colors["black"],
500
+ ).pack()
501
+
502
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
503
+
504
+ self.pencil_sketch_var = IntVar()
505
+ self.pencil_sketch_var.trace_add(
506
+ "write",
507
+ lambda *args: self.add_function(
508
+ self.aplication.pencil_sketch, self.pencil_sketch_var
509
+ ),
510
+ )
511
+ Checkbutton(
512
+ scrollable_frame,
513
+ text="Pencil Sketch",
514
+ variable=self.pencil_sketch_var,
515
+ font=self.font,
516
+ bg=self.colors["black"],
517
+ fg=self.colors["white"],
518
+ highlightbackground=self.colors["black"],
519
+ selectcolor=self.colors["black"],
520
+ ).pack()
521
+
522
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
523
+
524
+ self.color_quantization_var = IntVar()
525
+ self.color_quantization_var.trace_add(
526
+ "write",
527
+ lambda *args: self.add_function(
528
+ self.aplication.color_quantization, self.color_quantization_var
529
+ ),
530
+ )
531
+ Checkbutton(
532
+ scrollable_frame,
533
+ text="Color Quantization",
534
+ variable=self.color_quantization_var,
535
+ font=self.font,
536
+ bg=self.colors["black"],
537
+ fg=self.colors["white"],
538
+ highlightbackground=self.colors["black"],
539
+ selectcolor=self.colors["black"],
540
+ ).pack()
541
+
542
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
543
+
544
+ self.hand_tracker_var = IntVar()
545
+ self.hand_tracker_var.trace_add(
546
+ "write",
547
+ lambda *args: self.add_function(
548
+ self.aplication.detect_hands, self.hand_tracker_var
549
+ ),
550
+ )
551
+ Checkbutton(
552
+ scrollable_frame,
553
+ text="Hand Tracker",
554
+ variable=self.hand_tracker_var,
555
+ font=self.font,
556
+ bg=self.colors["black"],
557
+ fg=self.colors["white"],
558
+ highlightbackground=self.colors["black"],
559
+ selectcolor=self.colors["black"],
560
+ ).pack()
561
+
562
+ ttk.Separator(scrollable_frame, orient=HORIZONTAL).pack(fill=X, padx=3, pady=3)
563
+
564
+ self.face_tracker_var = IntVar()
565
+ self.face_tracker_var.trace_add(
566
+ "write",
567
+ lambda *args: self.add_function(
568
+ self.aplication.detect_faces, self.face_tracker_var
569
+ ),
570
+ )
571
+ Checkbutton(
572
+ scrollable_frame,
573
+ text="Face Tracker",
574
+ variable=self.face_tracker_var,
575
+ font=self.font,
576
+ bg=self.colors["black"],
577
+ fg=self.colors["white"],
578
+ highlightbackground=self.colors["black"],
579
+ selectcolor=self.colors["black"],
580
+ ).pack()
581
+
582
+ # Cria o label para exibir a imagem
583
+ self.image_label = Label(self.paned_window, bg=self.colors["black"])
584
+ self.paned_window.add(self.image_label)
585
+
586
+ def add_function(self, function: callable, var: IntVar) -> None:
587
+ """
588
+ Add or remove a function from the list of functions to be applied to the image
589
+
590
+ :param function: The function to be added or removed
591
+ :param var: The variable that controls the function
592
+ """
593
+ if var.get() == 1:
594
+ self.functions.append(function)
595
+ else:
596
+ self.functions.remove(function)
597
+
598
+ def process_optical_flow(self, frame: np.ndarray) -> np.ndarray:
599
+ """
600
+ Special handler for optical flow which needs to track previous frames
601
+
602
+ :param frame: The current frame
603
+ :return: The processed frame with optical flow
604
+ """
605
+ curr_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
606
+
607
+ if self.prev_gray is not None:
608
+ frame = self.aplication.optical_flow(self.prev_gray, curr_gray, frame)
609
+
610
+ self.prev_gray = curr_gray
611
+ return frame
612
+
613
+ def process_image(self, frame: np.ndarray) -> np.ndarray:
614
+ """
615
+ Process the image with the functions selected by the user
616
+
617
+ :param frame: The image to be processed
618
+ :return: The processed image
619
+ """
620
+ function_dict = {
621
+ self.aplication.apply_color_filter: [
622
+ (
623
+ self.lower_hue.get(),
624
+ self.lower_saturation.get(),
625
+ self.lower_value.get(),
626
+ ),
627
+ (
628
+ self.upper_hue.get(),
629
+ self.upper_saturation.get(),
630
+ self.upper_value.get(),
631
+ ),
632
+ ],
633
+ self.aplication.apply_edge_detection: [
634
+ self.lower_canny.get(),
635
+ self.upper_canny.get(),
636
+ ],
637
+ self.aplication.blur_image: [self.blur.get()],
638
+ self.aplication.rotate_image: [self.rotation_angle.get()],
639
+ self.aplication.resize_image: [self.width.get(), self.height.get()],
640
+ self.aplication.morphology: [
641
+ self.morph_op_var.get(),
642
+ self.morph_kernel_size.get(),
643
+ ],
644
+ }
645
+
646
+ for function in self.functions:
647
+ args = function_dict.get(function, [])
648
+ frame = function(frame, *args)
649
+
650
+ return frame
651
+
652
+ def run(self) -> None:
653
+ """
654
+ Run the main loop of the tkinter application
655
+ """
656
+ # Abre a webcam
657
+ self.cap = cv2.VideoCapture(0)
658
+ self.START_TIME = time.time()
659
+ while True:
660
+ # Lê um frame da webcam
661
+ ret, frame = self.cap.read()
662
+ if not ret:
663
+ break
664
+
665
+ # Aplica as funções do OpenCV
666
+ frame = self.process_image(frame)
667
+
668
+ output = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
669
+
670
+ if self.COUNTER % self.fps_avg_frame_count == 0:
671
+ self.FPS = self.fps_avg_frame_count / (time.time() - self.START_TIME)
672
+ self.START_TIME = time.time()
673
+ self.COUNTER += 1
674
+
675
+ # Show the FPS
676
+ fps_text = "FPS = {:.1f}".format(self.FPS)
677
+
678
+ cv2.putText(
679
+ output,
680
+ fps_text,
681
+ (24, 30),
682
+ cv2.FONT_HERSHEY_DUPLEX,
683
+ 1,
684
+ (0, 0, 0),
685
+ 1,
686
+ cv2.LINE_AA,
687
+ )
688
+
689
+ # Converte a imagem NumPy para uma imagem PIL
690
+ pil_image = Image.fromarray(output)
691
+
692
+ # Converte a imagem PIL para uma imagem Tkinter
693
+ tk_image = ImageTk.PhotoImage(pil_image)
694
+
695
+ # Exibe a imagem no label
696
+ self.image_label.config(image=tk_image)
697
+ self.image_label.image = tk_image
698
+
699
+ # Atualiza a janela tkinter
700
+ self.root.update()
701
+
702
+ cv2.waitKey(1)
703
+
704
+
705
+ def main():
706
+ # Cria a janela principal
707
+ root = Tk()
708
+ main_window = MainWindow(root)
709
+ main_window.run()
710
+
711
+
712
+ if __name__ == "__main__":
713
+ main()