WIP face detection experiment

face-detect
Noah Petherbridge 2024-01-08 20:10:12 -08:00
parent 19006877a2
commit a0df988ffa
10 changed files with 188 additions and 3 deletions

View File

@ -34,6 +34,19 @@ create extension postgis;
If you get errors like "Type geography not found" from Postgres when If you get errors like "Type geography not found" from Postgres when
running distance based searches, this is the likely culprit. running distance based searches, this is the likely culprit.
### Face Detection
Fedora: `dnf install python3 python3-opencv opencv-data`
Debian: `apt install python3-opencv opencv-data`
If you get an error like:
> facedetect: error: cannot load HAAR_FRONTALFACE_ALT2 from /usr/share/opencv/haarcascades/haarcascade_frontalface_alt2.xml
Check whether the correct path on disk is actually /usr/share/opencv4 instead of /usr/share/opencv.
One solution then is to symlink the path correctly.
## Building the App ## Building the App
This app is written in Go: [go.dev](https://go.dev). You can probably This app is written in Go: [go.dev](https://go.dev). You can probably

View File

@ -10,6 +10,7 @@ import (
"code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/models/backfill" "code.nonshy.com/nonshy/website/pkg/models/backfill"
"code.nonshy.com/nonshy/website/pkg/models/exporting" "code.nonshy.com/nonshy/website/pkg/models/exporting"
"code.nonshy.com/nonshy/website/pkg/photo"
"code.nonshy.com/nonshy/website/pkg/redis" "code.nonshy.com/nonshy/website/pkg/redis"
"code.nonshy.com/nonshy/website/pkg/worker" "code.nonshy.com/nonshy/website/pkg/worker"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -229,6 +230,9 @@ func initdb(c *cli.Context) {
// Auto-migrate the DB. // Auto-migrate the DB.
models.AutoMigrate() models.AutoMigrate()
// Initialize FaceScore face detection.
photo.InitFaceScore()
} }
func initcache(c *cli.Context) { func initcache(c *cli.Context) {

2
go.mod
View File

@ -14,6 +14,7 @@ require (
) )
require ( require (
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
@ -21,6 +22,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f // indirect github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f // indirect
github.com/esimov/pigo v1.4.6 // indirect
github.com/go-redis/redis v6.15.9+incompatible // indirect github.com/go-redis/redis v6.15.9+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect

9
go.sum
View File

@ -1,6 +1,8 @@
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b h1:TDxEEWOJqMzsu9JW8/QgmT1lgQ9WD2KWlb2lKN/Ql2o= git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b h1:TDxEEWOJqMzsu9JW8/QgmT1lgQ9WD2KWlb2lKN/Ql2o=
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b/go.mod h1:jl+Qr58W3Op7OCxIYIT+b42jq8xFncJXzPufhrvza7Y= git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b/go.mod h1:jl+Qr58W3Op7OCxIYIT+b42jq8xFncJXzPufhrvza7Y=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc=
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
@ -22,6 +24,9 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA= github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc= github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi46M=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
@ -32,6 +37,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -190,6 +196,7 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJ
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU= golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -215,6 +222,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -224,6 +232,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9w
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

@ -14,7 +14,7 @@ import (
// Version of the config format - when new fields are added, it will attempt // Version of the config format - when new fields are added, it will attempt
// to write the settings.toml to disk so new defaults populate. // to write the settings.toml to disk so new defaults populate.
var currentVersion = 2 var currentVersion = 3
// Current loaded settings.json // Current loaded settings.json
var Current = DefaultVariable() var Current = DefaultVariable()
@ -31,6 +31,7 @@ type Variable struct {
BareRTC BareRTC BareRTC BareRTC
Maintenance Maintenance Maintenance Maintenance
Encryption Encryption Encryption Encryption
FaceScore FaceScore
UseXForwardedFor bool UseXForwardedFor bool
} }
@ -52,6 +53,9 @@ func DefaultVariable() Variable {
SQLite: "database.sqlite", SQLite: "database.sqlite",
Postgres: "host=localhost user=nonshy password=nonshy dbname=nonshy port=5679 sslmode=disable TimeZone=America/Los_Angeles", Postgres: "host=localhost user=nonshy password=nonshy dbname=nonshy port=5679 sslmode=disable TimeZone=America/Los_Angeles",
}, },
FaceScore: FaceScore{
CascadeFile: "/path/to/cascade/file",
},
CronAPIKey: uuid.New().String(), CronAPIKey: uuid.New().String(),
} }
} }
@ -165,3 +169,9 @@ type Maintenance struct {
type Encryption struct { type Encryption struct {
AESKey []byte AESKey []byte
} }
// FaceScore settings for face detection in photos via esimov/pigo.
type FaceScore struct {
Enabled bool
CascadeFile string
}

View File

@ -75,6 +75,9 @@ func Edit() http.HandlerFunc {
setProfilePic = r.FormValue("intent") == "profile-pic" setProfilePic = r.FormValue("intent") == "profile-pic"
crop = pphoto.ParseCropCoords(r.FormValue("crop")) crop = pphoto.ParseCropCoords(r.FormValue("crop"))
// Re-compute the face score (admin only)
recomputeFaceScore = r.FormValue("recompute_face_score") == "true" && currentUser.IsAdmin
// Are we GOING private or changing to Inner Circle? // Are we GOING private or changing to Inner Circle?
goingPrivate = visibility == models.PhotoPrivate && visibility != photo.Visibility goingPrivate = visibility == models.PhotoPrivate && visibility != photo.Visibility
goingCircle = visibility == models.PhotoInnerCircle && visibility != photo.Visibility goingCircle = visibility == models.PhotoInnerCircle && visibility != photo.Visibility
@ -118,6 +121,17 @@ func Edit() http.HandlerFunc {
log.Error("SAVING PHOTO: %+v", photo) log.Error("SAVING PHOTO: %+v", photo)
// Are we re-computing the face score?
if recomputeFaceScore {
score, err := pphoto.ComputeFaceScore(pphoto.DiskPath(photo.Filename))
if err != nil {
session.FlashError(w, r, "Face score: %s", err)
} else {
session.Flash(w, r, "Face score recomputed!")
photo.FaceScore = &score
}
}
if err := photo.Save(); err != nil { if err := photo.Save(); err != nil {
session.FlashError(w, r, "Couldn't save photo: %s", err) session.FlashError(w, r, "Couldn't save photo: %s", err)
} }

View File

@ -21,8 +21,9 @@ type Photo struct {
Caption string Caption string
Flagged bool // photo has been reported by the community Flagged bool // photo has been reported by the community
Visibility PhotoVisibility Visibility PhotoVisibility
Gallery bool // photo appears in the public gallery (if public) Gallery bool // photo appears in the public gallery (if public)
Explicit bool // is an explicit photo Explicit bool // is an explicit photo
FaceScore *float64 // face detection score (best)
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }

95
pkg/photo/face_score.go Normal file
View File

@ -0,0 +1,95 @@
package photo
import (
"errors"
"os"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
pigo "github.com/esimov/pigo/core"
)
// Functionality to do with face detection in pictures.
var (
faceScoreReady bool
faceClassifier *pigo.Pigo
cascadeFile []byte
)
// InitFaceScore initializes the face recognition library (esimov/pigo).
func InitFaceScore() {
if faceScoreReady {
return
}
if !config.Current.FaceScore.Enabled {
log.Error("InitFaceScore: not enabled in settings, face detection will not run")
return
}
// Load the cascade file needed for face detection.
data, err := os.ReadFile(config.Current.FaceScore.CascadeFile)
if err != nil {
log.Error("InitFaceScore: could not load cascade file (%s): %s", config.Current.FaceScore.CascadeFile, err)
return
}
cascadeFile = data
log.Info("Initializing FaceScore with cascade file (%d bytes)", len(cascadeFile))
faceClassifier = pigo.NewPigo()
faceClassifier, err = faceClassifier.Unpack(cascadeFile)
if err != nil {
log.Error("InitFaceScore: could not unpack the cascade file: %s", err)
return
}
faceScoreReady = true
}
// ComputeFaceScore checks a photo on disk and returns the detected face score.
func ComputeFaceScore(filename string) (float64, error) {
if !faceScoreReady {
return 0, errors.New("face detection is not available")
}
src, err := pigo.GetImage(filename)
if err != nil {
return 0, err
}
var (
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,
},
}
)
// Run the classifier.
dets := faceClassifier.RunCascade(cParams, 0.0)
for _, row := range dets {
log.Warn("%+v", row)
}
// Note: the classifier may return multiple matched faces, return the highest score.
var highest float32
for _, row := range dets {
if row.Q > highest {
highest = row.Q
}
}
return float64(highest), nil
}

View File

@ -64,6 +64,14 @@
<span>Gallery</span> <span>Gallery</span>
</span> </span>
{{end}} {{end}}
<!-- Face detection score, TODO: admin only -->
{{if ne .FaceScore nil}}
<span class="tag is-success is-light">
<span class="icon"><i class="fa fa-smile"></i></span>
<span>{{.FaceScore}}</span>
</span>
{{end}}
</div> </div>
{{end}} {{end}}

View File

@ -390,6 +390,35 @@
{{end}} {{end}}
</div> </div>
<!-- Admin controls -->
{{if and .EditPhoto .CurrentUser.IsAdmin}}
<div class="field mb-5">
<label class="label">
<span>Admin Options</span>
<span class="icon"><i class="fa fa-peace"></i></span>
</label>
<span class="tag is-info">
<i class="fa fa-smile mr-2"></i>
Face detection score:
{{if eq .EditPhoto.FaceScore nil}}
<em>missing</em>
{{else}}
{{.EditPhoto.FaceScore}}
{{end}}
</span>
<div class="mt-2">
<label class="checkbox">
<input type="checkbox"
name="recompute_face_score"
value="true">
Re-compute the face score
</label>
</div>
</div>
{{end}}
{{if not .EditPhoto}} {{if not .EditPhoto}}
<div class="field"> <div class="field">
<label class="label">Confirm Upload</label> <label class="label">Confirm Upload</label>