20200623のNode.jsに関する記事は3件です。

SemanticSegmentationを行うnpmパッケージを作ったので使い方を説明してみる

はじめに

先日、大きな画像を分割してsemantic segmentationを行うnpm パッケージを作成したので、使い方の説明を兼ねてデモの作り方を説明してみます。

このパッケージは、スマホなどの性能の限られたデバイス上で精度と応答速度のトレードオフを調整しながらSemanticSegmentationするためのものです。
今回のデモとしては、このようなものを作ります。バーコードを検出してセグメンテーションを行います。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_365991_425fef2a-7eef-9bc4-21e0-3960a4c8154e.gif

前回のnpmパッケージを作成した際の投稿はこちらです。
https://qiita.com/wok/items/a6445d17724ed700e448

前準備

今回はReactでデモを作成します。
まずは環境構築をしましょう

$ create-react-app demo --typescript

パッケージをインストール

それではvirtual背景のパッケージをインストールします。

$ npm install scalable-semantic-segmentation-js
$ node node_modules/scalable-semantic-segmentation-js/bin/install_worker.js public
file is copied

使用するモデルの準備

次に使用するSemantic Segmentationのモデルを用意します。
このモデルは、[batch, height, width, channels]のshapeのテンソルを入力として想定しています。

$ ls public/WEB_MODEL/300x300_0.10/
group1-shard1of1.bin  model.json

デモのコード

準備ができたら、ソースコードを作成します。
ここでは、重要と思われる部分のみ解説します。ソース全体は下記のリポジトリに置いてあります。

本デモでは、Reactコンポーネントのメンバー変数としてモジュールのクラスのインスタンスを作っておきます。

  scalableSS:ScalableSemanticSegmentation = new ScalableSemanticSegmentation()

では、componentDidMoutからみて行きます。

componentDidMount() {
      console.log('Initializing')

      const initWorkerPromise = this.initWorker()                          // <-- (1)

      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { // <-- (2)
          const webCamPromise = navigator.mediaDevices
              .getUserMedia({
                  audio: false,
                  video: DisplayConstraintOptions[this.state.videoResolution]
              })
              .then(stream => {
                  console.log(this.videoRef)
                  this.videoRef.current!.srcObject = stream;               // <-- (3)
                  return new Promise((resolve, reject) => {
                      this.videoRef.current!.onloadedmetadata = () => {
                          resolve();
                      };
                  });
              });

          Promise.all([initWorkerPromise, webCamPromise])
              .then((res) => {
                  console.log('Camera and model ready!')
              })
              .catch(error => {
                  console.error(error);
              });
      }           
  }

(1)で今回SemanticSegmentationを行うインスタンスをイニシャライズします。このメソッドの内部は後述します。
(2)でビデオデバイスを取得し、(3)でHTMLVideoElementのソースに設定しています。

次に、initWorkerの中身を見ていきます。

async initWorker() {
      // SemanticSegmentation
      this.scalableSS.addInitializedListener(()=>{ // <-- (1-1)
          const props = this.props as any
          this.setState({initialized:true})
          this.requestScanBarcode()                // <-- (1-2)
      })
      this.scalableSS.addMaskPredictedListeners((maskBitmap:ImageBitmap)=>{// <-- (2-1)
          // 再キャプチャ
          this.requestScanBarcode()                                        // <-- (2-2)

      })

      this.scalableSS.init(
            AIConfig.SS_MODEL_PATH, 
            AIConfig.SPLIT_WIDTH, 
            AIConfig.SPLIT_HEIGHT, 
            AIConfig.SPLIT_MARGIN)                                         // <-- (3)
      return
  }

本メソッドでは、SemanticSegmentationを行うインスタンスをイニシャライズしています。
まず(1-1)で、インスタンス内部で使用するモデルのロードなどの初期化が完了したときのコールバック関数を設定します。
コールバック関数の中では(1-2)バーコードスキャンを行う関数を呼び出しています。
(2-1)は、セグメンテーションが完了したときに呼び出されるコールバック関数を設定しています。
このとき受け取るパラメータがセグメンテーションの結果のビットマップイメージになります。
コールバック関数の中でも(2-2)バーコードスキャンを行う関数を呼び出しています。これにより処理をループさせています。
(3)でインスタンスに使用するモデルの情報と、画像を分割する際に使用するマージン(分割後の隣り合う画像間でオーバラップする領域の割合)を指定します。第1引数がモデルのパス、第2、3引数がモデルで使用する幅と高さ、第4引数がマージンです。

最後に、requestScanBarcodeです。

  requestScanBarcode = async () => {
      console.log('requestScanBarcode')
      const video = this.videoRef.current!
      const controller = this.controllerCanvasRef.current!
      controller.width = this.overlayWidth
      controller.height = this.overlayHeight

      const captureCanvas = captureVideoImageToCanvas(video) <--(1)
      if(captureCanvas.width === 0){
          captureCanvas.remove()
          window.requestAnimationFrame(this.requestScanBarcode);
          return
      }
      this.scalableSS.predict(captureCanvas, 
                              this.state.colnum,
                              this.state.rownum) <--(2)
      captureCanvas.remove()
  }


ここでは、SemanticSegmentationを行う対象となる画像を取得し、SemanticSegmentionを実行しています。
(1)で、SemanticSegmentationの対象となる画像をHTMLVideoElementから取得しています。この画像を(2)の引数としてモジュールのインスタンスに与え、SemanticSegmentationを実行させています。第2,3引数は分割する行数と列数です。

以上で、セグメンテーションを行う処理は完了です。

デバッグ用に、セグメンテーションの様子や、グリッドの情報を表示することもできます。

                  <Label basic size="tiny" color={this.state.showSS?"red":"grey"} onClick={()=>{
                      const newValue = !this.state.showSS
                      this.scalableSS.previewCanvas = newValue ? this.workerSSMaskMonitorCanvasRef.current! : null
                      this.setState({showSS:newValue})
                  }}>ss</Label>
                  <Label basic size="tiny" color={this.state.showGrid?"red":"grey"} onClick={()=>{
                      const newValue = !this.state.showGrid
                      this.scalableSS.girdDrawCanvas = newValue ? this.controllerCanvasRef.current! : null
                      this.setState({showGrid:!this.state.showGrid})
                  }}>grid</Label>

this.scalableSS.previewCanvasとthis.scalableSS.girdDrawCanvasにそれぞれ描画するHTMLCanvasElementを設定してください。

デモ

https://scalable-ss-demo.herokuapp.com/index.html

ソースコードとnpmパッケージ

本ソースコードは下記のリポジトリに格納してあります。
https://github.com/FLECT-DEV-TEAM/ScalableSemanticSegmentationjs_demo

npmパッケージのページは次のURLになります。
https://www.npmjs.com/package/scalable-semantic-segmentation-js

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTMLのタイトルを取得する [Node.js, シェルスクリプト]

httpリクエストでHTMLのページを取得して正規表現でタイトルを引っ張り出します。Node.jsを使う方法とシェルスクリプト(Bash)を使う方法で出来たので書き残しておきます。

ちなみに、Node.jsもシェルスクリプトも初心者です。

環境
Mac OS 10.14.6

Node.js

Node.jsからダウンロードします。

$ node -v
v12.18.1

requestモジュール

Node.js標準でもHTTPリクエストできるそうですがrequestモジュールを使うと楽、らしい。

$ npm install request

これはできるだけ上位のディレクトリでやったほうがいい。インストールしたところ以下でrequestが使えるがそれより上のディレクトリでは使えない。(個人的な実験の結果)(npmについてのまとめ - Qiitaに詳しいことが書いてある(グローバルインストールとやらに失敗したので上の階層に入れた))

実装

get_title.js
var request = require('request');

var URL = process.argv[2];

var re_title = RegExp("(?<=<title.*>).*(?=</title>)")

function callback(error, response, body) {
  if (!error && response.statusCode == 200) {
    var title = re_title.exec(body);
    if(!title){
      console.log("No title")
    }else{
      console.log(title[0]);
    }
  }else{
    console.log("Error!");
  }
}

request(URL, callback);
  • 1行目 requestモジュールを使えるようにする
  • 3行目 コマンドライン引数からURLを取得する
  • 5行目 title要素を取得する正規表現(後述)
  • 7行目~ requestの結果に対するコールバック関数(後述)
  • 最終行 request実行

実行

$ node get_title.js https://...
タイトル

正規表現

ここで使う正規表現は

"(?<=<title.*>).*(?=</title>)"

です。(参考 正規表現 - JavaScript | MDN

  • (?<=<title.*>): これは「後読み」と呼ばれ、<title.*>に続くものをマッチさせますが<title.*>自体はマッチしません
  • .*: これは「任意の文字の(.)」「0回以上の繰り返し(*)」にマッチします
  • (?=</title>): これは「先読み」と呼ばれ、</title>が後に続くものをマッチさせますが</title>自体はマッチしません

この正規表現でHTMLを探索することでtitle要素の中身にマッチさせることができます。ちなみに、title開始タグを見つける「後読み」部分が<title>ではなく<title.*>を使っているのは、title開始タグにオマケがついているページがあったからです。ここ→request - npmなんですけど、ソースを見ると

<title data-react-helmet="true">request  -  npm</title>

って書いてあります。何のこっちゃわからないのですが。

requestのコールバック関数

そんなに深入りしないです。

リクエストがエラー吐いたりステータスコードが200じゃなかったら"Error!"と出力して終わる。
OKだったら正規表現でbodyから.exec()メソッドで最初にマッチするところを探して結果をtitleに入れる。
マッチすればtitle[0]がタイトルだし、マッチしなければnullが返ってくるので"No title"と出力。

まあ基本正常に返ってくることを期待しているので。

Bash

こっちのほうが、複雑だが面白かった。

実装

get_title.sh
#!/bin/bash

read URL

#RegExp='s/.*<title.*>\(.*\)<\/title>.*/\1/p'
RegExp='/.*<title.*>\(.*\)<\/title>.*/{s/.*<title.*>\(.*\)<\/title>.*/\1/p;q;}'

title=`curl -s $URL | sed -n $RegExp`

echo -e "\n[$title]($URL)\n" #markdown式
  • 1行目 おまじない。よくわからんけど書いとけ
  • 3行目 標準入力からURLを受け取る
  • 5,6行目 sedに送るコマンド
  • 8行目 curlとsedでページタイトルの取得
  • 10行目 出力(markdown式にカスタマイズ)

実行

$ bash get_title.sh
https://qiita.com/

[Qiita](https://qiita.com/)

curl

$ curl -s {URL}

{URL}の中身であるHTML文書を取得。さらにパイプで次のsedコマンドに流している。-sは進捗の表示をさせないオプション

sed

パイプでやるという条件を揃えて書くと

#!/bin/bash
$ echo -e {複数行文字列} | sed -n s/{置換前}/{置換後}/p

で、{複数行文字列}から{置換前}を含む行があれば{置換後}に置き換えて出力。-nは、デフォルトでは置換の有無にかかわらず毎行出力していたのを無効にするオプション。pは明示的に出力するフラグ

#!/bin/bash
$ echo -e "hogehoge\nfugafuga" | sed -n "s/g.*g/QQ/p"
hoQQe
fuQQa

5行目のコマンド

s/.*<title.*>\(.*\)<\/title>.*/\1/p

大枠 = s/{置換前}/{置換後}/p
{置換前} = .*<title.*>\(.*\)<\/title>.*
{置換後} = \1
  • {置換前}: 「任意文字列(.*)」「<title.*>」「.*」「<\/title>」「.*」の文字列を見つける。真ん中は「\(.*\)」としてエスケープつきの括弧で囲むことでグループ化して一時保存しています
  • {置換後}: 先ほど一時保存したところを\1で呼び出しています
#!/bin/bash
$ echo -e "hogehoge\nfugafuga" | sed -n "s/.*g\(.*\)g.*/\1/p"
eho
afu

6行目のコマンド

/.*<title.*>\(.*\)<\/title>.*/{s/.*<title.*>\(.*\)<\/title>.*/\1/p;q;}

大枠 = /{文字列}/コマンド
文字列 = .*<title.*>\(.*\)<\/title>.*(さっきの探索文字列)
コマンド = {s/.*<title.*>\(.*\)<\/title>.*/\1/p;q;}
  • 大枠: {文字列}がある行に対してコマンドを実行
  • コマンド: {}で囲んで複文にする。前半s/.*<title.*>\(.*\)<\/title>.*/\1/p;は先述の通りの置換&表示コマンド。後半q;はsedコマンドの終了。

これがなぜ欲しいかというと、request - npmの中になんだか知らんがtitle要素が出現するところが2行あるので。1回マッチしたら{変換&終了}する。

その他

sedの後一回変数に入れたいなぁと思って色々調べたら、式自体をバッククォートで囲んで代入すれば良いとのこと。

bashで改行付きでechoするためには-eオプションが必要→echo -e "\n[$title]($URL)\n"

おしまい

bashの方が色々設定とかせずにすみそうで良いな

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Lambda Nodejsのバージョンが8から12に強制アップデートされたら Error: Cannot find module 'jmespath' と出る問題

要約

AWS Lambdaで利用するnodejsのバージョンが nodejs8 -> nodejs10以降のアップデートすると aws-sdkが組みこまれるようになるため
node_modulesにaws-sdkを含んでいるとエラーになるので、取り除く必要がある

本文

nodejs8で書かれたAWS Lambdaスクリプトは非推奨となりました。
AWS Lambdaのnodejs10やnodejs12環境で同じスクリプトを実行すると、以下のようなエラーメッセージが表示される。

{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'jmespath'","stack":["Runtime.ImportModuleError: Error: Cannot find module 'jmespath'","    at _loadUserApp (/var/runtime/UserFunction.js:100:13)","    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)","    at Object.<anonymous> (/var/runtime/index.js:36:30)","    at Module._compile (internal/modules/cjs/loader.js:701:30)","    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)","    at Module.load (internal/modules/cjs/loader.js:600:32)","    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)","    at Function.Module._load (internal/modules/cjs/loader.js:531:3)","    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)","    at startup (internal/bootstrap/node.js:283:19)"]}

モジュール'jmespath'自体はpackage.jsonをみても見つからない、普通のモジュールの場合は、 requireの書き方を修正しすればよいが、
aws-sdkの場合は対応が異なる

# この様になっているものを
var aaa = require('foobar')

# このように相対パスを指定してあげれば良い
var aaa = require('./foobar')

node_moduleを使ってaws-sdk下記のように呼び出している場合は aws-sdk を node_modulesから削除する必要がある

var aws = require('aws-sdk');

aws-sdkを取り除くyarnの場合

yarn remove aws-sdk

aws-sdkを取り除くnpmの場合

npm uninstall aws-sdk

nodejs10 から lambda に aws-sdk が追加されました。 see: https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む