Snakeによる輪郭抽出を実装してみた

以下の本を読み始めた。 opluse.shop-pro.jp 第一章の動的輪郭モデルに関する話の冒頭で代表的な方法であるSnakeがでてきた。 アルゴリズムの説明があり、シンプルだったので自分で実装してみた。

こんな感じで動いた。

f:id:KYudy:20200531103535g:plain
m570の動的輪郭追跡(gamma=0.7)

gammaを0.7から0.5に変えると以下のようになった。画像の勾配に対して鈍感になるのでM570の左上のクリックボタンの輪郭を取れなくなってしまった。 一方で画像の勾配に鈍感な分、輪郭を縮めるので収束が速い。

f:id:KYudy:20200531101326g:plain
m570の動的輪郭追跡

ソースは以下のgistに載せた。 skimageをインポートしているが、Snakeのメインのアルゴリズムは自分で書いている。 Pythonでループを使って書かれているのでとても動作が遅いがアルゴリズムを理解するために書いたのであまり気にしていない。 Snakeのアルゴリズムはいくつかあるが、上記の書籍で紹介されているアルゴリズムは以下である。 Donna J et al. A Fast algorithm for active contours and curvature estimation. gist.github.com

GCPのdebianにsshするときにXの転送が失敗する場合の対処

GCPを使っているときにX転送して、画像をsxivで開いて確認したりしたくなることがある。 debianインスタンスでsxivを実行しようとしたら、以下のようなエラーが出た。

@localhost
$ ssh -Y gcphost
@gcphost
$ sxiv ...
sxiv: Error opening X display

よく見返すとsshの直後に以下のようなエラーも出ていた。

X11 forwarding request failed on channel 0

こういうときに役に立つのが、StackOverflowである。 https://stackoverflow.com/questions/38961495/x11-forwarding-request-failed-on-channel-0

以下の順番で設定していったが、結局xauthをインストールした後に直った。

  • /etc/ssh/sshd_configにX11Forwarding yesを追記
  • /etc/ssh/sshd_configにX11UseLocalhost noを追記
  • sudo apt install xauth

OpenCVを使って点線を含めた縦棒を画像から削除する

以下のような画像から数字や横棒を残しつつ点線を削除したい。

f:id:KYudy:20191026131016j:plain

方法はいくつか考えられるが、 一番最初に思いついたのはハフ変換を使って直線検出し、検出された線を画像から削除するという方法だった。

ただやってみると気づくのだが、検出された線の太さがわからないので、決め打ちの太さで線を打ち消すことになる。

上の画像みたいに太線と細線と点線が混じっている場合は、太い方を消すために、細い方の線に対しては本来線ではない部分も含めて削除することになる。 場合によっては隣接する文字を削ってしまい文字の判別が不可能になるかもしれない。

また、ハフ変換の場合は点線を直線として検出することができない場合がある。

よく考えると線が画像に対し水平垂直であるという前提条件がおけるのであれば、ハフ変換を用いるのは大仰である。 そこで、点線を含めて縦の線を削除するための簡単なアルゴリズムを作ったので紹介する。

アルゴリズムは、以下の2つで成り立つ

  • 点線をつなぐための膨張・収縮テクニックの応用
  • 投影(Vertical Projection) を使った直線が通るX軸の座標の求め方

点線をつなぐための膨張・収縮テクニックの応用

膨張・収縮は有名なテクニックであり、それらを組み合わせるとノイズ除去、細線化等が実現される。 OpenCVではそれぞれcv2.dilate()cv2.erode()という関数として提供されている。 labs.eecs.tottori-u.ac.jp

当初は「クロージング」と同じ仕組みで点線をつなぐ事ができると考えた。 しかし点の間の距離が大きい(5pxとか?)場合には膨張の回数を大きめにしないと点同士がつながらず、 一方でその回数を増やしすぎると点線に隣接する文字が癒着してしまうという問題が発生することに気づいた。

これを回避する方法はcv2dilate()の第2引数であるkernelにあった。 具体的にはkernelに以下の様な配列を指定しただけだ。

[[0 0 1 0 0]
 [0 0 1 0 0]
 [0 0 1 0 0]
 [0 0 1 0 0]
 [0 0 1 0 0]]

ご覧いただくと分かる通り、真ん中の縦の列のみ1、ほかは0という2次元の配列だ。 このカーネルによって膨張を縦方向に限定することができ、線と隣接する文字との癒着を防ぐことができる。 これで、癒着を防ぎつつ、点線を実線に変更することが可能になった。

このテクニックを上記の画像に適用すると以下のような画像が得られる。(色がおかしいけど・・・) 見事に点線がつながっている。一方で横線と数字の下部と癒着しているが、これは後述の縦線検出するのに障害にならない。 f:id:KYudy:20191026133518p:plain

投影(Vertical Projection) を使った直線が通るX軸の座標の求め方

ここまでで縦線は画像上にしっかり描画された状態になった。 あとは、直線を検出する上で太さを含めて検出したい。

「投影」とは、もともと画像上に横に並んでいる文字列を各文字をセグメンテーションするための方法である。 画像の同じX座標にあるピクセルを合計するというもので、np.sum()の引数axisに1を指定すれば「投影」できる。

文字がある場所は合計値が大きくなり、文字間の空白はノイズが値が小さくなる。 散布図をかけば文字のところは山になり、文字間は谷になる。

これを縦線を含む画像に適用した場合は、縦線は切り立って山(スパイク)を発生させる。 実際に膨張・収縮を適用した画像に投影を適用すると以下のような散布図を得られる。 f:id:KYudy:20191026134600p:plain

縦線を表すスパイクと文字を表す山が存在することがわかる。縦線と文字は高さで区別可能なので、しきい値でふるいにかけるとスパイクが発生しているX軸上の座標を求めれる。

実際に座標を求めて、そこに線を引いた結果は以下である。 f:id:KYudy:20191026134855p:plain

赤線の太さがもとの縦線と同じであることが確認できる。

技術的な制約

このアルゴリズムの限界は以下である

  1. 直線が歪んでいる場合はうまく検出できない可能性がある
  2. 画像に対して縦線が完全に垂直でなければならない

2.に関しては縦線を画像に対して垂直にする方法はある。縦線が傾いている場合に比べて、垂直になっている場合はスパイクの高さが大きくなる。これを利用する方法である。簡単に実現できると思うが、今回は詳細を割愛する。

サンプルコード

最後にサンプルコードを載せる。

import cv2
import numpy as np
import matplotlib.pyplot as plt


img_path = "cell.jpg"

thresh_spike = 200

img_org = cv2.imread(img_path)
img = img_org.copy()

_, img_th = cv2.threshold(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), 0, 255, cv2.THRESH_OTSU)

# reverse white and black for dilate and erode
img_th = 255 - img_th

# vertical kernel to connect dot line to solid line
kernel = np.zeros((5,5), np.uint8)
kernel[:, 2] = 1

img_th = cv2.dilate(img_th, kernel, iterations=8)
img_th = cv2.erode(img_th, kernel, iterations=2)

vp = np.sum((img_th != 0).astype(np.uint8), axis=0)
loc_x_spike = np.where(vp > thresh_spike)

# draw vertical lines
for x in loc_x_spike[0]:
    line_color = (255, 0, 0) # red
    cv2.line(img, (x, 0), (x, img.shape[0]), line_color, thickness=1)

_, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=4)
ax1.imshow(img_org)
ax2.scatter(range(len(vp)), vp)
ax3.imshow(img_th)
ax4.imshow(img)
plt.show()

JetsonのラインナップとPyTorchが使えるかの調査

Jetsonを検討していたので、その内容をメモる。自分にしか役に立たないかもしれない。
PyTorchしか実装できないマンなので、PyTorchをインストールできるか、動作は問題ないかあたりをWebを漁りながら確認する。
 

ラインナップ

シリーズ
シリーズ内のラインナップ
CUDAコア数
Tensorコア数
値段
Nano
 
128
-
129USD
TX2 Series
TX2 4GB,  TX2, TX2i
256
-
249USD〜
AGX Xavier Series
Volta
Xavier 8GB,  AGX Xavier
384, 512
48, 64
599USD〜
Compare Modulesで更に詳細が出てくる。
電源電圧もそれぞれ異なるというのが注意ポイントだろう。
Wi-Fiが搭載されているのはTX2のみである。他はついていない。
TensorコアがAGXだけに搭載されているが、これはなんだろうか?(要調査)

PyTorchが動くのか?

結論からいうと比較的簡単に動きそう。
プロセッサがx86ではないのでPyTorch公式は扱っていないが、NVIDIAがビルド済みの状態で配布しているので問題なさそうである。
2019-10-04時点で最新のv1.2のビルド済みのパッケージが配布されているようだ。
 
NVIDIAの公式サイトでNanoへのPyTorchのインストール方法が紹介されているが、
冒頭でtorch2trtがアナウンスされている。
torch2trtはPyTorchのモデルをTensorRTのモデルに変換するためのツールらしい。
TensorRTはCUDAで開発され、INT8, FLOAT16向けに最適化することでモデルの推論速度を向上するためのものらしい。torch2trtのベンチマークを確認すると変換後はCNNの推論速度が2〜3倍程度に向上している。
推論速度の向上が必要になれば利用を検討するべきかもしれない。(個人的には30fpsを超えているようなモデルは最適化が不要な気もする)

メカニカルキーボードのキートップを換装した

f:id:KYudy:20190911220417j:image

ARCHISSの87配列のメカニカルキーボードのキートップを換装した。キートップの表面が長年のタイピングによって研磨されツルツルになってカッコ悪いのと、シェル上の補完のためにタブキーを連打した結果キートップの根元が折れて外れるようになったためである。(代わりにCtrl+Iを使えばよいのだが)

世の中に流通してるメカニカルキーボードのスイッチはCHERRY社製であり、そのスイッチ向けに作られたキートップには互換性があるらしい。キーの幅も共通っぽい。

ということで、以下のキートップに換装してみた。87配列は104配列のテンキーレスバージョンぽい(実はよくわかってない)ので104配列向けのものから選んだ。

 

結果的には簡単な作業で、キーの高さなどもほぼ変わらず完璧な換装ができた。

 

換装前の状態。ツルピカ。左側のタブに前述の問題あり。

f:id:KYudy:20190911212519j:image

キートップ除去後の状態。赤軸のリニアで抜けるような打鍵感が好き。

f:id:KYudy:20190911212521j:image

届いたキートップ台風15号の猛威が関東を襲う最中、福岡から札幌へ遥々やってきた。

f:id:KYudy:20190911214344j:image

こんな感じの小袋が数個入っていた。キートップは二種類の樹脂を重ねているタイプ。

f:id:KYudy:20190911212605j:image

換装後(トップの写真と同じ)。各種機能系のキーは薄めのグレーで、英数字記号のキーは濃いめのグレーとなっている。

f:id:KYudy:20190911220417j:image

全てのキーに書かれた文字が透けており、ARCHISSのキーボードではCapsLockとScreenLockが淡く青色に光る。ちなみに標準のキーキャップは2つのキーの下部に矩形の透過部が存在する。

f:id:KYudy:20190911232629j:image

換装し終えた後で気づいたのだが、ARCHISSのMaestroシリーズと雰囲気が似ている。派手すぎないが、個性をある程度主張する感じに仕上がった。

送料込みで2000円の費用がかかったが、また数年使えるのであればお得ではなかろうか。

 

Focal lossの実装(PyTorch)

Focal lossとは教師データに含まれるクラスごとのインスタンスが不均一であるときに学習がうまくいかないことを是正するために提案されたものだ。 One stageのObject detectionで背景クラスが大半を占めることで発生する問題に対して効果的に働くらしい。 仕組みがシンプルなので適用先はObject detectionには限らない汎用的な仕組みだといえる。

詳しいことは元の論文を読むなり、Qiitaを読むなりすることをお勧めするが、 この記事ではPyTorchでFocal lossを使用するために私が行った修正等について説明したい。

PyTorchのコミュニティでFocal lossについて議論されており、以下がおすすめされていたので使ってみたが、途中でone_hotを作って計算するところがGPUに載せ替えないと動かないのが不満になり、そこだけ直した。(Skorchを使っているとlossの計算時に to(device) とかやりづらいので。) github.com

直した結果は以下である。修正前の実装をFocalLossWithOneHot とし、修正後の実装をFocalLossWithoutOneHotとしている。 main()to("cuda")してFocalLossWithOneHot はエラーを吐き、FocalLossWithoutOneHotはエラーを吐かないこと確認した。 gist.github.com

np.isin()について

概要

np.where()を使って特定の条件を満たす項目を抜き出す処理は良く行う。 今回はその発展として、複数の値に一致する項目を抜き出してきたいという状況を考える。 そのような状況では、np.isin()を使えばよいという話。

詳細

具体的には以下のようなフルーツ( fruits )と食べたいものリスト( i_want_to_eat )があるとして、自分の食べたいものに該当するインデックスを知りたいとする。

>>> fruits = ['apple', 'ringo', 'banana', 'mikan']
>>> i_want_to_eat = ['ringo', 'mikan']

np.isin()を使うと以下のようにfruitに入っている複数のフルーツの内i_want_to_eatに入っているフルーツに該当する部分がTrue、それ以外にFalseが入った配列が返ってくる。

>>> fruits = np.array(fruits)
>>> i_want_to_eat = ['ringo', 'mikan']
>>> np.isin(fruits, i_want_to_eat)
array([False,  True, False,  True])

もし配列からそのフルーツのみ抜き出したければ、普通にnumpyのブールインデックス参照を行えばよい

>>> fruits[np.isin(fruits, i_want_to_eat)]
array(['ringo', 'mikan'], dtype='<U6')

以上。 普通にマニュアルに書いてあるけど、ちょっと調べてなかなかたどり着けなかったので、メモ代わりに書いた。