ラズパイとSORACOMで子豚の出産通知を作ってみた

#システムを作った背景
知り合いの畜産農家と話していて、子豚の出産の時期になると出産時の事故を防ぐために夜中に起きて確認が必要な場合もあるらしく、とても大変であるという話を聞いたので、ラズパイとSORACOMを使って子豚の定期チェックと出産通知を行うシステムを作ってみました。

###全体構成
豚舎には電源はありますが、wifiのない環境だったので、SORACOM Airを使ってラズパイをネットワークにつなげました。
子豚を認識するための学習モデルはlobeを使って作成して、tflite形式で出力したものを搭載しています。
撮影した画像はGoogleDriveに転送して、子豚が生まれていることを検知したらLINEBOTが教えてくれる仕組みになっています。

![kouseizu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/68b7174b-3a39-9c95-0409-6202c95b942f.png)

#使用したSORACOMサービス
* SORACOM Air(SORACOMの3Gネットワーク)
* SORACOM Napter(ラズパイに外からログインするためのサービス)

#必要なパーツの紹介
|品名| 価格 | 備考 |
|------|--------|-------|
| [ラズパイ3Bセット](https://www.amazon.co.jp/gp/product/B07XQ6FLD5/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1)|  10780円  | ケースやSDカード付きのものを購入 |
| [温度センサーDHT11](https://www.amazon.co.jp/gp/product/B010GXAH4E/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1)|  800円|3個セットのものを購入  |
|[カメラモジュール Rカットカメラ](https://www.amazon.co.jp/gp/product/B087NH9BHL/ref=ppx_yo_dt_b_asin_title_o04_s00?ie=UTF8&psc=1)|  3299円  | 暗くなると赤外線カメラで撮影してくれます |
|[3G USBドングル AK-020 スターターキット](https://www.amazon.co.jp/dp/B01G1GSYHW/)|  6090円  | ネットワークに接続するために必要です |
|[中が見えるBOX](https://monosizecatalog.com/item-4906137327521/)|110円|seriaで買いました。サイズがぴったりなのでおすすめです|

これらを組み合わせてこのようなものを作りました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/2b823dab-494d-cca9-7958-1c9f233b7e62.png" width=30%>

これを豚舎に設置して、高い位置から子豚の様子を撮影しています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/1dd6d3fa-0b06-d7f8-084c-5291eda5cd8b.png" width=70%>

#処理の概要
処理の流れはこのようになっていて、これらの処理を30分周期で行ってます。
全てを紹介するのは大変なので、それぞれの関数について処理のポイントを紹介したいと思います。

1. 写真を撮影して、親豚の位置を判定
1. 子豚の有無を判定
1. 日付、温度、湿度、子豚のチェック結果を写真に記載
1. 認識エリアの合成
1. GoogleDriveのフォルダに同期
1. 子豚が産まれていたらLINE通知

##1.写真を撮影して親豚の位置を判定
最初に装置を作って豚舎に置いたときに、撮影する場所によって親豚が右に映る場合と左に映る場合がありました。
なのでまずは親豚の位置を判定する学習モデルを作成しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/6ff77fcb-0f81-0424-39f8-9246ab03c9c0.png" width=70%>

学習精度を上げるために、画像を白黒にしたものをlobeに読み込ませて学習します。
[lobeについてはこちら](https://lobe.ai/)

![position2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/3d8e8ebe-ce06-ccb7-4418-9fa6032c140b.png)

学習した情報はtflite形式でexportすることができるので、出力されたファイル一式をラズパイに移動させて組み込みます。
![tflite.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/557a24ec-aa2c-890d-a248-bb98614d9af6.png)


組み込んだモデルファイルと一緒に出力されるtflite_example.pyを関数内で実行して、親豚の位置を判定しています。
判定の結果を戻り値で返すようにしています。

```python

def photo_shot():

    filename = '/var/local/tmp/NowPhoto.jpg'
    gray_filename = '/var/local/tmp/GrayNowPhoto.jpg'
    position = 0

    #写真を撮影してファイル出力
    com1 = 'raspistill -w 2592 -h 1944 -q 10 -o ' + filename
    os.system(com1)

    #ファイルを白黒加工
    img=cv2.imread(filename)
    gray_cv = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    cv2.imwrite(gray_filename, gray_cv)

    #親豚の位置チェック
    com2 = "python3 /usr/local/bin/Position_TFLite/example/tflite_example.py " + gray_filename
    position_out = subprocess.getoutput(com2)
    position_out = str(position_out).split(" ")

    #位置の情報を戻り値で返す
    if 'left' in position_out[3]:
        position = PIG_LEFT
    else:
        position = PIG_RIGHT

    return position
```

##2.子豚の有無を判定
子豚が産まれたことを判定する学習モデルも同じように作成しています。
この時、余計な情報を省くためにチェックしない場所に対してマスク処理を行っています。
親豚の位置に応じた学習モデルを使っているので、マスク画像も2種類用意していて、四角の中に子豚が少しでも映っていた場合に子豚が産まれたと判断するようにしています。

![piglet6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/aa8e8c6e-e762-159c-e5a6-8bfbf6450673.png)

マスクファイル(right_mask.jpg,left_mask.jpg)はこのような画像を準備する必要があります。
違いが微妙ですが、それぞれ白い四角の中の画像を判定に使っています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/4fcbe99e-5d90-90a3-eb71-d03bc96a053f.png" width=60%>

以下が判定する関数です。戻り値で子豚の有無を返すようにしています。

```python
def piglet_check(position):

    gray_mask_filename = "/var/local/tmp/GrayMaskNowPhoto.jpg"
    left_maskfile = "/usr/local/bin/left_mask.jpg"
    right_maskfile = "/usr/local/bin/right_mask.jpg"
    ret = 0

    #親豚の位置に応じたマスクファイルを選択
    if position == PIG_LEFT:
        maskfile = left_maskfile
    else:
        maskfile = right_maskfile

    #マスク画像をもとの画像に合成
    img = cv2.imread(gray_filename)
    mask = cv2.imread(maskfile)
    mask = cv2.resize(mask, img.shape[1::-1])
    mask_img = cv2.bitwise_and(img, mask)
    cv2.imwrite(gray_mask_filename, mask_img)

    #子豚がいるかどうかをチェック
    if position == PIG_LEFT:
        com1 = "python3 /usr/local/bin/Piglet_right_TFLite/example/tflite_example.py " + gray_mask_filename
    else:
        com1 = "python3 /usr/local/bin/Piglet_left_TFLite/example/tflite_example.py " + gray_mask_filename

    piglet_out = subprocess.getoutput(com1)
    piglet_out = str(piglet_out).split(" ")

    #判定結果を戻り値で返す
    if 'No_piglet' in position_out[3]:
        ret = PIGLET_NO_DETECT
    else:
        ret = PIGLET_DETECT

    return ret
```

##3.日付、温度、湿度、子豚のチェック結果を写真に記載
この装置には温湿度センサもつけているので、写真を撮影した時点の温湿度と子豚の認識結果を画像に記載するようにしています。
温湿度を取得する処理は別のプロセスで動かしていて、10分周期で「NowTemp.txt」に出力してます。「NowPigletStatus.txt」には子豚の認識結果が保存されているので、それぞれのファイルから情報を取り出して画像に合成しています。

画像に文字を書き込む際、フォント情報が必要なので、IPAのipag.ttfのファイルをダウンロードしてラズパイに配置しています。
[IPAフォントについてはこちら](https://moji.or.jp/ipafont/fontspec/)

```python
def information_write():

    filename = '/var/local/tmp/NowPhoto.jpg'

    #対象のファイルを開いてサイズを取得
    img = Image.open(filename)
    width, height = img.size
    draw = ImageDraw.Draw(img)

    #合成に必要な情報を収集
    now = datetime.datetime.now()
    nowtime = now.strftime('%Y/%m/%d %H:%M')
    nowtemp = subprocess.getoutput("cat /var/local/NowTemp.txt")
    nowstatus = subprocess.getoutput("cat /var/local/NowPigletStatus.txt")
    font = ImageFont.truetype("/usr/local/bin/ipag.ttf", size=200)

    #日付情報と温湿度情報を書き込み
    templist = str(nowtemp).split(" ")
    addtext = nowtime + " " + templist[2] + "C " + templist[3] + "%"
    draw.text((0, 0), addtext, fill=(0,200,200), font=font)

    #子豚認識結果を書き込み
    statuslist = str(nowstatus).split(",")
    if statuslist[0] == 'piglet':
        addtext2 = "子豚認識率 " + str(statuslist[1]) + "◎"
    else:
        addtext2 = "子豚認識率 " + str(statuslist[1])
    draw.text((0, height-250), addtext2, fill=(0,200,200), font=font)
    
    #ファイルを上書き保存
    img.save(filename)
```

この処理で、画像の上下に情報を記載しています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/6a6aa942-6ad9-6d50-a7e5-6224cd367cb9.png" width=80%>

##4.画像認識エリアの合成
この装置は出産が始まりそうな親豚の場所にカメラを設置して使うのですが、カメラの置き方によって撮影内容にばらつきがありました。
カメラの撮影内容のばらつきを少なくするために、撮影位置の目安があると便利だと思い、認識するエリアに四角形を描画しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/8477eb86-41e4-69fb-e119-779fc4af5e0a.png" width=70%>

```python
def position_write(position):

    filename = '/var/local/tmp/NowPhoto.jpg'
    img = cv2.imread(filename)
    
    if position == PIG_LEFT:
        #親豚は左にいるので右側に四角を描画
        cv2.rectangle(img, (1350, 250), (2350, 1600), color=(0, 255, 0), thickness=4)
    else:
        #親豚は右にいるので左側に四角を描画
        cv2.rectangle(img, (250, 250), (1250, 1600), color=(0, 255, 0), thickness=4)

    cv2.imwrite(filename, img)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/b83ddd27-56ab-e099-da81-b6d01ca77541.png" width=80%>

##5. GoogleDriveのフォルダに同期
撮影した画像はGoogleDriveに送信します。
gdriveコマンドを使っていて、最初に古いファイルを削除して、新しいファイルをフォルダごと同期しています。

```python
def drive_sync():

    #クラウド上の古いファイルを削除する
    file_list = "gdrive list --query \"name = 'XXXXX.jpg' and trashed = false\" | grep XXXXX.jpg"
    id_str = os.popen(file_list).read().rstrip()
    id_list = id_str.split("   ")
    com1 = "gdrive delete " + id_list[0]
    os.system(com1)

    # 写真を同期してクラウド上のファイルを更新する
    com2 = 'gdrive sync upload --keep-local /var/local/XXXXX XXXXXXXXXXXXXXXXXXXX'
    os.system(com2)
```

GoogleDrive上での見やすいように、最新ファイルをトップに置いてそれ以外は別のフォルダに退避させるようにしています。
ここには記載してないですが、GASを使ってGoogleDrive上のファイル操作を行っています。

##6. 子豚が産まれていたらLINE通知
チェックして子豚が生まれていたらLINEBOTからブロードキャストにLINE通知を出すようにしています。
通知でGoogleDrive上にあるファイルのURLを伝えることで、すぐに画像が見えるようにしました。

[LINE BOTについてはこちら](https://developers.line.biz/ja/services/bot-designer/)

```python
def piglet_notice():
    from linebot import LineBotApi
    from linebot.models import TextSendMessage

    #LINEBOTのアクセストークン
    LINE_CHANNEL_ACCESS_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

    #子豚を検知した画像のファイルIDを取得
    com1 = "gdrive list --query \"name = 'XXXXXXXXXX.jpg' and trashed = false\" | grep XXXXXXXXXX.jpg"
    id_str = subprocess.getoutput(com1)
    id_list = id_str.split("   ")

    #ブロードキャストでメッセージとファイルURLを送信
    message = "子豚が産まれています。確認して下さい。 https://drive.google.com/uc?id=" + id_list[0] 
    messages = TextSendMessage(text=message)
    line_bot_api.broadcast(messages)
```

![line.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/f9ba100b-a825-c5ac-d1f8-6e44779db2ec.png)
通知が来たURLをクリックすると、画像ファイルを表示することができます。
ここでは子豚がしっかり映っているものを掲載していますが、実際には誤検知も少しあるので、最終判断は目視で行う必要があるのかなと思っています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1359985/fee4d14f-58c4-4d7a-afef-64690bc0c7ea.jpeg" width=50%>
※暗いと赤外線カメラで撮影するので、全体が紫になっています。

#おわりに
このシステムは通信にSORACOMの3G回線を使っているので、機材とは別に通信料金がかかってきます。このシステムの場合だと30分に1回500KB程度の画像を送信していますが、月額500円程度の通信費で運用することができるのでかなりお手軽な印象があります。

色々なツールを使っていますが、どれもさわり程度の知識で組み込むことができました。
LINEBOTを使ったのが良かったのか、普段からITに触れる機会の少ない農家の方にも使ってもらえています。
おいしいお肉を食べるために、IoTで農家の人が楽できるような仕組みがもっと増えていけばいいなと思います。