エアガン測定

エアガンに関する色々を測定したりしなかったりしています http://www.eonet.ne.jp/~daisaku-tech/index.html

エアガン電子ターゲットもどき作成

東京マルイのプロキャッチターゲットにBB弾が当たったところを検出する、電子ターゲットもどきを作成しました。

今はBB弾が当たった時の位置と順番の検出だけ。

LinuxのPCにWebカメラを接続し、PythonOpenCVで映像(画像)処理してBB弾を検出しています。

youtu.be

VS codeのターミナルでbyobuを使う

VS codeの設定

VS codeのターミナル(統合/内蔵ターミナル)でbyoubuを使おうとすると、F3などのキーがVS code側に取られてしまい、byobuの操作ができません。次の設定を行うことで解決できます。

VS codeの、設定 > ターミナル >

Integrated: Allow Chordsのチェックを外す。

Integrated: Commands To Skip Shell

「項目の追加」をクリックし、次のコマンドを入力。

-workbench.action.quickOpen
-workbench.action.terminal.findNext
-workbench.action.terminal.findPrevious
-workbench.action.debug.continue
-workbench.action.debug.restart
-workbench.action.debug.run
-workbench.action.debug.start
-workbench.action.debug.stop
-workbench.action.focusNextPart
-workbench.action.focusPreviousPart
-workbench.action.debug.pause
-workbench.action.quickOpen
-workbench.action.terminal.focusFind

これらは、おおよそ次のキー入力を、VS codeキーバインドから外す、という設定です。コマンドの頭に"-"をつけることで外す意味になっています。 (shift/ctrl+)F2/F3/F4/F5/F6/F12 ctrl+p/n/f/b/a/e/d/h

統合ターミナルで横取りされるキーバインドと調べ方

どのキーバインドVS code側に取られてしまうのかは、上記Integrated: Commands To Skip Shellの「既定の設定(JSON)」をクリックするとdefaultSettings.json内の「既定でスキップされるコマンド」にすべて記載されています。

残念ながら、これらのコマンドにはバインドされているキーが記載されていません。キーバインドの詳細は、shift+ctrl+pなどで「基本設定:既定のキーボードショートカットを開く(JSON)」で「既定のキーバインド」を開くとわかります。

統合ターミナルで入力したキーがVS code側に取られてしまうな、と思ったら、「既定のキーバインド」でそのキーを探し、該当するコマンドが「既定の設定(JSON)」にあればそれを上記の「項目の追加」で頭に"-"をつけて追加する、という感じです。

USBカメラの映像の背景差し替え

USBカメラの映像の背景を、指定した画像ファイルに置き換えてみました。

youtu.be

googleによるセマンティックセグメンテーションDeepLabV3+を使用しました。次のところの、Keras版を使用しました。

GitHub - bonlime/keras-deeplab-v3-plus: Keras implementation of Deeplab v3+ with pretrained weights

使用したPCのスペックは次の通り。

使用したライブラリなど。

このQiitaの記事を参考にさせていただきました。

keras-deeplab-v3-plusで人だけとってみる(ソース有り) - Qiita

プログラムは次の通り。

# -*- coding: utf-8 -*-
"""
背景差し替えプログラム
USBカメラの映像で、人物以外の背景を指定した画像ファイルに差し替えます。
引数に差し替えたい背景画像ファイル名をしてしてください。

$ python vwall.py 背景画像ファイル名

必要なライブラリ
python(3.6以上)
opencv(4.x)
tensorflow(1.13.1)またはtensorflor-gpu(1.13.1)
keras(2.2.4)
Keras版DeepLabV3+(2020年5月のリポジトリで、tensorflow 1.x用ブランチ)
https://github.com/bonlime/keras-deeplab-v3-plus
"""
import cv2
import numpy as np
import model
from keras.applications.mobilenetv2 import preprocess_input
import sys

if len(sys.argv) < 2:
    print("壁紙ファイル名を指定してください。")
    sys.exit(1)
    
wallfile = sys.argv[1]

def inference(model_dlv3, img):
    """
    推論実施
    :param model_dlv3: DeepLabV3のモデル
    :param img: カメラからの入力画像
    :return: 人物部分のマスク画像
    """
    img = np.array(img, dtype='float32')
    img = preprocess_input(img)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = cv2.resize(img, (512, 512))
    predicted = model_dlv3.predict(img[np.newaxis, ...])
    person_score = predicted[0, :, :, 15]
    back_score = predicted[0, :, :, 0]
    mask = (person_score > back_score).astype("uint8") * 255
    mask = cv2.resize(mask, (640, 480))

    return mask

def main():
    wall = cv2.imread(wallfile)
    wall = cv2.resize(wall, (640, 480))
    
    cap = cv2.VideoCapture(0)
    model_dlv3 = model.Deeplabv3()

    while(cap.isOpened()):
        img_camera = cap.read()[1]
        img_camera = cv2.resize(img_camera, (640, 480))
        mask = inference(model_dlv3, img_camera)
        mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
        n_mask = cv2.bitwise_not(mask)
        wall_mask = cv2.bitwise_and(wall, n_mask)
        person = cv2.bitwise_and(img_camera, mask)
        output = cv2.bitwise_or(person, wall_mask)

        cv2.imshow("camera input", img_camera)
        cv2.imshow("output", output)
        
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

VS Code でテキスト整形(改行付きラップ)

VS Code拡張機能、Rewrapを使うとテキスト文字列を改行付きで整形できます。

marketplace.visualstudio.com

https://raw.githubusercontent.com/wiki/stkb/Rewrap/images/example.png

このように一行の長いコメントを複数行のコメントに整形してくれます。EmacsのM-q(fill-paragraph)と同じ機能で、VS CodeでもキーバインドはAlt-qです。

ソースコードのコメントだけでなく、拡張子txtのテキストファイルでも大丈夫です。また、日本語でも大丈夫です。ざっと確認したところ、日本語の禁則処理も対応しています。、。-が行頭に来ないようにしてくれます。

Linux Kernel 4.19のリアルタイム性能

cyclictestによるリアルタイム性能測定

rt-testsパッケージに含まれるcyclictestとhackbenchを使ってDebian 10(Linux Kernel 4.19)のリアルタイム性能を見てみました。

結論から言うと、latencyが1ms程度の精度で良いのであれば、PREEMPT RT Kernelではない、標準Kernel(Voluntary PREEMPT)でも十分と思います。2000年代のKernel 2.4の頃は、PREEMPT RT Kernelでないとリアルタイム性能はダメダメだったと思うのですが、今のKernelはあまり精度が必要のないかなり緩いソフトリアルタイムなら十分な性能です。

cyclictest/hackbenchコマンド

cyclictestは、nanosleepまたはclock_nanosleepを使用して、指定した時間sleepしたあとどれぐらいの時間ずれがあるのかを繰り返し測定するコマンドです。

使用例

$ sudo cyclictest --smp -m -l 30000
# /dev/cpu_dma_latency set to 0us
policy: other/other: loadavg: 0.12 0.08 1.00 1/245 1587

T: 0 ( 1585) P: 0 I:1000 C:   2152 Min:     27 Act:   64 Avg:   63 Max:      85
T: 1 ( 1586) P: 0 I:1500 C:   1421 Min:     32 Act:   63 Avg:   65 Max:     125

Ctrl-cで停止します。"-p 49"のようにオプションをつけると、SCHED_FIFOでプライオリティ49で動作します。このオプションなしの場合、通常のSCHED_OTHERで動作します。

使用例:SCHED_FIFOの場合

$ sudo cyclictest --smp -m -l 30000 -p 49
# /dev/cpu_dma_latency set to 0us
policy: fifo: loadavg: 0.00 0.04 0.81 1/245 1596

T: 0 ( 1595) P:49 I:1000 C:   1676 Min:      5 Act:    6 Avg:    5 Max:      19
T: 1 ( 1596) P:49 I:1500 C:   1103 Min:      5 Act:    5 Avg:    5 Max:      43

また、hackbenchはCPUに負荷をかけるコマンドです。 使用例

$ hackbench -l 100000
Running in process mode with 10 groups using 40 file descriptors each (== 400 tasks)
Each sender will pass 100000 messages of 100 bytes
ここでCtrl-cにより停止すると、次の表示。
Signal 2 caught, longjmp'ing out!
longjmp'ed out, reaping children
sending SIGTERM to all child processes
signaling 400 worker threads to terminate
Time: 1.985

オプション"-l"で実行回数を指定します。

測定条件

次の条件でリアルタイム性能を測定しました。

  • ハードウェア:ECS Liva bat-mini:Celeron N2807 1.58GHz / RAM 2GB / eMMC 32GB
  • Debian GNU/Linux 10 (buster):Kernel 4.19
  • デスクトップが表示されている状態でsshによりログインし、コマンド実行。
  • 標準のKernelと、PREEMPT_RT Kernelで実行。
  • 何もアプリを起動していない状態と、hackbenchによりCPU負荷100%の状態でcyclictestを実行。
  • SCHED_OTHERと、SCHED_FIFO・プライオリティ49の2種類でcyclictestを実行。
  • ループ回数30000回。

標準Kernel

まずは標準Kernelの結果から。 uname -aの結果は次の通り。

$ uname -a
Linux XXX 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1+deb10u1 (2020-04-27) x86_64 GNU/Linux

標準Kernelのconfigは次の通り。

CONFIG_HIGH_RES_TIMERS=y
# CONFIG_PREEMPT_NONE is not set
CONFIG_PREEMPT_VOLUNTARY=y
# CONFIG_PREEMPT is not set

標準Kernel:負荷なし:通常プロセス(SCHED_OTHER)

T: 0 ( 1795) P: 0 I:1000 C:  30000 Min:     10 Act:   55 Avg:   63 Max:    9281
T: 1 ( 1796) P: 0 I:1500 C:  19955 Min:     26 Act:   56 Avg:   72 Max:    9602

Celeron N2807は2コア2スレッドのため、Tは0と1の2つ。IはIntervalでCはCount。Min/Avg/Maxはそれぞれ最小/平均/最大のlatency(μs)で、Actはループごとのlatencyで、実行中はこの部分が更新され続けます。

latencyは平均63と72μsということですが、最大は9281と9602と9msを超えています。

標準Kernel:負荷100%:通常プロセス(SCHED_OTHER)

T: 0 ( 2454) P: 0 I:1000 C:  30000 Min:     48 Act:   67 Avg: 1525 Max:  101095
T: 1 ( 2455) P: 0 I:1500 C:  27390 Min:     56 Act: 1822 Avg: 1272 Max:   59227

CPU負荷100%では、平均1525と1272と1msを超えており、最大は101095と59227と100msを超える場合が出ています。

標準Kernel:負荷なし:リアルタイムプロセス(SCHED_FIFOプライオリティ49)

T: 0 ( 1928) P:49 I:1000 C:  30000 Min:      3 Act:    5 Avg:    5 Max:     289
T: 1 ( 1929) P:49 I:1500 C:  20010 Min:      4 Act:    5 Avg:    5 Max:     369

SCHED_FIFOにすると負荷なしの状態では平均5μs、最大でも289と369μsとなります。

標準Kernel:負荷100%:リアルタイムプロセス(SCHED_FIFOプライオリティ49)

T: 0 ( 2463) P:49 I:1000 C:  30000 Min:      6 Act:   18 Avg:   14 Max:     446
T: 1 ( 2464) P:49 I:1500 C:  19266 Min:      7 Act:   16 Avg:   14 Max:      32

SCHED_FIFOなら負荷100%でも平均14μs、最大446と32μsとなり、latency 1ms以下で良いようなシステムでは十分な性能となっています。

以上は標準Kernelの結果です。リアルタイムプロセスSCHED_FIFOにすればそこそこいい性能と思います。

PREEMPT RT Kernel

次にPREEMPT_RTパッチ適用版のKernelです。Debianの場合、PREEMPT_RTパッチ適用版Kernelがaptコマンドで次のように簡単にインストールできます。

$ sudo apt install linux-image-rt-amd64

uname -a の結果は次の通り。

$ uname -a
Linux XXX 4.19.0-8-rt-amd64 #1 SMP PREEMPT RT Debian 4.19.98-1+deb10u1 (2020-04-27) x86_64 GNU/Linux

Kernelのconfig

CONFIG_HIGH_RES_TIMERS=y
CONFIG_PREEMPT=y
CONFIG_PREEMPT_RT_BASE=y
CONFIG_HAVE_PREEMPT_LAZY=y
CONFIG_PREEMPT_LAZY=y
# CONFIG_PREEMPT_NONE is not set
# CONFIG_PREEMPT_VOLUNTARY is not set
# CONFIG_PREEMPT__LL is not set
# CONFIG_PREEMPT_RTB is not set
CONFIG_PREEMPT_RT_FULL=y
CONFIG_PREEMPT_COUNT=y

PREEMPT RT Kernel:負荷なし:通常プロセス(SCHED_OTHER)

T: 0 (  837) P: 0 I:1000 C:  30000 Min:     10 Act:   64 Avg:   64 Max:    5289
T: 1 (  838) P: 0 I:1500 C:  19987 Min:     12 Act:   65 Avg:   65 Max:    7399

標準Kernelと大差ありません。

PREEMPT RT Kernel:負荷100%:通常プロセス(SCHED_OTHER)

T: 0 ( 1248) P: 0 I:1000 C:  30000 Min:     36 Act:   72 Avg: 1032 Max:  150414
T: 1 ( 1249) P: 0 I:1500 C:  26185 Min:     65 Act:   71 Avg:  656 Max:   68300

平均latencyが、標準Kernelより若干良いぐらい。

PREEMPT RT Kernel:負荷なし:リアルタイムプロセス(SCHED_FIFOプライオリティ49)

T: 0 (  843) P:49 I:1000 C:  30000 Min:      4 Act:    6 Avg:    5 Max:      75
T: 1 (  844) P:49 I:1500 C:  19997 Min:      4 Act:    6 Avg:    5 Max:      22

標準Kernelの同条件と比べると、最大latencyが75と22μsと、一桁少なくなっています。

PREEMPT RT Kernel:負荷100%:リアルタイムプロセス(SCHED_FIFOプライオリティ49)

T: 0 ( 1253) P:49 I:1000 C:  30000 Min:      7 Act:   15 Avg:   12 Max:      41
T: 1 ( 1254) P:49 I:1500 C:  16310 Min:      7 Act:   15 Avg:   12 Max:      29

標準Kernelの同条件と比べると、平均latencyが14μsに対して12μs、最大latencyが41と29μsと100μs以下になっています。

以上から、標準KernelよりもPREEMPT RT Kernelの方が、リアルタイムプロセスの最大latencyが少し良いという結果です。

PythonとOpenCVで車線検出

PythonOpenCVで、車線を検出するプログラムを作成しました。チョー単純なロジックなので、条件が想定外になると検出できないんですが、ある程度検出できています。


PythonとOpenCVによる車線検出

 

映像から1フレームの画像を取り出し、グレースケールに変換してからヒストグラムを平坦化します。空の部分は不要なので消去します。それとは別に、フレームの画像をHSVに変換してから一定の範囲の黄色だけを抽出します。これは、白線の検出用にグレースケールに変換してしまうと、黄色がわからなくなるため、別途黄色だけ抽出する、ということです。

抽出した黄色部分もグレースケールに変換してから、Cannyを使用して輪郭を抽出します。その輪郭画像に対してHoughLinesを使用して直線を検出しています。

使用している道路の映像は、PS4のゲーム、The Crew2のものです。画像処理用としては、実写の映像よりもだいぶ条件がいい状態です。実写の映像を自分で撮影してくるのはメンドイので横着しました。

# -*- coding: utf-8 -*-
"""
車線検出
Cannyによるエッジ検出とHoughLinesによる直線検出
python 3.6
opencv 4.1
"""
import cv2
import numpy as np
import sys

# コマンドライン引数 1番目=入力映像ファイル名 2番目=出力映像ファイル名 3番目=出力輪郭線ファイル名
args = sys.argv

if len(args) == 1:
    sys.exit(1)
    
input_file = args[1]

output_file = None
contour_file = None
if len(args) >= 3:
    output_file = args[2]
if len(args) == 4:
    contour_file = args[3]


def equalize(img_org):
    """ 平坦化
    """
    img_tmp = cv2.cvtColor(img_org, cv2.COLOR_BGR2GRAY)

    # ヒストグラム平坦化
    img_tmp = cv2.equalizeHist(img_tmp)

    # 画面の上半分消去
    count = np.array([[0, 0], [1279, 0], [1279, 340], [0, 340]])
    img_tmp = cv2.fillPoly(img_tmp, pts=[count], color=(0))

    return img_tmp


def yellow(img_org):
    """ 黄色線抽出
    HSVに変換してから黄色だけを抽出する
    """
    img_hsv = cv2.cvtColor(img_org, cv2.COLOR_BGR2HSV)

    # 日本の道路の黄色線
    # lower_color = np.array([0, 40, 50], np.uint8)
    # upper_color = np.array([15, 200, 200], np.uint8)
    
    # The Crew2の黄色線
    lower_color = np.array([15, 100, 50], np.uint8)
    upper_color = np.array([40, 200, 200], np.uint8)

    # マスク画像の生成
    img_mask = cv2.inRange(img_hsv, lower_color, upper_color)

    # フレーム画像とマスク画像の共通の領域を抽出
    img_tmp = cv2.bitwise_and(img_hsv, img_hsv, mask=img_mask)
    img_tmp = cv2.cvtColor(img_tmp, cv2.COLOR_BGR2GRAY)
    ret, img_tmp = cv2.threshold(img_tmp,
                                 10,                # 閾値
                                 256,               # 画素値の最大値
                                 cv2.THRESH_BINARY) # 2値化type

    # 画面の上半分消去
    count = np.array([[0, 0], [1279, 0], [1279, 340], [0, 340]])
    img_tmp = cv2.fillPoly(img_tmp, pts=[count], color=(0))

    return img_tmp
    

def road(img):
    """ 車線検出・描画
    HoughLinesを使用して一定の長さの直線を検出・描画
    """
    road = np.zeros((720, 1280, 3), np.uint8)
    lines = cv2.HoughLines(img,
                           1,          # lines
                           np.pi/180,  # rho
                           150)        # theta
    if lines is not None:
        for line in lines:
            for rho, theta in line:
                a = np.cos(theta)
                b = np.sin(theta)
                x0 = a*rho
                y0 = b*rho
                x1 = int(x0 + 2000*(-b))
                y1 = int(y0 + 2000*(a))
                x2 = int(x0 - 2000*(-b))
                y2 = int(y0 - 2000*(a))

                # 水平に近い線は描画しない
                if abs(a) < 0.15:
                    continue
                # print("rho,theta,a,b,x0,y0", rho, theta, a, b, x0, y0)

                cv2.line(road, (x1, y1), (x2, y2), (0 ,0, 255), 2)

    # 画面の上半分消去
    count = np.array([[0, 0], [1279, 0], [1279, 340], [0, 340]])
    road = cv2.fillPoly(road, pts=[count], color=(0, 0, 0))

    return road


def main():
    cap = cv2.VideoCapture(input_file)

    # 描画した映像の録画用
    fmt = cv2.VideoWriter_fourcc("m", "p", "4", "v")
    size = (1280, 720)
    if output_file is not None:
        output_writer = cv2.VideoWriter(output_file, fmt, 30, size)
    if contour_file is not None:
        contour_writer = cv2.VideoWriter(contour_file, fmt, 30, size)
    
    while(cap.isOpened()):
        # 1フレームの画像読み込み
        img_org = cap.read()[1]

        if img_org is None:
            break

        # グレースケールに変換してから平坦化
        img_tmp = equalize(img_org)
        # 黄色線は別に抽出してからグレースケールに変換
        img_yellow = yellow(img_org)
        # 2つの画像をorする
        img_tmp = cv2.bitwise_or(img_tmp, img_yellow)

        # blurをかけて細かな部分をつぶす
        img_tmp = cv2.blur(img_tmp, ksize=(5, 5))
        # 輪郭抽出
        img_for_draw = cv2.Canny(img_tmp,
                                 200,     # threshold1
                                 255,     # threshold2
                                 apertureSize = 3)
        # 車線検出・描画
        road_lines = road(img_for_draw)

        # グレースケールからRGBに変換
        img_for_draw = cv2.cvtColor(img_for_draw, cv2.COLOR_GRAY2BGR)

        # 元の画像と描画した車線を合成
        img = cv2.bitwise_or(img_org, road_lines)

        cv2.imshow("img", img)

        # 映像ファイルに保存
        if output_file is not None:
            output_writer.write(img)
        if contour_file is not None:
            contour_writer.write(img_for_draw)
        
        # qキーが押されたら終了
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

弾速計試作その3:透明ディスプレイ

透過型OLEDを使用した弾速計を作成しました。


透明ディスプレイ弾速計

銃口に取り付けたセンサーにより、弾速の計測と、発射数のカウントを行います。描画が遅いんで、連射すると表示が追いつきません...