2021/08/11

Jetson Nano JetPack4.6 tensorflow2.x/1.x installメモ

 なんだかちょっと使おうかなと思うたびにやっている気がするが。。。

JetsonNanoでSSDブートとかやってたが、ブートしなくなった。SDブートに切り替えしばらく使っていたが、それもブートしなくなった。多分ちゃんとシャットダウンしなかったからかなぁと思っている。かなりセットアップが面倒になってきたので、基本SDブートで、/homeをSSDに分けることにした。壊れてもSD焼きだけで済むように。

JetPackは4.6のイメージを焼いた。ストレージ壊れる前は4.5.xだったがついでに上げる。

また同じことが起きる可能性大なので、Tensorflowセットアップ手順だけメモっておく。今回は https://docs.nvidia.com/deeplearning/frameworks/install-tf-jetson-platform/index.html に沿って素直な手順で。ただ、そのとおりやっても引っかかる部分があるのでちょっとだけ手を加える。

最初にライブラリインストール

$ sudo apt install python3-dev libhdf5-serial-dev hdf5-tools libhdf5-dev zlib1g-dev zip libjpeg8-dev liblapack-dev libblas-dev gfortran

venvでTF2.x用のローカル環境作って、TF2インストール

$ sudo apt install python3-venv
$ python3 -m venv tf2
$ . ~/tf2/bin/activate
$ pip install -U pip testresources setuptools==49.6.0 wheel
$ pip install -U numpy==1.19.4 future==0.18.2 mock==3.0.5 h5py==2.10.0 keras_preprocessing==1.1.1 keras_applications==1.0.8 gast==0.2.2 futures protobuf pybind11
$ pip install --pre --extra-index-url https://developer.download.nvidia.com/compute/redist/jp/v46 tensorflow
h5pyのビルドに時間かかる。tensorflowインストール時にはgrpcioのビルドあたりからすごい時間かかる。https://developer.download.nvidia.com/compute/redist/jp/v46 をブラウザからアクセスしてもファイルリストが取れないため不安になるがpipからはアクセスできるようだ。grpcioが無事ビルドされたあとに以下のメッセージが。。

  Attempting uninstall: six
    Found existing installation: six 1.16.0
    Uninstalling six-1.16.0:
      Successfully uninstalled six-1.16.0
  Attempting uninstall: keras-preprocessing
    Found existing installation: Keras-Preprocessing 1.1.1
    Uninstalling Keras-Preprocessing-1.1.1:
      Successfully uninstalled Keras-Preprocessing-1.1.1
  Attempting uninstall: gast
    Found existing installation: gast 0.2.2
    Uninstalling gast-0.2.2:
      Successfully uninstalled gast-0.2.2
keras-preprocessing, gast入れなくてよかったんじゃ?

続いて、TF1.xのローカル環境でTF1.xインストール
$ deactivate
$ python3 -m venv tf1
$ . ~/tf1/bin/activate
$ pip install -U pip testresources setuptools==49.6.0
$ pip install -U numpy==1.18.5 future==0.18.2 mock==3.0.5 h5py==2.10.0 keras_preprocessing==1.1.1 keras_applications==1.0.8 gast==0.3.3 futures protobuf pybind11
$ pip install --pre --extra-index-url https://developer.download.nvidia.com/compute/redist/jp/v46 'tensorflow<2'
numpyのバージョンとかちょいと変更。
これでTF2.x/1.xのローカル環境構築完了。

ブートしなくなるのはどうもSSDドライブの相性が良くないのかも。と言っても他にないので細かい工夫続けながらですか。

2021/08/09

M5StickCplus と UnitV で YOLOv2

 M5StickCplus買いました。最初やりたかったのはBLEをプログラムして、ChromeのWebBluetoothから接続してみたかったから。BLE通してジャイロ情報送って簡単なアプリ作って。この時点でPlusじゃなくても良かったなと思ったけど、せっかく結構きれいなLCDが付いてるんで、カメラ接続してみたくなり、UnitV AI Camera買ってみた。

なぜかカメラだけのHatがないんですよね。M5CameraとかTimerCameraになってしまう。UnitVはM5StickVから色々取り除いた廉価版。AI処理用のKPUというのが入っているので簡単なモデルなら動くらしい。いずれやってみよう。

ここでは、StickCにCamera映像を描画することが第一目的だが、せっかくなのでUnitVに標準で入っている顔認識(tiny yolo v2)もついでにやってみたという内容。

まずはUnitV側のプログラム(MicroPython)

import time
import KPU as kpu
import sensor
import struct
from fpioa_manager import fm
from machine import UART

fm.register(35, fm.fpioa.UART1_TX, force=True)
fm.register(34, fm.fpioa.UART1_RX, force=True)
uart = UART(UART.UART1, 115200800timeout=1000read_buf_len=4096)

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.run(1)

model = kpu.load(0x300000)  # Load Model File from Flash
anchor = (1.8892.52452.94653.940563.99987,
          5.36585.1554376.922756.7183759.01025)
kpu.init_yolo2(model0.50.35anchor)

def transfer(img):
    header = struct.pack('>BBH'00img.size())
    uart.write(header)
    uart.write(img)

clock = time.clock()

while(True):
    clock.tick()
    img = sensor.snapshot()
    bbox = kpu.run_yolo2(modelimg)
    if bbox:
        for i in bbox:
            img.draw_rectangle(i.rect(), thickness=4)
    transfer(img.resize(160120).compress(10))
    print(clock.fps())

UnitVとStickCとは4Pinケーブルで接続してシリアル通信するので、UARTの設定をする。次にカメラ(sensor)の設定、KPUの設定、ループ内でカメラ>yolo処理>転送の流れ。

ポイントとしては、QVGAのまま転送すると超遅いので160x120にリサイズして、それをJPEG圧縮して転送している。だいたい1.5kbytesくらいになるので5fps位になる。

単純にカメラ処理だけなら10fps。KPU処理入れると8fpsくらい。転送が一番やばい。この時点で素直にM5StickVにしとけばと思ったが、遊びだしこの不自由さがまたたまらない!

最初からQQVGA(160x120)でやればと思ったが、yolo2がそのサイズを受け付けなかった。

そして以下がStickCplus側のプログラム

#include <M5StickCPlus.h>
#include <esp_timer.h>

#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_STDIO
#define STBI_ONLY_JPEG
#include "stb_image.h"

void *buffer = NULL;
TFT_eSprite canvas = TFT_eSprite(&M5.Lcd);

void *decode(void *bufferint lengthint *wint *h)
{
  int comp = 3;
  stbi_uc *image = stbi_load_from_memory((stbi_uc *)bufferlengthwh, &compcomp);
  uint8_t *src = (uint8_t *)image;
  uint16_t *dst = (uint16_t *)image;
  for (int i = 0i < *w * *hi++)
  {
    const uint8_t r = *src++;
    const uint8_t g = *src++;
    const uint8_t b = *src++;
    *dst++ = canvas.color565(rgb);
  }
  return image;
}

void setup()
{
  M5.begin();
  M5.Axp.ScreenBreath(13);
  M5.Lcd.setRotation(3);
  Serial2.begin(115200, SERIAL_8N1, 3233);

  canvas.createSprite(M5.Lcd.width(), M5.Lcd.height());
  canvas.setSwapBytes(false);
  canvas.setTextSize(2);

  size_t size = 4 * 1024;
  buffer = malloc(size);
  M5.Lcd.printf("alloc: %d"size);
}

void loop()
{
  static int64_t t0 = 0;

  if (Serial2.available() && Serial2.read() == 0 && Serial2.read() == 0)
  {
    int64_t t1 = esp_timer_get_time();

    int size = (Serial2.read() << 8) + Serial2.read();
    size = Serial2.readBytes((uint8_t *)buffer, (size_t)size);
    int64_t t2 = esp_timer_get_time();

    int wh;
    void *image = decode(buffersize, &w, &h);
    int64_t t3 = esp_timer_get_time();

    canvas.pushImage(canvas.width() - w, (canvas.height() - h) / 2wh, (uint16_t *)image);
    free(image);
    int64_t t4 = esp_timer_get_time();

    canvas.setCursor(00);
    canvas.printf("S %d\n"size);
    canvas.printf("R %d\n", (t2 - t1) / 1000);
    canvas.printf("D %d\n", (t3 - t2) / 1000);
    canvas.printf("I %d\n", (t4 - t3) / 1000);
    canvas.printf("T %d\n", (t3 - t0) / 1000);
    if (t0)
    {
      int ms = (t1 - t0) / 1000;
      canvas.printf("F %d\n"ms);
      canvas.printf("  %.1f\n"1000.0f / ms);
    }
    canvas.pushSprite(00);
    t0 = t1;
  }
}

色々書いてあるけど、やっていることは受信したらcanvasへ描画するだけ。

ポイントは、jpegデコードにnothing/stbのstb_image.hを使っていること。
M5ライブラリ内にも実はJpegデコードがあるがなぜかコメントアウトされている。無理やり復活させるのも気持ち悪いので、昔からよく利用しているstb_imageを使うことにした。

実はstbの前に、picojpeg 使ってやろうとした。メモリが少ない組み込み系ではこっちがいいかなと思った。PCでテストする分には問題なかったが実際組み込んでみると何故かMCU単位で変になる。UnitVのJpegエンコードとの相性かもしれない。
stb_imageは問題なかったし、パフォーマンスもstbの方が若干良かった。

_threadについて
UnitVのmicropython(OpenMV)には_threadが一応実装されている?ので、カメラとYOLOをメイン。転送をスレッド化すれば並列処理されていいのでは?と思った。
で実際に_threadで転送部を回して、queueがないので代わりにuheapqを介してイメージデータをスレッドに渡すようにしてみた。安定させるためにメイン、スレッドともにtime.sleep_ms(1)が必要だった。がしかし、スピードは変わらなかった。。。
内部実装がただのコルーチンなのではないかと思った。それぞれにsleep入れないと処理が回らないのもそれでうなずける。コルーチンのyield的なものが必要なのだろうと。

2021/07/30

lightdm ログイン画面にIPアドレス表示

起動直後に自身のIPアドレスを表示させたい!ログインせず!それもデカく!
下の写真だと分かりにくいけど、Ubuntuでlightdmのログインウィンドウ表示したときにIPアドレスを表示した状態がこれです。マシンはJetsonNano。アドレスが複数出てるのはDockerなど複数あるから。
まず、表示スクリプトはこんな感じです
hostname -I > /tmp/ip.txt
zenity --text-info --filename /tmp/ip.txt --width=800 --height=400 --font "Monaco 70"
これを実行可能なスクリプトファイルとしてどこかに作成する

ログイン前に表示する設定(lightdm)
/etc/lightdm/lightdm.conf.d/99-showip.conf を作成して
[Seat:*]
greeter-setup-script=<スクリプトへのパス>
を記入。99-showip.confとかじゃなくても何でもいいでしょう。全ファイル実行するっぽいので。

JetsonNanoのUbuntuDesktopの初期DMはgdm3なのでgdmで実現したかったが、残念ながらうまく機能しなかった。/etc/gdm3/Init/Default、/usr/share/gdm/greeter/autostartとか、色々記事探しながらやってみたが、記事通りには動作しなかった。。。ということでlightdmに変更した。

HDMI 3.5"LCDディスプレイ
写真のモニタはラズパイ向けのモニターで約3千円位で買えるスグレモノ。これはKumanというメーカーのものだが最近見かけない。代わりにOSOYOOってところのやつが同じ価格帯で出てる。
ちょっと繋ぎたいときに重宝する。このモニターサイズなのでデカく表示したかった。

なぜ表示したかったか
時々使用するLinuxマシンが複数あり、常時モニタやキーボードなどは接続してない。メインモニタは1つしか持ってない。sshとかVNCで使うのが常なのだけど、ふとIPアドレスが不明になってしまう。どれがどれだっけ?とか、DHCPで時々変わったりする。固定にすればいいんだけど結局忘れるし。面倒だし。
ということで、とにかく起動して、このチビモニタに接続すれば判明するようにしたかった。