猫猫头钢琴——CatPiano

·  阅读 1895
猫猫头钢琴——CatPiano

一起用代码吸猫!本文正在参与【喵星人征文活动】

src=http___5b0988e595225.cdn.sohucs.com_q_70,c_zoom,w_640_images_20191108_aa75477c7c5144108a70a1c7a7e22f49.gif&refer=http___5b0988e595225.cdn.sohucs.gif

背景介绍

在网上看到过很多铲屎官发自家猫主子弹钢琴的视频。我们家虽然有一只高贵的猫主子,但是苦于没有钢琴,无法发挥猫主子的艺术天赋。

image.png

作为一个合格的铲屎官,委屈谁也不能委屈了自己的猫主子,没有条件也要想办法创造条件。于是我们决定给我们家猫主子做一个 “电子” 钢琴,让猫主子可以尽情追求他的音乐梦想。

虽然说是要做 “电子” 钢琴,但是苦于时间有限(要给猫主子赚罐头钱),没办法买机械零件做键盘了,于是打算做一个 “丐版” 的,来给猫主子整一个 “虚拟” 的琴键。

另外,我们都知道,学钢琴最大的阻碍其实只有两个 —— 左手和右手。因此,我们打算整点不一样的 —— 用头来弹钢琴。

于是我们决定给猫主子做一个 “猫头钢琴”,用摄像头捕捉猫头的位置,不同位置对应不同的琴键,弹出不同的声音。这样一来解决了时间和金钱成本问题,二来又避免了猫主子学不会朝我们发脾气,还能让猫主子尽情享受音乐的快乐,一石三鸟,岂不美哉。

(猫主子:就是这么糊弄劳资的吗)

image.png

整体设计方案

要想实现猫头钢琴,要解决三个问题:

  • 怎么拍摄猫主子的视频
  • 怎么检测猫头的位置
  • 怎么弹琴与怎么显示

首先是视频的拍摄。我本来在双十一买了一个便宜的 USB 摄像头,但是因为发货慢到现在还没到货。所以只能拿手机录视频来先模拟摄像头采集图像。

猫头的检测是最麻烦的部分,我这里打算用深度学习来搞定。首先需要采集一些猫主子的图片,用标注工具标注下猫头。然后用旷视开源的 YOLOV5 模型训一把。最后用商汤近期开源的 openppl 来做模型推理部署。

至于如何弹琴,我打算用 mingus 来搞定。视频的处理与图形界面部分用 OpenCV 来做。

图像采集与标注

图像采集用手机就好,但是难点在于如何让猫主子乖乖配合。

这里我祭出了神器 —— 酸奶,溜着猫主子在屋子里转了好几圈,终于采到了一些合适的视频

(猫主子:爷这么可爱不给爷喝一口的吗)

image.png

采视频的时候手头的 windows 电脑没装 OpenCV,就找了个网站把视频转成图片了:www.img2go.com/convert-to-… ,当然用 OpenCV 的 VideoCapture 也能做到

转成图片后,用了一个在 github 上找的标注工具:github.com/tzutalin/la…

  • 众所周知,猫是流体,因此标注猫身子并不利于检测,因此最终选择标注猫头
  • 因为背景简单且任务单一,所以标注少量图片即可完成猫头检测
  • 标注时需要注意标注的格式,务必要采用 YOLOV5 模型可以识别的模式进行标注

我们最终标注了 430 张图片,训练集、验证集、测试集的数量分别是:400、20、10

YOLOV5 模型训练

模型训练

首先从 github 上 clone YOLOV5 的源码:github.com/ultralytics…

训练过程没对代码做太多改动,直接用的 repo 中的训练脚本 train.py

python .\train.py --data .\data\cat.yaml --weights .\weights\yolov5s.pt --img 160 --epochs 3000
复制代码

为了加速训练速度,可以适当缩小图片的尺寸,本次实验采用的图片尺寸是 (160, 160)

模型测试

模型训练完毕后,用未标注的图片验证下模型是否训对了。测试脚本同样参考YOLOv5 decect.py

模型前期训练效果图

train_batch0.jpg

模型前期验证效果图

val_batch0_pred.jpg

能够正确检出自家猫主子,模型应该是训的没问题

模型导出

为了更好的适配部署,我们将 PyTorch 模型转换成 ONNX 模型。模型转换同样可以直接采用 YOLOv5 的转换脚本 export.py

推理部署

推理框架安装

部署框架选择了商汤的 openppl,能够在 x86/CUDA 架构上高效运行,且有 python 接口,能够方便的搭建部署工程:github.com/openppl-pub…

下载 & 编译 openppl:

git clone https://github.com/openppl-public/ppl.nn.git
cd ppl.nn
./build.sh -DHPCC_USE_X86_64=ON -DHPCC_USE_OPENMP=ON -DPPLNN_ENABLE_PYTHON_API=ON
复制代码

编译后,在 ./pplnn-build/install/lib 路径下会生成 pyppl 包,设置下 PYTHONPATH 之后就可以用了

模型推理

按照 repo 里的 python example ,我写了一个 ModelRunner,用来给定输入跑出网络输出:

def RegisterEngines():
    engines = []

    # create x86 engine
    x86_options = pplnn.X86EngineOptions()
    x86_engine = pplnn.X86EngineFactory.Create(x86_options)

    engines.append(pplnn.Engine(x86_engine))
    return engines

class ModelRunner(object):
    def __init__(self, model_path):
        self.__initialize(model_path)
    
    def __initialize(self, model_path):
        # register engines
        engines = RegisterEngines()
        if len(engines) == 0:
            raise Exception('failed to register engines')

        # create runtime builder
        runtime_builder = pplnn.OnnxRuntimeBuilderFactory.CreateFromFile(model_path, engines)
        if not runtime_builder:
            raise Exception('failed to create runtime builder from file: %s' % (model_path))

        # create runtime
        self.runtime = runtime_builder.CreateRuntime()
        if not self.runtime:
            raise Exception('failed to create runtime')
    
    def get_input_tensor_shape(self):
        return self.runtime.GetInputTensor(0).GetShape().GetDims()
    
    def forward(self, input):
        if not self.runtime:
            raise Exception('runtime not created')
        
        # get input tensor info
        tensor = self.runtime.GetInputTensor(0)
        shape = tensor.GetShape()
        np_data_type = g_pplnntype2numpytype[shape.GetDataType()]
        dims = shape.GetDims()

        # feed input data
        input = np.ascontiguousarray(input) # use contiguousarray to avoid calc error
        status = tensor.ConvertFromHost(input)
        if status != pplcommon.RC_SUCCESS:
            raise Exception('failed to set input data')
        
        # start to inference
        status = self.runtime.Run()
        if status != pplcommon.RC_SUCCESS:
            raise Exception('failed to run')
        
        # wait for inference finished
        status = self.runtime.Sync()
        if status != pplcommon.RC_SUCCESS:
            raise Exception('failed to sync')
        
        # get output data
        out_datas = {}
        for i in range(self.runtime.GetOutputCount()):
            # get output tensor info
            tensor = self.runtime.GetOutputTensor(i)
            tensor_name = tensor.GetName()
            # fetch output data
            tensor_data = tensor.ConvertToHost()
            if not tensor_data:
                raise Exception('failed to get output ' + tensor_name)
            
            out_data = np.array(tensor_data, copy=False)
            out_datas[tensor_name] = copy.deepcopy(out_data)
        
        return out_datas
复制代码

数据预处理与后处理

数据预处理的代码比较简单:

# preprocess
img = cv2.resize(img, (self.input_img_w, self.input_img_h)) # resize
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)                  # BGR -> RGB
img = img.transpose(2, 0, 1)                                # HWC -> CHW
img = img.astype(dtype = np.float32)                        # uint8 -> fp32
img /= 255                                                  # normalize
img = np.expand_dims(img, axis=0)                           # add batch dimension
复制代码

由于输入 shape 为 (160, 160),且训练的所有图片 shape 一致,这里就没用 letterbox。

关于后处理,标准的 YOLOV5 有三个输出,需要结合不同层级的 anchor 计算输出的 box 位置。不过 repo 导出的 ONNX 模型已经替我们完成了这一部分工作,我们只需要对结果中的 box_score 和 class_score 做筛选,并进行 nms 就好啦。这里就不贴出后处理的代码了

制作琴键

琴键声音

琴键声音我这里用 python 的 mingus 库来解决的,安装非常简单:

pip3 install mingus
pip3 install fluidsynth
复制代码

播放音符也非常简单,就几行代码:

from mingus.midi import fluidsynth
fluidsynth.init('/usr/share/sounds/sf2/FluidR3_GM.sf2', 'alsa')    # for ubuntu
fluidsynth.play_Note(64, 0, 100)                                   # 标准音 a1
复制代码

键盘显示

键盘的图形用 OpenCV 来制作

钢琴键盘上一共有四种键 —— 三种白键和一种黑键,这里用 Enum 来描述这四种键,并对不同种类的键进行不同的图形处理,最终抽象成类 PianoKey

class KeyType(Enum):
    WHITE_KEY = 0,
    WHITE_KEY_LEFT = 1,
    WHITE_KEY_RIGHT = 2,
    BLACK_KEY = 3
复制代码

PianoKey 中有一个 play(self, position) 接口,一旦 position 落在了琴键的范围内,就认定琴键被按下,发出琴键对应的声音。

放一张琴键的效果图(黄色为被按下的琴键,程序员配色):

image.png

最终效果

把上述模块组合起来,就得到我们的 “猫头钢琴” 啦

贴一张最终的效果图,红框为检测到的猫头,红点为检测框的中心店,用这个点来触碰琴键:

image.png

视频 demo

视频 demo:www.bilibili.com/video/BV17h…

github repo 链接:github.com/ZichenTian/…