• 树莓派实现人脸打卡机


    之前用树莓派开发一套简易的视频监控平台,正好周日有时间,这次用树莓派实现了人脸打卡机。

    树莓派相关文章:

    1. 树莓派搭建nexus2.x私服
    2. 树莓派搭建视频监控平台
    3. 树莓派视频监控平台实现录制归档
    4. 树莓派实现人脸打卡机 (本文)

    1. 功能设计

    树莓派人脸打卡机,主要包括两个大方向的功能要求:
    a. 人脸采集存档
    b. 人脸识别签到
    这两个功能配合使用就能实现人脸打卡了, 通过人脸采集将人脸信息预存档在系统中,签到的时候,当人靠近摄像头时实时采集人脸,然后比对现有人脸,如果信息匹配则认为签到成功。

    下面是签到的效果:
    当人脸签到成功后,程序界面底部会显示签到时间和签到人的工号。

    raspi-face-sign-in

    2. 开发人脸采集模块

    人脸采集模块主要的工作就是从摄像头采集视频帧,然后交给界面回显,这里使用的是JavaCV中的opencv模块。
    频繁采集视频帧是一个很耗CPU的过程,我在这里做了一些优化处理,即:当检测到没有人脸的时候,程序休眠更长的时间(1秒),而当检测到人脸时,采集间隔调整为180毫秒。

    下面是完整的代码:

    /**
     * @author itqn
     */
    public class FaceCapture implements Runnable {
    
      private VideoCapture capture;
      private CascadeClassifier classifier;
      private OpenCVFrameConverter.ToMat matConvert;
      private JavaFXFrameConverter converter;
      private BiConsumer<Image, Rect> videoConsumer;
    
      public FaceCapture(BiConsumer<Image, Rect> videoConsumer) {
        this.videoConsumer = videoConsumer;
        init();
      }
    
      private void init() {
        capture = new VideoCapture();
        classifier = new CascadeClassifier("samples//haarcascade_frontalface_alt.xml");
        matConvert = new OpenCVFrameConverter.ToMat();
        converter = new JavaFXFrameConverter();
        capture.open(0);
      }
    
      private void destroy() {
        capture.close();
      }
    
      @Override
      public void run() {
        boolean find;
        Mat image = new Mat();
        RectVector vector = new RectVector();
        while (capture.isOpened()) {
          find = false;
          capture.read(image);
          classifier.detectMultiScale(image, vector);
          for (Rect rect : vector.get()) {
            find = true;
            Image video = converter.convert(matConvert.convert(image));
            videoConsumer.accept(video, rect);
            break;
          }
          // if no face sleep 1 second
          try {
            if (find) {
              TimeUnit.MILLISECONDS.sleep(180);
            } else {
              TimeUnit.SECONDS.sleep(1);
            }
          } catch (InterruptedException ignore) {
          }
        }
      }
    }
    

    这里调用者通过注册videoConsumer,来消费采集到的人脸图片,以及人脸区域。

    3. 开发人脸识别模块

    人脸识别这里直接采用opencv的native API,采用直方图对比的方式对比,这里采用相关性数据作为人脸识别成功的基准,如果相关度高于0.7则认为人脸匹配。
    程序通过将采集到的人脸信息跟已经存档的人脸信息注意对比,到达基准0.7以上则返回工号(图片是以工号命名的)。

    private static final double EXPECT_SCORE = 0.7d;
    
    public static String parser(String tmp, String dir) {
      Mat tmpImg = Imgcodecs.imread(tmp, 1);
      File imgDir = new File(dir);
      String[] fList = imgDir.list((d, n) -> n.endsWith(".png"));
      if (fList == null) {
        return null;
      }
      for (String f : fList) {
        Mat dstImg = Imgcodecs.imread(dir + File.separator + f, 1);
    
        Mat h1 = new Mat();
        Mat h2 = new Mat();
        Imgproc.calcHist(Collections.singletonList(tmpImg), channels, new Mat(), h1, histSize, ranges);
        Imgproc.calcHist(Collections.singletonList(dstImg), channels, new Mat(), h2, histSize, ranges);
        Core.normalize(h1, h1, 0d, 1d, Core.NORM_MINMAX, -1, new Mat());
        Core.normalize(h2, h2, 0d, 1d, Core.NORM_MINMAX, -1, new Mat());
    
        double score = Imgproc.compareHist(h1, h2, Imgproc.HISTCMP_CORREL);
        if (score > EXPECT_SCORE) {
          return f.substring(0, f.length() - 4);
        }
      }
      return null;
    }
    

    这里也可以将图片灰度化处理再对比。

    Imgproc.cvtColor(dst, hsv, Imgproc.COLOR_BGR2GRAY);
    

    4. 开发界面控制层

    界面使用JavaFX来开发,功能比较单一,只要程序启动的时候,启动视频采集线程即可。
    这里需要注意的是,当长时间没有识别到人脸的时候,界面不应该显示之前的人脸信息, 所以需要另起一个线程来监控是否有人脸识别信息,如果没有,则显示默认的图片。

    人脸采集回显部分

    private void startVideoCapture() {
      new Thread(new FaceCapture((v, r) -> {
        Image tmp = FaceUtils.sub(v, r.x(), r.y(), r.width(), r.height());
        try {
          FaceUtils.store(tmp, tmpPath);
          String id = FaceParser.parser(tmpPath, dir);
          if (id != null) {
            Platform.runLater(() -> {
              message.setText(sdf.format(new Date()) + ", 工号:" + id + "签到成功。");
              // for sign service
            });
          }
        } catch (IOException e) {
          alert.setContentText(e.getMessage());
          alert.show();
        }
        Platform.runLater(() -> {
          video.setImage(v);
          timestamp.set(System.currentTimeMillis());
          if (!find.get()) {
            avatar.setImage(tmp);
            find.set(true);
          }
        });
      })).start();
    }
    

    空闲监控,显示默认图部分

    这里认为2秒内没有人脸识别信息则认为是空闲。

    private void startVideoListener() {
      new Thread(() -> {
        while (true) {
          if (System.currentTimeMillis() - timestamp.get() > 2 * 1000) {
            Platform.runLater(() -> {
              video.setImage(DEF_VIDEO_IMAGE);
              avatar.setImage(DEF_AVA_TAR);
              uid.setText("");
              message.setText(DEF_MESSAGE);
            });
          }
          try {
            TimeUnit.SECONDS.sleep(2);
          } catch (InterruptedException ignore) {
          }
        }
      }).start();
    }
    

    界面布局

    布局采用JavaFX的fxml来设计。

    <BorderPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.172-ea"
      xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.itqn.raspi.video.VideoController">
      <right>
        <VBox alignment="CENTER" prefWidth="120.0" spacing="20.0" BorderPane.alignment="CENTER">
          <children>
            <ImageView fx:id="avatar" fitHeight="100.0" fitWidth="100.0"/>
              <HBox alignment="CENTER" prefHeight="40.0">
                <Label text="工号 "/>
                <TextField fx:id="uid" prefWidth="60"/>
              </HBox>
              <HBox alignment="CENTER" prefHeight="40.0" spacing="5.0">
                <Button onAction="#store" text="存档"/>
                <Button onAction="#reset" text="采集"/>
              </HBox>
            </children>
            <padding>
              <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
            </padding>
        </VBox>
      </right>
      <bottom>
        <HBox alignment="CENTER_LEFT" prefHeight="40.0" spacing="20.0" BorderPane.alignment="CENTER">
          <Label text="打开信息:"/>
          <Label fx:id="message"/>
            <padding>
              <Insets bottom="10.0" left="50.0" right="10.0" top="10.0"/>
            </padding>
        </HBox>
      </bottom>
      <center>
        <ImageView fx:id="video" fitWidth="320.0" fitHeight="180.0"/>
      </center>
    </BorderPane>
    

    5. 程序使用截图

    当没有检测到人脸的时候,程序会休眠更长的时间(1秒)以降低CPU的使用率,下面是空闲时的界面。

    raspi-face-freetime

    用户开始使用的时候,可以通过采集人脸进行工号绑定,下面是采集存档成功的界面。

    raspi-face-store

    6. 踩坑之旅

    由于程序是在Windows环境下开发的,程序开发完成,测试完美通过,然而树莓派是armv7架构的,默认安装的jdk8并不支持JavaFX。
    重新开发了一套基于swing的UI,原本的UI应该是这样的:

    raspi-face-javafx

    不支持JavaFX,有解决办法,不过测试了一下,效果不行,下面是解决方案:

    1. 通过下面的地址下载armv6hf-sdk
    https://gluonhq.com/products/mobile/javafxports/get/
    

    然后每次启动的时候指定ext模块

    java -Djava.ext.dirs=/home/pi/armv6hf-sdk/rt/lib/ext -jar raspi-video.jar
    
    1. 将armv6hf-sdk解压后复制到jre下面,可以通过下面这个地址2.1.4章节查看复制的位置,这种方式不用每次启动都带参数。
    https://docs.gluonhq.com/javafxports/
    
    1. armv6hf-sdk没有SwingFXUtils这个类

    解决这个问题比较简单,只需要将SwingFXUtils这个类的源码复制一份即可。

    =========================================================
    项目源码可关注公众号 “HiIT青年” 发送 “raspi-face” 获取。

    !!!基于Swing实现的界面模块也可以在公众号上下载!!!

    HiIT青年
    关注公众号,阅读更多文章。

  • 相关阅读:
    hadoop 3.0.0 alpha3 安装、配置
    集群使用初步
    转 mysql 中sql 语句查询今天、昨天、7天、近30天、本月、上一月 数据
    java 内存溢出
    获取手机上安装的应用信息
    使apk具有system权限
    Android基础之sqlite 数据库简单操作
    转 Android:文件下载和写入SD卡学习小结
    Android判断Service是否运行
    Android 定时重复启动弹出窗口。
  • 原文地址:https://www.cnblogs.com/itqn/p/13289528.html
Copyright © 2020-2023  润新知