查看原文
其他

设计师的AI自学之路:用图像识别玩忍术

程序猿DD 2019-07-14

作者:张晟

来源:人工智能与设计


某日工作室学妹问我,看视频学人工智能好枯燥,有没有实际项目可以实践下?


正巧室友刚做了一个识别剪刀石头布的图像识别程序,于是脑洞大开,改造了一下,做了这个识别结印手势来发动忍术的小游戏。

演示视频:

https://v.qq.com/txp/iframe/player.html?vid=j1341a2ewae&width=500&height=375&auto=0

这里我就把项目整理成教程,让大家都能做脑洞大开的创作。

感谢室友陆玄青提供的简单图像识别源码https://github.com/LuXuanqing/tutorial-image-recognition


改造后的识别手势玩火影忍者忍术源码在这里:

https://github.com/Arthurzhangsheng/NARUTO_game


本项目不要求有人工智能基础,但要有python基础,需要的环境:

  • tensorflow1.1

  • keras

  • opencv

  • python3

  • ffmpeg

  • PIL

  • pathlib

  • shutil

  • imageio

  • numpy

  • pygame

  • 一个摄像头


整体流程


下载源码后,用jupyternotebook打开tutorial.ipynb文件,按照里面的教程,一步一步运行,全部运行过后,就得到训练好的能识别手势的神经网络模型文件。运行model文件夹下的predict.py,即可开始试玩。


注意事项


我用的vscode编辑器,把当前工作路径设置为 NARUTO_game 这个主文件夹,并以此设置相关的相对路径,若直接cd到model文件夹来运行predict.py文件,需要手动调整源码中的相对路径。


其实教程具体操作已经全部写在tutorial.ipynb里了,为让大家更直观了解整个操作过程,这里就把tutorial.ipynb里的文字复制搬运到这里来。


Step1 - 采集数据


用手机拍摄视频记录你想要识别的物体。每段视频中只能包含一种物体,时长10~30秒,每个物体可以拍摄多段视频。视频尽量用4:3或1:1的长宽比,分辨率低越好(注意是低)。


进入data/video文件夹,为每种物体(手势)新建一个文件夹,然后把相应的视频导入进去。例如我拍摄了5段关于猫的视频和3段关于狗的视频,就在data/video文件夹下新建dog、cat两个文件夹,然后把把猫的视频全部放进cat文件夹,把狗的视频全部放进dog文件夹,视频的文件名无所谓。


识别结印手势的话我分了14类,12个结印为一类,空白动作为1类,还增加了一个取消动作(虽然这次并没有用到),一共14类。视频文件太大我就没传到github源码上了,大家可以自己用电脑摄像头录一下。

Step2 - 数据处理


在这一步,我们需要把视频转成图片,然后按照60%、20%、20%的比例拆分成训练集(training set)、验证集(validation set)、和测试集(test set)。 为了节省大家时间,我事先已经写好了相关的代码(utils.py),大家只要按照提示进行调用即可完成这一步骤


  1. import utils#################### 以下是你可以修改的部分 ####################fps = 5 # fps是视频的采样率,即每秒中采集多少张图片,建议设置为5~10# 每张图片的大小,根据你原始视频的比例进行缩放,建议不要超过300x300# 训练所需时间会和图像的像素数量成正比,建议设置得小一点,如160x120width = 160height = 90#################### 以上是你可以修改的部分 ####################utils.process_videos(fps, target_size=(width, height))


Step3 - 数据增强


把一张原始图片经过拉伸、旋转、斜切、反转等操作,可以生产若干新的不同的图片,用以扩充训练集数据量,有助于提高模型的预测准确性。


  1. from keras.preprocessing.image import ImageDataGenerator

  2. from pathlib import Path

  3. # 设置train,val,test的目录

  4. base_dir = Path('data')

  5. train_dir = base_dir/'train'

  6. val_dir = base_dir/'val'

  7. test_dir = base_dir/'test'

  8. # 创建train和val图像生成器,它们会不断地产生出新的图片

  9. #################### 以下是你可以修改的部分 ####################

  10. train_datagen = ImageDataGenerator(rescale=1./255,

  11.                                   rotation_range=10,

  12.                                   width_shift_range=0.2,

  13.                                   height_shift_range=0.2,

  14.                                   shear_range=0.2,

  15.                                   zoom_range=0.2,

  16.                                   horizontal_flip=False,

  17.                                   vertical_flip=True,

  18.                                   fill_mode='nearest')

  19. #################### 以上是你可以修改的部分 ####################

  20. train_generator = train_datagen.flow_from_directory(train_dir, target_size=(height,width))

  21. val_generator = train_datagen.flow_from_directory(val_dir, target_size=(height,width))

  22. # test的时候是模拟真实环境,所以要使用原始图片,不要对图片进行任何操作

  23. test_datagen = ImageDataGenerator(rescale=1./255)

  24. test_generator = test_datagen.flow_from_directory(test_dir, target_size=(height,width))


Step4 - 搭建卷积神经网络


在这一步我们要搭建神经网络的架构。 图像识别的常见方法是通过卷积操作提取图片中的特征,然后将特征输入到神经网络中,最后神经网络输出结果。所以在这一阶段,我们要分别准备卷积和神经网络两个部分。


4.1 - 卷积部分


迁移学习(transfer learning)


对图像进行卷积操作需要耗费大量计算资源,并且训练需要巨大的数据量,一般个人是搞不定这事的。 好消息是人们发现了一个有趣的现象:训练出来用于识别A物体的卷积神经网络,它的卷积部分也能够很好地被用于识别B物体。 所以我们可以把人家已经训练好的NB的卷积神经网络借来用,这就是迁移学习。


载入VGG16


VGG16是一个非常经典的卷积神经网络,16代表有16个层,前13层是卷积层,后3层是全连阶层。我们需要使用它的前13个卷积层,并且使用这些层的权值,用来从图像中提取特征。然后把提取后的特征输入到我们自己的神经网络中进行识别。


  1. import keras as K

  2. # load pretrained model and weights

  3. conv_layers = K.applications.VGG16(include_top=False, input_shape=(height,width,3))

  4. conv_layers.trainable = False

  5. print('per-trained model has been loaded')


4.2 - 神经网络部分


  1. model = K.models.Sequential()

  2. model.add(conv_layers)#载入VGG16的卷积部分

  3. model.add(K.layers.Flatten())#拉平成一维

  4. n_classes = len(utils.get_child_dir_names(base_dir/'video'))

  5. # 以下是你可以修改的部分

  6. model.add(K.layers.Dense(2048, activation='relu'))

  7. model.add(K.layers.Dropout(0.5))

  8. model.add(K.layers.Dense(2048, activation='relu'))

  9. model.add(K.layers.Dropout(0.5))

  10. # 以上是你可以修改的部分

  11. model.add(K.layers.Dense(n_classes, activation='softmax'))

  12. print('以下是神经网络的架构:')

  13. model.summary()


Step5 - 训练及验证


可以尝试选择不同的优化器和优化器参数(Keras文档),好的优化器能让训练结果尽快收敛并获得更高的准确率。


  1. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

  2. print('优化器设置完毕')


下面开始训练,为了节省时间只设置了迭代20次。你可以尝试不同迭代次看看它数对最终结果的影响。


  1. n_epochs = 20

  2. n_train_samples = utils.count_jpgs(train_dir)#训练集图片总数

  3. n_val_samples = utils.count_jpgs(val_dir)#val集图片总数

  4. batch_size = 32

  5. history = model.fit_generator(train_generator, steps_per_epoch=n_train_samples/batch_size, epochs=n_epochs,

  6.                              validation_data=val_generator, validation_steps=n_val_samples/batch_size, verbose=2)

  7. print('训练完毕')

画图看一下训练效果

  1. from matplotlib import pyplot as plt

  2. fig = plt.figure(figsize=(8,4), dpi=100)

  3. plt.plot(range(n_epochs), history.history['acc'], 'c', label='Training Accuracy', aa=True)

  4. plt.plot(range(n_epochs), history.history['val_acc'], 'darkorange', label='Validation Accuracy', aa=True)

  5. plt.legend()

  6. plt.xlabel('epoch')

  7. plt.ylabel('Accuracy')

  8. plt.ylim(0,1)

  9. plt.grid()

  10. plt.show()


怎么看训练的结果好不好 好的情况 总体上来看,train和val的正确率都随着迭代次数增加而上升,并且最后收敛于某一个比较高的数值。 两种不好的情况:

1.欠拟合(under-fitting)

train和val的正确率都比较低。 造成这种情况的原因有很多,常见的有:数据量不够大、神经网络设计得不合理、优化器选择不合理、迭代次数不够。


2.过拟合(over-fitting)

train的正确率很高,但是val正确率很低。 这种情况代表模型的泛化能力不好,它完全适应了训练集的数据(可以接近100%的正确率),但是不适用于验证集的数据。 解决方法是使用在Dense层后追加Dropout层或是在Densse层的选项中设置regularizer。


Step6 - 测试


如果上面的验证结果还不错,那恭喜你就快要成功了! 最后我们用测试集的数据来测试一下。


  1. n_test_samples = utils.count_jpgs(test_dir)

  2. _, test_acc = model.evaluate_generator(test_generator, steps=n_test_samples/batch_size)

  3. print('测试正确率:{}'.format(test_acc))


Step7 - 拍张照,让程序来判断它是什么


拍一张照,上传到 data/x 文件夹中,默认文件名是 myimage.jpg。如果你保存了其它文件名或是其它文件夹,需要修改下方代码中的路径。


先显示一下图片看看对不对

  1. from PIL import Image

  2. path = 'data/test/辰/chen_0.jpg'

  3. img = Image.open(path)

  4. img.show()


让程序来预测试试吧

  1. x = utils.preprocess(img, (width, height))

  2. y = model.predict(x)[0]

  3. class_indices = train_generator.class_indices#获得文件夹名的和类的序号对应的字典

  4. class_indices_reverse={v:k for k,v in class_indices.items()}#反转字典的索引和内容值

  5. utils.show_pred(y,class_indices_reverse)


Optional - 用自己电脑的摄像头做实时预测


先保存训练好的模型文件

  1. model.save('model/NARUTO.h5')

  2. utils.save_confg(class_indices_reverse,input_size=(160,90),fp='model/config.json')

  3. print('保存成功')


然后运行其中model文件夹下的的predict.py即可。


这里有个注意事项:我用的vscode编辑器,把当前工作路径设置为 NARUTO_game 这个主文件夹,并以此设置相关的相对路径,若直接cd到model文件夹来运行predict.py文件,需要手动调整源码中的相对路径

然后predict.py文件里大部分是关于如何根据识别到的图像结果,来做出放音效,放gif特效等操作,就不展开细讲每一步在做什么了,大家可以自己发挥想象力去改造。


用到的一些音效、gif图、字体也都放在源码仓库里了


  1. # coding=utf-8

  2. from keras import models

  3. import numpy as np

  4. import cv2

  5. import json

  6. import os

  7. from PIL import Image, ImageDraw, ImageFont  

  8. import pygame,time

  9. def load_config(fp):

  10.    with open(fp,encoding='UTF-8') as f:

  11.        config = json.load(f, encoding='UTF-8')

  12.        indices = config['indices']

  13.        input_size = config['input_size']

  14.        return indices, input_size

  15. def decode(preds, indices):

  16.    results = []

  17.    for pred in preds:

  18.        index = pred.argmax()

  19.        result = indices[str(index)]

  20.        results.append(result)

  21.        result = results[0]

  22.    return result

  23. def preprocess(arr, input_size):

  24.    input_size = tuple(input_size)

  25.    # resize

  26.    x = cv2.resize(arr, input_size)

  27.    # BGR 2 RGB

  28.    x = cv2.cvtColor(x, cv2.COLOR_BGR2RGB)

  29.    x = np.expand_dims(x, 0).astype('float32')

  30.    x /= 255

  31.    return x

  32. def put_text_on_img(img,

  33.                    text='文字信息',

  34.                    font_size = 50,

  35.                    start_location = (100, 0),

  36.                    font_color = (255, 255, 255),

  37.                    fontfile = 'model/font.ttf'):

  38.    '''

  39.    读取opencv的图片,并把中文字放到图片上

  40.    font_size = 100             #字体大小

  41.    start_location = (0, 0)     #字体起始位置

  42.    font_color = (0, 0, 0)      #字体颜色

  43.    fontfile = 'model/font.ttf' #字体文件

  44.    '''

  45.    # cv2读取图片  

  46.    cv2img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # cv2和PIL中颜色的hex码的储存顺序不同  

  47.    pilimg = Image.fromarray(cv2img)  

  48.    # PIL图片上打印汉字  

  49.    draw = ImageDraw.Draw(pilimg) # 图片上打印  

  50.    font = ImageFont.truetype(fontfile, font_size, encoding="utf-8") # 参数1:字体文件路径,参数2:字体大小  

  51.    draw.text(start_location, text, font_color, font=font) # 参数1:打印坐标,参数2:文本,参数3:字体颜色,参数4:字体  

  52.    # PIL图片转cv2 图片  

  53.    convert_img = cv2.cvtColor(np.array(pilimg), cv2.COLOR_RGB2BGR)  

  54.    # cv2.imshow("图片", cv2charimg) # 汉字窗口标题显示乱码  

  55.    return convert_img

  56. def playBGM():

  57.    bgm_path = r'audio/BGM.mp3'

  58.    pygame.mixer.init()

  59.    pygame.mixer.music.load(bgm_path)

  60.    pygame.mixer.music.set_volume(0.2)

  61.    pygame.mixer.music.play(loops=-1)

  62. def playsound(action):

  63.    sound_path1 = 'audio/test1.wav'

  64.    sound_path2 = 'audio/test2.wav'

  65.    sound_path3 = 'audio/huituzhuansheng.wav'

  66.    sound_path4 = 'audio/yingfenshen.wav'

  67.    if action == "寅":

  68.        sound1 = pygame.mixer.Sound(sound_path2)

  69.        sound1.set_volume(0.3)

  70.        sound1.play()

  71.    elif action == "申":

  72.        sound1 = pygame.mixer.Sound(sound_path1)

  73.        sound1.set_volume(0.5)

  74.        sound1.play()

  75.    elif action == '酉':

  76.        sound1 = pygame.mixer.Sound(sound_path3)

  77.        sound1.set_volume(1)

  78.        sound1.play()        

  79.    elif action == "丑":

  80.        sound1 = pygame.mixer.Sound(sound_path4)

  81.        sound1.set_volume(1)

  82.        sound1.play()  

  83.    else:

  84.        pass    

  85. def add_gif2cap(cap, pngimg):

  86.    # I want to put logo on top-left corner, So I create a ROI

  87.    rows1,cols1,channels1 = cap.shape

  88.    rows,cols,channels = pngimg.shape

  89.    roi = cap[(rows1-rows)//2:(rows1-rows)//2+rows, (cols1-cols)//2:(cols1-cols)//2+cols ]

  90.    # Now create a mask of logo and create its inverse mask also

  91.    img2gray = cv2.cvtColor(pngimg,cv2.COLOR_BGR2GRAY)

  92.    ret, mask = cv2.threshold(img2gray, 180, 255, cv2.THRESH_BINARY)

  93.    mask_inv = cv2.bitwise_not(mask)

  94.    # Now black-out the area of logo in ROI

  95.    img1_bg = cv2.bitwise_and(roi,roi,mask = mask_inv)

  96.    # Take only region of logo from logo image.

  97.    img2_fg = cv2.bitwise_and(pngimg,pngimg,mask = mask)

  98.    # Put logo in ROI and modify the main image

  99.    dst = cv2.add(img1_bg,img2_fg)

  100.    cap[(rows1-rows)//2:(rows1-rows)//2+rows, (cols1-cols)//2:(cols1-cols)//2+cols] = dst

  101.    return cap

  102. def add_gif2cap_with_action(action, cap, png_num):

  103.    if action == "寅":

  104.        pngpath = 'image/shuilongdan/png/action-%02d.png'%(png_num)

  105.        pngimg = cv2.imread(pngpath)

  106.        pngimg = cv2.resize(pngimg,None,fx=0.8, fy=0.8, interpolation = cv2.INTER_CUBIC)

  107.        cap = add_gif2cap(cap, pngimg)

  108.        return cap

  109.    else:

  110.        return cap

  111. def main():

  112.    indices, input_size = load_config('model/config.json')

  113.    model = models.load_model('model/NARUTO.h5')

  114.    cap = cv2.VideoCapture(0)

  115.    counter = 0      

  116.    counter_temp = 0 #计数器

  117.    action = "子"

  118.    playBGM()

  119.    png_num = 1 #用于计数动画图片序号的变量

  120.    while True:

  121.        _, frame_img = cap.read()

  122.        # predict

  123.        x = preprocess(frame_img,input_size)

  124.        y = model.predict(x)

  125.        action = decode(y,indices)

  126.        #播放音效,且每次播放间隔50个帧

  127.        counter+=1

  128.        if counter == 2:

  129.            #触发音效

  130.            playsound(action)            

  131.            counter += 1

  132.        if counter == 50:

  133.            counter = 0

  134.        #显示动作名  

  135.        frame_img = put_text_on_img(

  136.            img= frame_img,

  137.            text= "當前動作:"+action,

  138.            font_size = 50,

  139.            start_location = (0, 100),

  140.            font_color = (255, 150, 0)

  141.        )

  142.        #触发动画

  143.        if action == "寅":

  144.            frame_img = add_gif2cap_with_action(action, frame_img, png_num)

  145.            png_num += 1

  146.            if png_num >=37:#水龙弹动画有37帧

  147.                png_num=0

  148.        #show image

  149.        cv2.imshow('webcam', frame_img)

  150.        #按Q关闭窗口

  151.        if cv2.waitKey(1) & 0xFF == ord('q'):

  152.            break

  153.    cv2.destroyAllWindows()

  154.    cap.release()

  155. if __name__ == '__main__':

  156.    main()

  157.   # playBGM()


写在最后


我自己代码水平不高,可能引起知乎读者不适,因为编程和AI只是上学期才开始自学的 ಠᴗಠ。


真正的专业是工业设计(〃´-ω・),跟知乎人工智能大神没法比,正在努力学习python和AI中。

- END -


 近期热文:

    ……

    可关注我的公众号

    深入交流、更多福利

    扫码加入我的知识星球

    点击“阅读原文”,看本号其他精彩内容

      您可能也对以下帖子感兴趣

      文章有问题?点此查看未经处理的缓存