【SNS顔出しNGの方も安心】顔に目隠しを付けてくれるアプリを機械学習APIで作ったよ



##作ったもの
SNS顔出しNGの方のために、
顔に目隠しを付けてくれるアプリを作りました。

ウェブアプリなので、PC、スマホの機種は問いません。
こちらのリンクからどうぞ。
https://hardcore-ritchie-cca31e.netlify.app/

###使用例
写真をアップロードすると、いい感じに目隠しを付けてくれます。
![116153353_3186371734744672_7900174768791699760_o.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/0dac28f4-cd96-f8ca-0671-cdc3d85a0082.jpeg)


複数人でも大丈夫。
（普通の飲み会が、なにやら悪巧みしてそうな会合に変貌）
※コロナ禍より前の飲み会です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/88167621-2b14-e657-6249-16310899b9f9.jpeg" width=80%>


秘密のデートも目隠しを入れれば、誰にもバレません。
これで、プライバシーもバッチリ！
（写真はフリー写真サイト[「ぱくたそ」](https://www.pakutaso.com/) さんより）

<img width="781" alt="スクリーンショット 2020-08-04 21.42.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/4a699647-c6af-634e-73b7-e00038259a03.png">

これで、みなさんも安心してSNSに投稿できますね。


###番外編
なんと、私の３Dモデル（リアルアバター）にも反応しました。
つまり、私のアバターは人間と見分けがつかない？
ちなみに、このアバターは浅草にある[リアルアバター](https://www.real-avatar.com/)さんで作ってもらいました。
<img width="1181" alt="スクリーンショット 2020-08-04 21.49.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/0d4df524-97a2-4fd2-9da9-206357a92b17.png">


猫はダメでした。
「人間じゃないよ」ってアラートを出します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/7586786a-c349-3232-33f5-d335f7342c44.jpeg" width=80%>

##使用技術

###言語: 
JavaScript
###CSSフレームワーク
[Bootstrap](https://getbootstrap.com/) 
レスポンシブデザインを気持ち程度に使用

###ライブラリ

- [face-api.js](https://github.com/justadudewhohacks/face-api.js/)<br>顔認証、目の位置をこれで取得<p>
- [humane.js](http://wavded.github.io/humane-js/)<br>アラートを表示



## face-api.js について
TensorFlow.jsで学習済みの顔認識の機械学習モデルが使えるAPIです。

顔の検出、表情分析、年齢・性別分析などができます。
サービスアカウント不要、アクセストークン・シークレットキー不要、無料で手軽に利用できます。

###導入手順
[face-api.js](https://github.com/justadudewhohacks/face-api.js/)のREADMEやネット記事を読んで私が実行した手順は以下

- 以下のリンクからZIPファイルをダウンロード (Cloneでも可）<br>
https://github.com/justadudewhohacks/face-api.js/ 
- distフォルダにある face-api.js を自分のアプリと同じディレクトリに移動する
- weightsフォルダの名前を modelsに変更し、自分のアプリと同じディレクトリに移動する

```
index.html
face-api.js
models
```

他のフォルダ、ファイルは不要です。


###本件で使用したモデル
tinyFaceDetector　というモデルで顔を検出し、
faceLandmark68Net　というモデルで顔のランドマークを検出します。
この写真のように68個の顔のポイント１つ１つの画像上での座標が取得できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/63c85084-48ce-613f-4fed-1d87dc80e931.png" width=60%>

このような感じで座標を取得できます。（これが全部で６８個あります）
<img width="301" alt="スクリーンショット 2020-08-05 22.43.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/155b72c3-a7b9-5f19-d458-af9324866cca.png">


ポイント３６からポイント４５へ線を引けばいいのですが、それだと線が短すぎるので、
線の始点： X軸はポイント0、Y軸はポイント36
線の終点： X軸はポイント45、Y軸はポイント16
を採用してバランスをとりました。

本当は、ポイント36からポイント45の線の傾きを算出して、線を伸ばすようなコードを書けばよいのですが、あまり影響がないので、そのままにしました。


##参考サイト
以下のサイトに大変お世話になりました。
ありがとうございました。

- [face-api.js - ブラウザでの顔認識を行うJavaScript API](https://note.com/npaka/n/nc9c244b11089)
顔のランドマークを抽出するコードは、ほとんどこちらのサイト記載のものを使用しました。


- [File APIとCanvasでローカルの画像をアップロード→加工→ダウンロードする](https://www.tam-tam.co.jp/tipsnote/javascript/post13538.html)
ファイルをアップロードしてCanvas上に表示するコードは、ほとんどこちらのサイト記載のものを使用しました。



##コード
```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- Bootstrap -->
    <!-- CSS only -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous" />
    <!-- JS, Popper.js, and jQuery -->
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous" ></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous" ></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous" ></script>
    <style>
      .container {
      padding-top: 50px;
      }
    </style>

    <!-- face-api.jsの読み込み -->
    <script src="face-api.js"></script>

    <!-- humaneライブラリの読み込み -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/humane-js/3.2.2/themes/boldlight.css"/>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/humane-js/3.2.2/humane.min.js"></script>

  </head>
  <body>
    <div class="container">
      写真をアップロードしてください
      <div class="upload"><input type="file" name="file" id="file"></div>
      <div id="result"></div>
      <canvas id="canvas"></canvas>
    </div>
  </body>

  <script>
    const MODEL_URL = 'models/'
    let file = document.getElementById('file')
    let canvas = document.getElementById('canvas')
    let canvasWidth = 350
    let canvasHeight = 350
    let uploadImgSrc

    //ファイルをアップロードしてCanvas上に表示するのは、ほぼこのサイトどおり
    //https://www.tam-tam.co.jp/tipsnote/javascript/post13538.html
    // Canvasの準備
    canvas.width = canvasWidth
    canvas.height = canvasHeight
    var ctx = canvas.getContext('2d')

    function loadLocalImage(e) {
      // ファイル情報を取得
      let fileData = e.target.files[0]
      // 画像ファイル以外は処理を止める
      if(!fileData.type.match('image.*')) {
        humane.log("画像ファイルでお願いします")
      }
      // FileReaderオブジェクトを使ってファイル読み込み
      let reader = new FileReader()
      // ファイル読み込みに成功したときの処理
      reader.onload = function() {
        // Canvas上に表示する
        uploadImgSrc = reader.result;
        canvasDraw(fileData)
      }
      // ファイル読み込みを実行
      reader.readAsDataURL(fileData)
    }
    // ファイルが指定された時にloadLocalImage()を実行
    file.addEventListener('change', loadLocalImage, false)
    // Canvas上に画像を表示する
    function canvasDraw() {
      // canvas内の要素をクリアする
      ctx.clearRect(0, 0, canvasWidth, canvasHeight)
      // Canvas上に画像を表示
      let img = new Image()
      img.src = uploadImgSrc
      img.onload = function() {
      ctx.drawImage(img, 0, 0, canvasWidth, this.height * (canvasWidth / this.width))
      let canvasRate = canvasWidth / this.width
     
     //顔の情報を取得
      getFaceData(img,canvasRate)  
  }
}

async function getFaceData(img,canvasRate) {
  await faceapi.nets.tinyFaceDetector.load('models/') //モデル読み込み
  await faceapi.nets.faceLandmark68Net.load("models/") //モデル読み込み
  // 顔検出の実行
  const detectionsWithLandmarks = await faceapi.detectAllFaces(img,
        new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks()
  if (detectionsWithLandmarks.length == 0){
    humane.log('人間じゃないよ')
  }else{
    for (let n = 0; n < detectionsWithLandmarks.length ; n++ ) {
      let x0 = detectionsWithLandmarks[n].landmarks._positions[0].x * canvasRate
      let y0 = detectionsWithLandmarks[n].landmarks._positions[36].y * canvasRate
      let x1 = detectionsWithLandmarks[n].landmarks._positions[16].x * canvasRate
      let y1 = detectionsWithLandmarks[n].landmarks._positions[45].y * canvasRate

      let line_thickness =  (x1-x0) * .2 //線の太さを長さの２０％とした

      drawLine(x0,y0,x1,y1,line_thickness) //目隠し線を引く関数
    }
  }
}

//目隠し線を引く
function drawLine(x0,y0,x1,y1,line_thickness){ 
  ctx.strokeStyle = '#000000'
  ctx.lineWidth = line_thickness
  ctx.beginPath()
  ctx.moveTo(x0, y0) 
  ctx.lineTo(x1,y1)
  ctx.closePath()
  ctx.stroke()
}

</script>

</html>
```