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

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

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を切って回るような技術は
単に練習量を増やすだけでは
得ることはできないものである。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

EfficientNet-v2のMBConvライクなレイヤーをtensorflowで自作して精度を検証してみた

EfficientNet-v2が優秀なので、その技術を自分の自作ネットワークに応用して作ってみた。
その結果だけを書いてく備忘録。

目次
1. EfficentNetv2の特徴
2. 今回応用した技術
3. 精度(arcfaceあり)


1. EfficentNetv2の特徴

・MBConvとFused-MBconv(DepthwiseConvの代わりに1×1と3×3のConvを使う)を組み合わせた計算処理の高速化

NASで精度、パラメータ、計算時間を最適にする組み合わせの探索(だから高速で高性能)


・MBconvの中にDepthWiseConv2dを入れることで計算量を削減


・SCSE layerを使ってる

・shortcut=Trueでないブロックごとの最初のレイヤーはstride=2, 他はstride=1

2. 今回応用した技術


・Fused-MBConvの代わりに普通のResblockの亜種をパラメータを同じにして使うことによって、MBConvとFused-MBConvの組み合わせを擬似的に再現して計算速度を上げた。また軽いので層を重ねてもモデルのパラメータが軽量で済む。

・DepthWiseConv2dを全部のLayerの中央に入れて、計算量を削減。

・Conv2dの「kernel_initializer」やstridesなどMBConv特有のパラメータをまねた

・SCSE layerの適用

・Conv2d=> BN=> Activaionはやっぱり定石

深さは従来通り、深さごとにblockのoutputを2倍にしたのでEfficientNet-v2のようにLayerを多用できないけど、MBConvとFused-MBconvをほぼ再現できた感じがする。

[16, 32, 64, 128, 214, 512]で深くした。

NASのパラメータ探索は転用が難しいので使わなかった。


応用したLayer

import tensorflow as tf 
from tensorflow.keras.layers import *
import numpy as np

CONV_KERNEL_INITIALIZER = tf.keras.initializers.VarianceScaling(scale=2.0, mode="fan_out", distribution="truncated_normal")
channel_axis = -1


def fused_like_MBconv(init, nb_filter, k):
    residual = Conv2D(nb_filter * k, (1, 1), strides=(2, 2), padding='same', use_bias=False, kernel_initializer=CONV_KERNEL_INITIALIZER)(init)
    residual = BatchNormalization(axis=-1, momentum=0.9, epsilon=0.001)(residual)
    x = Conv2D(nb_filter * k, (3, 3), padding='same', use_bias=False, kernel_initializer=CONV_KERNEL_INITIALIZER)(init)
    x = BatchNormalization(axis=channel_axis, momentum=0.9, epsilon=0.001)(x)
    x = Activation("swish")(x)
    x = Dropout(0.4)(x)
    x = DepthwiseConv2D(3, padding="same", strides=1, use_bias=False, depthwise_initializer=CONV_KERNEL_INITIALIZER)(x)
    x = BatchNormalization(axis=channel_axis, momentum=0.9, epsilon=0.001)(x)
    x = Activation("swish")(x)
    x = Conv2D(nb_filter * k, (3, 3), padding='same', use_bias=False, kernel_initializer=CONV_KERNEL_INITIALIZER)(x)
    x = BatchNormalization(axis=channel_axis, momentum=0.9, epsilon=0.001)(x)
    x = MaxPooling2D((3, 3), strides=(2, 2), padding='same')(x)
    x = Add()([x, residual])
    return x

def mbconv_block(init, nb_filter, k=1):
    x = Conv2D(nb_filter * k, (1, 1), strides=(1, 1), padding='valid', use_bias=False, kernel_initializer=CONV_KERNEL_INITIALIZER)(init)
    x = BatchNormalization(axis=channel_axis, momentum=0.9, epsilon=0.001)(x)
    x = Activation("swish")(x)
    x = DepthwiseConv2D(3, padding="same", strides=1, use_bias=False, depthwise_initializer=CONV_KERNEL_INITIALIZER)(x)
    x = BatchNormalization(axis=channel_axis, momentum=0.9, epsilon=0.001)(x)
    x = Activation("swish")(x)
    x = scse(x)
    x = Conv2D(nb_filter * k, (1, 1), strides=(1, 1), padding='valid', use_bias=False, kernel_initializer=CONV_KERNEL_INITIALIZER)(x)
    x = BatchNormalization(axis=channel_axis, momentum=0.9, epsilon=0.001)(x)
    x = Dropout(0.2, noise_shape=(None, 1, 1, 1))(x)
    x = Add()([init, x])
    return x

def scse(input_x, se_ratio=4):
    h_axis, w_axis = [1, 2]
    filters = input_x.shape[channel_axis]
    reduction = filters // se_ratio
    # se = GlobalAveragePooling2D()(inputs)
    # se = Reshape((1, 1, filters))(se)
    se = tf.reduce_mean(input_x, [h_axis, w_axis], keepdims=True)
    se = Conv2D(reduction, kernel_size=1, use_bias=True, kernel_initializer=CONV_KERNEL_INITIALIZER)(se)
    # se = PReLU(shared_axes=[1, 2])(se)
    se = Activation("swish")(se)
    se = Conv2D(filters, kernel_size=1, use_bias=True, kernel_initializer=CONV_KERNEL_INITIALIZER)(se)
    se = Activation("sigmoid")(se)
    return Multiply()([input_x, se])
# when to use in Network
x = fused_like_MBconv(x1, nb_filter[i], k)
x = mbconv_block(x, k, nb_filter[i])
x = mbconv_block(x, k, nb_filter[i])


SCSE layerはそのまま、EfficientNet-v2のを使った。

ネットワークはちょっと公開できない。

3.精度(arcfaceあり)

122 labelの画像分類課題で、さらに11種のcolorのメタ情報を画像から分類するSIGNATEのコンペをタスクにして、11 labelの分類問題を解かせてみた。

データリークありや特定のラベルのデータがない上に、職人じゃなければ、普通の人間が画像をみても何が色の基準を表してるのかほぼ判別不可能なハードル高めな分類問題をend2endで解かせてみる。

難易度が高いので、arcfaceを使ってる。なので普通に解くよりも精度はよくなってる。


色の判別表

普通のEfficientNet-V2 (B2でpretrainなし)の精度(arcfaceあり)

  
Total params12,925,406
Trainable params 12,839,598
Non-trainable params 85,808
精度 88.02%


自作Layer付きCNNの判別精度(arcfaceあり)

  
Total params13,009,536
Trainable params 12,985,024
Non-trainable params 24,512
精度 86.21%


NASのパラメータ探索をしてないけど、かなり近い精度まで持ってけた上、他の試作Layerの中で一番精度が良かった。
NASの探索パラメータの、組み合わせは他に転用がしづらいので使わなかった。

blackごとにoutputを2倍にしていく手法は変わらないし、Layerの多重重ね技もできないけど、転用可能な優秀なレイヤーができた。



参考

2021年最強になるか!?最新の画像認識モデルEfficientNetV2を解説

MobileNet(v1,v2,v3)を簡単に解説してみた

Analysis of CNN+LSTM with Attentions

I created CNN+LSTM model that has Attention layers in LSTM side.

CNN is role of encoder and LSTM is one of decoder. 

Here, I'll write this as log of my analysis about how to use CNN output as LSTM input, and Attentions that are 'Self-Attenttion' and 'Source-Target Attention'.

This is just like my dialy, thus it's simple and short.

Contents
1.How to use CNN output as LSAM input
2.Analysis result of Attention
3.Self-Attention
4.Source-Target Attention
5.Summary


1.How to use CNN output as LSTM input

This time, input data are images and aim is meta label classification such as image colors and shape types.

First, input image to CNN-encoder and use its output to LSTM input.

CNN+LSTM Overall 


CNN output and LSTM input part 

"""params"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
LSTM_UNITS = 200
embed_size = 1024
H = W = 224
in_size = 3
input_size = (1, in_size, H, W)
inp1 = torch.rand(input_size, dtype=torch.float32).to(device)

"""CNN encoder"""
avgpool = nn.AdaptiveAvgPool2d(1)
lstm = nn.LSTM(embed_size, LSTM_UNITS, bidirectional=True, batch_first=True)
x = self.cnn(x)
print(x.shape)
# torch.Size([1, 512, 14, 14])
x = avgpool(x)
# torch.Size([1, 512, 1, 1])
x = torch.reshape(x, (1, embed_size, 1, 1))
b,f,_,_ = x.shape
embedding = x.reshape(1,b,f) 
print(embedding.shape)
#torch.Size([1, 1, 1024])


"""LSTM decoder """
lstm.flatten_parameters()
h_lstm1, (hidden1, cell1) = lstm1(embedding)
print(h_lstm1.shape, hidden1.shape)
#torch.Size([1, 1, 400]) torch.Size([2, 1, 200])


2.Analysis result of Attention

Attention is divided to Self-Attention and Source-Target Attention.

Self-Attention has only 1 input and can be used for many filed and easy to customized.

Source-Target Attention has 2 input that's input and memory(input is as query, memory is keys and value).
It is mainly for sequence data and text emmbadding vector, as mainly used at after LSTM output. 


This time, I analize and used the 2 Attentions to LSTM.

3.Self-Attention

Self-Attention is mainly for NLP and use for Network mainly like LSTM, seq2seq and Transformer.
But recent day, as improved ML, Attention layers is used to CNN (image classification).

I'll use Attention layer for CNN another time, and this time I used for LSTM for improving accuracy.


Self-Attention stracture

class SelfAttention(nn.Module):
    def __init__(self, lstm_dim):
        super(SelfAttention, self).__init__()
        self.lstm_dim = lstm_dim *2
        self.attn_weight = nn.Sequential(
            nn.Linear(lstm_dim *2, lstm_dim *2), 
            nn.Tanh(),
            nn.Linear(lstm_dim *2, lstm_dim *2)
        )
    def forward(self, lstm_output):
        attn_weight = self.attn_weight(lstm_output)
        attn_weight = F.softmax(attn_weight, dim=2)
        feats = torch.cat([lstm_output, attn_weight], 2)
        return feats.squeeze(0)

LSTM_UNITS = 200
num_cls = 12
input_size = (1, 1, 400)

# function and class
selfattension = SelfAttention(LSTM_UNITS)
attention_linear = nn.Linear(LSTM_UNITS*4, num_cls)
final_activate = nn.Softmax(dim=1)

# inputs
hidden = torch.rand(input_size, dtype=torch.float32).to(device)
print(hidden.shape) # torch.Size([1, 1, 400])


# in LSTM, after "hidden = h_lstm1 + h_lstm2"
hidden = selfattension(hidden)
print(hidden.shape) # torch.Size([1, 800])
output = attention_linear(hidden)
output = final_activate(output)
print(output.shape) # torch.Size([1, 12])


In LSTM, Self-Attention is mainly placed around LSTM's final layer.
I reffed Kaggle sample use and PyTorch github.


4.Source-Target Attention

This time, I used this at after LSTM's output.
In Transformer, use many "Source-Target Attention" and it called as "Multi-head Attention"

"Source-Target Attention" is harder to customize more than 'Self-Attenttion'. but it depends on technique and idea.(up to you)

"Source-Target Attention" structure

def source_target_attention(lstm_output, final_state):
    lstm_output = lstm_output.permute(1, 0, 2) # keys
    querys = final_state.squeeze(0) # query
    logits = torch.bmm(lstm_output, querys.unsqueeze(2)).squeeze(2)
    attn_weights = F.softmax(logits, dim=1)
    new_hidden_state = torch.bmm(lstm_output.transpose(1, 2), # value
                                 attn_weights.unsqueeze(2)).squeeze(2)
    return new_hidden_state

# LSTM outputs
lstm_output = torch.rand((1, 1, 400), dtype=torch.float32)
hidden1 = torch.rand((2, 1, 200), dtype=torch.float32)

# in LSTM, #h_lstm1, (hidden1, cell1) = self.lstm1(embedding)
hidden1 = hidden1.reshape(1, 1, 400)
lstm_output = source_target_attention(lstm_output, hidden1).unsqueeze(0)
print(lstm_output.shape)
# torch.Size([1, 1, 400])


5.Summary

this time I realized that CNN-LSTM with Attention is good for image label classification not only for NLP task.

good accuracy is due to coverting image to sequence data.(3d to 1d).
I roughly draw entire this time network image which show where I set the 2 Attentions as follows


and this time better loss and activation are

This time
Loss : nn.CrossEntropyLoss()
Final activationnn.Softmax(dim=1)


task was to classify color label in 11 colors and shape label in 2 shape type.(meta data label).

if you use this for NLP task, loss and final activation should be as follows.

NLP task
Loss : nn.NLLLoss()
Final activation :nn.LogSoftmax(dim=1)

Accuracy is almost as good as I expected. If I find better architecture, will improve more.


Reference site

Attention is all you need(jp)
self attention 】簡単に予測理由を可視化できる文書分類モデルを実装する

ROSでJetson nanoで撮影したカメラ画像を遠隔でホストPCのブラウザに表示する

Jetson Nanoで以前USBカメラを接続した。

trafalbad.hatenadiary.jp


今回VNC接続した身軽な状態のjetson nanoで撮影したカメラ画像を、遠隔でホストPCのGoogle chrome (Safari)で表示してみた。
その備忘録


目次
1. Jetson Nanoでブラウザに表示するための準備
2. 遠隔のホストPCのブラウザにカメラ画像を表示

1.Jetson Nanoでブラウザに表示するための準備

# パッケージの作成
cd ~/catkin_ws/src
catkin_create_pkg video_stream roscpp std_msgs rosilb image_transport cv_bridge
# CmakeList.txtとcppのスクリプト書き換え
cd ~/catkin_ws
catkin_make
source ~/.bashrc


以下のcppスクリプト書き換え

video_stream.cpp

#include <ros_video_stream/ros_video_stream.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <syslog.h>
#include <netdb.h>
#include <errno.h>
#include <opencv2/opencv.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>

#define CV_IMWRITE_JPEG_QUALITY 10

template<typename T>
  inline T ABS(T a)
  {
    return (a < 0 ? -a : a);
  }

template<typename T>
  inline T min(T a, T b)
  {
    return (a < b ? a : b);
  }

template<typename T>
  inline T max(T a, T b)
  {
    return (a > b ? a : b);
  }

template<typename T>
  inline T LENGTH_OF(T x)
  {
    return (sizeof(x) / sizeof(x[0]));
  }

namespace mjpeg_server
{

MJPEGServer::MJPEGServer(ros::NodeHandle& node) :
    node_(node), image_transport_(node), stop_requested_(false), www_folder_(NULL)
{
  ros::NodeHandle private_nh("~");
  private_nh.param("port", port_, 8080);
  header = "Connection: close\r\nServer: mjpeg_server\r\n"
      "Cache-Control: no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n"
      "Pragma: no-cache\r\n";
  sd_len = 0;
}

MJPEGServer::~MJPEGServer()
{
  cleanUp();
}

void MJPEGServer::imageCallback(const sensor_msgs::ImageConstPtr& msg, const std::string& topic)
{

  ImageBuffer* image_buffer = getImageBuffer(topic);
  boost::unique_lock<boost::mutex> lock(image_buffer->mutex_);
  // copy image
  image_buffer->msg = *msg;
  // notify senders
  image_buffer->condition_.notify_all();
}

void MJPEGServer::splitString(const std::string& str, std::vector<std::string>& tokens, const std::string& delimiter)
{
  // Skip delimiters at beginning.
  std::string::size_type lastPos = str.find_first_not_of(delimiter, 0);
  // Find first "non-delimiter".
  std::string::size_type pos = str.find_first_of(delimiter, lastPos);

  while (std::string::npos != pos || std::string::npos != lastPos)
  {
    // Found a token, add it to the vector.
    tokens.push_back(str.substr(lastPos, pos - lastPos));
    // Skip delimiters.  Note the "not_of"
    lastPos = str.find_first_not_of(delimiter, pos);
    // Find next "non-delimiter"
    pos = str.find_first_of(delimiter, lastPos);
  }
}

int MJPEGServer::stringToInt(const std::string& str, const int default_value)
{
  int value;
  int res;
  if (str.length() == 0)
    return default_value;
  res = sscanf(str.c_str(), "%i", &value);
  if (res == 1)
    return value;
  return default_value;
}

void MJPEGServer::initIOBuffer(iobuffer *iobuf)
{
  memset(iobuf->buffer, 0, sizeof(iobuf->buffer));
  iobuf->level = 0;
}

void MJPEGServer::initRequest(request *req)
{
  req->type = A_UNKNOWN;
  req->type = A_UNKNOWN;
  req->parameter = NULL;
  req->client = NULL;
  req->credentials = NULL;
}

void MJPEGServer::freeRequest(request *req)
{
  if (req->parameter != NULL)
    free(req->parameter);
  if (req->client != NULL)
    free(req->client);
  if (req->credentials != NULL)
    free(req->credentials);
}

int MJPEGServer::readWithTimeout(int fd, iobuffer *iobuf, char *buffer, size_t len, int timeout)
{
  size_t copied = 0;
  int rc, i;
  fd_set fds;
  struct timeval tv;

  memset(buffer, 0, len);

  while ((copied < len))
  {
    i = min((size_t)iobuf->level, len - copied);
    memcpy(buffer + copied, iobuf->buffer + IO_BUFFER - iobuf->level, i);

    iobuf->level -= i;
    copied += i;
    if (copied >= len)
      return copied;

    /* select will return in case of timeout or new data arrived */
    tv.tv_sec = timeout;
    tv.tv_usec = 0;
    FD_ZERO(&fds);
    FD_SET(fd, &fds);
    if ((rc = select(fd + 1, &fds, NULL, NULL, &tv)) <= 0)
    {
      if (rc < 0)
        exit(EXIT_FAILURE);

      /* this must be a timeout */
      return copied;
    }

    initIOBuffer(iobuf);

    /*
     * there should be at least one byte, because select signalled it.
     * But: It may happen (very seldomly), that the socket gets closed remotly between
     * the select() and the following read. That is the reason for not relying
     * on reading at least one byte.
     */
    if ((iobuf->level = read(fd, &iobuf->buffer, IO_BUFFER)) <= 0)
    {
      /* an error occured */
      return -1;
    }

    /* align data to the end of the buffer if less than IO_BUFFER bytes were read */
    memmove(iobuf->buffer + (IO_BUFFER - iobuf->level), iobuf->buffer, iobuf->level);
  }

  return 0;
}

int MJPEGServer::readLineWithTimeout(int fd, iobuffer *iobuf, char *buffer, size_t len, int timeout)
{
  char c = '\0', *out = buffer;
  unsigned int i;

  memset(buffer, 0, len);

  for (i = 0; i < len && c != '\n'; i++)
  {
    if (readWithTimeout(fd, iobuf, &c, 1, timeout) <= 0)
    {
      /* timeout or error occured */
      return -1;
    }
    *out++ = c;
  }

  return i;
}

void MJPEGServer::decodeBase64(char *data)
{
  union
  {
    int i;
    char c[4];
  } buffer;

  char* ptr = data;
  unsigned int size = strlen(data);
  char* temp = new char[size];
  char* tempptr = temp;
  char t;

  for (buffer.i = 0, t = *ptr; ptr; ptr++)
  {
    if (t >= 'A' && t <= 'Z')
      t = t - 'A';
    else if (t >= 'a' && t <= 'z')
      t = t - 'a' + 26;
    else if (t >= '0' && t <= '9')
      t = t - '0' + 52;
    else if (t == '+')
      t = 62;
    else if (t == '/')
      t = 63;
    else
      continue;

    buffer.i = (buffer.i << 6) | t;

    if ((ptr - data + 1) % 4)
    {
      *tempptr++ = buffer.c[2];
      *tempptr++ = buffer.c[1];
      *tempptr++ = buffer.c[0];
      buffer.i = 0;
    }
  }
  *tempptr = '\0';
  strcpy(data, temp);
  delete temp;
}

int MJPEGServer::hexCharToInt(char in)
{
  if (in >= '0' && in <= '9')
    return in - '0';

  if (in >= 'a' && in <= 'f')
    return (in - 'a') + 10;

  if (in >= 'A' && in <= 'F')
    return (in - 'A') + 10;

  return -1;
}

int MJPEGServer::unescape(char *string)
{
  char *source = string, *destination = string;
  int src, dst, length = strlen(string), rc;

  /* iterate over the string */
  for (dst = 0, src = 0; src < length; src++)
  {

    /* is it an escape character? */
    if (source[src] != '%')
    {
      /* no, so just go to the next character */
      destination[dst] = source[src];
      dst++;
      continue;
    }

    /* yes, it is an escaped character */

    /* check if there are enough characters */
    if (src + 2 > length)
    {
      return -1;
      break;
    }

    /* perform replacement of %## with the corresponding character */
    if ((rc = hexCharToInt(source[src + 1])) == -1)
      return -1;
    destination[dst] = rc * 16;
    if ((rc = hexCharToInt(source[src + 2])) == -1)
      return -1;
    destination[dst] += rc;

    /* advance pointers, here is the reason why the resulting string is shorter */
    dst++;
    src += 2;
  }

  /* ensure the string is properly finished with a null-character */
  destination[dst] = '\0';

  return 0;
}

void MJPEGServer::sendError(int fd, int which, const char *message)
{
  char buffer[BUFFER_SIZE] = {0};

  if (which == 401)
  {
    sprintf(buffer, "HTTP/1.0 401 Unauthorized\r\n"
            "Content-type: text/plain\r\n"
            "%s"
            "WWW-Authenticate: Basic realm=\"MJPG-Streamer\"\r\n"
            "\r\n"
            "401: Not Authenticated!\r\n"
            "%s",
            header.c_str(), message);
  }
  else if (which == 404)
  {
    sprintf(buffer, "HTTP/1.0 404 Not Found\r\n"
            "Content-type: text/plain\r\n"
            "%s"
            "\r\n"
            "404: Not Found!\r\n"
            "%s",
            header.c_str(), message);
  }
  else if (which == 500)
  {
    sprintf(buffer, "HTTP/1.0 500 Internal Server Error\r\n"
            "Content-type: text/plain\r\n"
            "%s"
            "\r\n"
            "500: Internal Server Error!\r\n"
            "%s",
            header.c_str(), message);
  }
  else if (which == 400)
  {
    sprintf(buffer, "HTTP/1.0 400 Bad Request\r\n"
            "Content-type: text/plain\r\n"
            "%s"
            "\r\n"
            "400: Not Found!\r\n"
            "%s",
            header.c_str(), message);
  }
  else
  {
    sprintf(buffer, "HTTP/1.0 501 Not Implemented\r\n"
            "Content-type: text/plain\r\n"
            "%s"
            "\r\n"
            "501: Not Implemented!\r\n"
            "%s",
            header.c_str(), message);
  }

  if (write(fd, buffer, strlen(buffer)) < 0)
  {
    ROS_DEBUG("write failed, done anyway");
  }
}

void MJPEGServer::decodeParameter(const std::string& parameter, ParameterMap& parameter_map)
{
  std::vector<std::string> parameter_value_pairs;
  splitString(parameter, parameter_value_pairs, "?&");

  for (size_t i = 0; i < parameter_value_pairs.size(); i++)
  {
    std::vector<std::string> parameter_value;
    splitString(parameter_value_pairs[i], parameter_value, "=");
    if (parameter_value.size() == 1)
    {
      parameter_map.insert(std::make_pair(parameter_value[0], std::string("")));
    }
    else if (parameter_value.size() == 2)
    {
      parameter_map.insert(std::make_pair(parameter_value[0], parameter_value[1]));
    }
  }
}

ImageBuffer* MJPEGServer::getImageBuffer(const std::string& topic)
{
  boost::unique_lock<boost::mutex> lock(image_maps_mutex_);
  ImageSubscriberMap::iterator it = image_subscribers_.find(topic);
  if (it == image_subscribers_.end())
  {
    image_subscribers_[topic] = image_transport_.subscribe(topic, 1,
                                                           boost::bind(&MJPEGServer::imageCallback, this, _1, topic));
    image_buffers_[topic] = new ImageBuffer();
    ROS_INFO("Subscribing to topic %s", topic.c_str());
  }
  ImageBuffer* image_buffer = image_buffers_[topic];
  return image_buffer;
}

// rotate input image at 180 degrees
void MJPEGServer::invertImage(const cv::Mat& input, cv::Mat& output)
{

  cv::Mat_<cv::Vec3b>& input_img = (cv::Mat_<cv::Vec3b>&)input; //3 channel pointer to image
  cv::Mat_<cv::Vec3b>& output_img = (cv::Mat_<cv::Vec3b>&)output; //3 channel pointer to image
  cv::Size size = input.size();

  for (int j = 0; j < size.height; ++j)
    for (int i = 0; i < size.width; ++i)
    {
      //outputImage.imageData[size.height*size.width - (i + j*size.width) - 1] = inputImage.imageData[i + j*size.width];
      output_img(size.height - j - 1, size.width - i - 1) = input_img(j, i);
    }
  return;
}

void MJPEGServer::sendStream(int fd, const char *parameter)
{
  unsigned char *frame = NULL, *tmp = NULL;
  int frame_size = 0, max_frame_size = 0;
  int tenk = 10 * 1024;
  char buffer[BUFFER_SIZE] = {0};
  double timestamp;
  //sensor_msgs::CvBridge image_bridge;
  //sensor_msgs::cv_bridge image_bridge;
  cv_bridge::CvImage image_bridge;

  ROS_DEBUG("Decoding parameter");

  std::string params = parameter;

  ParameterMap parameter_map;
  decodeParameter(params, parameter_map);
  
  
  ParameterMap::iterator itp = parameter_map.find("topic");
  if (itp == parameter_map.end())
    return;

  std::string topic = itp->second;
  increaseSubscriberCount(topic);
  ImageBuffer* image_buffer = getImageBuffer(topic);

  ROS_DEBUG("preparing header");
  sprintf(buffer, "HTTP/1.0 200 OK\r\n"
          "%s"
          "Content-Type: multipart/x-mixed-replace;boundary=boundarydonotcross \r\n"
          "\r\n"
          "--boundarydonotcross \r\n",
          header.c_str());

  if (write(fd, buffer, strlen(buffer)) < 0)
  {
    free(frame);
    return;
  }

  ROS_DEBUG("Headers send, sending stream now");

  while (!stop_requested_)
  {
    {
      /* wait for fresh frames */
      boost::unique_lock<boost::mutex> lock(image_buffer->mutex_);
      image_buffer->condition_.wait(lock);

      //IplImage* image;
      cv_bridge::CvImagePtr cv_msg;
      try
      {
        if (cv_msg = cv_bridge::toCvCopy(image_buffer->msg, "bgr8"))
        {
          ;    //image = image_bridge.toIpl();
        }
        else
        {
          ROS_ERROR("Unable to convert %s image to bgr8", image_buffer->msg.encoding.c_str());
          return;
        }
      }
      catch (...)
      {
        ROS_ERROR("Unable to convert %s image to ipl format", image_buffer->msg.encoding.c_str());
        return;
      }

      // encode image
      cv::Mat img = cv_msg->image;
      std::vector<uchar> encoded_buffer;
      std::vector<int> encode_params;

      // invert
      //int invert = 0;
      if (parameter_map.find("invert") != parameter_map.end())
      {
        cv::Mat cloned_image = img.clone();
        invertImage(cloned_image, img);
      }

      // quality
      int quality = 95;
      if (parameter_map.find("quality") != parameter_map.end())
      {
        quality = stringToInt(parameter_map["quality"]);
      }
      encode_params.push_back(CV_IMWRITE_JPEG_QUALITY);
      encode_params.push_back(quality);

      // resize image
      if (parameter_map.find("width") != parameter_map.end() && parameter_map.find("height") != parameter_map.end())
      {
        int width = stringToInt(parameter_map["width"]);
        int height = stringToInt(parameter_map["height"]);
        if (width > 0 && height > 0)
        {
          cv::Mat img_resized;
          cv::Size new_size(width, height);
          cv::resize(img, img_resized, new_size);
          cv::imencode(".jpeg", img_resized, encoded_buffer, encode_params);
        }
        else
        {
          cv::imencode(".jpeg", img, encoded_buffer, encode_params);
        }
      }
      else
      {
        cv::imencode(".jpeg", img, encoded_buffer, encode_params);
      }

      // copy encoded frame buffer
      frame_size = encoded_buffer.size();

      /* check if frame buffer is large enough, increase it if necessary */
      if (frame_size > max_frame_size)
      {
        ROS_DEBUG("increasing frame buffer size to %d", frame_size);

        max_frame_size = frame_size + tenk;
        if ((tmp = (unsigned char*)realloc(frame, max_frame_size)) == NULL)
        {
          free(frame);
          sendError(fd, 500, "not enough memory");
          return;
        }
        frame = tmp;
      }

      /* copy v4l2_buffer timeval to user space */
      timestamp = ros::Time::now().toSec();

      memcpy(frame, &encoded_buffer[0], frame_size);
      ROS_DEBUG("got frame (size: %d kB)", frame_size / 1024);
    }

    /*
     * print the individual mimetype and the length
     * sending the content-length fixes random stream disruption observed
     * with firefox
     */
    sprintf(buffer, "Content-Type: image/jpeg\r\n"
            "Content-Length: %d\r\n"
            "X-Timestamp: %.06lf\r\n"
            "\r\n",
            frame_size, (double)timestamp);
    ROS_DEBUG("sending intemdiate header");
    if (write(fd, buffer, strlen(buffer)) < 0)
      break;

    ROS_DEBUG("sending frame");
    if (write(fd, frame, frame_size) < 0)
      break;

    ROS_DEBUG("sending boundary");
    sprintf(buffer, "\r\n--boundarydonotcross \r\n");
    if (write(fd, buffer, strlen(buffer)) < 0)
      break;
  }

  free(frame);
  decreaseSubscriberCount(topic);
  unregisterSubscriberIfPossible(topic);

}

void MJPEGServer::sendSnapshot(int fd, const char *parameter)
{
  unsigned char *frame = NULL;
  int frame_size = 0;
  char buffer[BUFFER_SIZE] = {0};
  double timestamp;
  //sensor_msgs::CvBridge image_bridge;
  //sensor_msgs::cv_bridge image_bridge;

  std::string params = parameter;
  ParameterMap parameter_map;
  decodeParameter(params, parameter_map); // http://merry:8080/stream?topic=/remote_lab_cam1/image_raw?invert=1

  ParameterMap::iterator itp = parameter_map.find("topic");
  if (itp == parameter_map.end())
    return;

  std::string topic = itp->second;
  increaseSubscriberCount(topic);
  ImageBuffer* image_buffer = getImageBuffer(topic);

  /* wait for fresh frames */
  boost::unique_lock<boost::mutex> lock(image_buffer->mutex_);
  image_buffer->condition_.wait(lock);

  //IplImage* image;
  cv_bridge::CvImagePtr cv_msg;
  try
  {
    if (cv_msg = cv_bridge::toCvCopy(image_buffer->msg, "bgr8"))
    {
      ; //image = image_bridge.toIpl();
    }
    else
    {
      ROS_ERROR("Unable to convert %s image to bgr8", image_buffer->msg.encoding.c_str());
      return;
    }
  }
  catch (...)
  {
    ROS_ERROR("Unable to convert %s image to ipl format", image_buffer->msg.encoding.c_str());
    return;
  }

  cv::Mat img = cv_msg->image;
  std::vector<uchar> encoded_buffer;
  std::vector<int> encode_params;

  // invert
  //int invert = 0;
  if (parameter_map.find("invert") != parameter_map.end())
  {
    cv::Mat cloned_image = img.clone();
    invertImage(cloned_image, img);
  }

  // quality
  int quality = 95;
  if (parameter_map.find("quality") != parameter_map.end())
  {
    quality = stringToInt(parameter_map["quality"]);
  }
  encode_params.push_back(CV_IMWRITE_JPEG_QUALITY);
  encode_params.push_back(quality);

  // resize image
  if (parameter_map.find("width") != parameter_map.end() && parameter_map.find("height") != parameter_map.end())
  {
    int width = stringToInt(parameter_map["width"]);
    int height = stringToInt(parameter_map["height"]);
    if (width > 0 && height > 0)
    {
      cv::Mat img_resized;
      cv::Size new_size(width, height);
      cv::resize(img, img_resized, new_size);
      cv::imencode(".jpeg", img_resized, encoded_buffer, encode_params);
    }
    else
    {
      cv::imencode(".jpeg", img, encoded_buffer, encode_params);
    }
  }
  else
  {
    cv::imencode(".jpeg", img, encoded_buffer, encode_params);
  }

  // copy encoded frame buffer
  frame_size = encoded_buffer.size();

  // resize buffer
  if ((frame = (unsigned char*)malloc(frame_size)) == NULL)
  {
    free(frame);
    sendError(fd, 500, "not enough memory");
    return;
  }

  /* copy v4l2_buffer timeval to user space */
  timestamp = ros::Time::now().toSec();

  memcpy(frame, &encoded_buffer[0], frame_size);
  ROS_DEBUG("got frame (size: %d kB)", frame_size / 1024);

  /* write the response */
  sprintf(buffer, "HTTP/1.0 200 OK\r\n"
          "%s"
          "Content-type: image/jpeg\r\n"
          "X-Timestamp: %.06lf\r\n"
          "\r\n",
          header.c_str(), (double)timestamp);

  /* send header and image now */
  if (write(fd, buffer, strlen(buffer)) < 0 || write(fd, frame, frame_size) < 0)
  {
    free(frame);
    return;
  }

  free(frame);
  decreaseSubscriberCount(topic);
  unregisterSubscriberIfPossible(topic);
}

void MJPEGServer::client(int fd)
{
  int cnt;
  char buffer[BUFFER_SIZE] = {0}, *pb = buffer;
  iobuffer iobuf;
  request req;

  /* initializes the structures */
  initIOBuffer(&iobuf);
  initRequest(&req);

  /* What does the client want to receive? Read the request. */
  memset(buffer, 0, sizeof(buffer));
  if ((cnt = readLineWithTimeout(fd, &iobuf, buffer, sizeof(buffer) - 1, 5)) == -1)
  {
    close(fd);
    return;
  }

  /* determine what to deliver */
  if (strstr(buffer, "GET /?") != NULL)
  {
    req.type = A_STREAM;

    /* advance by the length of known string */
    if ((pb = strstr(buffer, "GET /")) == NULL)
    {
      ROS_DEBUG("HTTP request seems to be malformed");
      sendError(fd, 400, "Malformed HTTP request");
      close(fd);
      return;
    }
    pb += strlen("GET /"); // a pb points to the string after the first & after command
    int len = min(max((int)strspn(pb, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._/-1234567890?="), 0), 100);
    req.parameter = (char*)malloc(len + 1);
    if (req.parameter == NULL)
    {
      exit(EXIT_FAILURE);
    }
    memset(req.parameter, 0, len + 1);
    strncpy(req.parameter, pb, len);

    ROS_DEBUG("requested image topic[%d]: \"%s\"", len, req.parameter);
  }
  else if (strstr(buffer, "GET /stream?") != NULL)
  {
    req.type = A_STREAM;

    /* advance by the length of known string */
    if ((pb = strstr(buffer, "GET /stream")) == NULL)
    {
      ROS_DEBUG("HTTP request seems to be malformed");
      sendError(fd, 400, "Malformed HTTP request");
      close(fd);
      return;
    }
    pb += strlen("GET /stream"); // a pb points to the string after the first & after command
    int len = min(max((int)strspn(pb, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._/-1234567890?="), 0), 100);
    req.parameter = (char*)malloc(len + 1);
    if (req.parameter == NULL)
    {
      exit(EXIT_FAILURE);
    }
    memset(req.parameter, 0, len + 1);
    strncpy(req.parameter, pb, len);

    ROS_DEBUG("requested image topic[%d]: \"%s\"", len, req.parameter);
  }
  else if (strstr(buffer, "GET /snapshot?") != NULL)
  {
    req.type = A_SNAPSHOT;

    /* advance by the length of known string */
    if ((pb = strstr(buffer, "GET /snapshot")) == NULL)
    {
      ROS_DEBUG("HTTP request seems to be malformed");
      sendError(fd, 400, "Malformed HTTP request");
      close(fd);
      return;
    }
    pb += strlen("GET /snapshot"); // a pb points to the string after the first & after command
    int len = min(max((int)strspn(pb, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._/-1234567890?="), 0), 100);
    req.parameter = (char*)malloc(len + 1);
    if (req.parameter == NULL)
    {
      exit(EXIT_FAILURE);
    }
    memset(req.parameter, 0, len + 1);
    strncpy(req.parameter, pb, len);

    ROS_DEBUG("requested image topic[%d]: \"%s\"", len, req.parameter);
  }

  /*
   * parse the rest of the HTTP-request
   * the end of the request-header is marked by a single, empty line with "\r\n"
   */
  do
  {
    memset(buffer, 0, sizeof(buffer));

    if ((cnt = readLineWithTimeout(fd, &iobuf, buffer, sizeof(buffer) - 1, 5)) == -1)
    {
      freeRequest(&req);
      close(fd);
      return;
    }

    if (strstr(buffer, "User-Agent: ") != NULL)
    {
      req.client = strdup(buffer + strlen("User-Agent: "));
    }
    else if (strstr(buffer, "Authorization: Basic ") != NULL)
    {
      req.credentials = strdup(buffer + strlen("Authorization: Basic "));
      decodeBase64(req.credentials);
      ROS_DEBUG("username:password: %s", req.credentials);
    }

  } while (cnt > 2 && !(buffer[0] == '\r' && buffer[1] == '\n'));

  /* now it's time to answer */
  switch (req.type)
  {
    case A_STREAM:
    {
      ROS_DEBUG("Request for streaming");
      sendStream(fd, req.parameter);
      break;
    }
    case A_SNAPSHOT:
    {
      ROS_DEBUG("Request for snapshot");
      sendSnapshot(fd, req.parameter);
      break;
    }
    default:
      ROS_DEBUG("unknown request");
  }

  close(fd);
  freeRequest(&req);
  ROS_INFO("Disconnecting HTTP client");
  return;
}

void MJPEGServer::execute()
{

  ROS_INFO("Starting mjpeg server");

  struct addrinfo *aip, *aip2;
  struct addrinfo hints;
  struct sockaddr_storage client_addr;
  socklen_t addr_len = sizeof(struct sockaddr_storage);
  fd_set selectfds;
  int max_fds = 0;

  char name[NI_MAXHOST];

  bzero(&hints, sizeof(hints));
  hints.ai_family = PF_UNSPEC;
  hints.ai_flags = AI_PASSIVE;
  hints.ai_socktype = SOCK_STREAM;

  int err;
  snprintf(name, sizeof(name), "%d", port_);
  if ((err = getaddrinfo(NULL, name, &hints, &aip)) != 0)
  {
    perror(gai_strerror(err));
    exit(EXIT_FAILURE);
  }

  for (int i = 0; i < MAX_NUM_SOCKETS; i++)
    sd[i] = -1;

  /* open sockets for server (1 socket / address family) */
  int i = 0;
  int on;
  for (aip2 = aip; aip2 != NULL; aip2 = aip2->ai_next)
  {
    if ((sd[i] = socket(aip2->ai_family, aip2->ai_socktype, 0)) < 0)
    {
      continue;
    }

    /* ignore "socket already in use" errors */
    on = 1;
    if (setsockopt(sd[i], SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    {
      perror("setsockopt(SO_REUSEADDR) failed");
    }

    /* IPv6 socket should listen to IPv6 only, otherwise we will get "socket already in use" */
    on = 1;
    if (aip2->ai_family == AF_INET6 && setsockopt(sd[i], IPPROTO_IPV6, IPV6_V6ONLY, (const void *)&on, sizeof(on)) < 0)
    {
      perror("setsockopt(IPV6_V6ONLY) failed");
    }

    /* perhaps we will use this keep-alive feature oneday */
    /* setsockopt(sd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); */

    if (bind(sd[i], aip2->ai_addr, aip2->ai_addrlen) < 0)
    {
      perror("bind");
      sd[i] = -1;
      continue;
    }

    if (listen(sd[i], 10) < 0)
    {
      perror("listen");
      sd[i] = -1;
    }
    else
    {
      i++;
      if (i >= MAX_NUM_SOCKETS)
      {
        ROS_ERROR("Maximum number of server sockets exceeded");
        i--;
        break;
      }
    }
  }

  sd_len = i;

  if (sd_len < 1)
  {
    ROS_ERROR("Bind(%d) failed", port_);
    closelog();
    exit(EXIT_FAILURE);
  }
  else
  {
    ROS_INFO("Bind(%d) succeeded", port_);
  }

  ROS_INFO("waiting for clients to connect");

  /* create a child for every client that connects */
  while (!stop_requested_)
  {

    do
    {
      FD_ZERO(&selectfds);

      for (i = 0; i < MAX_NUM_SOCKETS; i++)
      {
        if (sd[i] != -1)
        {
          FD_SET(sd[i], &selectfds);

          if (sd[i] > max_fds)
            max_fds = sd[i];
        }
      }

      err = select(max_fds + 1, &selectfds, NULL, NULL, NULL);

      if (err < 0 && errno != EINTR)
      {
        perror("select");
        exit(EXIT_FAILURE);
      }
    } while (err <= 0);

    ROS_INFO("Client connected");

    for (i = 0; i < max_fds + 1; i++)
    {
      if (sd[i] != -1 && FD_ISSET(sd[i], &selectfds))
      {
        int fd = accept(sd[i], (struct sockaddr *)&client_addr, &addr_len);

        /* start new thread that will handle this TCP connected client */
        ROS_DEBUG("create thread to handle client that just established a connection");

        if (getnameinfo((struct sockaddr *)&client_addr, addr_len, name, sizeof(name), NULL, 0, NI_NUMERICHOST) == 0)
        {
          syslog(LOG_INFO, "serving client: %s\n", name);
        }

        boost::thread t(boost::bind(&MJPEGServer::client, this, fd));
        t.detach();
      }
    }
  }

  ROS_INFO("leaving server thread, calling cleanup function now");
  cleanUp();
}

void MJPEGServer::cleanUp()
{
  ROS_INFO("cleaning up ressources allocated by server thread");

  for (int i = 0; i < MAX_NUM_SOCKETS; i++)
    close(sd[i]);
}

void MJPEGServer::spin()
{
  boost::thread t(boost::bind(&MJPEGServer::execute, this));
  t.detach();
  ros::spin();
  ROS_INFO("stop requested");
  stop();
}

void MJPEGServer::stop()
{
  stop_requested_ = true;
}

void MJPEGServer::decreaseSubscriberCount(const std::string topic)
{
  boost::unique_lock<boost::mutex> lock(image_maps_mutex_);
  ImageSubscriberCountMap::iterator it = image_subscribers_count_.find(topic);
  if (it != image_subscribers_count_.end())
  {
    if (image_subscribers_count_[topic] == 1) {
      image_subscribers_count_.erase(it);
      ROS_INFO("no subscribers for %s", topic.c_str());
    }
    else if (image_subscribers_count_[topic] > 0) {
      image_subscribers_count_[topic] = image_subscribers_count_[topic] - 1;
      ROS_INFO("%lu subscribers for %s", image_subscribers_count_[topic], topic.c_str());
    }
  }
  else
  {
    ROS_INFO("no subscribers counter for %s", topic.c_str());
  }
}

void MJPEGServer::increaseSubscriberCount(const std::string topic)
{
  boost::unique_lock<boost::mutex> lock(image_maps_mutex_);
  ImageSubscriberCountMap::iterator it = image_subscribers_count_.find(topic);
  if (it == image_subscribers_count_.end())
  {
    image_subscribers_count_.insert(ImageSubscriberCountMap::value_type(topic, 1));
  }
  else {
    image_subscribers_count_[topic] = image_subscribers_count_[topic] + 1;
  }
  ROS_INFO("%lu subscribers for %s", image_subscribers_count_[topic], topic.c_str());
}

void MJPEGServer::unregisterSubscriberIfPossible(const std::string topic)
{
  boost::unique_lock<boost::mutex> lock(image_maps_mutex_);
  ImageSubscriberCountMap::iterator it = image_subscribers_count_.find(topic);
  if (it == image_subscribers_count_.end() ||
      image_subscribers_count_[topic] == 0)
  {
    ImageSubscriberMap::iterator sub_it = image_subscribers_.find(topic);
    if (sub_it != image_subscribers_.end())
    {
      ROS_INFO("Unsubscribing from %s", topic.c_str());
      image_subscribers_.erase(sub_it);
    }
  }
}
}

int main(int argc, char** argv)
{
  ros::init(argc, argv, "mjpeg_server");

  ROS_WARN("WARNING -- mjpeg_server IS NOW DEPRECATED.");
  ROS_WARN("PLEASE SEE https://github.com/RobotWebTools/web_video_server.");

  ros::NodeHandle nh;
  mjpeg_server::MJPEGServer server(nh);
  server.spin();

  return (0);
}

2.遠隔のホストPCのブラウザにカメラ画像を表示

以前の記事を参考にして
VNC接続したJetson Nanoを用意
・USBカメラ(ロジクール)をJetson Nanoに接続
・Jetsonでコマンド実行- UCBカメラ起動-ブラウザに送信
・ホストPCのブラウザで映像確認

Jetson Nano side


# USBカメラのデバイスcheck
$ ls /dev/video*
>>
# /dev/video0
# Jetsonのipアドレスを確認
$ ifconfig
# 1xx.xxx.10.xxx

$ roscore
# USBカメラ起動
$ roslaunch roscam_capture usb_cam.launch
# ブラウザに表示 (port指定)
$ rosrun video_stream video_stream _port:=10000

Jetson Nanoとの共有画面

ホストPC side




Safariでアクセス
トピック名=/usb_cam/image_raw
アドレス = http://1xx.xxx.10.xxx:10000/stream?topic=/usb_cam/image_raw


PCでは無事みれたけど、スマホとかipadでは見れなかった。

Web socketでjavascriptを使う方法もあるけど、こっちのがROSと併用できて、使いやすい。
VPNがもう使えるので、遠隔監視ロボかシステムでも作れそう。


参考

mjpeg_server
【WebSocket】Raspberry Piロボットに搭載されたカメラの映像を遠隔PCに配信する

バカな経営者に安売り買いされる日本の技術者のための詩

無能な経営者が多い。

将棋の騎士は駒を操る。駒の使い方を考える。

だがなんとITの経営者とかPMとか名乗る奴は、駒である技術者にビジネス的にどう使うかを聞いてくる。
これは将棋の騎士が駒に駒の使い方聞くようなもんだ。

会社は自分の目的達成の道具に過ぎない。利用価値のない人材、同じく会社は切る。お互いこれでイーブン対等なんだよ。

無能なやつの言うこと聞いてしょーもないもん作る現状か、本当に作りたいもの自分で作るかの葛藤の中で、幸福論で1番共感した詩

                                • -

メキシコの田舎町。海岸に小さなボートが停泊していた。
メキシコ人の漁師が小さな網に魚をとってきた。
その魚はなんとも生きがいい。それを見たアメリカ人旅行者は、
「すばらしい魚だね。どれくらいの時間、漁をしていたの」 と尋ねた。

すると漁師は
「そんなに長い時間じゃないよ」
と答えた。旅行者が
「もっと漁をしていたら、もっと魚が獲れたんだろうね。おしいなあ」
と言うと、
漁師は、自分と自分の家族が食べるにはこれで十分だと言った。

「それじゃあ、あまった時間でいったい何をするの」
と旅行者が聞くと、漁師は、
「日が高くなるまでゆっくり寝て、それから漁に出る。
戻ってきたら子どもと遊んで、女房とシエスタして。
夜になったら友達と一杯やって、ギターを弾いて、
歌をうたって…ああ、これでもう一日終わりだね」

すると旅行者はまじめな顔で漁師に向かってこう言った。
「ハーバード・ビジネス・スクールでMBAを取得した人間として、
きみにアドバイスしよう。いいかい、きみは毎日、もっと長い時間、
漁をするべきだ。 それであまった魚は売る。
お金が貯まったら大きな漁船を買う。そうすると漁獲高は上がり、儲けも増える。
その儲けで漁船を2隻、3隻と増やしていくんだ。やがて大漁船団ができるまでね。
そうしたら仲介人に魚を売るのはやめだ。
自前の水産品加工工場を建てて、そこに魚を入れる。
その頃にはきみはこのちっぽけな村を出てメキソコシティに引っ越し、
ロサンゼルス、ニューヨークへと進出していくだろう。
きみはマンハッタンのオフィスビルから企業の指揮をとるんだ」

漁師は尋ねた。
「そうなるまでにどれくらいかかるのかね」
「20年、いやおそらく25年でそこまでいくね」
「それからどうなるの」
「それから? そのときは本当にすごいことになるよ」

と旅行者はにんまりと笑い、

「今度は株を売却して、きみは億万長者になるのさ」
「それで?」
「そうしたら引退して、海岸近くの小さな村に住んで、
日が高くなるまでゆっくり寝て、 日中は釣りをしたり、
子どもと遊んだり、奥さんとシエスタして過ごして、
夜になったら友達と一杯やって、ギターを弾いて、
歌をうたって過ごすんだ。 どうだい。すばらしいだろう」

漁師は尋ねた。
「それ、私がいまやってることと何が違うんです?」