1
0
Fork 0

Faceserver apiserver initial import

master
Petr Masopust 6 years ago
parent 2589c60e15
commit 85d42b7a32
  1. 8
      apiserver/apiserver.yaml
  2. 37
      apiserver/apiserver/math.go
  3. 27
      apiserver/apiserver/math_test.go
  4. 224
      apiserver/apiserver/server.go
  5. 94
      apiserver/apiserver/storage.go
  6. 71
      apiserver/apiserver/vectorizer.go
  7. 9
      apiserver/go.mod
  8. 39
      apiserver/main.go

@ -0,0 +1,8 @@
port: 8081
vectorizer:
url: http://localhost:8080/vectorize
db:
user: faceserver
password: aaa
name: faceserver
host: localhost

@ -0,0 +1,37 @@
package apiserver
import (
"math"
)
// replaced with single run version of CosinMetric
func FrobeniusNorm(arr []float64) float64 {
sum := 0.0
for _, num := range arr {
anum := math.Abs(num)
sum += anum * anum
}
return math.Sqrt(sum)
}
func CosinMetric(x []float64, y []float64) float64 {
l := len(x)
if l != len(y) {
return -1.0
}
xsum := 0.0
ysum := 0.0
sum := 0.0
idx := 0
for idx < l {
xabs := math.Abs(x[idx])
yabs := math.Abs(y[idx])
xsum += xabs * xabs
ysum += yabs * yabs
sum += x[idx] * y[idx]
idx++
}
return sum / (math.Sqrt(xsum) * math.Sqrt(ysum))
}

@ -0,0 +1,27 @@
package apiserver
import (
"math"
"testing"
)
const eps = 1e-8
func equaleps(a, b float64) bool {
return math.Abs(a-b) < eps
}
func TestFrobeniusNorm(t *testing.T) {
norm := FrobeniusNorm([]float64{-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0})
if !equaleps(norm, 7.745966692414834) {
t.Errorf("Norm was incorrect, got: %f, want: %f.", norm, 7.745966692414834)
}
}
func TestCosinMetric(t *testing.T) {
norm := CosinMetric([]float64{0.46727048, 0.43004233, 0.27952332, 0.1524828, 0.47310451},
[]float64{0.03538705, 0.81665373, 0.15395064, 0.29546334, 0.50521321})
if !equaleps(norm, 0.8004287073454146) {
t.Errorf("CosineMetric was incorrect, got: %f, want: %f.", norm, 0.8004287073454146)
}
}

@ -0,0 +1,224 @@
package apiserver
import (
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/spf13/viper"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
var Dbo PgStorage
type JsonPerson struct {
Id string `json:"id,omitempty"`
Box []uint32 `json:"box,omitempty"`
Score float64 `json:"score,omitempty"`
Probability float64 `json:"probability,omitempty"`
}
type JsonResponse struct {
Status string `json:"status,omitempty"`
Url string `json:"url,omitempty"`
Filename string `json:"filename,omitempty"`
Directory string `json:"directory,omitempty"`
Persons []JsonPerson `json:"persons"`
}
func sendError(w http.ResponseWriter, err error) {
log.Printf("%v\n", err)
jsonResponse(w, 400, JsonResponse{
Status: err.Error(),
})
}
func Learn(w http.ResponseWriter, r *http.Request) {
filename, uid, result, err := uploadSave(w, r)
if err != nil {
sendError(w, err)
return
}
if len(result) != 1 {
sendError(w, errors.New("More than one face detected."))
return
}
pid := r.FormValue("person")
if pid == "" {
sendError(w, errors.New("Person identification is required."))
return
}
directory := r.FormValue("directory")
if directory == "" {
sendError(w, errors.New("Directory is required."))
return
}
person := Person{
Id: pid,
Directory: directory,
Filename: filename,
FilenameUid: uid,
Score: result[0].Score,
Box: result[0].Box,
Vector: result[0].Vector,
}
err = Dbo.Store(person)
if err != nil {
sendError(w, err)
return
}
jsonResponse(w, http.StatusCreated, JsonResponse{
Status: "OK",
Url: "/files/" + uid,
Filename: filename,
Directory: directory,
Persons: []JsonPerson{{
Id: pid,
Box: person.Box,
Score: person.Score,
}},
})
}
func Recognize(w http.ResponseWriter, r *http.Request) {
filename, uid, result, err := uploadSave(w, r)
if err != nil {
sendError(w, err)
return
}
directory := r.FormValue("directory")
if directory == "" {
sendError(w, errors.New("Directory is required."))
return
}
persons, err := Dbo.GetDirectory(directory)
if err != nil {
sendError(w, err)
return
}
jp := []JsonPerson{}
for _, r := range result {
maxprob := -1.0
var maxperson Person
for _, p := range persons {
cm := CosinMetric(r.Vector, p.Vector)
if cm > maxprob {
maxprob = cm
maxperson = p
}
}
jp = append(jp, JsonPerson{
Id: maxperson.Id,
Box: r.Box,
Score: r.Score,
Probability: maxprob,
})
}
jsonResponse(w, http.StatusCreated, JsonResponse{
Status: "OK",
Url: "/files/" + uid,
Filename: filename,
Directory: directory,
Persons: jp,
})
}
func uploadSave(w http.ResponseWriter, r *http.Request) (string, string, []VectorizerResult, error) {
if err := checkMethod(w, r); err != nil {
return "", "", nil, err
}
if err := r.ParseMultipartForm(32 << 20); err != nil {
return "", "", nil, err
}
file, handle, err := r.FormFile("file")
if err != nil {
return "", "", nil, err
}
defer file.Close()
mimeType := handle.Header.Get("Content-Type")
if err := checkFileType(mimeType); err != nil {
return "", "", nil, err
}
uid, err := saveFile(w, file, handle)
if err != nil {
return "", "", nil, err
}
reader, err := os.Open("./files/" + uid)
if err != nil {
return "", "", nil, err
}
defer reader.Close()
results, err := Vectorize(uid, reader, viper.GetString("vectorizer.url"))
if err != nil {
return "", "", nil, err
}
return handle.Filename, uid, results, nil
}
func checkMethod(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost {
return errors.New("POST method required")
}
return nil
}
func checkFileType(mimeType string) error {
switch mimeType {
case "image/jpeg", "image/png":
return nil
default:
return errors.New(fmt.Sprintf("Invalid file format %s", mimeType))
}
}
func generateFilename(filename string) string {
e := filepath.Ext(filename)
uid := uuid.New().String()
return uid + e
}
func saveFile(w http.ResponseWriter, file multipart.File, handle *multipart.FileHeader) (string, error) {
uid := generateFilename(handle.Filename)
f, err := os.OpenFile("./files/"+uid, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return "", err
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
return "", err
}
return uid, nil
}
func jsonResponse(w http.ResponseWriter, code int, message JsonResponse) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
resp, err := json.Marshal(message)
if err != nil {
log.Fatalf("Cannot format %v", message)
}
w.Write(resp)
}

@ -0,0 +1,94 @@
package apiserver
import (
"database/sql"
"fmt"
"github.com/lib/pq"
)
type Person struct {
Id string
Directory string
Filename string
FilenameUid string
Score float64
Box []uint32
Vector []float64
}
func (u Person) String() string {
return fmt.Sprintf("Person<%s %s %f %s %s %v %v>", u.Id, u.Directory, u.Score, u.Filename, u.FilenameUid, u.Box, u.Vector)
}
type PgStorage struct {
db *sql.DB
}
func NewStorage(user string, password string, database string, host string) (PgStorage, error) {
connStr := fmt.Sprintf("user=%s dbname=%s password=%s host=%s", user, database, password, host)
db, err := sql.Open("postgres", connStr)
if err != nil {
return PgStorage{}, err
}
pgo := PgStorage{
db: db,
}
return pgo, nil
}
func (pgo *PgStorage) CloseStorage() {
pgo.db.Close()
}
func (pgo *PgStorage) Store(person Person) error {
_, err := pgo.db.Exec("insert into persons (id, directory, filename, filenameuid, score, box, vector) values ($1, $2, $3, $4, $5, $6, $7)",
person.Id, person.Directory, person.Filename, person.FilenameUid, person.Score, pq.Array(person.Box), pq.Array(person.Vector))
return err
}
func (pgo *PgStorage) GetDirectory(directory string) ([]Person, error) {
var persons []Person
rows, err := pgo.db.Query("select id, filename, filenameuid, score, box, vector from persons where directory=$1", directory)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var id string
var filename string
var filenameuid string
var score float64
var box []int64
var vector []float64
err = rows.Scan(&id, &filename, &filenameuid, &score, pq.Array(&box), pq.Array(&vector))
if err != nil {
return nil, err
}
rebox := make([]uint32, len(box))
for i, v := range box {
rebox[i] = uint32(v)
}
persons = append(persons, Person{
Id: id,
Directory: directory,
Filename: filename,
FilenameUid: filenameuid,
Score: score,
Box: rebox,
Vector: vector,
})
}
err = rows.Err()
if err != nil {
return nil, err
}
return persons, nil
}

@ -0,0 +1,71 @@
package apiserver
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/textproto"
"path/filepath"
"strings"
)
type VectorizerResult struct {
Box []uint32 `json:"box"`
Vector []float64 `json:"vector"`
Score float64 `json:"score"`
}
func Vectorize(filename string, reader io.Reader, vectorizerUrl string) ([]VectorizerResult, error) {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", filepath.Base(filename)))
switch e := strings.ToLower(filepath.Ext(filename)); e {
case ".png":
h.Set("Content-Type", "image/png")
case ".jpg", ".jpeg":
h.Set("Content-Type", "image/jpeg")
default:
return nil, errors.New(fmt.Sprintf("Invalid extension %s", e))
}
fileWriter, err := bodyWriter.CreatePart(h)
if err != nil {
fmt.Println("error writing to buffer")
return nil, err
}
//iocopy
_, err = io.Copy(fileWriter, reader)
if err != nil {
return nil, err
}
contentType := bodyWriter.FormDataContentType()
bodyWriter.Close()
resp, err := http.Post(vectorizerUrl, contentType, bodyBuf)
if err != nil {
return nil, err
}
defer resp.Body.Close()
resp_body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result []VectorizerResult
err = json.Unmarshal(resp_body, &result)
if err != nil {
return nil, err
}
return result, nil
}

@ -0,0 +1,9 @@
module gitea.ehp.cz/Aprar/faceserver
go 1.12
require (
github.com/google/uuid v1.1.1
github.com/lib/pq v1.2.0
github.com/spf13/viper v1.4.0
)

@ -0,0 +1,39 @@
package main
import (
"fmt"
"log"
"net/http"
"strconv"
"gitea.ehp.cz/Aprar/faceserver/apiserver"
"github.com/spf13/viper"
)
func main() {
viper.SetConfigName("apiserver") // name of config file (without extension)
viper.AddConfigPath("/etc/faceserver/") // path to look for the config file in
viper.AddConfigPath("$HOME/.faceserver") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
viper.SetEnvPrefix("AS_")
viper.AutomaticEnv()
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
apiserver.Dbo, err = apiserver.NewStorage(viper.GetString("db.user"), viper.GetString("db.password"), viper.GetString("db.name"), viper.GetString("db.host"))
if err != nil {
panic(fmt.Errorf("Fatal error database connection: %s \n", err))
}
http.Handle("/", http.FileServer(http.Dir("./public")))
fs := http.FileServer(http.Dir("./files"))
http.Handle("/files/", http.StripPrefix("/files", fs))
http.HandleFunc("/learn", apiserver.Learn)
http.HandleFunc("/recognize", apiserver.Recognize)
log.Println("Running")
log.Fatal(http.ListenAndServe(":" + strconv.Itoa(viper.GetInt("port")), nil))
}
Loading…
Cancel
Save