项目5
PROJECT 5
基于OpenCV和CNN的
手语数字实时翻译
本项目基于Keras深度模型进行手语的分类,通过OpenCV库的相关算法捕捉手部位置,实现视频流及图片的手语实时识别。
5.1总体设计
本部分包括系统整体结构和系统流程。
5.1.1系统整体结构
系统整体结构如图51所示。
图51系统整体结构
5.1.2系统流程
系统流程如图52所示。
图52系统流程
5.2运行环境
本部分包括Python环境、TensorFlow环境、Keras环境和Android环境。
5.2.1Python环境
需要Python 3.6及以上配置,在清华镜像源中下载Anaconda并完成Python所需的配置,下载地址为https://mirrors.tuna.tsinghua.edu.cn/。
5.2.2TensorFlow环境
更换Anaconda镜像源,打开cmd直接依次输入下列命令,如图53所示。
图53输入的命令行指令
在Anaconda可视化界面的相应环境中删除默认镜像源。
在Anaconda相应环境的可视化包管理中搜索TensorFlow,选择1.12.0版本。
5.2.3Keras环境
在Anaconda相应环境的可视化包管理中搜索Keras相关包,选择与TensorFlow相对应的2.2.4版本。
5.2.4Android环境
本部分包括安装Android Studio、导入TensorFlow的jar包和so库。
1. 安装Android Studio
安装参考教程地址为https://developer.android.google.cn/studio/install.html。新建Android项目,打开Android Studio,选择菜单项File→New→New Project→Empty Activity→Next。
Name可自行定义; Save location为项目保存的地址,可自行定义; Minimum API为该项目能够兼容Android手机的最低版本,大于18即可。单击Finish按钮完成。
2. 导入TensorFlow的jar包和so库
下载地址为https://github.com/PanJinquan/MnisttensorFlowAndroidDemo/tree/master/app/libs。
在/app/libs下新建armeabiv7a文件夹,添加libtensorflow_inference.so; 将libandroid_tensorflow_inference_java.jar放在/app/libs下,右击add as Library。
app\build.gradle配置,在defaultConfig中添加:
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a"
}
在Android节点下添加sourceSets,用于制定jniLibs的路径:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
若没有dependencies,则增加TensorFlow编译的jar文件并导入:
implementation files('libs/libandroid_tensorflow_inference_java.jar')。
3. 导入OpenCV库
进入OpenCV官网https://opencv.org/,下载相应版本的安卓包并完成解压,如图54所示。
图54OpenCV官网界面
在Android Studio菜单中单击File→New→Import Module,选择安卓包中的sdk文件夹,如图55和图56所示。
图55Android Studio菜单界面
图56OpenCV sdk文件夹内容
单击菜单项File→Project Structure,选择Dependencies,在Modules栏下选择APP,单击左数第3栏中的【+】图标,选择Module Dependency,单击OK按钮退出。
打开根目录下的build.gradle文件,记下compileSdkVersionbuildToolsVersion、minSdkVersion和targetSdkVersion。单击sdk,打开根目录下的build.gradle文件,把文件中compileSdkVersion、buildToolsVersion、minSdkVersion和targetSdkVersion后的数值改成与APP中相同的文件,如图57所示。
图57Android Studio Project Structure界面图
在app/src/main下新建文件夹jniLibs,将OpenCVandroidsdk的sdk/native/libs下的所有文件复制到jniLibs下。
5.3模块实现
本项目包括6个模块: 数据预处理、数据增强、模型构建、模型训练及保存、模型评估和模型测试,下面分别介绍各模块的功能及相关代码。
5.3.1数据预处理
在Kaggle上下载相应的数据集,下载地址为https://www.kaggle.com/ardamavi/signlanguagedigitsdataset。
加载在本地文件夹中下载的数据集,相关代码如下:
#导入相应包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from keras.models import Sequential
from keras import layers
from keras import optimizers
from sklearn.model_selection import train_test_split
import os
#打印文件夹有关信息
print(os.listdir("/Users/chenjiyan/Desktop/信息系统设计项目/Sign-language-digits-dataset"))
#加载数据集
X=np.load("/Users/chenjiyan/Desktop/信息系统设计项目/Sign-language-digits-dataset/X.npy")
y=np.load("/Users/chenjiyan/Desktop/信息系统设计项目/Sign-language-digits-dataset/Y.npy")
print("The dataset loaded...")
#定义数据概览所需函数
#初始数据概览
#代码改自https://www.kaggle.com/serkanpeldek/cnn-practices-on-sign-language-digits
#独热标签解码
def decode_OneHotEncoding(label):
label_new=list()
for target in label:
label_new.append(np.argmax(target))#选择最大元素(即值为1)的索引
label=np.array(label_new)
return label
#因为原数据集标签标注有误,所以需要纠正数据集错误标签
def correct_mismatches(label):
label_map={0:9,1:0,2:7,3:6,4:1,5:8,6:4,7:3,8:2,9:5} #正确标签映射列表
label_new=list()
for s in label:
label_new.append(label_map[s])
label_new=np.array(label_new)
return label_new
#显示图像类别
def show_image_classes(image, label, n=10):
label=decode_OneHotEncoding(label)
label=correct_mismatches(label)
fig, axarr=plt.subplots(nrows=n, ncols=n, figsize=(18, 18))
axarr=axarr.flatten()
plt_id=0
start_index=0
for sign in range(10):
sign_indexes=np.where(label==sign)[0]
for i in range(n):
#逐行打印0~9的手语图片
image_index=sign_indexes[i]
axarr[plt_id].imshow(image[image_index], cmap='gray')
axarr[plt_id].set_xticks([])
axarr[plt_id].set_yticks([])
axarr[plt_id].set_title("Sign :{}".format(sign))
plt_id=plt_id+1
plt.suptitle("{} Sample for Each Classes".format(n))
plt.show()
number_of_pixels=X.shape[1]*X.shape[2]
number_of_classes=y.shape[1]
print(20*"*", "SUMMARY of the DATASET",20*"*")
print("an image size:{}x{}".format(X.shape[1], X.shape[2]))#获取图片像素大小
print("number of pixels:",number_of_pixels)
print("number of classes:",number_of_classes)
y_decoded=decode_OneHotEncoding(y.copy()) #标签解码
sample_per_class=np.unique(y_decoded, return_counts=True)
print("Number of Samples:{}".format(X.shape[0]))
for sign, number_of_sample in zip(sample_per_class[0], sample_per_class[1]):
print("{} sign has {} samples.".format(sign, number_of_sample))
print(65*"*")
show_image_classes(image=X, label=y.copy())
数据集预览效果如图58所示。
图58数据集预览效果
5.3.2数据增强
为方便展示生成图片的效果及对参数进行微调,本项目未使用keras直接训练生成器,而是先生成一个增强过后的数据集,再应用于模型训练。
在数据增强中,首先,定义一个图片生成器; 其次,通过生成器的flow()方法迭代进行数据增强。相关代码如下:
from keras.preprocessing.image import ImageDataGenerator
X_loaded = X.reshape(X.shape+(1,))
print("shape of X_loaded:",X_loaded.shape)
#定义图片生成器
datagen = ImageDataGenerator(featurewise_center=False,
#使数据集中心化, 按特征执行
featurewise_std_normalization=False,#使输入数据的每个样本均值为0
rotation_range=20,#设定旋转角度
width_shift_range=0.2, height_shift_range=0.2,
#设定随机水平及垂直位移的幅度
brightness_range=[0.1, 1.3],#亮度调整
horizontal_flip=False) #设定不发生水平镜像
#迭代进行数据增强输出
X_added=X_loaded[0]
y_added=y[0]
X_added = X_added.reshape((1,)+X_added.shape)#改变输入维数
print("shape of X_added:",X_added.shape)
i = 0
for batch in datagen.flow(X_loaded,y, batch_size=11, shuffle=True, seed=None):if i==0: print("shape of X in each batch:",batch[0].shape,"\n","shape of y in each batch:",batch[1].shape)
X_added=np.vstack((X_added,batch[0]))#添加图片
y_added=np.vstack((y_added,batch[1]))#添加标签
i += 1
if i%100==0:print("process:",i,"/",X_loaded.shape[0])#输出处理进度
if i >= X_loaded.shape[0]:# 生成器退出循环,生成数据总量为原来的批尺寸倍
break
X_added=np.vstack((X_added,X_loaded))
#最后添加原数据,此时生成数据为原来的批尺寸+1倍
y_added=np.vstack((y_added,y))
print("shape of X_added:",X_added.shape)
print("shape of y_added:",y_added.shape)
数据增强过程如图59所示。
图59数据增强过程
数据预览效果如图510所示。
图510数据预览效果
5.3.3模型构建
数据加载进模型之后,需要定义模型结构,并优化损失函数。
1. 定义模型结构
本次使用的卷积神经网络由四个卷积块及后接的全连接层组成,每个卷积块包含一个卷积层,并后接一个最大池化层进行数据的降维处理。为防止梯度消失以及梯度爆炸,进行了数据批量归一化,并设置丢弃正则化。相关代码如下:
#模型改自https://www.kaggle.com/serkanpeldek/cnn-practices-on-sign-language-digits
def build_conv_model_8():
model = Sequential()
model.add(layers.Convolution2D(32, (3, 3), activation='relu', input_shape=(64, 64, 1)))
model.add(layers.MaxPooling2D((2, 2))) #最大池化层
model.add(layers.BatchNormalization()) #批量归一化
model.add(layers.Dropout(0.25)) #随机丢弃节点
model.add(layers.Convolution2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.BatchNormalization())
model.add(layers.Dropout(0.25))
model.add(layers.Convolution2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.BatchNormalization())
model.add(layers.Dropout(0.25))
model.add(layers.Convolution2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.BatchNormalization())
model.add(layers.Dropout(0.25))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
return model
model=build_conv_model_8()
model.summary()
2. 优化损失函数
确定模型架构之后进行编译。这是多类别的分类问题,需要使用交叉熵作为损失函数。由于所有标签都带有相似的权重,经常使用精确度作为性能指标。RMSprop是常用的梯度下降方法,本项目将使用该方法优化模型参数。相关代码如下:
optimizer=optimizers.RMSprop(lr=0.0001)#优化器
model.compile(loss="categorical_crossentropy", optimizer=optimizer, metrics=['accuracy'])
5.3.4模型训练及保存
本部分包括模型训练和模型保存的相关代码。
1. 模型训练
定义模型结构后,通过训练集训练模型,使模型能够识别手语数字。此处将使用训练集、验证集和测试集用于拟合并保存模型。在训练模型过程中,为防止训练过度造成的模型准确度下降,还使用了early stopping技术在一定条件下提前终止训练模型。相关代码如下:
from keras.callbacks import EarlyStopping
def split_dataset(X, y, test_size=0.3, random_state=42):#分割数据集
X_conv=X.reshape(X.shape[0], X.shape[1], X.shape[2],1)
return train_test_split(X_conv,y, stratify=y,test_size=test_size,random_state=random_state)
callbacks=None
X_train, X_validation, y_train, y_validation = split_dataset(X_added, y_added)
X_validation, X_test, y_validation, y_test = split_dataset(X_validation, y_validation)
#epochs=80
earlyStopping = EarlyStopping(monitor = 'val_loss', patience=20, verbose = 1)
if callbacks is None:
callbacks = [earlyStopping]
#模型训练
#history = LossHistory()
history = model.fit(X_train, y_train,
validation_data=(X_validation, y_validation),
callbacks=[earlyStopping],
epochs=80,
verbose=1)
test_scores=model.evaluate(X_test, y_test, verbose=0) #模型评估
train_scores=model.evaluate(X_validation, y_validation, verbose=0)
print("[INFO]:Train Accuracy:{:.3f}".format(train_scores[1]))
print("[INFO]:Validation Accuracy:{:.3f}".format(test_scores[1]))
print(plt.plot(history.history["acc"]))
print(plt.plot(history.history["val_acc"]))
from sklearn.metrics import confusion_matrix
#生成混淆矩阵
X_CM=np.reshape(X_test,(X_test.shape[0],64,64,1))
y_pred=model.predict(X_CM) #使用整个数据集的数据进行评估
y_ture=decode_OneHotEncoding(y_test) #独热编码的解码
y_ture=correct_mismatches(y_ture) #图像标签的修正
y_pred=decode_OneHotEncoding(y_pred)
y_pred=correct_mismatches(y_pred)
confusion_matrix(y_ture, y_pred) #绘制混淆矩阵
训练过程如图511所示。
图511训练过程
2. 模型保存
为使训练的模型能够应用于Android Studio工程,将模型保存为.pb格式。相关代码如下:
from keras.models import Model
from keras.layers import *
from keras.models import load_model
import os
import tensorflow as tf
def keras_to_tensorflow(keras_model, output_dir, model_name,out_prefix="output_", log_tensorboard=True):
#如果目的路径不存在则新建目的路径
if os.path.exists(output_dir) == False:
os.mkdir(output_dir)
#根据Keras模型构建TensorFlow模型
out_nodes = []
for i in range(len(keras_model.outputs)):
out_nodes.append(out_prefix+str(i+1))
tf.identity(keras_model.output[i],out_prefix+str(i+ 1))
#将TensorFlow模型写入目标文件
sess=K.get_session()
from tensorflow.python.framework import graph_util, graph_io
init_graph=sess.graph.as_graph_def()main_graph=graph_util.convert_variables_to_constants(sess,init_graph,out_nodes)
graph_io.write_graph(main_graph,output_dir,name=model_name,as_text=False)
#展示相关信息
if log_tensorboard:
from tensorflow.python.tools import import_pb_to_tensorboardimport_pb_to_tensorboard.import_to_tensorboard(os.path.join(output_dir,model_name),output_dir)
output_dir="/Users/chenjiyan/Desktop/信息系统设计项目" #目的路径
keras_to_tensorflow(model,output_dir=output_dir,model_name="trained_model_imageDataGenerator.pb")
print("MODEL SAVED")
5.3.5模型评估
由于网络上缺乏手语识别相关模型,为方便在多种模型中选择最优模型,以及进行模型的调优,模型应用于安卓工程之前,需要先在PC设备上使用Python文件进行初步的运行测试,以便验证本方案的手语识别策略是否可行并选择最优的分类模型。具体步骤如下。
(1) 定义皮肤粒子的识别函数,在原图中将不符合肤色检测阈值的区域涂黑。相关代码如下:
#导入相应包
import cv2
import numpy as np
import keras
from keras.models import load_model
#肤色识别,函数引用自https://blog.csdn.net/qq_23149979/article/details/88569979
def skin(frame):
lower = np.array([0, 40, 80], dtype="uint8")
upper = np.array([20, 255, 255], dtype="uint8")
converted = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
skinMask = cv2.inRange(converted, lower, upper)#构建提取阈值
skinMask = cv2.GaussianBlur(skinMask, (5, 5), 0)
skin = cv2.bitwise_and(frame, frame, mask=skinMask)#将不满足条件的区域涂黑
return skin
(2) 打开本地摄像头权限,加载训练好的模型,在while()函数中设定识别手部区域的时间间隔。
(3) 使用肤色进行轮廓提取,将提取到的区域进行高斯滤波及二值化,并使用findContour()函数进行轮廓提取。对比每个轮廓大小,并忽略面积小于阈值的连通域。
(4) 使用boundingRect()函数提取原图的手部区域后,将提取到的区域送至训练好的模型进行分类。相关代码如下:
#主函数
def main():
capture = cv2.VideoCapture(0)
#model = load_model("/Users/chenjiyan/Desktop/信息系统设计项目/trained_model_ResNet.h5") #加载模型
model = load_model("/Users/chenjiyan/Desktop/信息系统设计项目/trained_model_2.h5")
#加载模型
iteator=0
while capture.isOpened():
iteator=iteator+1
if iteator>1000 :
iteator=0
pressed_key = cv2.waitKey(1)
_, frame1 = capture.read()
frame1=cv2.flip(frame1,1)
#显示摄像头
#cv2.imshow('Original',frame1)
#皮肤粒子识别
frame = skin(frame1)
#灰度
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
#高斯滤波
frame = cv2.GaussianBlur(frame, (5, 5), 0)
#二值化
ret, frame = cv2.threshold(frame, 50, 255, cv2.THRESH_BINARY)
#轮廓
_,contours,hierarchy = cv2.findContours(frame,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
#print("number of contours:%d" % len(contours))
cv2.drawContours(frame, contours, -1, (0, 255, 255), 2)
#找到最大区域并填充
area = []
for i in range(len(contours)):
area.append(cv2.contourArea(contours[i]))
max_idx = np.argmax(area)
for i in range(max_idx - 1):
cv2.fillConvexPoly(frame, contours[max_idx - 1], 0)
cv2.fillConvexPoly(frame, contours[max_idx], 255)
#处理后显示
x, y, w, h = cv2.boundingRect(contours[max_idx])
if x>20 :x=x-20
else :x=0
if y>20 :y=y-20
else :y=0
h=h+30
w=w+50
cv2.rectangle(frame1,(x,y),(x+w, y+h),(0,255,0), 2)
if iteator%5==0 :
#模型预测
chepai_raw = frame1[y:y + h, x:x + w] #提取识别的矩形区域
chepai=cv2.flip(chepai_raw,1) #水平镜像翻转
cv2.imshow("Live",chepai)#显示输入图像
chepai=cv2.resize(chepai,(64,64),interpolation=cv2.INTER_CUBIC)
#chepai = np.array(chepai)
chepai=cv2.cvtColor(chepai,cv2.COLOR_RGB2GRAY) #转换为灰度图片
chepai=chepai/255
chepai=np.reshape(chepai,(1,64,64,1))
label_map={0:9,1:0, 2:7, 3:6, 4:1, 5:8, 6:4, 7:3, 8:2, 9:5}
#result=model.predict_classes(chepai)
#由于未使用model=Sequential()序列化模型,故不能使用predict_classes
result = model.predict(chepai)
result=np.argmax(result,axis=1)
print(label_map[result[0]])
#显示图像
#cv2.imshow("Live",frame) #轮廓
cv2.imshow('Original',frame1)#原始图像
if pressed_key == 27:
break
cv2.destroyAllWindows()
capture.release()
5.3.6模型测试
评估整体模型可行性后,将手语识别模型应用于Android Studio工程中,完成APP。具体步骤如下。
1. 权限注册
首先,为分别实现在视频和相片中识别手语,注册两个activity活动; 其次,实现调用摄像头,在AndroidManifest.xml中进行摄像头权限及存储权限的申请。为了访问SD卡,注册FileProvider; 最后规定程序入口以及启动方式。相关代码如下:
android:label="手语实时识别">
2. 模型导入
模型导入相关操作如下。
(1) 将训练好的.pb文件放入app/src/main/assets下。若不存在assets目录,则右击菜单项main→new→Directory,输入assets创建目录。
(2) 新建类PredictionTF.java,在该类中加载so库,调用TensorFlow模型得到预测结果。
(3) 在MainActivity.java中声明模型存放路径,建立PredictTF对象,调用PredictionTF类,并输入相应类进行应用。相关代码如下:
//加载模型
String MODEL_FILE = "file:///android_asset/trained_model_imageDataGenerator.pb";//模型地址
PredictTF tf =new PredictTF(getAssets(),MODEL_FILE);
(4) 在MainActivity.java的onCreate()方法中加载OpenCV库,相关代码如下:
//加载OpenCV库
private void staticLoadCVLibraries(){
boolean load = OpenCVLoader.initDebug();
if(load) {
Log.i("CV", "Open CV Libraries loaded...");
}
}
3. 总体模型构建
在PredictTF内构建预测所需的相应函数,具体步骤如下。
(1) 加载so库并声明所需的属性,相关代码如下:
private static final String TAG = "PredictTF";
TensorFlowInferenceInterface tf;
static {//加载libtensorflow_inference.so库文件
System.loadLibrary("tensorflow_inference");
Log.e(TAG, "libtensorflow_inference.so库加载成功");
}
//PATH TO OUR MODEL FILE AND NAMES OF THE INPUT AND OUTPUT NODES
//各节点名称
private String INPUT_NAME = "conv2d_1_input";
private String OUTPUT_NAME = "output_1";
float[] PREDICTIONS = new float[10]; //手语分类模型的输出
private int[] INPUT_SIZE = {64, 64, 1}; //模型的输入形状
(2) 使用OpenCV进行手部位置的捕捉。
将不符合肤色检测阈值的区域涂黑,相关代码如下:
//肤色识别
private Mat skin(Mat frame) {
int iLowH = 0;
int iHighH = 20;
int iLowS = 40;
int iHighS = 255;
int iLowV = 80;
int iHighV = 255;
Mat hsv=new Mat();
Imgproc.cvtColor(frame,hsv,Imgproc.COLOR_RGBA2BGR); //将输入图片转为灰度图
Imgproc.cvtColor(hsv,hsv,Imgproc.COLOR_BGR2HSV);
Mat skinMask=new Mat();
Core.inRange(hsv, new Scalar(iLowH, iLowS, iLowV), new Scalar(iHighH, iHighS, iHighV),skinMask);
Imgproc.GaussianBlur(skinMask,skinMask,new Size(5,5),0);//高斯滤波
Mat skin=new Mat();
bitwise_and(frame,frame,skin,skinMask);//将不符合肤色阈值的区域涂黑
return skin;
}
在predict函数中将提取到的区域进行高斯滤波以及二值化,并使用findContour函数进行轮廓提取。对比轮廓大小,忽略面积小于阈值的连通域。最后使用boundingRect函数提取原图。相关代码如下:
//载入位图并转为OpenCV的Mat对象
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
Mat frame = new Mat();
if (imagePath == null) {System.out.print("imagePath is null");}
Utils.bitmapToMat(bitmap, frame);
Core.flip(frame,frame,1); //水平镜像翻转
//图片预处理
frame=skin(frame);//肤色识别
//Mat frame1=new Mat();
Imgproc.cvtColor(frame,frame,Imgproc.COLOR_BGR2GRAY); //转换为灰度图
Mat frame1=new Mat();
Imgproc.GaussianBlur(frame,frame1,new Size(5,5),0);//高斯滤波
double res=Imgproc.threshold(frame1, frame1,50, 255, Imgproc.THRESH_BINARY);
//二值化
//Imgproc.cvtColor(frame,frame,);
//提取所有轮廓
List contours=new ArrayList();
Mat hierarchy = new Mat();
Imgproc.findContours(frame1, contours, hierarchy,
Imgproc.RETR_EXTERNAL,
Imgproc.CHAIN_APPROX_SIMPLE); //轮廓提取
//找到最大区域并填充
int max_idx= 0;
List area=new ArrayList<>();
for (int i=0;i < contours.size();i++) {
area.add(Imgproc.contourArea((contours).get(i)));
//max_idx = area.indexOf(Collections.max(area));
}
max_idx = area.indexOf(Collections.max(area));
//得到矩形
Rect rect= Imgproc.boundingRect(contours.get(max_idx));
//提取相关区域
String mess;
mess= String.valueOf(frame.channels());
Log.i("CV", "the type of frame is:"+mess);
Mat chepai_raw = new Mat(frame,rect);//提取相关区域
Mat cheapi=new Mat();
Core.flip(chepai_raw,cheapi,1); //水平镜面反转
(3) 将OpenCV提取到的区域输入至手语分类模型中。
将提取到的位图转换成一维数组以供模型输入,相关代码如下:
Bitmap input = Bitmap.createBitmap(cheapi.cols(),
cheapi.rows(),
Bitmap.Config.ARGB_8888);
Utils.matToBitmap(cheapi,input);
float[] input_data=bitmapToFloatArray(input,64,64);
Bitmap.createBitmap()函数定义相关代码如下:
//将位图转换为一维浮点数组
//本函数修改自https://blog.csdn.net/chaofeili/article/details/89374324
public static float[] bitmapToFloatArray(Bitmap bitmap,int rx, int ry){
int height = bitmap.getHeight();
int width = bitmap.getWidth();
//计算缩放比例
float scaleWidth = ((float) rx) / width;
float scaleHeight = ((float) ry) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
bitmap=Bitmap.createBitmap(bitmap,0,0,width,height, matrix, true);
height = bitmap.getHeight();
width = bitmap.getWidth();
float[] result = new float[height*width];
int k = 0;
//行优先
for(int j = 0;j < height;j++){
for (int i = 0;i < width;i++){
int argb = bitmap.getPixel(i,j);
int r = Color.red(argb);
int g = Color.green(argb);
int b = Color.blue(argb);
int a = Color.alpha(argb);
//由于是灰度图,所以r,g,b分量是相等的
assert(r==g && g==b);
//Log.i(TAG,i+","+j+":argb = "+argb+", a="+a+", r="+r+", g="+g+", b="+b);
result[k++] = r / 255.0f;
}
}
return result;
}
由于原数据集存在标签错误的情况,需要为标签纠错提供正确的映射字典。相关代码如下:
Map label_map=new HashMap();
label_map.put(0,9);label_map.put(1,0);label_map.put(2,7);
label_map.put(3,6);label_map.put(4,1);
label_map.put(5,8);label_map.put(6,4);
label_map.put(7,3);label_map.put(8,2);label_map.put(9,5); //标签纠错
开始应用模型并得到预测的结果,相关代码如下:
//开始应用模型
tf = new TensorFlowInferenceInterface(getAssets(),MODEL_PATH); //加载模型
tf.feed(INPUT_NAME, input_data, 1, 64, 64, 1);
tf.run(new String[]{OUTPUT_NAME});
//将结果复制到预测矩阵
tf.fetch(OUTPUT_NAME,PREDICTIONS);
//获取最高预测结果
Object[] results = argmax(PREDICTIONS);//找到预测置信度最大的类作为分类结果
int class_index = (Integer) results[0];
float confidence = (Float) results[1];
/*try{
final String conf = String.valueOf(confidence * 100).substring(0,5);
//将分类索引转换为实际的标签名
final String label = ImageUtils.getLabel(getAssets().open("labels.json"),class_index);
//在UI(用户界面)上显示结果
runOnUiThread(new Runnable() {
@Override
public void run() {
resultView.setText(label + " : " + conf + "%");
}
});
} catch (Exception e){
}//*/
int label=label_map.get(class_index); //标签纠错
results [0]=label;
预测函数相关代码如下:
private Object[] perdict (Bitmap bitmap){
//载入位图并转为OpenCV的Mat对象
//Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
Mat frame = new Mat();
//if (imagePath == null) {System.out.print("imagePath is null");}
Utils.bitmapToMat(bitmap, frame);
//Core.flip(frame,frame,1); //水平镜像翻转
//图片预处理
Mat frame2=new Mat();
frame2=skin(frame);//肤色识别
Imgproc.cvtColor(frame2,frame2,Imgproc.COLOR_BGR2GRAY);//转换为灰度图
Mat frame1=new Mat();
Imgproc.GaussianBlur(frame2,frame1,new Size(5,5),0);//高斯滤波
double res=Imgproc.threshold(frame1, frame1,50, 255, Imgproc.THRESH_BINARY);//二值化
//提取所有轮廓
List contours=new ArrayList();
Mat hierarchy = new Mat();
Imgproc.findContours(frame1, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); //轮廓提取
//找到最大区域并填充
int max_idx= 0;
List area=new ArrayList<>();
for (int i=0;i < contours.size();i++) {
area.add(Imgproc.contourArea((contours).get(i)));
}
max_idx = area.indexOf(Collections.max(area));
//得到矩形
Rect rect= Imgproc.boundingRect(contours.get(max_idx));
//提取相关区域
String mess;
Imgproc.cvtColor(frame,frame,Imgproc.COLOR_BGR2GRAY); //将原图转换为灰度图
mess= String.valueOf(frame.channels());
Log.i("CV", "the type of frame is:"+mess);
Mat chepai_raw = new Mat(frame,rect);
mess= String.valueOf(chepai_raw.channels());
Log.i("CV", "the type of chepai_raw is:"+mess);
Mat cheapi=new Mat();
Core.flip(chepai_raw,cheapi,1); //水平镜面反转
//将提取的区域转为一维浮点数组输入模型
Bitmap input = Bitmap.createBitmap(cheapi.cols(), cheapi.rows(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(cheapi,input);
float[] input_data=bitmapToFloatArray(input,64,64);
Map label_map=new HashMap();
//{0:9,1:0, 2:7, 3:6, 4:1, 5:8, 6:4, 7:3, 8:2, 9:5};
label_map.put(0,9);label_map.put(1,0);label_map.put(2,7);label_map.put(3,6);label_map.put(4,1);
label_map.put(5,8);label_map.put(6,4);label_map.put(7,3);label_map.put(8,2);label_map.put(9,5); //标签纠错
//开始应用模型
tf=new TensorFlowInferenceInterface(getAssets(),MODEL_PATH);//加载模型
tf.feed(INPUT_NAME, input_data, 1, 64, 64, 1);
tf.run(new String[]{OUTPUT_NAME});
//copy the output into the PREDICTIONS array
tf.fetch(OUTPUT_NAME,PREDICTIONS);
//选择预测结果中置信度最大的类别
Object[] results = argmax(PREDICTIONS);
int class_index = (Integer) results[0];
float confidence = (Float) results[1];
/*try{
final String conf = String.valueOf(confidence * 100).substring(0,5);
//将预测的结果转换为0~9的标签
final String label = ImageUtils.getLabel(getAssets().open("labels.json"),class_index);
//显示结果
runOnUiThread(new Runnable() {
@Override
public void run() {
resultView.setText(label + " : " + conf + "%");
}
});
} catch (Exception e){
}
int label=label_map.get(class_index); //标签纠错
results [0]=label;
return results;
}
4. 处理视频中的预览帧数据
处理预览帧是对相机预览时的每一帧数据进行处理,识别特定帧中的手语类型。具体步骤如下。
(1) 新建处理预览帧所需的CameraPreview类和ProcessWithThreadPool类
(2) 绑定开始预览相机视频时的事件
修改mainActivity,在新建的方法startPreview()中添加初始化相机预览的代码,最后在stopPreview()方法中停止相机预览。通过removeAllViews()方法将相机预览移除时,将触发CameraPreview类中的相关结束方法,关闭相机预览。相关代码如下:
public void startPreview() {
//加载模型
StringMODEL_FILE="file:///android_asset/trained_model_imageDataGenerator.pb";
//模型地址
PredictTF tf =new PredictTF(getAssets(),MODEL_FILE);
//新建相机预览对象
final CameraPreview mPreview = new CameraPreview(this,tf);
//新建可视布局
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
//调用并初始化摄像头
SettingsFragment.passCamera(mPreview.getCameraInstance());
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
SettingsFragment.setDefault(PreferenceManager.getDefaultSharedPreferences(this));SettingsFragment.init(PreferenceManager.getDefaultSharedPreferences(this));
//设置开始按钮监听器
Button buttonSettings = (Button) findViewById(R.id.button_settings);
buttonSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getFragmentManager().beginTransaction().replace(R.id.camera_preview, new SettingsFragment()).addToBackStack(null).commit();
}
});
}
public void stopPreview() {
//移除相机预览界面
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.removeAllViews();
}
在mainActivity中,onCreate()方法中使用两个按钮绑定上述方法。相关代码如下:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//加载OpenCV库
staticLoadCVLibraries();
//绑定相机预览开始按钮
Button buttonStartPreview = (Button) findViewById(R.id.button_start_preview);
buttonStartPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startPreview();
}
});
//绑定相机预览停止按钮
Button buttonStopPreview = (Button) findViewById(R.id.button_stop_preview);
buttonStopPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stopPreview();
}
});
}
(3) 实时处理帧数据
在CameraPreview类中新增Camera.PreviewCallback接口声明,以实现此接口下的onPreviewFrame()方法。该方法用于获取每一帧的数据。相关代码如下:
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback{}
实现onPreviewFrame()方法,调用PredictTF类进行手语的识别。相关代码如下:
public void onPreviewFrame(byte[] data, Camera camera) {
switch (processType) {
case PROCESS_WITH_HANDLER_THREAD:
processFrameHandler.obtainMessage(ProcessWithHandlerThread.WHAT_PROCESS_FRAME, data).sendToTarget();
break;
case PROCESS_WITH_QUEUE:
try {
frameQueue.put(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
case PROCESS_WITH_ASYNC_TASK:
new ProcessWithAsyncTask().execute(data);
break;
case PROCESS_WITH_THREAD_POOL:
processFrameThreadPool.post(data);
try {
//延时3s处理
Thread.sleep(500);//手掌位置捕捉的延时
NV21ToBitmap transform = new NV21ToBitmap(getContext().getApplicationContext());
Bitmap bitmap= transform.nv21ToBitmap(data,1920,1080);
String num;
Object[] results=tf.perdict(bitmap);
if (results!=null) {
num = "result:" + results[0].toString() + "confidence:" + results[1].toString();
Toast.makeText(getContext().getApplicationContext(), num, Toast.LENGTH_SHORT).show();
Thread.sleep(3000);//如果监测到有效手语,则进行延时
}
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
default:
throw new IllegalStateException("Unexpected value: " + processType);
//*/
}
}
在surfaceCreated()中getCameraInstance()下添加有关代码,将此接口绑定到mCamera,每当有预览帧生成,就会调用onPreviewFrame()。相关代码如下:
mCamera.setPreviewCallback(this);
5. 处理图片数据
本部分包括调用摄像头获取图片、从相册中选择图片、对图片进行预测和显示。
(1) 调用摄像头获取图片。
file对象用于存放摄像头拍摄的照片,存放在当前应用缓存数据的位置,并调用getUriForFile()方法获取此目录。
为了兼容低版本,随后进行判断: 如果系统版本低于Android 7.0,则调用Uri的formFlie()方法将File对象转为Uri对象; 否则调用FileProvider对象的getUriForFile()方法将File对象转换成Uri对象。
构建Intent对象,将action指定为android.media.action.IMAGE_CAPTURE,再调用putExtra()方法指定图片的输出地址,调用startActivityForResul()方法打开相机程序,成功后返回TAKE_PHOTO进行下一步处理。相关代码如下:
//创建File对象,用于存储拍照后的图片
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT < 24) {
imageUri = Uri.fromFile(outputImage);
} else {
imageUri=FileProvider.getUriForFile(Second.this,"com.example.cameraapp.fileprovider", outputImage);
}
//启动相机程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
新增file_paths.xml文件,相关代码如下:
在Androidfest.xml文件中注册fileProvider,相关代码如下:
(2) 从相册中选择图片。
在AndroidManifest.xml文件中申请WRITE_EXTERNAL_STORAGE权限,因为从相册中选择图片需要访问到手机的SD卡数据。
创建Intent对象,将action指定为android.intent.action.GET_CONTENT,调用startActivityForResul()方法打开手机相册,成功后返回CHOOSE_PHOTO用于下一步处理。相关代码如下:
private void openAlbum() {
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PHOTO); // 打开相册
}
为了兼容低版本,用handleImageOnKitKat()和handleImageBeforeKitKat()函数分别对4.4以上及4.4版本以下手机的图片进行处理。
handleImageOnKitKat()用于解析之前封装好的统一资源标识符(Uri)对象,而handleImageBeforeKitKat()函数的Uri由于未进行封装,直接将Uri对象传入getImagePath()函数即可。相关代码如下:
private void handleImageOnKitKat(Intent data) {
String imagePath = null;
Uri uri = data.getData();
Log.d("TAG", "handleImageOnKitKat: uri is " + uri);
if (DocumentsContract.isDocumentUri(this, uri)) {
// 如果是文件类型的Uri,则通过文件ID处理
String docId = DocumentsContract.getDocumentId(uri);
if("com.android.providers.media.documents".equals(uri.getAuthority())) {
String id = docId.split(":")[1]; //解析数字格式的ID
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
}
else
if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
Uri contentUri=ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
//如果是内容类型的Uri,则使用普通方式处理
imagePath = getImagePath(uri, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
//如果是文件类型的Uri,直接获取图片路径即可
imagePath = uri.getPath();
}
displayImage(imagePath); //根据图片路径显示图片
}
private void handleImageBeforeKitKat(Intent data) {
Uri uri = data.getData();
String imagePath = getImagePath(uri, null);
displayImage(imagePath);
}
(3) 对图片进行预测和显示。
使用PredictTF对象相关方法预测出从相机或相册中获取的图片位图对象中的手势类型,并使用setImageBitmap函数显示。相关代码如下:
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode,resultCode, data);
switch (requestCode)
{
case TAKE_PHOTO:
if (resultCode == RESULT_OK) {
try {//将拍摄的照片显示出来并进行识别
Bitmapbitmap=BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
//识别图片
Object[] results=tf.perdict(bitmap);
String num;
num ="result:"+results[0].toString()+"confidence:"+results[1].toString();
Toast.makeText(Second.this, num , Toast.LENGTH_SHORT).show();
//展示图片
picture.setImageBitmap(bitmap);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK) {
//判断手机系统版本号
if (Build.VERSION.SDK_INT >= 19) {
//4.4及以上系统使用该方法处理图片
handleImageOnKitKat(data);
} else {
//4.4以下系统使用该方法处理图片
handleImageBeforeKitKat(data);
}
}
break;
default:
break;
}
}
6. 多页面设置
创建完使用视频和图片进行预测的类别后,在AndroidManifest.xml文件中注册两个activity。其中在MainActivity.java中使用视频进行预测,Second.java中使用图片进行预测。相关代码如下:
android:label="手语实时识别">
在每个页面设置跳转按钮及监听事件,通过setClass()方法设置跳转的页面,并使用startActivityForResult()方法启动跳转。相关代码如下:
//设置跳转按钮
Button buttonSettings = (Button) findViewById(R.id.button_settings);
buttonSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v){
Intent intent2 =new Intent(); //新建意图对象
intent2.setClass(MainActivity.this, Second.class);
startActivityForResult(intent2,0); //返回前一页
}
});
7. 布局文件代码
布局文件相关代码如下:
activity.xml:
second.xml:
5.4系统测试
本部分包括训练准确率、测试效果及模型应用。
5.4.1训练准确率
训练过程的准确率损失变化如图512和图513所示。
图512模型准确率
图513模型损失值
5.4.2测试效果
在初步评估中,使用Spyder编译运行相关评估代码之后,能够在以白色墙壁、各种光照的条件下较好地捕捉手部位置,并准确识别0~9共10个手语手势,如图514和图515所示。
图514捕捉手部区域效果图
图515识别手语效果图
5.4.3模型应用
本部分包括程序下载运行、应用使用说明和测试结果。
1. 程序下载运行
Android项目编译成功后,建议将项目运行到真机上进行测试。模拟器运行较慢,不建议使用。运行到真机方法如下:
(1) 将手机数据线连接到计算机,开启开发者模式,打开USB调试,单击Android项目的运行按钮,出现连接手机的选项,单击即可。
(2) Android Studio生成apk文件,发送至手机,在手机上下载该apk文件并安装即可。
2. 应用使用说明
打开APP,初始界面如图516所示。
图516应用初始界面
界面从上至下3个按钮分别为【转到图片识别】【开始翻译手语】【停止翻译】。界面依次显示本次的识别结果及置信度、捕捉到的手部区域、累计识别到的句子翻译。单击【开始识别】按钮,结果如图517所示。
图517预测结果显示界面
单击【转到图片识别】按钮,跳转到图片识别界面。单击【拍照识别】按钮调用摄像头拍照,切换前置及后置摄像头。单击【从相册中选择】即可调出相册界面。选择好图像后,APP将展示所选图片并返回手语识别结果,如图518所示。
3. 测试结果
手势识别“520”效果如图519所示。
图片识别其他手势效果如图520所示。
复杂背景效果如图521所示。
图518图片识别界面
图519手势识别“520”效果图
图520图片识别其他手势效果图
图521复杂背景测试效果图