編集履歴一覧に戻る
airpocketのアイコン画像

airpocket が 2023年12月22日14時50分16秒 に編集

コメント無し

本文の変更

# はじめに Raspberry Pi zero2 Wで @PINTO03091 さんのYOLOXの最小最速モデルを動かします。 画像処理約50msec、推論約50msecとなっており実質速度は100msec(10fps)程度をたたき出しました。

+

@[youtube](https://youtu.be/U7uy0EsC1A8) この記事の方法では、指定フレーム数の画像データを取得し、取得完了後にmp4データを出力しています。この方法ではリアルタイムに推論画像を確認できないため、[こちらの記事](https://elchika.com/article/d615d800-a360-49eb-bb88-c7d86f49c2be/)で動画をストリーミング出力する様改造しています。 ストリーミング版ではmp4ファイル出力は行っていませんので、用途に応じて修正するか用途によって使い分けてください。 @[twitter](https://twitter.com/AirpocketRobot/status/1737456492108984559)

# 環境 ハードはRPiにカメラを付けるだけで、操作はすべてPCからのヘッドレス運用を行います。 ## ハードウェア PC : [Raspberry Pi zero2 W](https://www.switch-science.com/products/7600) Camera : [Arducam IMX708搭載 Raspberry Pi用 固定焦点カメラモジュール(標準)](https://www.switch-science.com/collections/arducam/products/9036) カメラケーブル : [FFC 0.5mmピッチ 22PIN Bタイプ](https://www.amazon.co.jp/uxcell-FFC%E3%83%AA%E3%83%9C%E3%83%B3%E3%83%95%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%83%AC%E3%82%AD%E3%82%B7%E3%83%96%E3%83%AB%E3%82%B1%E3%83%BC%E3%83%96%E3%83%AB-3D%E3%83%97%E3%83%AA%E3%83%B3%E3%82%BF%E3%83%BC-%E3%83%A9%E3%83%83%E3%83%97%E3%83%88%E3%83%83%E3%83%97-%E3%82%AA%E3%83%BC%E3%83%87%E3%82%A3%E3%82%AA%E7%94%A8/dp/B09V2L3HNZ/) microSD : 16GB以上程度の適当なもの USB micro to USB type A ケーブル : データ通信用の適当なもの RPiとカメラとカメラケーブルだけ新規購入するだけだと¥7880で実行可能。 ※カメラは個人的趣味によりV3相当の固定焦点カメラを使用していますが、[カメラモジュールV3](https://www.switch-science.com/products/8700)でもそのまま動くはずです。Raspberry Pi zero2のカメラ設定を変更し、コードのimage キャプチャ部を修正すればUSBカメラやカメラモジュールV2でも動くはずです。 ## ソフトウェア RPiのOSはBullseye 64bit lite を使用します。 tfliteのruntimeはYOLOXのモデルは @PINTO03091 さんのリポジトリより使用させていただきます。 それぞれ[こちらのリポジトリ](https://github.com/PINTO0309/TensorflowLite-bin)の2.15.0と[こちらのリポジトリ](https://github.com/PINTO0309/PINTO_model_zoo/tree/main/426_YOLOX-Body-Head-Hand)のyolox_ti_body_head_hand_n_1x3x128x160_bgr_uint8.tfliteを使用します。 # 環境構築 ## Raspberry Pi OS RPiのOSはRaspberry Pi Imagerを使用してください。 使用するOSは、[Raspberry Pi OS (othrer)]⇒[Raspberry Pi OS(Legacy, 64-bit) Lite]です。 イメージの焼き方は紹介サイトがたくさんあると思いますので参考にしてください。 書き込む際に、[設定を編集する]⇒[サービス]からSSHを有効にしておきます。 また、ホスト名を付けて覚えておきます。 wifi接続設定もしておきます。 ## ヘッドレス設定 Raspberry Pi zero2 Wにはmicro HDMIやUSBポートが付いていますので、キーボードマウスやディスプレイを直接接続して作業できますが、面倒なのでUSBケーブルを通じたSSH接続をするのが便利です。 wifi設定をしておけばwifi接続でのSSHも可能ですが、ルートを二本確保しておいた方が何かと便利です。 USB SSHを使ったヘッドレス運用については[こちらのサイト](https://karakuri-musha.com/inside-technology/02-raspi-otg-setting/)や[こちらのサイト](https://hinablo.hinasur.com/2021/07/12/%E3%80%90ssh%E6%8E%A5%E7%B6%9A%E3%81%BE%E3%81%A7%E3%80%91raspberry-pi-zero-w%E3%82%92windowspc%E3%81%A0%E3%81%91%E3%81%A7%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97%E3%81%99%E3%82%8B/)に詳しいため、ポイントのみ記録しておきます。 ### RPi側 OSを焼いたmicroSDをPCで読み取り、config.txt とcmdline.txt を編集します。 config.txtは、一番最後に次の一行を追記します。 ``` dtoverlay=dwc2 ``` USB SSHするだけならこの行だけで良いのですが、ついでにIMX708を使うための設定も書き込んでおきます。 ``` dtoverlay=imx708 ``` cmdline.txtは、rootwait と quietの間に次の様に[modules-load=dwc2,g_ether]という文字列を挿入します。 ``` .....rootwait modules-load=dwc2,g_ether quiet...... ``` ### PC側 PCはwin10、win11 を想定しています。 [こちらのサイト](https://www.catalog.update.microsoft.com/Search.aspx?q=Ethernet%2FRNDIS%20Gadget)の、「Acer Incorporated. - Other hardware - USB Ethernet/RNDIS Gadget」の「Windows 7, Windows 8, Windows 8.1 and later drivers」をダウンロードします。 コマンドプロンプトでダウンロードしたフォルダに入り、次のコマンドで解凍します。 ``` expand '対象ファイル名' -F:* D:\OTGdriver ``` Raspberry Pi zero2 WにmicroSDカードを指し、USB microケーブルでPCと接続します。USB microコネクタは2個ついていますが、左側(HDMIコネクタ側)に挿します。 デバイスマネージャからRPiが刺さっているUSBシリアルデバイスを探し、[ドライバーの更新]から先ほどダウンロードしたドライバを指定してインストールします。 ### 動作の確認 USB micro ケーブルでPCと接続した状態で適当なツールからSSH接続します。OSを焼いた際に決めたhost名.local をターゲットとしてアクセスできます。 以降の作業はSSH接続したRPi上での作業となります。 ## swap 無効化と/ tmp RAM化 動画取得時のレイテンシを安定化させるためswapを無効化します。 ``` sudo systemctl stop dphys-swapfile sudo systemctl disable dphys-swapfile ``` 動画撮影のレイテンシを小さくするためtmpフォルダをRAMディスク化します。 ``` sudo nano /etc/fstab ``` 以下の行を追加 ``` tmpfs /tmp tmpfs defaults,size=64m,noatime,mode=1777 0 0 ``` SD上の/tmpを削除 ``` sudo rm -rf /tmp ``` ## カメラモジュールv3をデバイスツリーに追加 先ほどUSB SSHの際にconfig.txtに書き込んでいる場合は不要です。 config.txt にデバイスツリーを追加する設定を書き込みます。 ``` sudo nano /boot/config.txt ``` 以下の行を追加 ``` dtoverlay=imx708 ``` ここまで作業したら一度reboot します。 ## pipをインストール ``` sudo apt update && sudo apt -y upgrade sudo apt -y install python3-dev python3-pip ``` ## picamera2をインストール Raspberry Pi カメラモジュールV3についているIMX708を制御する為picamera2、opencv、依存ライブラリをインストールします。 ``` pip3 install picamera2 pip3 install opencv-python sudo apt -y install libgl1-mesa-dev ``` ## tflite runtimeをインストール 詳細情報は[リポジトリ](https://github.com/PINTO0309/TensorflowLite-bin)をご覧ください。 ``` sudo apt install -y swig libjpeg-dev zlib1g-dev python-is-python3 unzip wget curl git cmake make pip install -U pip pip install numpy TFVER=2.15.0.post1 PYVER=39 ARCH=aarch64 pip install --no-cache-dir https://github.com/PINTO0309/TensorflowLite-bin/releases/download/v${TFVER}/tflite_runtime-${TFVER/-/}-cp${PYVER}-none-linux_${ARCH}.whl ``` ## YOLOXモデルをダウンロード 適当な作業場所を作って作業します。 ``` mkdir ~/YOLOX cd ~/YOLOX ``` モデルダウンロード用のスクリプトをダウンロードして実行します。 ``` wget https://raw.githubusercontent.com/PINTO0309/PINTO_model_zoo/main/426_YOLOX-Body-Head-Hand/download_custom_yolox_for_quantization.sh sudo chmod 755 download_custom_yolox_for_quantization.sh ./download_custom_yolox_for_quantization.sh ``` 種々のモデルが含まれており、全部で1.4GBほどあります。しばらくダウンロードを待ちます。 今回使用するモデルは、以下の三つの8bit量子化されたモデルです。 yolox_ti_body_head_hand_n_1x3x128x160_bgr_uint8.tflite yolox_ti_body_head_hand_n_1x3x256x320_bgr_uint8.tflite yolox_ti_body_head_hand_n_1x3x480x640_bgr_uint8.tflite # 推論する 作業フォルダ内で以下のコードを実行すると10秒程度の動画を撮影してtest.mp4と言うファイルに保存します。 ``` """ This demo code is designed to run a lightweight model for edge devices at high speed instead of degrading accuracy due to INT8 quantization. runtime: https://github.com/PINTO0309/TensorflowLite-bin code cited from: https://qiita.com/UnaNancyOwen/items/650d79c88a58a3cc30ce https://github.com/PINTO0309/PINTO_model_zoo/blob/main/426_YOLOX-Body-Head-Hand/demo/demo_yolox-ti_tflite.py """ import cv2 import time import numpy as np from typing import List from picamera2 import Picamera2 import os # params WEIGHTS = "yolox_ti_body_head_hand_n_1x3x128x160_bgr_uint8.tflite" # WEIGHTS = "yolox_ti_body_head_hand_n_1x3x256x320_bgr_uint8.tflite" # WEIGHTS = "yolox_ti_body_head_hand_n_1x3x480x640_bgr_uint8.tflite" NUM_CLASSES = 3 SCORE_THRESHOLD = 0.50 IOU_THRESHOLD = 0.4 CAP_WIDTH = 320 #出力動画の幅 CAP_HEIGHT = 240 #出力動画の高さ LAW_WIDTH = 2304 #カメラ内のraw画像の幅 LAW_HEIGHT = 1296 #カメラ内のraw画像の高さ count = 0 folder_path ="/tmp/img" movie_length = 100 #撮影するフレーム数 time_list = [] num_threads = 4 #スレッド数 1-4を選択 exposure_time = 5000 #イメージセンサの露出時間 analog_gain = 20.0 #イメージセンサのgain coutner = 0 movie_file_name = "test.mp4" #出力ファイル名 codec = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') # detection model class for yolox class DetectionModel: # constructor def __init__( self, *, weight: str, ): self.__initialize(weight=weight) # initialize def __initialize( self, *, weight: str, ): from tflite_runtime.interpreter import Interpreter # type: ignore self._interpreter = Interpreter(model_path=weight, num_threads=num_threads) self._input_details = self._interpreter.get_input_details() self._output_details = self._interpreter.get_output_details() self._input_shapes = [ input.get('shape', None) for input in self._input_details ] self._input_names = [ input.get('name', None) for input in self._input_details ] self._output_shapes = [ output.get('shape', None) for output in self._output_details ] self._output_names = [ output.get('name', None) for output in self._output_details ] self._model = self._interpreter.get_signature_runner() self._h_index = 1 self._w_index = 2 strides = [8, 16, 32] self.grids, self.expanded_strides = \ self.__create_grids_and_expanded_strides(strides=strides) # create grids and expanded strides def __create_grids_and_expanded_strides( self, *, strides: List[int], ): grids = [] expanded_strides = [] hsizes = [self._input_shapes[0][self._h_index] // stride for stride in strides] wsizes = [self._input_shapes[0][self._w_index] // stride for stride in strides] for hsize, wsize, stride in zip(hsizes, wsizes, strides): xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize)) grid = np.stack((xv, yv), 2).reshape(1, -1, 2) grids.append(grid) shape = grid.shape[:2] expanded_strides.append(np.full((*shape, 1), stride)) grids = np.concatenate(grids, 1) expanded_strides = np.concatenate(expanded_strides, 1) return grids, expanded_strides # detect objects def __call__( self, *, image: np.ndarray, score_threshold: float, iou_threshold: float, ): self.image_shape = image.shape prep_image, resize_ratio_w, resize_ratio_h = self.__preprocess(image=image) datas = { f'{input_name}': input_data \ for input_name, input_data in zip(self._input_names, [np.asarray([prep_image], dtype=np.uint8)]) } outputs = [ output for output in \ self._model( **datas ).values() ][0] boxes, scores, class_ids = \ self.__postprocess( output_blob=outputs, resize_ratio_w=resize_ratio_w, resize_ratio_h=resize_ratio_h, ) boxes, scores, class_ids = \ self.__nms( boxes=boxes, scores=scores, class_ids=class_ids, score_threshold=score_threshold, iou_threshold=iou_threshold, ) return class_ids, scores, boxes # preprocess def __preprocess( self, *, image: np.ndarray, ): resize_ratio_w = self._input_shapes[0][self._w_index] / self.image_shape[1] resize_ratio_h = self._input_shapes[0][self._h_index] / self.image_shape[0] resized_image = \ cv2.resize( image, dsize=(self._input_shapes[0][self._w_index], self._input_shapes[0][self._h_index]) ) return resized_image, resize_ratio_w, resize_ratio_h # postprocess def __postprocess( self, *, output_blob: np.ndarray, resize_ratio_w: float, resize_ratio_h: float, ): output_blob[..., :2] = (output_blob[..., :2] + self.grids) * self.expanded_strides output_blob[..., 2:4] = np.exp(output_blob[..., 2:4]) * self.expanded_strides predictions: np.ndarray = output_blob[0] boxes = predictions[:, :4] boxes_xywh = np.ones_like(boxes) # yolox-ti boxes[:, 0] = boxes[:, 0] / resize_ratio_w boxes[:, 1] = boxes[:, 1] / resize_ratio_h boxes[:, 2] = boxes[:, 2] / resize_ratio_w boxes[:, 3] = boxes[:, 3] / resize_ratio_h boxes_xywh[:, 0] = (boxes[:, 0] - boxes[:, 2] * 0.5) boxes_xywh[:, 1] = (boxes[:, 1] - boxes[:, 3] * 0.5) boxes_xywh[:, 2] = ((boxes[:, 0] + boxes[:, 2] * 0.5) - boxes_xywh[:, 0]) boxes_xywh[:, 3] = ((boxes[:, 1] + boxes[:, 3] * 0.5) - boxes_xywh[:, 1]) scores = predictions[:, 4:5] * predictions[:, 5:] class_ids = scores.argmax(1) scores = scores[np.arange(len(class_ids)), class_ids] return boxes_xywh, scores, class_ids # non maximum suppression def __nms( self, *, boxes: np.ndarray, scores: np.ndarray, class_ids: np.ndarray, score_threshold: float, iou_threshold: float, ): indices = \ cv2.dnn.NMSBoxesBatched( bboxes=boxes, scores=scores, class_ids=class_ids, score_threshold=score_threshold, nms_threshold=iou_threshold, ) # OpenCV 4.7.0 or later keep_boxes = [] keep_scores = [] keep_class_ids = [] for index in indices: keep_boxes.append(boxes[index]) keep_scores.append(scores[index]) keep_class_ids.append(class_ids[index]) if len(keep_boxes) > 0: keep_boxes = np.vectorize(int)(keep_boxes) return keep_boxes, keep_scores, keep_class_ids # get raudom colors def get_colors(num: int): colors = [] np.random.seed(0) for _ in range(num): color = np.random.randint(0, 256, [3]).astype(np.uint8) colors.append(color.tolist()) return colors # save mp4 def movie_save(): global count average_time = 0 for i in range(count - 1): average_time += time_list[i + 1] average_time /= (count) fps = 1 / average_time print(fps) video = cv2.VideoWriter(movie_file_name, codec, fps, (CAP_WIDTH, CAP_HEIGHT)) if not video.isOpened(): print("cant't be opened") sys.exit() for i in range(count): frame = cv2.imread(folder_path + "image_" + str(i) + ".jpg") video.write(frame) video.release() # main def main(): global count print(count) # check folder path if not os.path.exists(folder_path): os.makedirs(folder_path) print("Image folder created") else: print("Image folder exists") # read image cap = Picamera2() config = cap.create_still_configuration(main={"size":(CAP_WIDTH, CAP_HEIGHT)},raw={"size":(LAW_WIDTH,LAW_HEIGHT)}) cap.configure(config) cap.set_controls({"ExposureTime":exposure_time, "AnalogueGain": analog_gain}) cap.start() # create detection model class for yolox model = DetectionModel(weight=WEIGHTS) # detect objects score_threshold = SCORE_THRESHOLD iou_threshold = IOU_THRESHOLD for i in range(movie_length): start_time_frame = time.perf_counter() #res, image = cap.read(i) image = cap.capture_array() image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) start_time = time.perf_counter() class_ids, scores, boxes = \ model( image=image, score_threshold=score_threshold, iou_threshold=iou_threshold, ) elapsed_time = time.perf_counter() - start_time cv2.putText( image, f'{elapsed_time*1000:.2f} ms', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA, ) cv2.putText( image, f'{elapsed_time*1000:.2f} ms', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 1, cv2.LINE_AA, ) # draw objects num_classes = NUM_CLASSES colors = get_colors(num_classes) for box, score, class_id in zip(boxes, scores, class_ids): color = colors[class_id] thickness = 2 line_type = cv2.LINE_AA cv2.rectangle(image, box, color, thickness, line_type) # save movie cv2.imwrite(folder_path + "image_" + str(count) + ".jpg", image) elapsed_time_frame = time.perf_counter() - start_time_frame print("frame_number = " + str(count) + " / time = " + str(elapsed_time_frame)) time_list.append(elapsed_time_frame) count += 1 movie_save() if __name__ == '__main__': main() ```