前面已经写了几篇wiki介绍facenet人脸分类,但是并没有写到将其移植到android上。这篇就是记录如何将facenet移植到android的。其中经历了约两个月的时间。并遇到问题停止不前。但还好些这篇wiki说明我们闯过了这个关。成功将facenet的tensorflow模型移植到了android上。
当我们准备进行移植时,查阅资料了解到android只支持tensorflow的pb模型。而facenet官网公布的训练好的模型只有ckpt结尾的模型文件。于是我们就开始了自己转换ckpt模型到pb模型的尝试之路。并且坚信这是可以转换的。但其实在这里断断续续尝试,过了3周左右的时间都未能成功。最后发现facenet github上更新的20170512-110547模型,其中包含pb模型。
facenet释放的freeze_graph.py
并且facenet自己的代码中就已经有将ckpt模型转换成pb模型的文件freeze_graph.py,代码请参考https://github.com/davidsandberg/facenet/blob/master/src/freeze_graph.py ,而我之前并没有看到或者去研究过这个文件的意义。从而浪费了时间自己研究。可以直接运行转换下载的ckpt模型为pb模型。
或者直接下载20170512-110547模型,其中包含pb模型。
验证pb模型
修改代码将pb模型加载出来并在validate_on_lfw中使用。确认能正常运行。其实这一步也可以不做,大家看的时候可以略过。不知道为什么当时自己就是想确认pb模型跟ckpt模型一样能够加载进行预测测试
在validate_on_lfw.py文件main()函数中做如下的modify:
def main(args): with tf.Graph().as_default(): with tf.Session() as sess: # Read the file containing the pairs used for testing pairs = lfw.read_pairs(os.path.expanduser(args.lfw_pairs)) # Get the paths for the corresponding images paths, actual_issame = lfw.get_paths(os.path.expanduser(args.lfw_dir), pairs, args.lfw_file_ext) - # Load the model - facenet.load_model(args.model) - - # Get input and output tensors - images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0") - embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0") - phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0") + # Load the model + graph_def = facenet.load_pb(args.model) + _ = tf.import_graph_def(graph_def, name=) //将读出来的graph_def 图,import到当前这个session中。就是这句最关键,参考github issue才解决。 + + images_placeholder = sess.graph.get_tensor_by_name("input:0") //再从这个session中取到张量的名字 + embeddings = sess.graph.get_tensor_by_name("embeddings:0") + phase_train_placeholder = sess.graph.get_tensor_by_name("phase_train:0") 其中的load_pb()方法在facenet文件中添加,代码如下: + + def load_pb(model): + # Check if the model is a model directory (containing a metagraph and a checkpoint file) + # or if it is a protobuf file with a frozen graph + model_exp = os.path.expanduser(model) + if (os.path.isfile(model_exp)): + print('Model filename: %s' % model_exp) + with gfile.FastGFile(model_exp, 'rb') as f: + graph_def = tf.GraphDef() + graph_def.ParseFromString(f.read()) + tf.import_graph_def(graph_def, name=) + else: + print('Model directory: %s' % model_exp) + pbfile = get_pb_filenames(model_exp) + pb_path = os.path.join(model_exp,pbfile) + print('pb path: %s' % pb_path) + with gfile.FastGFile(pb_path,'rb') as f: + graph_def = tf.GraphDef() + graph_def.ParseFromString(f.read()) + return graph_def + + def get_pb_filenames(model_dir): + files = os.listdir(model_dir) + pb_files = [s for s in files if s.endswith('.pb')] + if len(pb_files)==0: + raise ValueError('No pb file found in the model directory (%s)' % pb_files) + elif len(pb_files)>1: + raise ValueError('There should not be more than one meta file in the model directory (%s)' % pb_files) + pb_file = pb_files[0] + return pb_file +
1) 将.pb文件放入项目中app/src/main/assets
打开 Project view ,app/src/main/assets。
若不存在assets目录,右键main->new->folder->Assets Folder
2) 将本地下载的tensorflow 编译生成的android_tensorflow_inference_java.jar拷贝到app->libs下
编译方法:bazel build //tensorflow/contrib/android:android_tensorflow_inference_java
3) 打开 Project view,将libtensorflow_inference.so文件拷贝到 app/src/main/jniLibs/armeabi-v7a下(我用的android手机cpu属于armeabi-v7a)
编译方法:bazel build -c opt //tensorflow/contrib/android:libtensorflow_inference.so \
--crosstool_top=//external:android/crosstool \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--cpu=armeabi-v7a
4)我在AS编译的时候有遇到如下报错:
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.tzutalin.dlibtest-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tzutalin.dlibtest-1/lib/arm64, /vendor/lib64, /system/lib64]]] couldn't find "libtensorflow_inference.so"
解决方法是:
在build.gradle中的defaultConfig中增加如下配置:
ndk { abiFilters "armeabi-v7a" }
官方最新的方法就如下四步:
1)new一个TensorFlowInferenceInterface,传入pb模型路径。
inferenceInterface = new TensorFlowInferenceInterface(context.getAssets(), MODEL_FILE);
2) inferenceInterface.feed(INPUT_NODE, inputArray, inputsize, inputsize, inputsize, channel);
3) inferenceInterface.run(new String[]{OUTPUT_NODE}, false);
4) inferenceInterface.fetch(OUTPUT_NODE, outputs);
有很多网上的博客还用的旧的方法或者要在官方的classify demo TensorFlowImageClassifier文件中修改,其实都用不上。直接在你的代码中按照步骤来就可以。
这里关键的是要注意构建你的输入数组及参数。
其实这里也花了点时间研究,因为从python中使用misc.imread()读取图片数据后得到的矩阵数组与android中调用bitmap.getPixel()再通过Color.red()、Color.green()等转换的数组不一致。
但最后决定不再纠结,先将android得到的数据传入进去调用pb模型看下结果再说。最后事实证明android侧数据与python输入数据不一致没有关系。
实现的将图片转换成像素数组的函数如下:
1 private static final String MODEL_FILE = "file:///android_asset/face.pb";
2 private static final String INPUT_NODE = "input";
3 private static final String OUTPUT_NODE = "embeddings";
4 private static final String TAG = "facenet";
5 private TensorFlowInferenceInterface inferenceInterface;
6
7 static {
8 System.loadLibrary("tensorflow_inference");
9
10 }
11
12 public float[] bitmapToFloatArray(Bitmap bitmap) {
13 int height = bitmap.getHeight();
14 int width = bitmap.getWidth();
15 float[] result = new float[height * width * 3];
16 for (int j = 0; j < height; j++) {
17 for (int i = 0; i < width; i++) {
18 int argb = bitmap.getPixel(i, j);
19 int r = Color.red(argb);
20 int g = Color.green(argb);
21 int b = Color.blue(argb);
22 result[j * height* 3 + 3 * i + 0] = r / 1.0f;
23 result[j * height* 3 + 3 * i + 1] = g / 1.0f;
24 result[j * height* 3 + 3 * i + 2] = b / 1.0f;
25 }
26 }
27
28 return result;
29 }
30 public void getImageData(ArrayList<String> imagePath, Context context) {
31 if (!imagePath.isEmpty()) {
32 float[] iamgesPixels = new float[imagePath.size() * 160 * 160 * 3]; //这里160是输入图片的长,宽,3表示RGB三通道。输入的图片都是经过预处理后的人脸大小为160x160的图片
33 Log.d("facenet", " getImageData in not empty iamgesPixels length = " + iamgesPixels.length);
34 int imagesCount = imagePath.size();
35 inferenceInterface = new TensorFlowInferenceInterface(context.getAssets(), MODEL_FILE);
36 ArrayList<float[]> featureMat = new ArrayList<>();
37 for (int i = 0; i < imagesCount; i++) {
38 Log.d("facenet", "imagePath = " + imagePath.get(i) + " i = " + i);
39 Bitmap bitmap = BitmapFactory.decodeFile(imagePath.get(i)); //读取图片转换成bitmap
40 float[] imageArray = bitmapToFloatArray(bitmap); //将bitmap转换成像素数组
41 float[] outputs = new float[128];
42 inferenceInterface.feed(INPUT_NODE, imageArray, 1, 160, 160, 3); //第三个参数1表示1张图片
43 inferenceInterface.run(new String[]{OUTPUT_NODE}, false);
44 inferenceInterface.fetch(OUTPUT_NODE, outputs);
45 featureMat.add(outputs);
46 }
47 }
48 }
错误1:No Op'Switch'
No OpKernel was registered to support Op 'Switch' with these attrs. Registered devices: [CPU], Registered kernels: device='GPU'; T in [DT_STRING] device='GPU'; T in [DT_BOOL] device='GPU'; T in [DT_INT32] device='GPU'; T in [DT_FLOAT] device='CPU'; T in [DT_FLOAT] device='CPU'; T in [DT_INT32] [[Node: InceptionResnetV1/Conv2d_1a_3x3/BatchNorm/cond/Switch = Switch[T=DT_BOOL](phase_train, phase_train)]] at org.tensorflow.Session.run(Native Method) at org.tensorflow.Session.access$100(Session.java:48) at org.tensorflow.Session$Runner.runHelper(Session.java:295) at org.tensorflow.Session$Runner.run(Session.java:245) at org.tensorflow.contrib.android.TensorFlowInferenceInterface.run(TensorFlowInferenceInterface.java:142)
如第三节增加代码后发现移植报错,这里对于报错的解决也花费了很多时间,关键是没有多在网络查询,只是查了facenet github issue里没有人提这个问题,其实在别的pb移植案例也遇到了这个问题。
解决方法:
参考https://github.com/tensorflow/models/issues/1740介绍的方法
1)修改tensorflow/core/framework/register_types.h文件, 把所有的
#define TF_CALL_bool(m)
修改为
#define TF_CALL_bool(m) m(bool)
然后重新编译libtensorflow_inference.so文件:
bazel build -c opt //tensorflow/contrib/android:libtensorflow_inference.so \
--crosstool_top=//external:android/crosstool \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--cpu=armeabi-v7a
最后将新生产的so替换到AS项目中。至于为什么这么改,也没有搞明白,这可能是个workaround的方法
错误2: You must feed a value for placeholder tensor 'phase_train'
解决错误1后重新运行apk报下面的错误:
java.lang.IllegalArgumentException: You must feed a value for placeholder tensor 'phase_train' with dtype bool [[Node: phase_train = Placeholder[dtype=DT_BOOL, shape=[], _device="/job:localhost/replica:0/task:0/cpu:0"]()]]
解决办法:
1) 在inferenceInterface.feed(INPUT_NODE, imageArray, 1, 160, 160, 3);后面增加一行
inferenceInterface.feed("phase_train", new boolean[]{false});
2)TensorFlowInferenceInterface.java 增加支持feed bool的方法
因为添加了上面的代码,android编译不过,因为feed函数没有参数为bool类型的, 所以还需要修改tensorflow/contrib/android/java/org/tensorflow/contrib/android/TensorFlowInferenceInterface.java文件,增加支持feed bool的方法:
1
2 public void feed(String inputName, boolean[] src, long... dims) {
3 byte[] b = new byte[src.length];
4 for (int i = 0; i < src.length; ++i) {
5 b[i] = src[i] ? (byte)1 : (byte)0;
6 }
7 addFeed(inputName, Tensor.create(DataType.BOOL, dims, ByteBuffer.wrap(b)));
8 }
然后编译libandroid_tensorflow_inference_java.jar:
bazel build //tensorflow/contrib/android:android_tensorflow_inference_java
最后将新生产的jar替换到AS项目中。
错误3 No OpKernel was registered to support Op 'FIFOQueueV2'
解决上面两个问题运行仍然有如下报错:
No OpKernel was registered to support Op 'FIFOQueueV2' with these attrs. Registered devices: [CPU], Registered kernels:[[Node: batch_join/fifo_queue = FIFOQueueV2[capacity=1440, component_types=[DT_FLOAT, DT_INT64], container="", shapes=[[160,160,3], []], shared_name=""]()]]
解决方法:
修改facenet/src/freeze_graph.py,
增加 将FIFOQueueV2 op修改为PaddingFIFOQueueV2的代码
def freeze_graph_def(sess, input_graph_def, output_node_names): + elif node.op == 'FIFOQueueV2': + node.op = 'PaddingFIFOQueueV2'
然后运行freeze_graph.py脚本生成新的pb文件:
python src\freeze_graph.py models\freeze_model face.pb
将新的pb模型替换到AS工程中。再次运行即能正常运行并得到输出为128长度的数组。
为什么要将FIFOQueueV2改为PaddingFIFOQueueV2,也是根据如下的tf_op_files.txt中有PaddingFIFOQueueV2类似的cc文件存在,所以这样尝试改的。
在contrib/makefile/tf_op_files.txt中有
tensorflow/core/kernels/padding_fifo_queue_op.cc
tensorflow/core/kernels/padding_fifo_queue.cc
参考这篇文章讲了几种压缩方式TensorFlow Graph Transform Tool(GTT) 。我们就是尝试的其中的 quantize_weights,具体执行命令如下:
1,准备步骤:
./configure (其中很多不熟悉的如CUDA,openCL可以选择N) bazel build tensorflow/tools/graph_transforms:transform_graph
2,具体执行quantize_weights:
ckt@ubuntu:/home/workdir/tensorflow/tensorflow$ bazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=../facenet2/facenet/models/freezepb/face.pb --out_graph=../facenet2/facenet/models/freezepb/face_compress.pb --inputs='input' -- outputs='embeddings' --transforms='quantize_weights'
完成后pb模型从压缩前的93M减小到了25M。
将压缩后的模型放入到apk中后,确实减少了apk的size,但是其准确率也受到了影响。同样适用阈值为0.28时,准确率约为75.1%
07-27 15:04:10.251 10947-10981/com.tzutalin.dlibtest D/facenet: p_error ratio of error: 7.282767 07-27 15:04:10.251 10947-10981/com.tzutalin.dlibtest D/facenet: n_error ratio of error: 17.599535 07-27 15:04:10.251 10947-10981/com.tzutalin.dlibtest D/facenet: ratio of error: 24.882301
本篇主要介绍了移植facenetpb模型到android的过程,以及记录在移植过程中遇到问题的解决方法。以供后续遇到类似问题提供思路。
代码没有提交到gerit,这里是对应代码的patch,文件:0001-Feature-port-the-facenet-db-feature.rar可以在智能相册的基础上,打上这个patch测试。
对于输入数据的传入,inputnode,outputnode的名称还需多读python代码。理解其训练及预测代码才能传入正确参数。
移植到android后,发现facent的准确率,在用threshhold为0.28时,其错误率为:
postive_error 10.20 negative_error 1.28 total_error 11.49
准确率即为:88.5%
而参考另一篇wiki在PC上测试得到的准确率约为:91.3%。有一点差别,但可以确认facenet的pb移植在android是成功的。