在Windows 10环境中,我们使用dlib19.17+face_recognition对家庭照片进行分类,速度可以达到4-6秒/张,相关内容请参考:

Windows 10+dlib19.17+face_recognition:使用人脸识别对家庭照片进行分类,4-6秒/张(一)单线程

我们对这样的速度不太满意,打算提高它的运行速度(或者说:缩短整个任务的运行时间),在上一篇文章中我们发现Python的多线程不能提高并行效率,但可以用同时运行多个程序来缩短任务运行时间:

Windows 10+dlib19.17+face_recognition:使用人脸识别对家庭照片进行分类,4-6秒/张(二)并行加速

(1)多进程并行加速

在本文中,我们尝试用Python多进程来提高任务执行效率,代码如下:

# -*- coding: UTF-8 -*-

import dlib
import face_recognition
import numpy as np
from multiprocessing import Process
from multiprocessing import Pool
from datetime import datetime
import os
import shutil

#全局变量
number_process = 3

#人脸检测进程,将文件名转化为整数,按整除的余数分类待处理文件,若命中则复制文件到指定目录
def face_check_and_copy(num):

    # 从图片中加载已知的人脸并获得编码
    steve_image = face_recognition.load_image_file("f:/images/steve.jpg")
    steve_face_encoding = face_recognition.face_encodings(steve_image)[0]

    lucy_image = face_recognition.load_image_file("f:/images/lucy.jpg")
    lucy_face_encoding = face_recognition.face_encodings(lucy_image)[0]

    known_face_encodings = [
        steve_face_encoding,
        lucy_face_encoding,
    ]

    known_face_names = [
        "Steve",
        "Lucy",
    ]
    count_checked, count_copied, count_failed = 0, 0, 0

    # 遍历目录下的所有.jpg文件
    f = os.walk("f:\images")
    for path, d, filelist in f:
        for filename in filelist:
            if filename.endswith('jpg'):
                #将文件名的字符转化为ASCII码,累计得到一个整数
                only_name = os.path.splitext(filename)[0]
                int_filename = 0
                for every_char in only_name:
                    int_filename += ord(every_char)

                if int_filename % number_process == num:
                    image_path = os.path.join(path, filename)

                    # 加载图片
                    try:
                        unknown_image = face_recognition.load_image_file(image_path)
                    except:
                        print('%s.jpg load_image_file() failed.' %only_name)
                        count_failed += 1
                        continue

                    # 找到图中所有人脸的位置
                    try:
                        face_locations = face_recognition.face_locations(unknown_image)
                    except:
                        print('%s.jpg face_location() failed.' %only_name)
                        count_failed += 1
                        continue

                    # face_locations = face_recognition.face_locations(unknown_image, number_of_times_to_upsample=0, model="cnn")

                    # 根据位置加载人脸编码的列表
                    try:
                        face_encodings = face_recognition.face_encodings(unknown_image, face_locations)
                        count_checked += 1
                    except:
                        print('%s.jpg face_encodings() failed.' %only_name)
                        count_failed += 1
                        continue

                    # 遍历所有人脸编码,与已知人脸对比
                    for (top, right, bottom, left), face_encoding in zip(face_locations, face_encodings):
                        # 获得对比结果,tolerance值越低比对越严格
                        try:
                            matches = face_recognition.compare_faces(known_face_encodings, face_encoding, tolerance=0.4)
                        except:
                            print('%s.jpg compare_faces() failed.' % only_name)
                            count_failed += 1
                            break

                        # 获得比对成功的姓名(未做进一步处理),并复制文件到指定目录
                        name = "Unknown"
                        if True in matches:
                            first_match_index = matches.index(True)
                            name = known_face_names[first_match_index]
                            # 若同一个图片中有多个已知人脸,重复复制文件会报错
                            try:
                                shutil.copy(image_path, "f:/images_family")
                                count_copied += 1
                                break
                            except shutil.Error:
                                break
    print('No.%d process: %d pictures checked, %d pics copied, and %d pics failed.' %(num, count_checked, count_copied, count_failed))

#主进程开始
if __name__ == '__main__':

    #显示有几个CPU(硬件CPU的线程数),Pool()最大进程数默认为CPU的个数
    print('%d CPUs found.' %os.cpu_count())

    # 测试起始时间
    t1 = datetime.now()

    #创建子进程池,子进程异步并发执行
    poo = Pool()
    for i in range(number_process):
        poo.apply_async(face_check_and_copy, (i,))

    # 关闭子进程池
    poo.close()
    # 等待所有子进程结束
    poo.join()

    # 测试结束时间
    t2 = datetime.now()

    # 显示总的时间开销
    print('time spend: %d seconds, %d microseconds.' %((t2-t1).seconds, (t2-t1).microseconds))

测试结果表明,2个进程并发执行,相比单线程时的205秒,50张图片的人脸识别任务的总运行时间减少了40%,体现出并行计算加速的效果(因为把“从图片中加载已知的人脸并获得编码”的处理放到了每个子进程中,因此稍微增加了子进程的耗时),CPU的利用率大概在33%-74%之间,瞬间有摸高到100%;内存使用率在75%-85%之间,最高到94%:

3个进程并发执行,任务的总运行时间减少了50%,CPU的利用率在50%-60%左右,内存使用率在85%以上,效率提高一倍:

使用3个进程对更大规模的文件集进行处理,58个文件夹,1841个文件(包括:图片,非图片),共7.44GB,程序检查了1690张图片,发现其中135张包含已知人脸并复制到指定目录下,共运行4107秒(1小时8分27秒),处理速度平均2.43秒/张,与之前单线程(也是单进程)最快的处理速度4.1秒/张比较,处理速度提高也超过了40%,这样的速度就可以满足一般的实用要求了:

但当我们设定number_process = 4或者更大的数字时,由于电脑内存不足(笔记本电脑的内存为8GB),face_recognition函数报错,会出现加载图片数据(load_image_file)失败或者放弃对目标图片进行人脸识别(face_locations)的情况。

所以,在使用多进程并行加速的时候,一方面要尽量挖掘硬件资源的潜力,同时也要考虑软件库函数的健壮性及可扩展性问题。

(2)增强应用的健壮性

我们在不改变底层软件库的情况下,试图在应用层层面加强整个程序的健壮性,思路是:当face_recognition函数报错,所在的应用进程进入睡眠,过几秒后再次调用face_recognition函数,直至失败次数达到程序设定的上限为止。代码如下:

# -*- coding: UTF-8 -*-

import dlib
import face_recognition
import numpy as np
from multiprocessing import Process
from multiprocessing import Pool
from datetime import datetime
import time
import os
import shutil

#全局变量:进程数, 函数调用失败时的重试次数
number_process = 4
times_retry = 20

#人脸检测进程,将文件名转化为整数,按整除的余数分类待处理文件,若命中则复制文件到指定目录
def face_check_and_copy(num):

    # 从图片中加载已知的人脸并获得编码
    steve_image = face_recognition.load_image_file("f:/images/steve.jpg")
    steve_face_encoding = face_recognition.face_encodings(steve_image)[0]

    lucy_image = face_recognition.load_image_file("f:/images/lucy.jpg")
    lucy_face_encoding = face_recognition.face_encodings(lucy_image)[0]

    known_face_encodings = [
        steve_face_encoding,
        lucy_face_encoding,
    ]

    known_face_names = [
        "Steve",
        "Lucy",
    ]
    count_checked, count_copied, count_failed = 0, 0, 0
    fail_flag = 0

    # 遍历目录下的所有.jpg文件
    f = os.walk("f:/images")
    for path, d, filelist in f:
        for filename in filelist:
            if filename.endswith('jpg'):
                #将文件名的字符转化为ASCII码,累计得到一个整数
                only_name = os.path.splitext(filename)[0]
                int_filename = 0
                for every_char in only_name:
                    int_filename += ord(every_char)

                if int_filename % number_process == num:
                    image_path = os.path.join(path, filename)

                    # 加载图片,失败时重试
                    for i in range(times_retry):
                        try:
                            unknown_image = face_recognition.load_image_file(image_path)
                        except:
                            if i == times_retry - 1:
                                count_failed += 1
                                print('%s.jpg load_image_file() failed.' %only_name)
                                fail_flag = 1
                                break
                            print('%s.jpg load_image_file() retried %d times.' %(only_name, i+1))
                            time.sleep(i + 1)
                            continue
                        else:
                            break
                    if fail_flag == 1:
                        fail_flag = 0
                        continue

                    # 找到图中所有人脸的位置,失败时重试
                    for i in range(times_retry):
                        try:
                            face_locations = face_recognition.face_locations(unknown_image)
                            # face_locations = face_recognition.face_locations(unknown_image, number_of_times_to_upsample=0, model="cnn")
                        except:
                            if i == times_retry - 1:
                                count_failed += 1
                                print('%s.jpg face_locations() failed.' %only_name)
                                fail_flag = 1
                                break
                            print('%s.jpg face_locations() retried %d times.' %(only_name, i+1))
                            time.sleep(i + 1)
                            continue
                        else:
                            break
                    if fail_flag == 1:
                        fail_flag = 0
                        continue

                    # 根据位置加载人脸编码的列表,失败时重试
                    for i in range(times_retry):
                        try:
                            face_encodings = face_recognition.face_encodings(unknown_image, face_locations)
                        except:
                            if i == times_retry - 1:
                                count_failed += 1
                                print('%s.jpg face_encodings() failed.' %only_name)
                                fail_flag = 1
                                break
                            print('%s.jpg face_encodings() retried %d times.' %(only_name, i+1))
                            time.sleep(i + 1)
                            continue
                        else:
                            count_checked += 1
                            break
                    if fail_flag == 1:
                        fail_flag = 0
                        continue

                    # 遍历所有人脸编码,与已知人脸对比
                    for (top, right, bottom, left), face_encoding in zip(face_locations, face_encodings):
                        # 获得对比结果,失败时重试
                        for i in range(times_retry):
                            try:
                                matches = face_recognition.compare_faces(known_face_encodings, face_encoding, tolerance=0.4)
                            except:
                                if i == times_retry - 1:
                                    count_failed += 1
                                    print('%s.jpg compare_faces() failed.' % only_name)
                                    fail_flag = 1
                                    break
                                print('%s.jpg compare_faces() retried %d times.' %(only_name, i+1))
                                time.sleep(i + 1)
                                continue
                            else:
                                break
                        if fail_flag == 1:
                            fail_flag = 0
                            break

                        # 获得比对成功的姓名(未做进一步处理),并复制文件到指定目录
                        name = "Unknown"
                        if True in matches:
                            first_match_index = matches.index(True)
                            name = known_face_names[first_match_index]
                            # 若同一个图片中有多个已知人脸,重复复制文件会报错
                            try:
                                shutil.copy(image_path, "f:/images_family")
                                count_copied += 1
                                break
                            except shutil.Error:
                                break
    print('No.%d process: %d pictures checked, %d pics copied, and %d pics failed.' %(num, count_checked, count_copied, count_failed))

#主进程开始
if __name__ == '__main__':

    #显示有几个CPU,Pool()最大进程数默认为CPU的个数
    print('%d CPUs found.' %os.cpu_count())

    # 测试起始时间
    t1 = datetime.now()

    #创建子进程池,子进程异步并发执行
    poo = Pool()
    for i in range(number_process):
        poo.apply_async(face_check_and_copy, (i,))

    # 关闭子进程池
    poo.close()
    # 等待所有子进程结束e
    poo.join()

    # 测试结束时间
    t2 = datetime.now()

    # 显示总的时间开销
    print('time spend: %d seconds, %d microseconds.' %((t2-t1).seconds, (t2-t1).microseconds))

4个进程并发执行, 调用face_recognition函数时频繁报错,而且报错以后所占用的内存一直不释放,直到所有子进程执行完毕、主进程结束时相关内存才最终得到释放。测试的结果很糟糕:

所以,结论是:真正靠谱的健壮性及可扩展性必须由软件库函数的底层来保证;应用层的加固虽然能部分增强程序的健壮性,但仅局限于有限的效果上(当我们同时运行其他占据大量内存的应用时,3个进程并发执行时也会出现调用face_recognition函数报错的情况,这时使用上面的加强健壮性的代码就能够安全处理这些异常,并最终顺利完成对所有图片文件的处理)。

(完)

 

 

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐