推しの村山美羽ちゃんがほかメンバーのブログに登場したかを判定したい
はじめに
おはようございます
以前書いた記事の続きです
櫻坂46三期生ちゃんたちのブログが日々更新され、かわいいが溢れていますね
そんな中、あることに気づきました
先輩メンバーが2ショット写真とかあげてくれるじゃん
推しの写真を集めるためには他メンバーのブログも監視しなきゃじゃん
でも、全部収集するのはめんどくさい
そこで顔判別をして、推しかどうか判定してみようと試みました
あ、ブログと書いていますがこの記事の内容を実施した結果できるのは写真内に推しがいるかどうかです
どうやって実現するか
顔判別をするとなると機械学習とかそこらへんの技術が必要になりそうですが、私はまったくそこらへんの知見がありません
また、Go
推しでもあるのでほかの言語で実装もあまりしたくありません
なので golang face feature
などと検索してみました
すると以下の記事がヒットしました
どうやらdlib
と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
をインストールするためにごちゃごちゃいろいろやって鬼のようにハマりましたが、
最終的にはGitHub
のREADMEに記載があるように以下のスクリプトを用意して実行することで解決できました
#!/bin/bash -eu
apt-get update
apt-get -y install libdlib-dev libblas-dev libatlas-base-dev liblapack-dev libjpeg62-turbo-dev
もちろん、コンテナ内で以下を実行します
./install.sh
ただ、今後コンテナとしてサーバーを立てる際にはイメージサイズをなるべく小さくしたいのでできればdlib
をgit 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
}
動かしてみる
顔判定のために使うソースデータはこちらです
検索対象とするブログは櫻坂46キャプテンのまつりのブログを使います
さすが、キャプテンばっちり三期生と2ショットあげてくれてますね
前回作成した記事の実装を使ってまつりブログから画像を引っこ抜きます
引っこ抜いた画像はimages
ディレクトリに保存しておき
美羽ちゃんが写っている写真
$ go run cmd/main.go
Recognized
美羽ちゃんが写っていない写真(しーちゃんかわいい)
$ 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
しています
こちらに実装したコードを置いておきます
おわりに
写真の選定やパラメータ調整などまだまだ荒削りな部分はありますが、仕組みは一定できたかと思います
さて、自動収集に向けてどこにデプロイしようかな
おまけ
前回記事に画像ダウンロードの話はありませんでした...
以下画像ダウンロードの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