1. 程式人生 > 實用技巧 >樹莓派實現人臉打卡機

樹莓派實現人臉打卡機

之前用樹莓派開發一套簡易的視訊監控平臺,正好週日有時間,這次用樹莓派實現了人臉打卡機。

樹莓派相關文章:

  1. 樹莓派搭建nexus2.x私服
  2. 樹莓派搭建視訊監控平臺
  3. 樹莓派視訊監控平臺實現錄製歸檔
  4. 樹莓派實現人臉打卡機 (本文)

1. 功能設計

樹莓派人臉打卡機,主要包括兩個大方向的功能要求:
a. 人臉採集存檔
b. 人臉識別簽到
這兩個功能配合使用就能實現人臉打卡了, 通過人臉採集將人臉資訊預存檔在系統中,簽到的時候,當人靠近攝像頭時實時採集人臉,然後比對現有人臉,如果資訊匹配則認為簽到成功。

下面是簽到的效果:
當人臉簽到成功後,程式介面底部會顯示簽到時間和簽到人的工號。

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的使用率,下面是空閒時的介面。

使用者開始使用的時候,可以通過採集人臉進行工號繫結,下面是採集存檔成功的介面。

6. 踩坑之旅

由於程式是在Windows環境下開發的,程式開發完成,測試完美通過,然而樹莓派是armv7架構的,預設安裝的jdk8並不支援JavaFX。
重新開發了一套基於swing的UI,原本的UI應該是這樣的:

不支援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實現的介面模組也可以在公眾號上下載!!!


關注公眾號,閱讀更多文章。