生涯未熟

生涯未熟

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

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!!!!!!🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔

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.ReadDiros.File.Readdir があればいいんですけどねぇ・・・

現場からは以上です。

os.Stat, os.Lstatのシンボリックリンク指定時の違い

いつまで経っても覚えられないので自戒のために書く。

os.Stat

指定されたファイルパスの情報(os.FileInfo)を返す。
シンボリックリンクを指定した場合には、リンクを辿った先の情報を返す。

os.Lstat

指定されたファイルパスの情報(os.FileInfo)を返す。 シンボリックリンクを指定された場合には、リンクを辿らずシンボリックリンク自体の情報を返す。

注意

os.FileInfoのNameはStat, Lstatのどちらも指定したシンボリックリンクの名前自体になる。

echoのmiddlewareで一度読んだRequestBodyをもう一度読む

Gopherの皆さんはecho使ってますでしょうか?僕は薄くて好きなのでよく使ってます。

さて、echoにはmiddlewareという機能がございます。
Middleware | Echo - High performance, minimalist Go web framework

ハンドラの実行前に処理差し込めるやつですが、このmiddlewareとして以下のような処理を差し込むとします。

func CheckJSON(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        jsonBlob, err := ioutil.ReadAll(c.Request().Body)
        if err != nil {
            return err
        }
        s := string(jsonBlob)
        if !validator.IsJSON(s) {
            return c.NoContent(http.StatusBadRequest)
        }

        return next(c)
    }
}

飛んできたリクエストがJSONかどうか?というのをチェックするmiddlewareになります。
さて、このmiddlewareを実行した後にハンドラでこのような処理を実行するとしましょう。

func Hoge(c echo.Context) error {
    var jsonBody map[string]interface{}
    jsonBlob, err := ioutil.ReadAll(c.Request().Body)
    if err != nil {
        return err
    }
    err := json.Unmarshal(jsonBlob, &jsonBody)
    if err != nil {
        return errors.New("failed unmarshal json")
    }

    return c.NoContent(200)
}

またもやリクエストから読み込んでる処理ですね。
さて、これを実行すると failed unmarshal json のエラーになります。
これは ioutil.ReadAll がc.Request().Bodyを全て読み込んでいるため、次回以降読み込もうとするとnilが返ってくるためです。

で、ここからどうやって2回目の読み込みを実現するかですが、答えは簡単で「Bodyの内容を上書く」という解決方法があります。

func CheckJSON(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        jsonBlob, err := ioutil.ReadAll(c.Request().Body)
        if err != nil {
            return err
        }
        s := string(jsonBlob)
        if !validator.IsJSON(s) {
            return c.NoContent(http.StatusBadRequest)
        }

        c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(jsonBlob))

        return next(c)
    }
}

こうすることで2回目にキチンと ioutil.ReadAll を実行しても内容を読み込んでくれます。

刺し身たんぽぽについて

刺し身たんぽぽについて調べてみました。

f:id:syossan:20190106180834p:plain

刺し身たんぽぽとは一体何なのか?

刺身の盛り合わせに添えられている食菊(食用菊)の通称。花の大きさからタンポポと誤認されやすい。菊には殺菌作用があり、かつ見た目の彩りや香りも添える。さらに刺身のツマとして食べられる。近年では形骸化しつつある。 - weblio辞書より引用 -

なるほど、実際にはたんぽぽでは無いのですね。勉強になります。
あと、食べれるものとは知りませんでした。むしろ、あんな花花しいものを食べる人はいるのだろうか?

いつから刺し身たんぽぽは誕生したのか?

奈良時代に、日本で現在でも食用菊として栽培されている「延命楽(もってのほか・カキノモト)」が中国から伝来した[2]。 - wikipediaより引用 -

そんな1000年以上前から刺し身たんぽぽはあったのですね・・・しゅごい。

食用としては、江戸時代から民間で食されるようになったとされており[4]、1695年に記された『本朝食鑑』に「甘菊」の記述が見られる[5]。 - wikipediaより引用 -

実際に食されるようになったのは結構最近なのですね。
刺し身のツマとして用いられるようになった経緯についても深掘りしたかったのですが、検索しても見つからず。インターネットの敗北です。

刺し身たんぽぽの効用とは?

刺し身にたんぽぽが入っている理由としては「殺菌作用・彩り」の2点のようですね。
このたんぽぽ自体には発ガン効果の抑制・コレステロールの低下・中性脂肪を低下させる効果があるそうです。めちゃくちゃ有能やんけ。

刺し身たんぽぽの危険性

ただ、こんな刺し身たんぽぽですがキク科のアレルギーを持つ方にとっては食すのに危険性が伴います。

togetter.com

食べる時には自分がアレルギーを持っていないか気を付けましょう。
また、刺し身たんぽぽという単語を真に受けて、その辺に生えてるたんぽぽを生で食べないようにしましょう。普通に汚いですからね。

刺し身たんぽぽについて完全に理解した

年始から刺し身たんぽぽの話題で溢れていたので、完全に理解するために色々と調べてみましたがどうだったでしょうか?
こういう単語はしっかり調べた上で、きちんと使える大人になりましょうね!