DEV Community

Cameron Akhavan for PubNub

Posted on

How to Build your own Amazon Ring Security System in an Afternoon

In this tutorial, we are going to explore one of the most cutting edge technologies in machine learning AI…computer vision! To showcase its capabilities, this step-by-step article will walk you through building your very own desktop security system with a facial recognition machine learning algorithm.

With a simple webcam, your program will be able to recognize the faces of people you choose to allow into the system. It will trigger a text, email, and snapshot image alert system, if any unrecognized faces appear in front of your webcam. We’ll also use Cloudinary and PubNub to build a React Native application that can receive a snapshot image of an “intruder’s” face. It will also allow a user to be added to the system if you desire.

What is Computer Vision?

Computer Vision is a specific field in artificial intelligence that deals with training machine learning models to understand and interpret the visual world. By learning from images and frames from cameras and videos, a computer vision AI can accurately classify the objects it sees and subsequently perform reactionary tasks just like we humans do.

Source Accessed 7/31/19

Python Code for Your FaceTracking Alert System

Before jumping into the code, be sure you sign up for a free PubNub account so we don’t run into any issues later.

To start building the project from scratch, create your project’s directory using your computer’s command line app:

mkdir faceTrackingApp
cd faceTrackingApp
Then create a new Python file called facetracker.py.

Libraries and Dependencies for Computer Vision

OpenCV and face_recognition

First, let's import some machine learning libraries for our app's face tracking capabilities. The main libraries we are going to use are OpenCV and face_recognition.
import face_recognition # Machine Learning Library for Face Recognition
import cv2 # OpenCV
import numpy as np # Handling data
import time
import os,sys
OpenCV is the most popular machine learning library for realtime Computer Vision. The library has useful tools such as webcam control as well as models to train a face tracking app from scratch. However, our project will primarily use ageitgey's face_recognition python library as it already comes with a face recognition model out of the box, making it extremely quick and easy to use.

PubNub

Next, we're going to setup PubNub as our Data Stream Network to handle all of the data between our Python script and mobile application. After you've retrieved your free PubNub API keys, install the PubNub Python SDK.
pip install 'pubnub>=4.1.4'
Then, import the library in your python file,
from pubnub.callbacks import SubscribeCallback
from pubnub.pnconfiguration import PNConfiguration
from pubnub.pubnub import PubNub
from pubnub.enums import PNOperationType, PNStatusCategory
and configure a PubNub instance with your API keys.
# PubNub Config
pnconfig = PNConfiguration()
pnconfig.subscribe_key = "YOUR_SUBSCRIBE_KEY"
pnconfig.publish_key = "YOUR_PUBLISH_KEY"
pnconfig.ssl = False
pubnub = PubNub(pnconfig)

Cloudinary

Lastly, we will setup Cloudinary as our Content Delivery Network to store images of intruders' faces. This will work beautifully with PubNub as our python script can upload the image to Cloudinary, get the URL from the response, then PubNub will send that URL to our Client app to render. First, sign up for a free Cloudinary account and then install the Cloudinary Python SDK with:
pip install cloudinary
Setup the CLOUDINARY_URL environment variable by copying it from the Management Console. Using zsh/bash/sh:
export CLOUDINARY_URL=cloudinary://API-Key:API-Secret@Cloud-name
Import the library in your Python script,
from cloudinary.api import delete_resources_by_tag, resources_by_tag
from cloudinary.uploader import upload
from cloudinary.utils import cloudinary_url
and configure a Cloudinary instance.
# Cloudinary Config
os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '.'))
if os.path.exists('settings.py'):
    exec(open('settings.py').read())
DEFAULT_TAG = "python_sample_basic"

Machine Learning Face Tracking Algorithm

Before we begin building our face recognition machine learning model, we'll need to declare some global variables:
# Setup some Global Variables
video_capture = cv2.VideoCapture(0) # Webcam instance
known_face_names = [] # Names of faces
known_face_encodings = [] # Encodings of Faces
count = 0 # Counter for Number of Unknown Users
flag = 0 # Flag for Setting/Unsetting "Intruder Mode"
[NOTE: We create a count variable for the unknown users because we are going to dynamically save the user's snapshot image in a file path.] We're  going to append the count to the file name like an ID tag. In order to find this snapshot image later, we need to pull up that user's count variable, so we can find the image in the file path. To start training our face recognizer model, we’ll begin with two sample images of faces. You will need two images of two different people's faces in your project directory.
# Load a sample picture and learn how to recognize it.
sample_face_1 = face_recognition.load_image_file("sample_1.jpeg")
sample_face_1_encoding = face_recognition.face_encodings(sample_face_1)[0]

# Load a second sample picture and learn how to recognize it.
sample_face_2 = face_recognition.load_image_file("17.png")
sample_face_2_encoding = face_recognition.face_encodings(sample_face_2)[0]

# Create arrays of known face encodings and their names
known_face_encodings = [
    sample_face_1_encoding,
    sample_face_2_encoding
]

# Create Names for Sample Face encodings
known_face_names = [
    "sample_1",
    "sample_2"
]

# Initialize some variables
face_locations = []
face_encodings = []
face_names = []
process_this_frame = True
Next, we will declare a while loop that will run continuously for the duration of the app. The loop will be responsible for the main functions of our app:
  • Turning on and displaying the webcam feed
  • Tracking faces that appear in front of the webcam and drawing a red box around the face in realtime
  • Displaying a name below a known user's face and "Unknown" for a face that has not been added to the database
  • Calling a series of Alerts and Functions to handle when an "Unknown" face appears on screen
while(True):
    
    video_capture = cv2.VideoCapture(0)
    # Grab a single frame of video
    ret, frame = video_capture.read()

    # Resize frame of video to 1/4 size for faster face recognition processing
    small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)

    # Convert the image from BGR color (which OpenCV uses) to RGB color (which face_recognition uses)
    rgb_small_frame = small_frame[:, :, ::-1]

    # Only process every other frame of video to save time
    if process_this_frame:
        # Find all the faces and face encodings in the current frame of video
        face_locations = face_recognition.face_locations(rgb_small_frame)
        face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations)

        face_names = []
        for face_encoding in face_encodings:
            # See if the face is a match for the known face(s)
            matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
            name = "Unknown"

            # # If a match was found in known_face_encodings, just use the first one.
            # if True in matches:
            #     first_match_index = matches.index(True)
            #     name = known_face_names[first_match_index]

            # Or instead, use the known face with the smallest distance to the new face
            face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
            best_match_index = np.argmin(face_distances)
            if matches[best_match_index]:
                name = known_face_names[best_match_index]

            face_names.append(name)

            #---------------------See next section for this code block's explanation---------------------#

            ## Set Unknown User Flag and Send Alerts
            #global flag
            #if(name=='Unknown' and flag==0):
            #    flag = 1
            #    Alert()
            #
            #--------------------------------------------------------------------------------------------#

    process_this_frame = not process_this_frame

    # Display the results
    for (top, right, bottom, left), name in zip(face_locations, face_names):
        # Scale back up face locations since the frame we detected in was scaled to 1/4 size
        top *= 4
        right *= 4
        bottom *= 4
        left *= 4

        # Draw a box around the face
        cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)

        # Draw a label with a name below the face
        cv2.rectangle(frame, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED)
        font = cv2.FONT_HERSHEY_DUPLEX
        cv2.putText(frame, name, (left + 6, bottom - 6), font, 1.0, (255, 255, 255), 1)

    # Display the resulting image
    cv2.imshow('Video', frame)

    # Hit 'q' on the keyboard to quit!
    if cv2.waitKey(10) & 0xFF == ord('q'):
        break

# Release handle to the webcam
video_capture.release()
cv2.destroyAllWindows()

Sending Alerts

We shall take care of the case when an unregistered face appears in front of our webcam. We want our program to trigger an alert system the moment it sees an "Unknown" face. Luckily, all we need to do is add a few lines of code to our main while loop at the end of the for face_encoding in face_encodings: loop.
# Set Unknown User Flag and Send Alerts
global flag
if(name=='Unknown' and flag==0):
    flag = 1 # Stop repeated calls of Alerts until after the Unknown User is dealt with
    Alert() # Trigger Alert System
When the alert is triggered, we can then define a function to take a snapshot of the unknown user's face, call a function to upload the snapshot to Cloudinary, and finally call our Text/Email alert function.
def Alert():
    global count
    video_capture = cv2.VideoCapture(0) # Create Open CV Webcam Instance
    path = './' # Specify where you want the snapshot to be stored
    name = 'Unknown_User' + str(count) # Append User ID to File Path

    # Wait for 3 seconds
    print('Taking picture in 3')
    time.sleep(1)
    print('Taking picture in 2')
    time.sleep(1)
    print('Taking picture in 1')
    time.sleep(1)

    # Take Picture
    ret, frame = video_capture.read()

    # Grayscale Image to save memory space
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # Save Image in File Path
    status = cv2.imwrite('% s/% s.jpg' % (path, name),gray)
    print('Unknown User Saved to Database', status)

    # Upload Snapshot to Cloudinary 
    upload_files('% s/% s.jpg' % (path,name))
    
    # Send Out Email and Text Alerts
    sendAlerts()
[NOTE: We grayscale the image because the face recognizer doesn't need color to determine facial features. We also store the snapshot image locally so we can add the face to the recognizer if the client wants to add the user later.] When defining our upload_files() function, we're passing in the snapshot's file path so Cloudinary knows where to upload the file from. We then get the response URL of where the image lives in the cloud. We send this url along with a user ID (count of Unknown user) over PubNub to our client application. The client application can then render the image of the snapshot from the Cloudinary URL.
def upload_files(msg):
    global count # Make global changes to count
    response = upload(msg, tags=DEFAULT_TAG) # Upload Image to Cloudinary
    url, options = cloudinary_url( 
        response['public_id'],
        format=response['format'],
        width=200,
        height=150,
        crop="fill"
    )
    dictionary = {"url": url, "ID": count}
    pubnub.publish().channel('global').message(dictionary).pn_async(publish_callback)
    count+=1 # Increment Unknown User Count
In order to publish with PubNub, we need to define a publish callback.
def publish_callback(result, status):
    pass
    # Handle PNPublishResult and PNStatus
  To setup your Text and Email alerts, you'll need to sign up for a free ClickSend account as well as a free SendGrid account to get your API keys. Now you get to see the power and beauty behind PubNub Functions with our Partnered Blocks. Go ahead and visit both our ClickSend Block as well as our SendGrid Block. Through those links, PubNub will automatically generate a customizable PubNub Function. The serverless, open-source code will completely handle the APIs for you. All you need to do is put in your API keys and you're good to go! Once you've set up your PubNub Functions, you can define a sendAlerts() function to publish a message, implementing your Text and Email alerts:
def sendAlerts():
    dictionary = {
    "to" : 'RECEIVING PHONE NUMBER',
    "body": "There is an unregistered user at your desk!"
    }
    pubnub.publish().channel('clicksend-text').message(dictionary).pn_async(publish_callback)

    dictionary = {
    "to": "EMAIL RECEIVER",
    "toname": "EMAIL SENDER",
    "subject": "INTRUDER ALERT",
    "text": "THERE IS AN UNREGISTERED USER AT YOUR DESK"
    }   
    pubnub.publish().channel('email-sendgrid-channel').message(dictionary).pn_async(publish_callback)
NOTE: In order to properly use a PubNub block, you need to publish over the same channel specified in your block (you can check this in your blocks Functions dashboard) as well as formatting the message payload properly (according to the block's documentation).

Adding Users to Our Facetracker

When an Unregistered face is detected on our webcam, our Python script sends an email/text alert as well as a snapshot image to our client application. We now want to add the ability to add a user's face to our app's "known_faces" database, so the user will no longer trigger our alert system. To do this, the client application must publish a message over PubNub. To receive this message in our python application, we must subscribe to the channel the client is publishing from, and create a Subscriber Callback to handle the incoming message.
class MySubscribeCallback(SubscribeCallback):
    def status(self, pubnub, status):
        pass
        # The status object returned is always related to subscribe but could contain
        # information about subscribe, heartbeat, or errors
        # use the operationType to switch on different options
        if status.operation == PNOperationType.PNSubscribeOperation \
                or status.operation == PNOperationType.PNUnsubscribeOperation:
            if status.category == PNStatusCategory.PNConnectedCategory:
                pass
                # This is expected for a subscribe, this means there is no error or issue whatsoever
            elif status.category == PNStatusCategory.PNReconnectedCategory:
                pass
                # This usually occurs if subscribe temporarily fails but reconnects. This means
                # there was an error but there is no longer any issue
            elif status.category == PNStatusCategory.PNDisconnectedCategory:
                pass
                # This is the expected category for an unsubscribe. This means here
                # was no error in unsubscribing from everything
            elif status.category == PNStatusCategory.PNUnexpectedDisconnectCategory:
                pass
                # This is usually an issue with the internet connection, this is an error, handle
                # appropriately retry will be called automatically
            elif status.category == PNStatusCategory.PNAccessDeniedCategory:
                pass
                # This means that PAM does not allow this client to subscribe to this
                # channel and channel group configuration. This is another explicit error
            else:
                pass
                # This is usually an issue with the internet connection, this is an error, handle appropriately
                # retry will be called automatically
        elif status.operation == PNOperationType.PNSubscribeOperation:
            # Heartbeat operations can in fact have errors, so it is important to check first for an error.
            # For more information on how to configure heartbeat notifications through the status
            if status.is_error():
                pass
                # There was an error with the heartbeat operation, handle here
            else:
                pass
                # Heartbeat operation was successful
        else:
            pass
            # Encountered unknown status type
 
    def presence(self, pubnub, presence):
        pass  # handle incoming presence data
    def message(self, pubnub, message):
        addUser(message.message["ID"], message.message["name"])
 
 
pubnub.add_listener(MySubscribeCallback())
pubnub.subscribe().channels('ch1').execute()
NOTE: Above we assume the client is publishing the ID of the unknown user (for image file path) as well as the name of the user (to display below the user's face). With the parameters in hand, we can add the new user to our database.
def addUser(ID, name):
    global known_face_encodings, known_face_names, flag
    path = './Unknown_User' + str(ID) # Append User ID to File Path
    # Load User's picture and learn how to recognize it.
    user_image = face_recognition.load_image_file('% s.jpg' % (path)) # Load Image
    user_face_encoding = face_recognition.face_encodings(user_image)[0] # Encode Image
    known_face_encodings.append(user_face_encoding) # Add Encoded Image to 'Known Faces' Array
    known_face_names.append(name) # Append New User's Name to Database
    flag = 0 # Reset Unknown User Flag

React Native Code for Our Client Application

Setting Up Our Realtime React Native Environment

Install Xcode so we can build and simulate our app for IOS and Android Studio for Android. Then install Node.js and watchman using Homebrew:
brew install node
brew install watchman
Install the React Native CLI with NPM:
npm install -g react-native-cli
To create a React Native App template, enter the React Native CLI command in your project's directory:
react-native init client
cd client
Since we're going to be using PubNub in our Client app to send and receive messages, we'll need to install the PubNub React SDK,
npm install --save pubnub pubnub-react
and then link the library like so:
react-native link pubnub-react

Setting Up Realtime Pub/Sub Messaging

To start sending and receiving messages in realtime in our app, first import the PubNub React SDK.
import PubNubReact from 'pubnub-react';
Then import the TouchableOpacity and Image components from React Native,
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  Image,
} from 'react-native';

Now we add a constructor at the top of our App Component. The constructor will be responsible for setting up a PubNub instance with our Publish/Subscribe keys as well as initialize the following state variables:

  • image - Snapshot image from an unknown user alert (we initialize it with a placeholder image until a Snapshot alert arrives).
  • message - Incoming alert message from the face tracking app.
  • text - Client user's input for typing in the name of a user.
  • count -  To keep track of which unknown user we are getting an alert from.
export default class App extends React.Component {

  constructor(props) {
    super(props)

    this.pubnub = new PubNubReact({
      publishKey: "YOUR PUBLISH KEY",
      subscribeKey: "YOUR SUBSCRIBE KEY"
    })

    //Base State
    this.state = {
      image: require('./assets/PLACEHOLDER_IMAGE.jpg'),
      message: '',
      text: '',
      count: 0,
    }

    this.pubnub.init(this);
  }

/// .......VVV REST OF THE CODE VVV.......///
When our client app first fires up, we declare an asynchronous function that will subscribe to our face tracking alert channel and handle message events. In this case, we receive the ID (count of unknown user) as well as the snapshot image URL (from Cloudinary) of the unknown user.
async componentDidMount() {
  this.setUpApp()    
}

async setUpApp(){
  this.pubnub.getMessage("global", msg => {
    this.setState({count: msg.message.ID})
    this.setState({image: msg.message.url})
  })

  this.pubnub.subscribe({
    channels: ["global"],
    withPresence: false
  });
}
Once that image is received by the mobile app, the client user should then be able to add the unknown user to the face tracker's "known_faces" database. We can define a function to set the state of the client user's input for the unknown user's name.
 handleText = (name) => {
   this.setState({ text: name })
}

We can also write a function to publish the added user's name along with the added user's ID.

 publishName = (text) => {
  this.pubnub.publish({
    message: {
      ID: this.state.count,
      name: text,
    },
    channel: "ch1"
  });
}

Creating and Rendering App Components

At the top of our screen we'll render the snapshot image from an incoming "Unknown User" Alert. The source of this image will be a URI we grabbed from the alert message that we saved to state.

` <Image
  source={{uri: this.state.image}}
  style={{width: 250, height: 250}}/>                  `

Below that, we can display a suitable caption.

<Text>{'Do You Know This Person?'}</Text>

We then create a Text Input component to store the name of the User to be added to the face tracker, if the client decides to do so.

<TextInput style = {styles.input}
         underlineColorAndroid = "transparent"
         placeholder = "Name"
         placeholderTextColor = "#9a73ef"
         autoCapitalize = "none"
         onChangeText = {this.handleText}/>

Lastly, we create a submit button with TouchableOpacity to publish the added user's name for our Face Tracker to add to the system:

<TouchableOpacity
    style = {styles.submitButton}
    onPress = {
      () => this.publishName(this.state.text)
    }>
      <Text>"SUBMIT"</Text>
</TouchableOpacity>

Wrap all those components in a <View> </View> and you’re good to go!

Running the Program

First, start up the React Native client application on Android or iOS by opening a terminal in the client app's directory.

react-native run-ios

or

react-native run-android

Then, in another terminal window, run the Python face tracker.

python facetracker.py

If You're Still Hungry For More...

Feel free to send us any of your questions, concerns, or comments at devrel@pubnub.com.

If you’re still hungry for more PubNub Machine Learning content, here are some other articles that you may be interested in:

Top comments (0)