UE4 OpenCV+Dlib插件(支持Windows Android)人脸检测(二)

本篇是Dlib插件的编写,使用

难点主要在与Android端,PC端SDK打包好后基本就可以使用,Android端遇到了各种兼容之类的bug

参考

https://my.oschina.net/jjyuangu/blog/2243591
https://blog.csdn.net/u012234115/article/details/90642844

Dlib下载和编译(针对Windows平台和Android)

Dlib官网下载地址 工程里(源码在上一篇文章里)使用的是19.7版本,只针对UE4的Android版本

修改后的UE4版本

编译
  1. 打开dlib目录下的CmakeLists.txt
if (DLIB_ISO_CPP_ONLY)
      option(DLIB_JPEG_SUPPORT ${DLIB_JPEG_SUPPORT_STR} OFF)
      option(DLIB_LINK_WITH_SQLITE3 ${DLIB_LINK_WITH_SQLITE3_STR} OFF)
      option(DLIB_USE_BLAS ${DLIB_USE_BLAS_STR} OFF)
      option(DLIB_USE_LAPACK ${DLIB_USE_LAPACK_STR} OFF)
      option(DLIB_USE_CUDA ${DLIB_USE_CUDA_STR} OFF)
      option(DLIB_PNG_SUPPORT ${DLIB_PNG_SUPPORT_STR} OFF)
      option(DLIB_GIF_SUPPORT ${DLIB_GIF_SUPPORT_STR} OFF)
      #option(DLIB_USE_FFTW ${DLIB_USE_FFTW_STR} OFF)
      option(DLIB_USE_MKL_FFT ${DLIB_USE_MKL_FFT_STR} OFF)
else()
      option(DLIB_JPEG_SUPPORT ${DLIB_JPEG_SUPPORT_STR} ON)
      option(DLIB_LINK_WITH_SQLITE3 ${DLIB_LINK_WITH_SQLITE3_STR} ON)
      option(DLIB_USE_BLAS ${DLIB_USE_BLAS_STR} ON)
      option(DLIB_USE_LAPACK ${DLIB_USE_LAPACK_STR} ON)
      option(DLIB_USE_CUDA ${DLIB_USE_CUDA_STR} ON)
      option(DLIB_PNG_SUPPORT ${DLIB_PNG_SUPPORT_STR} ON)
      option(DLIB_GIF_SUPPORT ${DLIB_GIF_SUPPORT_STR} ON)
      #option(DLIB_USE_FFTW ${DLIB_USE_FFTW_STR} ON)
      option(DLIB_USE_MKL_FFT ${DLIB_USE_MKL_FFT_STR} ON)
endif()

将DLIB_USE_CUDA_和DLIB_PNG_SUPPORT_STR 的ON改成OFF, 否则与UE4冲突
2. dlib目录下的Config.h 添加一下代码



// If you are compiling dlib as a shared library and installing it somewhere on your system
// then it is important that any programs that use dlib agree on the state of the
// DLIB_ASSERT statements (i.e. they are either always on or always off).  Therefore,
// uncomment one of the following lines to force all DLIB_ASSERTs to either always on or
// always off.  If you don't define one of these two macros then DLIB_ASSERT will toggle
// automatically depending on the state of certain other macros, which is not what you want
// when creating a shared library.
//#define ENABLE_ASSERTS       // asserts always enabled 
//#define DLIB_DISABLE_ASSERTS // asserts always disabled 

//#define DLIB_ISO_CPP_ONLY
//#define DLIB_NO_GUI_SUPPORT
//#define DLIB_ENABLE_STACK_TRACE

// You should also consider telling dlib to link against libjpeg, libpng, libgif, fftw, CUDA, 
// and a BLAS and LAPACK library.  To do this you need to uncomment the following #defines.
// #define DLIB_JPEG_SUPPORT
// #define DLIB_PNG_SUPPORT
// #define DLIB_GIF_SUPPORT
// #define DLIB_USE_FFTW
// #define DLIB_USE_BLAS
// #define DLIB_USE_LAPACK
// #define DLIB_USE_CUDA
#ifndef STDTOSTRING_H
#define STDTOSTRING_H
    #if defined(ANDROID)
    #ifdef ATOMIC_INT_LOCK_FREE
    #undef ATOMIC_INT_LOCK_FREE
    #endif
    #define ATOMIC_INT_LOCK_FREE 2
    #include 
    #include 
    
    #include 
    #include 

    using namespace std;
    namespace std {
        template  std::string to_string(const T& n) {
            std::ostringstream stm;
            stm << n;
            return stm.str();
        }

        template  T round(T v) {
            return (v > 0) ? (v + 0.5) : (v - 0.5);
        }
    }
    #endif
#endif

  • 这个的目的是Android NDK中没有对应的sdk函数,需要我们手动添加上。 注意我们后面编好后, 需要将这个Config.h替换上去
  1. 先编译Android端的,后编译Windows端的
    Android:
    • 首先你需要在WIndows的环境变量中添加ANDROID_SDK_ROOT和NDK_ROOT,这里名字必须是这两个,后面的脚本根据名字来找android sdk ndk。
    • UE4 OpenCV+Dlib插件(支持Windows Android)人脸检测(二)_第1张图片
    • 创建ndk编译脚本
#-*-coding:utf-8-*-

import os
import shutil
import zipfile
import hashlib
import sys
import platform
import requests
import urllib
import subprocess

#工具类
class Utils():
    #如果目录不存,则创建。
    @staticmethod
    def mkDir(dirPath):
        if os.path.exists(dirPath) and os.path.isdir(dirPath):
            return
        parent = os.path.dirname(dirPath)
        if not (os.path.exists(parent) and os.path.isdir(parent)):
            Utils.mkDir(parent)
        
        os.mkdir(dirPath)
    
    #获取某个目录是否含有某个文件, extList获取指定的文件后缀
    @staticmethod
    def getAllDirFiles(dirPath, extList = None):
        ret = []    
        for file in os.listdir( dirPath):
            if os.path.isfile(os.path.join(dirPath, file)):
               ret.append(os.path.join(dirPath, file))
            else:
                ret += Utils.getAllDirFiles(os.path.join(dirPath, file))
        
        #需要过滤某些文件
        if extList != None:             
            extList = [tmp.lower() for tmp in extList]           
            ret = [path for path in ret if os.path.splitext(path)[1].lower()  in extList]    
        return ret

    #清理掉某个数据
    @staticmethod
    def cleanFile(path):
        if not os.path.exists(path):
            return
        if os.path.isdir(path):
            shutil.rmtree(path)
        elif os.path.isfile(path):
            os.remove(path)

    #将一个文件夹压缩成zip文件
    @staticmethod
    def makeZipFile(fileName, fromDir):        
        fileList = Utils.getAllDirFiles(fromDir)
        with zipfile.ZipFile(fileName , 'w')  as zip:
            for file in fileList:
                zip.write(file, os.path.relpath(file, fromDir))
    
    @staticmethod
    def extractZipFile(fileName, toDir = "."):
        file_zip = zipfile.ZipFile(fileName, 'r')
        for file in file_zip.namelist():
            file_zip.extract(file, toDir)
        file_zip.close()
            

    @staticmethod
    def sha256_checksum(filename, block_size=65536):
        sha256 = hashlib.sha256()
        with open(filename, 'rb') as f:
            for block in iter(lambda: f.read(block_size), b''):
                sha256.update(block)
        return sha256.hexdigest()

#获取python文件所在的路径
def p():
    frozen = "not"
    if getattr(sys, 'frozen',False):
        frozen = "ever so"
        return os.path.dirname(sys.executable)

    return os.path.split(os.path.realpath(__file__))[0]

#下载进度条回调 
def callbackfunc(blocknum, blocksize, totalsize):
    '''回调函数
    @blocknum: 已经下载的数据块
    @blocksize: 数据块的大小
    @totalsize: 远程文件的大小
    '''
    percent = 100.0 * blocknum * blocksize / totalsize
    if percent > 100:
        percent = 100
    
    max_arrow = 50 #进度条的长度
    num_arrow = int(percent * max_arrow/100.0) 

    process_bar = '\r[' + '>' * num_arrow + '#' * (max_arrow -  num_arrow) + ']'\
                      + '%.2f%%' % percent  #带输出的字符串,'\r'表示不换行回到最左边
    sys.stdout.write(process_bar) #这两句打印字符到终端
    sys.stdout.flush()

#andoird sdk 的操作sdk路径 
class  AndroidSDK():
    def __init__(self):
        self.ANDROID_SDK  =  self.getAndroidSDKPath()
        if self.ANDROID_SDK == None:
            self.ANDROID_SDK = self.installAndroidSDK()
            #更新android  sdk 
            self.updateSDK(['platforms;android-16'])
        
        self.cmakeDir = self.getCmakeDir()
        if self.cmakeDir == None:
            self.updateSDK(['cmake;3.6.4111459'])
            self.cmakeDir = self.getCmakeDir()

        self.NDKPath = self.getNDKPath()
        if self.NDKPath == None:
            self.updateSDK(['ndk-bundle'])
            self.NDKPath = self.getNDKPath()

        
    def installAndroidSDK(self):
        sysstr = platform.system().lower()
        
        SHA_256 = {
            "windows":'7e81d69c303e47a4f0e748a6352d85cd0c8fd90a5a95ae4e076b5e5f960d3c7a',
            'darwin':'ecb29358bc0f13d7c2fa0f9290135a5b608e38434aad9bf7067d0252c160853e',
            'linux':'92ffee5a1d98d856634e8b71132e8a95d96c83a63fde1099be3d86df3106def9',
        }
        
        #是否需要下载包
        needDownload  = True
        android_sdk_zip = "android_sdk.zip"
        if os.path.isfile(android_sdk_zip):
            sha256 = Utils.sha256_checksum(android_sdk_zip)
            if sha256.lower() == SHA_256[sysstr]:
                needDownload =  False
        
        print u"下载Android_sdk"
        #下载包
        if needDownload:
            sdk_download_url = 'https://dl.google.com/android/repository/sdk-tools-%s-4333796.zip'%(sysstr, )     
            urllib.urlretrieve(sdk_download_url, android_sdk_zip, callbackfunc)

        #解压文件
        Utils.extractZipFile(android_sdk_zip,  "./android_sdk")
        os.environ['ANDROID_HOME'] = os.path.realpath("android_sdk")
        return os.environ['ANDROID_HOME']

    def updateSDK(self, package = [ 'platforms;android-16', 'cmake;3.6.4111459', 'ndk-bundle' ]):
        sdkmanager = os.path.join(self.ANDROID_SDK, 'tools/bin/sdkmanager')
        if "windows" ==  platform.system().lower():
            sdkmanager =  sdkmanager + '.bat'
        else:
            cmd = 'chmod +x %s' %(sdkmanager,)
            os.system(cmd)

        args = ['"%s"' %(key) for key in package]

        args.insert(0, sdkmanager)
        cmd = 'echo y|' +  " ".join(args)
        os.system(cmd)
       

    #获取sdk里的 cmake 信息 
    def getCmakeDir(self):
        ndk_cmake_dir  = os.path.join(self.ANDROID_SDK,  "cmake")
        if  not  os.path.isdir(ndk_cmake_dir):
            return None
        
        cmake_dir_list = os.listdir(ndk_cmake_dir)
        list_len = len(cmake_dir_list)
        if list_len <= 0:
            return  None
       
        return os.path.join(ndk_cmake_dir, cmake_dir_list[list_len - 1] )
       
        
    def  getNDKPath(self):
        #通过系统变量来寻找
        environ_names = [
           'NDK_ROOT', 
        ]

        for name in environ_names:            
            #环境变量里不存在
            if name not  in os.environ.keys():
                continue

            android_ndk_path = os.environ[name]
            #验证如果不存在此目录 
            if not  os.path.isdir(android_ndk_path):
                continue
         
            return android_ndk_path
        
        ndk_bundle_dir  = os.path.join(self.ANDROID_SDK,  "ndk-bundle")
        ndk_bundle_list = os.listdir( ndk_bundle_dir)
        
        ndk_bundle_list_len = len(ndk_bundle_list)
        if ndk_bundle_list_len <= 0 :
            return None

        #取最后一个高版本的使用
        return  os.path.join(self.ANDROID_SDK,  "ndk-bundle/" + ndk_bundle_dir[ndk_bundle_list_len - 1] )

    # 根据系统变量android sdk的路径
    def getAndroidSDKPath(self):
        environ_names = [
           'ANDROID_HOME', 
           'ANDROID_SDK_ROOT'
        ]

        for name in environ_names:            
            #环境变量里不存在
            if name not  in os.environ.keys():
                continue

            android_sdk_path = os.environ[name]
            #验证如果不存在此目录 
            if not  os.path.isdir(android_sdk_path):
                continue
         
            return android_sdk_path
        
        #没有找到相应的sdk路径
        return None


if '__main__' == __name__:
    android_sdk = AndroidSDK()
    ANDROID_SDK = android_sdk.getAndroidSDKPath()
    ANDROID_NDK =android_sdk.getNDKPath()

    ANDROID_CMAKE = os.path.join(android_sdk.getCmakeDir(), 'bin/cmake')
    ANDROID_NINJA=os.path.join(android_sdk.getCmakeDir(),'bin/ninja')

    if "windows" ==  platform.system().lower():
        ANDROID_CMAKE =  ANDROID_CMAKE + '.exe'
        ANDROID_NINJA = ANDROID_NINJA + '.exe'

    pyPath = p()
    buildDir = os.path.join(pyPath, "build")
    outDir = os.path.join(pyPath, "out")


    Utils().cleanFile(outDir)
    Utils().mkDir(outDir)
    
    #需要打包的abi
    abiList = [
        'armeabi',
        'armeabi-v7a',
        "arm64-v8a",
        "x86",
        'x86_64',
        'mips',
        'mips64',
    ]
    for abi in abiList:
        os.chdir(pyPath)
        Utils().cleanFile(buildDir)
        Utils().mkDir(buildDir)
        os.chdir(buildDir)
        
        #导出目录
        outSoPath = os.path.join(outDir, "" + abi)
        Utils().cleanFile(outSoPath)
        Utils().mkDir(outSoPath)

        cmd = '''%s -DANDROID_ABI=%s   \
        -DANDROID_PLATFORM=android-16  \
        -DCMAKE_BUILD_TYPE=Release   \
        -DANDROID_NDK=%s    \
        -DCMAKE_CXX_FLAGS=-std=c++11 -frtti -fexceptions   \
        -DCMAKE_TOOLCHAIN_FILE=%s/build/cmake/android.toolchain.cmake    \
        -DCMAKE_MAKE_PROGRAM=%s -G "Ninja"    \
        -DDLIB_NO_GUI_SUPPORT=1 \
        -DCMAKE_INSTALL_PREFIX=%s \
        ..'''%(ANDROID_CMAKE,abi,ANDROID_NDK,ANDROID_NDK,ANDROID_NINJA, outSoPath ) 
        print cmd
        os.system(cmd)
        os.system("%s --build ."%(ANDROID_CMAKE, ))
        os.system("%s -P cmake_install.cmake"%(ANDROID_CMAKE, ))

执行 Python ./ndk_build.py,成功的话 会在sdk更目录创建出Out
UE4 OpenCV+Dlib插件(支持Windows Android)人脸检测(二)_第2张图片
里面有对应的Include文件和.a库文件,将Config.h覆盖这里,然后拷到插件对应的目录中
Windows:
安装CMake GUI,生成vs工程后就是传统的打包lib了,将lib拷贝到插件目录中
UE4 OpenCV+Dlib插件(支持Windows Android)人脸检测(二)_第3张图片
4. Dlib插件编写

  • Dlib中有C++ throw的写法,在NDK中默认是不支持的,需要在.build.cs中添加 bEnableExceptions = true;
  • Dlib 有一些局部变量覆盖全局变量的情况,会引起C4458 C4459警告,xx声明隐藏了全局声明,
    但是在UE4中这些警告会被认为是错误,无法编译错误,解决方法添加 ShadowVariableWarningLevel = WarningLevel.Off;
  • 其次就是正常添加上dlib.lib 还有libdlib.a的库,具体看工程代码,跟OpenCV差不多

5.Dlib测试使用

  • 这里参考TestDlibActor.cpp中的测试代码,如果log中有对应的结果,就是添加成功了
  • 这里单独说一下Dlib中加载保存训练数据的函数接口 serialize(const std::string FileName&)还有deserialize(const std::string FileName&)
    这两个函数在Windows端使用是没问题的,但是在Android端就会崩溃,看android logcat猜测原因是NDK中对智能指针调用ifstream析构函数时有问题,这里我们就麻烦一点,使用下面的代码进行处理
			UE_LOG(LogTemp, Warning, TEXT("Begin Serialize file TempPath = %s"), *TempPath );
			std::string strTemp(TCHAR_TO_UTF8(*TempPath));
			std::ofstream StreamOut(strTemp.c_str(), std::ios::binary);
			
			UE_LOG(LogTemp, Warning, TEXT("custom serialize begin, TempPath = %s"), *TempPath );
			serialize(learned_pfunct, StreamOut);
			StreamOut.clear();
			StreamOut.close();
			UE_LOG(LogTemp, Warning, TEXT("custom serialize end, TempPath = %s"), *TempPath );

			std::ifstream StreamIn(strTemp.c_str(), std::ios::binary);
			UE_LOG(LogTemp, Warning, TEXT("custom deserialize begin, begin deserialize TempPath = %s"), *TempPath );
			deserialize(learned_pfunct, StreamIn);
			UE_LOG(LogTemp, Warning, TEXT("custom deserialize end, begin deserialize TempPath = %s"), *TempPath );

PS:

  • 找到dlib\lzp_buffer\lzp_buffer_kernel_2.h 修改 inline bool vefrify()这个函数的函数名和下面的调用,否者会报意外的类型bool,因为和UE4冲突
  • 错误 “UpdateResourceW”: 不是“UTexture2D”的成员 在头文件中添加#define UpdateResource UpdateResource

你可能感兴趣的:(UE4学习)