В нашем офисе блокировка экрана - это привычка, которую нужно быстро выработать. Потому что, если вы оставите свой компьютер разблокированным, кто-то повеселится и поменяет ваши обои или псевдоним `sudo` на что-нибудь;)

И однажды я начал думать, а почему бы мне не автоматизировать это? И вот я перехожу к библиотеке Python для распознавания лиц. Его действительно легко настроить и использовать.

Но обо всем по порядку. Нам нужно проверить, можно ли заблокировать экран с Python и как это сделать.

Блокировка экрана

Я использую Linux Mint с окружением рабочего стола Cinnamon. И, к счастью, в случае с Cinnamon довольно легко заблокировать или разблокировать экран с помощью команды заставки.

cinnamon-screensaver-command --activate  # to lock the screen
cinnamon-screensaver-command --deactivate  # to unlock the screen

И запустить команду терминала из python совсем несложно:

from subprocess import call
LOCK_ARGS = {
    True: '--activate',
    False: '--deactivate',
}

def lock_screen(lock):
    call(('cinnamon-screensaver-command', LOCK_ARGS[lock]))

lock_screen(True)  # will lock the screen
lock_screen(False)  # will unlock the screen

Настройка face_recognition

Следующий шаг - узнать свое прекрасное лицо. Воспользуемся библиотекой распознавания лиц. В репозитории можно найти много хороших примеров, думаю, тот нам пригодится.

Он использует OpenCV для захвата потока с камеры. Также я решил использовать конституционную нейронную сеть для определения лиц в кадре. Чтобы точность была лучше.

from threading import Timer
import cv2
import face_recognition

def load_user_encoding():
    user_image = face_recognition.load_image_file(os.path.join(BASE_DIR, 'user.jpg'))
    user_image_face_encoding = face_recognition.face_encodings(user_image, num_jitters=10)[0]

    return user_image_face_encoding

def find_user_in_frame(frame, user_encoding):
    face_locations = face_recognition.face_locations(frame, model='cnn')
    face_encodings = face_recognition.face_encodings(frame, face_locations, num_jitters=2)

    for face_encoding in face_encodings:
        matches = face_recognition.compare_faces((user_encoding, ), face_encoding, tolerance=0.9)

        return any(matches)

if __name__ == '__main__':
    user_encoding = load_user_encoding()
    video_capture = cv2.VideoCapture(0)  # get a reference to webcam #0 (the default one)

    lock_timer = None
    process_this_frame = True
    
    while True:
        ret, frame = video_capture.read()
        small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
        rgb_small_frame = small_frame[:, :, ::-1]

        if process_this_frame:
            user_found = find_user_in_frame(rgb_small_frame, user_encoding)

            if user_found:
                print('user found')
                lock_screen(False)

                if lock_timer is not None:  # cancel lock timer if it exists
                    lock_timer.cancel()
                    lock_timer = None
            else:
                print('user not found')

                if lock_timer is None:  # start timer if it's not started already
                    lock_timer = Timer(5, lock_screen, (True,))
                    lock_timer.start()

        process_this_frame = not process_this_frame

Как видите, я использовал threading.Timer для блокировки экрана через 5 секунд, если пользователь не найден. Я рекомендую немного подождать, прежде чем заблокировать экран, потому что иногда он не может распознать ваше лицо на некоторых кадрах. Или вы можете просто отвернуться на мгновение.

Оптимизация

С этим решением у него есть неприятная задержка для чтения кадра и плохой кадровый пропуск. Поэтому я решил оптимизировать его и переместить процесс распознавания в отдельный процесс с помощью multiprocessing

Прежде всего, нам нужно переписать нашу функцию для поиска пользователя, чтобы она могла вызываться как Process с Pipe вместо return:

def find_user_in_frame(conn, frame, user_encoding):
    face_locations = face_recognition.face_locations(frame, model='cnn')
    face_encodings = face_recognition.face_encodings(frame, face_locations, num_jitters=2)

    found_user = False
    for face_encoding in face_encodings:
        matches = face_recognition.compare_faces((user_encoding, ), face_encoding, tolerance=0.9)

        found_user = any(matches)
        if found_user:
            break

    conn.send(found_user)

И после этого нам нужно вызвать эту функцию, используя multiprocessing.Process в основном цикле:

if __name__ == '__main__':
    user_encoding = load_user_encoding()
    video_capture = cv2.VideoCapture(0)  # get a reference to webcam #0 (the default one)

    lock_timer = None

    parent_conn, child_conn = Pipe()
    find_user_process = None
    while True:
        ret, frame = video_capture.read()

        small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)

        rgb_small_frame = small_frame[:, :, ::-1]

        # if process of finding user is not working - start new one
        if find_user_process is None:
            find_user_process = Process(target=find_user_in_frame, args=(child_conn, rgb_small_frame, user_encoding))
            find_user_process.start()
        # if process of finding user is already working - check is it done
        elif find_user_process is not None and not find_user_process.is_alive():
            user_found = parent_conn.recv()
            find_user_process = None

            if user_found:
                print('user found')
                lock_screen(False)
                if lock_timer is not None:
                    lock_timer.cancel()
                    lock_timer = None
            else:
                print('user not found')
                if lock_timer is None:
                    lock_timer = Timer(LOCK_TIMEOUT, lock_screen, (True,))
                    lock_timer.start()

Теперь он работает более плавно и задержка минимальна.

Спасибо за прочтение! Надеюсь, это было интересно и вам понравилось.

Исходный код можно найти на github:
https://github.com/Ignisor/face-screenlock

Игнисор при поддержке Go Wombat Team