LoginSignup
15

高校生が約1か月で電車の自動運転システムを作った話

Last updated at Posted at 2023-10-03

はじめに

 この論文?は筑波大学の受験のために作成したが、残念ながら落ちてしまい、そのままこの論文?を放置するのは勿体ないと思い、公開するのことにした。
@Neko_syashin

注意
作業が進み次第、内容を追加していきます。

12/13
内容を更新しました。

12/23
試運転の動画を投稿しました。
https://youtu.be/kAjmtUdXs90?si=Q7V_Mf8ruGz1n8ZR

ブラウザに表示される映像の様子
https://youtu.be/kSbipy8ivTU?si=DxVyfhAf5oFGzu2W

1. これまでの画像処理関係の活動

 私は、高校在籍時に独学でYoloを使用した画像認識を作成していた。そのときは、カメラや動画などを使い、人や車などを判別しカウント(図1)をしたり、カメラにサーボモータを取り付け、人が動いた方向に追尾するというシステム(図2)を開発していたり、車がバックするときにカーナビに表示される、空から見たような映像の開発(図3)などを行っていた。以下の画像がそのときに作成していたものである。
画像1-2-3.png
 高校3年生のとき、学校の課題研究の授業で、電車の自動運転化に取り組んだのでその制作について述べる。このプロジェクトのAIシステムの製作期間は約1か月である。

注意
まだ作成途中であり、中途半端なところが多く存在するが、大まかなシステム構成は完成している。

2. 担当範囲と電車、自動運転の仕様について

 私は電車の自動運転の部分を一人で担当した。電車の動力車と客車のボディおよび動力部については、別のメンバーが製作した。
1. 電車の主な仕様
 ・ 電車のモデルはJR貨物の北斗星
 ・ 動力車1両に、客車を1,2両繋げ、数人乗ることを想定
 ・ 線路のサイズは5インチゲージ(12.7cm幅)
 ・ 最高速度は時速10km程度
 ・ モータ(三相誘導電動機 60W)を動力車に4つ搭載
 ・ ESP32からPWM信号を与えて速度制御を行う
2. 自動運転の要求仕様
 ・ 無線接続された操作用PCがあり、そこで速度調整等の操作および電車付属のカメラからの映像の確認ができるようにする
 ・ 信号機の色を判別して自動で速度調整・停止を行う
 ・ 電車の近くに人が居た場合、緊急停車させる
 ・ 速度調整のためのPWM信号を出力するESP32に、必要な速度を指定して渡す。
以下の画像は、試作車の車体とフレームである。
add_image1.pngadd_image2.png
実際に車輪部分を組み立てた様子
add_image3.png

3. 使用するコンピュータ、システムの検討

 まずは核となる画像処理に使用するコンピュータについて検討したうえで、その後システム全体の構成を検討することにした。今回はGPUを搭載しAIの処理ができる小型コンピュータのJetson Nanoを使用したシステムを考案したが、その経緯について述べる。
1. AIによる画像処理を選んだ理由
 今回制作している電車は、様々な場所に設置して展示等を行う予定である。よって周辺の環境によっては人以外の動作物が存在し、それを検出して停止等の誤動作をする可能性がある。そのため、確実に「人」のみを検出できるAIによる画像処理を選択した。

2. Jetson Nanoを選択した理由
 AIによる画像処理を行うシステムとして、私はこれまではGPUを搭載したPCを使用してきた。しかし、今回のシステムに適用するには、重量面や車内の設置スペースの面で厳しい。また、電車はバッテリーで動作しているが、動力に多くの電力を使用する以上、とてもGPUを搭載したPCに800W程度の電力を割くことはできず、バッテリーの持ちを考慮し、断念した。
 以上から、小型かつ省電力のコンピュータでAIによる画像処理が必須であると考えた。これまでの経験から、Raspberry piがカメラで受け取った画像をクラウドに送信して処理することを考えたが、画像をクラウドに送信後、結果を受信するまでにインターネットの回線状況などによって遅延が生じるため、緊急停車のシステムには向かないと判断した。
 そのため、車体の中に組み込めるサイズで、省電力で高速にAIの処理が可能なGPUを搭載し、安価であるJetson Nanoを使用することにした。
3. 全体のシステム構成
 Jetson Nanoは、車体に取り付けているカメラから映像を取得し、その映像を画像認識のプログラムで処理する。このときに、人を検出した場合には、Jetson Nanoからローカルネットワーク名にあるESP32に緊急停車させる信号を送信する。ESP32で信号を受信したあと、ESP32からPWM信号を出力しモータを緊急停車させるというシステムにした。(図4) また、信号機の識別をする際にも同じ仕組みで処理を行う。
 処理した画像は、Raspberry Piで構築したWebサーバに送り、そのWebサイトに表示する。Raspberry Piは無線ルーターに接続されているため、操作用PCを同じ無線ルーターに接続し、WebサーバのIPアドレス先にアクセスすると、そのWebサイトで画像を確認することができる。
画像4.png

4. 課題、検討事項について

 上記のシステム構成を元に製作を進めたが、その上で直面した問題点・検討すべき点にどのように取り組んだか述べる。

【検討事項1】Jetson Nanoによる信号機・人の画像認識処理の検討

1. プログラムの開発環境の検討
 まず初めに、Jetson Nanoで画像認識のプログラムを作成するにあたり、DeepStreamと呼ばれるソフトを使用した場合と、Nvidiaが提供しているライブラリであるJetson-inferenceを用いてプログラム製作を行った場合について、比較し検討した。

DeepStream
メリット デメリット
プログラムが既に作成されているため、カメラや動画などを指定するだけで動く。 自分で作成したプログラムを導入しにくい。
プログラムが最適化されているため、Jetson Nanoの場合は8つの同時ストリーム処理ができる RTSP通信でディスプレイがなくても遠隔で映像を確認できるが、遅延がある。・セットアップに時間がかかる。
Jetson-inference
メリット デメリット
すべてPythonで作れる DeepStreamは複数の映像を処理できるが、こちらでは1,2ストリームが限界
処理がJetsonに最適化されていて動作が軽い セットアップに時間がかかる
独自のプログラムを組み込める RTSP通信だけでなく、WebSocketなどPythonで使えるものなら簡単に導入できる。

 DeepStreamは、モジュラープラグインのブロックを接続していく形式で簡単にプログラムが作れるメリットはあるが、独自のプログラムを組み込むには時間がかかるという大きな問題がある。今回は、独自プログラムの分量が多いことから、Jetson-inferenceを採用した。

2. 信号機および人の機械学習の検討
 人物の検出については、機械学習するには時間がかかるため、Googleが提供しているmobilenet-ssdというモデルを使用し、検出するようにした。
 信号機の識別はmobilenet-ssdではできないので、自分で機械学習を行った。
まず、モデルを作成するには大量の画像が必要になるため、Pythonを使用したスクレイピングで画像を収集した。今回収集できた画像枚数は100枚程で機械学習させるためには少ないため、Pythonを使用し、すべての画像を90度、180度、270度をそれぞれ回転させた画像を保存し、枚数を増やした。次に、画像をそれぞれの色ごとに分類する必要があるため、Vottというソフトを使用し1枚ずつ分類する。(図5)その後、これらをJetson Nanoを使用し、機械学習をさせた。実際に作成したモデルを使用したが、機械学習に使用した画像の枚数が少なかったためあまり精度は高くはなかったが、許容できる範囲であったためそのまま使用した。(図6)
画像5.png
画像6.png

【検討事項2】画像処理後の映像の転送遅延の改善

  Jetson Nanoで処理した映像を、無線LANで接続した操作用PCで、リアルタイムにWebやメディアプレイヤーなどで表示させることを考えたが、できるだけ遅延を少なくする必要がある。当初はRTSP通信を使用し、VLCメディアプレイヤーで確認してみたが、約2秒の遅延(図8)があり、とてもリアルタイム性があるとはいえないものだった。
  そこで、新たにWebSocketによる通信を検討した。RTSP通信はデータを単方向に送るのに対して、WebSocketはデータを双方向でやり取りでき、低遅延で通信できる。また、RTSP通信はVLCなどのメディアプレイヤーを使用し映像を確認していたのに対して、WebSocketは画像を1秒間に大量に送信し、映像のように動いているように見せており、ブラウザで映像を確認できるようになる。PythonでWebSocketを使用するにはFlaskというモジュールを使う必要があり、実際に使用してプログラムを作成した。(図7)
画像7.png
 処理の流れについて説明する。まずJetson NanoでPythonを用いて画像認識処理をした後の画像をjpegに変換する。そのあとjpeg画像をbase64のバイナリデータに変換する。jpeg画像のままだと、一度画像を保存してそのファイルをWebサーバが読み込む必要が生じる。結果、1秒間に30フレーム処理されるので、大量の画像が保存されデバイスの保存容量が圧迫されてしまう。base64のバイナリデータにすることで画像が文字列に変換され、変換された文字列をWebサーバ側でデコードすることでWebサイトに画像を表示することができる。Webサーバに文字列を送るには、WebSocketの通信を確立する必要があり、WebサイトでJavaScriptのSocket-ioライブラリを使用し、接続を確立している。
 WebSocketに切り替えたことにより、RTSP通信では約2秒の遅延があったのに対して、WebSocketでは遅延はほぼゼロになることを確認した。(図9)
画像8-9.png

【検討事項3】操作用PCへのデータ転送方法、ユーザインターフェイスの検討

  WebSocketを使用することになったため、Webにすべてのデバイスカメラの映像を表示することができるようになった。また、デザインはあくまで電車の制御室という目標なので、それを目指して作成した。(図10)しかし、まだ製作途中なので配置などは未完成である。
画像10.png
Jetson Nanoと操作用PC間の通信をWebSocketで構築したため、双方向に通信できるようなり、Webのみで制御することが可能になった。操作・画像確認用PCの表示画面は、htmlとCSS, JavaScriptを用いて作成した。速度の加減速、自動運転から手動運転に切り替える機能、電車の進行方向の変更機能を付け加えた。速度の加減速はゲージ式になっており、直感的に加減速のレベル(図11,12,13)を見やすいようにした。
画像11-12-13.png
また、ここまでWebで作れるなら、すべてWebで完結できるようにしようと考え、Webサイトから画像認識のプログラムを実行するために、すべてのデバイスと接続されたSSHをブラウザに組み込むことにした。以下の画像はWebサイトに埋め込まれたコマンドプロンプトからコマンドを実行した様子である。(図14)
画像14.png
このブラウザにアクセスするためには、Jetson NanoやRaspberry Piが入っているローカルネットワーク内に接続する必要がある。そのためには、車体の中に無線ルーターを組み込みWi-Fiを飛ばす必要がある。しかし、電車は動くので、見ている場所によっては、電車が遠くに行ったときに電波の接続が悪くなり、映像が乱れる可能性がある。これを改善するために、線路の脇などに散らばるように複数台の無線ルーターを置き、Mesh Wi-Fiで無線ルーター同士を繋ぐことにした。電車が移動しても電波の強度は強いままになるので途切れにくくなる。(図15)
画像15.png

【検討事項4】電車の速度制御部の検討

1. Jetson Nanoとモータ制御用ESP32との接続
 この自動運転システムは全方位の人を検出するため、カメラ1台と、それに対となるJetson Nanoを計6台搭載している。6台のJetson Nanoから、モータ制御用ESP32に速度調整の信号(接近する人を検出した場合の停止信号など)を送る必要がある。PythonのモジュールでWebsocketというものがあり、それを使用してESP32にローカルネットワーク経由で信号を送ろうとしたが、何か他のモジュールと競合してしまい、ブラウザに映像が映らない+ボタンの反応がしなくなったため、このモジュールを使用することはやめた。そこで、一度ブラウザに信号を送り、ブラウザからESP32に送信する方法にした。(図16)
画像16.png

2. ESP32によるモータ制御
 モータ(図17)を制御するには、Jetson Nanoから送られた値をESP32 でPWM信号に変換し、チームメイトが作成したPWM信号を周波数に変換するプログラムが書き込まれたマイコン(図18)へと送る必要がある。また、ESP32では速度の変更だけではなく、進行方向の変更もするため、モータに接続しているリレー回路(図19)にPWM信号を送る必要がある。
画像17-18-19.png

5. 完成したシステム

画像20.png

6. 残された課題、今後について

 このプロジェクトは10月に開催される文化祭までに完成する予定だが、まだ沢山の課題が残っている。例えば、信号の赤、青、黄と変更できる信号機の作成や、信号機の認識精度、速度計の作成、人との距離測定などが課題として残る。特に、人との距離測定は必須であり、現状では、遠くにいる人を検出した場合も緊急停車してしまうので、人を検出したときに、車体の先頭に取り付けた超音波センサなどの距離測定機からの値が5M以内のときに緊急停車させる。という風にしなければ完璧な自動運転とは言えない。
 私の代が卒業したら、このプロジェクトは後輩に受け継がれる流れになっているが、基礎的な構成はしっかりと検討したので、さらに進化させることができると考えている。

7.追加内容

1. 線路の枕木について

 文化祭の展示の際は発泡目地棒というプラスチックで作られた棒を20cmにカットし、枕木として使用していた。
add_image4.png
しかし、使用していたネジの皿が小さかったので、レールを持ち上げると枕木が抜けてしまう問題が発生した。また、発泡目地棒が軽すぎるがあまりに乗り心地があまり良くなかった。そこで、内田工業株式会社様からオレンジウッドという積水樹脂株式会社様が発売している合成樹脂を提供してもらい、それに変更した。その結果、乗り心地は快適に。また、レールを枕木に固定するネジを長くし、ワッシャーを加えることで、レールに接するネジの皿の面積率を増やし、しっかりと固定されるように改良した。
 add_image5.png

1. 信号機の作成

  信号機は主に自動運転のために使用することとし、赤、黄、青の三色とし、赤はブレーキ、黄は徐行、青は加速と分けた。

add_image6.png

信号機の制御は電車の手動運転と同じブラウザから直感的に操作できるように作成した。

add_image7.png

信号機の回路はESP32を使用し、作成した。回路としては、ブラウザからwebsocketでデータを受信し、受信した値によってESP32の出力PINを調整しPWMを出力する仕組みだ。例えば、ブラウザで赤を選択したら、ESP32のD23からPWMを出力する。出力されたPWMはトランジスタを経由して電流を増幅し、5Vリレーを動作させる。また、信号機のLEDは12VのテープLEDを使用している。
add_image8.png

8.ソースコード

 結構適当に書いているので、参考にならないと思います。

1.信号機の検出

from flask import Flask
from flask_socketio import SocketIO, emit
import cv2
from jetson_inference import detectNet
from jetson_utils import videoSource,cudaDeviceSynchronize,cudaToNumpy,cudaFont
import argparse
import sys
import base64
import time
from concurrent.futures import ThreadPoolExecutor
import signal


class Detect_frame():
    #初期化
    def __init__(self):
        self.app = Flask(__name__)
        self.socketio = SocketIO(self.app, cors_allowed_origins='*')
        self.socketio.on_event('update_state_mode', self.update_state_mode, namespace='')
        self.socketio.on_event('update_state_vectle', self.update_vectle_mode, namespace='')
        self.socketio.on_event('state-change', self.change_state_mode, namespace='')
        self.socketio.on_event('state-vectle', self.change_vectle_mode, namespace='')

        self.parser = argparse.ArgumentParser(description="Classify a live camera stream using an image recognition DNN.",
                                        formatter_class=argparse.RawTextHelpFormatter,
                                        epilog=detectNet.Usage())
        self.parser.add_argument("--network", type=str, default="", help="pre-trained model to load (see below for options)")
        self.parser.add_argument("--overlay", type=str, default="box,labels,conf", help="detection overlay flags (e.g. --overlay=box,labels,conf)\nvalid combinations are:  'box', 'labels', 'conf', 'none'")
        self.parser.add_argument("--threshold", type=float, default=0.6, help="minimum detection threshold to use")

        try:
            self.args = self.parser.parse_known_args()[0]
        except:
            print("")
            self.parser.print_help()
            sys.exit(0)

        #認識モデルの指定
        self.detect_net = detectNet(model="model/ssd-mobilenet.onnx", labels="model/labels.txt", 
                        input_blob="input_0", output_cvg="scores", output_bbox="boxes", 
                        threshold=self.args.threshold)
        #同時に実行できるスレッドの指定
        self.executor = ThreadPoolExecutor(max_workers=8) 
            
        self.cap = videoSource("csi://0", options={'width': 1280, 'height': 720, 'framerate': 30})
        self.running = True
        self.font = cudaFont()
        self.state = "manual_mode"
        self.state_vectle = "forward"
        self.tracking_time =  time.time()
        self.powervalue = 0
        self.capture_count = 0
        self.tracking_blue_count=0
        self.tracking_red_count=0
        self.tracking_yellow_count=0
        self.power_change_time = 0
        self.last_result = 0
        self.tm = cv2.TickMeter()
        self.count = 0
        self.max_count = 10
        self.fps = 0
        self.flag = "True"

        #進行方向を前進に初期化
        self.train_controle(self,5000)

    def generate_frames(self):
        self.tm.start()

        while self.running:
            self.cuda_frame = self.cap.Capture()
            if self.cuda_frame == None:
                break
            self.cap_time = time.time()
            self.detections = self.detect_net.Detect(self.cuda_frame,overlay=self.args.overlay)
            cudaDeviceSynchronize()
            print(f"sys:{(time.time() - self.cap_time):.2f}")
            if self.count == self.max_count:
                self.tm.stop()
                self.fps = self.max_count /self.tm.getTimeSec()
                self.tm.reset()
                self.tm.start()
                self.count = 0
            print(f"fps={self.fps:.2f}")
            self.count += 1
            self.font.OverlayText(self.cuda_frame, text=f"{self.fps:.2f}", 
                        x=5, y=5,
                        color=self.font.White, background=self.font.Gray40)
            
            frame = cudaToNumpy(self.cuda_frame)
            cudaDeviceSynchronize()

            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            #フレームをBase64エンコードして送信
            self.executor.submit(self.encode_frame, frame)  # 'encode_frame'関数を並列実行
            if self.flag == "True":
                self.executor.submit(self.detection_thread, self.detections) #detections表示を並列

    #映像をbase64に変換してからブラウザに画像(映像)を送信
    def encode_frame(self,frame):
        self._ , frame = cv2.imencode('.jpg', frame, params=[cv2.IMWRITE_JPEG_QUALITY, 70])
        jpg_frame = base64.b64encode(frame).decode('utf-8')
        self.socketio.emit('image_data',jpg_frame)

    def detection_thread(self,detections):
        for detection in detections:
            self.flag = "False"
            class_id = detection.ClassID
            #検出したオブジェクトのフレーム表示など
            class_label = self.detect_net.GetClassDesc(class_id)
            confidence = detection.Confidence
            print(f"detection: {confidence:.2f}% class #{class_id} ({class_label}) ({self.detect_net.GetNetworkFPS():.2f})")
            if self.state != "manual_mode":
                confidence = round(confidence, 2)
                #青判定
                if class_label == 'traffic_blue' and confidence*100 >= 80: 
                    self.tracking_blue_count += 1
                #赤判定
                elif class_label == 'traffic_red' and confidence*100 >= 80: 
                    self.tracking_red_count += 1
                #黄判定
                elif class_label == 'traffic_yellow' and confidence*100 >= 80: 
                    self.tracking_yellow_count += 1
                
                if  int(time.time() - self.tracking_time) >= 2:
                    print("TRACKING_TIME_START")
                    #青速度調整判定
                    if self.tracking_blue_count >= 5:
                        self.tracking_time = time.time()    
                        self.tracking_blue_count = 0
                        powervalue = 220
                        self.executor.submit(self.train_controle, powervalue)
                        self.tracking_time = time.time()
                    #赤速度調整判定
                    elif self.tracking_red_count >= 1:                       
                        self.tracking_time = time.time()
                        self.tracking_red_count = 0
                        powervalue = 40
                        self.executor.submit(self.train_controle, powervalue)
                        self.tracking_time = time.time()
                    #速度調整判定
                    elif self.tracking_yellow_count >= 2:                       
                        self.tracking_time = time.time()
                        self.tracking_yellow_count = 0
                        powervalue = 0
                        self.executor.submit(self.train_controle, powervalue)
                        self.tracking_time = time.time()
                    
                elif time.time() - self.tracking_time >= 20:
                    self.tracking_time = time.time()
                    print('reset')
                    self.tracking_blue_count = 0
                    self.tracking_red_count = 0
                    self.tracking_yellow_count = 0

        self.flag = "True"

    def train_controle(self,powervalue):
        if self.last_result != powervalue:
            print("traffic_controle_start")
            #websocketモジュールが何かと競合して使えなかったため、一度ブラウザに値を送信してからesp32に値を送信する。
            self.socketio.emit('auto_pwm', powervalue)
            self.last_result = powervalue
        #240 加速5 20 非常ブレーキ
        #220 加速4 60 ブレーキ4 
        #200 加速3 80 ブレーキ3
        #180 加速2 100 ブレーキ2
        #160 加速1 120 ブレーキ1
    
    #ブラウザが更新されたときに、現在の運転モードを送信する
    def update_state_mode(self):
        self.socketio.emit('update_state_mode', self.state)

    #ブラウザが更新されたときに、現在の方向モードを送信する
    def update_vectle_mode(self):
        self.socketio.emit('update_state_vectle', self.state_vectle)

    #ブラウザから運転モードの切り替えをしたときにstateを変更する
    def change_state_mode(self):
        if self.state == "auto_mode":
            self.state = "manual_mode"
        else:
            self.state = 'auto_mode'
        self.socketio.emit('update_state_mode', self.state)

    #ブラウザから運転モードの切り替えをしたときにvectleを変更する
    def change_vectle_mode(self):
        if self.state_vectle == "forward":
            self.state_vectle = "back"
        else:
            self.state_vectle = 'forward'
        self.socketio.emit('update_state_vectle', self.state_vectle)

    def shutdown_handler(self):
        print("Flask app is shutting down.")
        self.running = False
        self.cap.Close()
        time.sleep(1)
        self.ws.close()
        sys.exit(0)  # アプリケーションを終了させる

    def run(self):
        self.socketio.run(self.app, host='0.0.0.0', port=5000)

if __name__ == '__main__':
    ai = Detect_frame()
    try:
        ai.socketio.start_background_task(ai.generate_frames)
        ai.run()
    except KeyboardInterrupt:
        ai.shutdown_handler()

2.人の検出

from flask import Flask
from flask_socketio import SocketIO, emit
import cv2
from jetson_inference import detectNet
from jetson_utils import videoSource,cudaDeviceSynchronize,cudaToNumpy,cudaFont,videoOutput, Log
import argparse
import sys
import base64
import time
from concurrent.futures import ThreadPoolExecutor
import signal


class Detect_frame():
    def __init__(self):
        self.app = Flask(__name__)
        self.socketio = SocketIO(self.app, cors_allowed_origins='*')
        self.socketio.on_event('state-change', self.change_state_mode, namespace='')

        self.parser = argparse.ArgumentParser(description="Locate objects in a live camera stream using an object detection DNN.", 
                                        formatter_class=argparse.RawTextHelpFormatter, 
                                        epilog=detectNet.Usage() + videoSource.Usage())
        self.parser.add_argument("input", type=str, default="", nargs='?', help="URI of the input stream")
        self.parser.add_argument("output", type=str, default="", nargs='?', help="URI of the output stream")
        self.parser.add_argument("--network", type=str, default="ssd-mobilenet-v2", help="pre-trained model to load (see below for options)")
        self.parser.add_argument("--overlay", type=str, default="box,labels,conf", help="detection overlay flags (e.g. --overlay=box,labels,conf)\nvalid combinations are:  'box', 'labels', 'conf', 'none'")
        self.parser.add_argument("--threshold", type=float, default=0.5, help="minimum detection threshold to use") 

        try:
            self.args = self.parser.parse_known_args()[0]
        except:
            print("")
            self.parser.print_help()
            sys.exit(0)

        self.detectNet  = detectNet(self.args.network, sys.argv, self.args.threshold)
        
        self.executor = ThreadPoolExecutor(max_workers=30) 
            
        self.cap = videoSource("csi://0", options={'width': 1280, 'height': 720, 'framerate': 30})
        self.running = True
        self.font = cudaFont()
        self.state = "manual_mode"
        self.tracking_time =  time.time()
        self.powervalue = 0
        self.capture_count = 0
        self.person_count=0
        self.power_change_time = 0
        self.tm = cv2.TickMeter()
        self.count = 0
        self.max_count = 10
        self.fps = 0
        self.flag = "True"

        #進行方向を前進に初期化
        self.train_controle(self,5000)

    def generate_frames(self):
        self.tm.start()

        while self.running:
            self.cuda_frame = self.cap.Capture()
            if self.cuda_frame == None:
                break
            self.cap_time = time.time()
            self.detections = self.detect_net.Detect(self.cuda_frame,overlay=self.args.overlay)
            cudaDeviceSynchronize()
            print(f"sys:{(time.time() - self.cap_time):.2f}")
            # detection_thread(detections,cv2_time,capture_count) #detections表示を並列
            if self.count == self.max_count:
                self.tm.stop()
                self.fps = self.max_count /self.tm.getTimeSec()
                self.tm.reset()
                self.tm.start()
                self.count = 0
            print(f"fps={self.fps:.2f}")
            self.count += 1
            self.font.OverlayText(self.cuda_frame, text=f"{self.fps:.2f}", 
                        x=5, y=5,
                        color=self.font.White, background=self.font.Gray40)
            
            frame = cudaToNumpy(self.cuda_frame)
            cudaDeviceSynchronize()

            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            #フレームをBase64エンコードして送信
            self.executor.submit(self.encode_frame, frame)  # 'encode_frame'関数を並列実行
            if self.flag == "True":
                self.executor.submit(self.detection_thread, self.detections) #detections表示を並列

    def encode_frame(self,frame):
        self._ , frame = cv2.imencode('.jpg', frame, params=[cv2.IMWRITE_JPEG_QUALITY, 70])
        jpg_frame = base64.b64encode(frame).decode('utf-8')
        self.socketio.emit('image_data',jpg_frame)

    def detection_thread(self,detections):
        for detection in detections:
            self.flag = "False"
            class_id = detection.ClassID
            class_label = self.detect_net.GetClassDesc(class_id)
            confidence = detection.Confidence
            print(f"detection: {confidence:.2f}% class #{class_id} ({class_label}) ({self.detect_net.GetNetworkFPS():.2f})")
            if self.state != "manual_mode":
                confidence = round(confidence, 2)
                #青判定
                if class_label == 'person' and confidence*100 >= 80: 
                    self.person_count += 1

                if  int(time.time() - self.tracking_time) >= 2:
                    print("TRACKING_TIME_START")
                    #青速度調整判定
                    if self.person_count >= 5:
                        self.tracking_time = time.time()    
                        self.person_count = 0
                        powervalue = 20
                        self.executor.submit(self.train_controle, powervalue)
                        self.tracking_time = time.time()
                    
                elif time.time() - self.tracking_time >= 20:
                    self.tracking_time = time.time()
                    print('reset')
                    self.person_count = 0
                    
        self.flag = "True"

    def train_controle(self,powervalue):
        self.socketio.emit('auto_pwm', powervalue)
        self.last_result = powervalue
        #240 加速5 20 非常ブレーキ
        #220 加速4 60 ブレーキ4 
        #200 加速3 80 ブレーキ3
        #180 加速2 100 ブレーキ2
        #160 加速1 120 ブレーキ1

    def change_state_mode(self):
        if self.state == "auto_mode":
            self.state = "manual_mode"
        else:
            self.state = 'auto_mode'

    def shutdown_handler(self):
        print("Flask app is shutting down.")
        self.running = False
        self.cap.Close()
        time.sleep(1)
        self.ws.close()
        sys.exit(0)  # アプリケーションを終了させる

    def run(self):
        self.socketio.run(self.app, host='0.0.0.0', port=5000)

if __name__ == '__main__':
    ai = Detect_frame()
    try:
        ai.socketio.start_background_task(ai.generate_frames)
        ai.run()
    except KeyboardInterrupt:
        ai.shutdown_handler()

3.ESP32

#include "WiFi.h"
#include "ESPAsyncWebServer.h"

const char* ssid = "******";
const char* password =  "******";
const IPAddress ip(192,168,100,30);
const IPAddress gateway(192,168,100,1); //gatewayのIPアドレス
const IPAddress subnet(255,255,255,0);
int a;
char input;

AsyncWebServer server(80);
AsyncWebSocket ws("/");
 
void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
 
  if(type == WS_EVT_CONNECT){
 
  } else if(type == WS_EVT_DISCONNECT){
    Serial.println("Client disconnected");
 
  } else if(type == WS_EVT_DATA){
    Serial.print("Data received: ");
    if(len == 3){
      char input_1 = data[0]; 
      char input_2 = data[1]; 
      char input_3 = data[2];
      int a = atoi(&input_1);
      int b = atoi(&input_2);
      int c = atoi(&input_3);
      Serial.println(a);
      Serial.println("ok");
      analogWrite(23,a);
    }else if(len == 2){
      char input_1 = data[0]; 
      char input_2 = data[1]; 

      int a = atoi(&input_1);
      int b = atoi(&input_2);
      Serial.println(a);
      Serial.println("ok");
      analogWrite(23,a);
    }else if(len == 1){
      char input_1 = data[0]; 
      int a = atoi(&input_1);
      Serial.println(a);
      Serial.println("ok");
      analogWrite(23,a);
    }else if(len == 4){
      char input_1 = data[0];
      int a = atoi(&input_1);
      if(a == 5){
        Serial.println(a);
        analogWrite(22,255);
      }else if(a == 6){
        analogWrite(22,0);
      }
    }
  }
}

void setup() {
  pinMode(23, OUTPUT); // 向き
  pinMode(22, OUTPUT); // 速度
  Serial.begin(115200);
  WiFi.config(ip, gateway, subnet);
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
 
  Serial.println(WiFi.localIP());
 
  ws.onEvent(onWsEvent);
  server.addHandler(&ws);
 
  server.begin();
}
 
void loop(){}


4.WebControle

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>スタートレイン</title>
    <script src="javascript/jquery-3.6.0.min.js"></script>      
    <script src="javascript/socket.io.js"></script>
    <script type="text/javascript" src="javascript/controle.js" charset="utf-8"></script>
    <link rel="stylesheet" type="text/css" href="css/controle.css" />
    <link rel="stylesheet" type="text/css" href="css/ssh.css" />
    <link rel="stylesheet" type="text/css" href="css/index.css" />
    <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
    <link href="https://fonts.googleapis.com/css?family=Montserrat+Subrayada" rel="stylesheet">
    <script type="text/javascript" src="node_modules/xterm/lib/xterm.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-fit/lib/xterm-addon-fit.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-ligatures/lib/xterm-addon-ligatures.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-search/lib/xterm-addon-search.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-serialize/lib/xterm-addon-serialize.js" charset="utf-8"></script>
</head>
<body>
    <header>
        <h1 class="hero-text"><a href="index.html">StarTrainPoject</a></h1>
        <nav class="pc-menu">
            <ul>
            <li><a href="index.html">Home</a></li>
            <li><a href="about.html">About</a></li>
            <li><a href="Control.html">Controle</a></li>
            <li><a href="ssh.html">SSH</a></li>
            </ul>
        </nav>
        <div class="sp-menu">
            <input type="checkbox" id="sp-menu__check">
            <label for="sp-menu__check" class="sp-menu__box">
              <span></span>
            </label>
            <div class="sp-menu__content">
              <ul class="sp-menu__list">
                <li class="sp-menu__item">
                  <a class="sp-menu__link" href="#">Home</a>
                </li>
                <li class="sp-menu__item">
                  <a class="sp-menu__link" href="#">About</a>
                </li>
                <li class="sp-menu__item">
                  <a class="sp-menu__link" href="#">Controle</a>
                </li>
                <li class="sp-menu__item">
                  <a class="sp-menu__link" href="#">SSH</a>
                </li>
              </ul>
            </div>
        </div>
    </header>
    <div class="flask-container">
        <div class="flask-left">
            <h1>前方</h1>
                <div class="video-frame">
                    <a href="flask/flask1.html" target="_blank">
                        <img id="camera-image1" src="" alt="Camera Stream 1" width="448" height="252">
                    </a>
                </div>
        </div>
        <div class="flask-left">
            <h1>後方</h1>
            <div class="video-frame">
                <a href="flask/flask2.html" target="_blank">
                    <img id="camera-image2" src="" alt="Camera Stream 2" width="448" height="252">
                </a>
            </div>
        </div>
        <div class="flask-right">
            <h1></h1>
            <div class="video-frame">
                <a href="flask/flask3.html" target="_blank">
                    <img id="camera-image3" src="" alt="Camera Stream 3" width="448" height="252">
                </a>
            </div>
        </div>
        <div class="flask-right">
            <h1></h1>
            <div class="video-frame">
                <a href="flask/flask4.html" target="_blank">
                    <img id="camera-image4" src="" alt="Camera Stream 4" width="448" height="252">
                </a>
            </div>
        </div>
    </div>
    <div class="controle-main">
        <h1 class="controle-title">操作盤</h1>
        <div class="controle-sub">
            <div class="power-controle">
                <div class="clock-power">
                    <div class="merter">
                        <div class="nezi"></div>
                        <div class="speed-master">
                            <div class="speed-child">
                                <div class="dials">
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                </div>
                                <div class="dials-min">
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                    <div></div>
                                </div>
                                <div class="speed-num speed-1"></div>
                                <div class="speed-num speed-2">15</div>
                                <div class="speed-num speed-3"></div>
                                <div class="speed-num speed-4">20</div>
                                <div class="speed-num speed-8">0</div>
                                <div class="speed-num speed-9"></div>
                                <div class="speed-num speed-10">5</div>
                                <div class="speed-num speed-11"></div>
                                <div class="speed-num speed-12">10</div>
                                <div class="speed-num speed-a"></div>
                                <div class="hand speed-hand"></div>
                                <div class="speed-circle"></div>
                                <div class="speed-circle-2"></div>
                            </div>
                        </div>
                        <div class="clock">
                            <div class="clock-child">
                                <div class="clock-num num-1">1</div>
                                <div class="clock-num num-2">2</div>
                                <div class="clock-num num-3">3</div>
                                <div class="clock-num num-4">4</div>
                                <div class="clock-num num-5">5</div>
                                <div class="clock-num num-6">6</div>
                                <div class="clock-num num-7">7</div>
                                <div class="clock-num num-8">8</div>
                                <div class="clock-num num-9">9</div>
                                <div class="clock-num num-10">10</div>
                                <div class="clock-num num-11">11</div>
                                <div class="clock-num num-12">12</div>
                                <div class="clock-num num-a"></div>
                                <div class="hand hour-hand"></div>
                                <div class="hand minute-hand"></div>
                                <div class="hand second-hand"></div>
                            </div>
                        </div>
                    </div>
                    <div class="gauge">
                        <div class="gauge-bar"></div>
                    </div>
                    <div class="power-change">
                        <div class="power-button">
                            <button id="power-down"class="button-power" onclick="decreaseGauge()">下げる</button>
                            <button id="power-up" class="button-power" onclick="increaseGauge()">上げる</button>
                            <button id="power-reset" class="button-reset" onclick="resetGauge()">リセット</button>
                        </div>
                    </div>
                </div>
                <div class="state-display">
                    <div class="controle-state-mode"><h1 id="text-display-state" class="state"></h1></div>
                    <div class="controle-state-vectle"><h1 id="text-display-vectle" class="state"></h1></div>

                </div>
                <div class="change-button">
                    <div class="button-state">
                        <div class="state-mode-change">
                            <button id="state-mode" class="state-change">運転切替</button>
                        </div>
                        <div class="state-mode-change">
                            <button id="state-vectle" class="vectle-change">向き切替</button>
                        </div>
                    </div>
                    <div class="emergency">
                        <button id="emergency_btn" class="emergency_button">緊急停止</button>
                    </div>
                </div>
                    
                </div>
            </div>
            <div class="ssh">
                <div class="terminal" id="terminal2"></div>
            </div>
        </div>
    </div>
  <footer>
        <ul class="footer-menu">
            <li><a href="index.html">home</a></li>
            <li><a href="about.html">about</a></li>
            <li><a href="contact.html">contact us</a></li>
        </ul>
        <p class="all-right">© All rights reserved by star-train by ASK</p>
      </footer>
</body>
</html>

5.ControleScript

const socket1 = io.connect('http://192.168.100.11:5000');
const socket2 = io.connect('http://192.168.100.12:5000');
const socket3 = io.connect('http://192.168.100.13:5000');  
const socket4 = io.connect('http://192.168.100.14:5000');
const esp32 = new WebSocket('ws://192.168.100.30:80/');

socket1.on('image_data', function(imageData){
    var src = "data:image/jpeg;base64," + imageData;
    $("#camera-image1").attr('src', src);
});

socket2.on('image_data', function(imageData){
    var src = "data:image/jpeg;base64," + imageData;
    $("#camera-image2").attr('src', src);
});

socket3.on('image_data', function(imageData){
    var src = "data:image/jpeg;base64," + imageData;
    $("#camera-image3").attr('src', src);
});

socket4.on('image_data', function(imageData){
    var src = "data:image/jpeg;base64," + imageData;
    $("#camera-image4").attr('src', src);
});

document.addEventListener("DOMContentLoaded", function () {
    updateClock();
});
function updateClock() {
    const now = new Date();
    const hour = now.getHours();
    const minute = now.getMinutes();
    const second = now.getSeconds();

    const hourHand = document.querySelector(".hour-hand");
    const minuteHand = document.querySelector(".minute-hand");
    const secondHand = document.querySelector(".second-hand");

    // 時、分、秒を角度に変換して針を動かす
    const hourAngle = (hour % 12 + minute / 60) * 30 + 180;
    const minuteAngle = (minute + second / 60) * 6 + 180;
    const secondAngle = second * 6 + 180;

    hourHand.style.transform = `rotate(${hourAngle}deg)`;
    minuteHand.style.transform = `rotate(${minuteAngle}deg)`;
    secondHand.style.transform = `rotate(${secondAngle}deg)`;
    }
setInterval(updateClock, 1000);

var gaugeValue = 15// ゲージの初期値
var powervalue = 10

window.addEventListener('load', () => {
    // ローカルストレージから前回の `powervalue` と `gaugevalue` の値を取得
    const savedPowervalue = localStorage.getItem('powervalue');
    const savedGaugevalue = localStorage.getItem('gaugevalue');
    // ローカルストレージに値が保存されている場合、それらの値を変数に設定
    if (savedPowervalue !== null) {
        // powervalue = parseInt(savedPowervalue, 10);
        powervalue = parseInt(savedPowervalue, 10);
    } else {
        // ローカルストレージに値が保存されていない場合のデフォルト値
        powervalue = 10;
    }

    if (savedGaugevalue !== null) {
        // gaugevalue = parseInt(savedGaugevalue, 10);
        gaugeValue = parseInt(savedGaugevalue, 10);
    } else {
        // ローカルストレージに値が保存されていない場合のデフォルト値
        gaugeValue = 15;
    }

    // ゲージを更新
    updateGauge();
});

let flag = 0
const maxGaugeValue = 165; // ゲージの最大値
const gaugeColors = ['#ff0000', '#ff3300', '#ff6600', '#ff9900', '#ffcc00', '#000000', '#cccc33', '#b3cc33', '#99cc33', '#66cc33', '#00e600']; // ゲージの色のパレット

//ゲージの増加、速度送信
function updateGauge() {
    const gaugeBar = document.querySelector('.gauge-bar');
    const gaugeWidth = (gaugeValue / maxGaugeValue) * 100; // ゲージバーの幅の割合を計算
    gaugeBar.style.width = `${gaugeWidth}%`;
    let colorIndex;
    if (gaugeValue < 90) {
        colorIndex = Math.floor(gaugeValue / 15)-1; // 1~5までは15ずつ増加するので、15で割った商をインデックスに使用
    } else if (gaugeValue == 90) {
        colorIndex = 5; // 6段階目は黒色
    } else {
        colorIndex = Math.floor((gaugeValue - 7 * 15) / 15) + 6; // 7段階目以降は15ずつ増加するので、15で割った商に6を足した数をインデックスに使用
        colorIndex = Math.min(colorIndex, gaugeColors.length - 1); // カラーパレットの範囲を超えないように制限
    }
    gaugeBar.style.backgroundColor = gaugeColors[colorIndex];
}

function increaseGauge() {
    if(powervalue >= 60){
        gaugeValue += 15; // 1段階の増加量
        gaugeValue = Math.min(gaugeValue, maxGaugeValue); // 最大値を制限
        localStorage.setItem('gaugevalue', gaugeValue.toString());
        updateGauge();
    }
    if (powervalue  < 240 || powervalue != 240){
        if (powervalue == 140){
            powervalue = 0
        }
        else if(powervalue == 0){
            powervalue = 160
        }
        else if(powervalue == 240){
        }
        else{
            powervalue += 20
        }
        localStorage.setItem('powervalue', powervalue.toString());
        esp32.send(powervalue);
    }
}

function decreaseGauge() {
    if(powervalue >= 60){
        gaugeValue -= 15; // 1段階の減少量
        gaugeValue = Math.max(gaugeValue, 15); // 最小値を制限
        localStorage.setItem('gaugevalue', gaugeValue.toString());
        updateGauge();
    }
    if (powervalue  - 20 >= 20 || powervalue != 20){
        if (powervalue == 160){
            powervalue = 0
        }
        else if(powervalue == 0){
            powervalue = 140
        }
        else if(powervalue == 20){
        }
        else{
            powervalue -= 20
        }
        localStorage.setItem('powervalue', powervalue.toString());
        esp32.send(powervalue);
    }
}

document.addEventListener("DOMContentLoaded", function () {
    updateGauge();
});
// リセットボタンを押したらゲージをリセットする
document.addEventListener("DOMContentLoaded", function () {
    const resetBtn = document.getElementById("power-reset");

    resetBtn.addEventListener("click", function () {
        let reset_result = window.confirm('値が初期化されます。\n実行しますか?\n※必ず停車中に行ってください。');
        if (reset_result){
            resetGauge()
        }
    })
});

function resetGauge(){
    gaugeValue = 15
    powervalue = 60
    localStorage.setItem('gaugevalue', gaugeValue.toString());
    localStorage.setItem('powervalue', powervalue.toString());
    updateGauge();
};

let last_state = ""
let last_vectle = ""
// サーバーからのテキストを受信して表示
socket1.on('update_state_mode', function(text1) {
    if (last_state != text1){
        last_state = text1
        document.getElementById('text-display-state').innerText = text1;
        socket2.emit(text1)
        socket3.emit(text1)
        socket4.emit(text1)
        if (text1 == "auto_mode"){
                document.getElementById("state-mode").style.backgroundColor = "#ff000ar0";
                now_state_color_mode = 1
            }
            else{
                document.getElementById("state-mode").style.backgroundColor = "#0000FF"
                now_state_color_mode = 0
            } 
    }  
});

socket1.on('update_state_vectle', function(text2) {
    if (last_vectle != text2){
        last_vectle = text2
        document.getElementById('text-display-vectle').innerText = text2;
        if (text2 == "forward"){
                document.getElementById("state-vectle").style.backgroundColor = "#ff0000";
                now_state_color_mode = 1

            }
            else{
                document.getElementById("state-vectle").style.backgroundColor = "#0000FF"
                now_state_color_mode = 0
            }  
    }
});

// ブラウザが更新されたらサーバーにリクエストを送信
document.addEventListener("DOMContentLoaded", function() {
    socket1.emit('update_state_mode');
});
document.addEventListener("DOMContentLoaded", function() {
    socket1.emit('update_state_vectle');
});

document.addEventListener("DOMContentLoaded", function () {
    const stateBtn = document.getElementById("state-mode");
    stateBtn.addEventListener("click", function () {
        socket1.emit('state-change'); // ボタンをクリックしたときにサーバーにメッセージを送信
    })
});

var total_esp_count = 1
document.addEventListener("DOMContentLoaded", function () {
    const vectleBtn = document.getElementById("state-vectle");
    vectleBtn.addEventListener("click", function () {
        socket1.emit('state-vectle')
        if( total_esp_count % 2 == 0){
            esp32.send(5000)
        }else{
            esp32.send(6000)
        }
        total_esp_count += 1
    })
});



document.addEventListener("DOMContentLoaded", function () {
    const emergencyBtn = document.getElementById("emergency_btn");
    
    emergencyBtn.addEventListener("click", function () {
        const emergencyaudio = new Audio("music/emergency.mp3");
        let result = window.confirm('緊急停止ボタンが押されました\n実行しますか?');
        if (result){
            esp32.send(10);
            emergencyaudio.play();
            emergencyBtn.style.animation = "blink 3s linear infinite";
            document.getElementById("power-down").style.visibility = 'hidden'
            document.getElementById("power-up").style.visibility = 'hidden'
            document.getElementById("power-reset").style.visibility = 'hidden'
            resetGauge()
            setTimeout(function() {
                emergencyBtn.style.animation = "none"; // アニメーションを停止
                document.getElementById("power-down").style.visibility = 'visible'
                document.getElementById("power-up").style.visibility = 'visible'
                document.getElementById("power-reset").style.visibility = 'visible'
                emergencyaudio.pause();
                emergencyaudio.currentTime = 0;
            }, 2000);
        }
    });
});

let resetAngle = 60
document.addEventListener("DOMContentLoaded", function () {
    function speedmeter(targetAngle) {
        const hourHand = document.querySelector(".speed-hand");
        let currentAngle = 60; // スタートは60
        const increment = 1; // 1度ずつ増加する場合の値(調整可能)
    
        animate(); // アニメーションを開始
        // アニメーション関数
        function animate() {
            if (currentAngle < targetAngle) {
                currentAngle += increment;
                hourHand.style.transform = `rotate(${currentAngle}deg)`;
                requestAnimationFrame(animate);
            }
        }
    
    }
    
    // 例:120度までアニメーション
    speedmeter(120);
});

// jetsonから自動モードのときに速度を受け取り、esp32に送信する。
socket1.on('auto_pwm', function(powervalue){
    esp32.send(powervalue);
}); 
socket2.on('auto_pwm', function(powervalue){
    esp32.send(powervalue);
}); 
socket3.on('auto_pwm', function(powervalue){
    esp32.send(powervalue);
}); 
socket4.on('auto_pwm', function(powervalue){
    esp32.send(powervalue);
}); 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15