Skip to content

Commit

Permalink
feat(classifier): add neural network classifier for face reconization
Browse files Browse the repository at this point in the history
BREAK CHANGE:
  - Instance changed to Estimator
  - People saved into Storage (storage serialized as a zip file includes
    people.pb and classifier.model)
  • Loading branch information
bububa committed Oct 15, 2021
1 parent 8fa7ba6 commit 2cb4781
Show file tree
Hide file tree
Showing 19 changed files with 787 additions and 290 deletions.
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ all: *
proto:
protoc --go_out=./core ./core/*.proto
facenet:
go build -o=./bin/facenet ./cmd/facenet
go build -o=./bin/facenet ./cmd/facenet
cvcamera:
go build -o=./bin/cvcamera -tags=cv4 -ldflags "-linkmode external -s -w '-extldflags=-mmacosx-version-min=10.10'" ./cmd/camera
go build -o=./bin/cvcamera -tags=cv4 ./cmd/camera
linux_camera:
go build -o=./bin/linux_camera -tags=linux -ldflags "-linkmode external -s -w" ./cmd/camera
go build -o=./bin/linux_camera -tags=linux ./cmd/camera
android_camera:
go build -o=./bin/android_camera -tags=android -ldflags "-linkmode external -s -w" ./cmd/camera
go build -o=./bin/android_camera -tags=android ./cmd/camera
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,27 @@ go get -u github.com/bububa/facenet
### Train faces

```bash
./bin/facenet -net=./models/facenet -people=./models/people/people.pb -train={image folder for training} -output={fold path for output thumbs(optional)}
./bin/facenet -model=./models/facenet -db=./models/people.db -train={image folder for training} -output={fold path for output thumbs(optional)}
```

the train folder include folders which name is the label with images inside

### Update distinct labels

```bash
./bin/facenet -net=./models/facenet -people=./models/people/people.pb -update={labels for update seperated by comma} -output={fold path for output thumbs(optional)}
./bin/facenet -model=./models/facenet -db=./models/people.db -update={labels for update seperated by comma} -output={fold path for output thumbs(optional)}
```

### Delete distinct labels from people model

```bash
./bin/facenet -net=./models/facenet -people=./models/people/people.pb -delete={labels for delete seperated by comma} -output={fold path for output thumbs(optional)}
./bin/facenet -model=./models/facenet -db=./models/people.db -delete={labels for delete seperated by comma} -output={fold path for output thumbs(optional)}
```

### Detect faces for image

```bash
./bin/facenet -net=./models/facenet -people=./models/people/people.pb -detect={the image file path for detecting} -font={font folder for output image(optional)} -output={fold path for output thumbs(optional)}
./bin/facenet -model=./models/facenet -db=./models/people.db -detect={the image file path for detecting} -font={font folder for output image(optional)} -output={fold path for output thumbs(optional)}
```

## Camera & Server
Expand Down Expand Up @@ -99,6 +99,10 @@ Usage of camera:
Frame height (default 480)
-index int
Camera index
-model string
saved_mode path
-db string
classifier db
```

## User as lib
Expand All @@ -113,15 +117,15 @@ import (
)

func main() {
instance, err := facenet.New(
facenet.WithNet("./models/facenet"),
facenet.WithPeople("./models/people/people.pb"),
estimator, err := facenet.New(
facenet.WithModel("./models/facenet"),
facenet.WithDB("./models/people.db"),
facenet.WithFontPath("./font"),
)
if err != nil {
log.Fatalln(err)
}
err = instance.SetFont(&draw2d.FontData{
err = estimator.SetFont(&draw2d.FontData{
Name: "NotoSansCJKsc",
//Name: "Roboto",
Family: draw2d.FontFamilySans,
Expand All @@ -135,13 +139,13 @@ func main() {
{
labels := []string{"xxx", "yyy"}
for _, label := range labels {
if deleted := instance.DeletePerson(label); deleted {
if deleted := estimator.DeletePerson(label); deleted {
log.Printf("[INFO] person: %s deleted\n", label)
continue
}
log.Printf("[WRN] person: %s not found\n", label)
}
err := instance.SaveModel(request.People)
err := estimator.SaveDB("./models/people.db")
if err != nil {
log.Fatalln(err)
}
Expand All @@ -168,7 +172,7 @@ func main() {
failedColor := "#F44336"
strokeWidth := 2
successMarkerOnly := false
markerImg := instance.DrawMarkers(markers, txtColor, successColor, failedColor, 2, successMarkerOnly)
markerImg := estimator.DrawMarkers(markers, txtColor, successColor, failedColor, 2, successMarkerOnly)
if err := saveImage(markerImg, outputPath); err != nil {
log.Fatalln(err)
}
Expand Down
40 changes: 40 additions & 0 deletions classifier/classifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package classifier

import (
"io"

"github.com/bububa/facenet/core"
)

// Classifier represents classifier interface
type Classifier interface {
Identity() ClassifierIdentity
Train(people *core.People, split float64, iterations int, verbosity int)
BatchTrain(people *core.People, split float64, iterations int, verbosity int, batch int)
Predict(input []float32) []float64
Match(input []float32) (int, float64)
Write(io.Writer) error
Read(io.Reader) error
}

// ClassifierIdentity represents classifier type
type ClassifierIdentity int

const (
// UnknownClassifier represents unknown classifier which is not defined
UnknownClassifier ClassifierIdentity = iota
// NeuralClassifier represents neural deep learning classifier
NeuralClassifier
// BayesClassifier represents bayes classifier
BayesClassifier
)

// NewDefault returns a default Neural classifier
func NewDefault() Classifier {
return new(Neural)
}

const (
// NeuralMatchThreshold returns neural classifier match threshold
NeuralMatchThreshold float64 = 0.75
)
143 changes: 143 additions & 0 deletions classifier/deep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package classifier

import (
"encoding/json"
"io"

deep "github.com/patrikeh/go-deep"
"github.com/patrikeh/go-deep/training"

"github.com/bububa/facenet/core"
)

// Neural represents neural classifier
type Neural struct {
ml *deep.Neural
threshold float64
}

// Name return sclassifier name
func (n *Neural) Identity() ClassifierIdentity {
return NeuralClassifier
}

// Write implement Classifier interface
func (n *Neural) Write(w io.Writer) error {
dump := n.ml.Dump()
return json.NewEncoder(w).Encode(dump)
}

// Read implement Classifier interface
func (n *Neural) Read(r io.Reader) error {
var dump deep.Dump
if err := json.NewDecoder(r).Decode(&dump); err != nil {
return err
}
n.ml = deep.FromDump(&dump)
return nil
}

// SetThreadshold set Neural match threshold
func (n *Neural) SetThreadshold(threshold float64) {
n.threshold = threshold
}

func (n *Neural) peopleToExamples(people *core.People, split float64) (training.Examples, training.Examples) {
var data training.Examples
var heldout training.Examples
classes := len(people.GetList())
for idx, person := range people.GetList() {
var examples training.Examples
embeddings := person.GetEmbeddings()
for _, embedding := range embeddings {
e := training.Example{
Response: onehot(classes, idx),
Input: convInputs(embedding.GetValue()),
}
deep.Standardize(e.Input)
examples = append(examples, e)
}
examples.Shuffle()
t, h := examples.Split(split)
data = append(data, t...)
heldout = append(heldout, h...)
}
data.Shuffle()
heldout.Shuffle()
return data, heldout
}

func (n *Neural) initDeep(inputs int, layout []int, std float64, mean float64) {
n.ml = deep.NewNeural(&deep.Config{
Inputs: inputs,
Layout: layout,
// Activation: deep.ActivationTanh,
// Activation: deep.ActivationSigmoid,
Activation: deep.ActivationReLU,
//Activation: deep.ActivationSoftmax,
Mode: deep.ModeMultiClass,
Weight: deep.NewNormal(std, mean),
Bias: true,
})
}

// Train implement Classifier interface
func (n *Neural) Train(people *core.People, split float64, iterations int, verbosity int) {
n.initDeep(512, []int{64, 16, len(people.GetList())}, 0.5, 0)
//trainer := training.NewTrainer(training.NewSGD(0.01, 0.5, 1e-6, true), 1)
//trainer := training.NewTrainer(training.NewSGD(0.005, 0.5, 1e-6, true), 50)
//trainer := training.NewBatchTrainer(training.NewSGD(0.005, 0.1, 0, true), 50, 300, 16)
//trainer := training.NewTrainer(training.NewAdam(0.1, 0, 0, 0), 50)
// solver := training.NewSGD(0.01, 0.5, 1e-6, true)
solver := training.NewAdam(0.02, 0.9, 0.999, 1e-8)
trainer := training.NewTrainer(solver, verbosity)
data, heldout := n.peopleToExamples(people, split)
trainer.Train(n.ml, data, heldout, iterations)
}

// BatchTrain implement Classifier interface
func (n *Neural) BatchTrain(people *core.People, split float64, iterations int, verbosity int, batch int) {
n.initDeep(512, []int{64, 16, len(people.GetList())}, 0.5, 0)
//solver := training.NewSGD(0.01, 0.5, 1e-6, true)
solver := training.NewAdam(0.02, 0.9, 0.999, 1e-8)
trainer := training.NewBatchTrainer(solver, verbosity, batch, 4)
data, heldout := n.peopleToExamples(people, split)
trainer.Train(n.ml, data, heldout, iterations)
}

// Predict implement Classifier interface
func (n *Neural) Predict(embedding []float32) []float64 {
return n.ml.Predict(convInputs(embedding))
}

// Match returns best match result
func (n *Neural) Match(input []float32) (int, float64) {
scores := n.Predict(input)
var index = -1
var maxScore float64
threshold := n.threshold
if threshold < 1e-15 {
threshold = NeuralMatchThreshold
}
for idx, score := range scores {
if score >= threshold && maxScore < score {
maxScore = score
index = idx
}
}
return index, maxScore
}

func convInputs(embedding []float32) []float64 {
ret := make([]float64, len(embedding))
for i, v := range embedding {
ret[i] = float64(v)
}
return ret
}

func onehot(classes int, val int) []float64 {
res := make([]float64, classes)
res[val] = 1
return res
}
2 changes: 2 additions & 0 deletions classifier/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package classifier include different classifiers
package classifier
30 changes: 15 additions & 15 deletions cmd/camera/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ import (
)

var (
opts camera.Options
net *facenet.Instance
bind string
netPath string
peoplePath string
fontPath string
opts camera.Options
estimator *facenet.Estimator
bind string
modelPath string
dbPath string
fontPath string
)

func init() {
Expand All @@ -35,8 +35,8 @@ func init() {
flag.Float64Var(&opts.Width, "width", 640, "Frame width")
flag.Float64Var(&opts.Height, "height", 480, "Frame height")
flag.StringVar(&bind, "bind", ":56000", "set server bind")
flag.StringVar(&netPath, "net", "", "set facenet model path")
flag.StringVar(&peoplePath, "people", "", "set people model path")
flag.StringVar(&modelPath, "model", "", "set facenet model path")
flag.StringVar(&dbPath, "db", "", "set db path")
flag.StringVar(&fontPath, "font", "", "set font path")
}

Expand All @@ -46,18 +46,18 @@ func setup() error {
if err != nil {
return err
}
netPath = cleanPath(wd, netPath)
peoplePath = cleanPath(wd, peoplePath)
modelPath = cleanPath(wd, modelPath)
dbPath = cleanPath(wd, dbPath)
fontPath = cleanPath(wd, fontPath)
net, err = facenet.New(
facenet.WithNet(netPath),
facenet.WithPeople(peoplePath),
estimator, err = facenet.New(
facenet.WithModel(modelPath),
facenet.WithDB(dbPath),
facenet.WithFontPath(fontPath),
)
if err != nil {
return err
}
if err := net.SetFont(&draw2d.FontData{
if err := estimator.SetFont(&draw2d.FontData{
Name: "NotoSansCJKsc",
//Name: "Roboto",
Family: draw2d.FontFamilySans,
Expand All @@ -80,7 +80,7 @@ func main() {
}
log.Println("starting server...")
cam := camera.NewCamera(device)
srv := server.New(bind, net, cam)
srv := server.New(bind, estimator, cam)
srv.SetFrameSize(opts.Width, opts.Height)
srv.SetDelay(opts.Delay)

Expand Down
12 changes: 6 additions & 6 deletions cmd/camera/server/handlers/jpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (

// JPEG handler.
type JPEG struct {
ins *facenet.Instance
e *facenet.Estimator
cam *camera.Camera
}

// NewJPEG returns new JPEG handler.
func NewJPEG(ins *facenet.Instance, cam *camera.Camera) *JPEG {
return &JPEG{ins, cam}
func NewJPEG(e *facenet.Estimator, cam *camera.Camera) *JPEG {
return &JPEG{e, cam}
}

// ServeHTTP handles requests on incoming connections.
Expand All @@ -36,9 +36,9 @@ func (s *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("jpeg: read: %v", err)
return
}
if s.ins != nil {
if markers, err := s.ins.DetectFaces(img, DetectMinSize); err == nil {
img = s.ins.DrawMarkers(markers, TextColor, SuccessColor, FailedColor, StrokeWidth, false)
if s.e != nil {
if markers, err := s.e.DetectFaces(img, DetectMinSize); err == nil {
img = s.e.DrawMarkers(markers, TextColor, SuccessColor, FailedColor, StrokeWidth, false)
}
}

Expand Down
Loading

0 comments on commit 2cb4781

Please sign in to comment.