Skip to content

Commit 4e245c8

Browse files
committed
feat: added yoloface
1 parent 5d6050f commit 4e245c8

File tree

8 files changed

+328
-0
lines changed

8 files changed

+328
-0
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/dev6699/face
33
go 1.22.4
44

55
require (
6+
gocv.io/x/gocv v0.37.0
67
google.golang.org/grpc v1.64.0
78
google.golang.org/protobuf v1.34.2
89
)

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
22
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
gocv.io/x/gocv v0.37.0 h1:sISHvnApErjoJodz1Dxb8UAkFdITOB3vXGslbVu6Knk=
4+
gocv.io/x/gocv v0.37.0/go.mod h1:lmS802zoQmnNvXETpmGriBqWrENPei2GxYx5KUxJsMA=
35
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
46
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
57
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=

model/yoloface/README.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## Yoloface with face landmark5 detection
2+
3+
<img src="output.jpg">
4+
5+
---
6+
Model description
7+
[Get model](https://github.com/facefusion/facefusion-assets/releases/download/models/yoloface_8n.onnx)
8+
```
9+
{
10+
"name": "yoloface",
11+
"versions": [
12+
"1"
13+
],
14+
"platform": "onnxruntime_onnx",
15+
"inputs": [
16+
{
17+
"name": "images",
18+
"datatype": "FP32",
19+
"shape": [
20+
1,
21+
3,
22+
640,
23+
640
24+
]
25+
}
26+
],
27+
"outputs": [
28+
{
29+
"name": "output0",
30+
"datatype": "FP32",
31+
"shape": [
32+
1,
33+
20,
34+
8400
35+
]
36+
}
37+
]
38+
}
39+
```
40+
41+

model/yoloface/output.jpg

87 KB
Loading

model/yoloface/post.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package yoloface
2+
3+
import (
4+
"math"
5+
"sort"
6+
7+
"github.com/dev6699/face/model"
8+
"gocv.io/x/gocv"
9+
)
10+
11+
type Detection struct {
12+
BoundingBox model.BoundingBox
13+
FaceLandmark5 []gocv.Point2f
14+
Confidence float32
15+
}
16+
17+
func (m *Model) PostProcess(rawOutputContents [][]byte) (*Output, error) {
18+
// outputs": [
19+
// {
20+
// "name": "output0",
21+
// "datatype": "FP32",
22+
// "shape": [
23+
// 1,
24+
// 20,
25+
// 8400
26+
// ]
27+
// }
28+
// ]
29+
outputCount := 8400
30+
rawDetections, err := model.BytesToFloat32Slice(rawOutputContents[0])
31+
if err != nil {
32+
return nil, err
33+
}
34+
ratioWidth := m.ratioWidth
35+
ratioHeight := m.ratioHeight
36+
37+
var detections []Detection
38+
39+
boundingBoxRaw := rawDetections[:4*outputCount]
40+
scoreRaw := rawDetections[4*outputCount : 5*outputCount]
41+
faceLandmark5Raw := rawDetections[5*outputCount:]
42+
43+
for i := 0; i < outputCount; i++ {
44+
score := scoreRaw[i]
45+
if score < m.faceDetectorScore {
46+
continue
47+
}
48+
49+
d := Detection{
50+
Confidence: score,
51+
}
52+
53+
bboxRaw := []float32{
54+
boundingBoxRaw[i],
55+
boundingBoxRaw[i+outputCount],
56+
boundingBoxRaw[i+outputCount*2],
57+
boundingBoxRaw[i+outputCount*3],
58+
}
59+
60+
d.BoundingBox = model.BoundingBox{
61+
X1: float64(bboxRaw[0]-bboxRaw[2]/2) * float64(ratioWidth),
62+
Y1: float64(bboxRaw[1]-bboxRaw[3]/2) * float64(ratioHeight),
63+
X2: float64(bboxRaw[0]+bboxRaw[2]/2) * float64(ratioWidth),
64+
Y2: float64(bboxRaw[1]+bboxRaw[3]/2) * float64(ratioHeight),
65+
}
66+
67+
faceLandmark5Extract := []float32{}
68+
for j := 0; j < 15; j++ {
69+
if (j-2)%3 == 0 {
70+
continue
71+
}
72+
73+
idx := j*outputCount + i
74+
fl := faceLandmark5Raw[idx]
75+
if j%3 == 0 {
76+
fl *= ratioWidth
77+
}
78+
if (j-1)%3 == 0 {
79+
fl *= ratioHeight
80+
}
81+
82+
faceLandmark5Extract = append(faceLandmark5Extract, fl)
83+
}
84+
85+
faceLandmark5 := []gocv.Point2f{}
86+
for j := 0; j < len(faceLandmark5Extract); j += 2 {
87+
faceLandmark5 = append(faceLandmark5,
88+
gocv.Point2f{
89+
X: faceLandmark5Extract[j],
90+
Y: faceLandmark5Extract[j+1],
91+
})
92+
}
93+
d.FaceLandmark5 = faceLandmark5
94+
detections = append(detections, d)
95+
}
96+
97+
keepIndices := applyNMS(detections, m.iouThreshold)
98+
keepDetections := make([]Detection, len(keepIndices))
99+
for i, idx := range keepIndices {
100+
keepDetections[i] = detections[idx]
101+
}
102+
103+
sort.Slice(keepDetections, func(i, j int) bool {
104+
return keepDetections[i].Confidence > keepDetections[j].Confidence
105+
})
106+
107+
return &Output{
108+
Detections: keepDetections,
109+
}, nil
110+
}
111+
112+
// applyNMS performs non-maximum suppression to eliminate duplicate detections.
113+
func applyNMS(detections []Detection, iouThreshold float64) []int {
114+
boundingBoxList := []model.BoundingBox{}
115+
for _, d := range detections {
116+
boundingBoxList = append(boundingBoxList, d.BoundingBox)
117+
}
118+
119+
var keepIndices []int
120+
indices := make([]int, len(boundingBoxList))
121+
for i := range boundingBoxList {
122+
indices[i] = i
123+
}
124+
125+
areas := make([]float64, len(boundingBoxList))
126+
for i, box := range boundingBoxList {
127+
areas[i] = (box.X2 - box.X1 + 1) * (box.Y2 - box.Y1 + 1)
128+
}
129+
130+
for len(indices) > 0 {
131+
index := indices[0]
132+
keepIndices = append(keepIndices, index)
133+
var remainIndices []int
134+
135+
for _, i := range indices[1:] {
136+
xx1 := math.Max(boundingBoxList[index].X1, boundingBoxList[i].X1)
137+
yy1 := math.Max(boundingBoxList[index].Y1, boundingBoxList[i].Y1)
138+
xx2 := math.Min(boundingBoxList[index].X2, boundingBoxList[i].X2)
139+
yy2 := math.Min(boundingBoxList[index].Y2, boundingBoxList[i].Y2)
140+
141+
width := math.Max(0, xx2-xx1+1)
142+
height := math.Max(0, yy2-yy1+1)
143+
intersection := width * height
144+
union := areas[index] + areas[i] - intersection
145+
iou := intersection / union
146+
147+
if iou <= iouThreshold {
148+
remainIndices = append(remainIndices, i)
149+
}
150+
}
151+
indices = remainIndices
152+
}
153+
154+
return keepIndices
155+
}

model/yoloface/pre.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package yoloface
2+
3+
import (
4+
"image"
5+
"math"
6+
7+
"github.com/dev6699/face/protobuf"
8+
"gocv.io/x/gocv"
9+
)
10+
11+
func (m *Model) PreProcess(i *Input) ([]*protobuf.InferTensorContents, error) {
12+
img := i.Img
13+
width := img.Cols()
14+
height := img.Rows()
15+
16+
faceDetectorSize := Resolution{Width: 640, Height: 640}
17+
resizedVisionFrame, newWidth, newHeight := resizeFrameResolution(img.Clone(), faceDetectorSize)
18+
defer resizedVisionFrame.Close()
19+
20+
ratioHeight := float32(height) / float32(newHeight)
21+
ratioWidth := float32(width) / float32(newWidth)
22+
m.ratioHeight = ratioHeight
23+
m.ratioWidth = ratioWidth
24+
25+
contents := &protobuf.InferTensorContents{
26+
Fp32Contents: prepareDetectFrame(resizedVisionFrame, faceDetectorSize),
27+
}
28+
return []*protobuf.InferTensorContents{contents}, nil
29+
}
30+
31+
type Resolution struct {
32+
Width uint
33+
Height uint
34+
}
35+
36+
// resizeFrameResolution resize visionFrame where its resolution will be capped at maxResolution.
37+
func resizeFrameResolution(visionFrame gocv.Mat, maxResolution Resolution) (gocv.Mat, uint, uint) {
38+
width := visionFrame.Cols()
39+
height := visionFrame.Rows()
40+
41+
maxHeight := int(maxResolution.Height)
42+
maxWidth := int(maxResolution.Width)
43+
44+
if height > maxHeight || width > maxWidth {
45+
scale := math.Min(float64(maxHeight)/float64(height), float64(maxWidth)/float64(width))
46+
newWidth := int(float64(width) * scale)
47+
newHeight := int(float64(height) * scale)
48+
49+
gocv.Resize(visionFrame, &visionFrame, image.Point{X: newWidth, Y: newHeight}, 0, 0, gocv.InterpolationDefault)
50+
return visionFrame, uint(newWidth), uint(newHeight)
51+
}
52+
53+
return visionFrame, uint(width), uint(height)
54+
}
55+
56+
func prepareDetectFrame(visionFrame gocv.Mat, faceDetectorSize Resolution) []float32 {
57+
faceDetectorWidth := int(faceDetectorSize.Width)
58+
faceDetectorHeight := int(faceDetectorSize.Height)
59+
60+
detectVisionFrame := gocv.NewMatWithSize(faceDetectorHeight, faceDetectorWidth, gocv.MatTypeCV8UC3)
61+
defer detectVisionFrame.Close()
62+
63+
roi := detectVisionFrame.Region(image.Rect(0, 0, visionFrame.Cols(), visionFrame.Rows()))
64+
defer roi.Close()
65+
visionFrame.CopyTo(&roi)
66+
67+
output := make([]float32, 3*faceDetectorHeight*faceDetectorWidth)
68+
idx := 0
69+
for y := 0; y < faceDetectorHeight; y++ {
70+
for x := 0; x < faceDetectorWidth; x++ {
71+
pixel := detectVisionFrame.GetVecbAt(y, x)
72+
73+
output[idx] = (float32(pixel[0]) - 127.5) / 128.0
74+
output[faceDetectorHeight*faceDetectorWidth+idx] = (float32(pixel[1]) - 127.5) / 128.0
75+
output[2*faceDetectorHeight*faceDetectorWidth+idx] = (float32(pixel[2]) - 127.5) / 128.0
76+
idx++
77+
}
78+
}
79+
80+
return output
81+
}

model/yoloface/yoloface.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package yoloface
2+
3+
import (
4+
"github.com/dev6699/face/model"
5+
"gocv.io/x/gocv"
6+
)
7+
8+
type Model struct {
9+
faceDetectorScore float32
10+
iouThreshold float64
11+
ratioHeight float32
12+
ratioWidth float32
13+
}
14+
15+
type Input struct {
16+
Img gocv.Mat
17+
}
18+
19+
type Output struct {
20+
Detections []Detection
21+
}
22+
23+
type ModelT = model.Model[*Input, *Output]
24+
25+
var _ ModelT = &Model{}
26+
27+
func NewFactory(faceDetectorScore float32, iouThreshold float64) func() ModelT {
28+
return func() ModelT {
29+
return New(faceDetectorScore, iouThreshold)
30+
}
31+
}
32+
33+
func New(faceDetectorScore float32, iouThreshold float64) *Model {
34+
return &Model{
35+
faceDetectorScore: faceDetectorScore,
36+
iouThreshold: iouThreshold,
37+
}
38+
}
39+
40+
func (m *Model) ModelName() string {
41+
return "yoloface"
42+
}
43+
44+
func (m *Model) ModelVersion() string {
45+
return "1"
46+
}
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name: "yoloface"
2+
platform: "onnxruntime_onnx"

0 commit comments

Comments
 (0)