2021年11月19日金曜日

PYNQ-Z2向けのアクリル天板のレーザーカットを頼んでみた話



修論が忙しいのに何も手を付けていない中の人です。修論で自前のFPGAボード(PYNQ-Z2)を使ってるのですが、基本裸運用なのでショートやらなんやらが怖い状態です。今回はその対策としてアクリルの天板を作成してみた話です。

基板には元から足がついているので5mm程度浮いているが,
基本むき出しなので不安が残る





1.設計する


部品の配置図.
配置するエリアのサイズを入れることで,領域内に入りますよアピール.

天板の設計をするにあたり寸法図を探したのですが、ネットの海にはなかったようでした。そこでその辺にノギスが生えていたので各部を測定し寸法を割り出しました。寸法を割り出したのちFusion360で実際の設計を行い、dxfで出力しました。



2.発注する


完成予想図

設計ができたので次は発注です。今回は手間をあまりかけたくなかったので、レーザーカットのサービスを使うこととし、いろいろ探した結果いつも基板を頼んでいるElecrowのサービスを使うことにしました。
基板の発注と異なる点として、領域内であればいくつでも部品を置くことができます。無理に1つの部品にする必要はありませんでした。
先ほど作成したdxfのファイルとともに、カット時の部品の配置図と完成予想図をpdfで添付しました。完成予想図は正直に言っていらないのですが、たまたまあったので入れました。
発送業者に関しては、最近追加された佐川急便を使ってみました。

3.到着と組み立て


届いた天板とFPGA基板.合計5セット届く.

発注が11/10で、到着が11/18だったので1週間ちょっとぐらいでした。おそらく基板と同じ感覚で待てばよさそうです。
実際に組み立てるとこんな感じになりました。PYNQ-Z2に搭載されるチップであればそこまで発熱しないのでファンは不要なのですが、スペースが開いていたので付けました。
スペーサーは秋月、ねじ・ナット・ワッシャーは近所のホームセンターで購入しました。

組み立てた様子.この時はファンが外側についている.
ファンが外に出っ張っていると壊す恐れがあり,この後内側に移設した.

4.まとめ


初めてレーザーカットのサービスを使ってみましたが、特に詰まるところはありませんでした。簡単な形状であればプリント基板を作るよりも楽にできました。印字みたいなこともできるようなので、また機会があれば試してみたいです。


©2021 shts All Right Reserved.


2021年11月12日金曜日

ctypesを使ってPython3からC++の生配列(not std::vector)をいじる


修論がだんだん忙しくなってきた中の人です.研究でPythonからC++のコードをたたく必要が出てきたのでいろいろお試ししていたのですが,その際にいろいろ詰まったので書き残しておきます.今回のコードはこちらに上げてあります.



1.PythonからC++をさわる


Python側からC++のコードを実行する方法はPython.hやSWIG,Boost.Python,pybind11など様々なものがあります(参考1).今回の研究で使用している環境(HLSツールや実行環境)の問題で,以下の制約がありました.

  • 相互呼び出しの機能は不要(PythonからC++を実行できればOK)
  • STLコンテナが使えない(≒std::vectorなどを使う必要がない)
  • g++やCPythonに標準で入っているライブラリのみで行けると非常に楽

いろいろ調べた結果,とりあえずctypesを使ってみることにしました.

2.C++で共有ライブラリを作成する


今回は以下のC++コードをPythonから呼び出します.

// MAC_VEC.hpp
#include <cstdint>
namespace MAC_VEC{
    template<typename data_t>
    void mac_vec(uint32_t size, data_t a[], data_t x[], data_t b[], data_t y[]){
        for(uint32_t i = 0; i < size; i++){
            y[i] = a[i] * x[i] + b[i];
        }
    }
}

ctypesを使う場合テンプレートをそのまま呼び出すことはできないので,型を確定させるラッパーを作っておきます.

// MAC_VEC_WRAPPER.cpp
#include <cstdint>
#include "MAC_VEC.hpp"

extern "C" {
    void MAC_VEC_FP32(uint32_t size, float a[], float x[], float b[], float y[]){
        MAC_VEC::mac_vec<float>(size, a, x, b, y);
    }
    void MAC_VEC_INT16(uint32_t size, int16_t a[], int16_t x[], int16_t b[], int16_t y[]){
        MAC_VEC::mac_vec<int16_t>(size, a, x, b, y);
    }
    void MAC_VEC_UINT32(uint32_t size, uint32_t a[], uint32_t x[], uint32_t b[], uint32_t y[]){
        MAC_VEC::mac_vec<uint32_t>(size, a, x, b, y);
    }
}

とりあえずfloat(fp32),int16, uint32の3パターン準備しました.この時extern "C"をつけないと,コンパイラ側でシンボル名を書き換える操作を行うため,Python側から読めなくなることがあります(参考2).
この二つが準備できたら,g++で共有ライブラリ(.so)を生成します.

// generate_so.sh
g++ MAC_VEC_WRAPPER.cpp -shared -o MAC_VEC.so

コンパイルが通ると,MAC_VEC.soが生成されるはずです.

3.Pythonから動かす


先ほど作成したライブラリをPythonから動かしてみましょう.今回使用するctypesは,Python2.5から標準で入っているライブラリです.
この記事ではMAC_VEC_FP32のみを扱います.残りの二つのに関しては,こちらの実装を確認してください.

// C_API_TEST.py
import ctypes

# load .so file
libc = ctypes.cdll.LoadLibrary("./MAC_VEC.so")

# define params and src/dst data
len_vec = 8
coef = 1.1
vec_x = [i * coef for i in range(len_vec)]
vec_a = [2 * coef for i in range(len_vec)]
vec_b = [(i - 8) * coef for i in range(len_vec)]
vec_y = [0 for i in range(len_vec)]

# define data type and convert method
len_vec_type = ctypes.c_uint32
data_type = ctypes.c_float
vec_type = data_type * len_vec

# convert list-type data
_len_vec = len_vec_type(len_vec)
_vec_x = vec_type(*vec_x)
_vec_a = vec_type(*vec_a)
_vec_b = vec_type(*vec_b)
_vec_y = vec_type(*vec_y)

# execution
libc.MAC_VEC_FP32(_len_vec, _vec_a, _vec_x, _vec_b, _vec_y)

# convert results to list-type
result = list(_vec_y)
print(result)

配列の型を宣言するあたりの作法で詰まりました.配列のデータ型×配列の要素数で表現し,アンパックしたlistを投げ込むことで変換できるようです.
実行すると,こんな感じの結果が得られました.

実行結果.INT16とUINT32の結果も表示している.

とりあえず正しい結果が得られているようです.

4. まとめ


今回はPythonからC++の生配列をいじってみました.ライブラリの追加も最小限で済み,割と簡単に動かすことができました.少し凝ったデータのやり取りをしようとすると大変かもしれないですが,C/C++標準のデータ型であれば十分に使えると思います.


©2021 shts All Right Reserved.

2021年10月13日水曜日

GeekServoで遊んでみた




数か月間ブログの更新をさぼっていた中の人です。今回はLEGOと互換性のあるDCモータ・サーボモータであるGeekServoで遊んでみた話です。



0.GeekServoとは


GeekServoはKittenBot社から発売されているRCサーボモータ・DCモータのシリーズで、日本ではSwitchScienceあたりで購入できます。

1.外見



今回試したのは、GeekServoの9G Motor-Redです。GeekServoのシリーズの中ではこのモデルだけがDCモータで、あとはRCサーボになっています。
出力軸は十字のシャフトになっており、ギアなどを直接接続することができます。また、側面などに丸穴やポッチがあるのでペグやシャフトを接続できます。

2.実際に組んでみる



手元にあった部品を使ってとりあえず組んでみました。


中身はこんな感じです。横軸と縦軸の両方出力軸に設定できるあたりはいいのですが、ギアの段数が多いために終端のガタが大きいのと、伝達できるトルクが少ないのが欠点でしょうか。
ここまで大げさな構造になったのは、軸の位置が縦と横の両方で半ポッチずつずれているためで、縦方向には半ポッチ幅のパーツを挟んで調節し、横方向は12枚歯のベベルギアの位置をハーフブッシュでずらしてかみ合う位置に移動させました。
きれいに0.5ポッチずれているので、半ポッチずらすテクニックがあれば普通に使えそうです。

3.まとめ


今回初めてGeekServoを触ってみたのですが、割と使い勝手がよくびっくりしました。半ポッチずらす必要があるなど多少癖はあるものの、それさえ解消できれば非常に良いものかなと思います。


©2021 shts All Right Reserved.

2021年5月26日水曜日

UnitV2が手に入った



暇な時間が欲しい中の人です.今回はM5Stackから発売されたUnitV2を手に入れたのでテストしていきます.


0.UnitV2とは


UnitV2はM5Stackから発売されたプロトタイピングツールです.
M5Stackのラインナップの中でも最も計算性能が高いものになっています.
UnitV"2"の名前が示すようにUnitVの後継モデルとなっていますが,CPUに互換性はないうえに,2の方はLinuxが動作しています.

1.外観と内容物



UnitV2のサイズはUnitVよりもStickVに近いです.ただUnitV2はバッテリーを含まないのでその分薄くなっています.


内容物はこんな感じです.16GBのSDカードがついてきます.USBはTypeA-TypeC(おそらく2.0)です.


治具(1).GoProのものに似たヒンジになっています.


治具(2).LEGOの部品が付くやつです(赤い部品は付属品ではないです).


手持ちの画像認識系デバイスを並べました(UnitVはレンズ交換済み).UnitVの小ささが際立っていますね.

2.遊ぶ前の儀式


UnitV2とPCをUSBでつなげるとファンが回転し始め,だんだん暖かくなってきます.この時にデバイスマネージャでUnitV2を探すと不明なデバイスとして認識されます.そのためドライバを充てる必要があります.ドライバはチュートリアルのページにあるリンクで配布されているので突っ込んでおきます.


チュートリアルページの通りにドライバを認識させたのち,ブラウザ(Chromeで動作確認済み)を開き,URLのところに UnitV2.py or 10.254.239.1 と入力します.すると最初から入っているでもプログラムを確認できます.

3.デモの様子




こんな感じで動きます.Yoloの実行の様子を見ている感じだと,UnitVやStickVで使われていたK210のアクセラレータ使用時ほどではないにしろ,そこそこの速さで動いている感じです.

4.SSH



UnitV2は初期状態でSSH接続による操作をすることができます.


適当なターミナルソフトで 10.254.239.1:22 にアクセスし,username : m5stack,初期パスワード : 12345678で入れるはずです(本体裏面のシールに書いてある).
組み込みLinuxなので,コマンドポチポチしていくと色々見ることができます.
ただし,スーパーユーザーになれなかったので電源をコマンドラインから切ることができませんでした(中の人がやり方を知らないだけかもしれません).

cpuinfoを読みに行った様子.確かにデュアルコア.

5.その他



Armv7のデュアルコア(1.2GHz)は初期のRPiを凌駕するほどのCPU性能であり,当然のごとく熱がすごいです.この対策として冷却ファンがついているのですが,振動と音が出ます.もしかしたら気になる人がいるかもしれないです.

6.まとめ


UnitV2を初めて動かしてみました.ドライバのインストールと電源をコマンドラインから切れないこと以外は特に気になる点はありませんでした.
今後はncnnやOpenCVを使っていろいろやっていこうと思っています.



cargo run


©2021 shts All Right Reserved.



2021年4月10日土曜日

PYNQ V2.6でDMA転送をする話(手打ちでDMAコントローラを制御する)



前回,前々回ではPYNQのライブラリでDMA転送を行うための回路の作成とプログラミングを行いました.今回は,pynq.lib.dmaの関数ではなく,AXI-Liteのレジスタを直接操作して同じようなことができないかを探る話です.  



1.概要


DMAコントローラはAXI-Liteを用いて制御しており,PYNQのDMAライブラリ内でもAXI-Liteのレジスタを直接操作してDMA転送を行っています.今回はDMAライブラリが何をしてるかを理解するため,この操作を温かみのある手打ちで実装します.
ビットストリームは前回使用したものを転用するため,DMAのモードはシンプルモードのままでいきます.


参考
Python productivity for Zynq (Pynq),"pynq.lib.dma — Python productivity for Zynq (Pynq)"

2.方針



PS-PL間のDMA転送は,PSからPLへのデータの流れ(Memory-Mapped to Stream:MM2S)とPLからPSへのデータの流れ(Stream to Memory-Mapped:S2MM)の二つが存在し,これらを制御する必要があります.
MM2S,S2MMの制御手順はほぼ同じではあるものの独立して制御するため,同じ機能のレジスタがMM2S用,S2MM用の2つ準備されています.

今回はデータ転送直後にデータ読み出しを行うので,S2MMもMM2Sも同じようなタイミングで操作します.

3.方法


実際にAXI4_Liteを用いて,DMAコントローラを操作していきます.アクセスするレジスタとアドレスの関係は以下の通りです(カッコ内はソースコード内での変数名).

MM2S
DMA制御レジスタ(MM2S_DMACR):0x00
DMA状態レジスタ(MM2S_DMASR):0x04
ソースアドレスレジスタ(MM2S_SA):0x18
転送長さレジスタ(MM2S_LENGTH):0x28

S2MM
DMA制御レジスタ(MM2S_DMACR):0x30
DMA状態レジスタ(MM2S_DMASR):0x34
ソースアドレスレジスタ(MM2S_SA):0x48
転送長さレジスタ(MM2S_LENGTH):0x58

スキャッターギャザーモード(SGモード)でのアドレスはAXI DMA IPの製品ガイドを見てください.

参考
Lauri's blog,"AXI Direct Memory Access"

3-1.リセット


1. S2MMのDMA制御レジスタ(0x30)に0x04を書き込む(リセットビットを1にする)  
2. MM2SのDMA制御レジスタ(0x00)に0x04を書き込む(リセットビットを1にする)

(S2MMとMM2Sの状態を確認)  

3. S2MMのDMA制御レジスタに0x00を書き込む(全停止)  
4. MM2SのDMA制御レジスタに0x00を書き込む(全停止)  

(S2MMとMM2Sの状態を確認)  

最初にS2MM/MM2Sのリセットと停止を行います.通信に失敗してDMAのIPが動作したままだと転送が行えなくなります.
S2MMとMM2Sの状態はxxxx_DMASRを読み出して確認します.

3-2.転送


5. S2MMで書き込むメモリ領域の先頭アドレスをデスティネーションアドレスレジスタ(0x48)に書き込む  
6. MM2Sで読み込むメモリ領域の先頭アドレスをソースアドレスレジスタ(0x18)に書き込む  

(S2MMとMM2Sの状態を確認)  

7. S2MMのDMA制御レジスタ(0x30)に0xF001を書き込む(DMAチャンネルの実行&IOC_IrqENとErr_IrqEnの設定)  
8. MM2SのDMA制御レジスタ(0x00)に0xF001を書き込む(DMAチャンネルの実行&IOC_IrqENとErr_IrqEnの設定)  

(S2MMとMM2Sの状態を確認)  

9. S2MMの転送長を転送長さレジスタ(0x58)に書き込む(バイト単位)  
10. MM2Sの転送長を転送長さレジスタ(0x28)に書き込む(バイト単位)

11. MM2Sの同期を待つ
12. S2MMの同期を待つ(ここでフリーズするなら、全メモリの範囲がアドレスエディタで割り当てられているかを確認すること)  
(S2MMとMM2Sの状態を確認)  

転送長を書き込んだ段階で実際にDMAの転送が行われます.
S2MMやMM2Sの同期は、DMAのステータスがErr_IrqかIdleではない間、whileで待つようにしました。

4.まとめ


手打ちでDMAコントローラを制御してみました.マイコンのレジスタの設定とほぼ同じような感じで操作できるようになっており,個人的には比較的わかりやすかったです.
PYNQ V2.6からはSGモード用の関数も準備されており,直に叩くメリットはほぼないのですが,関数内で何をしているかが分かっていただければ幸いです.


©2021 shts All Right Reserved.

2021年4月9日金曜日

PYNQ V2.6でDMA転送をする話(ビットストリーム生成)



絶賛就活中,中の人です.気晴らしがてら進めていた研究の方でPYNQのDMAを使うことがあったのですが,その際に得た知見をまとめようと思います.
ビットストリームはこちらに上げてあります.  



1.環境



ボード:PYNQ-Z2(TUL製,xc7z020clg400-1搭載)
Vivado : Vivado 2020.1
PYNQ : V2.6
OS Ubuntu 20.04LTS

特に変哲のない感じですが,PYNQ V2.6はVivado 2020.1でビルドされているので,それに合わせてバージョンを決めた感じです.

2.どんな回路を作るか



CPUにぶら下がっているDDR3メモリとPL部をつなぎ,メモリ->FIFO(PL部)->メモリというような簡単な回路で動作確認を行います.

3.PS部とPL部の橋渡し.


PYNQ-Z2で使用されているZYNQ 7000シリーズでは,FPGA(PL)とCPU(PS)に接続されたDDR3メモリをAXIバスを介して接続することができます.この機能はZYNQ内に存在するポートを使うことで使用できます.
このポートいくつか存在しており,32b GP AXI Master Ports(M_AXI_GPx),32b GP AXI Slave Ports(S_AXI_GPx),AXI High Performance 32b/64b Slave Ports(S_AXI_HPx),64b AXI ACP Slave Portのように通信の種類に応じて分けられています.


4.AXIバスの通信規格

AXIバスの通信規格にはいくつかの種類があります(AXI4だとAXI4(-Full),AXI-Lite,AXI-Stream).
今回使用するDMAのIPの制約で,FIFOとDMAコントローラ間はAXI-Stream,PS(S_AXI_HP)とDMAコントローラ間はAXI-Fullを使用します.またいくつかのIPでAXI-Liteによる制御が必要なので,そちらも使用します.

参考

5.ブロックダイアグラムを作る

ここからブロックダイアグラムを作っていきます.

5-1.PSの設定



PSとして,Zynq7 Processing System(5.5)を使います.
DDRメモリやFIXIOへの接続ポートを先に生成しておきます。


ここではメモリとPLを接続するため,ポート(M AXI GP0とS AXI HP0)の設定を行います.Vivado 2020.1ではM AXI GP0の方は最初から有効化されているようで,有効化されていないS_AXI_HP0は手動で有効化します.この際に,S_AXI_HP0のデータ幅は64bitにしておきます.

5-2.DMAコントローラの設定



今回使うIPはXilinx提供のAXI Direct Memory Access(7.1)です.
PYNQのv2.6からScatter Gather Engineをサポートするようになったのですが,今回はシンプルモードで試すのでチェックを外しておきましょう.
また,バッファ長を設定するレジスタ(Width of Buffer Length Register)は最大値の26まで引き上げておきましょう.
ここではAddress Widthを32bitとしておきます.
DMAのIPを二つ配置し,それぞれRead-Only, Write-Onlyにすることも可能ですが,今回はR/Wの双方を使えるようにしておきます.
今回の回路ではint16のデータを流すので,Read Channel側のMemory Map Data Widthを32bit,Stream Data Widthを16bitに設定しておきます.

5-3.FIFOの設定



FIFOとして,Xilinx提供のAXI4-Stream Data FIFO(2.0)を使用します.特段設定をかえる必要はないと思います.

5-4.IPの配置と配線



PSのブロックをDiagramに追加し,DDRとFIXED_IOの自動配線を終えたのち,DMAコントローラとFIFOを追加します.
そして,DMAコントローラとFIFOのAXI Streamのバスをつなぎます.
DMAコントローラとPSのS_AXI_HP0との接続にはAXI SmartConnectをかませました(Vivado2020.1の自動配線だと,別のIPが出てきました).
その後、自動接続を使い、他の配線を行いました。

6. 合成とファイルの生成


先程のブロック図をValidate Designにかけると、アドレスが振られていない警告が出てくるので、アドレスエディタでアサインしておきましょう。もう一度チェックを走らせると、CriticalWarningは消えているはずです。

Generate Output Products,Generate HDL Wrapperをし,ビットストリームを生成しました.
PYNQ上で実行する際には,.bit及び.hwhが必要になります(V2.6では.tclは不要みたいです).
以下の二つのフォルダから.bit及び.hwhを探します.

プロジェクト名.runs/impl_1/デザイン名_wrapper.bit
プロジェクト名.src/source_1/bd/デザイン名/hw_handoff/デザイン名.hwh

これらのファイルをPYNQ上のフォルダに投げ込めば,Vivado上での操作は完了です.
(プロジェクト名.src/source_1/bd/デザイン名/hw_handoff/デザイン名.tcl は無くても動くみたいです。)

7.まとめ


ビットストリームの生成まで完成しました.次はPYNQ上での操作に移っていきます



©2021 shts All Right Reserved.

PYNQ V2.6でDMA転送をする話(JupyterNotebook上での実行)



前回に引き続きDMA転送の話です.今回は前回作成したビットストリームを実行していきます.今回のnotebookはこちらに上げています



1.PYNQで動かす


PYNQ上のJupyter Notebookにアクセスする話は割愛します.
とりあえず適当なからフォルダを作り,その中に先ほどの.bitと.hwhを入れ,.ipynbをそのフォルダ内で新規で作成しておきます..bitと.hwhはファイル名をそろえておきます(今後はdesign_1.bit, design_1.hwhとする).

1-1.オーバーレイのロード


from pynq import Overlay
from pynq import PL

OL = Overlay("design_1.bit")
print(OL.ip_dict.keys())
dma = OL.axi_dma_0

ビットストリームの読み込みはこれで完了です..bitのファイル名を使用して.hwhを読み込むようなので,名前はそろえておきましょう. OL.ip_dictには使用したIPのうち,PS側から操作できるものが辞書として登録されています. この辞書内からdmaのIPを探し出し,dmaとしておきましょう.

1-2.xlnkとAllocate


import numpy as np
from pynq import allocate
data_src = allocate((100,), dtype=np.int16)
for i in range(100):
    data_src[i] = i + 1

data_dst = allocate((512,), dtype=np.int16)

print("size of data_src :", data_src.nbytes, "Byte")
print("size of data_dst :", data_dst.nbytes, "Byte")

V2.6のPYNQでは,旧来より使われてきたXlnkに代わり,allocateを使うようになっています.今回はAXI-Streamのデータ幅を16bitにしたので,dtypeも16bitのモノに設定しました.入出力のバッファ領域をこれで確保しました.

1-3.実行


print(data_dst)

dma.sendchannel.transfer(data_src)
dma.sendchannel.wait()
print("send done")

dma.recvchannel.transfer(data_dst)
dma.recvchannel.wait()
print("receive done")

print(data_dst)

dmaのメンバ関数にsendchannel,recvchannelがあり,それぞれにtransferとwaitがあります.この辺は以前のPYNQと変わらずに使えます.

2.まとめ


とりあえずこのような形でPYNQ+DMAの動作を確認出来ました.
そのうちVivado HLSで作成したIPを埋め込み,動作させたいと思います.

参考


©2021 shts All Right Reserved.

2021年4月3日土曜日

スイッチサイエンスのジャンク品買ってみた


絶賛就活終わらない中の人です.先日Twitterを眺めていたところ,このような記事が流れてきました.



たまたまその時間は待機できそうだったので争奪戦に参加しました.結果1セット手に入れることができたので,少し見ていこうかと思います.


1.内容物



中身はこんな感じでした.内容物としては,

M5StickC x 1
M5Gray x 1
M5Faces x 1

となっていました(https://www.switch-science.com/catalog/7081/の例2のパターン).

2.少し動かしてみた


M5Gray.UIFlowのファームが元から書き込まれていた.

最初にUSBケーブルをつなぎ,電源だけを供給させて動作するか試してみました.
StickCとGrayについては,元からUIFlowのファームが書き込まれており,電源を入れただけで動作しました.

FacesにArduinoのサンプルコードを入れたところ.
加速度センサはMPU6886らしい.

Facesに関しては電源を入れても何も表示されませんでした.PCに接続したところCOMポートが割り当てられたので,試しにArduinoでサンプルコードを入れたところ無事動作することがわかりました.3種類のキーボードも動作することを確かめました.


唯一,Facesのベース内部のフレキが根元から断線していたため,グレードルが使えませんでした.今のところは特に使う場面は無いので,当面は放置になりそうです.

3.まとめ


ジャンクということもあり全く動かないことを覚悟していたのですが,あっさり動いてしまいました.返品されたもののうち軽い修正で使えそうなものを選んだとなっていたのですが,出荷前にある程度の修理や動作確認も行ったように見えました.
保証は当然なく,壊れても自己責任なので初心者には絶対にお勧めできませんが,個人的には満足な買い物でした.


©2021 shts All Right Reserved.

2021年2月28日日曜日

RPi Pico向けRustサンプルコードを動かす(Win10 + PowerShell + WSL)




就活解禁数時間前にブログを書き出す中の人です.少し前にRPi PicoをWSL + C/C++ SDKで実装する話を書いたのですが,どうやらRustで動くサンプルコードがあるようだったので,実際に動かしてみました.

今回はEmbedded Rustの話は置いといて,とにかくサンプルコードをRPi Pico上で動かすことにフォーカスします.そのためコードやクレートの解説はほかの資料に譲ることにします.





1.RPi PicoとEmbedded Rust


そもそもRustで組込みプログラミングができるのかという問題があるのですが,Arm Cortex-M系のマイコンであれば動かすことができるようです.RPi PicoのマイコンであるRP2040はCortex-M0+のデュアルコアなのでRustが動かせるというわけです.

参考:

2.何をやるか


今回はデバッガ等も使わずに,ただひたすらにバイナリを吐き出しLチカを
することにフォーカスします.

3.環境


今回は,コードのビルドとELF->UF2の変換で使う環境を変えました.

ビルド:Rust(1.52.0-nightly) + PowerShell
ELF->UF2の変換:WSL2(Ubuntu 20.04)

最初はWSL2一本でやろうと思ったのですが,cargo buildを実行した際に

error: RPC failed; curl 56 GnuTLS recv error (-24): Decryption has failed.

のエラーが出てしまい,ビルドが通らなくなる現象が解決しなかったので今回はPowerShellを使いました(GitHubとの通信で出るようで,git fetchやgit cloneが使えなくなる).
ビルド後の処理でC/C++SDKに含まれるツールが必要になるので,WSL2の方の環境構築もお勧めします.詳細は,こちらを参考にしてください.

ビルド環境の方は,公式の手順通りにRustを突っ込んだのちにarm-none-eabi-gdbを突っ込みます.この辺りはEmbedded Rustでの手順と同じですが,今回はデバッガを使わないので,OpenOCDなどは入れません.
arm-none-eabi-gdbをインストールする際に,必ず環境変数にパスを追加するようにしましょう.インストールの最後に出てくる画面にAdd path to environment variableのオプションが出てくるのでチェックを入れましょう(初期状態だとチェックがないはず).

4.サンプルコードのクローン



を適当な場所にクローンしましょう.ビルド作業はこのsample-project内で行います.

5.ビルド


早速ビルドに移るのですが,そのままではCortex-M0+のELFファイルを吐き出してくれません.そこでおまじない

rustup target add thumbv6m-none-eabi

を実行しておきます.これを実行したうえで

cargo run

すれば./target/thumbv6m-none-eabi/debug内にrp-testというELFファイルが吐き出されているはずです(実態はELFファイルだが,拡張子がない状態で吐き出される).

6.ELF2UF2


.elfも生成出来たのでRPi Picoへ実装したいのですが,Picoへ実装する際に.uf2に変換する必要があります.これを行うツールがRPi PicoのC/C++ SDKに入っています.このツールは事前にビルドする必要があります.今回はこのビルドをWSL2上で行うことにします.

cd (pico-sdkのあるフォルダ)
cd pico-sdk/tools/elf2uf2
mkdir build
cd build
cmake ..
make
sudo cp elf2uf2  /usr/local/bin/

ビルド終了後,WSL2でelf2uf2と打ち込み,

Usage: elf2uf2 (-v) <input ELF file> <output UF2 file>

と出てくればelf2uf2のビルド成功です.ここで先ほどのrp-testをrp-test.elfとリネームしておきましょう.elf2uf2の<input ELF file>は拡張子が.elfのファイルだけを受け入れるので注意しましょう.
最後に

elf2uf2 rp-test.elf rp-test.uf2

を実行すれば,rp-test.uf2が出来ているはずです.

7.実機で動かす




.uf2を実機に入れるところの操作はC/C++ SDKと全く同じなので割愛.
こんな感じでLチカ完了です.

8.感想


Embedded Rustとほぼ同じなので,開発環境についてはある程度ほかのマイコンでの知見が生かせそうです.またデバッガを使う場合は,OpenOCDやらなんやらのインストールが必要みたいです.

9.参考

"The Embedded Rust Book 日本語版",https://tomoyuki-nakabayashi.github.io/book/intro/index.html
"PicoボードにPico_SDKでC言語をビルドする", https://beta-notes.way-nifty.com/blog/2021/02/post-2fff25.html



©2021 shts All Right Reserved.

2021年2月26日金曜日

Raspberry Pi PicoのC/C++ 環境をWSL2上に構築する



絶賛就活中の中の人です.この度Raspberry Pi Pico(RPi Pico)が運良く入手できたので,開発環境を整えてみたいと思います.

この記事は@kmakさんのRaspberry Pi Pico を WSL からあつかうをWSL2(Ubuntu 20.04)向けにアレンジしたものになります.



1.RPi Picoの開発環境


RPi PicoはRaspberry Pi財団が開発したRP2040を使用した開発ボード的なものです.
RP2040はCortex-M0+を2つ搭載したいわゆるデュアルコアマイコンで,いろいろな特徴があります(詳しくはデータシートで).
公式サイト上ではMicroPython SDK,C/C++ SDKが公開されており,このほかにもRustなどを用いることができるようです.
今回はベーシックにC/C++を使っていきます.

参考:
Raspberry Pi財団, "Getting started with Raspberry Pi Pico", https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf
@kmak, "Raspberry Pi Pico を WSL からあつかう",

2.WSL2の下ごしらえ


今回はWSL2上にC/C++ SDKを突っ込むわけですが,SDKを入れる前に必要なものを入れます.必要なものは以下の4つ.

  • gcc-arm-none-eabi
  • cmake (>=3.13)
  • git
  • build-essential

ubuntu 20.04であれば,これらをaptでインストールすればいいようです.ubuntu18.04ではcmakeをaptで入れると古いバージョンがインストールされるので,自前でビルドしましょう.

3.SDKのインストール


ドキュメント通りにインストールする.


# SDKとサンプルを入れるpicoフォルダを作成する
$ cd ~/
$ mkdir pico
$ cd pico

# SDKをクローン
$ git clone -b master https://github.com/raspberrypi/pico-sdk.git
$ cd pico-sdk
$ git submodule update --init
$ cd ..

上記でSDK自体は入ったのですが,環境変数PICO_SDK_PATHにSDKの場所を追加する必要があります.

$ export PICO_SDK_PATH=/(pico-sdkを入れたフォルダの絶対パス)/pico-sdk

これでSDKの準備は完了です.

4.サンプルコードを入れてコンパイル


SDKの動作確認もかねてサンプルコードをクローンしてコンパイルします.


$ git clone -b master https://github.com/raspberrypi/pico-examples.git
$ cd pico-examples
$ mkdir build
$ cd build
$ cmake -G "Unix Makefiles" ..
$ make

これにはそこそこ時間(i5-7200Uで2hぐらい)がかかるので気長に待ちましょう.
(全部のサンプルコードをビルドしているためで,どれか一つだけであればここまで時間はかからない)
ビルドの成果物は./pico-examples/build内に生成されており,Lチカのコードであれば./pico-examples/build/blink/に入っています.この時,ボードに入れるファイルは.uf2になります.

5.ボードに入れる


RPi PicoはBOOTSELを押したままの状態でPCとつなげるとストレージとして認識されます.ここに先ほど作成したuf2ファイルを入れると自動的にストレージとしての認識が解除され,コードの実行を開始します.
bashか何かで自動的にボードに投げ込んでもいいのですが,ここでは脳死でGUIのエクスプローラを使って入れました(もう少し考えたいところ).


 
©2021 shts All Right Reserved.


2021年1月14日木曜日

PySimpleGUIとMatplotlibで高速グラフ描画





就活がいよいよ始まってきた中の人です.今回はPySimpleGUIに以前作成したMatplotlibの高速描画クラスを埋め込む話です. 

今回の話のコードはこちらから




1.PySimpleGUIについて


MatplotlibとGUIを組み合わせる場合,Tkinterとの組み合わせがよくあるパターンだと思います(Matplotlibの描画の問題で,多分これ一択).ただTkinterでGUIを組む場合,部品のレイアウトで苦労することがあるかと思います.そこで登場するのがPySimpleGUIです. 

売り文句は"Python GUI For Humans - Transforms tkinter, Qt, Remi, WxPython into portable people-friendly Pythonic interfaces." 
どうやらPythonで使えるGUIライブラリをラップして,"人間"でも使えるようにしたライブラリと考えればよさそうです(はてさて読めるのだろうか).

参考:

2.Matplotlibを埋め込む


Tkinterもラップしてくれているので,Matplotlibも埋め込めそうって考えてたら公式cookbookに参考例がありました.どうやら動画として動かせ...ませんでした.どうやらサンプルコードが古いバージョンのようで,新しいものでは動作しないようです.

で,情報を探し回っていたら@bear_montblancさんの記事を見つけ,試してみたら動作しました.とりあえず普通の使い方であれば,Matplotlibのグラフを埋め込んで動作させることが可能なようです.

参考:
Be4rR,"PySimpleGUIにMatplotlibを埋め込みたい",https://qiita.com/bear_montblanc/items/cce4e8c58dfa236200f6

3.自作クラスを埋め込む


普通の使い方であればMatplotlibのグラフを埋め込めたので,今度は先日作成した自作クラスを埋め込みます.

といっても,2.で実装したコードを一部書き換える程度であっさり動作しました.



if __name__ == "__main__":

    try:
        make_dpi_aware()

        # Generate Layout
        layout = [[sg.Text('Fast_Render_Matplotlib Plot')],
                  [sg.Canvas(key='-CANVAS-')],
                  [sg.Button("Add"), sg.Button("Clear")]]

        # Generate Window (finalize=True is Required)
        window = sg.Window('Embedding Fast_Render_Matplotlib In PySimpleGUI',
                           layout,
                           finalize=True,
                           element_justification='center',
                           font='Monospace 18')

        # Generate Fig to Embedding Graph
        fig = plt.figure(figsize=(5, 4))
        line_ax = fig.add_subplot(2, 1, 1)
        pos_ax = fig.add_subplot(2, 1, 2)
        points_num = 500
        scatter_view = plotter.Scatter(fig, pos_ax, len_points=points_num, show_icon=True, PySimpleGUI=True)
        line_view = plotter.Line(fig, line_ax, plot_area=(points_num, 1000), len_points=points_num, PySimpleGUI=True)

        # Associate Fig and Canvas.
        fig_agg = draw_figure(window['-CANVAS-'].TKCanvas, fig)

        while True:
            event, values = window.read()

            if event in (None, "Cancel"):
                break
            elif event == "Add":
                # Generate Random Data
                rand_array_x = np.random.randint(-1000, 1000, 1250).tolist()
                rand_array_y = np.random.randint(-1000, 1000, 1250).tolist()
                y = np.random.randint(-1000, 1000, points_num)

                # Plot Data
                line_view.plot(y)
                scatter_view.plot([rand_array_x, rand_array_y])

            elif event == "Clear":
                line_view.cla()
                scatter_view.cla()
                fig_agg.draw()

    except Exception as e:
        print(e, end="\n\n")
        import traceback
        traceback.print_exc()
        input("Press any key to continue...")
    else:
        print("Done")
    finally:
        window.close()



基本的には,ぼやけ防止の関数を走らせ,GUIのレイアウトを決定,ウィンドウを生成したのち,グラフの初期設定を行います.その後,グラフとレイアウトで設定したキャンバスを関連付けてあげることで準備は完了です.プロット類はボタンの入力をトリガとして走らせることで実行します.

参考:

3.実行速度





とりあえず50[fps]前半から60[fps]前半あたりが出ているようです.そこそこのフレームレートですね.

4.参考


コメント欄で教えてもらった,公式のデモコード集


©2021 shts All Right Reserved.

2020年12月31日木曜日

M5StickVでDonkeyCarのモデルは走らせられるのか(1)



年末進行感が全くないまま大みそかになっていた中の人です.今年はいろいろなことが変わった一年でしたが,いかがお過ごしでしょうか.
今回はM5StickVでDonkeyCarのモデルを走らせる話(1)です.個人的に気になっていたので,少しやってみようと思いました. 

今回の実装は,https://github.com/shtsno24/DonkeyCar_on_M5StickVに上げてあります.




1.DonkeyCarのモデル


https://www.donkeycar.com/より,DonkeyCarの一例

DonkeyCarはラジコンにRPiやカメラを組み合わせ,自動運転させようというプロジェクトです(車本体をさすこともあるとか).自動運転の心臓部はKerasによるDeepLearningのモデルで実装されており,使うデータの種類などでいくつかのパターンがあるようです.
2016年ごろにやっていたやつの結果を踏まえると,個人的にはcategoricalあたりがうまく動きそうです.今回はこれをM5StickV向けに変換します.

参考:
Donkey Car, "Keras - Donkey Car", http://docs.donkeycar.com/parts/keras/
腹筋開発,"Donkeycar3.1.0上の機械学習モデルを調べてみた", https://fight-tsk.blogspot.com/2019/09/donkeycar310.html

2.モデルの作成


本家の実装は汎用性が高い実装になっていますが,今回はcategoricalのみが動けばよいので,少し手を入れてミニマムな構成に直しました.


def Categorical(input_shape=(120, 160, 3), drop=0.2, l4_stride=1):
    """
    :param img_in:          input layer of network
    :param drop:            dropout rate
    :param l4_stride:       4-th layer stride, default 1, in Categorical, l4_stride=2
    """
    inputs = Input(shape=input_shape, name='img_in')
    x = conv2d_relu(inputs, 24, 5, 2, 1)
    x = Dropout(drop)(x)
    x = conv2d_relu(x, 32, 5, 2, 2)
    x = Dropout(drop)(x)
    x = conv2d_relu(x, 64, 5, 2, 3)
    x = Dropout(drop)(x)
    x = conv2d_relu(x, 64, 3, l4_stride, 4)
    x = Dropout(drop)(x)
    x = conv2d_relu(x, 64, 3, 1, 5)
    x = Dropout(drop)(x)

    x = Flatten(name='flattened')(x)

    x = Dense(100, name='dense_1')(x)
    x = ReLU()(x)
    x = Dropout(drop)(x)

    x = Dense(50, name='dense_2')(x)
    x = ReLU()(x)
    x = Dropout(drop)(x)

    outputs = []
    _x = Dense(15, name='throttle')(x)
    _x = Softmax()(_x)
    outputs.append(_x)
    _x = Dense(20, name='steer')(x)
    _x = Softmax()(_x)
    outputs.append(_x)

    model = Model(inputs=inputs, outputs=outputs)
    return model


def conv2d_relu(x, filters, kernel, strides, layer_num):
    """
    Helper function to create a standard valid-padded convolutional layer
    with square kernel and strides and unified naming convention
    :param filters:     channel dimension of the layer
    :param kernel:      creates (kernel, kernel) kernel matrix dimension
    :param strides:     creates (strides, strides) stride
    :param layer_num:   used in labelling the layer
    """
    x = Conv2D(filters=filters,
               kernel_size=(kernel, kernel),
               strides=(strides, strides),
               name='conv2d_' + str(layer_num))(x)
    x = ReLU()(x)

    return x



とりあえずこんな感じです(Conv2DのActivationとか使えばいいのですが,気にしない). Dropoutに関してはtfliteに変換する際に消えるので問題ないです.これをnncaseで変換し,PC上で実行時間のシミュレーションをしてみました(モデルの訓練はしていません).

参考:autorope, donkeycar/keras.py at dev · autorope/donkeycar, https://github.com/autorope/donkeycar/blob/dev/donkeycar/parts/keras.py

3.PC上でシミュレーション


入力画像

120x160x3のカラー画像(ランダムなピクセル値を使用)を使用し,実行時間を計測しました.


QuantizedConv2D: 16.6666ms
QuantizedConv2D: 35.7317ms
Dequantize: 0.0421ms
Conv2D: 21.1655ms
Quantize: 0.3078ms
KPUUpload: 0.0231ms
KPUConv2D: 15.5949ms
Pad: 0.0365ms
KPUUpload: 0.0038ms
KPUConv2D: 14.1453ms
Pad: 0.026ms
Transpose: 0.06ms
Dequantize: 0.0026ms
MatMul: 1.9926ms
Pad: 0.0108ms
Quantize: 0.0232ms
KPUUpload: 0.0039ms
KPUConv2D: 0.1828ms
Pad: 0.0011ms
Dequantize: 0.0006ms
MatMul: 0.0018ms
MatMul: 0.0012ms
Total: 106.024ms


PC上では100msで,全体としてはConv2Dの演算に時間がかかっていそうです.最初のQuantizedConv2DはKPU上で演算されていない様子なので,もう少し上手くやる方法がありそうです.意外とMatMulが高速みたいですが,実機だとどうなるかが気になります.

4.実機実装


実機で動かす様子

以前に実装していたコードを使ってパパっと実装.
(2021.1.1更新:時間のところが実際はFPSだったので時間表示に修正しました.)


KPU : fetch data from Camera
KPU : run kpu
KPU : fetch data from kpu
Time : 2.758 [s]


PCの結果の27倍実行に時間がかかっていることがわかりました.PCの結果やNNCaseのQAから察するに,おそらくフィルタサイズが5x5のConv2Dが部分的にしかアクセラレートされていないことが主な原因だと考えられます(1x1 or 3x3が完全な形でアクセラレートされる).

5.もう少し実行速度を上げる


AIが動くことが売りのM5StickVでDonkeyCarのモデルを実行してみました.結果として約400[ms]程度の実行時間がかかることがわかりました.実行できただけである程度は成功なのですが,実用上もう少し実行速度を上げる必要がありそうです.手法としては,

・5x5フィルタを複数の3x3フィルタに置き換える
・全結合の出力サイズを落とす
・SeparableConv2Dに置き換える

あたりが効果がありそうです.

6.最後に


今年も様々な方にこのブログを見ていただきました.来年は就活&修論に追われる予定なので,おそらく更新頻度は下がるかと思いますが何卒よろしくお願いします(UnitV2とかが出るので,それに関連した記事を書く予定です).

P.S. このブログを閲覧している方は,機会があったら中の人にこのブログを見たことを伝えていただけると幸いです.中の人が喜びます.

©2020 shts All Right Reserved.

2020年11月16日月曜日

M5StickVのIMUで詰まった話


M5StickVのIMUのドリフト補正したときのグラフ.
画像を作ってSDに書き出せるのは非常に便利.


研究をやり始めると,就活がおろそかになるシングルスレッド人間,中の人です.今回はM5StickVのIMUの話です.



1.M5StickVのIMUと回路について


M5StickVは生産された時期に応じて搭載しているIMUと,メインのマイコンであるK210との接続ピン・方法が異なっています.今回は2020/03以降に生産されたモデルで,MPU6886を搭載しているものを使用していきます.

参考:
SwitchScience, M5StickV--販売終了 - スイッチサイエンス, https://www.switch-science.com/catalog/5700/

2.MPU6886と会話する(I2C)


データの読出しに成功した.



MPU6886との通信は,SPIとI2Cの2つがあります.


どちらを使うかを決めるためには,G25をHigh or Lowにする必要があります(nnnさん教えてくださりありがとうございます).

手始めに,I2Cを使ってMPU6886との会話を試みます...


ほかの人だと読めたのに,なぜか読めない...
I2Cのコード自体は動いていたのでIMU側の問題と考え,データシートを眺めていましたが特に答えが得られなかったので,IMUにつながっているG24を操作しました.結果IMU側のアドレスをスキャンできました.
G24はIMUのアドレスを決定するピン(AD0)に接続されているのですが,G24を操作してHigh or Lowにしないと,回路的に浮いた状態になるようです(プルダウン/アップされていないみたい,SPIで読むときに,idle状態で不定にしないと値を吐き出すから?).
とりあえずnnnさんのサンプルコードを参考に,レジスタを設定したところ普通に動作しました.

参考:
m5stack, M5-Schematic/MPU-6886-000193+v1.1_GHIC.PDF.pdf at master · m5stack/M5-Schematic

anoken, purin_wo_motto_mimamoru_gijutsu/006_2_imu_new_MPU6886.py at master · , https://github.com/anoken/purin_wo_motto_mimamoru_gijutsu/blob/master/03_maixpy_example/006_2_imu_new_MPU6886.py

3.SPIで会話する


SPIは開発中...
なぜか読めないんだよね.





©2020 shts All Right Reserved.

2020年10月20日火曜日

Interface10月号のNNCaseネタをやってみる(途中経過)



インターン地獄に巻き込まれた中の人です(これ年明けも続くのつらいな).今回は雑誌記事の検証的なネタです.
Interface10月号のP.55-58に"AIマイコンK210の汎用AIチップとしての可能性を探る"という記事がありまして,BodyPixを変換し実装までやるという内容でした.記事内の結果では,ファームウェアがkmodelv4に対応していないため動かなかったという結論でした.
記事を読んだ感じでは動かせる気がしたので,とりあえずやってみます(ソースコードの公開は,もう少し先になりそうです).



1.モデルの変換


記事内では,PINTOさんのPINTO_model_zooにある.tfliteを使用していたので,同じようにモデルを準備します(量子化などは行わずfloatのままtfliteにする).入力サイズは320x240,出力はいじりません.
tfliteが生成出来たら,NNCaseを使い,kmodelへの変換を行います.NNCaseでの変換では,量子化のためのデータセット(データの分布を見たいから?)を指定する必要があるのですが,BodyPixのデータセットが見当たらなかったので,各ピクセルの値がランダムな320x240の画像を10枚生成し,データセットの代わりにしました(Imgフォルダに突っ込みました).もしかしたらCocoDatasetを使っているかもしれないですね.

.\ncc.exe compile .\035_BodyPix_tflite\bodypix_025_320x240_weight_quant.tflite .\035_BodyPix_tflite\bodypix_025_320x240_weight_quant.kmodel -i tflite -o kmodel --dataset .\Img\ --dataset-format image --inference-type uint8 --input-mean 0 --input-std 1 --dump-ir --input-type uint8 --max-allocator-solve-secs 120 --calibrate-method l2 --dump-weights-range --weights-quantize-threshold 1024 --output-quantize-threshold 4096

参考
中村仁昭,"AIマイコンK210の汎用AIチップとしての可能性を探る",Interface10月号,CQ出版,PP.55-58
Kaz Sato,Google Developers Japan: BodyPix の概要: ブラウザと TensorFlow.js によるリアルタイム人セグメンテーション,https://developers-jp.googleblog.com/2019/04/bodypix-tensorflowjs.html

2.ファームウェアの準備

kmodelv4を使う場合,ファームウェアのバージョン違いではじかれることがあります(SipeedやM5Stackが配布しているファームが古い時がある).今はアップデートされて行けるかも).そこで,手元のLinux環境でM5StickV向けにファームウェアをビルドしました.(MaixHubのサービスで,カスタムファームウェアのビルドをやっているみたいだけど,いろいろあって手元の環境でやった).
ファームウェアのビルドは紅樹 タカオさんの記事を参考にUbuntu20.04マシンで行いました.オプションで,support v4 kmodelというオプションがあるので,そこを有効化しておく必要があります(配布されているファームウェアだと,これが有効になってないかも).

参考
紅樹 タカオ,"M5StickVのファームウェアビルド手順",https://raspberrypi.mongonta.com/howto-build-firmware-of-m5stickv/

3.MaixPyIDE


BodyPixは4つの出力があるモデルなので,KPUを使う際にkpu.set_outputs()を4つ書いておきます.kpu.forward()を実行し推論をしたのち,kpu.get_output()で目的の出力を得ます.取り出したデータをいい感じに整形すれば完成です.

4.出力データを正しく表示したい


出力データが出たところで,4つの出力を確認していきます.まずは形状とデータサイズから.

{"fmap": "data"=0x80330b70, "size"=20400, "index": 41, "w": 17, "h": 20, "ch": 15, "typecode": f}
{"fmap": "data"=0x803382f8, "size"=40800, "index": 41, "w": 34, "h": 20, "ch": 15, "typecode": f}
{"fmap": "data"=0x80342258, "size"=28800, "index": 41, "w": 24, "h": 20, "ch": 15, "typecode": f}
{"fmap": "data"=0x8032f8b0, "size"=1200, "index": 41, "w": 1, "h": 20, "ch": 15, "typecode": f}

"size"のところと"w","h","ch"の計算がを合わせることで,データ型がわかります.

20400 / (17 x 20 x 15) = 4
40800 / (34 x 20 x 15) = 4
28800 / (24 x 20 x 15) = 4
1200 / (1 x 20 x 15) = 4

全部一致したので,4Byte,かつ出力データからFP32と分かります.形状はw*h*chのように見えますが,実際は,ch * w * hの順番らしいです(NNCaseの方の出力がこっち:MaixPyでのKPUのプロパティが違う).

NNCaseの出力.ch,w,hの順番になっている.

ここまではほぼ確定なのですが,ここからがよくわからないところで,この4つの出力モデルの解釈で,ch=24は体の部位ごとのセグメンテーション,ch=1は体とそれ以外のセグメンテーションと読めるのですが,ほかの2つがよくわかりませんでした.とりあえずch=1の奴を使ってみます.





赤い部分が濃いほど,人がそこにいる確率が高いことを表しているのですが,あまりうまくいっていなさそうです.

5.今後やりたいこと


データの解釈があっているかの確認や精度向上,高速化あたりをやっていきたいです.もしかしたらch,w,hの解釈もあっていないかもしれないですね...



©2020 shts All Right Reserved.


2020年9月11日金曜日

Matplotlibで散布図や折れ線グラフの30fps描画を実現したい




相変わらず引きこもり生活な中の人です。今回はMatplotlibで散布図や折れ線グラフを高速描画する話です。 



1.Matplotlibの描画の話


Pythonでグラフ描画を行う際によく使うのがMatplotlibです.Matlab-likeなインターフェースで扱いやすいのですが,リアルタイム描画を行おうとすると非常に遅いことがあります.
これに関してはいろいろな高速化のやり方があるのですが,この中でも手動更新のコードを自分で書くことで高速化を果たすやり方だと,データ数によっては100fps以上を狙うことができるらしいです.

参考:hukkumameo,【Python】matplotlibの手動で描画更新,俺言語。

2.手動更新について


描画の更新方法はいくつかあるのですが,今回は
  1. 描画領域を白く塗る
  2. データを描画する
  3. 描画領域のアップデート
  4. 画面の描画更新
でやってみました.(1.の参考に書いてあるcase4)

3.折れ線グラフ


折れ線グラフはhukkumameo氏がやった通りなのですが,少しいじってクラス化しました.まずは初期化から.

class Line:

    def __init__(self, fig, ax, plot_area=(1000, 1000), len_points=100):
        self.fig = fig
        self.plot_area = plot_area

        # axis setup
        self.line_ax = ax
        self.line_ax.set_xlim(0, plot_area[0])
        self.line_ax.set_ylim(-plot_area[1], plot_area[1])
        self.ydata = [0.0 for x in range(len_points)]
        self.line, = self.line_ax.plot(self.ydata)
        # show figure
        self.fig.canvas.draw()
        self.fig.show()

高速化のために自動での軸レンジの計算を手動に変更し,初期化部分で設定しています.描画するデータに関しては,専用のリスト(FIFO)を作成し,データ更新が走るたびに一番古いデータを破棄し,新しいデータを足していくようにしました.
次は更新周りです.

    def update_data(self, points):
        # draw background with white
        self.line_ax.draw_artist(self.line_ax.patch)

        # plot points
        self.ydata.append(points)
        self.ydata.pop(0)
        self.line.set_ydata(self.ydata)
        self.line_ax.draw_artist(self.line)

        # update this graph
        self.fig.canvas.blit(self.line_ax.bbox)

    def plot(self, ydata):
        self.update_data(ydata)
        self.fig.canvas.flush_events()

自分で書いておいてなんですが,update_dataとplotの分割はあんまり意味は無さそうです...

4.散布図


折れ線ができたら散布図もやりたいということで,実際にやってみました.描画の基本方針は折れ線グラフとは変わらないのですが,やり方が少し変わってきます.このあたりの話に関しては,差分更新によるmatplotlibのアニメーションの高速化の記事によく書かれています.

class Scatter:

    def __init__(self, fig, ax, plot_area=(1000, 1000), len_points=100, show_icon=False, icon_radius=100):
        self.fig = fig
        self.plot_area = plot_area
        self.icon_radius = icon_radius if show_icon is True else None

        # axis setup
        self.pos_ax = ax
        self.pos_ax.set_xlim(-plot_area[0], plot_area[0])
        self.pos_ax.set_ylim(-plot_area[1], plot_area[1])
        self.pos_points = self.pos_ax.scatter([], [])
        self.xy = [[0.0, 0.0] for x in range(len_points)]
        if show_icon is True:
            self.agent_icon = mpatches.RegularPolygon(xy=(0, 0), numVertices=4, radius=self.icon_radius, orientation=0.0, ec="r", fill=False)
            self.pos_ax.add_patch(self.agent_icon)

        # show figure
        self.fig.canvas.draw()
        self.fig.show()


折れ線グラフと同じく,軸のレンジを決めFIFOの準備をしています.これに加え,最新のデータを示すためのポリゴン描画のコードが入っています.差分更新の記事ではmpatches.Circleの例が挙げられていますが,今回はポリゴンの方を使用し,描画に関してもdraw_artistを使用します.mpatchesに関しては公式が詳しいです.

    def update_data(self, points, orientation=0.0):
        # draw background with white
        self.pos_ax.draw_artist(self.pos_ax.patch)

        # plot points
        self.xy.append(points)
        self.xy.pop(0)
        self.pos_points.set_offsets(self.xy)
        self.pos_ax.draw_artist(self.pos_points)

        # plot the icon
        if self.icon_radius is not None:
            self.agent_icon.xy = points
            self.agent_icon.orientation = orientation
            self.pos_ax.draw_artist(self.agent_icon)

        # update this graph
        self.fig.canvas.blit(self.pos_ax.bbox)

    def plot(self, points, orientation=0.0):
        self.update_data(points, orientation)
        self.fig.canvas.flush_events()

基本的には折れ線グラフと一緒です.こっちもupdate_dataとplotはひとまとめにした方がよかったかもしれません.

   matplotlib公式,matplotlib.patches — Matplotlib 3.3.1 documentation,Matplotlib 3.3.1 documentation

5.動かす


とりあえず,[-1000, 1000]の範囲でランダムな点の組(X,Y)を生成し,グラフに打っていくことにしました.これを5000回繰り返して,平均フレームレートを計算します.実行コードはこちらから.


平均フレームレートは22.9[fps]で,目標の30[fps]には届きませんでした....
試しに一度に表示する点数を,折れ線グラフと散布図の両方で5000 -> 2500にしてみます.


平均フレームレートは31.0[fps]で,目標は達成したようです.一度の表示する点数を2500 -> 1250とさらに減らしてみます.


平均フレームレートは52.2[fps]とそこそこ速くなりました.どうやら表示するデータ数に応じてフレームレートが変動するようです.

6.結局どうなのよ


これは目標の30fps描画というべきかについてはいろいろ考える必要があると思いますは,とりあえず部分的には目標達成ということにしましょう.もやもやするけど.散布図のみだったり,グラフの更新を並列で走らせるとか,更新を隠蔽するとかすればもっとフレームレートは上がるはずなので,まだまだといったところでしょうか.

7.参考


matplotlib公式,matplotlib.patches — Matplotlib 3.3.1 documentation,Matplotlib 3.3.1 documentation




©2020 shts All Right Reserved.