People: Detect number of faces (experimental) #22
This commit is contained in:
parent
f5a1cc6231
commit
a6bf89d104
11 changed files with 267 additions and 169 deletions
frontend/src/pages/settings
internal
classify
entity
face
photoprism
|
@ -241,7 +241,7 @@
|
|||
</v-checkbox>
|
||||
</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-model="settings.features.people"
|
||||
:disabled="busy"
|
||||
|
|
|
@ -47,3 +47,24 @@ func LocationLabel(name string, uncertainty int) Label {
|
|||
func (l Label) Title() string {
|
||||
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,
|
||||
}}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"academic gown": {
|
||||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"accordion": {
|
||||
Label: "instrument",
|
||||
|
@ -516,7 +516,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"bathtub": {
|
||||
Label: "living",
|
||||
|
@ -630,7 +630,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"bernese mountain dog": {
|
||||
Label: "dog",
|
||||
|
@ -672,7 +672,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"binder": {
|
||||
Label: "office",
|
||||
|
@ -804,13 +804,13 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"bonnet": {
|
||||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"book jacket": {
|
||||
Label: "book",
|
||||
|
@ -882,7 +882,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"box turtle": {
|
||||
Label: "turtle",
|
||||
|
@ -924,7 +924,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"breakwater": {
|
||||
Label: "water",
|
||||
|
@ -936,7 +936,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"briard dog": {
|
||||
Label: "dog",
|
||||
|
@ -1134,7 +1134,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"cardigan dog": {
|
||||
Label: "dog",
|
||||
|
@ -1242,7 +1242,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"chain saw": {
|
||||
Label: "outdoor",
|
||||
|
@ -1392,7 +1392,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"clock": {
|
||||
Label: "display",
|
||||
|
@ -1686,7 +1686,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"cup": {
|
||||
Label: "",
|
||||
|
@ -2088,7 +2088,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"feather boa": {
|
||||
Label: "",
|
||||
|
@ -2298,7 +2298,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"gallery": {
|
||||
Label: "",
|
||||
|
@ -2466,7 +2466,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"grand piano": {
|
||||
Label: "instrument",
|
||||
|
@ -2616,7 +2616,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"hair spray": {
|
||||
Label: "bottle",
|
||||
|
@ -2820,7 +2820,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"horizontal bar": {
|
||||
Label: "",
|
||||
|
@ -3054,7 +3054,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"jeep": {
|
||||
Label: "",
|
||||
|
@ -3072,7 +3072,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"jigsaw puzzle": {
|
||||
Label: "puzzle",
|
||||
|
@ -3126,7 +3126,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"king crab": {
|
||||
Label: "crab",
|
||||
|
@ -3198,7 +3198,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"labrador retriever dog": {
|
||||
Label: "dog",
|
||||
|
@ -3516,7 +3516,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"malamute dog": {
|
||||
Label: "dog",
|
||||
|
@ -3660,7 +3660,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"milk can": {
|
||||
Label: "",
|
||||
|
@ -3696,7 +3696,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"minivan": {
|
||||
Label: "car",
|
||||
|
@ -3792,7 +3792,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"mosque": {
|
||||
Label: "tower",
|
||||
|
@ -3870,19 +3870,19 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"neck brace": {
|
||||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"necklace": {
|
||||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"nematode": {
|
||||
Label: "worm",
|
||||
|
@ -4020,7 +4020,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"ox": {
|
||||
Label: "cow",
|
||||
|
@ -4080,7 +4080,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"palace": {
|
||||
Label: "historic",
|
||||
|
@ -4220,6 +4220,12 @@ var rules = LabelRules{
|
|||
Priority: 0,
|
||||
Categories: []string{},
|
||||
},
|
||||
"people": {
|
||||
Label: "people",
|
||||
Threshold: 0.300000,
|
||||
Priority: 0,
|
||||
Categories: []string{},
|
||||
},
|
||||
"perfume": {
|
||||
Label: "bottle",
|
||||
Threshold: 0.700000,
|
||||
|
@ -4428,7 +4434,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"pool table": {
|
||||
Label: "",
|
||||
|
@ -4896,7 +4902,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"sax": {
|
||||
Label: "instrument",
|
||||
|
@ -5256,7 +5262,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"soft-coated wheaten terrier dog": {
|
||||
Label: "dog",
|
||||
|
@ -5274,7 +5280,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"sorrel": {
|
||||
Label: "",
|
||||
|
@ -5538,7 +5544,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"sulphur butterfly": {
|
||||
Label: "butterfly",
|
||||
|
@ -5598,7 +5604,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"swimming trunks": {
|
||||
Label: "portrait",
|
||||
|
@ -6060,7 +6066,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"viaduct": {
|
||||
Label: "building",
|
||||
|
@ -6282,7 +6288,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"white stork": {
|
||||
Label: "bird",
|
||||
|
@ -6330,7 +6336,7 @@ var rules = LabelRules{
|
|||
Label: "portrait",
|
||||
Threshold: 0.500000,
|
||||
Priority: 0,
|
||||
Categories: []string{"portrait"},
|
||||
Categories: []string{},
|
||||
},
|
||||
"wine bottle": {
|
||||
Label: "bottle",
|
||||
|
|
|
@ -143,8 +143,6 @@ rapeseed:
|
|||
fashion:
|
||||
label: portrait
|
||||
threshold: 0.5
|
||||
categories:
|
||||
- portrait
|
||||
|
||||
vestment:
|
||||
see: fashion
|
||||
|
@ -4099,6 +4097,10 @@ portrait:
|
|||
categories:
|
||||
- people
|
||||
|
||||
people:
|
||||
label: people
|
||||
threshold: 0.3
|
||||
|
||||
shower cap:
|
||||
label: portrait
|
||||
categories:
|
||||
|
|
|
@ -17,9 +17,9 @@ type Marker struct {
|
|||
RefUID string `gorm:"type:VARBINARY(42);index;" json:"UID" yaml:"UID,omitempty"`
|
||||
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
||||
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"`
|
||||
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"`
|
||||
Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"`
|
||||
W float32 `gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty"`
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
package face
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
pigo "github.com/esimov/pigo/core"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
_ "image/jpeg"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
pigo "github.com/esimov/pigo/core"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
//go:embed cascade/lps/*
|
||||
var efs embed.FS
|
||||
|
||||
//go:embed cascade/facefinder
|
||||
var cascadeFile []byte
|
||||
|
||||
|
@ -62,49 +58,58 @@ var (
|
|||
|
||||
// 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,
|
||||
}
|
||||
minSize int
|
||||
maxSize int
|
||||
angle float64
|
||||
shiftFactor float64
|
||||
scaleFactor float64
|
||||
iouThreshold float64
|
||||
scoreThreshold float32
|
||||
perturb int
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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))
|
||||
|
||||
faces, params, err := fd.Detect(fileName)
|
||||
if err != nil {
|
||||
return det, fmt.Errorf("face: %v (detect faces)", err)
|
||||
}
|
||||
|
||||
det, err = fd.Results(faces, params)
|
||||
det, params, err := fd.Detect(fileName)
|
||||
|
||||
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.
|
||||
|
@ -117,9 +122,7 @@ func (fd *Detector) Detect(fileName string) (faces []pigo.Detection, params pigo
|
|||
return faces, params, err
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
_ = file.Close()
|
||||
}(file)
|
||||
defer file.Close()
|
||||
|
||||
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.
|
||||
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 (
|
||||
qThresh float32 = 5.0
|
||||
perturb = 63
|
||||
)
|
||||
|
||||
var (
|
||||
detections Faces
|
||||
results Faces
|
||||
eyesCoords []Point
|
||||
landmarkCoords []Point
|
||||
puploc *pigo.Puploc
|
||||
)
|
||||
|
||||
for _, face := range faces {
|
||||
if face.Q > qThresh {
|
||||
faceCoord := NewPoint(
|
||||
"face",
|
||||
face.Row-face.Scale/2,
|
||||
face.Col-face.Scale/2,
|
||||
face.Scale,
|
||||
)
|
||||
for _, face := range det {
|
||||
if face.Q < fd.scoreThreshold {
|
||||
continue
|
||||
}
|
||||
|
||||
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: perturb,
|
||||
}
|
||||
faceCoord := NewPoint(
|
||||
"face",
|
||||
face.Row-face.Scale/2,
|
||||
face.Col-face.Scale/2,
|
||||
face.Scale,
|
||||
)
|
||||
|
||||
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 {
|
||||
eyesCoords = append(eyesCoords, NewPoint(
|
||||
"eye_l",
|
||||
leftEye.Row,
|
||||
leftEye.Col,
|
||||
int(leftEye.Scale),
|
||||
))
|
||||
}
|
||||
leftEye := 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: perturb,
|
||||
}
|
||||
if leftEye.Row > 0 && leftEye.Col > 0 {
|
||||
eyesCoords = append(eyesCoords, NewPoint(
|
||||
"eye_l",
|
||||
leftEye.Row,
|
||||
leftEye.Col,
|
||||
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 {
|
||||
eyesCoords = append(eyesCoords, NewPoint(
|
||||
"eye_r",
|
||||
rightEye.Row,
|
||||
rightEye.Col,
|
||||
int(rightEye.Scale),
|
||||
))
|
||||
}
|
||||
rightEye := plc.RunDetector(*puploc, params.ImageParams, fd.angle, false)
|
||||
|
||||
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 _, 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 {
|
||||
landmarkCoords = append(landmarkCoords, NewPoint(
|
||||
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 {
|
||||
landmarkCoords = append(landmarkCoords, NewPoint(
|
||||
eye+"_v",
|
||||
|
@ -241,22 +247,31 @@ func (fd *Detector) Results(faces []pigo.Detection, params pigo.CascadeParams) (
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find mouth.
|
||||
for _, mouth := range mouthCascades {
|
||||
for _, flpc := range flpcs[mouth] {
|
||||
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, perturb, false)
|
||||
if flp.Row > 0 && flp.Col > 0 {
|
||||
landmarkCoords = append(landmarkCoords, NewPoint(
|
||||
"mouth_"+mouth,
|
||||
flp.Row,
|
||||
flp.Col,
|
||||
int(flp.Scale),
|
||||
))
|
||||
}
|
||||
// Find mouth.
|
||||
for _, mouth := range mouthCascades {
|
||||
for _, flpc := range flpcs[mouth] {
|
||||
if flpc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
flp := flpc.GetLandmarkPoint(leftEye, rightEye, params.ImageParams, fd.perturb, false)
|
||||
if flp.Row > 0 && flp.Col > 0 {
|
||||
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 {
|
||||
landmarkCoords = append(landmarkCoords, NewPoint(
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ type Faces []Face
|
|||
type Face struct {
|
||||
Rows int `json:"rows,omitempty"`
|
||||
Cols int `json:"cols,omitempty"`
|
||||
Score int `json:"score,omitempty"`
|
||||
Face Point `json:"face,omitempty"`
|
||||
Eyes Points `json:"eyes,omitempty"`
|
||||
Landmarks Points `json:"landmarks,omitempty"`
|
||||
|
|
|
@ -16,7 +16,7 @@ func TestDetect(t *testing.T) {
|
|||
"2.jpg": 1,
|
||||
"3.jpg": 1,
|
||||
"4.jpg": 1,
|
||||
"5.jpg": 2,
|
||||
"5.jpg": 1,
|
||||
"6.jpg": 1,
|
||||
"7.jpg": 0,
|
||||
"8.jpg": 0,
|
||||
|
@ -30,6 +30,7 @@ func TestDetect(t *testing.T) {
|
|||
"16.jpg": 1,
|
||||
"17.jpg": 1,
|
||||
"18.jpg": 2,
|
||||
"19.jpg": 0,
|
||||
}
|
||||
|
||||
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) {
|
||||
baseName := filepath.Base(fileName)
|
||||
|
||||
faces, err := Detect(fileName, DefaultDetector())
|
||||
faces, err := Detect(fileName)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -58,7 +59,7 @@ func TestDetect(t *testing.T) {
|
|||
}
|
||||
|
||||
if i, ok := expected[baseName]; ok {
|
||||
assert.Equal(t, len(faces), i)
|
||||
assert.Equal(t, i, len(faces))
|
||||
} else {
|
||||
t.Errorf("unknown test result for %s", baseName)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package face
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
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
|
||||
type FlpCascade struct {
|
||||
*pigo.PuplocCascade
|
||||
|
@ -19,7 +23,7 @@ func ReadCascadeDir(plc *pigo.PuplocCascade, path string) (result map[string][]*
|
|||
cascades, err := efs.ReadDir(path)
|
||||
|
||||
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 {
|
||||
|
@ -27,11 +31,20 @@ func ReadCascadeDir(plc *pigo.PuplocCascade, path string) (result map[string][]*
|
|||
}
|
||||
|
||||
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 {
|
||||
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})
|
||||
}
|
||||
|
||||
|
|
BIN
internal/face/testdata/19.jpg
vendored
Normal file
BIN
internal/face/testdata/19.jpg
vendored
Normal file
Binary file not shown.
After (image error) Size: 81 KiB |
|
@ -9,9 +9,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/face"
|
||||
"github.com/photoprism/photoprism/internal/meta"
|
||||
"github.com/photoprism/photoprism/internal/nsfw"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
|
@ -600,6 +602,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if file.FilePrimary {
|
||||
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 {
|
||||
log.Debugf("%s in %s (update title)", err, logName)
|
||||
}
|
||||
|
@ -759,7 +768,7 @@ func (ind *Index) NSFW(jpeg *MediaFile) bool {
|
|||
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) {
|
||||
start := time.Now()
|
||||
|
||||
|
@ -812,3 +821,31 @@ func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue