なめこ備忘録

プログラミングに関する備忘録や経験したこと,考えたことなど好き勝手書きます.

[tex: ]

Ubuntu 18.04 LTS + RTX2070でDeep Learning環境を整えるまでの備忘録

既に何度も環境構築をしたことがあるけど,毎回調べていると情報が錯綜しすぎてて苦労するので備忘録として残しておく.

OSの種類とかバージョンとか載ってるGPUだとかで,バージョンだけでなく使える方法も変わってくるので注意が必要.

事前準備

nvidia driverのバージョン確認

以下URLからnvidia driverの対応バージョンを調べておく.

https://www.nvidia.co.jp/Download/index.aspx?lang=jp

tensorflowの対応バージョン確認

tensorflowは結構厳密にバージョン指定をしているので以下URLで事前に確認しておく. 今回はtensorflow 1.13を入れる.

https://www.tensorflow.org/install/source

python

とりあえず3系をインストールしておく.

tensorflowのバージョンによって対応しているpythonバージョンも異なるので注意.

(nvidia driverのインストール)

もしかすると必要のないかもしれない手順(次のcudaインストールでドライバーごとインストールされているっぽい?).

nvidia driverの入っているリポジトリを追加し,aptからnvidia driverをインストールする.
今回はUbuntu 18.04 TLS & RTX2070に対応しているバージョン430をインストール.

$ sudo add-apt-repository ppa:graphics-drivers/ppa
$ sudo apt update
$ sudo apt  install nvidia-driver-430
$ nvidia-smi

で確認

cuda10.0のインストール

ネットワーク経由のインストーラータイプでCUDA 10をインストール.

$ sudo apt install cuda

でもインストールは可能だが,tensorflowはCUDA 10.0に対応しており,デフォルトだと最新版である10.1(10.2?)がインストールされるためバージョンを指定してインストール.

$ sudo apt-key adv --fetch-keys http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub
$ wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.1.168-1_amd64.deb
$ sudo dpkg -i cuda-repo-ubuntu1804_10.1.168-1_amd64.deb
$ sudo apt update
$ sudo apt install cuda-10-0
$ reboot

$ export PATH="/usr/local/cuda/bin:$PATH"
$ export LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
$ nvcc -V

で確認.

この時,nvidia-smiとnvccで表示されるCUDAバージョンが異なる場合があるが,気にしないで大丈夫そう.

cuDNN7.4のインストール

以下URLよりcuDNNをダウンロード(登録が必要).

https://developer.nvidia.com/rdp/cudnn-download

  • cuDNN Runtime Library for Ubuntu18.04 (Deb)
  • cuDNN Developer Library for Ubuntu18.04 (Deb)
  • cuDNN Code Samples and User Guide for Ubuntu18.04 (Deb)

この3つをダウンロードする.

以下コマンドでcuDNNをインストール

$ sudo dpkg -i libcudnn7_7.4.2.24-1+cuda10.0_amd64.deb
$ sudo dpkg -i libcudnn7-dev_7.4.2.24-1+cuda10.0_amd64.deb
$ sudo dpkg -i libcudnn7-doc_7.4.2.24-1+cuda10.0_amd64.deb

以下のようにコマンドだけでもインストールは可能とのこと(試していない). 以下のコマンドだとcuDNN 7.5が入る.

$ echo "deb https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64 /" | sudo tee /etc/apt/sources.list.d/nvidia-ml.list
$ sudo apt update
$ sudo apt install libcudnn7-dev=7.5.0.56-1+cuda10.0

各種フレームワークのインストール

tensorflow 1.13

pip installで必要なものは全部入る.
基本的にデフォルトだと最新のものが入るので必要ならバージョン指定する.

$ pip install numpy scipy scikit-learn pillow h5py
$ pip install tensorflow-gpu

keras

tensorflowが入っている環境でpip installするだけ.

$ pip install keras

chainer

numpy, cupyも含めてまとめてpip installするだけ.

$ pip install numpy cupy chainer

参考

UbuntuにNvidia GPUのDriver + CUDAをInstallする(GTX1080対応版) - Qiita

Ubuntu 18.04へのCUDAインストール方法 - Qiita

初めて自作PCを組んでみた

こんにちは,なめこです.

今回初めてPCを自作したのでその備忘録及び日記的な何かを残します.

目的

機械学習,というよりDeep Learningを気軽に試せるローカル環境が欲しくなったのでデスクトップPCが必要になりました. 流行りのクラウド環境にも一度は手を出したのですが,実行時間が金額に直結すると考えると「とりあえず学習させる」ということに中々踏み出せず断念しました...

BTOモデルではなく自作を選択した理由としては,

  • 単純に数万単位で安くなりそう
  • 欲しいところだけスペックを上げるみたいなことがしやすい

あたりですかね.
個人のDeep環境用のBTOモデル自体少なく,今回の自分にとっていい感じのものがなかったというのが大きいです.

完全初心者だったのでマザボ選びとかその他諸々のパーツのメーカーや型番をどう選べばいいのかわからなくてずっとぐるぐるしてたのですが,それはまた別の話.

予算

まずBTOモデルで要求をある程度満たしているものを調べたところ20万くらいでした. その上で自作PCの記事とかをいろいろ確認して,15万くらいあればなんとかなるかなって感じだったので予算は15万前後としました.

パーツ紹介

ほぼ全てのパーツに関しての知識がなさ過ぎてどういった基準で選べばいいのかわからなかったので,調べたり有識者に聞いてみたり1人で勝手に悩んだりしながら最終的にえいやって感じで決めていきました.

たぶんもう少しいろいろ吟味すればもうちょっと安くなったと思います.後悔はしてないのでいいですが.

CPU

インテル Core i5-9600K

最近ようやくインテル製のCPUの性能感がわかるようになってきたのでインテルにしました.
AMDはまだよくわかりません.

Deep環境ならCPUよりGPUとかメモリにお金かけた方が良いとのことだったのでi5です.

ゲームをする予定がないですし(やったとしてもそんなに重いのしない気がする),OCさせるつもりがなかったのでi5-9400とかを買う予定だったのですが,購入タイミングで色々あって気付いたら9600Kになってました.
てことでOCは飾りです.Kがついてるだけで強そうなのでまあいいでしょう(?).

マザーボード

ASRock Intel Z390 Extreme4

9世代CPUに対応,メモリ最大64GBまで載せれる,GPUが3つ載る,SLI対応(Zシリーズ)の要件を満たすものを選択.

拡張性をメインで考えました.
ただメモリはともかくとして,GPUを3つも載せることになるのかは疑問が残りますね.いっそマイニングでもさせますか.

ある程度欲しいスペックを定めた後は価格と雰囲気で選びました.

PC周りってなんでやたらと光るんですかね.

GPU

GIGABYTE グラフィックボード NVIDIA Geforce RTX 2070

Deep Learningの核となるGPU,結構迷いに迷いましたが,性能の高さとコスパを考えてRTX2070を選択.

メーカーを何基準で選ぶべきなのかの知識も経験も皆無だったので価格と見た目の感じで選びました.(玄人志向なめこが玄人じゃないのでやめました(?))
本音を言うと2080とか2080Tiとか欲しかったのですが,価格的に手が出ないなって感じだったので断念.(メモリ的にGTX1080Tiも一瞬視野に入ったけど高騰し過ぎてやばかったのでそっ閉じ)
将来的にはTitanとかTeslaあたりをホイホイ買えるようなお金持ちになりたいですね.

RTX2070だとSLIできないのでどうなんだろうとは思ったのですが,別にDeepでマルチGPU使うときはSLIを使わないとダメというわけではないですし,そもそもマルチGPUする人になるかすらわからないので割り切りました.
そう考えるとマザボも考え直してもいいのではって感じでしたが考え直すのが億劫だったのもあってえいやってしました.

因みにSLIという点ではGTX1070, 1070Ti, 1080あたりもできますが,単純にしばらく1GPUでやっていくことを考えるとまあ性能とコスパ的に2070がいいかなぁって感じました.(正直そんなに詳しくないので雰囲気.雰囲気でGPUを選んだ)

メモリ

CORSAIR DDR4 VENGEANCE LPX Series 16GB×2枚キット

画像を扱うならあればあるほど嬉しいメモリ.2万あれば32GB買えるとのことだったのでとりあえず32GB分を確保.
将来的には64GBまで増やしたいです.なくてストレスになるくらいならいっそ使い切れないほど欲しい派.

DDR4なら特に問題はないでしょうといった感じで選んでました.

余談ですが,今の所自分が新しくPC(ノート含む)を買うごとにメモリが倍になっているので2世代後くらいには128GBになってるかもしれません(なってない).

SSD

Samsung SSD 250GB 970 EVO Plus M.2

最初はSATAタイプを選んでたのですが,マザボがM.2対応なのにもったいないとお叱りを受けたのでこちらに変更.

とっても速いらしいです. 自分の持ってるPCがMacbook Air以外だと6年ほど前のSSDですらないdynabookなのでたぶん普通のSSDでも爆速気分だったと思います.

HDD

Seagate BarraCuda 3.5" 2TB HDD

6千円程度で2TBのHDDが買えるんですね.思ったより安くてびっくりでした.

特に何も考えず容量と価格で選択.

ケース

Thermaltake Versa H26 Black

ミドルタワーの安いやつ.それ以上でもそれ以下でもない脳死選択です.

強いて言うなら片面アクリルで中見えるとか,なんか標準搭載のファンが光るらしいとかはプラス査定でしたね.

電源

玄人志向 NEXTシリーズ 80 PLUS Bronze 600W ATX電源

ひとまず600Wあれば足りそうだった(そんな気がした.大丈夫だと思う.たぶん)のでお安いのを雑に選びました.

DVDドライブ

ASUS DRW-24D5MT

こちらもとりあえず安いものを選びました.

最悪必要ないものだった気がしないでもないですが,大した額ではないですし,必要になった時に困るよりはいいでしょう.

CPUクーラー

Thermaltake Contac Silent 12 サイドフロー型CPUクーラー

はい,購入直前でCPUをOCモデルにしたため完全に忘れていたCPUクーラーくんです.
これがないせいで他のパーツが届いてすぐに組むことができず,秋葉原まで買いに行きました.

せっかくなので(?)サイドフロー型を購入.取り付けが上手くできるわけがないのでプッシュピン方式です.

価格

価格の概算を置いておきます.

CPUクーラーを除き全部Amazonで一括で買ったのですが,最終的に156,000円程度になりました.
BTOよりは圧倒的に安いし,普通のショップでパーツ買い揃えるよりも安いし,なんなら全部お家まで持ってきてくれるので楽で最高にお得な気がします.
不安からちょっと高めのにしたりってのも含めてこのお値段なので満足です.

パーツ 値段
CPU 31,000
マザーボード 20,000
GPU 58,000
メモリ 18,000
SSD 9,000
HDD 6,000
ケース 4,000
電源 5,000
DVDドライブ 2,000
CPUクーラー 3,000
合計 156,000

組み立て

ドライバー片手に頑張りました.

組み立てること自体は初めてではなかったのですが(高専で一度組み立てからサーバ構築までやったはず),何年も前のことは覚えてなかったので実質初めてでした.
わからない部分(全部)は各種マニュアルとブラウザを行き来しながらなんとかしました.

CPUクーラーのプッシュピンの入りが甘くてクーラー設置時にプッシュピンが空を飛んだり,HDDを中々認識してくれなかったり,BIOSの設定難しいなぁってなったりしてました.
配線の綺麗さ?知らない子ですねぇ...

f:id:NAMEKO:20190615040145j:plain

こんな感じで青く光ります.特に考えずにパーツを選びましたがいい感じに寒色系で統一されました(?).

起動まで

Deep環境用ということでOSはUbuntu 18.04を選択.
手元にあったusbメモリをインストールディスクとして利用しました.

以下mac側のterminalのプチ備忘録

$ diskutil list
$ diskutil unMountDisk /dev/diskN
$ sudo dd if=(ubuntu image file) of=/dev/rdiskN bs=1m

あとがき

時間はかかりましたが,なんとか構成を考えて購入して組み立てて起動するまで走り抜けることができました.

CPUとかの最小構成パーツだけをマザーボードに取り付けた状態でBIOS起動した時が一番テンションが上がっていた気がします.
最後の方は疲労で死にかけていたので腰痛くらいしか覚えてないです.

さて,なんとかマシンが動くところまではきましたが,次はGPUを扱うための各種セットアップが待っています.
こちらは何度か経験はしていますが未だ慣れないので引き続き頑張っていきたいです.

つくばに来て1年が経ったので色々まとめてみた

こんにちは,なめこです.

つくばでも桜を見かけるようになり,気づけばすっかり春だなぁって感じの今日この頃.

そういえばもうつくばに来てから1年過ぎたんだなって若干感慨深くなったので,つくばのこととか,つくばに来てからのこととかを徒然なるままにまとめてみることにしました.

多分例によって長文化しますので悪しからず.短くまとめられるようになりたい(願望).

つくばのこと

環境

東京の秋葉原からつくばエクスプレス(通称TX)に乗り,1時間(快速なら45分ほど!早い!)ほど揺られるとたどり着く未開の地,それがつくばです.
...なんて言うとつくばを愛する方々には怒られる気がしますが,あながち間違いではないと思います.
終点であるつくば駅から出ると一瞬そこそこ都会かなと感じますが,筑波大学方向に進んでいくと見事な森が待っています.

自分の出身地もそれなりに田舎だと思っていましたが,「本当に関東圏なのん?」と疑問に感じるほどつくばは田舎です.

まず街灯がとんでもなく少ないです.ないところは本当に何もなくて夜になると真っ暗です.ライトで照らさないと何も見えないレベルで真っ暗です.
ただその分(?)星が綺麗です.少なくとも実家よりは綺麗でした.

飲食店,スーパー,コンビニ以外のお店が極端に少ないように感じます. 生きる分には問題ないですが,色々買い揃えようと思った時にTXで移動するか車を使うかしないと厳しいです.
あと遊ぶ場所はほとんどないです.おうちに集まってSwitchでもしましょう.

これは田舎とか関係ないですが,地盤がゆっるゆるです.
どれくらいゆるゆるかというと,近くを大型のトラックが通ると建物が揺れるような場所があったり,木の根っこに負けて歩道がガタガタする程度にはゆるゆるです.
道路の凹凸がいっぱいあるので歩いているだけで足腰が鍛えられます(たぶん).

あと地震が多いです.頻繁に揺れます.
そして揺れると「地下で◯実験」とか言い始める人が一定数います.本当にやってても不思議じゃない気がするのが不思議.

でも足元ばかり気にしているとそれはそれで危険です.
いつ屋根が落ちてきてもおかしくないのですから...

移動手段

学生たちの主な移動手段は自転車です.南北に5kmにわたって伸びる大きな大きな大学内を,たくさんの自転車が走り回っています.
運がいいとセグウェイの類を見ることもできます.なんて言うか自由ですね.

つくば駅から大学をぐるっと一周する循環バスも走っています.1周30分かかるというので驚きです.
そこそこの頻度で走ってますが,十中八九遅れてくるので注意が必要です.
しかし遅延読みでのんびり行くと定刻通りの運行をしていて逃したりもします.バスは難しいです.
因みに1年定期が8600円で買えます.大体月2往復もすれば十分に元が取れるので買い得です.

東京,というか外界へ出るためのほぼ唯一の手段であるみんな大好きTXですが,1時間は思ったより長いです.
そして高いです.東京での移動も考えると往復3000円は覚悟しましょう.時間的にも金銭的にも心理的にも,東京は想像以上に遠いところにあります.
さすが陸の孤島つくば.

車を持っているととんでもないレベルで重宝されます.そしてコストコパーティーの買い出し要員として駆り出されます.

住む場所

まず大体の人は自分の学類を元に住む場所のあたりをつけます.南北に5kmもあるんですから当たり前ですね.

あとは買い出しのしやすさなどでも決めたりしますが,去年つくばの真ん中あたりに新しいスーパーが増えたので選択肢は増えそうです.
因みに完全キャッシュレスです.あとお酒とつまみのバリエーションが豊富です.

もしこれからつくばの家を探す学生さんがいらっしゃいましたら,循環バスの経路付近に住むことをお勧めします.
インターンだとか就活だとかで頻繁に東京に行くことになってもそこまで大変じゃないです.

友達を自分の家に呼んだり東京に頻繁に行く可能性がある人は間違っても桜とかには住まないようにしましょう.
「遠いから」って言われて誰も来ませんし,つくば駅に行くまで時間がいっぱいかかるのでその時点で外出が嫌になります.

家賃相場はとっても低いです. 1K・8畳・風呂トイレ別・広々キッチン・室内洗濯機置場な条件でお部屋を借りる場合, リッチな感じの場所でも4~5万,大学の真ん中付近は3~4万くらいあれば住めます.
東京の真ん中で同じような部屋に住みたくても10万以上は当然のようにかかるのですごいですね.

北は北海道,南は沖縄まで.あと日本に限らず海外の人も多く,まさに多種多様な人がいます.ぐろーばる.
色んな訛りが聞けるのも楽しいです.

サークルに入ると色んな専攻の人と交流ができます.医学の人,心理の人,数学の人,地球の人,情報の人...etc.
いろんな価値観を持っている人と関わるのはとても楽しいです.場合によっては疲れますが,それでも多くの人と関わっておくのは悪くないと思います.

基本的にはいい人がいっぱいいると思うので積極的に関わっていきたい所存.

つくばに来てからのこと

言いたいこと言いたくないこと,言えること言えないこと,色々ありますが一度振り返っておきたいのでダイジェストで1年をお送りします.

入学から夏休みまで

期待に胸を膨らませて,とはいかなかった気がします.色んなことがうまくできるかなとビクビクしながら入学しました.
入学前に3年次編入生の集まりが何度かあり,知り合いはそれなりにいたので比較的気が楽でしたがそれでもビクビクです.

入学式で「Imagine The Future」を歌っているのを見て「これが大学か...」と圧倒されました.

入学式後に歩いていると大学2年生の方に話しかけられました.どうやらサークルの勧誘のようでした.当たり前ですが向こうはこちらを1年生だと思って話しかけています.まあ新1年生に見えますよね...ごめんね,実は3年生なの.
ときどき1年生と偽って話を続ける悪い遊びをしていました.いや,というか切り出すタイミングがわからない...コミュ障.

アルバイトを始め,サークルに入り,授業をいっぱいいっぱい詰めて自炊もちゃんとして...といった生活をしていました.
今考えると「なんで全部できると思ったの?」と小一時間問い詰めたくなるレベルであれこれ詰め込んで頑張っていました.
たぶん親元離れて大学まで来たんだから多くのことを完璧にやらなきゃと思い込んでしまっていたんですね.無理は良くないです.

まあ無理がたたって早々に体と精神がやられました.この頃は本当に何も楽しめていなかったと思います.

アルバイトをやめると体と精神にちょっと余裕が戻りました.何事も頑張りすぎるのは良くないなって感じです.

アルバイトを辞めたためお金がなくなってきてやばいと思いインターン先を探し始めました.
序盤はいっぱい落ちてメンタルブレイクされていましたがそれはまた別の機会に(たぶんない).

夏休み明けから年末まで

休み明けのタイミングから週1,2ペースで長期のインターンを始めました.
ちょっと無理やりインターンに行く日を空けたので(家から職場まで片道2時間かかるので1日勤務にしないと行けない),まだちょっと授業は詰まってましたが,やりたいことができていたので忙しいながらも充実していました.

この頃にコンテスト参加もしていました.久しぶりのデスマでした.

後半まあ,いろいろあって精神がやられていました.
この時知ったんですが,ストレスで味覚がおかしくなったりするんですね.初めて知りました.知見を得た.
あとストレスで食べるようになる人と食べなくなる人がいますが自分は後者でした.食べなくなると栄養が不足して悪循環に陥るので無理にでも食べましょう.
因みに個人的にはココアがお勧めです.あったかいやつ.亜鉛が多く含まれていてストレスに効くみたいなので最近は頻繁に摂取しています.

年始から今まで

考えに考えた結果院進をやめることにしました.
詳細はこちら(超絶長いので注意).

nanameko.hatenablog.com

そこからは基本インターンと就活とサークルをぐるぐるしています.
就活の話はまた今度まとめます.

この前サークルの合宿に参加してきました.頑張っていろんな人と話しました(えらい.みんな褒めて).
あと色々あって寸劇をしました.どうやらなめこは演技派なようです.

1年を振り返って

いろんなことに挑戦して,いろんな人と出会った1年でした.
そもそも地元を出ての一人暮らしなので,いろんなことが初めましてでした.

正の面でも負の面でも多くのことを体験しました.
負の面はまだ消化しきれていないものもありますが,とりあえず自然に消えない限りは墓場まで持っていく所存です.

まわりはいい人が多いのですが,やっぱり既にできているコミュニティに飛び込むのは難しかったです.
サークルも最近ようやく溶け込めるようになってきた気がします(もう引退済みですが).

食事をちゃんととって精神的に安定してから(たぶん今も若干不安定)色々振り返ってみると,入学直後くらいからずっと心が不安定な状態だったんだなと感じました.
精神的不安定な状態は自分がそうとは気づかないというのが怖いところですね.定期的に誰かと話すとか美味しいもの食べるとかひたすら寝るとかして無理をしすぎないようにしたいです.

先にも言いましたが,いろんなことを上手くやろう,完璧にやろうと神経質になりすぎていました.
もう少し余裕を持って,雑にゆるく生きられるように頑張らないことを頑張りたいです.

以下自分への戒めも込めて.

  • お金の管理は多少雑なくらいがちょうどいいです.
  • ご飯は隔週でもいいので時々は何も考えずに好きなものを食べましょう.ちょっとした外食くらいしてもいいじゃない.
  • 間食を増やして甘いものをいっぱい摂取しましょう.糖分は幸せになれます.
  • 将来への不安は丸めて何処かにポイしましょう.テキトーに誰かに愚痴るのも良いです.
  • もっと周りにちゃんと目を向けましょう.自分が思っているより自分のことを気にかけてくれている人は案外多いです.
  • 音楽を流しましょう.なんとなく落ち着く感じがあります.
  • 辛くなったらひたすら寝ましょう.睡眠不足は鬱の元です.
  • ココアを飲みましょう.追い詰められているときに飲むと急に視界が開ける感覚を味わえます.もしかしなくても薬物の類では?

こちらに来てからいろんなことが上手く回らず,人生って難しいなぁって感じになっています.
でも頑張りすぎるのは良くないとわかったので,ほどほどに,ゆるーく生きていきたいです.

Bug Shooting Challenge #2 に参加してきました

f:id:NAMEKO:20190303021857j:plain

こんにちは.なめこです.

2019/03/02にmixiの開催するワークショップBug Shooting Challenge #2(以下BSC)に参加してきたのでその概要や感想など色々書きます(途中途中に飯テロが挟まっています).

BSCとは

株式会社ミクシィ主催の学生対象の不具合調査体験ワークショップです.
ミクシィはこの他にGit ChallengeやTDD Challengeといったまた別のワークショップも開催しており,本イベントはそのうちの1つです.

詳細はこちらをご覧ください.

ざっくり説明すると,なんらかのバグが存在しているゲームサーバが与えられ,そのログから不具合の原因の特定・修正に挑戦するイベントです.

経緯

ミクシィの中の人に勧められて言われるがままに応募.
事前課題を極限まで熟成した後に提出しました.

その後無事選考を通過し,お呼ばれしたので参加しました.

準備

プログラムはRuby on Railsで書かれているとのことだったので(というか事前課題の時点でRails),Railsの勉強を1週間かけてやったりやらなかったり諦めたりしてました.

最終的にはMVCモデルに対するざっくりとした知識とrubyの基本構文を携え,一部メタ読みを踏まえて準備が完了しました(何も完了してないですね).

体験記

スケジュールは以下のような感じです.

  • イベントの説明や利用する技術の紹介
  • ランチ
  • 実際に不具合修正に挑戦
  • 懇親会

イベント概要説明・技術紹介

参加者は「2人だけのCREとして不具合調査を行う」という設定です.なので2人1組で協力して挑戦します.

因みになめこはdeltaチームでした.単にA,B,C,...と順にアルファベットを降って良さげな単語を選んだだけだとは思いますが,かっこよくていいですね,delta.

また,サービスの構成に関する説明と使用するツールについてのハンズオンがありました.
Docker,RailsAWSHadoopに関する爆速説明...雰囲気はわかったので良しとしましょう.

そんな感じで前提知識を軽く説明して午前は終了しました.

ランチ

振り返ると大量の釜飯が鎮座していました.

f:id:NAMEKO:20190303010636j:plain

中身はこんな感じでした.とても豪華なランチ...

f:id:NAMEKO:20190303025004j:plain

釜飯の種類が4種類くらいあったのにも驚きました. コンプリートするにはあと3回来ないと...(※BSCは現在1人1度しか参加できません)

ミクシィのエンジニアの方と一緒にランチをしながら色々な話を聞きました.

Challenge

説明を受け,お腹も膨れたところで本題です.

詳細は書けないのでふわっとした感じになりますが,とりあえず不具合調査の流れとしては以下ような感じです.

  1. 問題の内容を元にログを解析する
  2. 手元の環境で再現する
  3. バグの原因を探り,対策する
  4. バグの再現ができないことを確認する
  5. PRを送る

問題は全部で3問ありました.

1問目はどこから手をつければいいかがわからなくて序盤はただあたふたしていました.
チームメンバーがいい感じのログを見つけてきてくれたので,それを元にいろいろしてたら原因が見つかりました. バグを修正しPRの説明文を唸りながら書き上げて提出しました.

2,3問目はツールの使い方にも慣れが出てきたので2人で若干の役割分担をしつつ進めていきました. 紆余曲折はありましたがなんとか時間ギリギリで滑り込みPRできました.

とりあえず完答できたのでよかったです.

回答の採点後,総合MVPと各問題のMVPが発表されました(時系列的には懇親会の後半).

総合MVPは取れませんでしたが,2問目でのMVPをいただきました.
ぎりぎり首の皮一枚繋がってるような状態で回答していたので選ばれるとは思ってませんでしたが,講評を聞くとなるほどなぁと.

懇親会

ランチに引き続き懇親会も豪華でした.

f:id:NAMEKO:20190303031817j:plain

因みに冒頭の写真は懇親会で出たマカロンです.スイーツの類も中々豊富だったので満足です.

結構いろんな輪に入って多くの人と話せたんじゃないかと思います.(当社比+80%).
他の参加者やミクシィの中の人と色々と話をすることができ,技術の話や進路の話,仕事の話などをたくさんしました.
世間は広かったり狭かったりするなぁと感じました.

感想

何度かアプリ開発系のコンテストに参加したことがある程度で,こういったイベントへの参加は初めてでしたがとても充実した内容だったように思います.

単なるバグの修正の経験だけに留まらず,同種のバグを生み出さないために何を意識して開発を行うべきかや,バグが発生した場合のユーザー対応など,普段の個人的な開発やプロトタイプレベルのアプリ開発では目を向けてこなかった面がまだまだあることに気づくことができました.

具体的な課題としては,体系化された知識を正しくつけておくべきだと感じました.その場しのぎの開発は現状のスキルでも十分可能ですが,保守の面から見た際の管理のしやすさを考えた時,今の自分の付け焼きの知識だけでは不十分だと思います.プロダクト全体を俯瞰して見るためにはやはり十分な知識が必要です.

たった数行程度の間違いや不足であっても,ユーザーや会社に多大な損失を与えてしまう可能性があります.現実問題としてバグの存在しないプログラムを作ることは不可能ですが,それでも可能な限りバグを少なく,あるいは損失を小さくできるように常日頃から意識していければと思います.

謝辞

まずはチームメンバーの@ww_furu_tu に感謝を.ログ解析やバグ再現の面で特にお世話になりました.

またイベントの開催・紹介・運営をしてくださったミクシィの方々,本当にありがとうございました. 今回のイベントを経て見えてくるものは多く,自身の大きな糧となったと思います.重ねてお礼申し上げます.

最後に

曰く,「ブログを書くまでが遠足(BSC)」とのことです.

どんなものであれ,自身の経験をアウトプットしていくことは大事ですのでこれからも続けていきたいです.

蛇足

今回1問目と2問目の間に「もぐもぐタイム」なるものがあり,お菓子とドリンクをもらって話を聞きながら食べてました.

f:id:NAMEKO:20190303025535j:plain

やっぱり糖分摂取は必須ですよね.

画像処理.js -エッジ検出 3

こんにちは,なめこです.

jsでの画像処理3回目です.
前回の記事はこちらになります.

nanameko.hatenablog.com

今回はガウス関数を使ったエッジ検出を2種類実装していきます.

2つ目は普通のエッジ検出とはちょっと毛色が違う感じです.

DoG(Difference of Gaussian)

アルゴリズム

その名の通りGaussianの差分を元にエッジを検出します.

概要としては簡単で,分散の違う2つのガウシアンフィルタを適用した画像の差分を取り,その差が大きいものをエッジとします.

参考までに以下にグラフを示します.

f:id:NAMEKO:20190224181241j:plain

赤と青が分散の違う2つのガウシアン関数,緑がその差をとったグラフです.

ガウシアンフィルタはフィルタの総和が1になるように調整されているので,差分を取ると総和は0となります.
周辺画素の値と対象画素の値の差が小さい(エッジがない)と0に近づき,大きい(エッジがある)と適用後の値も大きくなります.

実装

let sig1 = 1.3;
let sig2 = 3.2;

const filterSize = parseInt(sig2*4+1);
const filterSizeHalf = (filterSize-1)/2;

function gaussian(sig, x, y){
    return math.exp(-(x*x+y*y)/2/sig/sig)/2/math.PI/sig/sig;
}

// DoGフィルタの作成
const filter1 = Array.from(new Array(filterSize)).map((v, i) =>
    Array.from(new Array(filterSize)).map((v, j) => 
        gaussian(sig1,i-filterSizeHalf,j-filterSizeHalf)
        ));
const filter2 = Array.from(new Array(filterSize)).map((v, i) =>
    Array.from(new Array(filterSize)).map((v, j) => 
        gaussian(sig2,i-filterSizeHalf,j-filterSizeHalf)
        ));

// 各フィルタの総和計算
const sumFilter1 = filter1.reduce((pre ,cur) => pre+cur.reduce((pre, cur) => pre+cur, 0),0);
const sumFilter2 = filter2.reduce((pre ,cur) => pre+cur.reduce((pre, cur) => pre+cur, 0),0);

// フィルタを正規化しつつ合成
const filter = map(filter1, (v, i, j) => v/sumFilter1-filter2[j][i]/sumFilter2);

// DoGフィルタ適用
const imgDog = conv.conv(img, image.sizes, filter, [filterSize,filterSize], {mode:conv.EXPAND});

// abs
const maxVal = max(imgDog);
const imgDogAbs = map(imgDog, v => v<0?0:(v/maxVal*255));

// 2値化
const imgDogAbsBin = bin(imgDogAbs, {threshold:20, maxVal:255});

2つのフィルタの正規化処理があるので少し長いです.

計算量を減らすために,別々にフィルタをかけてから差分を取るような方法ではなく,合成したフィルタをかけるようにしてあります.

フィルタ適用後の負の値を使用すると一つのエッジに対して複数回反応する(フィルタの両側に負値があるため)ので,負の値は切り捨てています.
また最大値が255となるような正規化も同時に行なっています.

結果を見やすくするために2値化処理も加えました.

実行結果

DoGフィルタの適用結果です.

計算量や実装の難しさはSobelフィルタ程度ですが,陰影への反応も少なく,主要なエッジは綺麗に検出できています.
ただしCanny法と同様にジグザグやグラデーションがかかってるようなエッジに関しては検出できていません.

f:id:NAMEKO:20190224160226p:plain
結果

f:id:NAMEKO:20190224160303p:plain
2値化したもの

FDoG(Flow-Based Difference of Gaussian)

FDoGはこちらの論文にある手法の一部です.

http://www.cs.umsl.edu/~kang/Papers/kang_tvcg09.pdf

アルゴリズム

DoGは等方性によって,エッジが途切れやすいなどの問題があります.
FDoGはエッジのベクトル方向を加味することでより綺麗なエッジ検出を可能とします(Sobelフィルタに対するCanny法のようなものです.たぶん).

ちょっと長くなるので今回は詳細は省きますが一連の流れとしては以下のような感じになります(詳細説明は全体の実装をした際にまとめてします).

  1. ノイズ除去
  2. エッジの接ベクトルを元にした流れ場(ETF)を求める
  3. 直交ベクトル方向に1次元DoGフィルタを適用
  4. ETFの曲線に沿って1次元Gaussianフィルタを適用

ノイズ除去にはGaussianフィルタを使用します.

2ではSobelフィルタでETFの元となるベクトル場を求め,ETFを求める処理を3回ほど繰り返します.

接ベクトルの直交方向にDoGフィルタをかけることでより効率的なエッジの検出ができます.
また,接ベクトル方向の曲線に沿ってGaussianフィルタをかけることで途切れているエッジをつなげることができます.

3,4の処理は複数回繰り返すことで結果が良くなるのでこちらも3回ほど繰り返します.

イラスト風の画像となるという特徴を持っており,処理後の画像も通常のエッジ検出とは違い,白地に黒で描かれたものになります.

実装

// 関数定義
const G = (r, sig) => math.exp(-(r*r)/2/sig/sig)/math.sqrt(2*math.PI)/sig;
const Wm = (g, g_) => (1 + math.tanh(g_ - g))/2;
const Wd = (tx, ty, tx_, ty_) => tx*tx_ + ty*ty_;

// 定数定義
let sigM = 2.5;
let sigC = 1.1;
let tau = 0.5;
let range = 3;

const sigS = sigC*1.6;
const rho = 0.997;

const T = parseInt(sigS*2+0.5);
const S = parseInt(sigM*2+0.5);

// 画像データ取得
const image = cv.imread(argv[0], cv.CV_8UC1);
const img = image.getDataAsArray();

const [height, width] = image.sizes;

// ノイズ除去
const imgGaus = gaussianBlur(img, {kSize:3});

// sobelフィルタ
let {sobelImg:g, sobelX:gx, sobelY:gy} = sobel(imgGaus);
sobelImg = normalize(g);
map(g, (v, i, j) => {
    if(v == 0)return;
    gx[j][i] /= v;
    gy[j][i] /= v;
    return;
});

// gx,gyを90度回転したものをtx,tyの初期値とする
let tx = map(gy, v => -v);
let ty = gx.concat();

// 正規化
g = normalize(g);

// ETF計算を複数回回すループ
for(let n=0;n<3;n++){
    console.log(`Edge Tangent Flow ${n} iteration`);

    const tx_cur = tx.concat();
    const ty_cur = ty.concat();

    // ETF計算
    map(g, (v, i, j) => {
        let tx_sum = 0;
        let ty_sum = 0;

        const tx_num = tx_cur[j][i];
        const ty_num = ty_cur[j][i];

        for(let l=-range; l<=range; l++){
            const j_ = j+l;
            if(j_ < 0 || j_ >= height)continue;
            for(let k=-range; k<=range; k++){
                const i_ = i+k;
                if(i_ < 0 || i_ >= width)continue;
                
                // 対象外領域は弾く
                const r = math.sqrt(k*k+l*l);
                if(r > range)continue;

                // 重み計算
                const w = Wm(v, g[j_][i_])*Wd(tx_num, ty_num, tx_cur[j_][i_], ty_cur[j_][i_]);

                tx_sum += tx_cur[j_][i_]*w;
                ty_sum += ty_cur[j_][i_]*w;
            }
        }

        // 正規化
        const r = math.sqrt(tx_sum*tx_sum + ty_sum*ty_sum);
        if(r != 0){
            tx_sum /= r;
            ty_sum /= r;
        }

        // tx,tyを更新
        tx[j][i] = tx_sum;
        ty[j][i] = ty_sum;
    });
}

// gx,gyを更新
gx = ty.concat();
gy = map(tx, v => -v);

// 定数定義
const THRESHOLD = math.cos(math.PI/8);

let He = img.concat();

// FDoGを複数回回すループ
for(let n=0;n<3;n++){
    console.log(`Flow Based DoG ${n} iteration`);

    // 直交方向に1次元DoGフィルタをかける
    const Hg = map(He, (_, i, j, img) => {
        const x = gx[j][i];
        const y = gy[j][i];
        if(x==0 && y==0)return 0;

        let Csum = 0;
        let Ssum = 0;
        let GCsum = 0;
        let GSsum = 0;

        // 角度に応じて探索方向を定める
        let dk = 0;
        let dl = 0;
        if(math.abs(x) < THRESHOLD){
            dl = 1;
        }
        if(math.abs(y) < THRESHOLD){
            dk = 1;
        }
        if(x*y < 0){
            dl = -1;
        }

        // DoG計算
        for(let l=-T; l<=T; l++){
            const di = l*dk;
            const dj = l*dl;
            const i_ = i + di;
            const j_ = j + dj;
            if(j_ < 0 || j_ >= height || i_ < 0 || i_ >= width)continue;

            const r = math.sqrt(di*di + dj*dj);
            gc = G(r, sigC);
            gs = G(r, sigS);
            GCsum += gc;
            GSsum += gs;
            Csum += img[j_][i_] * gc;
            Ssum += img[j_][i_] * gs;
        }
        return Csum/GCsum - rho*Ssum/GSsum;
    });

    // 流れ場に沿ってGaussianフィルタをかける
    He = map(Hg, (v, i, j, img) => {
        let x = tx[j][i];
        let y = ty[j][i];
        // 流れがなければ白地
        if(x==0 && y==0)return 255;

        sum = v;
        Gsum = G(0, sigM);

        // 接ベクトルの順方向と逆方向に探索
        for(let m=-1; m<2; m+=2){
            let r = 0;
            let i_ = i;
            let j_ = j;

            while(r < S){
                let di = 0;
                let dj = 0;
                if(math.abs(x) < THRESHOLD){
                    dj = m;
                }
                if(math.abs(y) < THRESHOLD){
                    di = m;
                }
                if(x*y < 0){
                    dj *= -1;
                }
                j_ += dj;
                i_ += di;
                if(j_ < 0 || j_ >= height || i_ < 0 || i_ >= width)break;

                r += math.sqrt(di*di + dj*dj);
                const g = G(r, sigM);
                Gsum += g;
                sum += img[j_][i_]*g;

                x = tx[j_][i_];
                y = ty[j_][i_];
                if(x==0 && y==0)break;
            }
        }

        sum /= Gsum;

        // 閾値処理
        return (sum<0 && (1 + math.tanh(sum)) < tau) ? 0 : 255;
    });
}

実行結果

実装時の定数をそのまま使用した場合の結果が以下になります.
DoGフィルタでは検出できなかったジグザグとしたエッジやグラデーションのかかった部分もこの手法では綺麗に検出できています.
またイラスト風というだけあって,陰影も表現されているのが面白いです.

f:id:NAMEKO:20190224160047p:plain
tau=0.5

比較のためtauを0.1, 0.9としたものを示します.tauを小さくすると陰影などの線が少なくなります.

f:id:NAMEKO:20190224160121p:plain
tau=0.1

f:id:NAMEKO:20190224160139p:plain
tau=0.9

まとめ

手軽に綺麗なエッジを取れるDoGとその改良版のFDoGを実装しました.

FDoGは処理は若干複雑ですが,ETF計算以外はガウス関数しか使っていないのが面白いです. またFDoGは自然画像に対して実行すると他の手法とは少し違った特徴が出てくるので試してみるといいかもしれません.

今回の実装含め,これまでのエッジ検出のプログラムは以下のGitHubのedgeディレクトリに置いてあります.

github.com

今回で一旦エッジ検出はクローズとなります.また面白いものが見つかったら書くかもしれません.

次回からは劣化画像修復に移る予定です. 今回までは畳み込みや対象画素付近の探索がメインでしたが,次からは行列計算がメインになります.

参考文献

【画像処理】DoGフィルタの原理・特徴・計算式 | アルゴリズム雑記

PC以外でもJSがしたい

こんにちは,なめこです.

今回はちょっとした思いつきに従ってReact Nativeで遊んでみます.

React NativeはiOSAndroid端末で動くアプリをJavaScriptで作れるフレームワークです.

以下の記事に構成に関する説明がされてます.

qiita.com

さて,まあ構成に関しては軽く流してもいいのですが,今回注目したのは以下の文

React Nativeで楽に作るスマホアプリ開発入門(基本編) - Qiita React NativeはWebkitスマホブラウザ)のJavaScriptランタイムで動く(つまり、スマホブラウザ上で動くようなコードを書いているイメージ)

要するに端末上でJSが動いてるわけですね.
それならアプリ上で任意のJSプログラムを動かすことも可能なのでは? と思い立ってしまったので挑戦してみることにしました.
上手くいけばタブレットなどでもNodeのプログラム実行ができるはずです.

今回はひとまずターミナルでのNodeの対話環境に近いものを目指します.

import周りとかstyle sheetに関する部分は本筋ではないので省略します. まとめたプログラムは最後に置いておきます.

前準備

ターミナル上で必要となる表示をstateとして用意しておきます. 入力と出力を履歴含めて保持できれば十分なのでstateの中身は少ないです.

this.state = {
  // 入力された文字列
  inputValue: '',
  // 入力履歴と出力のリスト
  list: [],
  // 入力履歴のリスト
  historyList: [],
};

JSが動くようにする

デフォルトで生成されるAppクラスの内部に関数_onPressを定義します. これは入力決定時(現時点ではENTERボタンを押した時)に実行されます.

そしてこの中で悪魔の関数を呼び出します. 文字列をJSのプログラムとして評価し,実行するevalさんです.
ある意味で便利ではありますが,危険度の高い関数でもあるのでご利用は計画的に.

evalの詳細は以下

developer.mozilla.org

_onPress = () => {
    // 現在のstateを取得
    const {inputValue, list, historyList} = this.state;

    // 入力文字を実行し,実行結果を取得
    const result = eval(inputValue);
    
    // 実行コードと結果を追加
    const _list = list.concat();
    const _historyList = historyList.concat();
    _list.push(inputValue+'');
    _list.push(result+'');
    _historyList.push(inputValue+'');

    // stateを更新
    this.setState({
      inputValue:'',
      list:_list,
      historyList:_historyList
    });
  };

全体を含む実行画面はこんな感じです.
ひとまずevalは機能しているようです.

f:id:NAMEKO:20190220233244p:plain

変数が保持されるようにする

先ほどの入力状態から以下のような入力をするとエラーが出ました.

f:id:NAMEKO:20190220233251p:plain

f:id:NAMEKO:20190220233254p:plain

_onPress内のevalで変数aにアクセスしたことでエラーが出てます.
原因は割とお察しですが一応varなしでの挙動も確認しておきましょう.

f:id:NAMEKO:20190220233257p:plain

案の定普通に動きました. varなどの宣言子を用いて変数宣言を行った場合,関数処理後にそのスコープを抜け,その場で宣言されたローカル変数は消えます. しかし何もついていない場合,グローバル変数として作られるので変数が残ります.

今回はvarで宣言した変数aが消えているので,ないものにアクセスしようとしてエラーになってるわけです.

varの他constやletも同様にエラーとなるので,とりあえず気にせずできるように今はこれらの文字列を消しておきます.

文字列の変換関数_encodeを定義してevalの実行前に呼び出すようにします.

// '(var|const|let) 'を削除
_encode = (str) => str.replace(/(var|const|let) /g, '');
// 入力文字を実行し,実行結果を取得
const result = eval(this._encode(inputValue));

f:id:NAMEKO:20190220233301p:plain

これで無理やりではありますがとりあえず動くようになりました.

文字列が使えるようにする

これで変数周りは一通り完了...だと良かったのですがまだ問題がありました.

f:id:NAMEKO:20190220233304p:plain

文字列を扱おうとこんなプログラムを書いてみると以下のようなエラーが出ました.

f:id:NAMEKO:20190220233308p:plain

\u2018????
と一瞬なりましたがUNICODEの「' (シングルクォーテーション)」ですね.
どうやら入力はUNICODEのようです. 明示的に変換してあげましょう.

_encode = (str) => str.replace(/(var|const|let) /g, '').replace(/(\u2018|\u2019|\u201c|\u201d)/g, "'");

ついでに「" (ダブルクォーテーション)」の方も変換してあります.

f:id:NAMEKO:20190220233312p:plain

これで文字列も使えるようになりました.

もちろんオブジェクトや配列も利用できます.

f:id:NAMEKO:20190220233317p:plain

変数宣言以外での宣言子の文字列を残す

文字列を扱えるようになってから気づきましたが,現状だと「var 」がどこにあっても削除されてしまいます

f:id:NAMEKO:20190221000909p:plain

正規表現をうまく使って文頭にある場合にのみ削除するようにします. 文頭指定だけだと「 var 」みたいな形になると反応しなくなるので,文頭から空白が続いた場合も対象になるようにします.

_encode = (str) => str.replace(/^ *(var|const|let) /g, '').replace(/(\u2018|\u2019|\u201c|\u201d)/g, "'");

f:id:NAMEKO:20190221001353p:plain

これでひとまずおかしな挙動は消えたと思います.

まとめ

今回は対話形式でのプログラミングができるようにしてみました.

変数宣言周りは無理やりなのでもう少しいい方法がないか検討してみますが,しばらくはこのままになりそうです.

少しずつ体裁とか色々整えていって色々できるようにしていきたいです.

プログラム

import React, {Component} from 'react';
import {
  Platform, 
  StyleSheet, 
  Alert,
  Text, 
  TextInput, 
  Button, 
  ImagePicker,
  Permissions,
  View,
  TouchableOpacity,
  FlatList
} from 'react-native';

type Props = {};
export default class App extends Component<Props> {
  constructor(props){
    super(props);
    this.state = {
      inputValue: '',
      // 入力履歴と出力のリスト
      list: [],
      // 入力履歴のリスト
      historyList: [],
    };
  }

  // テキスト入力の反映
  _onChangeText = inputValue => this.setState({inputValue});
  _onPress = () => {
    const {inputValue, list, historyList} = this.state;

    // 入力文字をエンコードした上で実行し,実行結果を取得
    const result = eval(this._encode(inputValue));
    // if(typeof(result) === 'string' || result instanceof String){
    //   result = `'${result}'`;
    // }
    // Alert.alert(result);
    // const result = this._data(this._encode(inputValue));
    // const result = eval(inputValue);
    
    // 実行コードと結果を追加
    const _list = list.concat();
    const _historyList = historyList.concat();
    _list.push(inputValue+'');
    _list.push(result+'');
    _historyList.push(inputValue+'');

    this.setState({
      inputValue:'',
      list:_list,
      historyList:_historyList
    });
  };

  // '(var|const|let) 'を削除,''や""を使用できるようにする
  _encode = (str) => str.replace(/^ *(var|const|let) /g, '').replace(/(\u2018|\u2019|\u201c|\u201d)/g, "'");
  
  render() {
    const {
      inputValue,
      list,
      historyList
    } = this.state;
    
    return (
      <View style={styles.container}>
        <FlatList style={styles.list} data={list} renderItem={({item}) => <Text style={[styles.item, styles.color]}>{item}</Text>} />
        
        <View style={styles.separator}/>
        <View style={styles.input}>
          <Text style={styles.color}>{'> '}</Text><TextInput style={[styles.inputArea, styles.color]} value={this.state.inputValue} onChangeText={this._onChangeText}/>
          <TouchableOpacity onPress={this._onPress}>
            <Text style={styles.color}> ENTER </Text>
          </TouchableOpacity>
        </View>

      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#1D1D1D',
  },
  input: {
    flexDirection:'row',
    height:20,
    margin:10,
  },
  list: {
    flex:1,
    margin:10,
    marginTop: 20,
  },
  item: {
    fontSize: 15,
    textAlign: 'left',
  },
  inputArea: {
    fontSize: 15,
    flex: 1,
  },
  color: {
    color: '#FEFEFE',
  },
  separator: {
    height: 1,
    backgroundColor: '#FEFEFE',
  },
});

画像処理.js -エッジ検出 2

こんにちは,なめこです.

jsでの画像処理2回目です.
前回の記事はこちらになります.

nanameko.hatenablog.com

今回はなんとなくエッジ検出について調べた時に見つけた手法を実装してみたいと思います.

こちらの記事の既存手法と提案手法(色情報と大津の閾値判別法を用いたエッジ検出)を両方実装していきます.
(既存手法の名称はわからなかったのでここでは「動的色境界によるエッジ検出」とします)

※詳細なアルゴリズムに関しては書かれていなかったので,正確には"それっぽい"実装となります.全くの見当外れではないとは思いますが100%正しいものではないと思いますので悪しからず

www.ail.cs.gunma-u.ac.jp

色情報を用いたエッジ検出のメリット

先に今回のような色情報RGBを使用することのメリットを簡単に説明しておきます.

まず前提として,カラー画像からグレースケール画像へ変換する際は以下のような式による計算が行われています.

$$Y = 0.299\times R+0.587\times G+0.114\times B$$

式に示すまでもないとは思いますがRGB -> Grayは非可逆な変換であり,RGBの組み合わせとYは一対一での対応はできません.
色によって係数が大きく異なるのも特徴的かもしれません.
そのためカラー画像の時に差が大きく明確であった境界も,グレースケール画像に変換することでその差が小さくなってしまう場合があります.

例えばRGBで表現された以下の2つの色 $$R:255, G:0, B:255$$ $$R:0, G,180, B:0$$ これはカラー画像においては全く違いますが,グレースケール画像に変換するとほぼ同じものになります.

つまりこういった色変化によって生じるエッジがある場合,グレースケール画像を前提とした手法では検出できません(あるいは検出できたとしても弱いエッジとなるためその後の処理で切り捨てられる可能性がある).

色の境界を用いたエッジ検出を行うことで,これらのエッジを検出することができます.
実用例としては道路標識検出などがあるようです. 駐車禁止の道路標識などがグレースケールでの手法だと検出できないようですね.

動的色境界によるエッジ検出

ひとまず簡単そうな既存手法から始めます.

アルゴリズム

記事に示されていた処理の流れとしては以下の通りです.

  1. カラー画像をRGB成分に分解
  2. 分解した画像を2値化
  3. 三つの画像を統合(3bit画像の生成)
  4. 色の変化している部分をエッジとして判定

3を経た上での4の処理の詳細がちょっとよくわからなかったので,今回は以下の流れで処理することにしました.

  1. カラー画像をRGB成分に分解
  2. 分解した画像を2値化
  3. 3つの画像それぞれでエッジ検出
  4. 3つのエッジ画像のいずれかでエッジであればエッジとして判定

2値化には大津の閾値判別法(判別分析法)を用います(2値化の代表的な手法なので詳細は省きます).

エッジ検出は既にSobelフィルタを実装済みなのでこちらの方が楽だと判断しました.
3bit画像で色変化が起きている部分を~という処理と大きな違いはないように思います.

実装

今回カラー画像として読み込んであるので,imgは3次元配列となります. ちなみにbinは2値化,sobelはSobelフィルタを適用する関数となります.実装の詳細はGitHubへどうぞ.

const imgBGR = [];
const imgBGRbin = [];
const imgBGRsobel = [];

// 色ごとに操作
for(let n=0; n<3; n++){
    // 単色のデータ取得
    imgBGR[n] = map(img, v=>v[n]);
    // 2値化
    imgBGRbin[n] = bin(imgBGR[n], {maxVal:255});
    // エッジ検出
    imgBGRsobel[n] = sobel(imgBGRbin[n]).sobelImg;
}
// RGB画像に戻す処理(3bit画像の生成)
const imgBin = map(imgBGRbin[0], (v,i,j) => [v, imgBGRbin[1][j][i], imgBGRbin[2][j][i]]);
// いずれかの色画像にエッジがあればカラー画像のエッジとして処理
const imgEdge = map(imgBGRsobel[0], (v,i,j) => (v+imgBGRsobel[1][j][i]+imgBGRsobel[2][j][i])>0?255:0);

RGB画像って言ってるのになんで変数名がBGRなのかというと,OpenCVではBGRの順になっているからです.まあ別にどの色を使うとかは考える必要はないのでなんとなく合わせてるだけですが.

単色のデータの取り出しは比較的簡単でしたが,RGB画像に戻す処理がちょっと汚いコードになってます.
綺麗な処理を思い付いたらしれっと直すかもしれません.メモリアクセス的に残念な気がしますし.

最終的なエッジ画像の作り方も結構雑です.3色のエッジ画像の画素を足して値があればエッジにしています.
今回は勾配の大きさによる切り捨ては行なっていません.

実行結果

2値化後の3bit画像及び今回の手法でのエッジ検出の結果を示します.

前回のSobelやCanny法によるエッジ抽出と比較するとよくわかりますが,グラデーションなどで境界が曖昧な部分もかなり綺麗に検出できています.
ただその代わりに陰影の部分に強く反応してしまっています.

f:id:NAMEKO:20190217040626p:plain
3bit画像

f:id:NAMEKO:20190217040712p:plain
エッジ抽出結果

色情報と大津の閾値判別法を用いたエッジ検出

既存手法である動的色境界によるエッジ検出の改良版です.

既存手法では明暗が激しい画像において(逆光の強い画像など),明暗の境界として表現されるのエッジしか検出できない問題があるため,それへの対応を行った手法です.

アルゴリズム

基本的には上記の既存手法と同様ですが,この手法では画像を小領域に分割してそれぞれに処理を行います.

大津の閾値判別法による閾値決定を小領域ごとに行うことで,小領域ごとの明るさの違いに対応した閾値が定まります.
ただしこれでは小さすぎる変化にも反応してノイズとして出てしまうので,小領域全体の明るさに対応したまた別の閾値を定め,その閾値以下であればその小領域内のその色の変化を無視するようにしています.

また最初にノイズ除去のためのガウシアンフィルタをかけています.

処理の流れとしては以下の通りです.

  1. 画像全体(各色ごと)にガウシアンフィルタをかけてノイズ除去
  2. 画像を小領域に分割
  3. 分割画像ごとに2値化(大津の閾値判別法 +a )
  4. 2値化した分割画像ごとにエッジ検出
  5. 分割画像の結合

このうち3の+aの詳細は不明だったので,「小領域全体の輝度の平均」と「大津の閾値判別法で求められる分散」の比が一定以下となる場合に色変化をなくす,という方法を適用することにします.
完全に「こんな感じかな?」って感じの思いつきレベルですのでどれほど正しいかは不明です

実装

let splitsize = 64;
let threshold = 1;

// ガウシアンフィルタ設定
const gaussianFilterSize = 5;
const gaussianFilterSizeHalf = (gaussianFilterSize-1)/2;
const sigma = gaussianFilterSizeHalf/2;
const gaussianFilter = Array.from(new Array(gaussianFilterSize)).map((_, j) =>
    Array.from(new Array(gaussianFilterSize)).map((_, i) => {
        return math.exp(-(j*j+i*i)/(2/sigma/sigma))/(2*math.PI*sigma*sigma);
    }));

// 変数準備
const imgBGR = [];
const imgBGRbin = [];
const imgBGRsobel = [];
const sizes = image.sizes;
const rowNum = sizes[0]/splitsize;
const colNum = sizes[1]/splitsize;

// 色ごとに操作
for(let n=0; n<3; n++){
    // 単色のデータ取得
    const v = map(img, v=>v[n]);
    // ガウシアンフィルタの適用
    const vGaus = conv.conv(v, image.sizes, gaussianFilter, [gaussianFilterSize,gaussianFilterSize], {mode:conv.EXPAND});
    const val = map(v, v=>0);
    const valBin = map(v, v=>0);

    // 小領域ごとに処理
    for(let j=0; j<rowNum; j++){
        for(let i=0; i<colNum; i++){
            const img_small = v.slice(j*splitsize, (j+1)*splitsize).map(v => v.slice(i*splitsize, (i+1)*splitsize));
            
            // otsuの2値化
            const img_smallbin = (img => {
                maxVal = 255;
                const hystgram = Array.from(new Array(256)).fill(0);
                let t = 0;
                let sum = 0;
                let sumPow = 0;
                let omg = img.length*img[0].length;
                for(let V of img){
                    for(let v of V){
                        hystgram[v]++;
                        sum += v;
                        sumPow += v*v;
                    }
                }
                // 全体平均の計算
                const mean = sum/omg;
                if(mean == 0){
                    return map(img, v => 0);
                }

                let omg1 = 0;
                let sum1 = 0;
                let sigmab_max = 0;
                for(let [i, n] of hystgram.entries()){
                    omg1 += n;
                    sum1 += i*n;
                    const omg2 = omg - omg1;
                    const sum2 = sum - sum1;
                    const m = sum1/omg1 - sum2/omg2;
                    const sigmab = omg1*omg2*m*m;
                    if(sigmab > sigmab_max){
                        sigmab_max = sigmab;
                        t = i;
                    }
                }
                sigmab_max /= omg*omg;
                // 全体平均と分散による閾値処理
                if(sigmab_max/mean < threshold){
                    return map(img, v => 0);
                }
                return map(img, v => t<v?maxVal:0);
            })(img_small);

            // Sobelフィルタの適用
            const img_smallsobel = sobel(img_smallbin).sobelImg;

            // 画像の結合処理
            for(let l=0; l<splitsize; l++){
                for(let k=0; k<splitsize; k++){
                    if(j*splitsize+l < sizes[0] && i*splitsize+k < sizes[1]){
                        valBin[j*splitsize+l][i*splitsize+k] = img_smallbin[l][k];
                        val[j*splitsize+l][i*splitsize+k] = img_smallsobel[l][k];
                    }
                }
            }
        }
    }
    // 2値化画像
    imgBGRbin[n] = valBin;
    // エッジ画像
    imgBGRsobel[n] = val;
}
// RGB画像に戻す処理
const imgBin = map(imgBGRbin[0], (v,i,j) => [v, imgBGRbin[1][j][i], imgBGRbin[2][j][i]]);
// いずれかの色画像にエッジがあればカラー画像のエッジとして処理
const imgEdge = map(imgBGRsobel[0], (v,i,j) => (v+imgBGRsobel[1][j][i]+imgBGRsobel[2][j][i])>0?255:0);

記事では小領域を50x50としていましたが,キリのいい数字が良かったので64x64としました.
また閾値は何度か試してみてとりあえず設定した値です.

既存手法の実装での2値化処理は用意しておいたモジュールに投げてましたが,今回は大津の閾値判別法の処理途中で求められる画素の総和や分散などが必要だったためその場で即時関数として実装しています.

分割・結合処理は正直なところかなり無駄が多いのでもう少しマシな方法を考えることにします.

実行結果

小領域ごとに2値化している関係で少し妙な感じの3bit画像ができています.
また,3bit画像を見るとわかりますが,エッジが小領域の端の部分にしか含まれていない場合は高確率で切り捨てられています.
これが原因で一部エッジが途切れていますね.

ただ既存手法で反応が強かった陰影には今回ほとんど反応していないので一長一短といったところでしょうか.

ちなみに小領域のサイズを変えると結果が全然違ってきます.画像によって最適な小領域のサイズが全然違ったので色々試してみる必要がありそうです.

f:id:NAMEKO:20190217042532p:plain
3bit画像(領域分割して生成したものを再度結合)

f:id:NAMEKO:20190217042642p:plain
エッジ検出結果

まとめ

今回は色情報を用いたエッジ検出を行いました.

基本的には,グレースケールの時に使っていた処理を色ごとに使って後で結合するといった流れのようです.
前回の比較もしたかったのでイラストに対して実行しましたが,自然画像(特に道路標識が写っている逆光の強い画像)に対して実行してみるとその有用性が理解できるので面白いかもしれません.

今回の実装は以下のGithubのedge/dinamicColorBoundaryEdge.js(既存手法),edge/colorEdge.js(提案手法)にあたります.

github.com

次回は「流れ場を考慮したエッジ検出」を実装する予定です.
たぶん次でエッジ検出は一旦終わりですかね.

参考文献

OpenCV: Color conversions

色情報と大津の閾値判別法を用いたエッジ検出 - OhtaLabWiki

判別分析法(大津の二値化) 画像処理ソリューション