├── dishsnitch1.png ├── dishsnitch4.png ├── dishsnitch5.png ├── dishsnitch8.png ├── no_one_here.mp3 ├── found_someone.mp3 ├── very_good_proud_of_you.mp3 ├── you_left_dirty_dishes.mp3 ├── requirements.txt ├── global_vars.py ├── config.py ├── part1_capture_image.py ├── LICENSE ├── part3_notify.py ├── README.md ├── .gitignore ├── part2_detect_dishes.py └── main_loop.py /dishsnitch1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/dishsnitch1.png -------------------------------------------------------------------------------- /dishsnitch4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/dishsnitch4.png -------------------------------------------------------------------------------- /dishsnitch5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/dishsnitch5.png -------------------------------------------------------------------------------- /dishsnitch8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/dishsnitch8.png -------------------------------------------------------------------------------- /no_one_here.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/no_one_here.mp3 -------------------------------------------------------------------------------- /found_someone.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/found_someone.mp3 -------------------------------------------------------------------------------- /very_good_proud_of_you.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/very_good_proud_of_you.mp3 -------------------------------------------------------------------------------- /you_left_dirty_dishes.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panorays/DishSnitch/HEAD/you_left_dirty_dishes.mp3 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | face_recognition 2 | opencv-python 3 | playsound 4 | pygame 5 | pymongo 6 | requests 7 | slacker 8 | -------------------------------------------------------------------------------- /global_vars.py: -------------------------------------------------------------------------------- 1 | global current_sink_status 2 | current_sink_status = "started_clean" 3 | global current_suspect 4 | current_suspect = "john doe" 5 | 6 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | saved_images_dir = './saved_images/' 2 | person_camera_index = 0 # Index of the camera to take photos of people 3 | sink_camera_index = 1 # Index of the camera pointing at the sink 4 | 5 | slack_token = '' 6 | slack_channel = '#dishsnitch' 7 | 8 | found_someone_mp3_path = 'found_someone.mp3' 9 | left_dirty_dishes_mp3_path = 'you_left_dirty_dishes.mp3' 10 | cleaned_dishes_mp3_path = 'very_good_proud_of_you.mp3' 11 | 12 | # MongoDB settings 13 | mongo_host = 'localhost' 14 | mongo_port = 27017 15 | -------------------------------------------------------------------------------- /part1_capture_image.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import cv2 4 | 5 | import config 6 | 7 | 8 | def take_sink_photo(): 9 | try: 10 | webcam = cv2.VideoCapture(config.sink_camera_index) 11 | check, frame = webcam.read() 12 | cv2.imshow("Capturing", frame) 13 | cv2.imwrite(os.path.join(config.saved_images_dir , 'saved_img.jpg'), img=frame) 14 | webcam.release() 15 | except: 16 | print("Turning off camera.") 17 | webcam.release() 18 | print("Camera off.") 19 | print("Program ended.") 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Panorays 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /part3_notify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Post slack message.""" 3 | 4 | import os 5 | 6 | import requests 7 | from slacker import Slacker 8 | 9 | import config 10 | import global_vars 11 | 12 | # https://github.com/os/slacker 13 | # https://api.slack.com/methods 14 | 15 | 16 | def post_slack(bad_guy): 17 | """Post slack message.""" 18 | slack = Slacker(config.slack_token) 19 | 20 | obj = slack.chat.post_message(config.slack_channel, ' '.join([bad_guy, 'left dirty dishes today!'])) 21 | print(obj.successful, obj.__dict__['body']['channel'], obj.__dict__['body']['ts']) 22 | 23 | dirty_dishes_image_path = os.path.join(config.saved_images_dir, 'saved_img.jpg') 24 | my_file = { 25 | 'file': (dirty_dishes_image_path, open(dirty_dishes_image_path, 'rb'), 'jpg') 26 | } 27 | 28 | payload = { 29 | "filename": "proof.jpg", 30 | "token": config.slack_token, 31 | "channels": [config.slack_channel], 32 | } 33 | 34 | r = requests.post("https://slack.com/api/files.upload", params=payload, files=my_file) 35 | 36 | 37 | if __name__ == '__main__': 38 | post_slack() 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DishSnitch 2 | 3 | DishSnitch: The "Who Left Dirty Dishes in the Sink" Detector 4 | By: Elad Shapira, Stephan Gross & Yarden Tzemach 5 | 6 | Dirty dishes! Our team is dealing with an abundance of those. Argh! 7 | 8 | Which brought our researchers to develop a DishSnitch. 9 | Yeah, an automated app that detects and shames the culprit by sending the full evidence—including pics—to the company’s dedicated Slack. 10 | **Face palm** 11 | 12 | For those who are interested in the DishSnitch, we placed the app’s files free to grab on Github—you’ll find the link at the end of the post. 13 | 14 | ![alt text](https://github.com/panorays/DishSnitch/blob/master/dishsnitch1.png "") 15 | 16 | Introduction 17 | 18 | This project was the culmination of an exciting two-day hackathon at Panorays, Panackathon. 19 | The idea was that teams work on a dedicated project related to the company, under the theme of automation. 20 | Considering that the dishes take up nerves, time and, well, smell bad, for the benefit of the company, we just had to create this DishSnitch. 21 | Together with me, this app was developed by Stephan Gross and Yarden Zemach, members of the Security team. 22 | (Necessary pat-on-the- back note: We actually developed in these two days two separate unrelated automation projects. 23 | The other project we worked on will be integrated within the Panorays platform shortly. Exciting times!) 24 | 25 | ![alt text](https://github.com/panorays/DishSnitch/blob/master/dishsnitch4.png "") 26 | ![alt text](https://github.com/panorays/DishSnitch/blob/master/dishsnitch5.png "") 27 | ![alt text](https://github.com/panorays/DishSnitch/blob/master/dishsnitch8.png "") 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /part2_detect_dishes.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import time 4 | 5 | import cv2 6 | import numpy as np 7 | from playsound import playsound 8 | import pygame as pg 9 | 10 | import config 11 | import global_vars 12 | 13 | ## Define our config values 14 | 15 | # What is our min dish count to alarm on? 16 | min_dishes = 1 17 | 18 | # Define areas we want to ignore 19 | # First value is the x range, second is the y range 20 | ignore_list = ["339-345,257-260"] 21 | 22 | # Set our timestamp 23 | time_stamp = time.strftime("%Y%m%d%H%M%S") 24 | 25 | # Set our circle detection variables 26 | circle_sensitivity = 40 # Larger numbers increase false positives 27 | #circle_sensitivity = 60 # Larger numbers increase false positives 28 | 29 | min_rad = 30 # Tweak this if you're detecting circles that are too small 30 | max_rad = 75 # Tweak if you're detecting circles that are too big (Ie: round sinks) 31 | 32 | # Cropping the image allows us to only process areas of the image 33 | # that should have images. Set our crop values 34 | crop_left = 0 35 | crop_right = 360 36 | crop_top = 150 37 | crop_bottom = 850 38 | 39 | 40 | def should_ignore(ignore_list, x, y): 41 | # Loop through our ignore_list and check for this x/y 42 | ignore = False 43 | for range in ignore_list: 44 | x_range = range.split(',')[0] 45 | y_range = range.split(',')[1] 46 | x_min = int(x_range.split('-')[0]) 47 | x_max = int(x_range.split('-')[1]) 48 | y_min = int(y_range.split('-')[0]) 49 | y_max = int(y_range.split('-')[1]) 50 | 51 | if (x >= x_min and x <= x_max and y >= y_min and y <= y_max): 52 | ignore = True 53 | 54 | return ignore 55 | 56 | 57 | def play_music(music_file, volume=0.8): 58 | ''' 59 | stream music with mixer.music module in a blocking manner 60 | this will stream the sound from disk while playing 61 | ''' 62 | # set up the mixer 63 | freq = 44100 # audio CD quality 64 | bitsize = -16 # unsigned 16 bit 65 | channels = 2 # 1 is mono, 2 is stereo 66 | buffer = 2048 # number of samples (experiment to get best sound) 67 | pg.mixer.init(freq, bitsize, channels, buffer) 68 | # volume value 0.0 to 1.0 69 | pg.mixer.music.set_volume(volume) 70 | clock = pg.time.Clock() 71 | try: 72 | pg.mixer.music.load(music_file) 73 | #print("Music file {} loaded!".format(music_file)) 74 | except pg.error: 75 | print("File {} not found! ({})".format(music_file, pg.get_error())) 76 | return 77 | pg.mixer.music.play() 78 | while pg.mixer.music.get_busy(): 79 | # check if playback has finished 80 | clock.tick(30) 81 | 82 | 83 | def check_if_dishes_exist(): 84 | # Note: Larger images require more processing power and have more false positives 85 | 86 | image_original = cv2.imread(os.path.join(config.saved_images_dir, 'saved_img.jpg')) 87 | 88 | #print("Cropping image to limit processing to just the sink") 89 | image = image_original[crop_left:crop_right, crop_top:crop_bottom] 90 | 91 | #print("Copying image") 92 | output = copy.copy(image) 93 | 94 | #print("Blurring image") 95 | blurred = cv2.GaussianBlur(image, (9, 9), 2, 2) 96 | cv2.imwrite(os.path.join(config.saved_images_dir, 'blurred.jpg'), blurred) 97 | 98 | #print("Converting to grey") 99 | gray = cv2.cvtColor(blurred, cv2.COLOR_BGR2GRAY) 100 | cv2.imwrite(os.path.join(config.saved_images_dir, 'gray.jpg'), gray) 101 | 102 | #print("Detecting circles in blurred and greyed image") 103 | circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 20, 104 | param1=100, 105 | param2=circle_sensitivity, 106 | minRadius=min_rad, 107 | maxRadius=max_rad) 108 | 109 | #print("Checking if we found images") 110 | if circles is not None: 111 | dish_count = 0 112 | print("Dishes Found!") 113 | # convert the (x, y) coordinates and radius of the circles to integers 114 | circles = np.round(circles[0, :]).astype("int") 115 | 116 | # loop over the (x, y) coordinates and radius of the circles 117 | for (x, y, r) in circles: 118 | # draw the circle in the output image, then draw a rectangle 119 | # corresponding to the center of the circle 120 | cv2.circle(output, (x, y), r, (0, 255, 0), 4) 121 | cv2.rectangle(output, (x - 5, y - 5), (x + 5, y + 5), (0, 128, 255), -1) 122 | # Check our ignore_list 123 | if (should_ignore(ignore_list, x, y)): 124 | print("Circle in ignore_list: Ignoring") 125 | else: 126 | dish_count += 1 127 | print("Dish count:%s" % (str(dish_count))) 128 | 129 | cv2.imwrite(os.path.join(config.saved_images_dir, 'detected.jpg'), output) 130 | 131 | if dish_count >= min_dishes: 132 | print("Playing dirty dishes sound..") 133 | global_vars.current_sink_status = "dirty" 134 | # optional volume 0 to 1.0 135 | volume = 1.0 136 | play_music(config.left_dirty_dishes_mp3_path, volume) 137 | else: 138 | print("No Dishes Found!") 139 | global_vars.current_sink_status = "clean" 140 | # optional volume 0 to 1.0 141 | volume = 1.0 142 | play_music(config.cleaned_dishes_mp3_path, volume) 143 | 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /main_loop.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import pprint 4 | import time 5 | 6 | import cv2 7 | import face_recognition 8 | import numpy as np 9 | import pygame as pg 10 | import pymongo 11 | 12 | import global_vars 13 | import config 14 | from part1_capture_image import take_sink_photo 15 | from part2_detect_dishes import check_if_dishes_exist 16 | from part3_notify import post_slack 17 | 18 | 19 | mongo_client = pymongo.MongoClient(host=config.mongo_host, port=config.mongo_port) 20 | targets_db = mongo_client.targets 21 | employees_pull = targets_db.employees 22 | 23 | known_face_encodings = [] 24 | known_face_names = [] 25 | last_sin = {} 26 | # Initialize some variables 27 | face_locations = [] 28 | face_encodings = [] 29 | face_names = [] 30 | process_this_frame = True 31 | total_time = [] 32 | cooldown = 10 # 3600 seconds between detection of targets = 1 hour 33 | 34 | 35 | def play_music(music_file, volume=0.8): 36 | ''' 37 | stream music with mixer.music module in a blocking manner 38 | this will stream the sound from disk while playing 39 | ''' 40 | # set up the mixer 41 | freq = 44100 # audio CD quality 42 | bitsize = -16 # unsigned 16 bit 43 | channels = 2 # 1 is mono, 2 is stereo 44 | buffer = 2048 # number of samples (experiment to get best sound) 45 | pg.mixer.init(freq, bitsize, channels, buffer) 46 | # volume value 0.0 to 1.0 47 | pg.mixer.music.set_volume(volume) 48 | clock = pg.time.Clock() 49 | try: 50 | pg.mixer.music.load(music_file) 51 | print("Music file {} loaded!".format(music_file)) 52 | except pg.error: 53 | print("File {} not found! ({})".format(music_file, pg.get_error())) 54 | return 55 | pg.mixer.music.play() 56 | while pg.mixer.music.get_busy(): 57 | # check if playback has finished 58 | clock.tick(30) 59 | 60 | for employee in employees_pull.distinct("full_name"): 61 | for fetch_info in employees_pull.find({"full_name": employee}): 62 | last_sin[fetch_info.get("full_name")] = (time.time() - cooldown) 63 | known_face_names.append(fetch_info.get("full_name")) 64 | #print ('starting to encode {} '.format(employee)) 65 | start_time = time.time() 66 | known_face_encodings.append( 67 | face_recognition.face_encodings(face_recognition.load_image_file(fetch_info.get("image")))[0]) 68 | end_time = time.time() 69 | finish_time = end_time - start_time 70 | #print ('{} has been encoded and took {} '.format(employee, finish_time)) 71 | total_time.append(finish_time) 72 | print (known_face_names) 73 | print ('total tine for the process {} '.format(round(sum(total_time)))) 74 | 75 | # Get a reference to webcam for capturing people 76 | video_capture = cv2.VideoCapture(config.person_camera_index) 77 | 78 | while True: 79 | # Grab a single frame of video 80 | ret, frame = video_capture.read() 81 | 82 | # Resize frame of video to 1/2 size for faster face recognition processing 83 | small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) 84 | 85 | # Convert the image from BGR color (which OpenCV uses) to RGB color (which face_recognition uses) 86 | rgb_small_frame = small_frame[:, :, ::-1] 87 | 88 | # Only process every other frame of video to save time 89 | if process_this_frame: 90 | # Find all the faces and face encodings in the current frame of video 91 | face_locations = face_recognition.face_locations(rgb_small_frame) 92 | face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations) 93 | face_names = [] 94 | for face_encoding in face_encodings: 95 | # See if the face is a match for the known face(s) 96 | matches = face_recognition.compare_faces(known_face_encodings, face_encoding) 97 | name = "Unknown" 98 | 99 | # # If a match was found in known_face_encodings, just use the first one. 100 | # if True in matches: 101 | # first_match_index = matches.index(True) 102 | # name = known_face_names[first_match_index] 103 | 104 | # Or instead, use the known face with the smallest distance to the new face 105 | face_distances = face_recognition.face_distance(known_face_encodings, face_encoding) 106 | best_match_index = np.argmin(face_distances) 107 | if matches[best_match_index]: 108 | name = known_face_names[best_match_index] 109 | for check in known_face_names: 110 | if check == name: 111 | pprint.pprint("We found you..." + name) 112 | # optional volume 0 to 1.0 113 | volume = 1.0 114 | play_music(config.found_someone_mp3_path, volume) 115 | 116 | if face_distances[best_match_index] < 0.49: 117 | if time.time() > (last_sin[name] + cooldown): 118 | last_sin[name] = time.time() 119 | print ("target found at {} ".format(check) + time.strftime("(%H:%M:%S - %d/%m/%Y)")) 120 | global_vars.current_suspect = name 121 | pprint.pprint("current_suspect after detected:" + global_vars.current_suspect) 122 | 123 | #get sink status 124 | #check_if_dishes_exist() 125 | pprint.pprint("sink status when face detected:" + global_vars.current_sink_status) 126 | pprint.pprint("current suspect when face detected:" + global_vars.current_suspect) 127 | else: 128 | cooldown_remain = ((last_sin[name] + cooldown) - time.time()) 129 | print ('{} you have more {} for cool-down'.format(name, str( 130 | datetime.timedelta(seconds=cooldown_remain)))) 131 | face_names.append(name) 132 | else: 133 | pprint.pprint("unknown person or person left... did someone leave the sink?") 134 | 135 | # take sink photo 136 | pprint.pprint("taking photo of sink..") 137 | take_sink_photo() 138 | 139 | # get sink status 140 | check_if_dishes_exist() 141 | pprint.pprint("sink status with unknwon:" + global_vars.current_sink_status) 142 | pprint.pprint("current suspect with unknwon:" + global_vars.current_suspect) 143 | 144 | if global_vars.current_sink_status == "dirty": 145 | pprint.pprint("Someone left DIRTY dishes!") 146 | pprint.pprint("last person: " + global_vars.current_suspect) 147 | 148 | if global_vars.current_suspect != "unknown": 149 | post_slack(global_vars.current_suspect) 150 | global_vars.current_sink_status = "dirty_notification_sent" 151 | global_vars.current_sink_status = "unknown" 152 | 153 | pprint.pprint("sink status after notification:" + global_vars.current_sink_status) 154 | pprint.pprint("current suspect after notification:" + global_vars.current_suspect) 155 | else: 156 | pprint.pprint("Someone left CLEAN dishes!") 157 | pprint.pprint("last person: " + global_vars.current_suspect) 158 | 159 | global_vars.current_suspect = "unknown" 160 | 161 | process_this_frame = not process_this_frame 162 | 163 | #Display the results 164 | for (top, right, bottom, left), name in zip(face_locations, face_names): 165 | # Scale back up face locations since the frame we detected in was scaled to 0.5 size 166 | top *= 2 167 | right *= 2 168 | bottom *= 2 169 | left *= 2 170 | 171 | # Draw a box around the face 172 | cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2) 173 | 174 | # Draw a label with a name below the face 175 | cv2.rectangle(frame, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED) 176 | font = cv2.FONT_HERSHEY_DUPLEX 177 | cv2.putText(frame, name, (left + 3, bottom - 3), font, 0.5, (255, 255, 255), 1) 178 | 179 | # Display the resulting image 180 | cv2.imshow('Video', frame) 181 | 182 | # Hit 'q' on the keyboard to quit! 183 | if cv2.waitKey(1) & 0xFF == ord('q'): 184 | break 185 | 186 | # Release handle to the webcam 187 | video_capture.release() 188 | cv2.destroyAllWindows() 189 | --------------------------------------------------------------------------------