├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── COPYING ├── Makefile ├── README.md ├── classify.cc ├── classify.h ├── doc.go ├── error.go ├── example_test.go ├── face.go ├── face_test.go ├── facerec.cc ├── facerec.h ├── go.mod ├── gofmt-staged.sh ├── jpeg_mem_loader.cc └── jpeg_mem_loader.h /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.blockchain.com/btc/payment_request?address=3LKKbbi34MHYRQSLV3ZiDGoKgUmCjhTumT&message=Kagami+open+source+projects+support 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /testdata 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | language: go 3 | go: 4 | - "stable" 5 | addons: 6 | apt: 7 | packages: 8 | - libdlib-dev 9 | - libblas-dev 10 | - libatlas-base-dev 11 | - liblapack-dev 12 | - libjpeg-turbo8-dev 13 | notifications: 14 | email: 15 | on_success: never 16 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | precommit: gofmt-staged 3 | 4 | gofmt-staged: 5 | ./gofmt-staged.sh 6 | 7 | testdata: 8 | git clone https://github.com/Kagami/go-face-testdata testdata 9 | 10 | test: testdata 11 | go test -v 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-face [![Build Status](https://travis-ci.org/Kagami/go-face.svg?branch=master)](https://travis-ci.org/Kagami/go-face) [![GoDoc](https://godoc.org/github.com/Kagami/go-face?status.svg)](https://godoc.org/github.com/Kagami/go-face) 2 | 3 | go-face implements face recognition for Go using [dlib](http://dlib.net), a 4 | popular machine learning toolkit. Read 5 | [Face recognition with Go](https://hackernoon.com/face-recognition-with-go-676a555b8a7e) 6 | article for some background details if you're new to 7 | [FaceNet](https://arxiv.org/abs/1503.03832) concept. 8 | 9 | ## Requirements 10 | 11 | To compile go-face you need to have dlib (>= 19.10) and libjpeg development 12 | packages installed. 13 | 14 | ### Ubuntu 18.10+, Debian sid 15 | 16 | Latest versions of Ubuntu and Debian provide suitable dlib package so just run: 17 | 18 | ```bash 19 | # Ubuntu 20 | sudo apt-get install libdlib-dev libblas-dev libatlas-base-dev liblapack-dev libjpeg-turbo8-dev 21 | # Debian 22 | sudo apt-get install libdlib-dev libblas-dev libatlas-base-dev liblapack-dev libjpeg62-turbo-dev 23 | ``` 24 | 25 | ### macOS 26 | 27 | Make sure you have [Homebrew](https://brew.sh) installed. 28 | 29 | ```bash 30 | brew install dlib 31 | ``` 32 | 33 | ### Windows 34 | 35 | Make sure you have [MSYS2](https://www.msys2.org) installed. 36 | 37 | 1. Run `MSYS2 MSYS` shell from Start menu 38 | 2. Run `pacman -Syu` and if it asks you to close the shell do that 39 | 3. Run `pacman -Syu` again 40 | 4. Run `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-dlib` 41 | 5. 42 | 1. If you already have Go and Git installed and available in PATH uncomment 43 | `set MSYS2_PATH_TYPE=inherit` line in `msys2_shell.cmd` located in MSYS2 44 | installation folder 45 | 2. Otherwise run `pacman -S mingw-w64-x86_64-go git` 46 | 6. Run `MSYS2 MinGW 64-bit` shell from Start menu to compile and use go-face 47 | 48 | ### Other systems 49 | 50 | Try to install dlib/libjpeg with package manager of your distribution or 51 | [compile from sources](http://dlib.net/compile.html). Note that go-face won't 52 | work with old packages of dlib such as libdlib18. Alternatively create issue 53 | with the name of your system and someone might help you with the installation 54 | process. 55 | 56 | ## Models 57 | 58 | Currently `shape_predictor_5_face_landmarks.dat`, `mmod_human_face_detector.dat` and 59 | `dlib_face_recognition_resnet_model_v1.dat` are required. You may download them 60 | from [go-face-testdata](https://github.com/Kagami/go-face-testdata) repo: 61 | 62 | ```bash 63 | wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat 64 | wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat 65 | wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat 66 | ``` 67 | 68 | ## Usage 69 | 70 | To use go-face in your Go code: 71 | 72 | ```go 73 | import "github.com/Kagami/go-face" 74 | ``` 75 | 76 | To install go-face in your $GOPATH: 77 | 78 | ```bash 79 | go get github.com/Kagami/go-face 80 | ``` 81 | 82 | For further details see [GoDoc documentation](https://godoc.org/github.com/Kagami/go-face). 83 | 84 | ## Example 85 | 86 | ```go 87 | package main 88 | 89 | import ( 90 | "fmt" 91 | "log" 92 | "path/filepath" 93 | 94 | "github.com/Kagami/go-face" 95 | ) 96 | 97 | // Path to directory with models and test images. Here it's assumed it 98 | // points to the clone. 99 | const dataDir = "testdata" 100 | 101 | var ( 102 | modelsDir = filepath.Join(dataDir, "models") 103 | imagesDir = filepath.Join(dataDir, "images") 104 | ) 105 | 106 | // This example shows the basic usage of the package: create an 107 | // recognizer, recognize faces, classify them using few known ones. 108 | func main() { 109 | // Init the recognizer. 110 | rec, err := face.NewRecognizer(modelsDir) 111 | if err != nil { 112 | log.Fatalf("Can't init face recognizer: %v", err) 113 | } 114 | // Free the resources when you're finished. 115 | defer rec.Close() 116 | 117 | // Test image with 10 faces. 118 | testImagePristin := filepath.Join(imagesDir, "pristin.jpg") 119 | // Recognize faces on that image. 120 | faces, err := rec.RecognizeFile(testImagePristin) 121 | if err != nil { 122 | log.Fatalf("Can't recognize: %v", err) 123 | } 124 | if len(faces) != 10 { 125 | log.Fatalf("Wrong number of faces") 126 | } 127 | 128 | // Fill known samples. In the real world you would use a lot of images 129 | // for each person to get better classification results but in our 130 | // example we just get them from one big image. 131 | var samples []face.Descriptor 132 | var cats []int32 133 | for i, f := range faces { 134 | samples = append(samples, f.Descriptor) 135 | // Each face is unique on that image so goes to its own category. 136 | cats = append(cats, int32(i)) 137 | } 138 | // Name the categories, i.e. people on the image. 139 | labels := []string{ 140 | "Sungyeon", "Yehana", "Roa", "Eunwoo", "Xiyeon", 141 | "Kyulkyung", "Nayoung", "Rena", "Kyla", "Yuha", 142 | } 143 | // Pass samples to the recognizer. 144 | rec.SetSamples(samples, cats) 145 | 146 | // Now let's try to classify some not yet known image. 147 | testImageNayoung := filepath.Join(imagesDir, "nayoung.jpg") 148 | nayoungFace, err := rec.RecognizeSingleFile(testImageNayoung) 149 | if err != nil { 150 | log.Fatalf("Can't recognize: %v", err) 151 | } 152 | if nayoungFace == nil { 153 | log.Fatalf("Not a single face on the image") 154 | } 155 | catID := rec.Classify(nayoungFace.Descriptor) 156 | if catID < 0 { 157 | log.Fatalf("Can't classify") 158 | } 159 | // Finally print the classified label. It should be "Nayoung". 160 | fmt.Println(labels[catID]) 161 | } 162 | ``` 163 | 164 | Run with: 165 | 166 | ```bash 167 | mkdir -p ~/go && cd ~/go # Or cd to your $GOPATH 168 | mkdir -p src/go-face-example && cd src/go-face-example 169 | git clone https://github.com/Kagami/go-face-testdata testdata 170 | edit main.go # Paste example code 171 | go get && go run main.go 172 | ``` 173 | 174 | ## Test 175 | 176 | To fetch test data and run tests: 177 | 178 | ```bash 179 | make test 180 | ``` 181 | 182 | ## FAQ 183 | 184 | ### How to improve recognition accuracy 185 | 186 | There are few suggestions: 187 | 188 | * Try CNN recognizing 189 | * Try different tolerance values of `ClassifyThreshold` 190 | * Try different size/padding/jittering values of `NewRecognizerWithConfig` 191 | * Provide more samples of each category to `SetSamples` if possible 192 | * Implement better classify heuristics (see [classify.cc](classify.cc)) 193 | * [Train](https://blog.dlib.net/2017/02/high-quality-face-recognition-with-deep.html) network (`dlib_face_recognition_resnet_model_v1.dat`) on your own test data 194 | 195 | ## License 196 | 197 | go-face is licensed under [CC0](COPYING). 198 | -------------------------------------------------------------------------------- /classify.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "classify.h" 4 | 5 | int classify( 6 | const std::vector& samples, 7 | const std::vector& cats, 8 | const descriptor& test_sample, 9 | float tolerance 10 | ) { 11 | if (samples.size() == 0) 12 | return -1; 13 | 14 | std::vector> distances; 15 | distances.reserve(samples.size()); 16 | auto dist_func = dlib::squared_euclidean_distance(); 17 | int idx = 0; 18 | for (const auto& sample : samples) { 19 | float dist = dist_func(sample, test_sample); 20 | if (tolerance < 0 || dist <= tolerance) { 21 | distances.push_back({cats[idx], dist}); 22 | } 23 | idx++; 24 | } 25 | 26 | if (distances.size() == 0) 27 | return -1; 28 | 29 | std::sort( 30 | distances.begin(), distances.end(), 31 | [](const auto a, const auto b) { return a.second < b.second; } 32 | ); 33 | 34 | int len = std::min((int)distances.size(), 10); 35 | std::unordered_map> hits_by_cat; 36 | for (int i = 0; i < len; i++) { 37 | int cat_idx = distances[i].first; 38 | float dist = distances[i].second; 39 | auto hit = hits_by_cat.find(cat_idx); 40 | if (hit == hits_by_cat.end()) { 41 | hits_by_cat[cat_idx] = {1, dist}; 42 | } else { 43 | hits_by_cat[cat_idx].first++; 44 | } 45 | } 46 | 47 | auto hit = std::max_element( 48 | hits_by_cat.begin(), hits_by_cat.end(), 49 | [](const auto a, const auto b) { 50 | auto hits1 = a.second.first; 51 | auto hits2 = b.second.first; 52 | auto dist1 = a.second.second; 53 | auto dist2 = b.second.second; 54 | if (hits1 == hits2) return dist1 > dist2; 55 | return hits1 < hits2; 56 | } 57 | ); 58 | return hit->first; 59 | } 60 | -------------------------------------------------------------------------------- /classify.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef dlib::matrix descriptor; 4 | 5 | int classify( 6 | const std::vector& samples, 7 | const std::vector& cats, 8 | const descriptor& test_sample, 9 | float tolerance 10 | ); 11 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package face implements face recognition for Go using dlib, a popular 3 | machine learning toolkit. 4 | */ 5 | package face 6 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package face 2 | 3 | // #include 4 | // #include "facerec.h" 5 | import "C" 6 | 7 | // An ImageLoadError is returned when provided image file is corrupted. 8 | type ImageLoadError string 9 | 10 | func (e ImageLoadError) Error() string { 11 | return string(e) 12 | } 13 | 14 | // An SerializationError is returned when provided model is corrupted. 15 | type SerializationError string 16 | 17 | func (e SerializationError) Error() string { 18 | return string(e) 19 | } 20 | 21 | // An UnknownError represents some nonclassified error. 22 | type UnknownError string 23 | 24 | func (e UnknownError) Error() string { 25 | return string(e) 26 | } 27 | 28 | // makeError constructs Go error for passed error info. 29 | func makeError(s string, code int) error { 30 | switch code { 31 | case C.IMAGE_LOAD_ERROR: 32 | return ImageLoadError(s) 33 | case C.SERIALIZATION_ERROR: 34 | return SerializationError(s) 35 | default: 36 | return UnknownError(s) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package face_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path/filepath" 7 | 8 | "github.com/Kagami/go-face" 9 | ) 10 | 11 | // Path to directory with models and test images. Here it's assumed it 12 | // points to the clone. 13 | const dataDir = "testdata" 14 | 15 | // This example shows the basic usage of the package: create an 16 | // recognizer, recognize faces, classify them using few known ones. 17 | func Example_basic() { 18 | // Init the recognizer. 19 | rec, err := face.NewRecognizer(filepath.Join(dataDir, "models")) 20 | if err != nil { 21 | log.Fatalf("Can't init face recognizer: %v", err) 22 | } 23 | // Free the resources when you're finished. 24 | defer rec.Close() 25 | 26 | // Test image with 10 faces. 27 | testImagePristin := filepath.Join(dataDir, "images", "pristin.jpg") 28 | // Recognize faces on that image. 29 | faces, err := rec.RecognizeFile(testImagePristin) 30 | if err != nil { 31 | log.Fatalf("Can't recognize: %v", err) 32 | } 33 | if len(faces) != 10 { 34 | log.Fatalf("Wrong number of faces") 35 | } 36 | 37 | // Fill known samples. In the real world you would use a lot of images 38 | // for each person to get better classification results but in our 39 | // example we just get them from one big image. 40 | var samples []face.Descriptor 41 | var cats []int32 42 | for i, f := range faces { 43 | samples = append(samples, f.Descriptor) 44 | // Each face is unique on that image so goes to its own category. 45 | cats = append(cats, int32(i)) 46 | } 47 | // Name the categories, i.e. people on the image. 48 | labels := []string{ 49 | "Sungyeon", "Yehana", "Roa", "Eunwoo", "Xiyeon", 50 | "Kyulkyung", "Nayoung", "Rena", "Kyla", "Yuha", 51 | } 52 | // Pass samples to the recognizer. 53 | rec.SetSamples(samples, cats) 54 | 55 | // Now let's try to classify some not yet known image. 56 | testImageNayoung := filepath.Join(dataDir, "images", "nayoung.jpg") 57 | nayoungFace, err := rec.RecognizeSingleFile(testImageNayoung) 58 | if err != nil { 59 | log.Fatalf("Can't recognize: %v", err) 60 | } 61 | if nayoungFace == nil { 62 | log.Fatalf("Not a single face on the image") 63 | } 64 | catID := rec.Classify(nayoungFace.Descriptor) 65 | if catID < 0 { 66 | log.Fatalf("Can't classify") 67 | } 68 | // Finally print the classified label. It should be "Nayoung". 69 | fmt.Println(labels[catID]) 70 | } 71 | -------------------------------------------------------------------------------- /face.go: -------------------------------------------------------------------------------- 1 | package face 2 | 3 | // #cgo CXXFLAGS: -std=c++1z -Wall -O3 -DNDEBUG -march=native 4 | // #cgo LDFLAGS: -ldlib -lblas -lcblas -llapack -ljpeg 5 | // #include 6 | // #include 7 | // #include "facerec.h" 8 | import "C" 9 | import ( 10 | "image" 11 | "io/ioutil" 12 | "math" 13 | "os" 14 | "unsafe" 15 | ) 16 | 17 | const ( 18 | rectLen = 4 19 | descrLen = 128 20 | shapeLen = 2 21 | 22 | // We get first 2^20 elements of array of shapes 23 | // (68 shapes per face in case of shape_predictor_68_face_landmarks.dat.bz2). 24 | // 68*shapeLen is bigger than rectLen and descrLen. 25 | maxElements = 1 << 20 26 | maxFaceLimit = maxElements / (68 * shapeLen) 27 | ) 28 | 29 | // A Recognizer creates face descriptors for provided images and 30 | // classifies them into categories. 31 | type Recognizer struct { 32 | ptr *C.facerec 33 | } 34 | 35 | // Face holds coordinates and descriptor of the human face. 36 | type Face struct { 37 | Rectangle image.Rectangle 38 | Descriptor Descriptor 39 | Shapes []image.Point 40 | } 41 | 42 | // Descriptor holds 128-dimensional feature vector. 43 | type Descriptor [128]float32 44 | 45 | func SquaredEuclideanDistance(d1 Descriptor, d2 Descriptor) (sum float64) { 46 | for i := range d1 { 47 | sum = sum + math.Pow(float64(d2[i]-d1[i]), 2) 48 | } 49 | 50 | return sum 51 | } 52 | 53 | // New creates new face with the provided parameters. 54 | func New(r image.Rectangle, d Descriptor) Face { 55 | return Face{r, d, []image.Point{}} 56 | } 57 | 58 | func NewWithShape(r image.Rectangle, s []image.Point, d Descriptor) Face { 59 | return Face{r, d, s} 60 | } 61 | 62 | // NewRecognizer returns a new recognizer interface. modelDir points to 63 | // directory with shape_predictor_5_face_landmarks.dat and 64 | // dlib_face_recognition_resnet_model_v1.dat files. 65 | func NewRecognizer(modelDir string) (rec *Recognizer, err error) { 66 | cModelDir := C.CString(modelDir) 67 | defer C.free(unsafe.Pointer(cModelDir)) 68 | ptr := C.facerec_init(cModelDir) 69 | 70 | if ptr.err_str != nil { 71 | defer C.facerec_free(ptr) 72 | defer C.free(unsafe.Pointer(ptr.err_str)) 73 | err = makeError(C.GoString(ptr.err_str), int(ptr.err_code)) 74 | return 75 | } 76 | 77 | rec = &Recognizer{ptr} 78 | return 79 | } 80 | 81 | func NewRecognizerWithConfig(modelDir string, size int, padding float32, jittering int) (rec *Recognizer, err error) { 82 | rec, err = NewRecognizer(modelDir) 83 | if err != nil { 84 | return 85 | } 86 | cSize := C.ulong(size) 87 | cPadding := C.double(padding) 88 | cJittering := C.int(jittering) 89 | C.facerec_config(rec.ptr, cSize, cPadding, cJittering) 90 | return 91 | } 92 | 93 | func (rec *Recognizer) recognize(type_ int, imgData []byte, maxFaces int) (faces []Face, err error) { 94 | if len(imgData) == 0 { 95 | err = ImageLoadError("Empty image") 96 | return 97 | } 98 | if maxFaces > maxFaceLimit { 99 | maxFaces = maxFaceLimit 100 | } 101 | cImgData := (*C.uint8_t)(&imgData[0]) 102 | cLen := C.int(len(imgData)) 103 | cMaxFaces := C.int(maxFaces) 104 | cType := C.int(type_) 105 | 106 | ret := C.facerec_recognize(rec.ptr, cImgData, cLen, cMaxFaces, cType) 107 | defer C.free(unsafe.Pointer(ret)) 108 | 109 | if ret.err_str != nil { 110 | defer C.free(unsafe.Pointer(ret.err_str)) 111 | err = makeError(C.GoString(ret.err_str), int(ret.err_code)) 112 | return 113 | } 114 | 115 | numFaces := int(ret.num_faces) 116 | if numFaces == 0 { 117 | return 118 | } 119 | numShapes := int(ret.num_shapes) 120 | 121 | // Copy faces data to Go structure. 122 | defer C.free(unsafe.Pointer(ret.shapes)) 123 | defer C.free(unsafe.Pointer(ret.rectangles)) 124 | defer C.free(unsafe.Pointer(ret.descriptors)) 125 | 126 | rDataLen := numFaces * rectLen 127 | rDataPtr := unsafe.Pointer(ret.rectangles) 128 | rData := (*[maxElements]C.long)(rDataPtr)[:rDataLen:rDataLen] 129 | 130 | dDataLen := numFaces * descrLen 131 | dDataPtr := unsafe.Pointer(ret.descriptors) 132 | dData := (*[maxElements]float32)(dDataPtr)[:dDataLen:dDataLen] 133 | 134 | sDataLen := numFaces * numShapes * shapeLen 135 | sDataPtr := unsafe.Pointer(ret.shapes) 136 | sData := (*[maxElements]C.long)(sDataPtr)[:sDataLen:sDataLen] 137 | 138 | for i := 0; i < numFaces; i++ { 139 | face := Face{} 140 | x0 := int(rData[i*rectLen]) 141 | y0 := int(rData[i*rectLen+1]) 142 | x1 := int(rData[i*rectLen+2]) 143 | y1 := int(rData[i*rectLen+3]) 144 | face.Rectangle = image.Rect(x0, y0, x1, y1) 145 | copy(face.Descriptor[:], dData[i*descrLen:(i+1)*descrLen]) 146 | for j := 0; j < numShapes; j++ { 147 | shapeX := int(sData[(i*numShapes+j)*shapeLen]) 148 | shapeY := int(sData[(i*numShapes+j)*shapeLen+1]) 149 | face.Shapes = append(face.Shapes, image.Point{shapeX, shapeY}) 150 | } 151 | faces = append(faces, face) 152 | } 153 | return 154 | } 155 | 156 | func (rec *Recognizer) recognizeFile(type_ int, imgPath string, maxFaces int) (face []Face, err error) { 157 | fd, err := os.Open(imgPath) 158 | if err != nil { 159 | return 160 | } 161 | defer fd.Close() 162 | imgData, err := ioutil.ReadAll(fd) 163 | if err != nil { 164 | return 165 | } 166 | return rec.recognize(type_, imgData, maxFaces) 167 | } 168 | 169 | // Recognize returns all faces found on the provided image, sorted from 170 | // left to right. Empty list is returned if there are no faces, error is 171 | // returned if there was some error while decoding/processing image. 172 | // Only JPEG format is currently supported. Thread-safe. 173 | func (rec *Recognizer) Recognize(imgData []byte) (faces []Face, err error) { 174 | return rec.recognize(0, imgData, 0) 175 | } 176 | 177 | func (rec *Recognizer) RecognizeCNN(imgData []byte) (faces []Face, err error) { 178 | return rec.recognize(1, imgData, 0) 179 | } 180 | 181 | // RecognizeSingle returns face if it's the only face on the image or 182 | // nil otherwise. Only JPEG format is currently supported. Thread-safe. 183 | func (rec *Recognizer) RecognizeSingle(imgData []byte) (face *Face, err error) { 184 | faces, err := rec.recognize(0, imgData, 1) 185 | if err != nil || len(faces) != 1 { 186 | return 187 | } 188 | face = &faces[0] 189 | return 190 | } 191 | 192 | func (rec *Recognizer) RecognizeSingleCNN(imgData []byte) (face *Face, err error) { 193 | faces, err := rec.recognize(1, imgData, 1) 194 | if err != nil || len(faces) != 1 { 195 | return 196 | } 197 | face = &faces[0] 198 | return 199 | } 200 | 201 | // Same as Recognize but accepts image path instead. 202 | func (rec *Recognizer) RecognizeFile(imgPath string) (faces []Face, err error) { 203 | return rec.recognizeFile(0, imgPath, 0) 204 | } 205 | 206 | func (rec *Recognizer) RecognizeFileCNN(imgPath string) (faces []Face, err error) { 207 | return rec.recognizeFile(1, imgPath, 0) 208 | } 209 | 210 | // Same as RecognizeSingle but accepts image path instead. 211 | func (rec *Recognizer) RecognizeSingleFile(imgPath string) (face *Face, err error) { 212 | faces, err := rec.recognizeFile(0, imgPath, 1) 213 | if err != nil || len(faces) != 1 { 214 | return 215 | } 216 | face = &faces[0] 217 | return 218 | } 219 | 220 | func (rec *Recognizer) RecognizeSingleFileCNN(imgPath string) (face *Face, err error) { 221 | faces, err := rec.recognizeFile(1, imgPath, 1) 222 | if err != nil || len(faces) != 1 { 223 | return 224 | } 225 | face = &faces[0] 226 | return 227 | } 228 | 229 | // SetSamples sets known descriptors so you can classify the new ones. 230 | // Thread-safe. 231 | func (rec *Recognizer) SetSamples(samples []Descriptor, cats []int32) { 232 | if len(samples) == 0 || len(samples) != len(cats) { 233 | return 234 | } 235 | cSamples := (*C.float)(unsafe.Pointer(&samples[0])) 236 | cCats := (*C.int32_t)(unsafe.Pointer(&cats[0])) 237 | cLen := C.int(len(samples)) 238 | C.facerec_set_samples(rec.ptr, cSamples, cCats, cLen) 239 | } 240 | 241 | // Classify returns class ID for the given descriptor. Negative index is 242 | // returned if no match. Thread-safe. 243 | func (rec *Recognizer) Classify(testSample Descriptor) int { 244 | cTestSample := (*C.float)(unsafe.Pointer(&testSample)) 245 | return int(C.facerec_classify(rec.ptr, cTestSample, -1)) 246 | } 247 | 248 | // Same as Classify but allows to specify max distance between faces to 249 | // consider it a match. Start with 0.6 if not sure. 250 | func (rec *Recognizer) ClassifyThreshold(testSample Descriptor, tolerance float32) int { 251 | cTestSample := (*C.float)(unsafe.Pointer(&testSample)) 252 | cTolerance := C.float(tolerance) 253 | return int(C.facerec_classify(rec.ptr, cTestSample, cTolerance)) 254 | } 255 | 256 | // Close frees resources taken by the Recognizer. Safe to call multiple 257 | // times. Don't use Recognizer after close call. 258 | func (rec *Recognizer) Close() { 259 | C.facerec_free(rec.ptr) 260 | rec.ptr = nil 261 | } 262 | -------------------------------------------------------------------------------- /face_test.go: -------------------------------------------------------------------------------- 1 | package face_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | "unsafe" 12 | 13 | "github.com/Kagami/go-face" 14 | ) 15 | 16 | var ( 17 | rec *face.Recognizer 18 | 19 | idolTests = map[string]string{ 20 | "elkie.jpg": "Elkie, CLC", 21 | "chaeyoung.jpg": "Chaeyoung, Twice", 22 | "chaeyoung2.jpg": "Chaeyoung, Twice", 23 | "sejeong.jpg": "Sejeong, Gugudan", 24 | "jimin.jpg": "Jimin, AOA", 25 | "jimin2.jpg": "Jimin, AOA", 26 | "jimin4.jpg": "Jimin, AOA", 27 | "meiqi.jpg": "Mei Qi, WJSN", 28 | "chaeyeon.jpg": "Chaeyeon, DIA", 29 | "chaeyeon3.jpg": "Chaeyeon, DIA", 30 | "tzuyu2.jpg": "Tzuyu, Twice", 31 | "nayoung.jpg": "Nayoung, PRISTIN", 32 | "luda2.jpg": "Luda, WJSN", 33 | "joy.jpg": "Joy, Red Velvet", 34 | } 35 | ) 36 | 37 | type Idol struct { 38 | ID string `json:"id"` 39 | Name string `json:"name"` 40 | BandName string `json:"band_name"` 41 | } 42 | 43 | type IdolFace struct { 44 | Descriptor string `json:"descriptor"` 45 | IdolID string `json:"idol_id"` 46 | } 47 | 48 | type IdolData struct { 49 | Idols []Idol `json:"idols"` 50 | Faces []IdolFace `json:"faces"` 51 | byID map[string]*Idol 52 | } 53 | 54 | type TrainData struct { 55 | samples []face.Descriptor 56 | cats []int32 57 | labels []string 58 | } 59 | 60 | func getTPath(fname string) string { 61 | return filepath.Join("testdata", "images", fname) 62 | } 63 | 64 | func getIdolData() (idata *IdolData, err error) { 65 | data, err := ioutil.ReadFile(filepath.Join("testdata", "idols.json")) 66 | if err != nil { 67 | return 68 | } 69 | idata = &IdolData{} 70 | err = json.Unmarshal(data, idata) 71 | if err != nil { 72 | return 73 | } 74 | idata.byID = make(map[string]*Idol) 75 | for i, _ := range idata.Idols { 76 | idol := &idata.Idols[i] 77 | idata.byID[idol.ID] = idol 78 | } 79 | return 80 | } 81 | 82 | func str2descr(s string) face.Descriptor { 83 | b, err := base64.StdEncoding.DecodeString(s) 84 | if err != nil { 85 | panic(err) 86 | } 87 | return *(*face.Descriptor)(unsafe.Pointer(&b[0])) 88 | } 89 | 90 | func getTrainData(idata *IdolData) (tdata *TrainData) { 91 | var samples []face.Descriptor 92 | var cats []int32 93 | var labels []string 94 | 95 | var catID int32 96 | var prevIdolID string 97 | catID = -1 98 | for i, _ := range idata.Faces { 99 | iface := &idata.Faces[i] 100 | descriptor := str2descr(iface.Descriptor) 101 | samples = append(samples, descriptor) 102 | if iface.IdolID != prevIdolID { 103 | catID++ 104 | labels = append(labels, iface.IdolID) 105 | } 106 | cats = append(cats, catID) 107 | prevIdolID = iface.IdolID 108 | } 109 | 110 | tdata = &TrainData{ 111 | samples: samples, 112 | cats: cats, 113 | labels: labels, 114 | } 115 | return 116 | } 117 | 118 | func recognizeAndClassify(fpath string, tolerance float32) (id int, err error) { 119 | id = -1 120 | f, err := rec.RecognizeSingleFile(fpath) 121 | if err != nil || f == nil { 122 | return 123 | } 124 | if tolerance < 0 { 125 | id = rec.Classify(f.Descriptor) 126 | } else { 127 | id = rec.ClassifyThreshold(f.Descriptor, tolerance) 128 | } 129 | return 130 | } 131 | 132 | func TestSerializationError(t *testing.T) { 133 | _, err := face.NewRecognizer("/notexist") 134 | switch err.(type) { 135 | case face.SerializationError: 136 | // skip 137 | default: 138 | t.Fatalf("Wrong error: %v", err) 139 | } 140 | } 141 | 142 | func TestInit(t *testing.T) { 143 | var err error 144 | rec, err = face.NewRecognizer(filepath.Join("testdata", "models")) 145 | if err != nil { 146 | t.Fatalf("Can't init face recognizer: %v", err) 147 | } 148 | } 149 | 150 | func TestImageLoadError(t *testing.T) { 151 | _, err := rec.Recognize([]byte{1, 2, 3}) 152 | switch err.(type) { 153 | case face.ImageLoadError: 154 | // skip 155 | default: 156 | t.Fatalf("Wrong error: %v", err) 157 | } 158 | } 159 | 160 | func TestNumFaces(t *testing.T) { 161 | faces, err := rec.RecognizeFile(getTPath("pristin.jpg")) 162 | if err != nil { 163 | t.Fatalf("Can't get faces: %v", err) 164 | } 165 | numFaces := len(faces) 166 | if numFaces != 10 { 167 | t.Fatalf("Wrong number of faces: %d", numFaces) 168 | } 169 | } 170 | 171 | func TestEmptyClassify(t *testing.T) { 172 | var sample face.Descriptor 173 | id := rec.Classify(sample) 174 | if id >= 0 { 175 | t.Fatalf("Shouldn't recognize but got %d category", id) 176 | } 177 | } 178 | 179 | func TestIdols(t *testing.T) { 180 | idata, err := getIdolData() 181 | if err != nil { 182 | t.Fatalf("Can't get idol data: %v", err) 183 | } 184 | tdata := getTrainData(idata) 185 | rec.SetSamples(tdata.samples, tdata.cats) 186 | 187 | for fname, expected := range idolTests { 188 | t.Run(fname, func(t *testing.T) { 189 | names := strings.Split(expected, ", ") 190 | expectedIname := names[0] 191 | expectedBname := names[1] 192 | 193 | catID, err := recognizeAndClassify(getTPath(fname), -1) 194 | if err != nil { 195 | t.Fatalf("Can't recognize: %v", err) 196 | } 197 | if catID < 0 { 198 | t.Errorf("%s: expected “%s” but not recognized", fname, expected) 199 | return 200 | } 201 | idolID := tdata.labels[catID] 202 | idol := idata.byID[idolID] 203 | actualIname := idol.Name 204 | actualBname := idol.BandName 205 | 206 | if expectedIname != actualIname || expectedBname != actualBname { 207 | actual := fmt.Sprintf("%s, %s", actualIname, actualBname) 208 | t.Errorf("%s: expected “%s” but got “%s”", fname, expected, actual) 209 | } 210 | }) 211 | } 212 | } 213 | 214 | func TestClassifyThreshold(t *testing.T) { 215 | id, err := recognizeAndClassify(getTPath("nana.jpg"), 0.1) 216 | if err != nil { 217 | t.Fatalf("Can't recognize: %v", err) 218 | } 219 | if id >= 0 { 220 | t.Fatalf("Shouldn't recognize but got %d category", id) 221 | } 222 | id, err = recognizeAndClassify(getTPath("nana.jpg"), 0.8) 223 | if err != nil { 224 | t.Fatalf("Can't recognize: %v", err) 225 | } 226 | if id < 0 { 227 | t.Fatalf("Should have recognized but got %d category", id) 228 | } 229 | } 230 | 231 | func TestClose(t *testing.T) { 232 | rec.Close() 233 | } 234 | -------------------------------------------------------------------------------- /facerec.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "facerec.h" 7 | #include "jpeg_mem_loader.h" 8 | #include "classify.h" 9 | 10 | using namespace dlib; 11 | 12 | template