人工智障demo
简介
这个项目主要的功能为摄像头识别指定的人脸,识别完成后,自动弹出语音提示并进行对话交流。项目内内置了问题和匹配的答案,对外部人提问的东西,需要识别出语音,然后语音播出问题。
整个项目使用python做的,如果你用的其他语言,也没有问题
主要技术难点
-
人脸抓取
如何获取到人脸并抓取?
-
人脸识别
如何识别出特定人的脸?
-
语音合成
如何将指定的文字合成语音?
-
语音识别
如何获取用户的语音输入,并识别出来?
-
问题识别
如何识别用户提的问题?
-
与前端交互
如何在后台得到指定的消息时,实时和前端进行交互?
实现
人脸抓取
人脸识别的主要难点在于捕获人脸和识别。
-
人脸抓取
在查阅和问了相关的人之后,发现他们做的东西都是基于手机的,而很多开源的sdk工具都有基于手机的硬件上的支持,所以做起来也比较方便。但是基于PC的和python的几乎没有。但是技术路线也就那么几条路,人脸的抓取主要用的
opencv
+dlib
。其中opencv主要的作用是调用摄像头。dlib则是提供检测器来发现人脸。下面提供一个python使用opencv和dlib来捕获人脸的例子
# created at 2018-05-11
# updated at 2018-09-07
# Author: coneypo
# Blog: http://www.cnblogs.com/AdaminXie
# GitHub: https://github.com/coneypo/Dlib_face_recognition_from_camera
# 进行人脸录入
# 录入多张人脸
import dlib # 人脸识别的库 Dlib
import numpy as np # 数据处理的库 Numpy
import cv2 # 图像处理的库 OpenCv
import os
import shutil
# Dlib 预测器
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
# 创建 cv2 摄像头对象
cap = cv2.VideoCapture(0)
# cap.set(propId, value)
# 设置视频参数,propId 设置的视频参数,value 设置的参数值
cap.set(3, 480)
# 截图 screenshoot 的计数器
cnt_ss = 0
# 人脸截图的计数器
cnt_p = 0
# 存储人脸的文件夹
current_face_dir = 0
# 保存
path_make_dir = "data/faces_from_camera/"
path_csv = "data/csvs_from_camera/"
# 清除文件夹中原有的文件
def pre_clear():
folders_rd = os.listdir(path_make_dir)
for i in range(len(folders_rd)):
shutil.rmtree(path_make_dir+folders_rd[i])
csv_rd = os.listdir(path_csv)
for i in range(len(csv_rd)):
os.remove(path_csv+csv_rd[i])
# 清除已存在的人脸和csv文件
pre_clear()
# 人脸种类数目的计数器
person_cnt = 0
# cap.isOpened() 返回 true/false 检查初始化是否成功
while cap.isOpened():
print("cap is opened")
# cap.read()
# 返回两个值:
# 一个布尔值 true/false,用来判断读取视频是否成功/是否到视频末尾
# 图像对象,图像的三维矩阵q
flag, im_rd = cap.read()
# 每帧数据延时 1ms,延时为 0 读取的是静态帧
kk = cv2.waitKey(1)
# 取灰度
img_gray = cv2.cvtColor(im_rd, cv2.COLOR_RGB2GRAY)
# 人脸数 rects
rects = detector(img_gray, 0)
# print(len(rects))q
# 待会要写的字体
font = cv2.FONT_HERSHEY_COMPLEX
# 按下 'n' 新建存储人脸的文件夹
if kk == ord('n'):
person_cnt += 1
# current_face_dir = path_make_dir + time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
current_face_dir = path_make_dir + "person_" + str(person_cnt)
print('\n')
for dirs in (os.listdir(path_make_dir)):
if current_face_dir == path_make_dir + dirs:
shutil.rmtree(current_face_dir)
print("删除旧的文件夹:", current_face_dir)
os.makedirs(current_face_dir)
print("新建的人脸文件夹: ", current_face_dir)
# 将人脸计数器清零
cnt_p = 0
if len(rects) != 0:
# 检测到人脸
# 矩形框
for k, d in enumerate(rects):
# 计算矩形大小
# (x,y), (宽度width, 高度height)
pos_start = tuple([d.left(), d.top()])
pos_end = tuple([d.right(), d.bottom()])
# 计算矩形框大小
height = d.bottom() - d.top()
width = d.right() - d.left()
# 根据人脸大小生成空的图像
cv2.rectangle(im_rd, tuple([d.left(), d.top()]), tuple([d.right(), d.bottom()]), (0, 255, 255), 2)
im_blank = np.zeros((height, width, 3), np.uint8)
# 按下 's' 保存摄像头中的人脸到本地
if kk == ord('s'):
cnt_p += 1
for ii in range(height):
for jj in range(width):
im_blank[ii][jj] = im_rd[d.top() + ii][d.left() + jj]
cv2.imwrite(current_face_dir + "/img_face_" + str(cnt_p) + ".jpg", im_blank)
print("写入本地:", str(current_face_dir) + "/img_face_" + str(cnt_p) + ".jpg")
# 显示人脸数
cv2.putText(im_rd, "Faces: " + str(len(rects)), (20, 100), font, 0.8, (0, 255, 0), 1, cv2.LINE_AA)
# 添加说明
cv2.putText(im_rd, "Face Register", (20, 40), font, 1, (255, 255, 255), 1, cv2.LINE_AA)
cv2.putText(im_rd, "N: New face folder", (20, 350), font, 0.8, (255, 255, 255), 1, cv2.LINE_AA)
cv2.putText(im_rd, "S: Save face", (20, 400), font, 0.8, (255, 255, 255), 1, cv2.LINE_AA)
cv2.putText(im_rd, "Q: Quit", (20, 450), font, 0.8, (255, 255, 255), 1, cv2.LINE_AA)
# 按下 'q' 键退出
if kk == ord('q'):
break
# 窗口显示
# cv2.namedWindow("camera", 0) # 如果需要摄像头窗口大小可调
cv2.imshow("camera", im_rd)
# 释放摄像头
cap.release()
# 删除建立的窗口
cv2.destroyAllWindows()
其中while
循环用来不停的从摄像头获取图像帧。详细的说明可参见代码中的注释。
人脸识别
人脸的识别不是自己开发,现在国内已有比较成熟的图像识别的开源工具,其中百度的还不错,免费的。后续的语音的识别和合成都是用的百度的,详情参阅百度api文档,说的比较详细(我懒得写)。
这里需要说明的是,使用人脸识别需要预先将人脸上传上去,详细的文档可参见百度的人脸库管理
百度的图片识别的QPS为每秒2次,但是经过实际操作发现并非如此,频繁的调用会导致api调用超时。
我在实际操作的过程中,只要通过opencv捕获到人脸后就进行识别操作,当查找到人脸时还好,如果找不到,由于上面在while循环中操作,所以api调用非常频繁,导致前面摄像头直接卡死。整个程序崩溃的情况。
语音合成
同上,百度有很详细的例子
语音识别
同上,百度有很详细的例子
声音捕获
声音的捕获是个很麻烦的事情,举个例子,非直接硬件操作,你没法知道此时此刻用户是否在说话,是否说完的也不好弄,所以需要将软件捕获的数据流进行分析,判断出这段有没有在录入声音。
python里面对声音的录入是使用的PyAudio
库
import pyaudio
import wave
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024
RECORD_SECONDS = 5
WAVE_OUTPUT_FILENAME = "file.wav"
audio = pyaudio.PyAudio()
# start Recording
stream = audio.open(format=FORMAT, channels=CHANNELS,
rate=RATE, input=True,
frames_per_buffer=CHUNK)
print("recording...")
frames = []
for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
data = stream.read(CHUNK)
frames.append(data)
print("finished recording")
# stop Recording
stream.stop_stream()
stream.close()
audio.terminate()
waveFile = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
waveFile.setnchannels(CHANNELS)
waveFile.setsampwidth(audio.get_sample_size(FORMAT))
waveFile.setframerate(RATE)
waveFile.writeframes(b''.join(frames))
waveFile.close()
上面的代码是demo来表示如何进行声音录制并保存到文件。接下来在展示一段项目中使用的代码。
项目中主要由针对声音波形的判断。
def short_record_voice(self, channels, frame_rate=16000, chunk=1024, record_max_second=10, ws=None):
if ws:
ws.send_actions("recording", "录音中")
pa = PyAudio()
stream = pa.open(format=paInt16, channels=channels,
rate=frame_rate, input=True,
frames_per_buffer=chunk)
audio_buf = []
count_temp = 0
start_temp = False
for i in range(0, int(frame_rate / chunk * record_max_second)):
string_audio_data = stream.read(chunk)
audio_buf.append(string_audio_data)
# 处理音频,保留有声音的部分
flag, count_temp, start_temp = self.calc_end(string_audio_data, frame_rate, count_temp, start_temp)
if flag:
break
file_path = self.sound_dir + self.record_auido_name
self.save_wave_file(file_path, audio_buf, channels, 2, frame_rate)
stream.close()
return file_path
def save_wave_file(self, file_path, wave_buffer, channels=1, sample_width=2, sample_frequency=16000):
if not wave_buffer:
wave_buffer = wave_buffer
with wave.open(file_path, 'wb') as wf:
wf.setnchannels(channels)
wf.setsampwidth(sample_width)
wf.setframerate(sample_frequency)
if isinstance(wave_buffer, list):
for data_block in wave_buffer:
wf.writeframes(data_block)
elif not isinstance(wave_buffer, bytes):
raise Exception("Type of bin_data need bytes!")
else:
wf.writeframes(wave_buffer)
def calc_end(self, audio_data, framerate, count_temp, start_temp, prefix=80, suffix=8):
# 将波形数据转换为数组
wave_data = numpy.fromstring(audio_data, dtype=numpy.short)
# 将wave_data数组改为2列,行数自动匹配。在修改shape的属性时,需使得数组的总长度不变。
wave_data.shape = -1, 2
# 将数组转置
wave_data = wave_data.T
# 采样点数,修改采样点数和起始位置进行不同位置和长度的音频波形分析
N = 44100
start = 0 # 开始采样位置
df = framerate / (N - 1) # 分辨率
freq = [df * n for n in range(0, N)] # N个元素
wave_data2 = wave_data[0][start:start + N]
c = numpy.fft.fft(wave_data2) * 2 / N
# 常规显示采样频率一半的频谱
d = int(len(c) / 2)
# 仅显示频率在4000以下的频谱
while freq[d] > 4000:
d -= 10
t = (abs(c[:d - 1]), 'r')
if int(t[0][0] * 10) > 0:
count_temp = 0
start_temp = True
else:
count_temp += 1
if count_temp > suffix and start_temp:
count_temp = 0
start_temp = False
return True, count_temp, start_temp
elif count_temp > prefix:
count_temp = 0
start_temp = False
return True, count_temp, start_temp
else:
return False, count_temp, start_temp
calc_end
方法就是对波形的处理,这里的preffix
和suffix
参数是调出来的,主要是用来表示多长时间不说话就停止录音,可以根据实际的情况进行调整。
关于语音识别的功能这块,提供一位道友的解决方案,非常牛逼的,我深受启发 https://github.com/zthxxx/python-Speech_Recognition
问题识别
项目要求内置10个问题,这10个问题要求程序在录入语音后能够识别出来。大体思路如下
- 问题和答案先匹配好
- 语音识别出的问题进行分词
- 分词结果到对应的问题中去匹配,查找最匹配的内容
- 匹配出的问题查找对应的答案并合成语音
- 播放合成的语音
这里简单说明一下第2、3点。
文本识别主要的库为textrank4zh
,详情参见使用文档
其他的就没什么说的了,都是些搬砖的工作。
其他
这里在说说大概的思路的遇到的坑。
项目的主要实现思路是将摄像头捕获人脸作为一个单独的线程,而主线程为声音线程。两个线程的通讯使用python的queue
来实现,当检测到人脸时,向queue中写数据,而声音线程不停的从queue中获取数据,从而判断是否捕获到人脸(可优化,刚开始的时候是想通过queue来多设置一些消息好让两个线程配置使用,后来发现没什么卵用)。
这样有个问题,在mac上使用的时候,mac不允许辅线程启动摄像头,win倒是可以,所以我将两个线程换了一下。把摄像头作为主线程,声音作为辅线程。
前端的交互使用websocket
,实时将后端消费发给前端,前端做出相应的响应。websocket使用的一个老铁搭建的,他用的java(可优化,java比较吃内存和性能,可以考虑使用tornado-websocket
)。
这里提供一个python tornado的websocket的例子
# -*- encoding:utf-8 -*-
# 2014-12-18
# author: orangleliu
import tornado.web
import tornado.websocket
import tornado.httpserver
import tornado.ioloop
class WebSocketHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True
def open(self):
pass
def on_message(self, message):
print(self.get_argument("sid"))
print(message)
self.write_message(u"Your message was: " + message)
def on_close(self):
pass
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/ws', WebSocketHandler)
]
settings = {"template_path": "."}
tornado.web.Application.__init__(self, handlers, **settings)
if __name__ == '__main__':
ws_app = Application()
server = tornado.httpserver.HTTPServer(ws_app)
server.listen(20088)
tornado.ioloop.IOLoop.instance().start()
后续可优化
- 去掉queue队列的使用,将queue队列的使用转为全局变量操作(前提是queue没有做其他的消息使用)
- 优化声音线程,将声音的启动改为信号量方式,捕获到人脸后将声音线程启动。
- 优化摄像头线程,能否在声音显示的时候关闭摄像头?摄像头线程为主线程的话无法实现
- 替换原有的java版本
websocket
。改为tornado-websocket
的版本