commit
1d4308cc0a
16 changed files with 725 additions and 145 deletions
45
README.md
45
README.md
|
|
@ -1,46 +1,41 @@
|
|||
# Howdy for Ubuntu
|
||||
|
||||
Windows Hello™ style authentication for Ubuntu. Use your build in IR emitters and camera in combination with face recognition to prove who you are.
|
||||
Windows Hello™ style authentication for Ubuntu. Use your built-in IR emitters and camera in combination with face recognition to prove who you are.
|
||||
|
||||
Using the central authentication system in Linux (PAM), this works everywhere you would otherwise need your password: Login, lock screen, sudo, su, etc.
|
||||
|
||||
### Installation
|
||||
|
||||
First we need to install pam-python, fswebcam and OpenCV from the Ubuntu repositories:
|
||||
Run the installer by pasting (`ctrl+shift+V`) the following command into the terminal:
|
||||
|
||||
```
|
||||
sudo apt install libpam-python fswebcam libopencv-dev python-opencv
|
||||
wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py
|
||||
```
|
||||
|
||||
After that, install the face_recognition python module. There's an excellent step by step guide on how to do this on [its github page](https://github.com/ageitgey/face_recognition#installation).
|
||||
|
||||
In the root of your cloned repo is a file called `config.ini`. The `device_id` variable in this file is important, make sure it is the IR camera and not your normal webcam.
|
||||
|
||||
Now it's time to let Howdy learn your face. The learn.py script will make 3 models of your face and store them as an encoded set in the `models` folder. To run the script, open a terminal, navigate to this repository and run:
|
||||
|
||||
```
|
||||
python3 learn.py
|
||||
```
|
||||
|
||||
The script should guide you through the process.
|
||||
|
||||
Finally we need to tell PAM that there's a new module installed. Open `/etc/pam.d/common-auth` as root (`sudo nano /etc/pam.d/common-auth`) and add the following line to the top of the file:
|
||||
|
||||
```
|
||||
auth sufficient pam_python.so /path/to/pam.py
|
||||
```
|
||||
|
||||
Replace the final argument with the full path to pam.py in this repository. The `sufficient` control tells PAM that Howdy is enough to authenticate the user, but if it fails we can fall back on more traditional methods.
|
||||
This will guide you through the installation. When that's done run `howdy USER add` and replace `USER` with your username to add a face model.
|
||||
|
||||
If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action.
|
||||
|
||||
**Note:** The build of dlib can hang on 100% for over a minute, give it time.
|
||||
|
||||
### Command line
|
||||
|
||||
The installer adds a `howdy` command to manage face models for the current user. Use `howdy help` to list the available options.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Any errors in the script itself get logged directly into the console and should indicate what went wrong. If authentication still fails but no errors are printed you could take a look at the last lines in `/var/log/auth.log` to see if anything has been reported there.
|
||||
Any python errors get logged directly into the console and should indicate what went wrong. If authentication still fails but no errors are printed you could take a look at the last lines in `/var/log/auth.log` to see if anything has been reported there.
|
||||
|
||||
If you encounter an error that hasn't been reported yet, don't be afraid to open a new issue.
|
||||
|
||||
### Uninstalling
|
||||
|
||||
There is an uninstaller available, run `sudo python3 /lib/security/howdy/uninstall.py` to remove Howdy from your system.
|
||||
|
||||
### A note on security
|
||||
|
||||
This script is in no way as secure as a password and will never be. Although it's harder to fool than normal face recognition, a person who looks similar to you or well-printed photo of you could be enough to do it.
|
||||
|
||||
To minimize the chance of this script being compromised, it's recommend to store this repo in `/etc/pam.d` and to make it read only.
|
||||
To minimize the chance of this script being compromised, it's recommend to leave this repo in /lib/security and to keep it read only.
|
||||
|
||||
DO NOT USE THIS SCRIPT AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM.
|
||||
DO NOT USE HOWDY AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM.
|
||||
|
|
|
|||
15
autocomplete.sh
Normal file
15
autocomplete.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Autocomplete file run in bash
|
||||
# Will sugest arguments on tab
|
||||
|
||||
_howdy() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts="help list add remove clear"
|
||||
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _howdy howdy
|
||||
41
cli.py
Executable file
41
cli.py
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# CLI directly called by running the howdy command
|
||||
|
||||
# Import required modules
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Check if the minimum of 3 arugemnts has been met and print help otherwise
|
||||
if (len(sys.argv) < 3):
|
||||
print("Howdy IR face recognition help")
|
||||
import cli.help
|
||||
sys.exit()
|
||||
|
||||
# The command given
|
||||
cmd = sys.argv[2]
|
||||
|
||||
# Requre sudo for comamnds that need root rights to read the model files
|
||||
if cmd in ["list", "add", "remove", "clear"] and os.getenv("SUDO_USER") is None:
|
||||
print("Please run this command with sudo")
|
||||
sys.exit()
|
||||
|
||||
# Call the right files for the given command
|
||||
if cmd == "list":
|
||||
import cli.list
|
||||
elif cmd == "help":
|
||||
print("Howdy IR face recognition")
|
||||
import cli.help
|
||||
elif cmd == "add":
|
||||
import cli.add
|
||||
elif cmd == "remove":
|
||||
import cli.remove
|
||||
elif cmd == "clear":
|
||||
import cli.clear
|
||||
else:
|
||||
# If the comand is invalid, check if the user hasn't swapped the username and command
|
||||
if sys.argv[1] in ["list", "add", "remove", "clear", "help"]:
|
||||
print("Usage: howdy <user> <command>")
|
||||
else:
|
||||
print('Unknown command "' + cmd + '"')
|
||||
import cli.help
|
||||
0
cli/__init__.py
Normal file
0
cli/__init__.py
Normal file
|
|
@ -1,24 +1,35 @@
|
|||
# Save the face of the user in encoded form
|
||||
|
||||
# Import required modules
|
||||
import face_recognition
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# Import config and extra functions
|
||||
import configparser
|
||||
import utils
|
||||
|
||||
|
||||
# Try to import face_recognition and give a nice error if we can't
|
||||
# Add should be the first point where import issues show up
|
||||
try:
|
||||
import face_recognition
|
||||
except ImportError as err:
|
||||
print(err)
|
||||
|
||||
print("\nCan't import the face_recognition module, check the output of")
|
||||
print("pip3 show face_recognition")
|
||||
sys.exit()
|
||||
|
||||
# Get the absolute path to the current file
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Read config from disk
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
|
||||
config.read(path + "/../config.ini")
|
||||
|
||||
def captureFrame(delay):
|
||||
"""Capture and encode 1 frame of video"""
|
||||
global encodings
|
||||
global insert_model
|
||||
|
||||
# Call fswebcam to save a frame to /tmp with a set delay
|
||||
exit_code = subprocess.call(["fswebcam", "-S", str(delay), "--no-banner", "-d", "/dev/video" + str(config.get("video", "device_id")), tmp_file])
|
||||
|
|
@ -54,36 +65,53 @@ def captureFrame(delay):
|
|||
for point in enc[0]:
|
||||
clean_enc.append(point)
|
||||
|
||||
encodings.append(clean_enc)
|
||||
insert_model["data"].append(clean_enc)
|
||||
|
||||
# The current user
|
||||
user = os.environ.get("USER")
|
||||
user = sys.argv[1]
|
||||
# The name of the tmp frame file to user
|
||||
tmp_file = "/tmp/howdy_" + user + ".jpg"
|
||||
# The permanent file to store the encoded model in
|
||||
enc_file = "./models/" + user + ".dat"
|
||||
enc_file = path + "/../models/" + user + ".dat"
|
||||
# Known encodings
|
||||
encodings = []
|
||||
|
||||
# Make the ./models folder if it doesn't already exist
|
||||
if not os.path.exists("models"):
|
||||
if not os.path.exists(path + "/../models"):
|
||||
print("No face model folder found, creating one")
|
||||
os.makedirs("models")
|
||||
os.makedirs(path + "/../models")
|
||||
|
||||
# To try read a premade encodings file if it exists
|
||||
try:
|
||||
encodings = json.load(open(enc_file))
|
||||
except FileNotFoundError:
|
||||
encodings = False
|
||||
|
||||
# If a file does exist, ask the user what needs to be done
|
||||
if encodings != False:
|
||||
encodings = utils.print_menu(encodings)
|
||||
else:
|
||||
encodings = []
|
||||
|
||||
print("\nLearning face for the user account " + user)
|
||||
print("Please look straight into the camera for 5 seconds")
|
||||
print("Adding face model for the user account " + user)
|
||||
|
||||
# Set the default label
|
||||
label = "Initial model"
|
||||
|
||||
# If models already exist, set that default label
|
||||
if len(encodings) > 0:
|
||||
label = "Model #" + str(len(encodings) + 1)
|
||||
|
||||
# Ask the user for a custom label
|
||||
label_in = input("Enter a label for this new model [" + label + "]: ")
|
||||
|
||||
# Set the custom label (if any) and limit it to 24 characters
|
||||
if label_in != "":
|
||||
label = label_in[:24]
|
||||
|
||||
# Prepare the metadata for insertion
|
||||
insert_model = {
|
||||
"time": int(time.time()),
|
||||
"label": label,
|
||||
"id": len(encodings),
|
||||
"data": []
|
||||
}
|
||||
|
||||
print("\nPlease look straight into the camera for 5 seconds")
|
||||
|
||||
# Give the user time to read
|
||||
time.sleep(2)
|
||||
|
|
@ -93,6 +121,9 @@ for delay in [30, 6, 0]:
|
|||
time.sleep(.3)
|
||||
captureFrame(delay)
|
||||
|
||||
# Insert full object into the list
|
||||
encodings.append(insert_model)
|
||||
|
||||
# Save the new encodings to disk
|
||||
with open(enc_file, "w") as datafile:
|
||||
json.dump(encodings, datafile)
|
||||
33
cli/clear.py
Normal file
33
cli/clear.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Clear all models by deleting the whole file
|
||||
|
||||
# Import required modules
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Get the full path to this file
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
# Get the passed user
|
||||
user = sys.argv[1]
|
||||
|
||||
# Check if the models folder is there
|
||||
if not os.path.exists(path + "/../models"):
|
||||
print("No models created yet, can't clear them if they don't exist")
|
||||
sys.exit()
|
||||
|
||||
# Check if the user has a models file to delete
|
||||
if not os.path.isfile(path + "/../models/" + user + ".dat"):
|
||||
print(user + " has no models or they have been cleared already")
|
||||
sys.exit()
|
||||
|
||||
# Double check with the user
|
||||
print("This will clear all models for " + user)
|
||||
ans = input("Do you want to continue [y/N]: ")
|
||||
|
||||
# Abort if they don't answer y or Y
|
||||
if (ans.lower() != "y"):
|
||||
print('\nInerpeting as a "NO"')
|
||||
sys.exit()
|
||||
|
||||
# Delete otherwise
|
||||
os.remove(path + "/../models/" + user + ".dat")
|
||||
print("\nModels cleared")
|
||||
16
cli/help.py
Normal file
16
cli/help.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Prints a simple help page for the CLI
|
||||
|
||||
print("""
|
||||
Usage:
|
||||
howdy <user> <command> [argument]
|
||||
|
||||
Commands:
|
||||
help Show this help page
|
||||
list List all saved face models for the current user
|
||||
add Add a new face model for the current user
|
||||
remove [id] Remove a specific model
|
||||
clear Remove all face models for the current user
|
||||
|
||||
For support please visit
|
||||
https://github.com/Boltgolt/howdy\
|
||||
""")
|
||||
46
cli/list.py
Normal file
46
cli/list.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# List all models for a user
|
||||
|
||||
# Import required modules
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
# Get the absolute path and the username
|
||||
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
|
||||
user = sys.argv[1]
|
||||
|
||||
# Check if the models file has been created yet
|
||||
if not os.path.exists(path + "/models"):
|
||||
print("Face models have not been initialized yet, please run:")
|
||||
print("\n\thowdy " + user + " add\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Path to the models file
|
||||
enc_file = path + "/models/" + user + ".dat"
|
||||
|
||||
# Try to load the models file and abort if the user does not have it yet
|
||||
try:
|
||||
encodings = json.load(open(enc_file))
|
||||
except FileNotFoundError:
|
||||
print("No face model known for the user " + user + ", please run:")
|
||||
print("\n\thowdy " + user + " add\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Print a header
|
||||
print("Known face models for " + user + ":")
|
||||
print("\n\t\033[1;29mID Date Label\033[0m")
|
||||
|
||||
# Loop through all encodings and print info about them
|
||||
for enc in encodings:
|
||||
# Start with a tab and print the id
|
||||
print("\t" + str(enc["id"]), end="")
|
||||
# Print padding spaces after the id
|
||||
print((4 - len(str(enc["id"]))) * " ", end="")
|
||||
# Format the time as ISO in the local timezone
|
||||
print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(enc["time"])), end="")
|
||||
# End with the label
|
||||
print(" " + enc["label"])
|
||||
|
||||
# Add a closing enter
|
||||
print()
|
||||
78
cli/remove.py
Normal file
78
cli/remove.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Remove a encoding from the models file
|
||||
|
||||
# Import required modules
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# Get the absolute path and the username
|
||||
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
|
||||
user = sys.argv[1]
|
||||
|
||||
# Check if enough arguments have been passed
|
||||
if len(sys.argv) == 3:
|
||||
print("Please add the ID of the model to remove as an argument")
|
||||
print("You can find the IDs by running:")
|
||||
print("\n\thowdy " + user + " list\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Check if the models file has been created yet
|
||||
if not os.path.exists(path + "/models"):
|
||||
print("Face models have not been initialized yet, please run:")
|
||||
print("\n\thowdy " + user + " add\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Path to the models file
|
||||
enc_file = path + "/models/" + user + ".dat"
|
||||
|
||||
# Try to load the models file and abort if the user does not have it yet
|
||||
try:
|
||||
encodings = json.load(open(enc_file))
|
||||
except FileNotFoundError:
|
||||
print("No face model known for the user " + user + ", please run:")
|
||||
print("\n\thowdy " + user + " add\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Tracks if a encoding with that id has been found
|
||||
found = False
|
||||
|
||||
# Loop though all encodings and check if they match the argument
|
||||
for enc in encodings:
|
||||
if str(enc["id"]) == sys.argv[3]:
|
||||
# Double check with the user
|
||||
print('This will remove the model called "' + enc["label"] + '" for ' + user)
|
||||
ans = input("Do you want to continue [y/N]: ")
|
||||
|
||||
# Abort if the answer isn't yes
|
||||
if (ans.lower() != "y"):
|
||||
print('\nInerpeting as a "NO"')
|
||||
sys.exit()
|
||||
|
||||
# Mark as found and print an enter
|
||||
found = True
|
||||
print()
|
||||
break
|
||||
|
||||
# Abort if no matching id was found
|
||||
if not found:
|
||||
print("No model with ID " + sys.argv[3] + " exists for " + user)
|
||||
sys.exit()
|
||||
|
||||
# Remove the entire file if this encoding is the only one
|
||||
if len(encodings) == 1:
|
||||
os.remove(path + "/models/" + user + ".dat")
|
||||
print("Removed last model, howdy disabled for user")
|
||||
else:
|
||||
# A place holder to contain the encodings that will remain
|
||||
new_encodings = []
|
||||
|
||||
# Loop though all encodin and only add thos that don't need to be removed
|
||||
for enc in encodings:
|
||||
if str(enc["id"]) != sys.argv[3]:
|
||||
new_encodings.append(enc)
|
||||
|
||||
# Save this new set to disk
|
||||
with open(enc_file, "w") as datafile:
|
||||
json.dump(new_encodings, datafile)
|
||||
|
||||
print("Removed model " + sys.argv[3])
|
||||
69
compair.py
69
compair.py
|
|
@ -1,69 +0,0 @@
|
|||
# Compair incomming video with known faces
|
||||
# Running in a local python instance to get around PATH issues
|
||||
|
||||
# Import required modules
|
||||
import face_recognition
|
||||
import cv2
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import configparser
|
||||
|
||||
# Read config from disk
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
|
||||
|
||||
def stop(status):
|
||||
"""Stop the execution and close video stream"""
|
||||
video_capture.release()
|
||||
sys.exit(status)
|
||||
|
||||
# Make sure we were given an username to tast against
|
||||
try:
|
||||
if not isinstance(sys.argv[1], str):
|
||||
sys.exit(1)
|
||||
except IndexError:
|
||||
sys.exit(1)
|
||||
|
||||
# The username of the authenticating user
|
||||
user = sys.argv[1]
|
||||
# List of known faces, encoded by face_recognition
|
||||
encodings = []
|
||||
# Amount of frames already matched
|
||||
tries = 0
|
||||
|
||||
# Try to load the face model from the models folder
|
||||
try:
|
||||
encodings = json.load(open(os.path.dirname(os.path.abspath(__file__)) + "/models/" + user + ".dat"))
|
||||
except FileNotFoundError:
|
||||
sys.exit(10)
|
||||
|
||||
# Verify that we have a valid model file
|
||||
if len(encodings) < 3:
|
||||
sys.exit(1)
|
||||
|
||||
# Start video capture on the IR camera
|
||||
video_capture = cv2.VideoCapture(int(config.get("video", "device_id")))
|
||||
|
||||
while True:
|
||||
# Grab a single frame of video
|
||||
ret, frame = video_capture.read()
|
||||
|
||||
# Get all faces from that frame as encodings
|
||||
face_encodings = face_recognition.face_encodings(frame)
|
||||
|
||||
# Loop through each face
|
||||
for face_encoding in face_encodings:
|
||||
# Match this found face against a known face
|
||||
matches = face_recognition.face_distance(encodings, face_encoding)
|
||||
|
||||
# Check if any match is certain enough to be the user we're looking for
|
||||
for match in matches:
|
||||
if match < int(config.get("video", "certainty")) and match > 0:
|
||||
stop(0)
|
||||
|
||||
# Stop if we've exceded the maximum retry count
|
||||
if tries > int(config.get("video", "frame_count")):
|
||||
stop(11)
|
||||
|
||||
tries += 1
|
||||
140
compare.py
Normal file
140
compare.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Compare incomming video with known faces
|
||||
# Running in a local python instance to get around PATH issues
|
||||
|
||||
# Import required modules
|
||||
import cv2
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import math
|
||||
import configparser
|
||||
|
||||
# Start timing
|
||||
timings = [time.time()]
|
||||
|
||||
# Read config from disk
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
|
||||
|
||||
def stop(status):
|
||||
"""Stop the execution and close video stream"""
|
||||
video_capture.release()
|
||||
sys.exit(status)
|
||||
|
||||
# Make sure we were given an username to tast against
|
||||
try:
|
||||
if not isinstance(sys.argv[1], str):
|
||||
sys.exit(1)
|
||||
except IndexError:
|
||||
sys.exit(1)
|
||||
|
||||
# The username of the authenticating user
|
||||
user = sys.argv[1]
|
||||
# The model file contents
|
||||
models = []
|
||||
# Encoded face models
|
||||
encodings = []
|
||||
# Amount of frames already matched
|
||||
tries = 0
|
||||
|
||||
# Try to load the face model from the models folder
|
||||
try:
|
||||
models = json.load(open(os.path.dirname(os.path.abspath(__file__)) + "/models/" + user + ".dat"))
|
||||
except FileNotFoundError:
|
||||
sys.exit(10)
|
||||
|
||||
# Check if the file contains a model
|
||||
if len(models) < 1:
|
||||
sys.exit(10)
|
||||
|
||||
# Put all models together into 1 array
|
||||
for model in models:
|
||||
encodings += model["data"]
|
||||
|
||||
# Import face recognition, takes some time
|
||||
timings.append(time.time())
|
||||
import face_recognition
|
||||
timings.append(time.time())
|
||||
|
||||
# Start video capture on the IR camera
|
||||
video_capture = cv2.VideoCapture(int(config.get("video", "device_id")))
|
||||
timings.append(time.time())
|
||||
|
||||
# Fetch the max frame height
|
||||
max_height = int(config.get("video", "max_height"))
|
||||
|
||||
# Start the read loop
|
||||
frames = 0
|
||||
while True:
|
||||
frames += 1
|
||||
|
||||
# Grab a single frame of video
|
||||
# Don't remove ret, it doesn't work without it
|
||||
ret, frame = video_capture.read()
|
||||
|
||||
# Get the height and with of the image
|
||||
height, width = frame.shape[:2]
|
||||
|
||||
# If the hight is too high
|
||||
if max_height < height:
|
||||
# Calculate the amount the image has to shrink
|
||||
scaling_factor = max_height / float(height)
|
||||
# Apply that factor to the frame
|
||||
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
|
||||
|
||||
# Save the new size for diagnostics
|
||||
scale_height, scale_width = frame.shape[:2]
|
||||
|
||||
# Convert from BGR to RGB
|
||||
frame = frame[:, :, ::-1]
|
||||
|
||||
# Get all faces from that frame as encodings
|
||||
face_encodings = face_recognition.face_encodings(frame)
|
||||
|
||||
# Loop through each face
|
||||
for face_encoding in face_encodings:
|
||||
# Match this found face against a known face
|
||||
matches = face_recognition.face_distance(encodings, face_encoding)
|
||||
|
||||
# Check if any match is certain enough to be the user we're looking for
|
||||
match_index = 0
|
||||
for match in matches:
|
||||
match_index += 1
|
||||
|
||||
# Try to find a match that's confident enough
|
||||
if match * 10 < float(config.get("video", "certainty")) and match > 0:
|
||||
timings.append(time.time())
|
||||
|
||||
# If set to true in the config, print debug text
|
||||
if config.get("debug", "end_report") == "true":
|
||||
def print_timing(label, offset):
|
||||
"""Helper function to print a timing from the list"""
|
||||
print(" " + label + ": " + str(round((timings[1 + offset] - timings[offset]) * 1000)) + "ms")
|
||||
|
||||
print("Time spend")
|
||||
print_timing("Starting up", 0)
|
||||
print_timing("Importing face_recognition", 1)
|
||||
print_timing("Opening the camera", 2)
|
||||
print_timing("Searching for known face", 3)
|
||||
|
||||
print("\nResolution")
|
||||
print(" Native: " + str(height) + "x" + str(width))
|
||||
print(" Used: " + str(scale_height) + "x" + str(scale_width))
|
||||
|
||||
print("\nFrames searched: " + str(frames) + " (" + str(round(float(frames) / (timings[4] - timings[2]), 2)) + " fps)")
|
||||
print("Certainty of winning frame: " + str(round(match * 10, 3)))
|
||||
|
||||
exposures = ["long", "medium", "short"]
|
||||
model_id = math.floor(float(match_index) / 3)
|
||||
|
||||
print("Winning model: " + str(model_id) + " (\"" + models[model_id]["label"] + "\") using " + exposures[match_index % 3] + " exposure\n")
|
||||
|
||||
# End peacegully
|
||||
stop(0)
|
||||
|
||||
# Stop if we've exceded the maximum retry count
|
||||
if time.time() - timings[3] > int(config.get("video", "timout")):
|
||||
stop(11)
|
||||
|
||||
tries += 1
|
||||
16
config.ini
16
config.ini
|
|
@ -9,11 +9,19 @@ suppress_unknown = false
|
|||
[video]
|
||||
# The certainty of the detected face belonging to the user of the account
|
||||
# On a scale from 1 to 10, values above 5 are not recommended
|
||||
certainty = 3
|
||||
certainty = 3.5
|
||||
|
||||
# The number of frames to capture and to process before timing out
|
||||
frame_count = 30
|
||||
# The number of seconds to search before timing out
|
||||
timout = 4
|
||||
|
||||
# The /dev/videoX id to capture frames from
|
||||
# In my case, video0 is the normal camera and video1 is the IR version
|
||||
# Should be set automatically by the installer
|
||||
device_id = 1
|
||||
|
||||
# Scale down the video feed to this maximum height
|
||||
# Speeds up face recognition but can make it less precise
|
||||
max_height = 320
|
||||
|
||||
[debug]
|
||||
# Show a short but detailed diagnostic report in console
|
||||
end_report = false
|
||||
|
|
|
|||
235
installer.py
Normal file
235
installer.py
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Installation script to install howdy
|
||||
# Runs completely independent of the others
|
||||
|
||||
# Import required modules
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import fileinput
|
||||
import urllib.parse
|
||||
|
||||
def log(text):
|
||||
"""Print a nicely formatted line to stdout"""
|
||||
print("\n>>> \033[32m" + text + "\033[0m\n")
|
||||
|
||||
def handleStatus(status):
|
||||
"""Abort if a command fails"""
|
||||
if (status != 0):
|
||||
print("\033[31mError while running last command\033[0m")
|
||||
sys.exit()
|
||||
|
||||
# Check if we're running as root
|
||||
user = os.getenv("SUDO_USER")
|
||||
if user is None:
|
||||
print("Please run this script as a sudo user")
|
||||
sys.exit()
|
||||
|
||||
# Print some nice intro text
|
||||
print("\n\033[33m HOWDY INSTALLER FOR UBUNTU\033[0m")
|
||||
print(" Version 1, 2016/02/05\n")
|
||||
|
||||
# Let it sink in
|
||||
time.sleep(.5)
|
||||
log("Installing required apt packages")
|
||||
|
||||
# Install packages though apt
|
||||
handleStatus(subprocess.call(["apt", "install", "-y", "libpam-python", "fswebcam", "libopencv-dev", "python-opencv"]))
|
||||
|
||||
log("Starting camera check")
|
||||
|
||||
# Get all devices
|
||||
devices = os.listdir("/dev")
|
||||
# The picked video device id
|
||||
picked = False
|
||||
|
||||
# Loop though all devices
|
||||
for dev in devices:
|
||||
# Only use the video devices
|
||||
if (dev[:5] == "video"):
|
||||
time.sleep(.5)
|
||||
|
||||
# The full path to the device is the default name
|
||||
device_name = "/dev/" + dev
|
||||
# Get the udevadm details to try to get a better name
|
||||
udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8")
|
||||
|
||||
# Loop though udevadm to search for a better name
|
||||
for line in udevadm.split("\n"):
|
||||
# Match it and encase it in quotes
|
||||
re_name = re.search('product.*=(.*)$', line, re.IGNORECASE)
|
||||
if re_name:
|
||||
device_name = '"' + re_name.group(1) + '"'
|
||||
|
||||
# Show what device we're using
|
||||
print("Trying " + device_name)
|
||||
|
||||
# Let fswebcam keep the camera open in the background
|
||||
sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid)
|
||||
|
||||
# Ask the user if this is the right one
|
||||
print("\033[33mOne of your cameras should now be on.\033[0m")
|
||||
ans = input("Did your IR emitters turn on? [y/N]: ")
|
||||
|
||||
# The user has answered, kill fswebcam
|
||||
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
|
||||
|
||||
# Set this camera as picked if the answer was yes, go to the next one if no
|
||||
if (ans.lower() == "y"):
|
||||
picked = dev[5:]
|
||||
break
|
||||
else:
|
||||
print("Inerpeting as a \"NO\"\n")
|
||||
|
||||
# Abort if no camera was picked
|
||||
if (picked == False):
|
||||
print("\033[31mNo suitable IR camera found\033[0m")
|
||||
sys.exit()
|
||||
|
||||
log("Cloning dlib")
|
||||
|
||||
# Clone the git to /tmp
|
||||
handleStatus(subprocess.call(["git", "clone", "https://github.com/davisking/dlib.git", "/tmp/dlib_clone"]))
|
||||
|
||||
log("Building dlib")
|
||||
|
||||
# Start the build without GPU
|
||||
handleStatus(subprocess.call(["cd /tmp/dlib_clone/; python3 setup.py install --yes USE_AVX_INSTRUCTIONS --no DLIB_USE_CUDA"], shell=True))
|
||||
|
||||
log("Cleaning up dlib")
|
||||
|
||||
# Remove the no longer needed git clone
|
||||
handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"]))
|
||||
|
||||
log("Installing face_recognition")
|
||||
|
||||
# Install face_recognition though pip
|
||||
handleStatus(subprocess.call(["pip3", "install", "face_recognition"]))
|
||||
|
||||
log("Cloning howdy")
|
||||
|
||||
# Make sure /lib/security exists
|
||||
if not os.path.exists("/lib/security"):
|
||||
os.makedirs("/lib/security")
|
||||
|
||||
# Clone howdy into it
|
||||
handleStatus(subprocess.call(["git", "clone", "https://github.com/Boltgolt/howdy.git", "/lib/security/howdy"]))
|
||||
|
||||
# Manually change the camera id to the one picked
|
||||
for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1):
|
||||
print(line.replace("device_id = 1", "device_id = " + picked), end="")
|
||||
|
||||
# Secure the howdy folder
|
||||
handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True))
|
||||
|
||||
# Make the CLI executable as howdy
|
||||
handleStatus(subprocess.call(["ln -s /lib/security/howdy/cli.py /usr/bin/howdy"], shell=True))
|
||||
handleStatus(subprocess.call(["chmod +x /usr/bin/howdy"], shell=True))
|
||||
|
||||
# Install the command autocomplete, don't error on failure
|
||||
subprocess.call(["sudo cp /lib/security/howdy/autocomplete.sh /etc/bash_completion.d/howdy"], shell=True)
|
||||
|
||||
log("Adding howdy as PAM module")
|
||||
|
||||
# Will be filled with the actual output lines
|
||||
outlines = []
|
||||
# Will be fillled with lines that contain coloring
|
||||
printlines = []
|
||||
# Track if the new lines have been insterted yet
|
||||
inserted = False
|
||||
|
||||
# Open the PAM config file
|
||||
with open("/etc/pam.d/common-auth") as fp:
|
||||
# Read the first line
|
||||
line = fp.readline()
|
||||
|
||||
while line:
|
||||
# Add the line to the output directly, we're not deleting anything
|
||||
outlines.append(line)
|
||||
|
||||
# Print the comments in gray and don't insert into comments
|
||||
if line[:1] == "#":
|
||||
printlines.append("\033[37m" + line + "\033[0m")
|
||||
else:
|
||||
printlines.append(line)
|
||||
|
||||
# If it's not a comment and we haven't inserted yet
|
||||
if not inserted:
|
||||
# Set both the comment and the linking line
|
||||
line_comment = "# Howdy IR face recognition\n"
|
||||
line_link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n"
|
||||
|
||||
# Add them to the output without any markup
|
||||
outlines.append(line_comment)
|
||||
outlines.append(line_link)
|
||||
|
||||
# Make the print orange to make it clear what's being added
|
||||
printlines.append("\033[33m" + line_comment + "\033[0m")
|
||||
printlines.append("\033[33m" + line_link + "\033[0m")
|
||||
|
||||
# Mark as inserted
|
||||
inserted = True
|
||||
|
||||
# Go to the next line
|
||||
line = fp.readline()
|
||||
|
||||
# Print a file Header
|
||||
print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m")
|
||||
|
||||
# Loop though all printing lines and use the enters from the file
|
||||
for line in printlines:
|
||||
print(line, end="")
|
||||
|
||||
# Print a footer
|
||||
print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n")
|
||||
|
||||
# Ask the user if this change is okay
|
||||
print("Lines will be insterted in /etc/pam.d/common-auth as shown above")
|
||||
ans = input("Apply this change? [y/N]: ")
|
||||
|
||||
# Abort the whole thing if it's not
|
||||
if (ans.lower() != "y"):
|
||||
print("Inerpeting as a \"NO\", aborting")
|
||||
sys.exit()
|
||||
|
||||
print("Adding lines to PAM\n")
|
||||
|
||||
# Write to PAM
|
||||
common_auth = open("/etc/pam.d/common-auth", "w")
|
||||
common_auth.write("".join(outlines))
|
||||
common_auth.close()
|
||||
|
||||
# From here onwards the installation is complete
|
||||
# We want to gather more information about the types or IR camera's
|
||||
# used though, and the following lines are data collection
|
||||
|
||||
# List all video devices
|
||||
diag_out = "Video devices [IR=" + picked + "]\n"
|
||||
diag_out += "```\n"
|
||||
diag_out += subprocess.check_output(['ls /dev/ | grep video'], shell=True).decode("utf-8")
|
||||
diag_out += "```\n"
|
||||
|
||||
# Get some info from the USB kernel listings
|
||||
diag_out += "Lsusb output\n"
|
||||
diag_out += "```\n"
|
||||
diag_out += subprocess.check_output(['lsusb -vvvv | grep -i "Camera\|iFunction"'], shell=True).decode("utf-8")
|
||||
diag_out += "```\n"
|
||||
|
||||
# Get camera information from video4linux
|
||||
diag_out += "Udevadm\n"
|
||||
diag_out += "```\n"
|
||||
diag_out += subprocess.check_output(['udevadm info -r --query=all -n /dev/video' + picked + ' | grep -i "ID_BUS\|ID_MODEL_ID\|ID_VENDOR_ID\|ID_V4L_PRODUCT\|ID_MODEL"'], shell=True).decode("utf-8")
|
||||
diag_out += "```"
|
||||
|
||||
# Print it all as a clickable link to a new github issue
|
||||
print("https://github.com/Boltgolt/howdy-reports/issues/new?title=Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n")
|
||||
|
||||
# Let the user know what to do with the link
|
||||
print("Installation complete.")
|
||||
print("If you want to help the development, please use the link above to post some camera-related information to github")
|
||||
|
||||
# Remove the installer if it was downloaded to /tmp
|
||||
if os.path.exists("/tmp/howdy_install.py"):
|
||||
os.remove("/tmp/howdy_install.py")
|
||||
6
pam.py
6
pam.py
|
|
@ -1,4 +1,4 @@
|
|||
# PAM interface in python, launches compair.py
|
||||
# PAM interface in python, launches compare.py
|
||||
|
||||
# Import required modules
|
||||
import subprocess
|
||||
|
|
@ -15,8 +15,8 @@ config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
|
|||
def doAuth(pamh):
|
||||
"""Start authentication in a seperate process"""
|
||||
|
||||
# Run compair as python3 subprocess to circumvent python version and import issues
|
||||
status = subprocess.call(["python3", os.path.dirname(os.path.abspath(__file__)) + "/compair.py", pamh.get_user()])
|
||||
# Run compare as python3 subprocess to circumvent python version and import issues
|
||||
status = subprocess.call(["python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()])
|
||||
|
||||
# Status 10 means we couldn't find any face models
|
||||
if status == 10:
|
||||
|
|
|
|||
36
uninstall.py
Normal file
36
uninstall.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Completely remove howdy from the system
|
||||
|
||||
# Import required modules
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Check if we're running as root
|
||||
user = os.getenv("SUDO_USER")
|
||||
if user is None:
|
||||
print("Please run the uninstaller as a sudo user")
|
||||
sys.exit()
|
||||
|
||||
# Double check with the user for the last time
|
||||
print("This will remove Howdy and all models generated with it")
|
||||
ans = input("Do you want to continue? [y/N]: ")
|
||||
|
||||
# Abort if they don't say yes
|
||||
if (ans.lower() != "y"):
|
||||
sys.exit()
|
||||
|
||||
# Remove files and symlinks
|
||||
subprocess.call(["rm -rf /lib/security/howdy/"], shell=True)
|
||||
subprocess.call(["rm /usr/bin/howdy"], shell=True)
|
||||
subprocess.call(["rm /etc/bash_completion.d/howdy"], shell=True)
|
||||
|
||||
# Remove face_recognition and dlib
|
||||
subprocess.call(["pip3 uninstall face_recognition dlib -y"], shell=True)
|
||||
|
||||
# Print a tearbending message
|
||||
print("""
|
||||
Howdy has been uninstalled :'(
|
||||
|
||||
There are still lines in /etc/pam.d/common-auth that can't be removed automatically
|
||||
Run "nano /etc/pam.d/common-auth" to remove them by hand\
|
||||
""")
|
||||
25
utils.py
25
utils.py
|
|
@ -1,25 +0,0 @@
|
|||
# Useful support functions
|
||||
|
||||
def print_menu(encodings):
|
||||
"""Show a menu asking the user what he wants to do"""
|
||||
if len(encodings) == 3:
|
||||
print("There is 1 existing face model for this user")
|
||||
else:
|
||||
print("There are " + str(int(len(encodings) / 3)) + " existing face models for this user")
|
||||
print("What do you want to do?\n")
|
||||
|
||||
print("1: Add additional face model")
|
||||
print("2: Overwrite older model(s)")
|
||||
print("0: Exit")
|
||||
|
||||
com = input("Option: ")
|
||||
|
||||
if com == "1":
|
||||
return encodings
|
||||
elif com == "2":
|
||||
return []
|
||||
elif com == "0":
|
||||
sys.exit()
|
||||
else:
|
||||
print("Invalid option '" + com + "'\n")
|
||||
return print_menu(encodings)
|
||||
Loading…
Reference in a new issue