生涯未熟

生涯未熟

プログラミングをちょこちょこと。

pigoを用いた🤔ツールの開発

昨日のお昼休憩中に暇だったのでこんなツールを作りました。

見た目のインパクトからかプチバズりになり恐縮しております。

今回はどんな風にGoで開発したのか?というところを書いていきます。

pigoについて

開発するきっかけになった顔検出ライブラリです。

github.com

このライブラリはREADMEにも書いてあるように、「OpenCVインスコするのダルいからGoで顔検出ライブラリ作ったわ」という感じの分かりやすい思想に基づいて作成されました。(※尚、Webカメラを使ったリアルタイム検出にはOpenCVpythonインスコが必要になります。

実際に使ってみると、多少に検出誤差はあるもののほぼ顔を検出してくれて「おもすれー!」と感嘆。
多少暗い画像や、画質が荒かったり、顔が小さすぎるとちょっと検出が厳しい面はありました。

開発

さて、開発してみようと思ったのですが、まず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!!!!!!🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔