昨日のお昼休憩中に暇だったのでこんなツールを作りました。
Goで顔を判定して🤔にする謎ツールを昼休憩中に作成した pic.twitter.com/QcyfcOiLwk
— しょっさん@ʕ ◔ϖ◔ʔ (@syossan27) January 16, 2019
見た目のインパクトからかプチバズりになり恐縮しております。
今回はどんな風にGoで開発したのか?というところを書いていきます。
pigoについて
開発するきっかけになった顔検出ライブラリです。
このライブラリはREADMEにも書いてあるように、「OpenCVインスコするのダルいからGoで顔検出ライブラリ作ったわ」という感じの分かりやすい思想に基づいて作成されました。(※尚、Webカメラを使ったリアルタイム検出にはOpenCVやpythonのインスコが必要になります。
実際に使ってみると、多少に検出誤差はあるもののほぼ顔を検出してくれて「おもすれー!」と感嘆。
多少暗い画像や、画質が荒かったり、顔が小さすぎるとちょっと検出が厳しい面はありました。
開発
さて、開発してみようと思ったのですが、まずpigoでどのようにコード書けばいいのか悩みました。
以下のようにREADMEに書かれているexampleを見てください。
cascadeFile, err := ioutil.ReadFile("/path/to/cascade/file") if err != nil { log.Fatalf("Error reading the cascade file: %v", err) } src, err := pigo.GetImage("/path/to/image") if err != nil { log.Fatalf("Cannot open the image file: %v", err) } pixels := pigo.RgbToGrayscale(src) cols, rows := src.Bounds().Max.X, src.Bounds().Max.Y cParams := pigo.CascadeParams{ MinSize: fd.minSize, MaxSize: fd.maxSize, ShiftFactor: fd.shiftFactor, ScaleFactor: fd.scaleFactor, ImageParams: pigo.ImageParams{ Pixels: pixels, Rows: rows, Cols: cols, Dim: cols, }, } pigo := pigo.NewPigo() // Unpack the binary file. This will return the number of cascade trees, // the tree depth, the threshold and the prediction from tree's leaf nodes. classifier, err := pigo.Unpack(cascadeFile) if err != nil { log.Fatalf("Error reading the cascade file: %s", err) } angle := 0.0 // cascade rotation angle. 0.0 is 0 radians and 1.0 is 2*pi radians // Run the classifier over the obtained leaf nodes and return the detection results. // The result contains quadruplets representing the row, column, scale and detection score. dets := classifier.RunCascade(cParams, angle) // Calculate the intersection over union (IoU) of two clusters. dets = classifier.ClusterDetections(dets, 0.2)
これ読むと、なんとなくニュアンスは伝わるのですが、「fd.minSizeとかのfdってなんやねん」とか色々疑問点が沸いてきました。
そこで、コマンド版のコードを覗いてみることに👀
pigo/main.go at master · esimov/pigo · GitHub
ここ読むとfdとは何ぞやとか色々理解が得られるので読むのをオススメします。
さて、読んでみるとpigoは以下のような流れで検出を行います。
- 顔検出に使うデータを読み込む
- 顔検出対象になる画像を読み込む
- 検出結果の取得
これをコードで書き起こしてみましょう。
package sample import ( "github.com/esimov/pigo/core" "io/ioutil" "log" ) func main() { // 顔検出のためのバイナリデータ読み込み cascadeFile, err := ioutil.ReadFile("/go/src/github.com/esimov/pigo/data/facefinder") if err != nil { log.Fatalf("Error reading the cascade file: %v", err) } // 検出対象となる画像を読み込み src, err := pigo.GetImage("/path/input.jpg") if err != nil { log.Fatalf("Cannot open the image file: %v", err) } pixels := pigo.RgbToGrayscale(src) // 画像をグレースケールに変換 cols, rows := src.Bounds().Max.X, src.Bounds().Max.Y // 画像サイズを取得 // 検出の際に使うパラメータ cParams := pigo.CascadeParams{ MinSize: 20, MaxSize: 1000, ShiftFactor: 0.1, ScaleFactor: 1.1, // 検出対象の画像パラメータ ImageParams: pigo.ImageParams{ Pixels: pixels, Rows: rows, Cols: cols, Dim: cols, }, } pigo := pigo.NewPigo() // 顔検出のためのバイナリデータをUnpack classifier, err := pigo.Unpack(cascadeFile) if err != nil { log.Fatalf("Error reading the cascade file: %s", err) } // 検出する顔の角度設定 angle := 0.0 // 検出の実行 dets := classifier.RunCascade(cParams, angle) // 検出されたデータ(dets)をゴニョゴニョ・・・ }
こんなコードになります。
ここまでで、検出した結果が dets
に格納されているはずなので、この結果から顔にthinking_faceを貼っ付けるところまでやってみましょう。
package main import ( "github.com/esimov/pigo/core" "github.com/fogleman/gg" "github.com/nfnt/resize" "image" "image/jpeg" "image/png" "io/ioutil" "log" "os" "path/filepath" ) type detectionResult struct { coords []image.Rectangle } var dc *gg.Context func main() { // 顔検出のためのバイナリデータ読み込み cascadeFile, err := ioutil.ReadFile("/go/src/github.com/esimov/pigo/data/facefinder") if err != nil { log.Fatalf("Error reading the cascade file: %v", err) } // 検出対象となる画像を読み込み src, err := pigo.GetImage("/path/input.jpg") if err != nil { log.Fatalf("Cannot open the image file: %v", err) } pixels := pigo.RgbToGrayscale(src) // 画像をグレースケールに変換 cols, rows := src.Bounds().Max.X, src.Bounds().Max.Y // 画像サイズを取得 // 検出の際に使うパラメータ cParams := pigo.CascadeParams{ MinSize: 20, MaxSize: 1000, ShiftFactor: 0.1, ScaleFactor: 1.1, // 検出対象の画像パラメータ ImageParams: pigo.ImageParams{ Pixels: pixels, Rows: rows, Cols: cols, Dim: cols, }, } pigo := pigo.NewPigo() // 顔検出のためのバイナリデータをUnpack classifier, err := pigo.Unpack(cascadeFile) if err != nil { log.Fatalf("Error reading the cascade file: %s", err) } angle := 0.0 // 検出する顔の角度設定 // 検出の実行 dets := classifier.RunCascade(cParams, angle) // 顔の重なり検出に関する何か・・・?🤔 dets = classifier.ClusterDetections(dets, 0.2) // 出力画像の用意 dc = gg.NewContext(cols, rows) dc.DrawImage(src, 0, 0) // 🤔を描く err = drawThinkingFace(dets) if err != nil { log.Fatalf("Error creating the image output: %s", err) } } func drawThinkingFace(faces []pigo.Detection) error { var qThresh float32 = 5.0 // 顔検出スコア閾値 // 🤔画像の読み込み thinkingFace, _ := os.Open("/path/thinking_face.png") thinkingFaceImg, _, err := image.Decode(thinkingFace) for _, face := range faces { // 顔検出スコアが閾値を上回っているかチェック if face.Q > qThresh { // 検出された顔のサイズから🤔画像のリサイズ resizeXY := int(float32(face.Scale) * 1.5) resizeThinkingFaceImg := resize.Resize(uint(resizeXY), uint(resizeXY), thinkingFaceImg, resize.Lanczos3) // 出力画像に🤔を貼り付け dc.DrawImage(resizeThinkingFaceImg, face.Col-face.Scale/2, face.Row-face.Scale/2) } } // 出力画像を出力 img := dc.Image() output, err := os.OpenFile("/path/output.jpg", os.O_CREATE|os.O_RDWR, 0755) if err != nil { return err } ext := filepath.Ext(output.Name()) switch ext { case ".jpg", ".jpeg": jpeg.Encode(output, img, &jpeg.Options{Quality: 100}) case ".png": png.Encode(output, img) } return err }
こんな感じになりました。
さっきより微妙にボリューミーになりましたが、大きく違うのは drawThinkingFace
があるところですかね。
基本的には dets
で取得された顔の情報から、適宜座標やサイズを良い感じにして🤔を貼り付けてます。
これをサンプルにパラメータを良い感じに弄ると良い感じに出力されるので、最高に良い感じのツールですね。
というわけで、pigoはまだWebカメラを使ったリアルタイム検出とかも機能としてはあるので、興味のあるGopherはやってみてアウトプットしてみてください!!!
Good ThinkingFace Life!!!!!!🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔