【個人開発】ピースサインで開いた指の角度で競い合ってみるサービスを開発しました✌️

# 🔹はじめに🔹
写真を撮っている時、たまに見かけませんか？

「あの人、ピースサインの指、開き過ぎちゃう？」

そして考えました。

「あの人のピースサイン、何度くらい開いているのか...知りたい！」

そんなもどかしい好奇心から生まれたサービスを紹介します◎

<br>
<br>

（補足）
所謂ネタアプリですがこちらPFコンテストでの優秀賞作品になりました✌️
本戦の様子が公開されているのでお時間ある方は是非ご覧ください^^
他の登壇者の作品も素敵なサービスばかりでした✨

https://youtu.be/5aKI900iGVo

# 🔹サービス概要🔹
[![Image from Gyazo](https://i.gyazo.com/31ff6d41b383bf24f830cc70c761a632.png)](https://gyazo.com/31ff6d41b383bf24f830cc70c761a632)

ピースサインで開いた指の角度で競い合ってみるサービスです✌️

※現在はサービスの公開を終了しています。

https://github.com/akinari0919/v-sign

### __🔻メインターゲット__
- ピースサインで開いた指の角度を知りたい探求者達...
- 開き過ぎる指を自慢したい亡者達...

### __🔻実装済みの機能__
- 全ユーザー（ログイン不要）
  - カメラへ✌️サインをかざすだけで指の角度を測定
  - MAX値の画像をリアルタイムで更新
  - 画像データは保存・公開でき、角度に応じてランキング表示される
  - 保存時点のラインキングと角度結果をTwitterでつぶやける

|自動撮影|保存・公開|角度ランキング|
|---|---|---|
|[![Image from Gyazo](https://i.gyazo.com/7e565c631d71fc4cc08dbcdda4edb32e.gif)](https://gyazo.com/7e565c631d71fc4cc08dbcdda4edb32e)|[![Image from Gyazo](https://i.gyazo.com/42e8c896eac57c2f1392898f539a1c92.gif)](https://gyazo.com/42e8c896eac57c2f1392898f539a1c92)|[![Image from Gyazo](https://i.gyazo.com/f0eec0b10a94c449532fefc8452e3d0f.gif)](https://gyazo.com/f0eec0b10a94c449532fefc8452e3d0f)|
|カメラへ✌️サインをかざすだけで指の角度を測定|保存時点のランキングと角度結果をTwitterでつぶやける|ページ送りで10件ずつ表示|

# ピースサインの角度取得について✌️
本サービスの根幹部分ですが、MediaPipeという手の骨格ランドマークを識別する機械学習ライブラリを使用しました。（便利過ぎて発狂しました...!）
[![Image from Gyazo](https://i.gyazo.com/0e6e149b6209cbe6116372201880c256.png)](https://gyazo.com/0e6e149b6209cbe6116372201880c256)
以下は簡単な実装手順です。

|①ランドマークの取得|②角度算出式導入|③Vの表示|
|---|---|---|
|<img src="https://i.gyazo.com/0a694d4b2ed3fd8dcc46c9fb51905477.gif" width="500">|<img src="https://i.gyazo.com/7ebd7163525a44a2a23aae4b3ff911f4.png" width="400">|<img src="https://i.gyazo.com/54678b3e07099607e772ff7b8657a959.gif" width="500">|
|公式や開発記事を元に導入はスムーズでした。|∠ABC角度を求める計算式を使用。<br>B点については上記ランドマークの⑤と⑨の中間点を別途算出してます。<br>（実装コードは後ほど紹介）|Canvasを用いてランドマーク位置に沿って描写させました。<br>リアルタイム表示が良きです...!|

上記の時点で完成と思いきや所々に欠陥が散見されたためV表示の条件を整備しました。
以下は見られた欠陥の一例です。

|✌️サインのみに対応していない|左手に対応していない|裏手に対応していない|
|---|---|---|
|<img src="https://i.gyazo.com/4b787856e7d700f0fe83e76893073e78.gif" width="400">|<img src="https://i.gyazo.com/bbce9f1238ba828d511bb4b8687d10bf.gif" width="400">|<img src="https://i.gyazo.com/b69607ec8b72d7629498c70527d059d8.gif" width="400">|

他にも手を前面に倒したら2次元的に角度が上がってしまう対策等も取っています。
条件を増やしすぎると今度はV表示の難易度が上がってしまうため程々設定で落ち着きました。
それでもまだ穴があるので、既に抜け道見つけて投稿されたデータはどうやって~~淘汰して~~対応するかも思考中です^^;

# 🔻工夫ポイント3つ🔻
### ①自動撮影でMAX角度を表示
ピースサインしながら撮影ボタンを押すのは器用さが必要で、セルフタイマーにして撮影するのも撮影の瞬間にV表示がされていなければ角度が結果表示されない等の懸念がありました。考え抜いた末、角度がMAXになったら撮影するようにしてリアムタイムで更新されるようにしました。
```js:front/src/components/Measure.vue
export default {
  data: function() {
    return {
      item: {
        angle: 0 // <= MAX角度が格納される
      },
      check: {
        angle: 0 // <= リアルタイムで角度が格納される
      }
    }
  },

  methods: {
    // 省略（撮影やランドマーク取得etc.）

    findHands(results) {
      // 省略（V表示や角度算出etc.）

      // MAXデータをリアルタイムデータが上回った場合（バグ発生のため10°以上の条件追加）
      if (this.check.angle > this.item.angle && this.check.angle > 10) {
        // 省略（画像データや角度データを更新）
      }
    }
  }
}
```

### ②ページ遷移させず操作感向上
~~そもそも実装が面倒なので~~シンプルなTOPページ完結型のサービスにし、補足が必要な箇所はモーダル表示を駆使しました。

### ③ページネーションで表示データ取得を制限
ランキング登録が増えれば増えるほど、TOPページ表示と同時に大量の画像データの取得がされてパフォーマンスを落としてしまうため、Vuetifyのページネーションを活用し最大10件までのデータ取得で済むようにしました。

# 🔻苦労ポイント3つ🔻
### ①∠ABC角度算出
はい、これで算出できます（どーん）
[![Image from Gyazo](https://i.gyazo.com/e824b3828378c63bff5261f535115d41.png)](https://gyazo.com/e824b3828378c63bff5261f535115d41)
いや分からん...
幸い数学は嫌いではないので理屈は直ぐ理解しましたが、コードに落とし込む方法は模索しました。
こちらは泥臭いですが何とか実装できました。
```js
const ba0 = indexX - mcpX // A点(X軸) - B点(X軸)
const ba1 = indexY - mcpY // A点(Y軸) - B点(Y軸)
const bc0 = middleX - mcpX // C点(X軸) - B点(X軸)
const bc1 = middleY - mcpY // C点(Y軸) - B点(Y軸)
const babc = ba0 * bc0 + ba1 * bc1
const ban = (ba0 * ba0) + (ba1 * ba1)
const bcn = (bc0 * bc0) + (bc1 * bc1)
const radian = Math.acos(babc / (Math.sqrt(ban * bcn)))
const angle = radian * 180 / Math.PI
```
これが更にアドバイス頂きまして、現状は以下のようになっています。
```js
const a = {x: indexX, y: indexY}
const b = {x: mcpX, y: mcpY}
const c = {x: middleX, y: middleY}

const d0 = (Math.atan2(b.x - a.x, b.y - a.y) -
            Math.atan2(b.x - c.x, b.y - c.y)) * 180 / Math.PI
const d1 = (360 + d0) % 360 // ∠ABC
const angle = d1 > 180 ? 360 - d1 : d1 // 180度を境に小さい方の角度に変換
```
`atan2`(アークタンジェント2)ありがとう〜👏
そして数学って素晴らしい...！

### ②Vue.js移行
公式ではJavaScriptまでの実装コードのみの公開だったため、ネット上の開発記事を読み漁り実装しました。

### ③スマホ対応
こちらも公式で公開している実装コードでは独自のWEBカメラ実装となり、どうやらスマホに対応していないことが分かりました。
ごにょごにょと`navigator.mediaDevices.getUserMedia`を取り入れることで何とか対応させることができました。

# 🔹使用技術🔹
### __🔻バックエンド__
- Ruby(3.1.0) ※APIモード
- Ruby on Rails(7.0.2.3)
### __🔻Gem__
- rspec-rails
- factory_bot_rails
- rubocop
- pry-byebug
- annotate
- simplecov

### __🔻テスト__
- Rspec

### __🔻フロント__
- Vue.js(2.6.14)
- Vuetify
### __🔻ライブラリ__
- MediaPipe

### __🔻インフラ__
- Docker(20.10.11)
- PostgreSQL
- Heroku

# 🔹テーブル設計・ER図🔹
※ 2022/6/12執筆時点ではSignsテーブルのみ使用
[![Image from Gyazo](https://i.gyazo.com/865ca9483d8eaca593e17da27b20758b.png)](https://gyazo.com/865ca9483d8eaca593e17da27b20758b)

### 🔻Signsテーブル
投稿データを格納するテーブル<br>
- ゲストユーザーでも投稿者名を残せるように`name`カラムを追加
- `type`カラムのenumを用いて✌️or🖖を判別
- `angle`カラムに取得した角度を格納
- `user_id`カラムはnull許容

### 🔻Usersテーブル　※未使用
ログインユーザー用テーブル

### 🔻Commentsテーブル　※未使用
投稿データにログインユーザーがコメントする為のテーブル

# 🔹おわりに🔹
メイン実装（角度測定などフロント部分）を1週間程で作り切ったサービスなので、バックエンドもまだまだ寂しい状態です。未使用のテーブルを使うなど構想もありますが、より使いやすい・使ってみたいと思ってもらえるサービスにしていきたいので、記事のご指摘含めてご意見アドバイスがありましたらご教示いただければ幸いです...！

Twitterからのコメントも大歓迎です！
https://twitter.com/runmizzo

最後までお読み頂きありがとうございました✌️


# 🔹参照🔹

https://paiza.hatenablog.com/entry/2022/01/21/150000

https://imagingsolution.blog.fc2.com/blog-entry-50.html

https://blog.ariafloat.com/article/anglecalc/

https://dev.classmethod.jp/articles/mediapipe-recognize-hand-pose-with-multi-hand-tracking/

https://gist.github.com/TheJLifeX/74958cc59db477a91837244ff598ef4a

https://stackoverflow.com/questions/69009153/mediapipe-javascript-to-vue-component

https://reffect.co.jp/vue/vuetify-simple-pagination