2021-05-24 15:54:22 +02:00
|
|
|
/*
|
|
|
|
|
|
|
|
Package face provides face landmark detection.
|
|
|
|
|
|
|
|
Copyright (c) 2018 - 2021 Michael Mayer <hello@photoprism.org>
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
PhotoPrism® is a registered trademark of Michael Mayer. You may use it as required
|
|
|
|
to describe our software, run your own server, for educational purposes, but not for
|
|
|
|
offering commercial goods, products, or services without prior written permission.
|
|
|
|
In other words, please ask.
|
|
|
|
|
|
|
|
Feel free to send an e-mail to hello@photoprism.org if you have questions,
|
|
|
|
want to support our work, or just want to say hello.
|
|
|
|
|
|
|
|
Additional information can be found in our Developer Guide:
|
|
|
|
https://docs.photoprism.org/developer-guide/
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
package face
|
|
|
|
|
|
|
|
import (
|
|
|
|
"embed"
|
|
|
|
"fmt"
|
2021-05-24 17:57:44 +02:00
|
|
|
_ "image/jpeg"
|
2021-05-24 15:54:22 +02:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
|
|
|
|
|
|
pigo "github.com/esimov/pigo/core"
|
|
|
|
)
|
|
|
|
|
|
|
|
//go:embed cascade/lps/*
|
|
|
|
var efs embed.FS
|
|
|
|
|
|
|
|
var log = event.Log
|
|
|
|
|
|
|
|
//go:embed cascade/facefinder
|
|
|
|
var cascadeFile []byte
|
|
|
|
|
|
|
|
//go:embed cascade/puploc
|
|
|
|
var puplocFile []byte
|
|
|
|
|
|
|
|
var (
|
|
|
|
classifier *pigo.Pigo
|
|
|
|
plc *pigo.PuplocCascade
|
|
|
|
flpcs map[string][]*FlpCascade
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
p := 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 = p.Unpack(cascadeFile)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("face: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
pl := pigo.NewPuplocCascade()
|
|
|
|
plc, err = pl.UnpackCascade(puplocFile)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("face: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
flpcs, err = ReadCascadeDir(pl, "cascade/lps")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("face: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
eyeCascades = []string{"lp46", "lp44", "lp42", "lp38", "lp312"}
|
|
|
|
mouthCascades = []string{"lp93", "lp84", "lp82", "lp81"}
|
|
|
|
)
|
|
|
|
|
|
|
|
// Detector struct contains Pigo face detector general settings.
|
|
|
|
type Detector struct {
|
|
|
|
minSize int
|
|
|
|
maxSize int
|
|
|
|
angle float64
|
|
|
|
shiftFactor float64
|
|
|
|
scaleFactor float64
|
|
|
|
iouThreshold float64
|
|
|
|
}
|
|
|
|
|
|
|
|
func DefaultDetector() *Detector {
|
|
|
|
return &Detector{
|
|
|
|
minSize: 20,
|
|
|
|
maxSize: 1000,
|
|
|
|
angle: 0.0,
|
|
|
|
shiftFactor: 0.1,
|
|
|
|
scaleFactor: 1.1,
|
|
|
|
iouThreshold: 0.2,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
// Point represents face landmark coordinates.
|
|
|
|
type Point struct {
|
|
|
|
Id string `json:"id,omitempty"`
|
|
|
|
Row int `json:"x,omitempty"`
|
|
|
|
Col int `json:"y,omitempty"`
|
|
|
|
Scale int `json:"size,omitempty"`
|
2021-05-24 15:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Detect runs the detection algorithm over the provided source image.
|
2021-05-24 17:57:44 +02:00
|
|
|
func Detect(fileName string, fd *Detector) (det Results, err error) {
|
2021-05-24 15:54:22 +02:00
|
|
|
if !fs.FileExists(fileName) {
|
|
|
|
return det, fmt.Errorf("face: file '%s' not found", fileName)
|
|
|
|
}
|
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
log.Debugf("\nface: detecting faces in %s", txt.Quote(fileName))
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
faces, params, err := fd.Detect(fileName)
|
2021-05-24 15:54:22 +02:00
|
|
|
if err != nil {
|
|
|
|
return det, fmt.Errorf("face: %v (detect faces)", err)
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
det, err = fd.Results(faces, params)
|
2021-05-24 15:54:22 +02:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return det, fmt.Errorf("face: %s (Results)", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("\nface: %s done in \x1b[92m%.2fs\n", txt.Quote(fileName), time.Since(start).Seconds())
|
|
|
|
|
|
|
|
return det, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Detect runs the detection algorithm over the provided source image.
|
2021-05-24 17:57:44 +02:00
|
|
|
func (fd *Detector) Detect(fileName string) (faces []pigo.Detection, params pigo.CascadeParams, err error) {
|
2021-05-24 15:54:22 +02:00
|
|
|
var srcFile io.Reader
|
|
|
|
|
|
|
|
file, err := os.Open(fileName)
|
2021-05-24 17:57:44 +02:00
|
|
|
|
2021-05-24 15:54:22 +02:00
|
|
|
if err != nil {
|
2021-05-24 17:57:44 +02:00
|
|
|
return faces, params, err
|
2021-05-24 15:54:22 +02:00
|
|
|
}
|
2021-05-24 17:57:44 +02:00
|
|
|
|
|
|
|
defer func(file *os.File) {
|
|
|
|
_ = file.Close()
|
|
|
|
}(file)
|
|
|
|
|
2021-05-24 15:54:22 +02:00
|
|
|
srcFile = file
|
|
|
|
|
|
|
|
src, err := pigo.DecodeImage(srcFile)
|
|
|
|
if err != nil {
|
2021-05-24 17:57:44 +02:00
|
|
|
return faces, params, err
|
2021-05-24 15:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pixels := pigo.RgbToGrayscale(src)
|
|
|
|
cols, rows := src.Bounds().Max.X, src.Bounds().Max.Y
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
imageParams := &pigo.ImageParams{
|
2021-05-24 15:54:22 +02:00
|
|
|
Pixels: pixels,
|
|
|
|
Rows: rows,
|
|
|
|
Cols: cols,
|
|
|
|
Dim: cols,
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
params = pigo.CascadeParams{
|
2021-05-24 15:54:22 +02:00
|
|
|
MinSize: fd.minSize,
|
|
|
|
MaxSize: fd.maxSize,
|
|
|
|
ShiftFactor: fd.shiftFactor,
|
|
|
|
ScaleFactor: fd.scaleFactor,
|
2021-05-24 17:57:44 +02:00
|
|
|
ImageParams: *imageParams,
|
2021-05-24 15:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run the classifier over the obtained leaf nodes and return the Result results.
|
|
|
|
// The result contains quadruplets representing the row, column, scale and Result score.
|
2021-05-24 17:57:44 +02:00
|
|
|
faces = classifier.RunCascade(params, fd.angle)
|
2021-05-24 15:54:22 +02:00
|
|
|
|
|
|
|
// Calculate the intersection over union (IoU) of two clusters.
|
|
|
|
faces = classifier.ClusterDetections(faces, fd.iouThreshold)
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
return faces, params, nil
|
2021-05-24 15:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Results adds landmark coordinates to detected faces and returns the results.
|
2021-05-24 17:57:44 +02:00
|
|
|
func (fd *Detector) Results(faces []pigo.Detection, params pigo.CascadeParams) (Results, error) {
|
2021-05-24 15:54:22 +02:00
|
|
|
var (
|
|
|
|
qThresh float32 = 5.0
|
|
|
|
perturb = 63
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2021-05-24 17:57:44 +02:00
|
|
|
detections Results
|
|
|
|
eyesCoords []Point
|
|
|
|
landmarkCoords []Point
|
2021-05-24 15:54:22 +02:00
|
|
|
puploc *pigo.Puploc
|
|
|
|
)
|
|
|
|
|
|
|
|
for _, face := range faces {
|
|
|
|
if face.Q > qThresh {
|
2021-05-24 17:57:44 +02:00
|
|
|
faceCoord := &Point{
|
2021-05-24 15:54:22 +02:00
|
|
|
Col: face.Row - face.Scale/2,
|
|
|
|
Row: face.Col - face.Scale/2,
|
|
|
|
Scale: face.Scale,
|
|
|
|
}
|
|
|
|
|
|
|
|
if face.Scale > 50 {
|
2021-05-24 17:57:44 +02:00
|
|
|
// Find left eye.
|
2021-05-24 15:54:22 +02:00
|
|
|
puploc = &pigo.Puploc{
|
|
|
|
Row: face.Row - int(0.075*float32(face.Scale)),
|
|
|
|
Col: face.Col - int(0.175*float32(face.Scale)),
|
|
|
|
Scale: float32(face.Scale) * 0.25,
|
|
|
|
Perturbs: perturb,
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
leftEye := plc.RunDetector(*puploc, params.ImageParams, fd.angle, false)
|
2021-05-24 15:54:22 +02:00
|
|
|
|
|
|
|
if leftEye.Row > 0 && leftEye.Col > 0 {
|
2021-05-24 17:57:44 +02:00
|
|
|
eyesCoords = append(eyesCoords, Point{
|
|
|
|
Id: "eye_l",
|
2021-05-24 15:54:22 +02:00
|
|
|
Col: leftEye.Row,
|
|
|
|
Row: leftEye.Col,
|
|
|
|
Scale: int(leftEye.Scale),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
// Find right eye.
|
2021-05-24 15:54:22 +02:00
|
|
|
puploc = &pigo.Puploc{
|
|
|
|
Row: face.Row - int(0.075*float32(face.Scale)),
|
|
|
|
Col: face.Col + int(0.185*float32(face.Scale)),
|
|
|
|
Scale: float32(face.Scale) * 0.25,
|
|
|
|
Perturbs: perturb,
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
rightEye := plc.RunDetector(*puploc, params.ImageParams, fd.angle, false)
|
2021-05-24 15:54:22 +02:00
|
|
|
|
|
|
|
if rightEye.Row > 0 && rightEye.Col > 0 {
|
2021-05-24 17:57:44 +02:00
|
|
|
eyesCoords = append(eyesCoords, Point{
|
|
|
|
Id: "eye_r",
|
2021-05-24 15:54:22 +02:00
|
|
|
Col: rightEye.Row,
|
|
|
|
Row: rightEye.Col,
|
|
|
|
Scale: int(rightEye.Scale),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, eye := range eyeCascades {
|
|
|
|
for _, flpc := range flpcs[eye] {
|
2021-05-24 17:57:44 +02:00
|
|
|
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, false)
|
2021-05-24 15:54:22 +02:00
|
|
|
if flp.Row > 0 && flp.Col > 0 {
|
2021-05-24 17:57:44 +02:00
|
|
|
landmarkCoords = append(landmarkCoords, Point{
|
|
|
|
Id: eye,
|
2021-05-24 15:54:22 +02:00
|
|
|
Col: flp.Row,
|
|
|
|
Row: flp.Col,
|
|
|
|
Scale: int(flp.Scale),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
flp = flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, true)
|
2021-05-24 15:54:22 +02:00
|
|
|
if flp.Row > 0 && flp.Col > 0 {
|
2021-05-24 17:57:44 +02:00
|
|
|
landmarkCoords = append(landmarkCoords, Point{
|
|
|
|
Id: eye,
|
2021-05-24 15:54:22 +02:00
|
|
|
Col: flp.Row,
|
|
|
|
Row: flp.Col,
|
|
|
|
Scale: int(flp.Scale),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-24 17:57:44 +02:00
|
|
|
// Find mouth.
|
2021-05-24 15:54:22 +02:00
|
|
|
for _, mouth := range mouthCascades {
|
|
|
|
for _, flpc := range flpcs[mouth] {
|
2021-05-24 17:57:44 +02:00
|
|
|
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, false)
|
2021-05-24 15:54:22 +02:00
|
|
|
if flp.Row > 0 && flp.Col > 0 {
|
2021-05-24 17:57:44 +02:00
|
|
|
landmarkCoords = append(landmarkCoords, Point{
|
|
|
|
Id: mouth,
|
2021-05-24 15:54:22 +02:00
|
|
|
Col: flp.Row,
|
|
|
|
Row: flp.Col,
|
|
|
|
Scale: int(flp.Scale),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-24 17:57:44 +02:00
|
|
|
flp := flpcs["lp84"][0].GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, true)
|
2021-05-24 15:54:22 +02:00
|
|
|
if flp.Row > 0 && flp.Col > 0 {
|
2021-05-24 17:57:44 +02:00
|
|
|
landmarkCoords = append(landmarkCoords, Point{
|
|
|
|
Id: "lp84",
|
2021-05-24 15:54:22 +02:00
|
|
|
Col: flp.Row,
|
|
|
|
Row: flp.Col,
|
|
|
|
Scale: int(flp.Scale),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
detections = append(detections, Result{
|
2021-05-24 17:57:44 +02:00
|
|
|
Rows: params.ImageParams.Rows,
|
|
|
|
Cols: params.ImageParams.Cols,
|
|
|
|
Face: *faceCoord,
|
|
|
|
Eyes: eyesCoords,
|
|
|
|
Landmarks: landmarkCoords,
|
2021-05-24 15:54:22 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return detections, nil
|
|
|
|
}
|