Python创建接口项目(FastAPI)、人脸识别、Dockerfile制作

前言

我们要实现一个人脸识别的功能,人脸识别的都是调用本地的图片,所以我们搭建一个接口服务来提供图片的上传。

接口

一般接口

接口使用FastAPI框架

https://fastapi.tiangolo.com/zh/#_4

注意

该框架需要Python 3.6 及更高版本

环境变量中添加

KEY VALUE
Path D:\Tools\Python310
D:\Tools\Python310\Scripts

另外要注意

系统变量的优先级要比用户变量的优先级高,如果配置后还是2.x版本,就要看看是否系统变量中也配置了。

配置完成后要重启开发工具,不用重启电脑。

安装

1
2
pipenv install fastapi
pipenv install uvicorn[standard]

创建一个 main.py 文件并写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}

通过以下命令运行服务器:

1
pipenv run uvicorn main:app --reload

使用浏览器访问

http://127.0.0.1:8000/items/5?q=somequery

这样我们的接口服务就搭建好了。

对接的接口文档地址

http://127.0.0.1:8000/docs

静态文件

1
2
from fastapi.staticfiles import StaticFiles
app.mount("/web", StaticFiles(directory="web"), name="web")

所有以/web/开头的请求都会访问到web目录中。

允许跨域访问

1
2
3
4
5
6
7
8
9
from starlette.middleware.cors import CORSMiddleware
# 配置 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有域名访问,或指定域名,例如 ["https://example.com"]
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"], # 允许的 HTTP 方法
allow_headers=["*"], # 允许的请求头
)

文件上传

一般文件上传

要用 File,需要先安装这个库

1
pipenv install python-multipart

代码

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
# -*- coding:utf-8 -*-
import uuid
import uvicorn
import os

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.get("/")
def read_root():
return {"code": 0, "msg": "请求成功"}


# file 参数类型是字节 bytes
@app.post("/upfile/")
async def upfile(file: bytes = File(...)):
return {"file_size": len(file)}


@app.post("/uploadfile/")
async def uploadfile(image: UploadFile = File(...)):
try:
if not os.path.exists("images"):
os.makedirs("images")
except Exception as e:
print(e)
suffix_arr = image.filename.split(".")
suffix = suffix_arr[len(suffix_arr) - 1]
file_name = os.getcwd() + "/images/" + str(uuid.uuid1()) + "." + suffix

with open(file_name, "wb+") as f:
f.write(image.file.read())
f.close()

return {"filename": file_name}


if __name__ == "__main__":
uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)

注意

uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)其中host不要设置为127.0.0.1,否则无法远程访问。

Base64图片上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
import uuid
import base64
from fastapi import Body

@app.post("/base64file")
async def uploadfile(image=Body(None), suffix=Body(None)):
base64_string = ""
# 查找 Base64 数据的头部部分
if image.startswith('data:image/') and ';base64,' in image:
# 找到头部的分隔符
header_end = image.index(';base64,') + len(';base64,')
# 切片获取纯 Base64 数据
base64_string = image[header_end:]
else:
# 如果没有找到头部部分,直接返回原始字符串
base64_string = base64_string
imgdata = base64.b64decode(base64_string)
file_name = os.getcwd() + "/images/" + str(uuid.uuid1()) + "." + suffix
file = open(file_name, 'wb')
file.write(imgdata)
file.close()
return {"code": 0, "obj": file_name}

接口返回格式

HTML

1
2
3
4
5
from fastapi.responses import HTMLResponse

@app.get('/', response_class=HTMLResponse)
def read_root():
return "<h1>人像服务</h1>"

纯文本

1
2
3
4
5
from fastapi.responses import PlainTextResponse

@app.get('/', response_class=PlainTextResponse)
def read_root():
return "人像服务"

JSON

1
2
3
@app.get("/")
def read_root():
return {"code": 0, "msg": "请求成功"}

接口文档服务

文档无法访问

默认的swagger引用的JS和CSS无法访问了。

我们可以引用到项目下本地加载

通过百度网盘分享的文件:static.zip
链接:https://pan.baidu.com/s/1ZSF1K3gApxlRoS6qvK-TkQ?pwd=psvm
提取码:psvm

把文件解压到项目根目录

代码中配置路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from fastapi import FastAPI
from fastapi import Body
from starlette.staticfiles import StaticFiles
from fastapi.openapi.docs import get_swagger_ui_html

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

# 自定义 Swagger UI 的 HTML 页面
@app.get("/api_docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url="/openapi.json",
title="接口文档",
swagger_js_url='/static/swagger/swagger-ui-bundle.js',
swagger_css_url='/static/swagger/swagger-ui.css',
swagger_favicon_url='/static/swagger/img.png',
)

路由要修改一个路由,不要用/docs

禁用文档服务

默认会自动生成文档/docs

删除文档

1
app = FastAPI(docs_url=None)

去掉跳转

1
2
3
4
@app.get("/")
def root():
response = RedirectResponse(url="/docs")
return response

错误状态码

1
2
3
4
5
6
7
8
9
from fastapi import FastAPI, HTTPException
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI(docs_url=None)

@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)

运行

推荐按如下配置

1
2
3
4
5
6
7
8
@app.on_event("startup")
def startup_event():
import logging
logging.getLogger("uvicorn.error").info("Application started at http://localhost:8000")


if __name__ == "__main__":
uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)

人像识别

face_recognition

https://github.com/ageitgey/face_recognition

安装

1
2
3
pipenv install cmake
pipenv install dlib
pipenv install face_recognition

使用

1
2
3
import face_recognition
image = face_recognition.load_image_file("your_file.jpg")
face_locations = face_recognition.face_locations(image)

deepface

https://github.com/serengil/deepface

安装

1
pipenv install deepface

使用

1
2
3
from deepface import DeepFace
result = DeepFace.verify(img1_path = "img1.jpg", img2_path = "img2.jpg")
print(result)

注意

这个库主要用于对比人脸相似度,人脸的特征等功能。

接口对接人脸识别

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
# -*- coding:utf-8 -*-
import uuid
import uvicorn
import os

from fastapi import FastAPI, File, UploadFile
from starlette.responses import RedirectResponse
import face_recognition

app = FastAPI()


@app.get("/")
def root():
response = RedirectResponse(url="/docs")
return response


@app.post("/uploadfile/")
async def uploadfile(image: UploadFile = File(...)):
try:
if not os.path.exists("images"):
os.makedirs("images")
except Exception as e:
print(e)
suffix_arr = image.filename.split(".")
suffix = suffix_arr[len(suffix_arr) - 1]
file_name = os.getcwd() + "/images/" + str(uuid.uuid1()) + "." + suffix

with open(file_name, "wb+") as f:
f.write(image.file.read())
f.close()
image = face_recognition.load_image_file(file_name)
face_locations = face_recognition.face_locations(image)
if len(face_locations) > 0:
return {"code": 0, "obj": face_locations}
else:
return {"code": 1, "obj": face_locations}

if __name__ == "__main__":
uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)

WEB获取摄像头做识别

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
<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title></title>
</head>

<body>
<div class="imgouter">
<div class="left_div"><video id="v"></video></div>
<canvas id="canvas" style="display:none;"></canvas>
<img id="photo" alt="photo" class="right_div">
</div>
<div class="msg"></div>

</body>
<style>
body {
margin: 0;
padding: 0;
}

.imgouter {
display: flex;
height: 90vh;
width: 100vw;
}

.left_div,
.right_div {
width: 0;
flex: 1;
}

#v {
width: 100%;
height: 100%;
object-fit: fill;
}

.msg {
display: flex;
align-items: center;
justify-content: center;
height: 10vh;
font-size: 30px;
}
</style>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
function upload_img (imgstr) {
let arr = imgstr.split(";base64,");
let suffix = arr[0].split("/")[1];
let base64str = arr[1];
let para = {
image: base64str,
suffix: suffix
};
axios
.post('/base64file', para)
.then(function (response) {
let data = response.data;
if (data.code === 0) {
document.querySelector(".msg").innerHTML = "发现人像:" + data.obj.length;
} else {
document.querySelector(".msg").innerHTML = "未发现人像";
}
})
.catch(function (error) {
console.log(error);
});

}
!(function () {
// 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function (constraints) {
// 首先,如果有getUserMedia的话,就获得它
var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;

// 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
}

// 否则,为老的navigator.getUserMedia方法包裹一个Promise
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
}
}
const constraints = {
video: true,
audio: false
};
let videoPlaying = false;
let v = document.getElementById('v');
let promise = navigator.mediaDevices.getUserMedia(constraints);
promise.then(stream => {
// 旧的浏览器可能没有srcObject
if ("srcObject" in v) {
v.srcObject = stream;
} else {
// 防止在新的浏览器里使用它,应为它已经不再支持了
v.src = window.URL.createObjectURL(stream);
}
v.onloadedmetadata = function (e) {
v.play();
videoPlaying = true;
take_pic();
setInterval(() => {
take_pic();
}, 3000);
};
}).catch(err => {
console.error(err.name + ": " + err.message);
});

function take_pic () {
if (videoPlaying) {
let canvas = document.getElementById('canvas');
canvas.width = v.videoWidth;
canvas.height = v.videoHeight;
canvas.getContext('2d').drawImage(v, 0, 0);
let data = canvas.toDataURL("image/jpeg", 0.8);
//console.info(data);
upload_img(data);
document.getElementById('photo').setAttribute('src', data);
}
}

})();
</script>

</html>

web端只能传base64,后端也要处理base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import base64
import uuid
import os
from fastapi import Body

@app.post("/base64file")
async def uploadfile(image=Body(None), suffix=Body(None)):
imgdata = base64.b64decode(image)
file_name = os.getcwd() + "/images/" + str(uuid.uuid1()) + "." + suffix
file = open(file_name, 'wb')
file.write(imgdata)
file.close()
try:
image = face_recognition.load_image_file(file_name)
face_locations = face_recognition.face_locations(image)
if os.path.exists(file_name):
os.remove(file_name)
if len(face_locations) > 0:
return {"code": 0, "obj": face_locations}
else:
return {"code": 1, "obj": face_locations}
except Exception as e:
return {"code": 1, "obj": [], "msg": str(e)}

Docker

配置文件Dockerfile

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
# Pull base image
FROM python:3.9.0

MAINTAINER psvmc "psvmc@outlook.com"

# Set Charset
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

RUN mkdir /opt/face-recognition/
ADD ./* /opt/face-recognition/
ADD ./web /opt/face-recognition/web
RUN chmod 755 /opt/face-recognition/main.py
RUN chmod 755 /opt/face-recognition/startup.sh
RUN python -m pip install --upgrade pip -i https://pypi.douban.com/simple/ --trusted-host pypi.douban.com
RUN pip install pipenv -i https://pypi.douban.com/simple/ --trusted-host pypi.douban.com
RUN pip install cmake -i https://pypi.douban.com/simple/ --trusted-host pypi.douban.com
RUN cd /opt/face-recognition/ && pipenv install --skip-lock

# Expose ports.
EXPOSE 8000

# Define default command.
WORKDIR /opt/face-recognition/
ENTRYPOINT /opt/face-recognition/startup.sh

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

构建

1
docker build -t psvmc/face_recognition .

运行

1
docker run -d -p 8000:8000 --name face_recognition --restart=always psvmc/face_recognition

查看启动日志

1
docker logs face_recognition

删除

1
2
docker stop face_recognition
docker rm face_recognition