parent
2589c60e15
commit
85d42b7a32
8 changed files with 509 additions and 0 deletions
@ -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…
Reference in new issue