Go+GoCVでSIFT特徴量取得アプリとYOLOを使った物体検出アプリを作ってみる
こんにちは、株式会社SHIFT、自動化エンジニアの水谷です。
Go言語でのアプリ開発記事も今回で3回目になりました。今回はオープンソースの画像処理ライブラリとして有名なOpenCVをGoから使ってみたいと思います。
OpenCVを使うと言っても、高度な画像処理を行うアプリだとコードも長く、難易度も高くなるので、今回はSIFT特徴量を使った画像マッチングをお題にします。なぜSIFT特徴量をお題に選んだかというと、SHIFTに綴りが似ているから、という単純な理由ですがw
また、せっかくですのでYOLOを使った機械学習による物体検出もおまけ程度にやってみたいと思います。
OpenCVについて
OpenCVはインテルが開発して、オープンソースとして公開されているコンピュータービジョンライブラリで、だれでも無料で使用することができます(一部の機能はライセンスが異なりますので、ライセンス情報をしっかり読んで使用する必要があります)。OpenCVはC++で作られたクロスプラットフォームライブラリなのですが、最近ではPythonから使用することも多いですね。
OpenCVは画像処理に関するありとあらゆる作業ができるのではないかと思うほど関数(メソッド)が多く、しかも高度にチューニングされているので、動作速度が速いことも特徴です。このようなライブラリが無料で使用できることは、とても幸せなことだなと思いながら、ありがたく使わせていただこうと思います。
GoCVのインストール
さて、GoCVのインストールについてですが、少し手順が複雑です。先ほども書きましたようにOpenCVはC++で書かれていますので、前回のwxWidgetと同じように、Goから使うにはそのソースコードをビルドし、Goのラッパー(GoCV)から呼び出す形になります。
ということで、まずはOpenCVをGoCVから使える形でビルドするのに必要なツールをインストールします。そのツールは2つあり、コンパイラのgccと、makeファイル(やVisual Studio用のソリューションファイル)を生成するCMakeというツールです。
まずgccですが、GNUツールチェーンのWindows移植版であるMinGWをインストールするのが手っ取り早いかと思います。
MinGWのインストール
MinGWのインストール方法は前回も書いたように、下のサイトから64ビット版のインストーラーをダウンロードして、デフォルトオプションでインストールすればOKです。
https://mingw-w64.org/doku.php/download
インストールが終わりましたら、gccがインストールされているフォルダにパスを通しておいてください(バージョンによって異なりますが、例えば C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin)。
CMakeのインストール
続いてCMakeですが、こちらもオフィシャルサイトから64ビット版のインストーラーをダウンロードして、デフォルトオプションでインストールすればOKです。
インストールが終わりましたら、cmake.exeがインストールされているフォルダにパスを通しておいてください(デフォルトではC:\Program Files\CMake\bin)。
GoCVのインストールとビルド
それでは、GoCVをインストールします。まずは、以下のコマンドを実行してダウンロードします。
go get -u -d gocv.io/x/gocv
続いて、コマンドプロンプトでビルドを行います。
cd %GOPATH%\gocv.io\x\gocv
win_build_opencv.cmd
OpenCV 4.5.3のソースコードをダウンロードして、CMakeでMingGW用のmakefileが作成され、ビルドとインストールが行われます。かなり時間がかかります(私が使っている Core i5-8250U メモリ8GBのノートPCで1時間強)ので、気長に待ちましょう。
また、最後のステップでディレクトリ変更に失敗するかもしれませんが、これは問題ではありません。
なお、OpenCVはデフォルトでは C:\opencv 以下でビルドされていますので、"C:\opencv\build\install\x64\mingw\bin"にパスを通しておきましょう。
GoCVの動作確認
新たにコマンドプロンプトを開いて、以下のコマンドで動作確認を行いましょう。
cd %GOPATH%\gocv.io\x\@v0.28.0\cmd\version
go run main.go
ビルドと実行に数十秒程度かかりますが、下のようにバージョン情報が表示されれば、インストール成功です。
gocv version: 0.28.0
opencv lib version: 4.5.2
GoCVを使ったアプリ開発
今回作ったアプリでは、PCに接続されているカメラで撮った画像の中から、参考画像に映っているもの(今回はSHIFT社内誌SHIFTOMOの表紙)をSIFT特徴量を使ってリアルタイムに見つけ出すようなことをしています。
SIFT特徴量
SIFT特徴量は、異なる画像間でのマッチングを行うための数値(高次元ベクトル)で、対象物が回転していても、拡大縮小されていても、明るさが異なってもその値が(ほとんど)変わらないことが特徴です。画像中の特徴点(SIFT特徴量が大きい点)同士を対応させる(1つ目の画像の特徴点が2つ目の画像のどの位置に対応するかを特徴量を比較することで見つけ出す)ことで、例えばパノラマ写真作成時に異なる画像間で、どの位置でどの角度で繋げればよいか、などがわかります。
ディレクトリの準備
では、アプリの開発に進みましょう。まずは以下のようにディレクトリの作成とプロジェクトの初期化を行います。
mkdir cvSIFT
cd cvSIFT
go mod init example.com/cvSIFT
GoCVをインポートする
まずコードの先頭でGoCVをインポートします。今回は画像のグレースケール変換も行いますので、image/colorも合わせてインポートします。
import (
"image/color"
"gocv.io/x/gocv"
)
比較元画像の読み込み
続いて、比較元画像を読み込みますが、これはgocv.IMReadで簡単に行えます。この際、第2引数にgocv.IMReadGrayScaleを指定することで、グレースケール画像として読み込んでいます。
shiftomo := gocv.IMRead("SHIFTOMO.jpg", gocv.IMReadGrayScale)
defer shiftomo.Close()
2行目のように、"defer"行を書いておくことで、画像が不要となれば確実にClose()を行ってくれます。
カメラのオープン
今回のアプリでは、Webカメラから画像を取得しますので、カメラをオープンします。
このためのコードも、下のようにとても簡単です。
webcam, err := gocv.OpenVideoCapture(0)
if err != nil {
return
}
defer webcam.Close()
gocv.OpenVideoCapture()の引数は、カメラのデバイスID(デバイス番号)で、0から始まる整数です。複数カメラが接続されている場合は、この値を変更することでカメラを指定します。
比較元画像の特徴点抽出
その後カメラ画像用のMat(画像データ)や、SIFT特徴量を計算するオブジェクトのインスタンスを作った後、元画像のSIFT特徴点の抽出(および特徴量の計算)を行います。OpenCVでは、この複雑な処理を1メソッドで行ってくれますので、GoCVでのコードも以下の1行です。
kp1, des1 := sift.DetectAndCompute(shiftomo, gocv.NewMat())
カメラ画像の読み込みとグレースケール変換
ここでforループに入っていきます。
for文の中での処理は、カメラから画像を取得し、それをグレースケール変換し、特徴点の抽出を行って、元画像のそれと比較。両画像を表示し、マッチした点に線を引く、という流れになります。
最初のカメラ画像の取得とグレースケール変換は以下のコードで実現できます。とても簡単ですね。
if ok := webcam.Read(&img); !ok {
return
}
gocv.CvtColor(img, &imgGray, gocv.ColorRGBToGray)
その後、元画像に対して行ったのと同様に、特徴点抽出を行います。
kp2, des2 := sift.DetectAndCompute(imgGray, gocv.NewMat())
マッチした特徴点の選び出し
これで両画像の特徴点が得られましたので、合致する点を探します。
OpenCVにはこの作業を行うクラス(BFMatcher)も用意されているので、インスタンスを作成して、メソッド(KnnMatch)を呼び出せばOKです。
bf := gocv.NewBFMatcher()
matches := bf.KnnMatch(des1, des2, 2)
var matchedPoints []gocv.DMatch
for _, m := range matches {
if len(m) > 1 {
if m[0].Distance < 0.6*m[1].Distance {
matchedPoints = append(matchedPoints, m[0])
}
}
}
ここで、マッチしたものの中から、特徴量の差(距離の差)が0.6以下のものだけを選んで、matchedPointsに入れています。
※この"0.6"は色々試した結果、ちょうど良い値だったため選んだものです。
結果の表示
あとは、OpenCVが持つ、DrawMatches()メソッドを使って、1つのウィンドウに両画像を表示し、一致する点を線で結んで表示しています。
c1 := color.RGBA{R: 0, G: 255, B: 0, A: 0}
c2 := color.RGBA{R: 255, G: 0, B: 0, A: 0}
// show matching image
if len(matchedPoints) > 0 {
out := gocv.NewMat()
gocv.DrawMatches(shiftomo, kp1, imgGray, kp2, matchedPoints, &out, c1, c2, make([]byte, 0), gocv.DrawDefault)
window.IMShow(out)
}
実行結果は、このようになります。若干間違えたマッチングポイントもありますが、大きさも明るさも異なり、回転もしていますが、多くの点がマッチしていることがわかります。
コード全体を載せておきます。
package main
import (
"image/color"
"gocv.io/x/gocv"
)
func main() {
// open reference image
shiftomo := gocv.IMRead("SHIFTOMO.jpg", gocv.IMReadGrayScale)
defer shiftomo.Close()
// open USB camera
webcam, err := gocv.OpenVideoCapture(0)
if err != nil {
return
}
defer webcam.Close()
// prepare Mat for captured image and grayscaled image
img := gocv.NewMat()
defer img.Close()
imgGray := gocv.NewMat()
defer imgGray.Close()
// create a window
window := gocv.NewWindow("SHIFTOMO SIFT")
defer window.Close()
// create SIFT object
sift := gocv.NewSIFT()
defer sift.Close()
// find SIFT key points on the reference image
kp1, des1 := sift.DetectAndCompute(shiftomo, gocv.NewMat())
for {
if ok := webcam.Read(&img); !ok {
return
}
if img.Empty() {
continue
}
// convert to grayscale image
gocv.CvtColor(img, &imgGray, gocv.ColorRGBToGray)
// find key points on the captured image
kp2, des2 := sift.DetectAndCompute(imgGray, gocv.NewMat())
// find match points
bf := gocv.NewBFMatcher()
matches := bf.KnnMatch(des1, des2, 2)
var matchedPoints []gocv.DMatch
for _, m := range matches {
if len(m) > 1 {
if m[0].Distance < 0.6*m[1].Distance {
matchedPoints = append(matchedPoints, m[0])
}
}
}
// match color
c1 := color.RGBA{R: 0, G: 255, B: 0, A: 0}
// point color
c2 := color.RGBA{R: 255, G: 0, B: 0, A: 0}
// show matching image
if len(matchedPoints) > 0 {
out := gocv.NewMat()
gocv.DrawMatches(shiftomo, kp1, imgGray, kp2, matchedPoints, &out, c1, c2, make([]byte, 0), gocv.DrawDefault)
window.IMShow(out)
}
if window.WaitKey(1) >= 0 {
break
}
}
}
YOLOによる物体検出
GoCVには、cmd/dnn-detectionに、OpenCV 3.4に同梱されているCaffe face trackingやTensorflow object detectionモデルに対応した物体検出のサンプルが収録されているので、(モデルを入手する必要がありますが)顔のトラッキングや物体検出をGo環境で試してみることができます。
これらのサンプルコードを紹介してもよかったのですが、私は個人的にもDarknet YOLOをよく使っていたこともあり、YOLOのモデルファイルとWeightsファイルを読み込んで動作するコードをdnn-detectionのコードを元に作ってみました。
package main
import (
"fmt"
"image"
"image/color"
"gocv.io/x/gocv"
)
func main() {
// open capture device
webcam, err := gocv.OpenVideoCapture(0)
if err != nil {
fmt.Printf("Error opening video capture device")
return
}
defer webcam.Close()
window := gocv.NewWindow("Yolo")
defer window.Close()
img := gocv.NewMat()
defer img.Close()
// open DNN object tracking model
net := gocv.ReadNet("yolov3.weights", "yolov3.cfg")
if net.Empty() {
fmt.Printf("Error reading network model")
return
}
defer net.Close()
net.SetPreferableBackend(gocv.NetBackendType(gocv.NetBackendCUDA))
net.SetPreferableTarget(gocv.NetTargetType(gocv.NetTargetCUDA))
var ratio float64 = 0.00392
var mean gocv.Scalar = gocv.NewScalar(0, 0, 0, 0)
var swapRGB bool = true
fmt.Printf("Start reading device")
firsttime := true
for {
if ok := webcam.Read(&img); !ok {
fmt.Printf("Device closed")
return
}
if img.Empty() {
continue
}
// convert image Mat to 416x416 blob
blob := gocv.BlobFromImage(img, ratio, image.Pt(416, 416), mean, swapRGB, false)
// feed the blob into the detector
net.SetInput(blob, "")
// run a forward pass thru the network
prob := net.Forward("")
if firsttime == true {
fmt.Printf("prob.Total() = %v, prob.Size() = %v", prob.Total(), prob.Size())
firsttime = false
}
performDetection(&img, prob)
prob.Close()
blob.Close()
window.IMShow(img)
if window.WaitKey(1) >= 0 {
break
}
}
}
func performDetection(frame *gocv.Mat, results gocv.Mat) {
totalResults := results.Total() / 85
for i := 0; i < totalResults; i++ {
confidence := results.GetFloatAt(i, 4)
if confidence > 0.03 {
center_x := int(results.GetFloatAt(i, 0) * float32(frame.Cols()))
center_y := int(results.GetFloatAt(i, 1) * float32(frame.Rows()))
width := int(results.GetFloatAt(i, 2) * float32(frame.Cols()))
height := int(results.GetFloatAt(i, 3) * float32(frame.Rows()))
gocv.Rectangle(frame, image.Rect(center_x - width /2, center_y - height / 2, center_x + width / 2, center_y + height / 2), color.RGBA{0, 255, 0, 0}, 2)
}
}
}
YOLOはblobのサイズ(画像サイズ)が416x416と少し大きいことや、スケールや平均値が違うこと、あとは出力される値が大きく違うのでそれに対応するための変更を行ったことになります。
画面に動物の写真が表示されている状態でこれを実行すると、下のように物体検出が行われて、検出された動物(やCD)に枠が表示されました。
と、YOLOはうまく動いているのですが、これ実は大きな問題があります。というのも、YOLOによるディテクションがGPUではなく、CPU上で実行されるため、とても遅い(1枚の画像につき1秒程度=1fps)のです。
YOLOをGPUで動作させるには、OpenCVをCUDAやOpenCLを使用するようなオプションでビルドする必要があるのですが、いろいろ試してみても、残念なことにMinGW環境でCUDA向けにビルドできませんでした。
調べてみると、CUDAのMinGW向けライブラリが存在しないので、ビルドできないとのこと。残念ですが諦めるしかなさそうです。。。
――――――――――――――――――――――――――――――――――
ということで、OpenCVをGoから使ってみましたが、いかがだったでしょうか? 現状ではCUDAが使えない(簡単には使えない?)点がちょっと辛いところですが、別途試したHaar-Like Cascadeでの顔検出は十分高速に動きましたし、画像の各種変換なども当然ながら高速です。
とは言え、C++やPythonからOpenCVを使ったコードのサンプルはネットにたくさんありますが、GoCVで書かれたコードのサンプルが少ないので、OpenCVに慣れてない方がいきなりGoCVで画像処理アプリを作成するのは、少々ハードルが高いのかな、とも思いました。
――――――――――――――――――――――――――――――――――
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/