Skip to content

Yanshee 人工智能 8 让机器人看图识物

UBTEDU edited this page Sep 17, 2018 · 9 revisions

课程目标

本节课将学习使用 sklearn机器学习库 和 opencv 机器视觉库进行基本的手写数字识别(判别), 让你的机器人能看图识别出数字手写体。通过本节课你可以学习如何配置python 第三方python 模块,机器学习概念,用于机器学习的基本图像处理, 了解sklearn 机器学习库, 学习简单的knn 算法与knn 分类器并为日后机器学习打下基础,了解opencv机器视觉库。了解当前比较流行的物体识别方法。

课程引入原因

让机器人识别出它所看到的物体就是我们所谓的物体识别,当今世界人工智能领域的物体识别是一个非常庞大的课题,我们希望通过本节课来为大家打开物体识别学习的一扇窗。识别(判别)手写数据集是物体识别的第一课。这些手写数据集图像,已经有相当成熟的预处理,背景简单,主体突出,轮廓明显,只需在进行简单的二值化便可进行训练与识别。这对于没有很多图像处理的新手来说是非常友好的。因此希望这个基础课程可以带您进入机器学习之物体识别的世界。而后续我们将推出更多与物体识别相关的深度课程。

基础概念及知识点介绍

MNIST DATABASE

MNIST 数据库是一个手写数字机器学习图片库,包含60000训练样本,10000测试样本

Sklearn

sklearn是机器学习中一个常用的python第三方模块,网址:http://scikit-learn.org/stable/index.html ,里面对一些常用的机器学习方法进行了封装,在进行机器学习任务时,并不需要每个人都实现所有的算法,只需要简单的调用sklearn里的模块就可以实现大多数机器学习任务。

机器学习

机器学习 (Machine Learning, ML) 是一门多领域交叉学科,涉及概率论,统计学,逼近论,凸分析,算法复杂度等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取的知识技能,重新组织已有的知识结构使之不断改善自身的性能。机器学习分为监督学习,非监督学习,半监督学习,强化学习和深度学习等。本次课程用的是监督学习的结果去判别。

监督学习

利用一组已知类别的样本调整分类器的参数,使其达到所要求性能的过程, 也称为有教师学习。分类器识别结果为该分类的分类标签。当然,本节课的简单的knn模型并不需要复杂的调整参数,如有需求可自行参阅学习其它sklearn 机器学习模型。

图片预处理

场景图像有着截然不同的成像特性如分辨率低、大小不一、光照不均等。这些特性影响着文本定位、词图像分割到字符识别等各个过程。在将场景条件下的文本图像输入到各个模块前,对图像进行必要的预处理,对定位和识别正确率的提高有一定的帮助。常见的图片预处理为图像灰度化,形态学膨胀,形态学腐蚀,图像去噪,图像二值化等。更多数学原理详情请参阅数学形态学资料。

图像灰度化

将彩色图像转化成为灰度图像的过程成为图像的灰度化处理。彩色图像中的每个像素的颜色有R、G、B三个分量决定,而每个分量有255中值可取,这样一个像素点可以有1600多万(255255255)的颜色的变化范围。而灰度图像是R、G、B三个分量相同的一种特殊的彩色图像,其一个像素点的变化范围为255种,所以在数字图像处理种一般先将各种格式的图像转变成灰度图像以使后续的图像的计算量变得少一些。灰度图像的描述与彩色图像一样仍然反映了整幅图像的整体和局部的色度和亮度等级的分布和特征。

灰度空间与RGB颜色空间转换公式:

图像二值化

图像的二值化处理就是将图像上的点的灰度置为0或255,也就是将整个图像呈现出明显的黑白效果。即将256个亮度等级的灰度图像通过适当的阀值选取而获得仍然可以反映图像整体和局部特征的二值化图像。在数字图像处理中,二值图像占有非常重要的地位,特别是在实用的图像处理中,以二值图像处理实现而构成的系统是很多的,要进行二值图像的处理与分析,首先要把灰度图像二值化,得到二值化图像,这样子有利于再对图像做进一步处理时,图像的集合性质只与像素值为0或255的点的位置有关,不再涉及像素的多级值,使处理变得简单,而且数据的处理和压缩量小。为了得到理想的二值图像,一般采用封闭、连通的边界定义不交叠的区域。所有灰度大于或等于阀值的像素被判定为属于特定物体,其灰度值为255表示,否则这些像素点被排除在物体区域以外,灰度值为0,表示背景或者例外的物体区域。

图像膨胀与腐蚀

图像的膨胀与腐蚀作用是为了分割独立的图像元素,以及连接相邻的元素。图像膨胀求局部最大值(极大值),相反地,图像腐蚀就是求局部最小值 (极小值)。

KNN

邻近算法,或者说K最近邻(kNN,k-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。KNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。

上图中,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果 K=3,由于红色三角形所占比例为2/3,绿色圆将被赋予红色三角形那个类,如果K=5,由于蓝色四方形比例为3/5,因此绿色圆被赋予蓝色四方形类。

Sklearn 中的 knn分类器数据结构

KNN分类器的数据结构分为两种kd_tree (K - Dimension tree) 和 ball_tree。这两种树的核心思想是数据的分割。以下将重点介绍KD_tree , 有兴趣的人可学习ball_tee。Kd_tree 来源于BS_tree( binary search tree二叉搜索树)。 如果将BS_tree看做是一维的数据分割,那么KD_tree 可以看成是多维的数据分割,也就是平面分割。以下显示了一个BS_tree。

这个树根节点为 2 , 左子树所有节点数值小于2而右子树大于2。以此类推,左子树第一个节点左边所有节点数值小于 -1 右边节点大于 -1。 右子树同样。重复上诉过程,直到达到树顶。这样做的好处是如果我想获得3,我只用在右枝搜索。

以下显示了一个KD_tree

建立树的思路为 假设我的数据为{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)} 数据只有两维

  1. 分别计算x和 y 方向上的方差,得知x方向方差更大所以选垂直于x轴分裂

  2. 计算数据x方向上的数据的均值,得知均值为 7 所以该节点的分割线为 x = 7

  3. 划分的左子为{(2,3);(5,4);(4,7)}右子为{(9,6);(8,1)}

左子和右子的划分重复1,2两步骤,以此类推。

例如数据结构中有点{(2,3);(5,4);(9,6);(4,7);(8,1);(7,2)},希望搜索点(2,4.5)。

通过第一步我可以获得一个搜索列表 {(7,2);(5,4);(4,7)}

先判断(4,7), 距离为 3.202

再判断(5,4) 以(2,4.5) 为圆心,半径为 3.202 画圆。此圆与 y = 4 相交。跳到(5,4)的左子空间搜索,将(2,3) 加入列表。此时,新列表{(7,2), (2,3)} 另外,(5,4) 与 (2,4,5) 距离为 3.04 < 3.202 所以新圆半径为 3.04

搜索(2,3) 因为(2,3) 为叶节点,所以直接与(2,4.5)计算距离得距离为 1.5

最后,搜索(7,2) 无相交,所以最相邻的点为(2,3)

环境准备

因为树莓派Arm 架构算力不足,我们需要在桌面或移动笔记本端进行模型训练。 以下步骤将讲解如何在Ubuntu16.04下配置我们需要的环境。

配置python

Ubuntu16.04默认安装了python,我们直接使用即可。

安装pip工具

系统命令行输入下面命令: sudo easy_install pip

配置sklearn/Opencv/numpy(pip 安装)

打开Ubuntu命令行输入以下命令。

pip install scipy
pip intstall -U scikit-learn
pip install python-opencv
pip install numpy

你可以使用以下命令查看你的第三方库是否安装

pip list

你也可以使用apt-get等其它方法安装,这里不做介绍,有兴趣的同学可以自行了解学习。

下载MINST数据集

下载地址: http://yann.lecun.com/exdb/mnist/

解压MINST数据集

将其放到本地目录下,这里的目录是:/home/share/mnist_project/

程序流程

我们通过经典的Sklearn Knn分类器的方法来训练Mnist数据集识别模型,最后通过识别程序来验证了该模型结果的正确性。

模型生成流程

识别流程

程序指引

功能函数

将功能函数保存为p1_utils.py

#coding=utf-8
##############################################################
################Author: Xuyang Jie###########################

################Date: 14/03/2018##############################

################Modify: 16/09/2018###########################
##############################################################
import cv2
import numpy
import os
import struct


def swap(array1,array2):
    return array2,array1

def unpack_file(image_file_dir,label_file_dir):
    label = []
    image = []
    counter = 0

    # unpack labels
    file = open(label_file_dir,'rb')
    buf = file.read()
    index = 0
    magic,size = struct.unpack_from('>II',buf,index)
    index += struct.calcsize('>II')
    temp_label = struct.unpack_from('>' + str(size) + 'B',buf,index)

    # unpack images
    file = open(image_file_dir,'rb')
    buf = file.read()
    file.close()
    index = 0
    magic,size,rows,cols = struct.unpack_from('>IIII',buf,index)
    index += struct.calcsize('>IIII')

    for i in range(size):
        temp = struct.unpack_from('>784B',buf,index)
        img_array = numpy.array(temp)
        img = img_array.reshape(28,28)
        image.append(img)
        label.append(temp_label[i])
        index += struct.calcsize('>784B')

    return label,image,size


def quick_sort(myList,img_list,l, r):
    if l >= r:
        return
    stack = []
    stack.append(l)
    stack.append(r)
    while stack:
        low = stack.pop(0)
        high = stack.pop(0)
        if high - low <= 0:
            continue
        x = myList[high]
        i = low - 1
        for j in range(low, high):
            if myList[j] <= x:
                i += 1
                myList[i],myList[j] = swap(myList[i],myList[j])
                img_list[i],img_list[j] = swap(img_list[i],img_list[j])
        myList[i + 1],myList[high] = swap(myList[i + 1],myList[high])
        img_list[i + 1],img_list[high] = swap(img_list[i + 1],img_list[high])
        stack.extend([low, i, i + 2, high])
    return myList,img_list

def QuickSort(myList,img_list,start,end):
    if start < end:
        i,j = start,end
        base = myList[i]
        while i < j:
            while (i < j) and (myList[j] >= base):
                j = j - 1
            myList[i],myList[j] = swap(myList[i],myList[j])
            img_list[i],img_list[j] = swap(img_list[i],img_list[j])
            while (i < j) and (myList[i] <= base):
                i = i + 1
            myList[i],myList[j] = swap(myList[i],myList[j])
            img_list[i],img_list[j] = swap(img_list[i],img_list[j])
        QuickSort(myList,img_list,start, i - 1)
        QuickSort(myList,img_list,j + 1, end)
    return myList,img_list

# quick sorting and save image
def save_image_train(save_path,label,image,size):
    quick_sort(label,image,0,size - 1)
    for i in range(size):
        cv2.imwrite(save_path + '/' + str(i) + '.jpg',image[i])
    return label


def save_label_to_file(save_label_path,total_label):
    file = open(save_label_path, 'w')
    file.write(str(total_label))
    file.close()


def load_label_from_file(save_label_path,total_label):
    file = open(save_label_path,'r')
    buf = file.read()
    for index in range(1,len(buf) - 1,3):
        total_label.append(int(buf[index]))
    return total_label

def load_label_train(start,end,label):
    return label[start:end + 1]
  
# save train/test images
def save_image(save_path,path):
    file = open(path,'rb')
    buf = file.read()
    file.close()
    index = 0
    magic,size,rows,cols = struct.unpack_from('>IIII',buf,index)
    index += struct.calcsize('>IIII')

    for i in range(size):
        temp = struct.unpack_from('>784B',buf,index)
        img_array = numpy.array(temp)
        img = img_array.reshape(28,28)
        cv2.imwrite(save_path + '/'+ str(i) + '.jpg',img)
        index += struct.calcsize('>784B')

# load labels
def load_label(path,rand_index):
    file = open(path,'rb')
    buf = file.read()
    index = 0
    magic,size = struct.unpack_from('>II',buf,index)
    index += struct.calcsize('>II')
    temp_label = []
    label = numpy.zeros(len(rand_index))
    temp_label = struct.unpack_from('>' + str(size) + 'B',buf,index)
    for counter in range(len(rand_index)):
        for i in range(len(temp_label)):
            if rand_index[counter] == i:
                label[counter] = temp_label[i]
                break
    return label

# pre_process, binarization\par
# No need to gray_scale the picture\par
def pre_process(load_path):
    img = cv2.imread(load_path)
    temp_gray_img = img
    gray_img = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY)
    for x in range(28):
        for y in range(28):
            if gray_img[x,y] >= 127:
                gray_img[x,y] = 1
            else:
                gray_img[x,y] = 0
    return gray_img,temp_gray_img

# Load image, train or test, start from 0, allow repeat
def load_image(name,start,end,load_path):
    imglist = []
    img_vec = numpy.zeros([end - start + 1, 28 * 28],int)
    counter = 0
    if name == 'test':
        index = numpy.random.randint(start,high = (end + 1), size = (end - start + 1) )
    else:
        index = [i for i in range(start,end + 1)]
    for parent, dirname, files in os.walk(load_path):
        for file in files:
            pre_process_img,img = pre_process(load_path + '/' + str(index[counter]) + '.jpg')
            imglist.append(img)
            for rows in range(28):
                for cols in range(28):
                    img_vec[counter, rows * 28 + cols] = pre_process_img[rows,cols]
            counter += 1
            if counter == (end - start + 1):
                return img_vec,index,imglist

# check file and path
def check_path(path,work_dir_length,name):
    flag = True
    if name == "source":
        for i in range(len(path)):
            if( os.path.isfile(path[i]) == False ):
                print 'File ' + path[i][work_dir_length + 1:len(path[i])]  + ' is not in your work directory, please check'
                flag = False
    else:
        for i in range(len(path)):
            if( os.path.exists(path[i]) == False ):
                os.makedirs(path[i])
    return flag

训练程序

将训练程序与p1_utils.py放在相同文件夹中。训练程序名字自定。 初始设置的训练集数目多,推荐在ubuntu下训练。你可以改变每一个数字 的训练样本来更改训练集。将从压缩包解压后的文件train-images.idx3-ubyte,train-labels.idx1-ubyte 置于工作目录下 ps:由于总样本数据多,在查看样本照片时请确保机器有足够内存

#coding=utf-8
##############################################################
################Author: Xuyang Jie###########################

################Date: 14/03/2018#############################

################Modify: 16/09/2018###########################
##############################################################

from p1_utils import * 
from sklearn.externals import joblib
from sklearn import neighbors
import os
import time

###########################################################################
##  The index of the training images start from 0 and up to 59999
##  By modifying start and end, you can change the model
##  The default one will be 0 to 59999
############################################################################

# Path config
work_dir = os.getcwd();
train_image_file = work_dir + '/train-images.idx3-ubyte'
train_label_file = work_dir + '/train-labels.idx1-ubyte'
save_train_image_path = work_dir + '/train_image_save'
save_train_label_path = work_dir + '/train_label_save'
model_save_path = work_dir


################################################################
# start: 0,5923,12665,18623,24754,30596,36017,41935,48200,54051
# end: 5922,12664,18622,24753,30595,36016,41934,48199,54050,59999
#################################################################


start = [0,5923,12665,18623,24754,30596,36017,41935,48200,54051] # The first training image index (0 ~ 9)
end = [5922,12664,18622,24753,30595,36016,41934,48199,54050,59999] # The last trainging image index (0 ~ 9)
total_label = []
total_image = []
current_train_image = []
current_train_label = []

if(not check_path([train_image_file,train_label_file],len(work_dir),'source')):
	exit(0)
check_path([save_train_image_path,save_train_label_path,model_save_path],len(work_dir),'save')


if(not os.listdir(save_train_image_path)):
	print 'Save training images'
	save_train_start = time.time()
	total_label, total_image, size = unpack_file(train_image_file,train_label_file)
	total_label = save_image_train(save_train_image_path,total_label,total_image,size)
	save_label_to_file(save_train_label_path + '/label.txt',total_label)
	print('Finish saving training images. Use time: %.2f' % (time.time() - save_train_start))


print 'Load training images and label'
load_train_start = time.time()
if(len(total_label) == 0):
	load_label_from_file(save_train_label_path + '/label.txt',total_label)
for index in range(10):
	temp_img_vec, unuse1, unuse2 = load_image('train',start[index],end[index],save_train_image_path)
	temp_label = load_label_train(start[index],end[index], total_label)
	current_train_label.extend(temp_label)
	current_train_image.extend(temp_img_vec)
print('Finish loading training images and label. Use time: %.2f' % (time.time() - load_train_start))


print 'Train and save model'
train_start = time.time()
knn = neighbors.KNeighborsClassifier(algorithm = 'kd_tree', n_neighbors = 3)
knn.fit(current_train_image,current_train_label)
joblib.dump(knn,model_save_path + '/train_model.mk1')
print('Finish training and saving. Use time: %.2f' % (time.time() - train_start))

识别程序

我们在Ubuntu上训练好模型之后,将解压后的文件t10k-images.idx3-ubyte,t10k-labels.idx1-ubyte和train_model.mk1(训练模型)放到到树莓派工作目录下,本课程目录为树莓派/home/pi/mnist_project/下。保存本文件为mnist_test.py。将本文件与p1_utils.py置于工作目录下。

#coding=utf-8
#############################################################

################Author: Xuyang Jie###########################

################Date: 14/03/2018#############################

################Modify: 16/09/2018###########################

##############################################################
from p1_utils import *
from sklearn.externals import joblib
from sklearn import neighbors
import time
import sys
import os

# Path config
work_dir = os.getcwd()
test_label_file = work_dir + '/t10k-labels.idx1-ubyte'
test_image_file = work_dir + '/t10k-images.idx3-ubyte'
save_test_image_path = work_dir + '/test_image_save'
model_save_path = work_dir

# Files and dir checking
if(int(sys.argv[1]) > 10000 or int(sys.argv[1] <= 0)):
	print 'The number of test images should be 1 ~ 10000'
	exit(0)

if(not check_path([model_save_path + '/train_model.mk1',test_label_file,test_image_file],len(work_dir),'source')):
	exit(1)
check_path([save_test_image_path],len(work_dir),'save')


if(not os.listdir(save_test_image_path)):
	print 'Save test images'
	save_test_start = time.time()
	save_image(save_test_image_path,test_image_file)
	print('Finish saving test images. Use time: %.2f' % (time.time() - save_test_start))


print 'Load test images'
load_start = time.time()
start = numpy.random.randint(0, high = 10000 - int(sys.argv[1]), size = 1)
image_vec, rand_index, img = load_image('test',start[0],start[0] + int(sys.argv[1]) - 1, save_test_image_path)
print('Finish loading images. Use time: %.2f' % (time.time() - load_start))


print 'Load test labels'
load_label_start = time.time()
labels = load_label(test_label_file,rand_index)
print('Finish loading labels. Use time: %.2f' % (time.time() - load_label_start))


print 'Load KNN model'
load_model_start = time.time()
knn = joblib.load(model_save_path + '/train_model.mk1')
print('Finish loading. Use time: %.2f' % (time.time() - load_model_start))


print 'Predict'
predict_start = time.time()
result = knn.predict(image_vec)
print('Finish predicting. Use time: %.2f' % (time.time() - predict_start))


true_num = 0
for i in range(len(result)):
	res = cv2.resize(img[i],(240,180),interpolation = cv2.INTER_CUBIC)
	cv2.putText(res,'Predict:' + str(result[i]),(10,20),cv2.FONT_HERSHEY_SIMPLEX,0.7,(255,0,0),1)
	cv2.imshow('Digits-Recognition',res)
	cv2.waitKey(0)
	if result[i] == labels[i]:
		true_num += 1
print('true_num: %d' % true_num)
accuracy = true_num * 1.0 / int(sys.argv[1]) 
print('Accuracy: %.2f%%' % (accuracy * 100))

在树莓派工作目录下输入以下执行命令:

python mnist_test.py 100

第一个数字参数为希望识别的数字个数,观察执行结果。

扩展阅读

目前常用的训练物体识别模型的方法除了本节课里用到的经典方法之外,还有许多成熟的框架方法,比如:TensorFlow、Caffe等常用深度学习框架,YOLO、darknet模型框架。还有许多Git Hub上开源的模型框架都是我们可以进一步了解和学习的内容。总之,物体识别是一个非常广阔的课题,谷歌、微软、百度、Face++等等多家人工智能公司在这方面有较大优势。

Clone this wiki locally