项目5 PROJECT 5 基于OpenCV和CNN的 手语数字实时翻译 本项目基于Keras深度模型进行手语的分类,通过OpenCV库的相关算法捕捉手部位置,实现视频流及图片的手语实时识别。 5.1总体设计 本部分包括系统整体结构和系统流程。 5.1.1系统整体结构 系统整体结构如图51所示。 图51系统整体结构 5.1.2系统流程 系统流程如图52所示。 图52系统流程 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直接依次输入下列命令,如图53所示。 图53输入的命令行指令 在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/MnisttensorFlowAndroidDemo/tree/master/app/libs。 在/app/libs下新建armeabiv7a文件夹,添加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/,下载相应版本的安卓包并完成解压,如图54所示。 图54OpenCV官网界面 在Android Studio菜单中单击File→New→Import Module,选择安卓包中的sdk文件夹,如图55和图56所示。 图55Android Studio菜单界面 图56OpenCV sdk文件夹内容 单击菜单项File→Project Structure,选择Dependencies,在Modules栏下选择APP,单击左数第3栏中的【+】图标,选择Module Dependency,单击OK按钮退出。 打开根目录下的build.gradle文件,记下compileSdkVersionbuildToolsVersion、minSdkVersion和targetSdkVersion。单击sdk,打开根目录下的build.gradle文件,把文件中compileSdkVersion、buildToolsVersion、minSdkVersion和targetSdkVersion后的数值改成与APP中相同的文件,如图57所示。 图57Android Studio Project Structure界面图 在app/src/main下新建文件夹jniLibs,将OpenCVandroidsdk的sdk/native/libs下的所有文件复制到jniLibs下。 5.3模块实现 本项目包括6个模块: 数据预处理、数据增强、模型构建、模型训练及保存、模型评估和模型测试,下面分别介绍各模块的功能及相关代码。 5.3.1数据预处理 在Kaggle上下载相应的数据集,下载地址为https://www.kaggle.com/ardamavi/signlanguagedigitsdataset。 加载在本地文件夹中下载的数据集,相关代码如下: #导入相应包 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()) 数据集预览效果如图58所示。 图58数据集预览效果 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) 数据增强过程如图59所示。 图59数据增强过程 数据预览效果如图510所示。 图510数据预览效果 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) #绘制混淆矩阵 训练过程如图511所示。 图511训练过程 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) 使用肤色进行轮廓提取,将提取到的区域进行高斯滤波及二值化,并使用findContour()函数进行轮廓提取。对比每个轮廓大小,并忽略面积小于阈值的连通域。 (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: