mochimochi000の日記

備忘録です

道路監視システム

 

はじめに

以下の前回記事で難所であり要所はクリアしたので、いよいよ道路監視システムとしてのシステム化を行う。

realtek.hatenablog.com

全体的な処理の流れ

1、10分に一度撮像して保存、後続処理に渡す

2、画像をもとに物体検知、数をカウント

2-1、(デバッグ用)検知した物体を四角形で囲っている画像を最長辺を指定して表示

3、2-1の画像を保存

4、1日単位のcsvファイルに検知した物体ごとの数を追記して保存

制限事項

・現時点でローカル保存のみで、NAS等の外部ストレージ保存は手動

今後自動NASバックアップを検討

・自動削除機能は未実装

現時点で1回10分当たり1MB弱、1日で140MB程度要してローカルのストレージが32GB(実際22GB)なので削除機能が別途

・夜間輝度調整

夜間は撮影しても真っ暗で、輝度調整しても限界なので諦める。

・タイムラプス動画作成

制限というよりも削除前に動画化して残すと面白いかも

ハードウェア系

・ラズパイ4B 4GB

・中華カメラモジュール 5M pixel(¥1000)

https://www.amazon.co.jp/%E3%82%AB%E3%83%A1%E3%83%A9%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB-%E6%84%9F%E5%85%89%E3%83%81%E3%83%83%E3%83%97OV5647%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC-5M%E7%94%BB%E7%B4%A0-Raspberry-Model/dp/B07G572B3R/ref=pd_ci_mcx_mh_mcx_views_0?pd_rd_w=aFZMs&content-id=amzn1.sym.b7ff8ae3-d3eb-4b00-a35f-7c651f284b6c%3Aamzn1.symc.409c7fce-cbd2-4cf4-a6cb-824c258c8778&pf_rd_p=b7ff8ae3-d3eb-4b00-a35f-7c651f284b6c&pf_rd_r=8FQVNVSZCY8CGK05YN1G&pd_rd_wg=MckfR&pd_rd_r=876a86d9-1b97-43fe-ae57-75f9a903232a&pd_rd_i=B07G572B3R

・配置環境

カメラモジュールが基盤剥き出しなので養生(見た目悪い)

配置状況

 

撮像対象のイメージ(イラスト

撮像先イメージ

 

ソースコード

・画像から分析、結果を出力

github.com

 

・分析結果からデータを可視化してWebサイト上に動的に掲載

github.com

 

実行結果

csvファイル

csvファイルに追記した結果は気が向いたタイミングでグラフ化予定。

遠いところにある車などは検知ができていないので、このモデルの限界と思われ。

中華カメラでも問題なく動作しているので地味にすごい。

しばらく運用して気になるところは随時改良予定。

画像からオブジェクトを検出する(Darknet YOLOv4-tiny model)

はじめに

ラズパイ4とカメラモジュールが転がっていたので何かobject検知系で何か作ろうと思いつく。

その前段で必要前提条件である画像からのオブジェクト検知について使えそうな技術を検証したのが本記事。

全体システムの最終的なゴール

簡易的な交通量監視システムの構築

(趣味の範疇なので精度や速度は無視する。)

本記事の検証項目

静止画像からオブジェクトを検知できて、raspi4bでも動作する軽量な方法を調査・検証

検証実施内容

GPT4-Vision

OpenAI社のVisionモデルでサクッとやろうとしたが、結果的には不適合で検証終了。

https://openai.com/pricing

gpt-4v price

メリット

APIに画像を載せるだけなので導入が超手軽

⚪︎費用も512x512pixの画像なら約0.002ドル/1リクエスト(0.3円くらい)とお手頃。

デメリット

❌オブジェクトのカウントをしてくれない

CAPTHA対策なのかオブジェクトの数を数えるように指示してもNGの旨が返ってくる。

JSON形式でレスポンスを固定できない

システムプロンプトで指示はできるが、完全固定ではない模様(95%くらい守ってくれる?)

△安いとはいえリクエストの都度費用がかかるため大量のリクエストに不向き

1時間に1回リクエストで月220円くらいかかる。

TensorFlow LiteのMobileNet+SSDモデル

お手頃順にtensorFlowを検証したが、こちらはそもそもうまく動作しなかったため、不採用。

Colab上で動作検証をしたが、オブジェクト検知部分がおかしく色々調査してみたが、時間がかかりそうであったためスキップ。

・利用したモデル

lite-model_ssd_mobilenet_v1_1_metadata_2.tflite

github.com

ソースコード(google colab)

!pip install tflite-runtime
import os
import time
from datetime import datetime
import numpy as np
import tflite_runtime.interpreter as tflite
from PIL import Image
from google.colab import drive
drive.mount('/content/drive')
 

# モデルとラベルファイルのパス
model_path = '/content/drive/My Drive/Colab Notebooks/detect.tflite'
label_path = '/content/drive/My Drive/Colab Notebooks/labelmap.txt'

# ラベルファイルの読み込み
with open(label_path, 'r') as file:
labels = [line.strip() for line in file.readlines()]
print(labels)
# TensorFlow Liteモデルのロード
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

# 入出力詳細の取得
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
input_shape = input_details[0]['shape']

# 画像の読み込みと前処理
image = Image.open('/content/drive/My Drive/Colab Notebooks/car_2.jpg')
image = image.resize*1
input_data = np.expand_dims(image, axis=0)

# 推論の実行
interpreter.set_tensor(input_details[0]['index'], input_data)
# 結果の取得
detections = interpreter.get_tensor(output_details[0]['index'])[0]

# 検出されたオブジェクトのラベルと信頼度を出力
for det in detections:
label_index = int(det[0])
print(det)
score = float(det[1])
if label_index in range(len(labels)) and score > 0.5: # 信頼度が50%以上のものだけを表示
print(f"検出されたオブジェクト: {labels[label_index]}, 信頼度: {score:.2f}")

# 車のカウント
car_count = sum(1 for det in detections if det[0] == labels.index('car'))
print(f'画像中の車の数: {car_count}')
['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', '???', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', '???', 'backpack', 'umbrella', '???', '???', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', '???', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', '???', 'dining table', '???', '???', 'toilet', '???', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', '???', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'] [0.6060126 0.4098847 0.81949747 0.74221253] 検出されたオブジェクト: person, 信頼度: 0.41 [0.6511592 0.78893995 0.788709 0.97133744] 検出されたオブジェクト: person, 信頼度: 0.79 [0.63011193 0.58784795 0.658774 0.6115582 ] 検出されたオブジェクト: person, 信頼度: 0.59 [0.67801225 0.96616584 0.75084054 0.98877805] 検出されたオブジェクト: person, 信頼度: 0.97 [0.6713592 0.80989736 0.76514316 0.9385486 ] 検出されたオブジェクト: person, 信頼度: 0.81 [0.6352516 0.84297746 0.65822405 0.8611011 ] 検出されたオブジェクト: person, 信頼度: 0.84 [0.631746 0.6318665 0.6647895 0.6600792] 検出されたオブジェクト: person, 信頼度: 0.63 [0.674443 0.8473915 0.7712389 0.9538163] 検出されたオブジェクト: person, 信頼度: 0.85 [0.62252915 0.5297136 0.6571772 0.5649137 ] 検出されたオブジェクト: person, 信頼度: 0.53 [0.59755045 0.9395489 0.6974551 0.9893865 ] 検出されたオブジェクト: person, 信頼度: 0.94 画像中の車の数: 0
YOLOv4-tinyモデル

本記事で紹介するモデル。特に問題なく動作したためこちらを採用。

こんなお手頃に動作するなんて驚きですね。

 

・簡単な説明(by gpt4)

特徴: YOLO(you only look once)は非常に高速であり、多くの一般物体を検出するための学習済みモデルが豊富に用意されています。
使用方法: YOLOの学習済みモデル(例えばYOLOv3-tinyやYOLOv4-tiny)をダウンロードし、YOLOフレームワークを使用して画像から車両を検出します。

 

メリット

⚪︎ローカルで動作するのでAPI費用が不要

⚪︎実行に要する時間も数秒程度(対象画像の解像度に比例するがHD画質程度であればこのくらい)

デメリット

△導入や閾値調整が必要

 

・利用した学習済みモデル(各種外部ファイルはここからダウンロード)

github.com

 

ソースコード(Colab)

import os
import time
from datetime import datetime
import cv2
import numpy as np
from PIL import Image
from google.colab import drive
from google.colab.patches import cv2_imshow
from collections import Counter
drive.mount('/content/drive')

# 共通パス
base_path = '/content/drive/My Drive/Colab Notebooks/'

# パスの設定
config_path = base_path + 'yolov4-tiny.cfg'
weights_path = base_path + 'yolov4-tiny.weights'
labels_path = base_path + 'coco.names'
image_path = base_path + 'car_4.jpg'

def load_labels(path):
with open(path, 'r') as f:
return [line.strip() for line in f.readlines()]

def load_network(config_path, weights_path):
return cv2.dnn.readNetFromDarknet(config_path, weights_path)

def get_output_layers(net):
layer_names = net.getLayerNames()
return [layer_names[i - 1] for i in net.getUnconnectedOutLayers().flatten()]


def detect_objects(net, image, output_layers):
blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (416, 416), swapRB=True, crop=False)
net.setInput(blob)
return net.forward(output_layers)

def draw_predictions(image, detections, labels):
H, W = image.shape[:2]
boxes =
confidences =
class_ids =
detected_labels =

for detection in detections:
for output in detection:
scores = output[5:]
class_id = np.argmax(scores)
confidence = scores[class_id]

if confidence > 0.5:
box = output[:4] * np.array([W, H, W, H])
centerX, centerY, width, height = box.astype('int')
x, y = int(centerX - width / 2), int(centerY - height / 2)

boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
class_ids.append(class_id)

indices = cv2.dnn.NMSBoxes(boxes, confidences, score_threshold=0.5, nms_threshold=0.4)

if len(indices) > 0:
for i in indices.flatten():
x, y, w, h = boxes[i]
color = [int(c) for c in np.random.randint(0, 255, size=(3,))]
cv2.rectangle(image, (x, y), (x + w, y + h), color, 2)
text = "{}: {:.4f}".format(labels[class_ids[i]], confidences[i])
cv2.putText(image, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
detected_labels.append(labels[class_ids[i]])

return Counter(detected_labels)

# 実行部分
labels = load_labels(labels_path)
net = load_network(config_path, weights_path)
output_layers = get_output_layers(net)
image = cv2.imread(image_path)

detections = detect_objects(net, image, output_layers)
label_counts = draw_predictions(image, detections, labels)

for label, count in label_counts.items():
print(f"{label}: {count}")

cv2_imshow(image)
car: 5
truck: 1

分析画像(一部抜粋)

概ねあってます。

今流行りでのLLMではないですが、これくらいの精度でるなら交通量調査人の代替もすぐではないでしょうか。

 

*1:input_shape[1], input_shape[2]

AWS: コンテナをECRにpushしてECSタスクで起動する

概要

cloud9でイメージを作成→ECR へpush→ECSのタスクとして起動する

dockerfile作成(git参照)

github.com

0.ECRの認証を行う

aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/m2p8a3y8

docker push public.ecr.aws/m2p8a3y8/tweet-something:latest

1.image build

凡例:docker build -t [image_name]:[tag] [Dockerfile_target]

ex) docker build -t python3_cont:1.0 ./

1a.container run

ローカルでコンテナを起動したい場合。

-d でバックグラウンド起動(他と組み合わせて-idt と指定)

docker run -itd [python3_cont]:[1.0]

1b.container exec

ローカルでコンテナの中へ入ってデバッグしたい場合。

sudo docker exec -it <<1a2dfe4802bc>> bash

<<*>>の部分は任意のコンテナID

2.container tag

ECRへ登録するためにECRのタグをイメージに付与する。

docker tag python3_cont:1.0 public.ecr.aws/m2p8a3y8/tweet-something:latest

3.container push

ECRへイメージを登録。

docker push public.ecr.aws/m2p8a3y8/tweet-something:latest

 

 

参考文献

Dockerコンテナをずっと起動しておく - WEB開発ノート

Dockerコマンド よく使うやつ - Qiita

DockerでPython実行環境を作ってみる - Qiita

Dockerfileによるビルド - とほほのWWW入門

AWS EC2のストレージ(EBS)料金を大幅に抑える

EBSの料金を安くする方法

cloud9でたまにしかEC2を起動しないのにEBSで地味に料金がかかっているので、snapshotを作成して料金を抑える。

現状:12ヶ月の無料期間が終了後はgp2の50GBで月に5ドル料金が発生している。

作成手順

1.ボリュームのsnapshotを作成する

EC2 → サイドメニューのボリューム → 対象のボリューム → スナップショットの作成

1分程度で作成完了。

1.snapshot作成
2.EBSボリュームの削除

ボリュームのデタッチを選択

ボリュームの削除

 

これで課金されなくなりました。課金の単位は毎秒(内部的には毎分)で発生するので、削除後にすぐに効力が出ます。

snapshotの料金は見落としているかもですが、請求書上は発生しておらず課金形態が謎。

以下の金額が計上されてました。どうやらEBS料金の4割程度になるみたいです。

$0.05 per GB-Month of snapshot data stored - Asia Pacific (Tokyo)

$0.12 per GB-month of General Purpose SSD (gp2) provisioned storage - Asia Pacific

 

復元手順

 

1.スナップショットからボリュームの作成

サイドバー スナップショット→対象のボリュームを選択→スナップショットからボリュームの作成

 

ボリューム設定は以下を除きデフォルトのまま。

アベイラビリティゾーン(AZ)は作成手順1に記載のAZを選択すること

作成ボタンを押すとすぐにボリュームができる。

2.ボリュームのアタッチ

1、紐づけるインスタンスを選択

2、デバイス名は作成手順1で確認したデバイス名にする(画像は途切れていますが)

/dev/sda1 と入力

 

これで復元完了。

待ち時間はほとんどありませんでした。

ラズパイ起動時にサービスを自動立ち上げする

はじめに

宅急便自動受け取りシステムver2のpythonプログラムをOS起動時に自動起動させたい。

以前はcronでやってたがsystemdでやるのがいい感じらしいのでこの手法でやる。

設定

全体的な流れ

・サービスを登録

・サービス有効化

これだけ

サービス内容

好きなエディタで編集

sudo nano /etc/systemd/system/start_app.service

[Unit]

Description=door open program

After=multi-user.target

 

[Service]

ExecStart=/usr/bin/python3 main.py

WorkingDirectory=/home/<user_name>/Desktop/doorOpen

Restart=always

 

[Install]

WantedBy=multi-user.target

 

Restart=always

で異常終了しても自動でアプリを再起動してくれる

 

有効化

sudo systemctl enable start_app.service

無効化する際は

sudo systemctl disable start_app.service

 

sudo reboot now

自動起動

その他

以上の設定を行うとLINE通知が届かなくなるが、パスや権限の問題の可能性が大。。。

・参考

https://monomonotech.jp/kurage/raspberrypi/systemd_autostart.html

マンションのオートロックを自動で解錠するVer2

前回作ったもの(下記事)から引っ越しにより改修を加えたので改修箇所を中心に記事にします。

realtek.hatenablog.com

 

主な改修点

ラズパイが話せるようになった

LINE通知できるようになった

イベント検知で駆動できるようにコードを書き換えた

GPIOからシャットダウン・起動できるようになった

 

全体像

全体像

 

 

回路接写

動画Ver

youtube.com

 

ソースコードGitHub

GitHub - tech0x86/raspi3B-doorOpen: auto lock unlocker

 

ラズパイが話せるようになった

OpenJtalkを利用して任意の音声を話せるようになりました。

jtalk.shのシェルスクリプトpythonからコールしてます。

 

jtalk.sh

#!/bin/bash

tempfile=`tempfile`

option="-m /usr/share/hts-voice/mei/mei_happy.htsvoice \

-x /var/lib/mecab/dic/open-jtalk/naist-jdic \

-ow $tempfile"

echo "$1" | open_jtalk $option

aplay -q $tempfile

rm $tempfile

 

main.py

import subprocess

def jtalk_script(message):
    subprocess.run([PATH_JTALK +" "+message],shell=True)

 

 

・参考

Raspberry Pi(ラズパイ)を音声合成で日本語をしゃべらせてみた!(OpenJTalk) | りけろぐ

LINE通知できるようになった

LINE Notifyで来訪等を通知できるようになりました。

TOKENは下記コマンドでOSの環境変数化しています。

export LINE_TOKEN=XXXX

 

main.py

import requests
LINE_NOTIFY_TOKEN = os.environ.get("LINE_TOKEN")
LINE_NOTIFY_API = "https://notify-api.line.me/api/notify"

# line に通知する
def send_line_message(notification_message):
    headers = {"Authorization": f"Bearer {LINE_NOTIFY_TOKEN}"}
    data = {"message": f" {notification_message}"}
    r = requests.post(LINE_NOTIFY_API, headers = headers, data = data)
    print(r)
    time.sleep(1)

 

通知の画面

 

・参考

Pythonで、LINEにメッセージを通知する方法【LINE NOTIFY】 | ラズパイの実

イベント検知で駆動できるようにコードを書き換えた

前作の無限ループで状態をチェックするのもあまりスマートではないため、割り込み関数的な処理に変更。(以下の関数)

GPIO.add_event_detect(PIN_CDS, GPIO.RISING, callback=light_detected, bouncetime=5000) # 割り込み関数

 

ただ、誤作動が結構ある。。

 

GPIOからシャットダウン・起動できるようになった

プログラムがフリーズ等してもタクトスイッチを押すことでOSを安全にシャットダウン・起動できるようになった

下記ファイルの最終行に追記

/boot/config.txt

dtoverlay=gpio-shutdown

 

回路上でGPIO 3 (ピン番号 5)  GND (ピン番号 6)を短絡する

 

・参考

ラズパイでコードを書かずにシャットダウン・電源ボタンを作る - Qiita

ラズパイをディスプレイ無しでSSH接続までセットアップする

環境を新規構築もしくは引越しした際に環境が変わった時に、わざわざディスプレイとキーボードを繋いでセットアップするのが面倒だったので、SSH接続までの手順を記録。

前提条件

ルータにDHCP機能があること(有線、無線)

クライアント端末はMac OS(他のOSの場合は適宜読み替えてください)

 

新規環境構築

Raspberry Pi Imager を利用してOSをSDカードへ書き込み

一番上のOSを選択。

raspberry pi image書き込み

オプションを設定する(重要)

画像の通り自身の環境に合わせて設定

SSHログインする際に必要なのでメモしておく。

オプションの設定

OS書き込み

10分程度で書き込み完了。SDカードをラズパイに差して電源をいれて起動。

 

SSH接続

上の設定で間違いがなければ自宅のLANに接続されているはず。

ルータのDHCP機能でIPが自動割り振られているが、IPアドレスは分からないので、ホストネームとユーザ名を指定してssh ログインする(下記コマンド)。

ssh <username>@<hostname>.local

上の例だと

ssh pi3b1@pi3b1.local

※ログインパスワードは↑の画像で設定したユーザログインのものと同じ。

 

IPアドレスでログインする場合は

ssh pi3b1@192.168.0.10 とか

 

IPアドレス固定

DHCPだと勝手にIPアドレスが変わってしまうため、静的IPアドレスを設定。

以下は無線LANの設定例(有線LANの場合はwlan0ではなく、eh0)

下記ファイルを編集

nano /etc/dhcpcd.conf

<ファイル末尾に追記>

interface wlan0

static ip_address=192.168.0.10

static routers=192.168.0.1

static domain_name_servers=8.8.8.8 192.168.0.1

 

ラズパイを再起動して設定を反映

sudo reboot now

 

上記操作後にクライアント端末(macとか)で同じようにSSH接続しようとすると

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

のエラーが出る場合があります。

もし出たらクライアント端末で、該当のラズパイのアドレス箇所を削除しましょう。

nano /Users/<user_name>/.ssh/known_hosts

 

パッケージ更新

下記コマンドでパッケージを更新する。apt-getから変わったみたい。

sudo apt update

sudo apt full-upgrade

 

既存の環境修正

引越しなどでルータが変わった場合に接続先を更新する対応です。

必要なのはLANケーブル1本だけ。

無線LANには勝手に繋がらないので、ルータ/ハブに有線で繋いでDHCPでアドレスを自動割り当てされるようにしましょう。

そしたら下の書き方で接続されるはず。

ssh pi3b1@pi3b1.local

 

無線LANの設定の更新

下記ファイルに接続情報があるみたいです。

nano 等で開いて各自の環境に合わせて編集。

/etc/wpa_supplicant/wpa_supplicant.conf

編集

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=JP

network={
ssid="ssid" #接続する予定のWi-FiのSSID"で括る
psk="password" #パスワード
key_mgmt=WPA-PSK
}

あとは再起動しましょう。