People: Detect number of faces (experimental) #22

This commit is contained in:
Michael Mayer 2021-05-25 18:01:21 +02:00
parent f5a1cc6231
commit a6bf89d104
11 changed files with 267 additions and 169 deletions

View file

@ -241,7 +241,7 @@
</v-checkbox> </v-checkbox>
</v-flex> </v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2"> <v-flex v-if="config.experimental" xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox <v-checkbox
v-model="settings.features.people" v-model="settings.features.people"
:disabled="busy" :disabled="busy"

View file

@ -47,3 +47,24 @@ func LocationLabel(name string, uncertainty int) Label {
func (l Label) Title() string { func (l Label) Title() string {
return txt.Title(txt.Clip(l.Name, txt.ClipDefault)) return txt.Title(txt.Clip(l.Name, txt.ClipDefault))
} }
// FaceLabels returns matching labels if there are people in the image.
func FaceLabels(count int, src string, uncertainty int) Labels {
var r LabelRule
if count < 1 {
return Labels{}
} else if count == 1 {
r = rules["portrait"]
} else {
r = rules["people"]
}
return Labels{Label{
Name: r.Label,
Source: src,
Uncertainty: uncertainty,
Priority: r.Priority,
Categories: r.Categories,
}}
}

View file

@ -12,13 +12,13 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"academic gown": { "academic gown": {
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"accordion": { "accordion": {
Label: "instrument", Label: "instrument",
@ -516,7 +516,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"bathtub": { "bathtub": {
Label: "living", Label: "living",
@ -630,7 +630,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"bernese mountain dog": { "bernese mountain dog": {
Label: "dog", Label: "dog",
@ -672,7 +672,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"binder": { "binder": {
Label: "office", Label: "office",
@ -804,13 +804,13 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"bonnet": { "bonnet": {
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"book jacket": { "book jacket": {
Label: "book", Label: "book",
@ -882,7 +882,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"box turtle": { "box turtle": {
Label: "turtle", Label: "turtle",
@ -924,7 +924,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"breakwater": { "breakwater": {
Label: "water", Label: "water",
@ -936,7 +936,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"briard dog": { "briard dog": {
Label: "dog", Label: "dog",
@ -1134,7 +1134,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"cardigan dog": { "cardigan dog": {
Label: "dog", Label: "dog",
@ -1242,7 +1242,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"chain saw": { "chain saw": {
Label: "outdoor", Label: "outdoor",
@ -1392,7 +1392,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"clock": { "clock": {
Label: "display", Label: "display",
@ -1686,7 +1686,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"cup": { "cup": {
Label: "", Label: "",
@ -2088,7 +2088,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"feather boa": { "feather boa": {
Label: "", Label: "",
@ -2298,7 +2298,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"gallery": { "gallery": {
Label: "", Label: "",
@ -2466,7 +2466,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"grand piano": { "grand piano": {
Label: "instrument", Label: "instrument",
@ -2616,7 +2616,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"hair spray": { "hair spray": {
Label: "bottle", Label: "bottle",
@ -2820,7 +2820,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"horizontal bar": { "horizontal bar": {
Label: "", Label: "",
@ -3054,7 +3054,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"jeep": { "jeep": {
Label: "", Label: "",
@ -3072,7 +3072,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"jigsaw puzzle": { "jigsaw puzzle": {
Label: "puzzle", Label: "puzzle",
@ -3126,7 +3126,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"king crab": { "king crab": {
Label: "crab", Label: "crab",
@ -3198,7 +3198,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"labrador retriever dog": { "labrador retriever dog": {
Label: "dog", Label: "dog",
@ -3516,7 +3516,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"malamute dog": { "malamute dog": {
Label: "dog", Label: "dog",
@ -3660,7 +3660,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"milk can": { "milk can": {
Label: "", Label: "",
@ -3696,7 +3696,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"minivan": { "minivan": {
Label: "car", Label: "car",
@ -3792,7 +3792,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"mosque": { "mosque": {
Label: "tower", Label: "tower",
@ -3870,19 +3870,19 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"neck brace": { "neck brace": {
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"necklace": { "necklace": {
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"nematode": { "nematode": {
Label: "worm", Label: "worm",
@ -4020,7 +4020,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"ox": { "ox": {
Label: "cow", Label: "cow",
@ -4080,7 +4080,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"palace": { "palace": {
Label: "historic", Label: "historic",
@ -4220,6 +4220,12 @@ var rules = LabelRules{
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{},
}, },
"people": {
Label: "people",
Threshold: 0.300000,
Priority: 0,
Categories: []string{},
},
"perfume": { "perfume": {
Label: "bottle", Label: "bottle",
Threshold: 0.700000, Threshold: 0.700000,
@ -4428,7 +4434,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"pool table": { "pool table": {
Label: "", Label: "",
@ -4896,7 +4902,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"sax": { "sax": {
Label: "instrument", Label: "instrument",
@ -5256,7 +5262,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"soft-coated wheaten terrier dog": { "soft-coated wheaten terrier dog": {
Label: "dog", Label: "dog",
@ -5274,7 +5280,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"sorrel": { "sorrel": {
Label: "", Label: "",
@ -5538,7 +5544,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"sulphur butterfly": { "sulphur butterfly": {
Label: "butterfly", Label: "butterfly",
@ -5598,7 +5604,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"swimming trunks": { "swimming trunks": {
Label: "portrait", Label: "portrait",
@ -6060,7 +6066,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"viaduct": { "viaduct": {
Label: "building", Label: "building",
@ -6282,7 +6288,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"white stork": { "white stork": {
Label: "bird", Label: "bird",
@ -6330,7 +6336,7 @@ var rules = LabelRules{
Label: "portrait", Label: "portrait",
Threshold: 0.500000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{"portrait"}, Categories: []string{},
}, },
"wine bottle": { "wine bottle": {
Label: "bottle", Label: "bottle",

View file

@ -143,8 +143,6 @@ rapeseed:
fashion: fashion:
label: portrait label: portrait
threshold: 0.5 threshold: 0.5
categories:
- portrait
vestment: vestment:
see: fashion see: fashion
@ -4099,6 +4097,10 @@ portrait:
categories: categories:
- people - people
people:
label: people
threshold: 0.3
shower cap: shower cap:
label: portrait label: portrait
categories: categories:

View file

@ -17,9 +17,9 @@ type Marker struct {
RefUID string `gorm:"type:VARBINARY(42);index;" json:"UID" yaml:"UID,omitempty"` RefUID string `gorm:"type:VARBINARY(42);index;" json:"UID" yaml:"UID,omitempty"`
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"` MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"` MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
MarkerScore int `gorm:"type:SMALLINT"`
MarkerLabel string `gorm:"type:VARCHAR(255);" json:"Label" yaml:"Label,omitempty"` MarkerLabel string `gorm:"type:VARCHAR(255);" json:"Label" yaml:"Label,omitempty"`
MarkerMeta string `gorm:"type:TEXT;" json:"Meta" yaml:"Meta,omitempty"` MarkerMeta string `gorm:"type:TEXT;" json:"Meta" yaml:"Meta,omitempty"`
Uncertainty int `gorm:"type:SMALLINT"`
X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"` X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"`
Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"` Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"`
W float32 `gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty"` W float32 `gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty"`

View file

@ -1,22 +1,18 @@
package face package face
import ( import (
"embed" _ "embed"
"fmt" "fmt"
pigo "github.com/esimov/pigo/core"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
_ "image/jpeg" _ "image/jpeg"
"io" "io"
"os" "os"
"time" "path/filepath"
"runtime/debug"
"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
//go:embed cascade/facefinder //go:embed cascade/facefinder
var cascadeFile []byte var cascadeFile []byte
@ -62,49 +58,58 @@ var (
// Detector struct contains Pigo face detector general settings. // Detector struct contains Pigo face detector general settings.
type Detector struct { type Detector struct {
minSize int minSize int
maxSize int maxSize int
angle float64 angle float64
shiftFactor float64 shiftFactor float64
scaleFactor float64 scaleFactor float64
iouThreshold float64 iouThreshold float64
} scoreThreshold float32
perturb int
func DefaultDetector() *Detector {
return &Detector{
minSize: 20,
maxSize: 1000,
angle: 0.0,
shiftFactor: 0.1,
scaleFactor: 1.1,
iouThreshold: 0.2,
}
} }
// Detect runs the detection algorithm over the provided source image. // Detect runs the detection algorithm over the provided source image.
func Detect(fileName string, fd *Detector) (det Faces, err error) { func Detect(fileName string) (faces Faces, err error) {
defer func() {
if r := recover(); r != nil {
log.Errorf("face: %s (panic)\nstack: %s", r, debug.Stack())
}
}()
fd := &Detector{
minSize: 20,
maxSize: 1000,
angle: 0.0,
shiftFactor: 0.1,
scaleFactor: 1.1,
iouThreshold: 0.2,
scoreThreshold: 10.0,
perturb: 63,
}
if !fs.FileExists(fileName) { if !fs.FileExists(fileName) {
return det, fmt.Errorf("face: file '%s' not found", fileName) return faces, fmt.Errorf("face: file '%s' not found", txt.Quote(filepath.Base(fileName)))
} }
start := time.Now() log.Debugf("face: detecting faces in %s", txt.Quote(filepath.Base(fileName)))
log.Debugf("\nface: detecting faces in %s", txt.Quote(fileName)) det, params, err := fd.Detect(fileName)
faces, params, err := fd.Detect(fileName)
if err != nil {
return det, fmt.Errorf("face: %v (detect faces)", err)
}
det, err = fd.Results(faces, params)
if err != nil { if err != nil {
return det, fmt.Errorf("face: %s (Faces)", err) return faces, fmt.Errorf("face: %v (detect faces)", err)
} }
log.Debugf("\nface: %s done in \x1b[92m%.2fs\n", txt.Quote(fileName), time.Since(start).Seconds()) if det == nil {
return faces, fmt.Errorf("face: no result")
}
return det, nil faces, err = fd.Faces(det, params)
if err != nil {
return faces, fmt.Errorf("face: %s (faces)", err)
}
return faces, nil
} }
// Detect runs the detection algorithm over the provided source image. // Detect runs the detection algorithm over the provided source image.
@ -117,9 +122,7 @@ func (fd *Detector) Detect(fileName string) (faces []pigo.Detection, params pigo
return faces, params, err return faces, params, err
} }
defer func(file *os.File) { defer file.Close()
_ = file.Close()
}(file)
srcFile = file srcFile = file
@ -157,70 +160,73 @@ func (fd *Detector) Detect(fileName string) (faces []pigo.Detection, params pigo
} }
// Faces adds landmark coordinates to detected faces and returns the results. // Faces adds landmark coordinates to detected faces and returns the results.
func (fd *Detector) Results(faces []pigo.Detection, params pigo.CascadeParams) (Faces, error) { func (fd *Detector) Faces(det []pigo.Detection, params pigo.CascadeParams) (Faces, error) {
var ( var (
qThresh float32 = 5.0 results Faces
perturb = 63
)
var (
detections Faces
eyesCoords []Point eyesCoords []Point
landmarkCoords []Point landmarkCoords []Point
puploc *pigo.Puploc puploc *pigo.Puploc
) )
for _, face := range faces { for _, face := range det {
if face.Q > qThresh { if face.Q < fd.scoreThreshold {
faceCoord := NewPoint( continue
"face", }
face.Row-face.Scale/2,
face.Col-face.Scale/2,
face.Scale,
)
if face.Scale > 50 { faceCoord := NewPoint(
// Find left eye. "face",
puploc = &pigo.Puploc{ face.Row-face.Scale/2,
Row: face.Row - int(0.075*float32(face.Scale)), face.Col-face.Scale/2,
Col: face.Col - int(0.175*float32(face.Scale)), face.Scale,
Scale: float32(face.Scale) * 0.25, )
Perturbs: perturb,
}
leftEye := plc.RunDetector(*puploc, params.ImageParams, fd.angle, false) if face.Scale > 50 {
// Find left eye.
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: fd.perturb,
}
if leftEye.Row > 0 && leftEye.Col > 0 { leftEye := plc.RunDetector(*puploc, params.ImageParams, fd.angle, false)
eyesCoords = append(eyesCoords, NewPoint(
"eye_l",
leftEye.Row,
leftEye.Col,
int(leftEye.Scale),
))
}
// Find right eye. if leftEye.Row > 0 && leftEye.Col > 0 {
puploc = &pigo.Puploc{ eyesCoords = append(eyesCoords, NewPoint(
Row: face.Row - int(0.075*float32(face.Scale)), "eye_l",
Col: face.Col + int(0.185*float32(face.Scale)), leftEye.Row,
Scale: float32(face.Scale) * 0.25, leftEye.Col,
Perturbs: perturb, int(leftEye.Scale),
} ))
}
rightEye := plc.RunDetector(*puploc, params.ImageParams, fd.angle, false) // Find right eye.
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: fd.perturb,
}
if rightEye.Row > 0 && rightEye.Col > 0 { rightEye := plc.RunDetector(*puploc, params.ImageParams, fd.angle, false)
eyesCoords = append(eyesCoords, NewPoint(
"eye_r",
rightEye.Row,
rightEye.Col,
int(rightEye.Scale),
))
}
if rightEye.Row > 0 && rightEye.Col > 0 {
eyesCoords = append(eyesCoords, NewPoint(
"eye_r",
rightEye.Row,
rightEye.Col,
int(rightEye.Scale),
))
}
if leftEye != nil && rightEye != nil {
for _, eye := range eyeCascades { for _, eye := range eyeCascades {
for _, flpc := range flpcs[eye] { for _, flpc := range flpcs[eye] {
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, false) if flpc == nil {
continue
}
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, fd.perturb, false)
if flp.Row > 0 && flp.Col > 0 { if flp.Row > 0 && flp.Col > 0 {
landmarkCoords = append(landmarkCoords, NewPoint( landmarkCoords = append(landmarkCoords, NewPoint(
eye, eye,
@ -230,7 +236,7 @@ func (fd *Detector) Results(faces []pigo.Detection, params pigo.CascadeParams) (
)) ))
} }
flp = flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, true) flp = flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, fd.perturb, true)
if flp.Row > 0 && flp.Col > 0 { if flp.Row > 0 && flp.Col > 0 {
landmarkCoords = append(landmarkCoords, NewPoint( landmarkCoords = append(landmarkCoords, NewPoint(
eye+"_v", eye+"_v",
@ -241,22 +247,31 @@ func (fd *Detector) Results(faces []pigo.Detection, params pigo.CascadeParams) (
} }
} }
} }
}
// Find mouth. // Find mouth.
for _, mouth := range mouthCascades { for _, mouth := range mouthCascades {
for _, flpc := range flpcs[mouth] { for _, flpc := range flpcs[mouth] {
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, false) if flpc == nil {
if flp.Row > 0 && flp.Col > 0 { continue
landmarkCoords = append(landmarkCoords, NewPoint( }
"mouth_"+mouth,
flp.Row, flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, fd.perturb, false)
flp.Col, if flp.Row > 0 && flp.Col > 0 {
int(flp.Scale), landmarkCoords = append(landmarkCoords, NewPoint(
)) "mouth_"+mouth,
} flp.Row,
flp.Col,
int(flp.Scale),
))
} }
} }
flp := flpcs["lp84"][0].GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, true) }
flpc := flpcs["lp84"][0]
if flpc != nil {
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, fd.perturb, true)
if flp.Row > 0 && flp.Col > 0 { if flp.Row > 0 && flp.Col > 0 {
landmarkCoords = append(landmarkCoords, NewPoint( landmarkCoords = append(landmarkCoords, NewPoint(
"lp84", "lp84",
@ -266,16 +281,18 @@ func (fd *Detector) Results(faces []pigo.Detection, params pigo.CascadeParams) (
)) ))
} }
} }
detections = append(detections, Face{
Rows: params.ImageParams.Rows,
Cols: params.ImageParams.Cols,
Face: faceCoord,
Eyes: eyesCoords,
Landmarks: landmarkCoords,
})
} }
results = append(results, Face{
Rows: params.ImageParams.Rows,
Cols: params.ImageParams.Cols,
Score: int(face.Q),
Face: faceCoord,
Eyes: eyesCoords,
Landmarks: landmarkCoords,
})
} }
return detections, nil return results, nil
} }

View file

@ -47,6 +47,7 @@ type Faces []Face
type Face struct { type Face struct {
Rows int `json:"rows,omitempty"` Rows int `json:"rows,omitempty"`
Cols int `json:"cols,omitempty"` Cols int `json:"cols,omitempty"`
Score int `json:"score,omitempty"`
Face Point `json:"face,omitempty"` Face Point `json:"face,omitempty"`
Eyes Points `json:"eyes,omitempty"` Eyes Points `json:"eyes,omitempty"`
Landmarks Points `json:"landmarks,omitempty"` Landmarks Points `json:"landmarks,omitempty"`

View file

@ -16,7 +16,7 @@ func TestDetect(t *testing.T) {
"2.jpg": 1, "2.jpg": 1,
"3.jpg": 1, "3.jpg": 1,
"4.jpg": 1, "4.jpg": 1,
"5.jpg": 2, "5.jpg": 1,
"6.jpg": 1, "6.jpg": 1,
"7.jpg": 0, "7.jpg": 0,
"8.jpg": 0, "8.jpg": 0,
@ -30,6 +30,7 @@ func TestDetect(t *testing.T) {
"16.jpg": 1, "16.jpg": 1,
"17.jpg": 1, "17.jpg": 1,
"18.jpg": 2, "18.jpg": 2,
"19.jpg": 0,
} }
if err := fastwalk.Walk("testdata", func(fileName string, info os.FileMode) error { if err := fastwalk.Walk("testdata", func(fileName string, info os.FileMode) error {
@ -40,7 +41,7 @@ func TestDetect(t *testing.T) {
t.Run(fileName, func(t *testing.T) { t.Run(fileName, func(t *testing.T) {
baseName := filepath.Base(fileName) baseName := filepath.Base(fileName)
faces, err := Detect(fileName, DefaultDetector()) faces, err := Detect(fileName)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -58,7 +59,7 @@ func TestDetect(t *testing.T) {
} }
if i, ok := expected[baseName]; ok { if i, ok := expected[baseName]; ok {
assert.Equal(t, len(faces), i) assert.Equal(t, i, len(faces))
} else { } else {
t.Errorf("unknown test result for %s", baseName) t.Errorf("unknown test result for %s", baseName)
} }

View file

@ -1,12 +1,16 @@
package face package face
import ( import (
"embed"
"errors" "errors"
"path/filepath" "path/filepath"
pigo "github.com/esimov/pigo/core" pigo "github.com/esimov/pigo/core"
) )
//go:embed cascade/lps
var efs embed.FS
// FlpCascade holds the binary representation of the facial landmark points cascade files // FlpCascade holds the binary representation of the facial landmark points cascade files
type FlpCascade struct { type FlpCascade struct {
*pigo.PuplocCascade *pigo.PuplocCascade
@ -19,7 +23,7 @@ func ReadCascadeDir(plc *pigo.PuplocCascade, path string) (result map[string][]*
cascades, err := efs.ReadDir(path) cascades, err := efs.ReadDir(path)
if len(cascades) == 0 { if len(cascades) == 0 {
return nil, errors.New("the cascade directory is empty") return result, errors.New("the cascade directory is empty")
} }
if err != nil { if err != nil {
@ -27,11 +31,20 @@ func ReadCascadeDir(plc *pigo.PuplocCascade, path string) (result map[string][]*
} }
for _, cascade := range cascades { for _, cascade := range cascades {
cf, err := filepath.Abs(path + "/" + cascade.Name()) cf := filepath.Join(path, cascade.Name())
f, err := efs.ReadFile(cf)
if err != nil { if err != nil {
return nil, err return result, err
} }
flpc, err := plc.UnpackFlp(cf)
flpc, err := plc.UnpackCascade(f)
if err != nil {
return result, err
}
result[cascade.Name()] = append(result[cascade.Name()], &FlpCascade{flpc, err}) result[cascade.Name()] = append(result[cascade.Name()], &FlpCascade{flpc, err})
} }

BIN
internal/face/testdata/19.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -9,9 +9,11 @@ import (
"time" "time"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/classify" "github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/meta" "github.com/photoprism/photoprism/internal/meta"
"github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
@ -600,6 +602,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
if file.FilePrimary { if file.FilePrimary {
labels := photo.ClassifyLabels() labels := photo.ClassifyLabels()
if Config().Experimental() && Config().Settings().Features.People {
faces := ind.detectFaces(m)
photo.AddLabels(classify.FaceLabels(len(faces), entity.SrcImage, 10))
photo.PhotoPeople = len(faces)
}
if err := photo.UpdateTitle(labels); err != nil { if err := photo.UpdateTitle(labels); err != nil {
log.Debugf("%s in %s (update title)", err, logName) log.Debugf("%s in %s (update title)", err, logName)
} }
@ -759,7 +768,7 @@ func (ind *Index) NSFW(jpeg *MediaFile) bool {
return false return false
} }
// classifyImage returns all matching labels for a media file. // classifyImage classifies a JPEG image and returns matching labels.
func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) { func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
start := time.Now() start := time.Now()
@ -812,3 +821,31 @@ func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
return results return results
} }
// detectFaces detects faces in a JPEG image and returns them.
func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
if jpeg == nil {
return face.Faces{}
}
thumbName, err := jpeg.Thumbnail(Config().ThumbPath(), "fit_720")
if err != nil {
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
return face.Faces{}
}
start := time.Now()
faces, err := face.Detect(thumbName)
if err != nil {
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
}
elapsed := time.Since(start)
log.Debugf("index: face detection took %s", elapsed)
return faces
}