見出し画像

Go+GoCVでSIFT特徴量取得アプリとYOLOを使った物体検出アプリを作ってみる

こんにちは、株式会社SHIFT、自動化エンジニアの水谷です。

Go言語でのアプリ開発記事も今回で3回目になりました。今回はオープンソースの画像処理ライブラリとして有名なOpenCVをGoから使ってみたいと思います。

OpenCVを使うと言っても、高度な画像処理を行うアプリだとコードも長く、難易度も高くなるので、今回はSIFT特徴量を使った画像マッチングをお題にします。なぜSIFT特徴量をお題に選んだかというと、SHIFTに綴りが似ているから、という単純な理由ですがw

また、せっかくですのでYOLOを使った機械学習による物体検出もおまけ程度にやってみたいと思います。


OpenCVについて

OpenCVはインテルが開発して、オープンソースとして公開されているコンピュータービジョンライブラリで、だれでも無料で使用することができます(一部の機能はライセンスが異なりますので、ライセンス情報をしっかり読んで使用する必要があります)。OpenCVはC++で作られたクロスプラットフォームライブラリなのですが、最近ではPythonから使用することも多いですね。

画像2

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)
		}

実行結果は、このようになります。若干間違えたマッチングポイントもありますが、大きさも明るさも異なり、回転もしていますが、多くの点がマッチしていることがわかります。

画像2

コード全体を載せておきます。

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)に枠が表示されました。

画像3

と、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で画像処理アプリを作成するのは、少々ハードルが高いのかな、とも思いました。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷 裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)にPMとして出向中。RGAでは主にAnsibleやOpenshiftに関する案件をプレーイングマネジャーとして担当しながら、Ansibleの社内教育や、外部セミナー講師も行っている。
最近の趣味は電動キックボードでの散歩。

お問合せはお気軽に
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/