人工智障demo

简介

这个项目主要的功能为摄像头识别指定的人脸,识别完成后,自动弹出语音提示并进行对话交流。项目内内置了问题和匹配的答案,对外部人提问的东西,需要识别出语音,然后语音播出问题。

整个项目使用python做的,如果你用的其他语言,也没有问题

主要技术难点

  • 人脸抓取

    如何获取到人脸并抓取?

  • 人脸识别

    如何识别出特定人的脸?

  • 语音合成

    如何将指定的文字合成语音?

  • 语音识别

    如何获取用户的语音输入,并识别出来?

  • 问题识别

    如何识别用户提的问题?

  • 与前端交互

    如何在后台得到指定的消息时,实时和前端进行交互?

实现

人脸抓取

人脸识别的主要难点在于捕获人脸和识别。

  • 人脸抓取

    在查阅和问了相关的人之后,发现他们做的东西都是基于手机的,而很多开源的sdk工具都有基于手机的硬件上的支持,所以做起来也比较方便。但是基于PC的和python的几乎没有。但是技术路线也就那么几条路,人脸的抓取主要用的opencv+dlib 。其中opencv主要的作用是调用摄像头。dlib则是提供检测器来发现人脸。下面提供一个python使用opencvdlib来捕获人脸的例子

# 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方法就是对波形的处理,这里的preffixsuffix参数是调出来的,主要是用来表示多长时间不说话就停止录音,可以根据实际的情况进行调整。

关于语音识别的功能这块,提供一位道友的解决方案,非常牛逼的,我深受启发 https://github.com/zthxxx/python-Speech_Recognition

问题识别

项目要求内置10个问题,这10个问题要求程序在录入语音后能够识别出来。大体思路如下

  1. 问题和答案先匹配好
  2. 语音识别出的问题进行分词
  3. 分词结果到对应的问题中去匹配,查找最匹配的内容
  4. 匹配出的问题查找对应的答案并合成语音
  5. 播放合成的语音

这里简单说明一下第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的版本
posted @ 2018-10-30 14:54  rilweic  阅读(354)  评论(0编辑  收藏  举报