ラズパイでサンタ🎅を見張ってSlackに通知する！

# 初めに
- ラズパイと画像認識で何か動くものを作りたいな〜
- アドベントカレンダーってクリスマスに向けてやるものだし、なんかしら絡めたいな〜

ということから「サンタ検出器」を作ろう！！と思い調べてみると、[既に偉大な先駆者](https://wp.tech-style.info/archives/1224)がおられました！

ですが、今回はJavaを使って同じようなことをやってみようと思います！

# 対象としている方
- プログラミング学び初めの方
- 簡単に動くもの作りたいよって方
- Javaでカメラ撮影する例を知りたい方
    - Pythonとかはよくあるけど、意外とJavaの例が無い気がしたので。
- 画像認識サービス(Cloud Vison API)を使ってみたい方

# 何ができるの？
ラズパイに接続したカメラで写真を撮影し、サンタさんが写っていたらSlackに通知できます！
※ラズパイに限らず、カメラ付きのノートPCなどでも同じように動作します。

# 処理概要
1. ラズパイに接続したカメラで写真を撮影する。
2. 撮影した写真をGCPのCloud Vision APIに送信し、画像解析をしてもらう。
3. 解析結果を受信し、ラベルに「santa claus」が入っていたらSlackのBotで証拠写真をアップロードする。

# 用意するもの
- Raspberry Pi
- カメラモジュール or USBカメラ(試してないですが、たぶん動きます。)
- Googleアカウント
- Slackアカウント
- ソース
    - https://github.com/AZKZ/santa-detector


(上にも書いてありますが、ラズパイじゃなくても動きます。)

# 設定手順
## Cloud Vision APIの利用設定
画像解析のAPIを使うための設定です。
[公式のクイックスタート](https://cloud.google.com/vision/docs/quickstart-client-libraries?hl=ja)の「4.認証設定」でJSONファイルをダウンロードするところまでやりましょう。

保存先は基本的にどこでも構いません。
**一応、無料期間中は自分で有料アカウントにアップグレードしない限り課金はされないっぽいですが、念の為ご自身でご確認ください**🙌

![スクショ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364697/149c31a0-22a7-ebb5-a176-485be4e7e4b3.png)


## Slack Botの利用設定
[Slack API](https://api.slack.com/)が英語でわかりずらいかもなので、簡単に説明します！
※Slackのユーザー登録とかは省きます。

1.まず[ここ](https://api.slack.com/apps)から「**Create An App**」でSlack Appを作成する。
![2020-12-15-062124_1360x768_scrot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364697/75a58137-c809-1e3c-782c-cc5bad9dfffb.png)
2.左のバーの「**OAuth & Permissions**」から「**Scopes>Bot Token Scopes**」で「**files:write**」を追加する。
3.そしたら「xxob-」から始まるTokenが発行される。これを後で使います！

# 実際に動かしてみる！
1.https://github.com/AZKZ/santa-detector からgit cloneローカルに落とす。
2.「santa-detector」配下に移動して以下のコマンドでビルドする。
※依存ライブラリのサイズが大きいからかなり時間がかかるかも😅


```shell
sh gradlew shadowjar
#[Windowsの場合は] gradlew shadowjar
```

3.実行ファイルの「santa-detector」(windowsの場合はsanta-detector.bat)の設定値を変更する。
※Google Vision APIを使うライブラリが環境変数で設定されていることが前提だったので、他の設定値もそれに合わせました。

```shell:santa-detector
#!/usr/bin/env sh
cd $(cd $(dirname $0); pwd)
export IMAGE_SAVE_DIR="/xxxx/xxxx/xxxx/xxx" ←画像保存先ディレクトリ
export GOOGLE_APPLICATION_CREDENTIALS="/xxxx/xxxx/xxxx/xxx/xxxx.json" ←Google Vision APIのJSONファイルの絶対パス
export SLACK_BOT_TOKEN="xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ←Slack BotのToken
export SLACK_CHANNEL="#general" ←Slackの通知するチャンネル
export VIDEO_CAPTURE_INDEX="0" ←カメラのインデックス 接続されたカメラが複数ある場合は適宜変更してください。

java -jar build/libs/santa-detector-1.0-SNAPSHOT-all.jar
```

4.このファイルを実行する。
5.サンタが写るとこんな感じで通知されます！！
![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364697/2da889fa-015c-ac9b-2ff4-6962ce489c85.png)

**コスプレのような生半可な仕上がりではサンタとは認めてくれなそうな感じでした**😦


# 作ったクラスの説明
## 写真を撮影するクラス

```java:Camera.java 
package azkz.camera;

import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_videoio.VideoCapture;

import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Camera {

    private VideoCapture videoCapture;

    public Camera(VideoCapture videoCapture) {
        this.videoCapture = videoCapture;
    }

    /**
     * @param saveDirPath 画像保存先ディレクトリの絶対パス
     * @return 撮影された画像ファイルの絶対パス
     */
    public String takePicture(String saveDirPath) {
        // 引数からFileオブジェクトをインスタンス化
        File saveDir = new File(saveDirPath);

        // 指定したディレクトリが存在しない場合 or ディレクトリではない場合
        if (!saveDir.exists() || !saveDir.isDirectory()) {
            throw new IllegalArgumentException("指定したディレクトリが存在しない、もしくはディレクトリではありません。");
        }

        // マトリックスのインスタンスを生成
        Mat mat = new Mat();

        // 撮影
        videoCapture.read(mat);

        // ファイル名 (2020年1月1日 13:15:30に撮影した場合 -> 20200101-131530.jpg)
        String fileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")) + ".jpg";
        String imageFilPath = saveDir.getAbsolutePath() + "/" + fileName;
        // ファイルとして出力
        opencv_imgcodecs.imwrite(imageFilPath, mat);

        return imageFilPath;
    }
}
```

## Vision APIを使うクラス
参考:[Vision APIクイックスタート ラベル検出](https://cloud.google.com/vision/docs/quickstart-client-libraries?hl=ja#java)

```java:
package azkz.imagerecognition;

import com.google.cloud.vision.v1.*;
import com.google.cloud.vision.v1.Feature.Type;
import com.google.protobuf.ByteString;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;

/**
 * GoogleのVisionAPIを使うクラス
 */
public class GoogleVisionApi {

    /**
     * 画像のラベル検出をする。
     * 処理内容はほぼガイドの通り。
     *
     * @param imageFile 画像ファイル
     * @return VisionAPIのラベル検出の結果を返す。
     * @throws IOException 画像ファイル読み込みエラー時の例外。
     * @throws GoogleVisionApiException VisionAPIのレスポンスがエラーの場合の例外。
     * @see <a href="https://cloud.google.com/vision/docs/labels#java">ラベル検出のガイド</a>
     */
    public static List<EntityAnnotation> detectLabels(File imageFile) throws IOException, GoogleVisionApiException {
        // 画像解析に使うクライアントを初期化する。
        // try-with-resourcesで処理終了後にクライアントを自動でcloseする。
        try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {

            // 画像ファイルを読み込む。
            byte[] bytes = Files.readAllBytes(imageFile.toPath());
            ByteString imgBytes = ByteString.copyFrom(bytes);

            // リクエストを組み立てる。
            List<AnnotateImageRequest> requests = new ArrayList<>();
            Image img = Image.newBuilder().setContent(imgBytes).build();
            Feature feat = Feature.newBuilder().setType(Type.LABEL_DETECTION).build();
            AnnotateImageRequest request =
                    AnnotateImageRequest.newBuilder().addFeatures(feat).setImage(img).build();
            requests.add(request);

            // APIリクエストを送信し、解析後のレスポンスを受け取る。
            BatchAnnotateImagesResponse batchAnnotateImagesResponse = vision.batchAnnotateImages(requests);
            AnnotateImageResponse response = batchAnnotateImagesResponse.getResponses(0);

            // エラーの場合は例外をthrow
            // 正常の場合はラベルの検出結果のリストを返す。
            if (response.hasError()) {
                throw new GoogleVisionApiException("レスポンスがエラーです。" + response.getError().getMessage());
            } else {
                return response.getLabelAnnotationsList();
            }
        }
    }
}
```


## Slackに画像をアップロードするクラス
参考:[公式 サンプルコード](https://api.slack.com/methods/files.upload/code)

公式のやつより「Slack.getInstance().methods()」を使ったほうがちょっと楽にかけたかなって気がします！

```Java
package azkz.messenger;

import com.slack.api.Slack;
import com.slack.api.methods.SlackApiException;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.List;

/**
 * Slackにメッセージを送るクラス
 */
public class SlackMessenger {

    private String token;

    /**
     * コンストラクタ
     * @param token botのトークン([xoxb-]から始まるもの)
     */
    public SlackMessenger(String token) {
        this.token = token;
    }

    /**
     * @param channels チャンネルID or チャンネル名
     * @param comment  アップロード時のコメント
     * @param file     アップロードするファイル
     */
    public void uploadFile(List<String> channels, String comment, File file) throws IOException, SlackApiException {
        var client = Slack.getInstance().methods();
        var result = client.filesUpload(r -> r
                .token(token)
                .channels(channels)
                .file(file)
                .filename(file.getName())
                .initialComment(comment)
        );
        System.out.println(result);
    }
}
```

# 最後に
ラズパイのアドベントカレンダーに書くことか？感は否めないですが、
少しでもプログラミングを楽しむためのきっかけやヒントになっていたら嬉しいです！

特にプログラミング始めたての方は難易度の割にそれらしく動くので、実感がつかめていいんじゃないでしょうか！

2021年はもうちょい記事投稿したい！！！

読んでいただきありがとうございました🙏🙏🙏