手把手 Golang 实现静态图像与视频流人脸识别

说起人脸识别,大家首先想到的实现方式应该是 Python 去做相关的处理,因为相关的机器学习框架,库都已经封装得比较好了。但是我们今天讨论的实现方式换成 Golang,利用 Golang 去做静态图像和视频流人脸识别的相应处理。

 

静态图像人脸识别

首先我们来进行静态的人脸识别,Golang 这边相较于 Python 社区来说相对少一些,不过依然有一些优秀的库可以供我们使用。今天我们用到的就是 go-face 这个库。该库利用 dlib 去实现人脸识别,一个很受欢迎的机器学习工具集,它可以说是人脸识别中使用最多的软件包之一。在产学界有广泛应用,涵盖了机器人学,嵌入式设备,移动设备等等。在它官网的文档中提到在 Wild 基准测试中识别标记面部的准确度达到惊人的 99.4%,这也说明为什么它能得到广泛的应用。

 

在我们开始码代码之前,首先需要安装 dlib。Windows 平台相对麻烦一些,具体在官网有安装方案,这里我介绍两个平台。

Ubuntu 18.10+, Debian sid

最新版本的 Ubuntu 和 Debian 都提供合适的 dlib 包,所以只需要运行。

1
2
3
4
# Ubuntu
sudo apt-get install libdlib-dev libblas-dev liblapack-dev libjpeg-turbo8-dev
# Debian
sudo apt-get install libdlib-dev libblas-dev liblapack-dev libjpeg62-turbo-dev

  

macOS

确保安装了 Homebrew

1
brew install dlib

  

创建项目及准备工作

在 GOPATH 的 src 目录下,创建项目文件,命令如下。

1
2
3
sudo makedir go-face-test
# 创建 main.go
sudo touch main.go

  

然后进入该目录下,生成 mod 文件。

1
sudo go mod init

  

调用该命令后,在 go-face-test 目录下应该已经生成了 go.mod 文件。

 

该库需要三个模型 shape_predictor_5_face_landmarks.dat, mmod_human_face_detector.datdlib_face_recognition_resnet_model_v1.dat,在 go-face-test 目录下下载相应的测试数据。

1
git clone https://github.com/Kagami/go-face-testdata testdata

  

最终的项目结构应该如图。

img

 

代码实现

首先,我们利用代码检查环境是否正常。初始化识别器,释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
 
import (
    "fmt"
 
    "github.com/Kagami/go-face"
)
 
const dataDir = "testdata"
 
// testdata 目录下两个对应的文件夹目录
const (
    modelDir  = dataDir + "/models"
    imagesDir = dataDir + "/images"
)
 
func main() {
    fmt.Println("Face Recognition...")
 
    // 初始化识别器
    rec, err := face.NewRecognizer(modelDir)
    if err != nil {
        fmt.Println("Cannot INItialize recognizer")
    }
    defer rec.Close()
 
    fmt.Println("Recognizer Initialized")
}

  

编译然后运行代码。

1
sudo go run main.go

  

应该得到下面输出。

1
2
Face Recognition...
Recognizer Initialized

  

到这一步,我们已经成功的设置好了需要的一切。

 

检测图片中人脸数量

首先准备一张林俊杰的照片,放到任意目录下,为了演示方便,我放在了 main.go 同级目录下。

img

 

如你所见,现在什么都没有,只有一张图片,接下来我们要让计算机计算图片中的人脸数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
 
import (
    "fmt"
    "log"
 
    "github.com/Kagami/go-face"
)
 
const dataDir = "testdata"
 
// testdata 目录下两个对应的文件夹目录
const (
    modelDir  = dataDir + "/models"
    imagesDir = dataDir + "/images"
)
 
func main() {
    fmt.Println("Face Recognition...")
 
    // 初始化识别器
    rec, err := face.NewRecognizer(modelDir)
    if err != nil {
        fmt.Println("Cannot INItialize recognizer")
    }
    defer rec.Close()
 
    fmt.Println("Recognizer Initialized")
 
    // 调用该方法,传入路径。返回面部数量和任何错误
    faces, err := rec.RecognizeFile("linjunjie.jpeg")
    if err != nil {
        log.Fatalf("无法识别: %v", err)
    }
    // 打印人脸数量
    fmt.Println("图片人脸数量: ", len(faces))
}

  

核心代码其实就是一行,go-face 封装进行识别的方法,传入相应路径的图片文件,执行代码后结果如下。

1
2
3
Face Recognition...
Recognizer Initialized
图片人脸数量:  1

  

现在笨笨的计算机已经会数人脸数量了。那....如果一张照片里面有多人准不准呢,我们试试看,准备一张多人合照图片。

img

heyin.jpeg

 

我们将第 31 行代码换成如下即可。

1
faces, err := rec.RecognizeFile("heyin.jpeg")

  

运行后的结果应该打印 (图片人脸数量: 6),接下来正式看展我们的人脸识别。

 

人脸识别

首先我们准备一张合照,这里依然沿用上面的 heyin.jpeg

 

整个处理过程大致分为以下几步。

1.将合影中人物映射到唯一 ID, 然后将唯一 ID 和对应人物相关联。

1
2
3
4
5
6
7
8
9
10
var samples []face.Descriptor
    var peoples []int32
    for i, f := range faces {
        samples = append(samples, f.Descriptor)
        // 每张脸唯一 id
        peoples = append(peoples, int32(i))
    }
 
    // Pass samples to the recognizer.
    rec.SetSamples(samples, peoples)

  

2.接下来我们封装一个人脸识别的方法,传入识别器和照片路径,打印对应人物 ID,人物名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func RecognizePeople(rec *face.Recognizer, file string) {
    people, err := rec.RecognizeSingleFile(file)
    if err != nil {
        log.Fatalf("无法识别: %v", err)
    }
    if people == nil {
        log.Fatalf("图片上不是一张脸")
    }
    peopleID := rec.Classify(people.Descriptor)
    if peopleID < 0 {
        log.Fatalf("无法区分")
    }
    fmt.Println(peopleID)
    fmt.Println(labels[peopleID])
}

  

3.最后我们传入想要识别的图片,目前传入了 3 张图片,感兴趣的小伙伴可以传入其他图片尝试。

img

jay.jpeg

 

img

linjunjie.jpeg

 

img

taozhe.jpeg

4.调用三次。

1
2
3
RecognizePeople(rec, "jay.jpeg")
    RecognizePeople(rec, "linjunjie.jpeg")
    RecognizePeople(rec, "taozhe.jpeg")

  

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main
 
import (
    "fmt"
    "log"
 
    "github.com/Kagami/go-face"
)
 
const dataDir = "testdata"
 
// testdata 目录下两个对应的文件夹目录
const (
    modelDir  = dataDir + "/models"
    imagesDir = dataDir + "/images"
)
 
// 图片中的人名
var labels = []string{
    "萧敬腾",
    "周杰伦",
    "unknow",
    "王力宏",
    "陶喆",
    "林俊杰",
}
 
func main() {
    fmt.Println("Face Recognition...")
 
    // 初始化识别器
    rec, err := face.NewRecognizer(modelDir)
    if err != nil {
        fmt.Println("Cannot INItialize recognizer")
    }
    defer rec.Close()
 
    fmt.Println("Recognizer Initialized")
 
    // 调用该方法,传入路径。返回面部数量和任何错误
    faces, err := rec.RecognizeFile("heyin.jpeg")
    if err != nil {
        log.Fatalf("无法识别: %v", err)
    }
    // 打印人脸数量
    fmt.Println("图片人脸数量: ", len(faces))
 
    var samples []face.Descriptor
    var peoples []int32
    for i, f := range faces {
        samples = append(samples, f.Descriptor)
        // 每张脸唯一 id
        peoples = append(peoples, int32(i))
    }
 
    // 传入样例到识别器
    rec.SetSamples(samples, peoples)
 
    RecognizePeople(rec, "jay.jpeg")
    RecognizePeople(rec, "linjunjie.jpeg")
    RecognizePeople(rec, "taozhe.jpeg")
}
 
func RecognizePeople(rec *face.Recognizer, file string) {
    people, err := rec.RecognizeSingleFile(file)
    if err != nil {
        log.Fatalf("无法识别: %v", err)
    }
    if people == nil {
        log.Fatalf("图片上不是一张脸")
    }
    peopleID := rec.Classify(people.Descriptor)
    if peopleID < 0 {
        log.Fatalf("无法区分")
    }
    fmt.Println(peopleID)
    fmt.Println(labels[peopleID])
}

  

运行结果

最后我们运行代码。

1
2
go build main.go
./main

  

结果如下

1
2
3
4
5
6
7
图片人脸数量:  6
1
周杰伦
5
林俊杰
4
陶喆

  

恭喜你,你已经成功的识别出这三张图片是谁了,到这一步,静态的图像人脸识别已经完成了。

 

静态人脸识别总结

到这一步我们已经可以成功的利用 Go 实现了静态人脸识别。将其运用到项目中也不是不可,不过它有诸多局限,使用的场景较为单一,只能用在例如用户上传人脸身份识别,单一人脸识别等场景;图片格式较为单一,暂时不支持 PNG 格式等缺点。

 

视频流人脸识别

背景

静态的人脸识别应用场景较为局限,不能够放到比较重要的环境中,例如金融,保险,安防等领域,存在伪造等可能。而且单纯的静态人脸识别,意义不大。动态的视频流拥有更加广阔的应用空间,充分应用在智能安防,手势识别,美颜等领域。5G 时代,众多业务将围绕视频这一块展开,如何将视频业务与核心业务实现解耦,声网的 RTE 组件做得不错,作为 RTE-PaaS 的开创者,声网已经有较多的技术积累,通过 RTE 组件的形式有很多好处。

 

RTE 优点

1.应用无关性

可以在不同的项目间共享,实现复用,避免多次开发的重复性工作

2.平台无关性

广泛应用于操作系统,编程语言及各领域

3.丰富的三方模块

能够提供例如白板教学,视频美颜,鉴黄等众多模块供开发者使用

 

代码实现

这里我们来实现一下视频流的相关人脸识别,之前的静态识别就是为了动态视频流人脸识别做铺垫。我们来说一下视频流的人脸识别的实现思路,静态的图像人脸识别已经完成,而视频是多帧的连续,我们只需要抽取片段捕获关键帧,识别出人像,人后输出对应关联的人名。

 

准备工作

这里我们用到的是 gocv(底层使用 OpenCV),这里我们暂时略过具体的安装流程,按照官方文档安装即可。

 

1.设置视频捕捉的设备,一般来说默认 0

1
2
3
4
5
6
7
8
9
10
// set to use a video capture device 0
    deviceID := 0
 
    // open webcam
    webcam, err := gocv.OpenVideoCapture(deviceID)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer webcam.Close()

  

2.打开展示窗口

1
2
3
// open display window
    window := gocv.NewWindow("Face Detect")
    defer window.Close()

  

3.准备图像矩阵,检测到人脸时显示矩形框的配置

1
2
3
4
5
6
// prepare image matrix
    img := gocv.NewMat()
    defer img.Close()
 
    // color for the rect when faces detected
    blue := color.RGBA{0, 0, 255, 0}

  

4.加载人脸识别分类器,用一个死循环,里面加上我们的相关识别服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
for {
        if ok := webcam.Read(&img); !ok {
            fmt.Printf("cannot read device %v\n", deviceID)
            return
        }
        if img.Empty() {
            continue
        }
 
        // detect faces
        rects := classifier.DetectMultiScale(img)
        fmt.Printf("found %d faces\n", len(rects))
 
        // draw a rectangle around each face on the original image
        for _, r := range rects {
            gocv.Rectangle(&img, r, blue, 3)
      imgFace := img.Region(r)
            buff, err:=gocv.IMEncode(".jpg",imgFace)
            if err != nil {
                fmt.Println("encoding to jpg err:%v", err)
                break
            }
 
            RecognizePeopleFromMemory(rec, buff)
        }
 
        // show the image in the window, and wait 1 millisecond
        window.IMShow(img)
        window.WaitKey(1)
    }

  

其中有几个步骤需要将一下,目前来说 gocv.IMEncode 只支持将捕获到的图片转成 PNGJPGGIF 三种格式。转换后的字节流放在内存中,然后将字节流传入我们的人脸识别函数中即可。

1
2
3
4
5
6
7
8
9
10
// RecognizeSingle returns face if it's the only face on the image or
// nil otherwise. Only JPEG format is currently supported. Thread-safe.
func (rec *Recognizer) RecognizeSingle(imgData []byte) (face *Face, err error) {
    faces, err := rec.recognize(0, imgData, 1)
    if err != nil || len(faces) != 1 {
        return
    }
    face = &faces[0]
    return
}

  

注意事项

由于 go-face 只支持 JPEG 的格式,所以我们捕捉的帧只能转换成 JPG 格式

然后简单的封装一个字符流的识别函数。这里需要说明一下,之所以将 log.Fatal 换成了 log.Println 的原因是在视频流级别的识别中可能会出现没有人脸的情况,这个时候程序应当是正常运行的,不能退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func RecognizePeopleFromMemory(rec *face.Recognizer, img []byte) {
    people, err := rec.RecognizeSingle(img)
    if err != nil {
        log.Println("无法识别: %v", err)
        return
    }
    if people == nil {
        log.Println("图片上不是一张脸")
        return
    }
    peopleID := rec.Classify(people.Descriptor)
    if peopleID < 0 {
        log.Println("无法区分")
        return
    }
    fmt.Println(peopleID)
    fmt.Println(labels[peopleID])
}

  

最后完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package main
 
import (
    "fmt"
    "image/color"
    "log"
 
    "github.com/Kagami/go-face"
    "gocv.io/x/gocv"
)
 
const dataDir = "testdata"
 
// testdata 目录下两个对应的文件夹目录
const (
    modelDir  = dataDir + "/models"
    imagesDir = dataDir + "/images"
)
 
// 图片中的人名
var labels = []string{
    "萧敬腾",
    "周杰伦",
    "unknow",
    "王力宏",
    "陶喆",
    "林俊杰",
}
 
func main() {
    // 初始化识别器
    rec, err := face.NewRecognizer(modelDir)
    if err != nil {
        fmt.Println("Cannot INItialize recognizer")
    }
    defer rec.Close()
 
    fmt.Println("Recognizer Initialized")
 
    // 调用该方法,传入路径。返回面部数量和任何错误
    faces, err := rec.RecognizeFile("heyin.jpeg")
    if err != nil {
        log.Fatalf("无法识别: %v", err)
    }
    // 打印人脸数量
    fmt.Println("图片人脸数量: ", len(faces))
 
    var samples []face.Descriptor
    var peoples []int32
    for i, f := range faces {
        samples = append(samples, f.Descriptor)
        // 每张脸唯一 id
        peoples = append(peoples, int32(i))
    }
 
    // Pass samples to the recognizer.
    rec.SetSamples(samples, peoples)
 
    RecognizePeople(rec, "jay.jpeg")
    RecognizePeople(rec, "linjunjie.jpeg")
    RecognizePeople(rec, "taozhe.jpeg")
 
    // set to use a video capture device 0
    deviceID := 0
 
    // open webcam
    webcam, err := gocv.OpenVideoCapture(deviceID)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer webcam.Close()
 
    // open display window
    window := gocv.NewWindow("Face Detect")
    defer window.Close()
 
    // prepare image matrix
    img := gocv.NewMat()
    defer img.Close()
 
    // color for the rect when faces detected
    blue := color.RGBA{0, 0, 255, 0}
 
    // load classifier to recognize faces
    classifier := gocv.NewCascadeClassifier()
    defer classifier.Close()
 
    if !classifier.Load("./haarcascade_frontalface_default.xml") {
        fmt.Println("Error reading cascade file: data/haarcascade_frontalface_default.xml")
        return
    }
 
    fmt.Printf("start reading camera device: %v\n", deviceID)
    for {
        if ok := webcam.Read(&img); !ok {
            fmt.Printf("cannot read device %v\n", deviceID)
            return
        }
        if img.Empty() {
            continue
        }
 
        // detect faces
        rects := classifier.DetectMultiScale(img)
        if len(rects) == 0 {
            continue
        }
 
        fmt.Printf("found %d faces\n", len(rects))
 
        // draw a rectangle around each face on the original image
        for _, r := range rects {
            gocv.Rectangle(&img, r, blue, 3)
 
            imgFace := img.Region(r)
            buff, err:=gocv.IMEncode(".jpg",imgFace)
            if err != nil {
                fmt.Println("encoding to jpg err:%v", err)
                break
            }
 
            RecognizePeopleFromMemory(rec, buff)
        }
 
        // show the image in the window, and wait 1 millisecond
        window.IMShow(img)
        window.WaitKey(1)
    }
}
 
func RecognizePeople(rec *face.Recognizer, file string) {
    people, err := rec.RecognizeSingleFile(file)
    if err != nil {
        log.Fatalf("无法识别: %v", err)
    }
    if people == nil {
        log.Fatalf("图片上不是一张脸")
    }
    peopleID := rec.Classify(people.Descriptor)
    if peopleID < 0 {
        log.Fatalf("无法区分")
    }
    fmt.Println(peopleID)
    fmt.Println(labels[peopleID])
}
 
func RecognizePeopleFromMemory(rec *face.Recognizer, img []byte) {
    people, err := rec.RecognizeSingle(img)
    if err != nil {
        log.Println("无法识别: %v", err)
        return
    }
    if people == nil {
        log.Println("图片上不是一张脸")
        return
    }
    peopleID := rec.Classify(people.Descriptor)
    if peopleID < 0 {
        log.Println("无法区分")
        return
    }
    fmt.Println(peopleID)
    fmt.Println(labels[peopleID])
}

  

接下来我们运行代码,应该能够拉起摄像头,这个时候我手持林俊杰的照片进行识别,我们可以看到左下角已经输出对应的人名了。

img

 

视频流人脸识别总结

到这一步,恭喜你,你已经能够完成视频流人脸识别了。但是,这里要说明一下,为了快速的实现,我们的样本集是比较少的,识别成功率相对来说比较低。不过一个简单的动态人脸识别已经搭好了。

总结

虽然我们实现了动态的人脸识别,但是在更为复杂的应用场景下难以实现相应的需求,而且存在图片格式等限制,缺乏人脸处理的其他模块,美颜,鉴黄等功能。不过通过第三方的 SDK,例如声网等平台去实现对应的需求,园区的人脸识别,视频会议,云课堂等场景,能够实现快速搭建,能够几行代码就能够完成相应的接入,并围绕 RTE 等组件进行人脸识别的相关开发。为开发节约大量时间和成本,可以将开发重心转移到更加核心的业务。

 

posted @   声网  阅读(853)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示