GoオンリーでGUI上からQRコード読み取りをしてみた

# はじめに
 Goで使えるOpenCV, [GoCV](https://gocv.io)を使ったリアルタイムのQRコードのスキャンをGUIアプリ実現する[therecipe/qt](https://github.com/therecipe/qt)内で実装したので備忘録として残そうと思います．

## 前提条件
- Go, OpenCVがインストール済みであること
- Webカメラが用意されていること
## 環境
- OS: macOS 13 Ventura
- Go 1.19
- OpenCV: 4.6.0
- GoCV: v0.31.0
- therecipe/qt: v0.0.0-20200904063919-c0c124a5770d
- [gozbar](https://github.com/MordFustang21/gozbar): v0.0.0-20191118235142-b46f682b2cab

# インストール
## gozbar
gozbarは読み取ったQRを画像に変換し,情報を取り出すライブラリです.  
以下のコマンドで簡単に取り込むことが出来ます
```zsh
go get -u https://github.com/MordFustang21/gozbar
```
## GoCV
```zsh
go get -u -d gocv.io/x/gocv
```
さらに, pkgconfigにOpenCVのパスを通す必要があるので以下を入力します.
```shell:~/.zshrc
export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/Cellar/opencv/4.6.0/lib/pkgconfig"

export CGO_LDFLAGS="-L/usr/local/Cellar/opencv/4.6.0/lib -lopencv_stitching -lopencv_superres 
-lopencv_videostab -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib 
-lopencv_dnn_objdetect -lopencv_dpm 
-lopencv_face -lopencv_photo -lopencv_fuzzy -lopencv_hfs 
-lopencv_img_hash -lopencv_line_descriptor -lopencv_optflow 
-lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_stereo 
-lopencv_structured_light -lopencv_phase_unwrapping -lopencv_surface_matching 
-lopencv_tracking -lopencv_datasets -lopencv_dnn -lopencv_plot 
-lopencv_xfeatures2d -lopencv_shape -lopencv_video -lopencv_ml -lopencv_ximgproc -lopencv_calib3d 
-lopencv_features2d -lopencv_highgui -lopencv_videoio -lopencv_flann -lopencv_xobjdetect 
-lopencv_imgcodecs -lopencv_objdetect -lopencv_xphoto -lopencv_imgproc -lopencv_core"

export CGO_CPPFLAGS="-I/usr/local/Cellar/opencv/4.6.0/include"
```
次にGoCVのディレクトリに入って```make install```する必要があるのですが, 自分の環境だとこのようなエラーが出てきました.
```zsh
cd go/pkg/mod/gocv.io/x/gocv@v0.31.0
sudo make install

(略)

cd: OLDPWD not set.
``` 
GoCVのIssuesを見てみると, 環境がMacの場合で発生するらしく, 解決策として```make```の代わりに```gmake```を使わないといけないみたいです.([参考元](https://github.com/hybridgroup/gocv/issues/348))  
その場合, ```brew install gmake```でgmakeをインストールしてください. インストールが出来たら以下のコマンドを実行します．
```zsh
cd go/pkg/mod/gocv.io/x/gocv@v0.31.0
sudo gmake install
```
gmakeでのインストールが完了したら,
```zsh
go run go/pkg/mod/gocv.io/x/gocv@v0.31.0/cmd/version/main.go
```
を実行してインストールできているか確認してください. 正常にインストールされていれば以下が表示されると思います.
```zsh
gocv version: 0.31.0
opencv lib version: 4.6.0
```

## therecipe/qt
```zsh
brew install qt
```
インストール完了後, ```.zshrc```に環境変数を設定し設定を反映させます.
```shell:.zshrc
export QT_HOMEBREW=true
```
```zsh
source ~/.zshrc
```
その後, リポジトリをクローンしセットアップを実行します.
```zsh
go get -v -tags=no_env github.com/therecipe/qt/cmd/...
$(go env GOPATH)/bin/qtsetup
```

# コードを書く
インストールを全て完了したらこのような感じにコードを書きます．
```golang:main.go
package main

import (
    "log"
    "os"

	"github.com/mordfustang21/gozbar"
	"github.com/therecipe/qt/core"
	"github.com/therecipe/qt/widgets"

	"gocv.io/x/gocv"
)

func QRScan() string {
	webcam, _ := gocv.VideoCaptureDevice(0)
	img := gocv.NewMat()
	var qrinfo string

	for {
		webcam.Read(&img)

		if img.Empty() {
			continue
		}

		qr, _ := img.ToImage()
		qrimg := gozbar.FromImage(qr)
		//ScanしたQRコードを画像に変換し，それを取り込む

		s := gozbar.NewScanner()
		err := s.SetConfig(gozbar.QRCODE, gozbar.CFG_ENABLE, 1)
		if err != nil {
			log.Fatal("error setting config", err)
		}
		//ScanするためのConfig

		res := s.Scan(qrimg)
		//Scan

		if res == nil { //Scanできた場合，結果を表示(resはerror handling!)
			qrimg.First().Each(func(r string) {
				//fmt.Println(r)
				qrinfo = r
			})
			webcam.Close()
			s.Destroy()
			return qrinfo
		}
	}
}

func main() {

	// QWidgetsをスタートさせるために必要な処理
	app := widgets.NewQApplication(len(os.Args), os.Args)

	// ウィンドウ生成
	window := widgets.NewQMainWindow(nil, 0)
	window.SetMinimumSize2(360, 420)
	window.SetWindowTitle("QR Reader")
	window.SetWindowFlags(core.Qt__CustomizeWindowHint | core.Qt__WindowCloseButtonHint | core.Qt__WindowMinimizeButtonHint)
	//最大化ボタン無効化
	//window.SetAttribute(core.Qt__WA_DeleteOnClose, true)

	// ウィジェットを作成し，NewQVBoxLayoutを使ってレイアウトを作成
	// ウィンドウの中央にウィジェットを配置
	widget := widgets.NewQWidget(nil, 0)
	widget.SetLayout(widgets.NewQVBoxLayout())
	window.SetCentralWidget(widget)

    // 文字を表示させるラベルの生成，文字入力フォームの作成，表の作成
	label := widgets.NewQLabel2("now:", nil, 0)
	widget.Layout().AddWidget(label)

	// ボタン生成，QRコード読み取り処理を行う
	button := widgets.NewQPushButton2("QR Scan", nil)

	widget.Layout().AddWidget(button)

	button.ConnectClicked(func(bool) {
		res := QRScan()
		label.SetText("now:" + res)
	})
	// ウィンドウ表示
	window.Show()

	// Qtのループを開始，app.Exit()が呼ばれるかユーザによって終了されるまで継続
	app.Exec()
}

```
その後, go.modを作成します. 以下のような流れで作成してください.
qrという名前は各自で変更してください.
```zsh
go mod init qr
go mod tidy
```

コードが書けたので,[文字をQRコード化するサイト](https://qr.quel.jp/text.php)から生成したQRコードを読み取ってみましょう．

今回はこんなQRコードを用意しました．
![qr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728089/25d9d06f-faf0-d8d1-8f3d-3633d5e7ed38.png)


```
go run main.go
```
実行すると以下のウインドウが出現し, QR ScanをクリックするとWebカメラを検出しスキャンを開始します.
Macの場合,カメラを使用するかのダイアログが出てくるので必ず許可してください.
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728089/a11d48a9-b86e-93c0-67d3-2ecc64f09d91.png">
Webカメラのランプが点灯したらスキャン可能という合図なので,スマートフォンなどでQRを読み取ります.
QRを検出すると, now: に書かれてる部分に文字が出力されます.
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728089/67d90557-8119-5337-fdbf-58fe90c42e6f.png">

しっかりと出力されましたね！


# 余談
本来,IMShowを用いてカメラ画面を表示させたかったのですが,
```
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007ff80ecf1c3b __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007ff80e843e25 objc_exception_throw + 48
	2   CoreFoundation                      0x00007ff80ed19ea6 _CFBundleGetValueForInfoKey + 0
	3   AppKit                              0x00007ff811da66b6 -[NSWindow(NSWindow_Theme) _postWindowNeedsToResetDragMarginsUnlessPostingDisabled] + 307
	4   AppKit                              0x00007ff811d9323e -[NSWindow _initContent:styleMask:backing:defer:contentView:] + 1247
	5   AppKit                              0x00007ff811d92d58 -[NSWindow initWithContentRect:styleMask:backing:defer:] + 42
	6   AppKit                              0x00007ff8120638dd -[NSWindow initWithContentRect:styleMask:backing:defer:screen:] + 50
	7   libopencv_highgui.4.6.0.dylib       0x0000000004f87c50 cvNamedWindow + 344
	8   libopencv_highgui.4.6.0.dylib       0x0000000004f82742 _ZN2cv11namedWindowERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEEi + 1245
	9   QR_Reader                   0x0000000004379f8c Window_New + 44
	10  QR_Reader                    0x0000000004068fe4 runtime.asmcgocall.abi0 + 100
)
libc++abi: terminating with uncaught exception of type NSException
SIGABRT: abort
PC=0x7ff80eb6630e m=7 sigcode=0
signal arrived during cgo executio
```
と表示されてしまい，実装することができませんでした.
IMShowを使用した時のみこの画面が表示されたのでOpenCV側から弾かれてしまった感じですね.

# あとがき
導入にかなり時間がかかったライブラリたちですが,いざ導入が完了すればさまざまな用途で使えるのでしばらくはGoオンリーでもやっていけそうです.
これを活用すれば勤怠管理や入場管理をするアプリケーションを開発できるかもしれませんね！

参考:
[therecipe/qt を用いて Go から GUI を利用する方法(Windows/Mac)](https://qiita.com/manymanyuni/items/a28acb545200f1f45c79)
[MordFustang21/gozbar](https://github.com/MordFustang21/gozbar)
[GoCV issues#348](https://github.com/hybridgroup/gocv/issues/348)
[GoCV Getting Started(macOS)](https://gocv.io/getting-started/macos/)
[GoCV Hello-Video](https://gocv.io/writing-code/hello-video/)