アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

Jetson nanoでPWM制御をするまでの手順【ハードウェア】

今回はJetson nanoでPWM(Pulse Width Modulation)制御をするまでの手順をまとめてく。

Jetson nanoでPWMのPinは「32, 33」の二つ
これを使ってサーボモータをマルチスレッドで二つ動かす。


目次
1. 配線図
2. Jetson nanoでGPIOのインストール
3. PWM設定-「Jetson-IO tool」の実行
4. PIN番号を合わせる
5. PWMでサーボモータを制御

1.配線図

Jetson nanoの40 PINs(サーボモータの配線は3つあってそれぞれ、PWMとGNDと電圧V。


Jetson nanoとサーボモータ(SG-5010)の配線図。



2 Jetson nanoでGPIOのインストール

PWM用のGPIOをインストール。

$ git clone https://github.com/NVIDIA/jetson-gpio.git
$ cd jetson-gpio
$ sudo python3 setup.py install

# グループ作成とユーザーの追加
$ sudo groupadd -f -r gpio
$ sudo usermod -a -G gpio <ユーザー名>

# udevルールの追加と反映
$ sudo cp lib/python/Jetson/GPIO/99-gpio.rules /etc/udev/rules.d/
$ sudo udevadm control --reload-rules && sudo udevadm trigger
# 再起動して使えるようにする
$ sudo reboot

3 PWM設定-「Jetson-IO tool」の実行

Jetson nanoにあるPINを外部ピンへ拡張する。

$ sudo /opt/nvidia/jetson-io/jetson-io.py

「Jetson Expansion Header Tools」でpwm0, pwm2を選択
「save and reboot to reconfigure pins」で再起動して反映。

|                Select desired functions (for pins):                |
 |                                                                    |
 |                 [ ] aud_mclk      (7)                              |
 |                 [ ] i2s4          (12,35,38,40)                    |
 |                 [*] pwm0          (32)                             |
 |                 [*] pwm2          (33)                             |
 |                 [ ] spi1          (19,21,23,24,26)                 |
 |                 [ ] spi2          (13,16,18,22,37)                 |
 |                 [ ] uartb-cts/rts (11,36)                          |


****jetson-io.py実行時に一瞬、画面に何かが表示されて直ぐに終了してしまう場合

DTB ファイルが /boot ディレクトリ配下に存在しているので、/boot/dtb ディレクトリを作成して、その中にコピー。

cd /boot
sudo mkdir dtb
sudo cp *.dtb* dtb/

再度
$ sudo /opt/nvidia/jetson-io/jetson-io.py

PWMが設定できてるなら下の様にpwm-0, pwm-2がnullになる

$ sudo cat /sys/kernel/debug/pwm
>>>>>>

platform/70110000.pwm, 1 PWM device
 pwm-0   (pwm-regulator       ): requested enabled period: 2500 ns duty: 0 ns polarity: normal

platform/7000a000.pwm, 4 PWM devices
 pwm-0   ((null)              ): period: 0 ns duty: 0 ns polarity: normal
 pwm-1   (pwm-regulator       ): requested enabled period: 8000 ns duty: 1440 ns polarity: normal
 pwm-2   ((null)              ): period: 0 ns duty: 0 ns polarity: normal
 pwm-3   (pwm-fan             ): requested enabled period: 45334 ns duty: 0 ns polarity: normal

4. PIN番号を合わせる

Jetson Nanoでは普通に40PINを拡張しただけではサーボモータが動かない。

サーボモータが動くようにするにはさらに設定が必要で一番苦労したところ。

# assign Pin32 to PWM0
$ busybox devmem 0x700031fc 32 0x45
$ busybox devmem 0x6000d504 32 0x2
# assign Pin33 to PWM2
$ busybox devmem 0x70003248 32 0x46
$ busybox devmem 0x6000d100 32 0x00

$ cd /sys/devices/7000a000.pwm/pwm/pwmchip0
# Control Pin 32 of PWM0
echo 0 > export
echo 20000000 > pwm0/period
echo 2500000 > pwm0/duty_cycle
echo 1 > pwm0/enable

# Control Pin33 of PWM2
echo 2 > export
echo 20000000 > pwm2/period
echo 1500000 > pwm2/duty_cycle
echo 1 > pwm2/enable

設定したらサーボモータが動いた。



NVIDIAの元記事

参考記事


5 PWMでサーボモータを制御

1つのサーボモータを動かす

import RPi.GPIO as GPIO
import time

output_pins = {
    'JETSON_XAVIER': 18,
    'JETSON_NANO': 32,
    'JETSON_NX': 33,
    'CLARA_AGX_XAVIER': 18,
    'JETSON_TX2_NX': 32,
    'JETSON_ORIN': 18,
}
output_pin = output_pins.get(GPIO.model, None)
if output_pin is None:
    raise Exception('PWM not supported on this board')


def main():
    # Pin Setup:
    # Board pin-numbering scheme
    GPIO.setmode(GPIO.BOARD)
    # set pin as an output pin with optional initial state of HIGH
    GPIO.setup(output_pin, GPIO.OUT, initial=GPIO.HIGH)
    p = GPIO.PWM(output_pin, 50)
    val = 7.25
    incr = 0.25
    p.start(val)

    print("PWM running. Press CTRL+C to exit.")
    try:
        while True:
            time.sleep(1)
            if val >= 12:
                incr = -incr
            if val <= 2.5:
                incr = -incr
            val += incr
            p.ChangeDutyCycle(val)
    finally:
        p.stop()
        GPIO.cleanup()

if __name__ == '__main__':
    main()

マルチスレッドで2つのサーボモータを動かす

#!/usr/bin/env python

import Jetson.GPIO as GPIO
import time
import threading
import sys
from formura import Angle2Duty

OUTPUT_PIN1 = 32 
OUTPUT_PIN2 = 33
CYCLE = 50
t=2

def setup_device():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(OUTPUT_PIN1, GPIO.OUT, initial=GPIO.HIGH)
    pw = GPIO.PWM(OUTPUT_PIN1, CYCLE)

    GPIO.setup(OUTPUT_PIN2, GPIO.OUT, initial=GPIO.HIGH)
    ph = GPIO.PWM(OUTPUT_PIN2, CYCLE)
    return pw, ph

def pw_loop(pw):
    while flag:
        dc1 = Angle2Duty(450)
        pw.start(dc1)
        print("width dc {}".format(dc1))
        time.sleep(t)
        dc2 = Angle2Duty(500)
        pw.start(dc2)
        print("width dc {}".format(dc2))
        time.sleep(t)
        dc3 = Angle2Duty(410)
        pw.start(dc3)
        print("width dc {}".format(dc3))
        time.sleep(t)
        dc4 = Angle2Duty(300)
        pw.start(dc4)
        print("width dc {}".format(dc4))
        time.sleep(t)

def ph_loop(ph):
    while flag:
        dc1 = Angle2Duty(120)
        ph.start(dc1)
        print("ph height {}".format(dc1))
        time.sleep(t)
        dc2 = Angle2Duty(180)
        ph.start(dc2)
        print("ph height {}".format(dc2))
        time.sleep(t)
        dc3 = Angle2Duty(240)
        ph.start(dc3)
        print("ph height {}".format(dc3))
        time.sleep(t)
        dc4 = Angle2Duty(170)
        ph.start(dc4)
        print("ph height {}".format(dc4))
        time.sleep(t)
        

if __name__ == '__main__':
    flag = True
    c=0
    pw, ph = setup_device()
    th1 = threading.Thread(target=pw_loop, args=(pw,))
    th1.start()
    th2 = threading.Thread(target=ph_loop, args=(ph,))
    th2.start()
    while True:
        c +=1
        if c==3000:
            flag =False
            th1.join()
            th2.join()
            pw.stop()
            ph.stop()
            GPIO.cleanup()
            sys.exit(1)


参考記事

JetPack 4.3 (r32.3.1) で追加された Jetson-IO tool を使用して Pinmux テーブルを設定してみた。
Jetson Nano の 2 つのハードウェア PWM を使用してみた
Jetson Nano の GPIO にサーボモータをつないで制御してみる

物体検出(yolov7)のバウンディングボックスから奥行きの距離(depth)を計算してみた

Jetsonに2つのカメラをつけて、それぞれに物体検出のyolov7で推論をかける。そこで得たbboxの位置座標から視差(disparity)を求めて、奥行きの距離(depth)を求めてみた。

その過程と結果の備忘録


1. データセットの用意
2. disparityからdipthを求める手順
3. パラメーターとDipthの計算式
4. yolov7でbboxを推論
5. 結果:bboxと計測値からdisparityとdepthの関係図


1. データセットの用意

JetsonでCSIカメラのimx219を2台使って、右(right) と左(left) 用カメラとして使った。

実際の使用した場面の画像

使ったCSIカメラ


2. disparityからdipthを求める手順

下のサイトを参考にした。

webカメラ2台で距離測定その2

disparityの公式図

参考サイトによると、上のdisparityの公式を近似式で求めることができ、その結果disparityとdepthは反比例する関係になる。

・disparityが大きくなればdepthは小さくなる
・disparityが小さくなればdepthは大きくなる




参考サイトだとopencvで物体の重心からdepthを求めてだけど、今回は物体検出のbboxの位置座標からdisparityを求めた。

opencvを使ったdisparityからdipthの計算手順

1. カメラから画像を取得
2. 色抽出&二値化
3. 平滑化処理
4. 対象の重心位置の推定
5. 左右の画像から得られた対象の重心位置の差を求める
6. 重心位置の差から距離を求める


物体検出を使ったdisparityからdipthの計算手順

1. カメラから画像を取得
2. 物体検出で推論してbboxを取得
3. 左右のbboxのx座標の差の絶対値を求める
4. 3からdipth(距離)を求める




3. パラメーターとDipthの計算式

カメラとかの採寸やサイズは下の通り。

各変数の値を求めた結果、dipthの計算式は下のようになった。

パラメーター

画像サイズ:(Width, Height) = (1280, 960)

焦点距離:0.315 cm. ( f= 0.315

カメラ間の距離:2.6 cm. ( T= 2.6

画像素子:2.8μm = 0.00028 cm ( imgelement= 0.00028

Depth計算式

dipth(cm)= \dfrac {Tf} {imgelement \times disparity} = \dfrac {2925} {disparity}(cm)




4. yolov7でbboxを推論

下がyolov7で検出した画像。クラスが間違ってるのは後処理をすっ飛ばしてるからだけど、今は関係ないので割愛。


物体検出(yolov7)のバウンディングボックスから奥行きの距離(depth)を計算してみた

今回はbboxのrightとleftのx座標を引き算して、その絶対値を求めて、disparityを求める。


5. 結果:bboxと計測値からdisparityとdepthの関係図

下が大まかな計測値を表にしてグラフ化したもの。
disparityとdipthは計算式から求めた。


import pandas as pd

# 表の項目 :index, right_x, right_y, right_w, right_h, left_x. left_y, left_w, left_h, disparity
columns = ['idx', 'Lx', 'Ly', 'Lw', 'Lh', 'Rx', 'Ry', 'Rw', 'Rh']
# listed は計測値のlist
df = pd.DataFrame(listed, columns=columns)


# 計算式
def cm_disparity(x, y):
    disp = abs(x-y)
    return disp

def dist_formula(disparity):
    T = 2.6 # cm
    f = 0.315 # cm
    img_element = 0.0001*2.8 # cm
    K = int(T*f/img_element) # 2925
    dist = K/disparity
    return dist 
 
# Disparityを計算
df["disparity"] = df.apply(lambda x : cm_disparity(x["Lx"], x["Rx"]), axis=1)

# dispを計算
df['distance(z)'] = df['disparity'].apply(dist_formula)


# disparity と dipthの関係図をplot

fig, ax = plt.subplots()
x = np.array(df['disparity'])
y = np.array(df['distance(z)'])
ax.plot(x, y, label="test")
ax.set_xlabel('disparity')
ax.set_ylabel('distance(z)')
plt.show()

計測値

disparityとdipthの関係図

disparityとdepthが上手く反比例してる。
物体検出のbboxの座標からでも十分正確なdepthの距離が求められることがわかった。


参考サイト

webカメラ2台で距離測定その2

Swiftでファイルをs3にuploadしてみる。


目次
1. cocoapodsをinstallして、Podfileからライブラリをinstall
2. AWSのUpload用のバケットをs3で作成
3. AppDelegate.swiftにS3のupload用のコードに貼り付け
4. SwiftからS3に画像をアップロード
5. s3で画像の確認


1. cocoapodsをinstallして、Podfileからライブラリをinstall

brewじゃなく、gemでcocoapodsをinstall。

1. install CocoaPods and setup

$ gem install cocoapods
$ pod setup

2. プロジェクト floder へ移動

3. podfile 作成
$ pod init

4. podfile 書き直す ('AWSCognito' 'AWSS3'を付け足す)
$ open Podfile

# pod 'AWSCognito' 'AWSS3' をPodfileに追記
```
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'ResNet50prj' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for ResNet50prj
  pod 'AWSCognito' # ここ
  pod 'AWSS3' # ここ
  target 'ResNet50prjTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'ResNet50prjUITests' do
    # Pods for testing
  end

end
```

5. ライブラリをinstall
$ pod install
```


2. AWSのUpload用のバケットをs3で作成

AWS > Cognito > IDプールの管理 > 新しい ID プールの作成

・IDプール名 :に任意の名前を入力
・認証されていないIDのアクセスを有効にする

でプールの作成

次に詳細で「ポリシードドキュメント」を編集

S3のPutObject(必要であればGetObjectも)の許可を追加。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",   ←ここを追加
        "s3:GetObject",   ←もしS3に保存した画像をアプリで取得したければ追加
        "mobileanalytics:PutEvents",
        "cognito-sync:*",
        "cognito-identity:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

「許可」をクリック

Amazon Cognitoで作業開始」で赤枠のコードをコピー

3. AppDelegate.swiftにS3のupload用のコードに貼り付け

XocdeのAppDelegate.swiftを開き、下の作業。
・AWSCognitoをインポート
・コピーしたコードをのdidFinishLaunchingWithOptionsの中に貼り付け

import AWSCognito
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
  let credentialsProvider = ****
  let configuration = ****
  AWSServiceManager.default().defaultServiceConfiguration = configuration
  return true
}


4. SwiftからS3に画像をアップロード

s3で「"pacifista-px3"」名前のバケットを作成。


AWSS3.swift

import SwiftUI
import AWSS3

func uploadData(data: Data)-> String{
    var results_ = ""
    
    let transferUtility = AWSS3TransferUtility.default()
    // アップロードするバケット名/アップしたいディレクトリ
    let bucket = "pacifista-px3"
    // ファイル名
    let key = "users.png"
    let contentType = "application/png"
    // アップロード中の処理
    let expression = AWSS3TransferUtilityUploadExpression()
    expression.progressBlock = {(task, progress) in
       DispatchQueue.main.async {
         // アップロード中の処理をここに書く
       }
    }
        
    // アップロード後の処理
    let completionHandler: AWSS3TransferUtilityUploadCompletionHandlerBlock?
    completionHandler = { (task, error) -> Void in
       DispatchQueue.main.async {
         if let error = error {
             fatalError(error.localizedDescription) // 失敗
             results_ = "upload falid"
         } else {
            // アップロード後の処理をここに書く
             results_ = "upload succeed"
         }
       }
     }
        
     // アップロード
     transferUtility.uploadData(
       data,
       bucket: bucket,
       key: key,
       contentType: contentType,
       expression: expression,
       completionHandler: completionHandler
     ).continueWith { (task) -> Any? in
       if let error = task.error as NSError? {
           fatalError(error.localizedDescription)
           results_ = "upload falid"
       } else {
           // アップロードが始まった時の処理をここに書く
           results_ = "upload succeed"
       }
       
       return nil
     }
    return results_
   }


ContentView.swift

struct ContentView: View {
    // config
    @State var S3result = ""

    〜〜〜〜〜〜〜
    func S3Upload(uiimage:UIImage){
        if let pngImage = uiimage.pngData() {
            S3result = uploadData(data: pngImage)
        }
    }
    // main
    var body: some View {
        VStack {
            〜〜〜〜
            Text(S3result)
                .padding()
                .font(.title)
            Button("Upload to S3"){
                S3Upload(uiimage:captureImage!)
            }
            .frame(height: 50)
            .multilineTextAlignment(.center)
   〜〜〜〜〜〜
         } 
    }
}

uploadしてみる。




成功した。

5. s3で画像の確認

バケットにちゃんとあるのがわかる


Swiftでs3に画像をuploadできた。

参考

【Xcode/Swift】CocoaPodsの使い方を徹底解説
SwiftからS3に画像をアップロードする方法

Yolov7にSinkhorn lossを使って実験してみた

OTA-loss とかOCcostとか話題になってる昨今に絡んで、「Sinkhorn」とかいう手法が気になってた。

何でもlossかなんかのmatrixを最適化する手法でSinkhorn lossとかいうのもある。Yolov7を使ってる最中だったので、なんとかYolov7にSinkhornを使ってみたかった。

なので、物体検出の最新版Yolov7にSinkhorn使って検証してみた。その備忘録。

目次
1. Sinkhornとは
2. Sinkhornと Yolov7のOTA-lossの融合
3. 検証結果


1. Sinkhornとは

Sinkhornとは二つの変数で求めたcost matrixを最小化する手法で最適化輸送問題に使われる。

簡単にどう使うかというとpredictionとground truthで求めたlossをcost matrixとして使って、Sinkhornで最適化して、lossをさらに改善しようという話。


predictionとground truthを確立分布とみなして同じに近づけるので、Kullback-Leibler Divergence lossと仕組みは近いと思う。

Sinkhornとlossの仕組み


細かい説明はした記事を参照。

OTA(Optimal Transport Assignment for Object Detection)
最適化輸送問題




Sinkhorn loss

import torch
import torch.nn as nn

# Adapted from https://github.com/gpeyre/SinkhornAutoDiff
class SinkhornDistance(nn.Module):
    def __init__(self, model, eps, max_iter, reduction='none'):
        super(SinkhornDistance, self).__init__()
        self.device = next(model.parameters()).device
        self.eps = eps
        self.max_iter = max_iter
        self.reduction = reduction

    def forward(self, cost, pred, truth):
        '''
    	We can easily see that the optimal transport corresponds to assigning each point 
    	in the support of pred(x) to the point of truth(y)
    	'''
        x, y = pred, truth
        # The Sinkhorn algorithm takes as input three variables :
        C = cost  # Wasserstein cost function
        x_points = x.shape[-2]
        y_points = y.shape[-2]
        if x.dim() == 2:
            batch_size = 1
        else:
            batch_size = x.shape[0]

        # both marginals are fixed with equal weights
        mu = torch.empty(batch_size, x_points, dtype=torch.float,
                         requires_grad=False, device=self.device).fill_(1.0 / x_points).squeeze()
        nu = torch.empty(batch_size, y_points, dtype=torch.float,
                         requires_grad=False, device=self.device).fill_(1.0 / y_points).squeeze()

        u = torch.zeros_like(mu)
        v = torch.zeros_like(nu)
        # To check if algorithm terminates because of threshold
        # or max iterations reached
        actual_nits = 0
        # Stopping criterion
        thresh = 1e-1

        # Sinkhorn iterations
        for i in range(self.max_iter):
            u1 = u  # useful to check the update
            u = self.eps * (torch.log(mu+1e-8) - torch.logsumexp(self.M(C, u, v), dim=-1)) + u
            v = self.eps * (torch.log(nu+1e-8) - torch.logsumexp(self.M(C, u, v).transpose(-2, -1), dim=-1)) + v
            err = (u - u1).abs().sum(-1).mean()

            actual_nits += 1
            if err.item() < thresh:
                break

        U, V = u, v
        # Transport plan pi = diag(a)*K*diag(b)
        pi = torch.exp(self.M(C, U, V))
        # Sinkhorn distance
        cost = torch.sum(pi * C, dim=(-2, -1))

        if self.reduction == 'mean':
            cost = cost.mean()
        elif self.reduction == 'sum':
            cost = cost.sum()

        return cost #, pi, C

    def M(self, C, u, v):
        "Modified cost for logarithmic updates"
        "$M_{ij} = (-c_{ij} + u_i + v_j) / \epsilon$"
        return (-C + u.unsqueeze(-1) + v.unsqueeze(-2)) / self.eps

    @staticmethod
    def ave(u, u1, tau):
        "Barycenter subroutine, used by kinetic acceleration through extrapolation."
        return tau * u + (1 - tau) * u1

2. Sinkhornと Yolov7のOTA-lossの融合

Yolov7のOTA-lossに Sinkhornを使ってみた。object lossとiou lossは3次元でなかったり、相性が悪かったので、class lossだけに使うことにした。

Sinkhornを使ったyolov7のOTA-loss

class ComputeLossOTA:

          if self.use_cost:
               lcls_cost = self.BCEcls(ps[:, 5:], t)
               lcls += self.sinkhorn(cost=lcls_cost.unsqueeze(2), pred=ps[:, 5:].unsqueeze(2), truth=t.unsqueeze(2))
          else:
               lcls += self.BCEcls(ps[:, 5:], t)  # BCE


3. 検証結果

普通のYolov7

confusion matrix

loss curve

P_curve

F1 curve


Sinkhornを使ったYolov7

confusion matrix

loss curve

P_curve

F1 curve

うまい具合にいってるけど、全体で的に見るとSinkhornに入れる引数のshapeが3次元の必要性から、使うドメインは限られてくる。

今回はclass lossにしか使ってなかったから、object lossとiou lossに使えばもっと改善する可能性もある。

少なくともlossの改善には悪い影響は与えないんじゃないかと思う。

参照記事

yolov7
SinkHorn

SwiftのSwiftUI App形式で「SceneDelegate」とかを使ったカメラアプリ作成ログ

機械学習のcoreML用にswiftを使ったカメラアプリを作ってみた。その時、

・従来のAppKitApp Delegate形式
から
・新しい記述形式のSwiftUI App形式

を主にした
・SceneDelegate.swift
・AppDelegate.swift

を使ったカメラアプリの作成して使う方法を簡単にまとめる

目次
1.ファイル構成
2.Info.plistの設定
3.ファイル一覧




1.ファイル構成

メインのファイル構成

$ tree ResNet50prj
├── Interactor.swift
├── CameraController.swift
├── CameraViewController.swift
├── ContentView.swift
├── Info.plist
├── AppDelegate.swift
└── SceneDelegate.swift

Info.plistの設定


・カメラのアクセス許可
・SceneDelegateとAppDeletegateを使った形式でアプリを使うためにApplication Scene Manifestを追加


以下のように設定

ファイル一覧

AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }
}

SceneDelegate.swift

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }
}

ContentView.swift

import SwiftUI
struct ContentView: View {
    var body: some View {
        CameraViewController()
            .edgesIgnoringSafeArea(.top)
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Interactor.swift

import Foundation
import AVKit

final class SimpleVideoCaptureInteractor: NSObject, ObservableObject {
    private let captureSession = AVCaptureSession()
    @Published var previewLayer: AVCaptureVideoPreviewLayer?
    private var captureDevice: AVCaptureDevice?

    /// - Tag: CreateCaptureSession
     func setupAVCaptureSession() {
         print(#function)
         captureSession.sessionPreset = .photo
         if let availableDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first {
             captureDevice = availableDevice
         }

         do {
             let captureDeviceInput = try AVCaptureDeviceInput(device: captureDevice!)
             captureSession.addInput(captureDeviceInput)
         } catch let error {
             print(error.localizedDescription)
         }

         let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.name = "CameraPreview"
        previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        previewLayer.backgroundColor = UIColor.black.cgColor
         self.previewLayer = previewLayer

         let dataOutput = AVCaptureVideoDataOutput()
         dataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String:kCVPixelFormatType_32BGRA]

         if captureSession.canAddOutput(dataOutput) {
             captureSession.addOutput(dataOutput)
         }
         captureSession.commitConfiguration()
     }

    func startSettion() {
        if captureSession.isRunning { return }
        captureSession.startRunning()
    }

    func stopSettion() {
        if !captureSession.isRunning { return }
        captureSession.stopRunning()
    }
}

CameraController.swift

import UIKit
import AVFoundation

class CameraController: NSObject{
    var captureSession: AVCaptureSession?
    var backCamera: AVCaptureDevice?
    var backCameraInput: AVCaptureDeviceInput?
    var previewLayer: AVCaptureVideoPreviewLayer?
    
    enum CameraControllerError: Swift.Error {
        case captureSessionAlreadyRunning
        case captureSessionIsMissing
        case inputsAreInvalid
        case invalidOperation
        case noCamerasAvailable
        case unknown
    }
    
    func prepare(completionHandler: @escaping (Error?) -> Void){
        func createCaptureSession(){
            self.captureSession = AVCaptureSession()
        }
        func configureCaptureDevices() throws {
            let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .back)
            
            self.backCamera = camera
            
            try camera?.lockForConfiguration()
            camera?.unlockForConfiguration()
            
        }
        func configureDeviceInputs() throws {
            guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
            
            if let backCamera = self.backCamera {
                self.backCameraInput = try AVCaptureDeviceInput(device: backCamera)
                
                if captureSession.canAddInput(self.backCameraInput!) { captureSession.addInput(self.backCameraInput!)}
                else { throw CameraControllerError.inputsAreInvalid }
                
            }
            else { throw CameraControllerError.noCamerasAvailable }
            
            captureSession.startRunning()
            
        }
        
        DispatchQueue(label: "prepare").async {
            do {
                createCaptureSession()
                try configureCaptureDevices()
                try configureDeviceInputs()
            }
                
            catch {
                DispatchQueue.main.async{
                    completionHandler(error)
                }
                
                return
            }
            
            DispatchQueue.main.async {
                completionHandler(nil)
            }
        }
    }
    func displayPreview(on view: UIView) throws {
        guard let captureSession = self.captureSession, captureSession.isRunning else { throw CameraControllerError.captureSessionIsMissing }
        
        self.previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        self.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
        self.previewLayer?.connection?.videoOrientation = .portrait
        
        view.layer.insertSublayer(self.previewLayer!, at: 0)
        self.previewLayer?.frame = view.frame
    }   
}

CameraViewController.swift

import UIKit
import SwiftUI

final class CameraViewController: UIViewController {
    let cameraController = CameraController()
    var previewView: UIView!
    
    override func viewDidLoad() {
                
        previewView = UIView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
        previewView.contentMode = UIView.ContentMode.scaleAspectFit
        view.addSubview(previewView)
        
        cameraController.prepare {(error) in
            if let error = error {
                print(error)
            }
            
            try? self.cameraController.displayPreview(on: self.previewView)
        }
        
    }
}

extension CameraViewController : UIViewControllerRepresentable{
    public typealias UIViewControllerType = CameraViewController
    
    public func makeUIViewController(context: UIViewControllerRepresentableContext<CameraViewController>) -> CameraViewController {
        return CameraViewController()
    }
    
    public func updateUIViewController(_ uiViewController: CameraViewController, context: UIViewControllerRepresentableContext<CameraViewController>) {
    }
}

参考サイト

SwiftUI-Simple-camera-app
今までStoryboardしか使っていないプロジェクトで、SwiftUIをとりあえず表示させるまで。

1次元のsignal(信号)データの異常検知(信号処理)備忘録

信号データ(1次元の波形データ)を使った「傷あり/なし」の異常検知モデルを作ったので、役に立ったこと、たたなかったこと、とかを備忘録でまとめてく。

本来のタスクとちょっとルールを変えて、時系列データ自体が異常かどうかの2値分類タスクにした。

目次
1. データについて
2. 1dCNNとLSTMの組み合わせ
3. MultiHead-AttentionとMLP-Blockの活用
4. 結果
5. その他の使えるTips



1. データについて

データの異常・正常のパターンは公開できないけど、少し異常データの方が荒い感じになってる。

異常データが傷がありを示す異常データ。


前処理で異常データの傷の部分を際立つようにした。

1dCNNのインプット形式は、
(batch_size, time_length, chennel)


・前処理前のShape :(batch, 8, 50000~60000)

・前処理後のshape : (batch, 3, 30000)

シンプルにXとY成分を2乗してルート取ったり、位相を強調したり、データの特徴に合わせた前処理をした。

正常データ


異常データ

また今回は高周波数でもないし、ノイズは差分で取り除けたので、

・バンドパスフィルタ(ハイパス、ローパス)
フーリエ変換(fftとか)


は必要ないand 役にたたなかった。

またスペクトル変換してConv2dで処理する方法もあったけど、1データにつき波形が8こ含まれてるため、異常の傷を強調して1dCNNを使った。

バンドパスフィルター

def apply_bandpass(x, lf=1, hf=100, order=16, sr=30000):
    sos = signal.butter(order, [lf, hf], btype="bandpass", output="sos", fs=sr)
    normalization = np.sqrt((hf - lf) / (sr / 2))
    x = signal.sosfiltfilt(sos, x) / normalization
    return x

2. 1dCNNとLSTMの組み合わせ

傷を強調するように前処理して、1次元の波形を1dCNNで局所部分の特徴を抽出。そのあとにシーケンスデータとしてそのままLSTMに入れた。

def cnn_embed(self, x):
        ss = []
        xs = x.permute(0, 2, 1)
        for i in range(self.tstep):
            xs_ = xs[:, i, :].unsqueeze(1)
            fts = self.cnn1d(xs_)
            ss.append(fts)
        x = torch.cat(ss, dim=1)
        # Standardization
        std, mean = torch.std_mean(x, dim=(1,2), unbiased=False, keepdim=True)
        x = torch.div(x-mean, std)
        x = self.lstm(x)
        return x

1dCNNの出力を標準化するのはデータを整える意味で強力。

1dCNNの出力をLSTMに使うのは、

・細かい箇所を抽出できる。
・シーケンス長データの時系列の特徴を抽出できる。

のコンボが強力だと思う。

また1dCNNでpooling層を使うとエイリアシングが起こるらしいけど、今回のデータは周波数変換してないので、pooling層はあっても問題なし。

スペクトル変換して周波数にしてconv1dに入れるなら、pooling層のエイリアシング対策は必要。



3. MultiHead-AttentionとMLP-Blockの活用

Vit(Vision Transformer)を真似して、

・MultiHead-Attention
MLP

を使った、Transformerの亜種を作った。

特にnn.LayerNormとLSTMは相性がすごいよかった。

class CNN1d_Transformer(nn.Module):
    def __init__(self, tstep=4, embed_dim=256, hidden_dim=256):
        super(CNN1d_Transformer,self).__init__()
        self.tstep = tstep
        # Encoder
        self.cnn1d = CNN1d(embed_dim)
        self.lstm = LSTMModel(466, hidden_dim)
        # Decoder
        self.self_attention = MultiheadSelfAttention(hidden_dim*2, hidden_dim*2, heads=4)
        self.norm_layer = nn.LayerNorm([768, hidden_dim*2])
        self.dropout = nn.Dropout(p=0.2)
        self.mlp = SelfAttention(hidden_dim*2, hidden_dim*2)
        
    def transfomer(self, encout, attention_is=False):
        ## attention block
        x = self.norm_layer(encout)
        #if attention_is:
        x = self.self_attention(x)
        x = self.dropout(x)
        x = x + encout
        ## mlp block
        y = self.norm_layer(x)
        return y


LSTM出力のembed featureをMlutiHead-AttentionとMLPとかのレイヤーに通すことで、いろんな特徴量を得られる。

これらを足し算することで、精度向上や汎化性能、valid loss の改善に一役買ったてくれた。


TransFormerとかMetaFormer

そういえばDeeplabv3+でもASPPとかいうのがあったので、やってることは同じ感じ
ASPP

ASPP


4. 結果

87.5%

loss


5. その他の使えるTips

・kaggleコンペで重力波のコンペがあったのでかなり1dCNNと前処理の方は参考になった。

・1dCNNでフーリエ変換した周波数データはpooling層による、エイリアシングに要注意。

ニューラルネット中の標準化はデータを整える意味で強力

・augumatationはtimeshiftやノイズ、リサンプルのサイズ変換

1dCNNでエイリアシングを防止対策


また、DTWとか信号処理や音声とかの1次元の手法は画像分野でも使えるので結構面白かった。

やっぱりこんぺの勝ち負けは順位で搾取されるんじゃなくて、どれだけ参加者本人が楽しんで技術アイデアを得るとかなんだよなと常々思った。


参考サイト

https://www.kaggle.com/code/gyozzza/g2net-1dcnn-transformer

https://github.com/pytorch/vision/blob/main/torchvision/models/vision_transformer.py

what you get after exploring what you want

そんな中でも
10~20人程度の小規模な会社の場合
新規顧客の対応を社長さんが
してくれる場合がある。

昨日初めて訪問した会社でも
社長さんが応対をされていた。

新規で導入を考えている加工機を
製造しているメーカーさんだったので
社内にあるデモ機や実際に出荷する前の
機械も見させてもらい、
実際の加工テストまで対応いただいた。

百聞は一見に如かずと言うが
まさにその言葉を体現するような
良い出張になった。

そんなテストが終了したあとに工場内を
見ているとデモ機の奥に
バイクが置かれているのをふと見つけた。

「あのバイクは何なんですか?」

ふと尋ねると、先方からは
「よくぞ聞いてくれた」というオーラが
既に漂っていた。

どうやら社長の趣味でしているバイクらしい。

そこからバイクの面白さ、
モータースポーツの面白さを
語りだした社長の様子は
まさに子供のようであった。

バイクで転倒してあばら骨を5本折ったが
それでもバイクに乗り続けた話や
レースのために日本のあらゆるところまで
行ったお話を聞いていると
「好き」を通り越しているとすら
感じられるほどであった。

そして、そのお話を聞いていて
同時に感じたことであるが、
このような企業を経営する社長の方は
絶対的な趣味を持っている人が
多いということであった。

ゴルフが3度の食事よりも好きで
グリップを握って夜も寝ている社長。

日本刀の収集が好きすぎて
ギャラリーのような部屋を作った社長

どの人も一般的な趣味というレベルを
明らかに超えるハマり方をしている。

これは一体なぜだろうか。

それは趣味を極めることは
ビジネスをすることに似ているからでは
ないだろうか。

趣味と一言で言っても色んなものがある。
そして、その深さにも色んな程度があるが
深く掘れば掘るほど難しさが
出てくるものである。

ゴルフであれば100を切るぐらいの
スコアで回れるようになるのは
一定の練習量があればかなうかもしれないが
80を切って回るような技術は
単に練習量を増やすだけでは
得ることはできないものである。

そこには抑えるべきポイントが必ずあり
それを的確に押さえながら
圧倒的な練習量をこなす必要がある。

ビジネスにおいてもこれは同じである。

参入障壁の低い比較的簡単な技術は
競合他社にもマネやすいし、
価格競争にすぐに陥ってしまう。

しかし、一見簡単に見える技術でも
独自のノウハウを蓄積して得られた
特殊な加工技術に対しては
ニーズはそれほど多くなくとも
高い加工費を出してでも依頼したい人は
いるものなのだ。

そんな独自のノウハウを蓄積した
特殊技術を得ることはまさに
趣味を極めることと同じである。

実際私がお会いしてきた趣味を極めた
社長の方々はご自身が技術者として
仕事に関わられている場合がほどんどであった。

よく巷では「好きを仕事にする」という
言葉が言われているが、
それは趣味を仕事にするという意味だけではない。

好きな趣味を極めるような気持ちで
仕事をすることでもあるのだ。

趣味を極めているこれらの社長の方々は
全員そろってギラギラとしており
活力にあふれている。

これも仕事を好きな趣味のように
しているからに他ならない。

趣味にそんなにのめり込もうとすると
お金も時間もかかるし、
僕ら一般人には無理。

そんな風に思う気持ちはよくわかるが
自分が好きな趣味にすら
お金をかけられない人が
ビジネスで大切な部分に投資は
できないものである。

一見仕事とは関係ないように
見えてしまうが、
趣味を極めることはビジネスで
成功するためには大切なことで
あるのではないだろうか。

あなたは何か趣味があるだろうか。