来源: https://codeantenna.com/a/jRxHFeO0F9
# 手把手 Golang 实现静态图像与视频流人脸识别
[实时音视频互动应用开发教程](https://codeantenna.com/tag/%E5%AE%9E%E6%97%B6%E9%9F%B3%E8%A7%86%E9%A2%91%E4%BA%92%E5%8A%A8%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8B "【实时音视频互动应用开发教程】标签搜索")[技术干货](https://codeantenna.com/tag/%E6%8A%80%E6%9C%AF%E5%B9%B2%E8%B4%A7 "【技术干货】标签搜索")[音视频](https://codeantenna.com/tag/%E9%9F%B3%E8%A7%86%E9%A2%91 "【音视频】标签搜索")[人脸识别](https://codeantenna.com/tag/%E4%BA%BA%E8%84%B8%E8%AF%86%E5%88%AB "【人脸识别】标签搜索")
* * *
说起人脸识别,大家首先想到的实现方式应该是 Python 去做相关的处理,因为相关的机器学习框架,库都已经封装得比较好了。但是我们今天讨论的实现方式换成 Golang,利用 Golang 去做静态图像和视频流人脸识别的相应处理。
# 静态图像人脸识别
首先我们来进行静态的人脸识别,Golang 这边相较于 Python 社区来说相对少一些,不过依然有一些优秀的库可以供我们使用。今天我们用到的就是[go-face](https://github.com/Kagami/go-face)这个库。该库利用[dlib](http://dlib.net/)去实现人脸识别,一个很受欢迎的机器学习工具集,它可以说是人脸识别中使用最多的软件包之一。在产学界有广泛应用,涵盖了机器人学,嵌入式设备,移动设备等等。在它官网的文档中提到在 Wild 基准测试中识别标记面部的准确度达到惊人的 99.4%,这也说明为什么它能得到广泛的应用。
在我们开始码代码之前,首先需要安装 dlib。Windows 平台相对麻烦一些,具体在官网有安装方案,这里我介绍两个平台。
### Ubuntu 18.10+, Debian sid
最新版本的 Ubuntu 和 Debian 都提供合适的 dlib 包,所以只需要运行。
~~~
# 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](https://brew.sh/)。
~~~
brew install dlib
~~~
## 创建项目及准备工作
在 GOPATH 的 src 目录下,创建项目文件,命令如下。
~~~
sudo makedir go-face-test
# 创建 main.go
sudo touch main.go
~~~
然后进入该目录下,生成 mod 文件。
~~~
sudo go mod init
~~~
调用该命令后,在 go-face-test 目录下应该已经生成了**go.mod**文件。
该库需要三个模型**shape\_predictor\_5\_face\_landmarks.dat**,**mmod\_human\_face\_detector.dat**和**dlib\_face\_recognition\_resnet\_model\_v1.dat**,在 go-face-test 目录下下载相应的测试数据。
~~~
git clone https://github.com/Kagami/go-face-testdata testdata
~~~
最终的项目结构应该如图。
![img](https://codeantenna.com/image/https://img-blog.csdnimg.cn/img_convert/d6cb565d63c8dedc183f3aec0ff65ca8.png)
## 代码实现
首先,我们利用代码检查环境是否正常。初始化识别器,释放资源。
~~~
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")
}
~~~
编译然后运行代码。
~~~
sudo go run main.go
~~~
应该得到下面输出。
~~~
Face Recognition...
Recognizer Initialized
~~~
到这一步,我们已经成功的设置好了需要的一切。
## 检测图片中人脸数量
首先准备一张林俊杰的照片,放到任意目录下,为了演示方便,我放在了**main.go**同级目录下。
![img](https://codeantenna.com/image/https://img-blog.csdnimg.cn/img_convert/a8d39c1387669f2c39f370fb36465c21.png)
如你所见,现在什么都没有,只有一张图片,接下来我们要让计算机计算图片中的人脸数量。
~~~
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 封装进行识别的方法,传入相应路径的图片文件,执行代码后结果如下。
~~~
Face Recognition...
Recognizer Initialized
图片人脸数量: 1
~~~
现在笨笨的计算机已经会数人脸数量了。那…如果一张照片里面有多人准不准呢,我们试试看,准备一张多人合照图片。
![img](https://codeantenna.com/image/https://img-blog.csdnimg.cn/img_convert/41a01981bc1a3e4bde40c67d7d733e4e.png)
heyin.jpeg
我们将第 31 行代码换成如下即可。
~~~
faces, err := rec.RecognizeFile("heyin.jpeg")
~~~
运行后的结果应该打印 (**图片人脸数量: 6**),接下来正式看展我们的人脸识别。
## 人脸识别
首先我们准备一张合照,这里依然沿用上面的**heyin.jpeg**。
整个处理过程大致分为以下几步。
1.将合影中人物映射到唯一 ID, 然后将唯一 ID 和对应人物相关联。
~~~
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,人物名字。
~~~
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](https://codeantenna.com/image/https://img-blog.csdnimg.cn/img_convert/1594691a7c1390c1bc7ba601f0819ee6.png)
jay.jpeg
![img](https://codeantenna.com/image/https://img-blog.csdnimg.cn/img_convert/a8d39c1387669f2c39f370fb36465c21.png)
linjunjie.jpeg
![img](https://codeantenna.com/image/https://img-blog.csdnimg.cn/img_convert/912ba5a73c9e61cab142af45cc2db0b4.png)
taozhe.jpeg
4.调用三次。
~~~
RecognizePeople(rec, "jay.jpeg")
RecognizePeople(rec, "linjunjie.jpeg")
RecognizePeople(rec, "taozhe.jpeg")
~~~
代码如下
~~~
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])
}
~~~
## 运行结果
最后我们运行代码。
~~~
go build main.go
./main
~~~
结果如下
~~~
图片人脸数量: 6
1
周杰伦
5
林俊杰
4
陶喆
~~~
恭喜你,你已经成功的识别出这三张图片是谁了,到这一步,静态的图像人脸识别已经完成了。
## 静态人脸识别总结
到这一步我们已经可以成功的利用 Go 实现了静态人脸识别。将其运用到项目中也不是不可,不过它有诸多局限,使用的场景较为单一,只能用在例如用户上传人脸身份识别,单一人脸识别等场景;图片格式较为单一,暂时不支持 PNG 格式等缺点。
# 视频流人脸识别
## 背景
静态的人脸识别应用场景较为局限,不能够放到比较重要的环境中,例如金融,保险,安防等领域,存在伪造等可能。而且单纯的静态人脸识别,意义不大。动态的视频流拥有更加广阔的应用空间,充分应用在智能安防,手势识别,美颜等领域。5G 时代,众多业务将围绕视频这一块展开,如何将视频业务与核心业务实现解耦,声网的**RTE**组件做得不错,作为 RTE-PaaS 的开创者,声网已经有较多的技术积累,通过 RTE 组件的形式有很多好处。
**RTE 优点**
1.应用无关性
可以在不同的项目间共享,实现复用,避免多次开发的重复性工作
2.平台无关性
广泛应用于操作系统,编程语言及各领域
3.丰富的三方模块
能够提供例如白板教学,视频美颜,鉴黄等众多模块供开发者使用
## 代码实现
这里我们来实现一下视频流的相关人脸识别,之前的静态识别就是为了动态视频流人脸识别做铺垫。我们来说一下视频流的人脸识别的实现思路,静态的图像人脸识别已经完成,而视频是多帧的连续,我们只需要抽取片段捕获关键帧,识别出人像,人后输出对应关联的人名。
### 准备工作
这里我们用到的是[gocv](https://gocv.io/getting-started/macos/)(底层使用 OpenCV),这里我们暂时略过具体的安装流程,按照官方文档安装即可。
1.设置视频捕捉的设备,一般来说默认 0
~~~
// 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.打开展示窗口
~~~
// open display window
window := gocv.NewWindow("Face Detect")
defer window.Close()
~~~
3.准备图像矩阵,检测到人脸时显示矩形框的配置
~~~
// prepare image matrix
img := gocv.NewMat()
defer img.Close()
// color for the rect when faces detected
blue := color.RGBA{0, 0, 255, 0}
~~~
4.加载人脸识别分类器,用一个死循环,里面加上我们的相关识别服务
~~~
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**只支持将捕获到的图片转成**PNG**,**JPG**,**GIF**三种格式。转换后的字节流放在内存中,然后将字节流传入我们的人脸识别函数中即可。
~~~
// 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 的原因是在视频流级别的识别中可能会出现没有人脸的情况,这个时候程序应当是正常运行的,不能退出。
~~~
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])
}
~~~
最后完整代码如下
~~~
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](https://codeantenna.com/image/https://img-blog.csdnimg.cn/img_convert/6437c45ce91fb97f1140a03f37724233.png)
## 视频流人脸识别总结
到这一步,恭喜你,你已经能够完成视频流人脸识别了。但是,这里要说明一下,为了快速的实现,我们的样本集是比较少的,识别成功率相对来说比较低。不过一个简单的动态人脸识别已经搭好了。
# 总结
虽然我们实现了动态的人脸识别,但是在更为复杂的应用场景下难以实现相应的需求,而且存在图片格式等限制,缺乏人脸处理的其他模块,美颜,鉴黄等功能。不过通过第三方的 SDK,例如声网等平台去实现对应的需求,园区的人脸识别,视频会议,云课堂等场景,能够实现快速搭建,能够几行代码就能够完成相应的接入,并围绕 RTE 等组件进行人脸识别的相关开发。为开发节约大量时间和成本,可以将开发重心转移到更加核心的业务。
版权声明:本文为CSDN博主「agora\_cloud」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:[https://blog.csdn.net/agora\_cloud/article/details/119522891](https://blog.csdn.net/agora_cloud/article/details/119522891)
- Golang
- Beego框架
- Gin框架
- gin框架介绍
- 使用Gin web框架的知名开源线上项目
- go-admin-gin
- air 热启动
- 完整的form表单参数验证语法
- Go 语言入门练手项目推荐
- Golang是基于多线程模型
- golang 一些概念
- Golang程序开发注意事项
- fatal error: all goroutines are asleep - deadlock
- defer
- Golang 的内建调试器
- go部署
- golang指针重要性
- 包(golang)
- Golang框架选型比较: goframe, beego, iris和gin
- GoFrame
- golang-admin-项目
- go module的使用方法及原理
- go-admin支持多框架的后台系统(go-admin.cn)
- docker gocv
- go-fac
- MSYS2
- 企业开发框架系统推荐
- gorm
- go-zero
- 优秀系统
- GinSkeleton(gin web 及gin 知识)
- 一次 request -> response 的生命周期概述
- 路由与路由组以及gin源码学习
- 中间件以及gin源码学习
- golang项目部署
- 独立部署golang
- 代理部署golang
- 容器部署golang
- golang交叉编译
- goravel
- kardianos+gin 项目作为windows服务运行
- go env
- 适用在Windows、Linux和macOS环境下打包Go应用程序的详细步骤和命令
- Redis
- Dochub
- Docker部署开发go环境
- Docker部署运行go环境
- dochub说明
- Vue
- i18n
- vue3
- vue3基本知识
- element-plus 表格单选
- vue3后台模板
- Thinkphp
- Casbin权限控制中间件
- 容器、依赖注入、门面、事件、中间件
- tp6问答
- 伪静态
- thinkphp-queue
- think-throttle
- thinkphp队列queue的一些使用说明,queue:work和queue:listen的区别
- ThinkPHP6之模型事件的触发条件
- thinkphp-swoole
- save、update、insert 的区别
- Socket
- workerman
- 介绍
- 从ThinkPHP6移植到Webman的一些技术和经验(干货)
- swoole
- swoole介绍
- hyperf
- hf官网
- Swoft
- swoft官网
- easyswoole
- easyswoole官网地址
- EASYSWOOLE 聊天室DEMO
- socket问答
- MySQL
- 聚簇索引与非聚簇索引
- Mysql使用max获取最大值细节
- 主从复制
- 随机生成20万User表的数据
- MySQL进阶-----前缀索引、单例与联合索引
- PHP
- 面向切面编程AOP
- php是单线程的一定程度上也可以看成是“多线程”
- PHP 线程,进程、并发、并行 的理解
- excel数据画表格图片
- php第三方包
- monolog/monolog
- league/glide
- 博客-知识网站
- php 常用bc函数
- PHP知识点的应用场景
- AOP(面向切面编程)
- 注解
- 依赖注入
- 事件机制
- phpspreadsheet导出数据和图片到excel
- Hyperf
- mineAdmin
- 微服务
- nacos注册服务
- simps-mqtt连接客户端simps
- Linux
- 切换php版本
- Vim
- Laravel
- RabbitMQ
- thinkphp+rabbitmq
- 博客
- Webman框架
- 框架注意问题
- 关于内存泄漏
- 移动端自动化
- 懒人精灵
- 工具应用
- render
- gitlab Sourcetree
- ssh-agent失败 错误代码-1
- 资源网站
- Git
- wkhtmltopdf
- MSYS2 介绍
- powershell curl 使用教程
- NSSM(windows服务工具)
- MinGW64
- 知识扩展
- 对象存储系统
- minio
- 雪花ID
- 请求body参数类型
- GraphQL
- js 深拷贝
- window 共享 centos文件夹
- 前端get/post 请求 特殊符号 “+”传参数问题
- 什么是SCM系统?SCM系统与ERP系统有什么区别?
- nginx 日志格式统一为 json
- 特殊符号怎么打
- 收藏网址
- 收藏-golang
- 收藏-vue3
- 收藏-php
- 收藏-node
- 收藏-前端
- 规划ITEM
- 旅游类
- 人脸识别
- dlib
- Docker&&部署
- Docker-compose
- Docker的网络模式
- rancher
- DHorse
- Elasticsearch
- es与kibana都docke连接
- 4种数据同步到Elasticsearch方案
- GPT
- 推荐系统
- fastposter海报生成
- elasticsearch+logstash+kibana
- beego文档系统-MinDoc
- jeecg开源平台
- Java
- 打包部署
- spring boot
- 依赖
- Maven 相关 命令
- Gradle 相关命令
- mybatis
- mybatis.plus
- spring boot 模板引擎
- SpringBoot+Maven多模块项目(创建、依赖、打包可执行jar包部署测试)完整流程
- Spring Cloud
- Sentinel
- nacos
- Apollo
- java推荐项目
- gradle
- Maven
- Nexus仓库管理器
- Python
- Masonite框架
- scrapy
- Python2的pip2
- Python3 安装 pip3
- 安全攻防
- 运维技术
- 腾讯云安全加固建议
- 免费freessl证书申请
- ruby
- homeland
- Protobuf
- GIT
- FFMPEG
- 命令说明
- 音频
- ffmpeg合并多个MP4视频
- NODEJS
- 开发npm包
- MongoDB
- php-docker-mongodb环境搭建
- mongo基本命令
- Docker安装MongoDB最新版并连接
- 少儿编程官网
- UI推荐
- MQTT
- PHP连接mqtt
- EMQX服务端
- php搭建mqtt服务端