🍠

推しの村山美羽ちゃんがほかメンバーのブログに登場したかを判定したい

2023/04/08に公開約7,400字

はじめに

おはようございます

以前書いた記事の続きです

https://zenn.dev/takokun/articles/4844163c2f242d

櫻坂46三期生ちゃんたちのブログが日々更新され、かわいいが溢れていますね

そんな中、あることに気づきました
先輩メンバーが2ショット写真とかあげてくれるじゃん

推しの写真を集めるためには他メンバーのブログも監視しなきゃじゃん
でも、全部収集するのはめんどくさい

そこで顔判別をして、推しかどうか判定してみようと試みました

あ、ブログと書いていますがこの記事の内容を実施した結果できるのは写真内に推しがいるかどうかです

どうやって実現するか

顔判別をするとなると機械学習とかそこらへんの技術が必要になりそうですが、私はまったくそこらへんの知見がありません
また、Go推しでもあるのでほかの言語で実装もあまりしたくありません

なので golang face feature などと検索してみました

すると以下の記事がヒットしました

https://tutorialedge.net/golang/go-face-recognition-tutorial-part-one/

どうやらdlibgo-faceというライブラリを使えば顔判別ができるようです

http://dlib.net/

https://github.com/Kagami/go-face

環境

私は普段 m1 mac を使っていますが、うまく動かすことができなかったので、WSL2上で動かすことにしました
どうせデプロイするならLinux環境で動かすことになるでしょうし

uname -a
Linux HP-Pavilion-Aero 5.15.90.1-microsoft-standard-WSL2 #1 SMP Fri Jan 27 02:56:13 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

実装してみる

dlibを使うためにはいろいろインストールする必要があります
ただ、ローカル環境にごちゃごちゃいれるのが嫌だったのでDocker内で作業することにします

以下のスクリプトにて環境を用意します

#!/bin/bash -eu
#
# useage ./go.sh <version>
# ex)
# ./go.sh 1.20.0

GO_VERSION=$1

docker run --rm -it --name go${GO_VERSION} -v "$(pwd)":/app -w /app golang:${GO_VERSION}-bullseye

以下のコマンドを実行しコンテナ内に入ります
このとき実行ディレクトリをマウントするようにしているので実装などを置くことができます

./go.sh 1.19.8

(個人開発で作っているものがgolangci-lintの対応ができておらず未だ1.19です...)

dlibをインストールするためにごちゃごちゃいろいろやって鬼のようにハマりましたが、
最終的にはGitHubREADMEに記載があるように以下のスクリプトを用意して実行することで解決できました

#!/bin/bash -eu

apt-get update

apt-get -y install libdlib-dev libblas-dev libatlas-base-dev liblapack-dev libjpeg62-turbo-dev

もちろん、コンテナ内で以下を実行します

./install.sh

ただ、今後コンテナとしてサーバーを立てる際にはイメージサイズをなるべく小さくしたいのでできればdlibgit cloneしてきてbuildする形式にしたいところです

また、imagesディレクトリをルート配下に作成して以下のModelsと記載があるものをダウンロードします
これがないとgo-faceが動かないです

wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat

Goの実装はというと、検索してヒットした記事をほぼそのまま活用しています
異なる点としては顔判別をするためのオリジナル画像をbase64にエンコードしてコードに直接埋め込んでみた点です
思いっきりハードコーディングですが、今後デプロイすることとかを考えてこのような形式にしてみました

package internal

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"path/filepath"

	"github.com/Kagami/go-face"
)

const (
	DataDir   = "images"
	Tolerance = 0.13
)

func ExtractFaceFeature(target string) (face.Face, error) {
	rec, err := face.NewRecognizer(DataDir)
	if err != nil {
		fmt.Println("Cannot initialize recognizer")

		return face.Face{}, fmt.Errorf("cannot initialize recognizer %w", err)
	}
	defer rec.Close()

	origin := filepath.Join(DataDir, target)

	faces, err := rec.RecognizeFile(origin)
	if err != nil {
		return face.Face{}, fmt.Errorf("can't recognize: %w", err)
	}
	if len(faces) == 0 {
		return face.Face{}, fmt.Errorf("no faces found")
	}

	return faces[0], nil
}

func FaceFeatureToBase64(src face.Face) (string, error) {
	jsonData, err := json.Marshal(src)
	if err != nil {
		return "", fmt.Errorf("can't marshal: %w", err)
	}

	enc := base64.StdEncoding.EncodeToString(jsonData)

	return enc, nil
}

func Base64ToFaceFeature(src string) (*face.Face, error) {
	var f face.Face

	dec, err := base64.StdEncoding.DecodeString(src)
	if err != nil {
		return nil, fmt.Errorf("can't decode: %w", err)
	}

	if err := json.Unmarshal(dec, &f); err != nil {
		return nil, fmt.Errorf("can't unmarshal: %w", err)
	}

	return &f, nil
}

別のコマンドにてオリジナル画像をbase64に変換して標準出力しコードに埋め込み

package main

import (
	"fmt"
	"go-face-recognition/internal"
)

func main() {
	target := "miu-chan.jpg"

	face, err := internal.ExtractFaceFeature(target)
	if err != nil {
		panic(err)
	}

	b, err := internal.FaceFeatureToBase64(face)
	if err != nil {
		panic(err)
	}

	fmt.Println(b)
}

本体の実装はこちらです

package main

import (
	"fmt"
	"go-face-recognition/internal"
	"log"
	"path/filepath"

	"github.com/Kagami/go-face"
)

const (
	originBase64 = "eyJSZW...."
)

func main() {
	target := "target.jpg"

	if RecognizeTarget(target) {
		fmt.Println("Recognized")
	} else {
		fmt.Println("Not Recognized")
	}
}

func RecognizeTarget(targetImg string) bool {
	rec, err := face.NewRecognizer(internal.DataDir)
	if err != nil {
		fmt.Println("Cannot initialize recognizer")
	}
	defer rec.Close()

	faces, err := rec.RecognizeFile(filepath.Join(internal.DataDir, targetImg))
	if err != nil {
		log.Fatalf("Can't recognize: %v", err)
	}
	if len(faces) == 0 {
		log.Fatalf("No faces found")
	}

	var targets []face.Descriptor
	var indexs []int32

	for i, f := range faces {
		targets = append(targets, f.Descriptor)
		indexs = append(indexs, int32(i))
	}

	rec.SetSamples(targets, indexs)

	miuChan, err := internal.Base64ToFaceFeature(originBase64)
	if err != nil {
		log.Fatalf("Can't recognize: %v", err)
	}
	if miuChan == nil {
		log.Fatalf("No faces found")
	}

	match := rec.ClassifyThreshold(miuChan.Descriptor, internal.Tolerance)

	return match >= 0
}

動かしてみる

顔判定のために使うソースデータはこちらです

https://sakurazaka46.com/files/14/diary/s46/blog/moblog/202303/mobDzbral.jpg

検索対象とするブログは櫻坂46キャプテンのまつりのブログを使います
さすが、キャプテンばっちり三期生と2ショットあげてくれてますね

https://sakurazaka46.com/s/s46/diary/detail/49782?ima=5458&cd=blog

前回作成した記事の実装を使ってまつりブログから画像を引っこ抜きます
引っこ抜いた画像はimagesディレクトリに保存しておき

美羽ちゃんが写っている写真

https://sakurazaka46.com/files/14/diary/s46/blog/moblog/202304/mobQGWCXX.jpg

$ go run cmd/main.go
Recognized

美羽ちゃんが写っていない写真(しーちゃんかわいい)

https://sakurazaka46.com/files/14/diary/s46/blog/moblog/202304/mobNuWbha.jpg

$ go run cmd/main.go
Not Recognized

無事、動くことは確認できましたが

  • ソースデータとする画像によっては判別できない
  • 顔判別をするClassifyThresholdのパラメータ調整が必要

など精度を高めるためには課題はあります

成果物

ディレクトリ構成は以下のようになっています

├── cmd
│   ├── face2base64
│   │   └── main.go
│   └── main.go
├── go.mod
├── go.sh
├── go.sum
├── images
│   ├── dlib_face_recognition_resnet_model_v1.dat
│   ├── mmod_human_face_detector.dat
│   ├── shape_predictor_5_face_landmarks.dat
│   ├── miu-chan.jpg
│   └── target.jpg
├── install.sh
├── internal
│   └── face.go
├── main.go
└── vendor
    ├── github.com
    │   └── Kagami
    └── modules.txt

コンテナマウントした場合、起動の度に依存関係をダウンロードしていたので go mod vendor しています

こちらに実装したコードを置いておきます

https://github.com/takokun778/go-face-recognition

おわりに

写真の選定やパラメータ調整などまだまだ荒削りな部分はありますが、仕組みは一定できたかと思います

さて、自動収集に向けてどこにデプロイしようかな

おまけ

前回記事に画像ダウンロードの話はありませんでした...

以下画像ダウンロードのGo実装です

package main

import (
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
)

const (
	filepath = "./image.jpg"
)

func main() {
	flag.Parse()

	url := flag.Arg(0)

	if err := DownloadImage(url, filepath); err != nil {
		panic(err)
	}

	fmt.Printf("downloaded successfully! %s", url)
}

func DownloadImage(url string, filepath string) error {
	file, err := os.Create(filepath)
	if err != nil {
		return fmt.Errorf("failed to create file: %w", err)
	}
	defer file.Close()

	resp, err := http.Get(url)
	if err != nil {
		return fmt.Errorf("failed to get: %w", err)
	}
	defer resp.Body.Close()

	if _, err = io.Copy(file, resp.Body); err != nil {
		return fmt.Errorf("failed to copy: %w", err)
	}

	return nil
}

確か、ChatGPTに聞いた気がします

Discussion

ログインするとコメントできます