使用Raspberry Pi,Arduino和Python进行活体检测检测

生物识别漏洞

几乎所有人脸识别系统都有一个明显的漏洞 - 像这样的硅胶面具:
这里写图片描述
被视为真面目。此外,人们在戴着乳胶面具时抢劫银行 并欺骗移民,乳胶面具变得越来越复杂和随时可用。我买了这个低端面具:
这里写图片描述

在伦敦一家服装店约30英镑。推销员告诉我,大都会警察每隔几周就会联系一名戴着面具犯罪的人。警察实际上到所有商店去搜索销售收据(记住:用现金支付)。

2009年,我 和Ted Dunstone一起写了一本书,我们在其中包含了一章关于生物识别漏洞的内容。我们预测,随着识别系统变得越来越普遍,人们将开始寻找新的方法来绕过它们。我们建议供应商不再仅仅专注于真实演示的匹配准确性,而是开发现场和欺骗检测技术。不幸的是,这是一个在很大程度上被忽视的问题,正如最近的iPhone 5s指纹扫描仪攻击所证明的那样 。一些商业人脸识别系统可以通过更容易的演示攻击进行欺骗 - 只需在登记的人身上放置照片或视频剪辑(例如在iPad上)以使用他们的身份获取访问权限。

据我所知,没有专门针对这个问题的产品。我认为看看是否可以使用廉价的组件和开源软件开发一个简单的欺骗检测系统会很有趣。

伪装检测

仅使用来自可见光谱的信息难以检测掩模。然而,在远红外(IR)光谱中,真实面具有独特的热特征:
这里写图片描述
掩模的表面温度更接近环境温度,并且热分布更均匀。在可见光波长和远红外线中创建一个看似人类的掩模将是一个挑战。

不幸的是,高分辨率热像仪(如上图所示)非常昂贵 - 从5,000美元到50,000美元不等。我们的想法是使用多光谱成像:将高分辨率可见波长相机与低分辨率红外传感器结合使用。基本概念很简单。受试者必须具有看起来 像脸的东西 ,并且这个面部对象的温度必须与人类活皮肤的温度一致。想办法解决这个问题并不难,但这个相对简单的系统可以帮助防范最简单和最常见的恶搞。

组装检测单元
1.你需要什么
Raspberry Pi:由于其低成本,低功耗要求和活跃的社区,这是该项目的完美计算机。
红外传感器:有几种选择。松下制作了 Grid-EYE。我们使用的是 Melexis MLX90620,分辨率为16×4。这种传感器的优点是价格便宜,并且有一些开源代码可用(见下文)。RoBoard构建了一个 方便的MLX90620模块 ,可以直接连接到控制器。它有点贵,但它让我免于焊接几个位。
Arduino:我使用的是Nano,但是任何一块板子(比如Uno)都没问题。
相机:我使用官方的Raspberry Pi相机板。但是,任何兼容RPi的网络摄像头都可以。
其他:有源USB集线器,电线和电缆,LED,wifi加密狗,外壳等。
2.连接红外传感器
我花了一些时间试图让Raspberry Pi和MLX90620直接相互通话。我取得了一些进展,但最终我放弃了,因为RPi的I2C库和一些Melexis组件之间似乎存在不兼容性(详见 本文 )。将它连接到Arduino很简单。这里的大部分信息 都 适用于MLX90620,Arduino官方论坛上有一些有用的帖子。

这是RoBoard的IR组件连接到Arduino Nano:
这里写图片描述
arduino_ir

我使用SparkFun的github repo中的Arduino草图, 使用I2C协议与传感器连接。我做了一些小修改,以支持串行通信,控制LED,并将温度值存储为字节:

int LEDs[] = {4, 5, 6}; // the pins used for connecting the LED

// NOTE: I have not included all of the global variables and helper
//   functions here, as these are mostly unchanged. Please see
//   the SparkFun github repo link above for the rest of the source.

void setLED(int r, int g, int b)
{
  digitalWrite(LEDs[0], r);
  digitalWrite(LEDs[1], g);
  digitalWrite(LEDs[2], b);
}

void setup()
{
  Serial.begin(115200);

  // Init the I2C pins
  i2c_init();

  // Enable pull-ups
  PORTC = (1 << PORTC4) | (1 << PORTC5);
  delay(5); //Init procedure calls for a 5ms delay after power-on

  // Read the entire EEPROM
  read_EEPROM_MLX90620();

  // Configure the MLX sensor with the user's choice of refresh rate
  setConfiguration(refreshRate);

  // Calculate the current Tambient
  calculate_TA();

  // Init LED pins
  pinMode(LEDs[0], OUTPUT);
  pinMode(LEDs[1], OUTPUT);
  pinMode(LEDs[2], OUTPUT);
}

void loop()
{
  int incomingByte = 0;

  // Tambient changes more slowly than the pixel readings, so
  //  update TA only every X loops.
  if(loopCount++ == 20)   {
    // Calculate the new Tambient
    calculate_TA();

    // Check that the POR flag is not set
    if(checkConfig_MLX90620())
    {
      // Re-write the configuration bytes to the MLX
      setConfiguration(refreshRate);
    }
    loopCount = 0; // Reset count
  }

  // check if there has been any requests
  if (Serial.available() < 0) {
    char c = Serial.read();
    if (c == 'I') {
      // Get the 64 bytes of raw pixel data into the irData array
      readIR_MLX90620();

      // Run the calculations to fill the temperatures array
      calculate_TO();
      for (int i = 0 ; i < 64 ; i++)
      {
        Serial.write(temperatures[i]);
      }
      Serial.println('');
    }
    else if (c == 'R') { setLED(HIGH, LOW, LOW); }
    else if (c == 'G') { setLED(LOW, HIGH, LOW); }
    else if (c == 'B') { setLED(LOW, LOW, HIGH); }
    else if (c == 'x') { setLED(LOW, LOW, LOW); }
  }
  delay(50);
}

3.连接Raspberry Pi和Ardunio

下一步是在Raspberry Pi和Arduino之间建立通信。Raspberry Pi需要能够发出两种类型的命令:

要求获取最新温度值
更改LED颜色的说明
设置通信有三种标准方法:通过串行USB电缆,GPIO上的串行引脚和使用I2C(本网站讨论所有这三种方法 )。我们将使用USB,因为它可靠,设置简单,也可用于为Arduino供电(尽管确保使用有源USB集线器)。

把它放在一个盒子里
我选择了尺寸为11cm x 15cm x 7cm的盒子。我钻了三个孔:一个在摄像头前面,另一个在前面用于红外传感器,另一个在后面用于LED。有源USB集线器是最大的组件 - 我需要拆开它并将其切断,以便它适合我的盒子:
这里写图片描述
在上图中,您可以看到相机和红外传感器的孔。这里包含所有内容:
这里写图片描述

如你所见,有相当多的空白空间。但是,厚USB线占用了相当多的空间,所以我无法使用更小的盒子。设计合理的外壳会更加紧凑。

编程Raspberry Pi

1.安装库
推荐但可选的步骤是使用sudo raspi-config 选项对Raspberry Pi进行超频 。这将使一切运行得更快一些。

接下来,我们将确保Raspberry Pi上的所有内容都是最新的,并安装一些库:PIL,numpy,scipy,OpenCV,pySerial和其他各种库。在shell中执行以下命令:

sudo rpi-update
sudo apt-get update
sudo apt-get dist-upgrade
sudo apt-get install python-setuptools
sudo apt-get install git-core
sudo apt-get install cmake
sudo apt-get install python-numpy
sudo apt-get install python-scipy
sudo apt-get install python-imaging
sudo apt-get install libopencv-dev
sudo apt-get install python-opencv
sudo apt-get install python-serial

完成这些(可能需要一段时间)后,我们 从github repo 安装最新的 scikit-image:

sudo python -m easy_install cython
git clone http://github.com/scikit-image/scikit-image.git
cd scikit-image/
sudo python setup.py install

运行以下Python脚本以确保所有内容都已正确安装,并且RPI和Arduino之间的通信正在运行:

import serial
import time
import skimage
import cv2

# establish a connection with the Arduino
ser = serial.Serial('/dev/ttyUSB0', 115200)
time.sleep(1)
ser.setDTR(level=0)
time.sleep(1)

# test the LED
for color in ['R', 'G', 'B']:
    ser.write(color)
    time.sleep(1)
ser.write('x')

# get the most recent temperature readings
ser.write('I')
print ser.readline()
如果一切正常,它将循环显示所有LED颜色并打印出最新

2.用相机板拍摄图像

我在这个项目中使用了官方Rasberry Pi相机板。相机拍摄的图像质量非常高。然而,在撰写本文时,RPi基金会尚未发布相机的官方Video4Linux驱动程序,这使得使用Python变得更加困难。有一些解决方法。运行以下Python脚本以捕获图像,将其转换为灰度,并均衡直方图:

import subprocess
import skimage
from skimage import io, exposure

def AcquireImage():
    fn = r'/run/shm/temp.jpg'
    proc = subprocess.Popen('raspistill -o %s -w 480 -h 320 -n -t 0'%(fn),
                            shell=True, stderr=subprocess.STDOUT)
    proc.wait()
    im = io.imread(fn, as_grey=True)
    im = exposure.equalize_hist(im)

    return skimage.img_as_ubyte(im)

im = AcquireImage()
io.imsave('image.jpg', im)

3.将IR传感器输出与光学图像对齐

让我们来看看我们现在拥有的东西:

Arduino可以
(1)从IR传感器获得温度读数,
(2)设置LED的颜色Raspberry Pi可以与Arduino通信,并可以拍照
下图显示了我的测试设置。如您所见,看到相机和传感器彼此靠近,并指向相同的方向:
我们现在需要做的是将温度值与光学图像中的相应位置对齐。换句话说,我们需要对16×4温度阵列应用旋转,缩放和平移。起初我试图手动调整转换参数,但这是不准确和繁琐的。以下是我用于自动化对齐阶段的技术的高级概述:
这里写图片描述
用热水填充红色咖啡杯
在拍摄区域周围移动杯子时拍摄60张照片。对于每张照片,记录相应的IR数组值。
找到马克杯的位置:
照片:应用图像处理技术找到红色斑点的中间部分
红外阵列:IR阵列尺寸的两倍(从16×4到32×8),应用高斯滤波器,找到峰值温度的位置。这是IR中杯子的(内插)位置。
步骤3产生60对对应点。现在找到旋转,缩放和平移参数,这些参数可以在应用变换后最小化点之间的(最小二乘)距离。这是一个经典的优化问题,可以使用scipy.optimize.minimizePowell方法的实现来解决 。
以下是与光学图像对齐的(平滑)温度读数的示例:
如您所见,由于杯子很热,因此红外光谱中存在明显的峰值。请注意,由于杯子处于运动状态(朝向我),因此存在轻微的错位,并且温度读数存在滞后现象。
这里写图片描述
我发现MLX90620可以提供准确/一致的温度读数。我最大的抱怨是宽高比(4:1)对于这个应用来说不方便。如果它垂直定向,则该单元将支持一系列用户高度,但该单元需要直接指向主体。另一方面,如果它是水平定向的(如上图所示),则主体可以更自由地左右移动,并且一次可以跟踪多个人。但是,如果受试者特别短或高,则可能存在问题。我最终使用了垂直方向。

我有兴趣测试松下Grid-EYE,它也有64像素,但它们排列成方形(8×8)。我相信不久之后,一些廉价的高分辨率红外组件就会上市。

4.活体检测

我们现在已经准备好所有的活动以进行活体检测。过程如下:

  1. 拍个照
  2. 使用OpenCV的面部发现器查找照片中的面部
  3. 如果找到的面数为0或大于1,则返回步骤1
  4. 从红外传感器获取温度阵列
  5. 找到脸部的温度
  6. 如果面部温度低于经验确定的阈值(例如25°C):
  7. 将面部分类为假的,并照亮红色LED
  8. 如果面部温度高于阈值:
  9. 将脸部分类为真实,并照亮绿色LED
  10. 可选:以JSON格式保存所有结果,Web服务器将使用该格式

    代码如下:

import numpy as np
import cv2
import subprocess
import os, sys, time
import serial
import json
import datetime
import skimage
from skimage import io, exposure, transform

FACE_TEMP_THRESH = 25
JSON_DIR = r'/home/pi/workspace/disguise/data'
IMAGE_DIR = r'[path to web2py application folder]/static/images/'
FACE_CASCADE_FN = r'[OpenCV cascades]/haarcascade_frontalface_alt.xml'

# IR registration parameters
ROTATION = 0
SCALE = (34.7, 34.8)
TRANSLATION = (70, 86)

# initialize communication with Andriod
ser = serial.Serial('/dev/ttyUSB0', 115200)
time.sleep(1)
ser.setDTR(level=0)
time.sleep(1)
ser.write('B')
time.sleep(1)

def AcquireImage():
    fn = r'/run/shm/temp.jpg';
    proc = subprocess.Popen('raspistill -o %s -w 480 -h 320 -n -t 0'%(fn),
                            shell=True, stderr=subprocess.STDOUT)
    proc.wait()
    im = io.imread(fn, as_grey=True)
    im = exposure.equalize_hist(im)

    return skimage.img_as_ubyte(im)

def MainLoop():
    # load the OpenCV face finder
    faceClassifier = cv2.CascadeClassifier(FACE_CASCADE_FN)
    file_count = 0

    while True:
        file_count += 1

        # clear the LED
        ser.write('x')

        # take a photo and look for a face
        im = AcquireImage()
        faceRects = faceClassifier.detectMultiScale(im,
                         1.2, 2, cv2.cv.CV_HAAR_SCALE_IMAGE)
        print 'Faces detected: %d' % (len(faceRects))
        if len(faceRects) != 1:
            # no faces found, so take a new photo
            continue

        # get face coordinates, and make sure the face
        #  isn't too far away
        x, y, w, h = faceRects[0]
        if w < 80:
            print 'Face too far: %d' % (w)
            continue

        # get the temperature array, and align with the image
        ser.write('I')
        ir_raw = ser.readline()
        ir = np.frombuffer(ir_raw.strip(), np.uint8).astype(np.float32)
        ir = ir.reshape((16, 4))[::-1, ::-1]

        # align the IR array with the image
        tform = transform.AffineTransform(scale=SCALE, rotation=ROTATION,
                                          translation=TRANSLATION)
        ir_aligned = transform.warp(ir, tform.inverse, mode='constant',
                                    output_shape=im.shape)

        # set the temperature of the face as the maximum
        #  in the face bounding box
        face_temp = ir_aligned[y:y+h, x:x+w].max()

        # create a dictionary to store the results
        results = {}
        results['timestamp'] = str(datetime.datetime.now())
        results['faces_found'] = str(len(faceRects))
        results['upper_left_x'] = str(x)
        results['upper_left_y'] = str(y)
        results['face_width'] = str(w)
        results['face_height'] = str(h)
        results['face_termperature'] = str(face_temp)
        results['disguise_detected'] = str(face_temp < FACE_TEMP_THRESH)
        results['ir_values'] = ','.join('%0.02f'%(f) for f in ir.flat)

        # determine face vs non-face based on temperature
        if face_temp < FACE_TEMP_THRESH:
            ser.write('G')
            time.sleep(5)
        else:
            ser.write('R')
            time.sleep(5)

        # save image in a place where the web server can display it
        fn = 'capture_%03d.jpg' % (file_count%100)
        results['optical_image_filename'] = fn
        io.imsave(os.path.join(IMAGE_DIR, fn), im)

        # save the results dictionary as a JSON file
        fn = os.path.join(JSON_DIR,'results_%03d.json' % (file_count%100))
        with open(fn, 'w') as f:
            f.write(json.dumps(results))

if __name__ == '__main__':
    MainLoop()

5. web2py Web界面

最后一步是可选的。对于到目前为止所描述的设备,唯一的输出方式是后部的LED指示灯。但是,在许多情况下,通过网络远程监控结果会更方便。 web2py是一个开源Python Web框架,非常适合这个应用程序。它捆绑了快速启动和运行应用程序所需的一切,包括Web服务器,管理界面和IDE(在进行无头RPi开发时非常方便)。

要在Raspberry Pi上安装web2py,请从终端执行以下操作:

wget http://web2py.com/examples/static/web2py_src.zip
unzip web2py_src.zip
cd web2py

请注意,要远程使用管理界面,您需要生成自签名SSL证书。完成后,执行以下命令启动web2py

sudo python web2py.py -i 0.0.0.0 -p 8000 -c server.crt -k server.key

输入管理员密码,访问https://[ip address of your RPi]:8000/,您就可以开始了。web2py可以使用数据库后端,用户身份验证等为您创建模板应用程序。对于我们的应用程序,我们创建了一个控制器,而不是以JSON的形式返回最新结果:

import glob
import os
import json

JSON_DIR = r'/home/pi/workspace/disguise/data'

def index():
    # load most recent JSON file
    files = glob.glob(os.path.join(JSON_DIR, '*.json'))
    if len(files) == 0: return response.json([])
    files.sort(key=lambda x: os.path.getmtime(x))
    fn = files[-1]

    with open(fn) as f:
        return response.json(json.loads(f.read()))

演示视频

以下视频演示了一些演示攻击,从相机的角度来看。第一次攻击使用印刷的纸板面具(可在网上获得几美元),第二次攻击使用在平板电脑上播放的视频剪辑。此演示使用网络摄像头和PC创建,而不是上面概述的Raspberry Pi方法。结果相同但帧速率更高。红 盒子 =假脸,绿 盒 =真面目:
视频网址:https://youtu.be/Z-U-eXakxy0
以下视频显示了基于Raspberry Pi的检测器。这有点难以看到,但要注意设备后部的LED。红色LED =检测到假冒,绿色LED =未检测到假冒
演示视频2:https://youtu.be/UGyM817b1gc

总结

这个系统并非万无一失。想办法击败它并不难,但也不难想出改进它的方法(例如使用欧拉视频放大检测脉冲)。重点是,如果我们要依赖生物识别系统(例如边境控制),我们需要了解它们的局限性,并研究如何使它们更安全。

此盒子还有许多其他潜在用途。从本质上讲,它是:(低成本硬件)+(开源软件)+(计算机视觉)+(远程温度感应)的组合。这里有一些想法:

相同的系统可用于检测体温异常高或低的人(或动物)。例如,检测有发烧症状的人。
皮肤温度可以洞察计算机用户的情绪状态。例如,它可能能够检测用户体验测试的挫败程度,或者与在线治疗系统集成。

猜你喜欢

转载自blog.csdn.net/fengzhongluoleidehua/article/details/80953194