Go界隈で有名なDave Cheney氏の書いたPractical Goを読んだ時のメモをザッと貼り付ける。
pigoを用いた🤔ツールの開発
昨日のお昼休憩中に暇だったのでこんなツールを作りました。
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!!!!!!🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔
ioutil.ReadDirでのシンボリックリンクの扱い
ioutil.ReadDirでディレクトリ内を走査した時にシンボリックリンクがあった場合、LStatとStatのどちらでFileInfoが返ってくるか分からなかったため試してみた。
testディレクトリ内に別のディレクトリに対してシンボリックリンクを張ったものを置いてあります。
package main import ( "fmt" "io/ioutil" "os" "path/filepath" ) func main() { currentDir, err := os.Getwd() if err != nil {} dirPath := filepath.Join(currentDir, "test") infos, err := ioutil.ReadDir(dirPath) for _, info := range infos { fmt.Println("IsDir: ", info.IsDir()) } }
これで、実行結果が true
ならリンク先を辿っているStatであり、 false
ならリンク先を辿らないLstatになります。
ʕ ◔ϖ◔ʔ % go run main.go IsDir: false
おっ、これはLstatの動きですね。
一応 ioutil.ReadDir
のコードも追っかけていきましょう。
Goの ioutil.ReadDir
は内部的にはソート機能のついた os.File.Readdir
で、そちらのコードを眺めてみると。
func (f *File) readdir(n int) (fi []FileInfo, err error) { dirname := f.name if dirname == "" { dirname = "." } names, err := f.Readdirnames(n) fi = make([]FileInfo, 0, len(names)) for _, filename := range names { fip, lerr := lstat(dirname + "/" + filename) if IsNotExist(lerr) { // File disappeared between readdir + stat. // Just treat it as if it didn't exist. continue } if lerr != nil { return fi, lerr } fi = append(fi, fip) } if len(fi) == 0 && err == nil && n > 0 { // Per File.Readdir, the slice must be non-empty or err // must be non-nil if n > 0. err = io.EOF } return fi, err }
真ん中くらいで lstat
使ってますね。
もし、「ディレクトリ内を走査しつつ、シンボリックリンクだった場合はリンク先を辿る」といった処理を実現したい場合には、以下のようにする感じですかね。
package main import ( "fmt" "io/ioutil" "os" "path/filepath" ) func main() { currentDir, err := os.Getwd() if err != nil {} dirPath := filepath.Join(currentDir, "test") infos, err := ioutil.ReadDir(dirPath) for _, info := range infos { filePath := filepath.Join(dirPath, info.Name()) i, err := os.Stat(filePath) if err != nil {} fmt.Println("IsDir: ", i.IsDir()) } }
これを実行すると
ʕ ◔ϖ◔ʔ % go run main.go IsDir: true
おっ、ちゃんとシンボリックリンクのリンク先を辿れてますね。
面倒なのでStatに対応した ioutil.ReadDir
か os.File.Readdir
があればいいんですけどねぇ・・・
現場からは以上です。