Преглед на файлове

添加虹软ArcSoft测试页

Frankensteinly преди 7 месеца
родител
ревизия
cccfaedc8e

BIN
app/libs/arcsoft_face.jar


BIN
app/libs/arcsoft_image_util.jar


+ 3 - 0
app/src/main/AndroidManifest.xml

@@ -34,6 +34,9 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.ISCS"
         tools:targetApi="31">
+        <activity
+            android:name=".view.activity.test.face.arcsoft.ArcsoftTestActivity"
+            android:exported="false" />
         <activity
             android:name=".view.activity.test.face.FaceActivity"
             android:exported="false" />

+ 26 - 0
app/src/main/java/com/grkj/iscs/util/BitmapUtil.kt

@@ -4,6 +4,9 @@ import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.Canvas
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.graphics.YuvImage
 import android.os.Environment
 import com.bumptech.glide.Glide
 import com.bumptech.glide.load.DataSource
@@ -173,4 +176,27 @@ object BitmapUtil {
 
         return bitmap
     }
+
+    /**
+     * 将NV21格式的数据转换为Bitmap。(效率低,用NV21ToBitmap类)
+     *
+     * @param nv21 NV21格式的字节数组
+     * @param width 图像的宽度
+     * @param height 图像的高度
+     * @return 转换后的Bitmap对象
+     */
+    fun convertNV21ToBitmap(nv21: ByteArray?, width: Int, height: Int): Bitmap? {
+        val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
+        val outputStream = ByteArrayOutputStream()
+        yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, outputStream)
+        val imageBytes = outputStream.toByteArray()
+        return try {
+            val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
+            outputStream.close()
+            bitmap
+        } catch (e: Exception) {
+            e.printStackTrace()
+            null
+        }
+    }
 }

+ 5 - 0
app/src/main/java/com/grkj/iscs/view/activity/MainActivity.kt

@@ -11,6 +11,7 @@ import com.grkj.iscs.view.activity.test.RfidActivity
 import com.grkj.iscs.view.activity.test.ModbusActivity
 import com.grkj.iscs.view.activity.test.WidgetTestActivity
 import com.grkj.iscs.view.activity.test.WebSocketActivity
+import com.grkj.iscs.view.activity.test.face.arcsoft.ArcsoftTestActivity
 import com.grkj.iscs.view.activity.test.fingerprint.FingerPrintActivity
 
 class MainActivity : BaseActivity<ActivityMainBinding>() {
@@ -63,6 +64,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
             startActivity(Intent(this, FaceActivity::class.java))
         }
 
+        mBinding?.arcsoft?.setOnClickListener {
+            startActivity(Intent(this, ArcsoftTestActivity::class.java))
+        }
+
         mBinding?.finger?.setOnClickListener {
             startActivity(Intent(this, FingerPrintActivity::class.java))
         }

+ 254 - 0
app/src/main/java/com/grkj/iscs/view/activity/test/face/arcsoft/ArcsoftTestActivity.kt

@@ -0,0 +1,254 @@
+package com.grkj.iscs.view.activity.test.face.arcsoft
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.graphics.Point
+import android.hardware.Camera
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.arcsoft.face.AgeInfo
+import com.arcsoft.face.ErrorInfo
+import com.arcsoft.face.Face3DAngle
+import com.arcsoft.face.FaceEngine
+import com.arcsoft.face.FaceInfo
+import com.arcsoft.face.GenderInfo
+import com.arcsoft.face.LivenessInfo
+import com.arcsoft.face.enums.DetectFaceOrientPriority
+import com.arcsoft.face.enums.DetectMode
+import com.grkj.iscs.databinding.ActivityArcsoftTestBinding
+import com.grkj.iscs.util.BitmapUtil
+import com.grkj.iscs.util.ToastUtils
+import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.view.base.BaseActivity
+
+class ArcsoftTestActivity : BaseActivity<ActivityArcsoftTestBinding>(), OnGlobalLayoutListener {
+
+    private var cameraHelper: CameraHelper? = null
+//    private val drawHelper: DrawHelper? = null
+    private var previewSize: Camera.Size? = null
+    private val rgbCameraId = Camera.CameraInfo.CAMERA_FACING_BACK
+    private var faceEngine: FaceEngine? = null
+    private var afCode = -1
+    private val processMask: Int =
+        FaceEngine.ASF_AGE or FaceEngine.ASF_FACE3DANGLE or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS
+
+
+    companion object {
+        private const val ACTION_REQUEST_PERMISSIONS: Int = 0x001
+        /**
+         * 所需的所有权限信息
+         */
+        private val NEEDED_PERMISSIONS: Array<String?> = arrayOf(
+            Manifest.permission.CAMERA,
+            Manifest.permission.READ_PHONE_STATE
+        )
+    }
+
+    override val viewBinding: ActivityArcsoftTestBinding
+        get() = ActivityArcsoftTestBinding.inflate(layoutInflater)
+
+    override fun initView() {
+
+        //在布局结束后才做初始化操作
+        mBinding?.texturePreview?.viewTreeObserver?.addOnGlobalLayoutListener(this)
+    }
+
+    protected fun checkPermissions(neededPermissions: Array<String?>?): Boolean {
+        if (neededPermissions == null || neededPermissions.size == 0) {
+            return true
+        }
+        var allGranted = true
+        for (neededPermission in neededPermissions) {
+            allGranted = allGranted and (ContextCompat.checkSelfPermission(
+                this,
+                neededPermission!!
+            ) == PackageManager.PERMISSION_GRANTED)
+        }
+        return allGranted
+    }
+
+    private fun initEngine() {
+        faceEngine = FaceEngine()
+        afCode = faceEngine!!.init(
+            this,
+            DetectMode.ASF_DETECT_MODE_VIDEO,
+            DetectFaceOrientPriority.valueOf("ASF_OP_0_ONLY"),
+            16,
+            20,
+            FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_AGE or FaceEngine.ASF_FACE3DANGLE or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS
+        )
+        LogUtil.i("initEngine:  init: $afCode")
+        if (afCode != ErrorInfo.MOK) {
+//            ToastUtils.tip(getString(R.string.init_failed, afCode))
+        }
+    }
+
+    private fun unInitEngine() {
+        if (afCode == 0) {
+            afCode = faceEngine!!.unInit()
+            LogUtil.i("unInitEngine: $afCode")
+        }
+    }
+
+    private fun initCamera() {
+        val metrics = DisplayMetrics()
+        windowManager.defaultDisplay.getMetrics(metrics)
+
+        val cameraListener: CameraListener = object : CameraListener {
+            override fun onCameraOpened(
+                camera: Camera,
+                cameraId: Int,
+                displayOrientation: Int,
+                isMirror: Boolean
+            ) {
+                LogUtil.i("onCameraOpened: $cameraId  $displayOrientation $isMirror")
+                previewSize = camera.parameters.previewSize
+//                drawHelper = DrawHelper(
+//                    previewSize.width,
+//                    previewSize.height,
+//                    previewView.getWidth(),
+//                    previewView.getHeight(),
+//                    displayOrientation,
+//                    cameraId,
+//                    isMirror,
+//                    false,
+//                    false
+//                )
+            }
+
+
+            override fun onPreview(nv21: ByteArray?, camera: Camera?) {
+//                if (faceRectView != null) {
+//                    faceRectView.clearFaceInfo()
+//                }
+                val faceInfoList: List<FaceInfo> = ArrayList()
+                //                long start = System.currentTimeMillis();
+                var code = faceEngine!!.detectFaces(
+                    nv21,
+                    previewSize!!.width,
+                    previewSize!!.height,
+                    FaceEngine.CP_PAF_NV21,
+                    faceInfoList
+                )
+                if (code == ErrorInfo.MOK && faceInfoList.size > 0) {
+                    code = faceEngine!!.process(
+                        nv21,
+                        previewSize!!.width,
+                        previewSize!!.height,
+                        FaceEngine.CP_PAF_NV21,
+                        faceInfoList,
+                        processMask
+                    )
+                    if (code != ErrorInfo.MOK) {
+                        return
+                    }
+                } else {
+                    return
+                }
+
+                val ageInfoList: List<AgeInfo> = ArrayList()
+                val genderInfoList: List<GenderInfo> = ArrayList()
+                val face3DAngleList: List<Face3DAngle> = ArrayList()
+                val faceLivenessInfoList: List<LivenessInfo> = ArrayList()
+                val ageCode = faceEngine!!.getAge(ageInfoList)
+                val genderCode = faceEngine!!.getGender(genderInfoList)
+                val face3DAngleCode = faceEngine!!.getFace3DAngle(face3DAngleList)
+                val livenessCode = faceEngine!!.getLiveness(faceLivenessInfoList)
+
+                // 有其中一个的错误码不为ErrorInfo.MOK,return
+                if ((ageCode or genderCode or face3DAngleCode or livenessCode) != ErrorInfo.MOK) {
+                    return
+                }
+
+
+                // 自己加的,必须有活体检测
+                if (faceLivenessInfoList.none { it.liveness == LivenessInfo.ALIVE }) {
+                    return
+                }
+//                val bitmap = FaceUtils.getFaceBitmap(nv21!!, previewSize!!.width, previewSize!!.height, faceInfoList[0])
+                val bitmap = NV21ToBitmap(this@ArcsoftTestActivity).nv21ToBitmap(nv21, previewSize!!.width, previewSize!!.height)
+//                val bitmap = BitmapUtil.convertNV21ToBitmap(nv21, previewSize!!.width, previewSize!!.height)
+                LogUtil.i("识别结果 : ${bitmap == null} - $faceInfoList")
+                mBinding?.ivCapture?.setImageBitmap(bitmap)
+//                if (faceRectView != null && drawHelper != null) {
+//                    val drawInfoList: MutableList<DrawInfo> = ArrayList<DrawInfo>()
+//                    for (i in faceInfoList.indices) {
+//                        drawInfoList.add(
+//                            DrawInfo(
+//                                drawHelper.adjustRect(faceInfoList[i].rect),
+//                                genderInfoList[i].gender,
+//                                ageInfoList[i].age,
+//                                faceLivenessInfoList[i].liveness,
+//                                RecognizeColor.COLOR_UNKNOWN,
+//                                null
+//                            )
+//                        )
+//                    }
+//                    drawHelper.draw(faceRectView, drawInfoList)
+//                }
+            }
+
+            override fun onCameraClosed() {
+                LogUtil.i("onCameraClosed: ")
+            }
+
+            override fun onCameraError(e: Exception) {
+                LogUtil.i("onCameraError: " + e.message)
+            }
+
+            override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
+//                if (drawHelper != null) {
+//                    drawHelper.setCameraDisplayOrientation(displayOrientation)
+//                }
+                LogUtil.i("onCameraConfigurationChanged: $cameraID  $displayOrientation")
+            }
+        }
+        cameraHelper = CameraHelper.Builder()
+            .previewViewSize(Point(mBinding?.texturePreview!!.getMeasuredWidth(), mBinding?.texturePreview!!.getMeasuredHeight()))
+            .rotation(windowManager.defaultDisplay.rotation)
+            .specificCameraId(rgbCameraId ?: Camera.CameraInfo.CAMERA_FACING_FRONT)
+            .isMirror(false)
+            .previewOn(mBinding?.texturePreview!!)
+            .cameraListener(cameraListener)
+            .build()
+        cameraHelper!!.init()
+        cameraHelper!!.start()
+    }
+
+    fun afterRequestPermission(requestCode: Int, isAllGranted: Boolean) {
+        if (requestCode == ACTION_REQUEST_PERMISSIONS) {
+            if (isAllGranted) {
+                initEngine()
+                initCamera()
+            } else {
+                ToastUtils.tip("permission denied")
+            }
+        }
+    }
+
+    override fun onDestroy() {
+        if (cameraHelper != null) {
+            cameraHelper!!.release()
+            cameraHelper = null
+        }
+        unInitEngine()
+        super.onDestroy()
+    }
+
+    override fun onGlobalLayout() {
+        mBinding?.texturePreview?.viewTreeObserver?.removeOnGlobalLayoutListener(this)
+        if (!checkPermissions(NEEDED_PERMISSIONS)) {
+            ActivityCompat.requestPermissions(
+                this,
+                NEEDED_PERMISSIONS,
+                ACTION_REQUEST_PERMISSIONS
+            )
+        } else {
+            initEngine()
+            initCamera()
+        }
+    }
+}

+ 443 - 0
app/src/main/java/com/grkj/iscs/view/activity/test/face/arcsoft/CameraHelper.java

@@ -0,0 +1,443 @@
+package com.grkj.iscs.view.activity.test.face.arcsoft;
+
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * 相机辅助类,和{@link CameraListener}共同使用,获取nv21数据等操作
+ */
+public class CameraHelper implements Camera.PreviewCallback {
+    private static final String TAG = "CameraHelper";
+    private Camera mCamera;
+    private int mCameraId;
+    private Point previewViewSize;
+    private View previewDisplayView;
+    private Camera.Size previewSize;
+    private Point specificPreviewSize;
+    private int displayOrientation = 0;
+    private int rotation;
+    private int additionalRotation;
+    private boolean isMirror = false;
+
+    private Integer specificCameraId = null;
+    private CameraListener cameraListener;
+
+    private CameraHelper(Builder builder) {
+        previewDisplayView = builder.previewDisplayView;
+        specificCameraId = builder.specificCameraId;
+        cameraListener = builder.cameraListener;
+        rotation = builder.rotation;
+        additionalRotation = builder.additionalRotation;
+        previewViewSize = builder.previewViewSize;
+        specificPreviewSize = builder.previewSize;
+        if (builder.previewDisplayView instanceof TextureView) {
+            isMirror = builder.isMirror;
+        } else if (isMirror) {
+            throw new RuntimeException("mirror is effective only when the preview is on a textureView");
+        }
+    }
+
+    public void init() {
+        if (previewDisplayView instanceof TextureView) {
+            ((TextureView) this.previewDisplayView).setSurfaceTextureListener(textureListener);
+        } else if (previewDisplayView instanceof SurfaceView) {
+            ((SurfaceView) previewDisplayView).getHolder().addCallback(surfaceCallback);
+        }
+
+        if (isMirror) {
+            previewDisplayView.setScaleX(-1);
+        }
+    }
+
+    public void start() {
+        synchronized (this) {
+            if (mCamera != null) {
+                return;
+            }
+            //相机数量为2则打开1,1则打开0,相机ID 1为前置,0为后置
+            mCameraId = Camera.getNumberOfCameras() - 1;
+            //若指定了相机ID且该相机存在,则打开指定的相机
+            if (specificCameraId != null && specificCameraId <= mCameraId) {
+                mCameraId = specificCameraId;
+            }
+
+            //没有相机
+            if (mCameraId == -1) {
+                if (cameraListener != null) {
+                    cameraListener.onCameraError(new Exception("camera not found"));
+                }
+                return;
+            }
+            if (mCamera == null) {
+                mCamera = Camera.open(mCameraId);
+            }
+
+            displayOrientation = getCameraOri(rotation);
+            mCamera.setDisplayOrientation(displayOrientation);
+            try {
+                Camera.Parameters parameters = mCamera.getParameters();
+                parameters.setPreviewFormat(ImageFormat.NV21);
+
+                //预览大小设置
+                previewSize = parameters.getPreviewSize();
+                List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
+                if (supportedPreviewSizes != null && supportedPreviewSizes.size() > 0) {
+                    previewSize = getBestSupportedSize(supportedPreviewSizes, previewViewSize);
+                }
+                parameters.setPreviewSize(previewSize.width, previewSize.height);
+
+                //对焦模式设置
+                List<String> supportedFocusModes = parameters.getSupportedFocusModes();
+                if (supportedFocusModes != null && supportedFocusModes.size() > 0) {
+                    if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+                        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
+                    } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
+                        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+                    } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
+                        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
+                    }
+                }
+                mCamera.setParameters(parameters);
+                if (previewDisplayView instanceof TextureView) {
+                    mCamera.setPreviewTexture(((TextureView) previewDisplayView).getSurfaceTexture());
+                } else {
+                    mCamera.setPreviewDisplay(((SurfaceView) previewDisplayView).getHolder());
+                }
+                mCamera.setPreviewCallback(this);
+                mCamera.startPreview();
+                if (cameraListener != null) {
+                    cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror);
+                }
+            } catch (Exception e) {
+                if (cameraListener != null) {
+                    cameraListener.onCameraError(e);
+                }
+            }
+        }
+    }
+
+    private int getCameraOri(int rotation) {
+        int degrees = rotation * 90;
+        switch (rotation) {
+            case Surface.ROTATION_0:
+                degrees = 0;
+                break;
+            case Surface.ROTATION_90:
+                degrees = 90;
+                break;
+            case Surface.ROTATION_180:
+                degrees = 180;
+                break;
+            case Surface.ROTATION_270:
+                degrees = 270;
+                break;
+            default:
+                break;
+        }
+        additionalRotation /= 90;
+        additionalRotation *= 90;
+        degrees += additionalRotation;
+        int result;
+        Camera.CameraInfo info = new Camera.CameraInfo();
+        Camera.getCameraInfo(mCameraId, info);
+        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+            result = (info.orientation + degrees) % 360;
+            result = (360 - result) % 360;
+        } else {
+            result = (info.orientation - degrees + 360) % 360;
+        }
+        return result;
+    }
+
+    public void stop() {
+        synchronized (this) {
+            if (mCamera == null) {
+                return;
+            }
+            mCamera.setPreviewCallback(null);
+            mCamera.stopPreview();
+            mCamera.release();
+            mCamera = null;
+            if (cameraListener != null) {
+                cameraListener.onCameraClosed();
+            }
+        }
+    }
+
+    public boolean isStopped() {
+        synchronized (this) {
+            return mCamera == null;
+        }
+    }
+
+    public void release() {
+        synchronized (this) {
+            stop();
+            previewDisplayView = null;
+            specificCameraId = null;
+            cameraListener = null;
+            previewViewSize = null;
+            specificPreviewSize = null;
+            previewSize = null;
+        }
+    }
+
+    private Camera.Size getBestSupportedSize(List<Camera.Size> sizes, Point previewViewSize) {
+        if (sizes == null || sizes.size() == 0) {
+            return mCamera.getParameters().getPreviewSize();
+        }
+        Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]);
+        Arrays.sort(tempSizes, new Comparator<Camera.Size>() {
+            @Override
+            public int compare(Camera.Size o1, Camera.Size o2) {
+                if (o1.width > o2.width) {
+                    return -1;
+                } else if (o1.width == o2.width) {
+                    return o1.height > o2.height ? -1 : 1;
+                } else {
+                    return 1;
+                }
+            }
+        });
+        sizes = Arrays.asList(tempSizes);
+
+        Camera.Size bestSize = sizes.get(0);
+        float previewViewRatio;
+        if (previewViewSize != null) {
+            previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y;
+        } else {
+            previewViewRatio = (float) bestSize.width / (float) bestSize.height;
+        }
+
+        if (previewViewRatio > 1) {
+            previewViewRatio = 1 / previewViewRatio;
+        }
+        boolean isNormalRotate = (additionalRotation % 180 == 0);
+
+        for (Camera.Size s : sizes) {
+            if (specificPreviewSize != null && specificPreviewSize.x == s.width && specificPreviewSize.y == s.height) {
+                return s;
+            }
+            if (isNormalRotate) {
+                if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) {
+                    bestSize = s;
+                }
+            } else {
+                if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) {
+                    bestSize = s;
+                }
+            }
+        }
+        return bestSize;
+    }
+
+    public List<Camera.Size> getSupportedPreviewSizes() {
+        if (mCamera == null) {
+            return null;
+        }
+        return mCamera.getParameters().getSupportedPreviewSizes();
+    }
+
+    public List<Camera.Size> getSupportedPictureSizes() {
+        if (mCamera == null) {
+            return null;
+        }
+        return mCamera.getParameters().getSupportedPictureSizes();
+    }
+
+
+    @Override
+    public void onPreviewFrame(byte[] nv21, Camera camera) {
+        if (cameraListener != null) {
+            cameraListener.onPreview(nv21, camera);
+        }
+    }
+
+    private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
+        @Override
+        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+//            start();
+            if (mCamera != null) {
+                try {
+                    mCamera.setPreviewTexture(surfaceTexture);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        @Override
+        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+            Log.i(TAG, "onSurfaceTextureSizeChanged: " + width + "  " + height);
+        }
+
+        @Override
+        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+            stop();
+            return false;
+        }
+
+        @Override
+        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+
+        }
+    };
+    private SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+//            start();
+            if (mCamera != null) {
+                try {
+                    mCamera.setPreviewDisplay(holder);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            stop();
+        }
+    };
+
+    public void changeDisplayOrientation(int rotation) {
+        if (mCamera != null) {
+            this.rotation = rotation;
+            displayOrientation = getCameraOri(rotation);
+            mCamera.setDisplayOrientation(displayOrientation);
+            if (cameraListener != null) {
+                cameraListener.onCameraConfigurationChanged(mCameraId, displayOrientation);
+            }
+        }
+    }
+    public boolean switchCamera() {
+        if (Camera.getNumberOfCameras() < 2) {
+            return false;
+        }
+        // cameraId ,0为后置,1为前置
+        specificCameraId = 1 - mCameraId;
+        stop();
+        start();
+        return true;
+    }
+
+    public static final class Builder {
+
+        /**
+         * 预览显示的view,目前仅支持surfaceView和textureView
+         */
+        private View previewDisplayView;
+
+        /**
+         * 是否镜像显示,只支持textureView
+         */
+        private boolean isMirror;
+        /**
+         * 指定的相机ID
+         */
+        private Integer specificCameraId;
+        /**
+         * 事件回调
+         */
+        private CameraListener cameraListener;
+        /**
+         * 屏幕的长宽,在选择最佳相机比例时用到
+         */
+        private Point previewViewSize;
+        /**
+         * 传入getWindowManager().getDefaultDisplay().getRotation()的值即可
+         */
+        private int rotation;
+        /**
+         * 指定的预览宽高,若系统支持则会以这个预览宽高进行预览
+         */
+        private Point previewSize;
+
+        /**
+         * 额外的旋转角度(用于适配一些定制设备)
+         */
+        private int additionalRotation;
+
+        public Builder() {
+        }
+
+
+        public Builder previewOn(View val) {
+            if (val instanceof SurfaceView || val instanceof TextureView) {
+                previewDisplayView = val;
+                return this;
+            } else {
+                throw new RuntimeException("you must preview on a textureView or a surfaceView");
+            }
+        }
+
+
+        public Builder isMirror(boolean val) {
+            isMirror = val;
+            return this;
+        }
+
+        public Builder previewSize(Point val) {
+            previewSize = val;
+            return this;
+        }
+
+        public Builder previewViewSize(Point val) {
+            previewViewSize = val;
+            return this;
+        }
+
+        public Builder rotation(int val) {
+            rotation = val;
+            return this;
+        }
+
+        public Builder additionalRotation(int val) {
+            additionalRotation = val;
+            return this;
+        }
+
+        public Builder specificCameraId(Integer val) {
+            specificCameraId = val;
+            return this;
+        }
+
+        public Builder cameraListener(CameraListener val) {
+            cameraListener = val;
+            return this;
+        }
+
+        public CameraHelper build() {
+            if (previewViewSize == null) {
+                Log.e(TAG, "previewViewSize is null, now use default previewSize");
+            }
+            if (cameraListener == null) {
+                Log.e(TAG, "cameraListener is null, callback will not be called");
+            }
+            if (previewDisplayView == null) {
+                throw new RuntimeException("you must preview on a textureView or a surfaceView");
+            }
+            return new CameraHelper(this);
+        }
+    }
+
+}

+ 40 - 0
app/src/main/java/com/grkj/iscs/view/activity/test/face/arcsoft/CameraListener.java

@@ -0,0 +1,40 @@
+package com.grkj.iscs.view.activity.test.face.arcsoft;
+
+import android.hardware.Camera;
+
+
+public interface CameraListener {
+    /**
+     * 当打开时执行
+     * @param camera 相机实例
+     * @param cameraId 相机ID
+     * @param displayOrientation 相机预览旋转角度
+     * @param isMirror 是否镜像显示
+     */
+    void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror);
+
+    /**
+     * 预览数据回调
+     * @param data 预览数据
+     * @param camera 相机实例
+     */
+    void onPreview(byte[] data, Camera camera);
+
+    /**
+     * 当相机关闭时执行
+     */
+    void onCameraClosed();
+
+    /**
+     * 当出现异常时执行
+     * @param e 相机相关异常
+     */
+    void onCameraError(Exception e);
+
+    /**
+     * 属性变化时调用
+     * @param cameraID  相机ID
+     * @param displayOrientation    相机旋转方向
+     */
+    void onCameraConfigurationChanged(int cameraID, int displayOrientation);
+}

+ 480 - 0
app/src/main/java/com/grkj/iscs/view/activity/test/face/arcsoft/FaceUtils.kt

@@ -0,0 +1,480 @@
+package com.grkj.iscs.view.activity.test.face.arcsoft
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.media.Image
+import android.media.ImageReader
+import android.os.Environment
+import android.util.Log
+import android.util.Size
+import com.arcsoft.face.*
+import com.arcsoft.face.enums.DetectFaceOrientPriority
+import com.arcsoft.face.enums.DetectMode
+import com.arcsoft.imageutil.ArcSoftImageFormat
+import com.arcsoft.imageutil.ArcSoftImageUtil
+import com.arcsoft.imageutil.ArcSoftImageUtilError
+import com.arcsoft.imageutil.ArcSoftRotateDegree
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import kotlin.math.abs
+
+object FaceUtils {
+    val EXT_ROOT_PATH : String = Environment.getExternalStorageDirectory().absolutePath
+    private var faceEngine: FaceEngine? = null
+//    private val faceDataList = ArrayList<FaceBean>()
+
+    /**
+     * 存放特征的目录
+     */
+    private const val NEW_FEATURE_DIR = "face-features"
+
+    fun extFile(path: String) : File {
+        return File(EXT_ROOT_PATH + File.separator + "refuse-class" + File.separator + path)
+    }
+    
+    fun moveOldFolderToNew() {
+        val oldRegDir = File(EXT_ROOT_PATH + File.separator + "register")
+        if (!oldRegDir.exists() || !oldRegDir.isDirectory) {
+            Log.i("FACE", "旧目录 register 不存在")
+            return
+        }
+        val oldDir = File(oldRegDir.path + File.separator + "features")
+        if (!oldDir.exists() || !oldDir.isDirectory) {
+            Log.i("FACE", "旧目录 register/features 不存在")
+            return
+        }
+        val newDir = extFile(NEW_FEATURE_DIR)
+        if (!newDir.exists()) {
+            val success = newDir.mkdirs()
+            Log.i("FACE", "创建新目录 /refuse-class/face-features : ${success}")
+        }
+        for (oldFile in oldDir.listFiles()) {
+            val newFile = extFile(NEW_FEATURE_DIR + File.separator + oldFile.name)
+            if (!newFile.exists()) {
+                newFile.createNewFile()
+            }
+            val data = ByteArray(FaceFeature.FEATURE_SIZE)
+            val fis = FileInputStream(oldFile)
+            fis.read(data)
+            fis.close()
+            Log.i("FACE", "读取旧目录人脸特征:${oldFile.name}")
+            val fos = FileOutputStream(newFile)
+            fos.write(data)
+            fos.close()
+            Log.i("FACE", "写入新目录人脸特征:${oldFile.name}")
+            oldFile.delete()
+            Log.i("FACE", "删除旧目录人脸特征:${oldFile.name}")
+        }
+        oldDir.delete()
+        Log.i("FACE", "删除旧目录 register/features")
+        oldRegDir.delete()
+        Log.i("FACE", "删除旧目录 register")
+    }
+
+
+    /**
+     * 初始化
+     *
+     * @param context 上下文对象
+     * @return 是否初始化成功
+     */
+    fun init(context: Context): Int {
+        synchronized(this) {
+            if (faceEngine == null) {
+                val t0 = System.currentTimeMillis()
+                faceEngine = FaceEngine()
+                val engineCode = faceEngine!!.init(
+                    context,
+                    DetectMode.ASF_DETECT_MODE_VIDEO,
+                    DetectFaceOrientPriority.ASF_OP_ALL_OUT,
+                    16,
+                    1,
+                    // 人脸监测 | 人脸特征 | RGB 活体
+                    FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_FACE_RECOGNITION or FaceEngine.ASF_LIVENESS
+                )
+                val tt = System.currentTimeMillis() - t0
+                Log.i("face", "人脸识别引擎初始化:$engineCode, 耗时:$tt")
+                if (engineCode == ErrorInfo.MOK) {
+                    initFaceList()
+                } else {
+                    faceEngine = null
+                }
+                return engineCode
+            }
+            return ErrorInfo.MOK
+        }
+    }
+
+    /**
+     * 销毁
+     */
+//    fun unInit() {
+//        synchronized(this) {
+//            faceDataList.clear()
+//            faceEngine?.unInit()
+//            faceEngine = null
+//        }
+//    }
+
+    /**
+     * 清空人脸特性
+     */
+    fun clearFaces() {
+        extFile(NEW_FEATURE_DIR).delete()
+    }
+
+    /**
+     * 初始化人脸特征数据以及人脸特征数据对应的注册图
+     *
+     */
+    private fun initFaceList() {
+        synchronized(this) {
+            val featureDir = extFile(NEW_FEATURE_DIR)
+            if (!featureDir.exists() || !featureDir.isDirectory) {
+                return
+            }
+            for (featureFile in featureDir.listFiles()) {
+                try {
+                    val fis = FileInputStream(featureFile)
+                    val feature = ByteArray(FaceFeature.FEATURE_SIZE)
+                    fis.read(feature)
+                    fis.close()
+//                    faceDataList.add(FaceBean(featureFile.name, feature))
+                } catch (e: IOException) {
+                    Log.e("face", "加载人脸数据异常", e)
+                }
+            }
+        }
+    }
+
+    /**
+     * 检测人脸信息
+     */
+    fun detectFace(nv21: ByteArray, width: Int, height: Int) : FaceInfo? {
+        return faceEngine?.run {
+            val faceInfos = ArrayList<FaceInfo>()
+            val code = detectFaces(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfos)
+            if (code == ErrorInfo.MOK && faceInfos.size > 0) {
+                Log.d("face", "检测到 ${faceInfos.size} 个人脸")
+                return faceInfos.maxByOrNull { faceInfo ->
+                    val r = faceInfo.rect
+                    abs((r.left - r.right) * (r.top - r.bottom))
+                }
+            } else {
+                Log.i("face", "未检测到人脸: $code")
+                return null
+            }
+        }
+    }
+
+    /**
+     * 活体检测
+     */
+    fun detectLive(nv21: ByteArray, faceInfo: FaceInfo, width: Int, height: Int) : LivenessInfo? {
+        return faceEngine?.run {
+            val pcode = process(nv21, width, height, FaceEngine.CP_PAF_NV21, listOf(faceInfo), FaceEngine.ASF_LIVENESS)
+            val livenesses = ArrayList<LivenessInfo>()
+            if (pcode == ErrorInfo.MOK) {
+                val lcode = getLiveness(livenesses)
+                if (lcode == ErrorInfo.MOK && livenesses.size > 0) {
+                    return livenesses[0]
+                } else {
+                    Log.d("face", "提取 RGB 活体信息失败:$lcode")
+                }
+            } else {
+                Log.i("face", "人脸属性检测失败:$pcode")
+            }
+            return null
+        }
+    }
+
+    /**
+     * 人脸特征提取
+     */
+    fun getFaceFeature(nv21: ByteArray, faceInfo: FaceInfo, width: Int, height: Int) : FaceFeature? {
+        return faceEngine?.run {
+            val faceFeature = FaceFeature()
+            val code = extractFaceFeature(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfo, faceFeature)
+            if (code == ErrorInfo.MOK) {
+                return faceFeature
+            } else {
+                Log.i("face", "人脸特征提取失败:$code")
+            }
+            return null
+        }
+    }
+
+    /**
+     * 在特征库中搜索最相似的一个
+     * 原名:getTopOfFaceLib
+     * @param feature 传入特征数据
+     * @return 比对结果
+     */
+//    fun findMostSimilarFace(feature: FaceFeature): FaceResultBean? {
+//        if (faceDataList.size == 0) {
+//            return null
+//        }
+//        return faceEngine?.run {
+//            val tmpFeature = FaceFeature()
+//            val faceSimilar = FaceSimilar()
+//            var maxSimilarScore = 0f
+//            var maxSimilarIndex = -1
+//            for (i in faceDataList.indices) {
+//                tmpFeature.featureData = faceDataList[i].featureData
+//
+//                compareFaceFeature(feature, tmpFeature, faceSimilar)
+//
+//                if (faceSimilar.score > maxSimilarScore) {
+//                    maxSimilarScore = faceSimilar.score
+//                    maxSimilarIndex = i
+//                }
+//            }
+//            return if (maxSimilarIndex > -1) {
+//                FaceResultBean(faceDataList[maxSimilarIndex].name, maxSimilarScore)
+//            } else null
+//        }
+//    }
+
+    /**
+     * 获取面部图像
+     */
+    fun getFaceBitmap(nv21: ByteArray, width: Int, height: Int, faceInfo: FaceInfo): Bitmap? {
+        if (width % 4 != 0 || nv21.size != width * height * 3 / 2) {
+            Log.e("face", "invalid params")
+            return null
+        }
+        return faceEngine?.run {
+            // 保存注册结果(注册图、特征数据)
+            // 为了美观,扩大rect截取注册图
+            val cropRect = getBestRect(width, height, faceInfo.rect)
+            if (cropRect == null) {
+                Log.e("face", "registerNv21: cropRect is null!")
+                return null
+            }
+            cropRect.left = cropRect.left and 3.inv()
+            cropRect.top = cropRect.top and 3.inv()
+            cropRect.right = cropRect.right and 3.inv()
+            cropRect.bottom = cropRect.bottom and 3.inv()
+            // 创建一个头像的 Bitmap,存放旋转结果图
+            return getHeadImage(nv21, width, height, faceInfo.orient, cropRect)
+        }
+    }
+
+    /**
+     * 保存人脸特征
+     */
+//    fun saveFeature(feature: FaceFeature, name: String): Boolean {
+//        synchronized(this) {
+//            //特征存储的文件夹
+//            val featureDir = extFile(NEW_FEATURE_DIR)
+//            if (!featureDir.exists() && !featureDir.mkdirs()) {
+//                Log.e("face", "registerNv21: can not create feature directory")
+//                return false
+//            }
+//            try {
+//                val fosFeature = FileOutputStream(featureDir.path + File.separator + name)
+//                fosFeature.write(feature.featureData)
+//                fosFeature.close()
+//                //内存中的数据同步
+//                faceDataList.add(FaceBean(name, feature.featureData))
+//                return true
+//            } catch (e: IOException) {
+//                Log.e("face", "保存图片特征异常", e)
+//                return false
+//            }
+//        }
+//    }
+
+    /**
+     * 截取合适的头像并旋转,保存为注册头像
+     *
+     * @param originImageData 原始的BGR24数据
+     * @param width           BGR24图像宽度
+     * @param height          BGR24图像高度
+     * @param orient          人脸角度
+     * @param cropRect        裁剪的位置
+     * @param imageFormat     图像格式
+     * @return 头像的图像数据
+     */
+    private fun getHeadImage(
+        originImageData: ByteArray,
+        width: Int,
+        height: Int,
+        orient: Int,
+        cropRect: Rect,
+        imageFormat: ArcSoftImageFormat = ArcSoftImageFormat.NV21
+    ): Bitmap {
+        val headImageData = ArcSoftImageUtil.createImageData(cropRect.width(), cropRect.height(), imageFormat)
+        val cropCode = ArcSoftImageUtil.cropImage(
+            originImageData,
+            headImageData,
+            width,
+            height,
+            cropRect,
+            imageFormat
+        )
+        if (cropCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+            throw RuntimeException("crop image failed, code is $cropCode")
+        }
+
+        //判断人脸旋转角度,若不为0度则旋转注册图
+        var rotateHeadImageData: ByteArray? = null
+        val rotateCode: Int
+        val cropImageWidth: Int
+        val cropImageHeight: Int
+        // 90度或270度的情况,需要宽高互换
+        if (orient == FaceEngine.ASF_OC_90 || orient == FaceEngine.ASF_OC_270) {
+            cropImageWidth = cropRect.height()
+            cropImageHeight = cropRect.width()
+        } else {
+            cropImageWidth = cropRect.width()
+            cropImageHeight = cropRect.height()
+        }
+        var rotateDegree: ArcSoftRotateDegree? = null
+        when (orient) {
+            FaceEngine.ASF_OC_90 -> rotateDegree = ArcSoftRotateDegree.DEGREE_270
+            FaceEngine.ASF_OC_180 -> rotateDegree = ArcSoftRotateDegree.DEGREE_180
+            FaceEngine.ASF_OC_270 -> rotateDegree = ArcSoftRotateDegree.DEGREE_90
+            FaceEngine.ASF_OC_0 -> rotateHeadImageData = headImageData
+            else -> rotateHeadImageData = headImageData
+        }
+        // 非0度的情况,旋转图像
+        if (rotateDegree != null) {
+            rotateHeadImageData = ByteArray(headImageData.size)
+            rotateCode = ArcSoftImageUtil.rotateImage(
+                headImageData,
+                rotateHeadImageData,
+                cropRect.width(),
+                cropRect.height(),
+                rotateDegree,
+                imageFormat
+            )
+            if (rotateCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+                throw RuntimeException("rotate image failed, code is $rotateCode")
+            }
+        }
+        // 将创建一个Bitmap,并将图像数据存放到Bitmap中
+        val headBmp = Bitmap.createBitmap(cropImageWidth, cropImageHeight, Bitmap.Config.RGB_565)
+        if (ArcSoftImageUtil.imageDataToBitmap(
+                rotateHeadImageData,
+                headBmp,
+                imageFormat
+            ) != ArcSoftImageUtilError.CODE_SUCCESS
+        ) {
+            throw RuntimeException("failed to transform image data to bitmap")
+        }
+        return headBmp
+    }
+
+    /**
+     * 将图像中需要截取的Rect向外扩张一倍,若扩张一倍会溢出,则扩张到边界,若Rect已溢出,则收缩到边界
+     *
+     * @param width   图像宽度
+     * @param height  图像高度
+     * @param srcRect 原Rect
+     * @return 调整后的Rect
+     */
+    private fun getBestRect(
+        width: Int,
+        height: Int,
+        srcRect: Rect?
+    ): Rect? {
+        if (srcRect == null) {
+            return null
+        }
+        val rect = Rect(srcRect)
+        // 原rect边界已溢出宽高的情况
+        val maxOverFlow = Math.max(
+            -rect.left,
+            Math.max(
+                -rect.top,
+                Math.max(rect.right - width, rect.bottom - height)
+            )
+        )
+        if (maxOverFlow >= 0) {
+            rect.inset(maxOverFlow, maxOverFlow)
+            return rect
+        }
+        // 原rect边界未溢出宽高的情况
+        var padding = rect.height() / 2
+        // 若以此padding扩张rect会溢出,取最大padding为四个边距的最小值
+        if (!(rect.left - padding > 0 && rect.right + padding < width && rect.top - padding > 0 && rect.bottom + padding < height)) {
+            padding = Math.min(
+                Math.min(
+                    Math.min(rect.left, width - rect.right),
+                    height - rect.bottom
+                ),
+                rect.top
+            )
+        }
+        rect.inset(-padding, -padding)
+        return rect
+    }
+
+    fun optimalOutputSize(cameraManager: CameraManager,cameraId: String, width: Int, height: Int) : Size {
+        return cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+            ?.getOutputSizes(ImageReader::class.java)
+            ?.minByOrNull {
+                (it.width - height) * (it.height - width)
+            }
+            ?: Size(width, height)
+    }
+
+    fun getDataByNV21(cropRect:Rect,format:Int,planes:Array<Image.Plane>) : ByteArray {
+        val width: Int = cropRect.width()
+        val height: Int = cropRect.height()
+        val data = ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8)
+        val rowData = ByteArray(planes[0].rowStride)
+        var channelOffset = 0
+        var outputStride = 1
+        for (i in planes.indices) {
+            when (i) {
+                0 -> {
+                    channelOffset = 0
+                    outputStride = 1
+                }
+                1 -> {
+                    channelOffset = width * height + 1
+                    outputStride = 2
+                }
+                2 -> {
+                    channelOffset = width * height
+                    outputStride = 2
+                }
+            }
+            val buffer = planes[i].buffer
+            val rowStride = planes[i].rowStride
+            val pixelStride = planes[i].pixelStride
+            val shift = if (i == 0) 0 else 1
+            val w = width shr shift
+            val h = height shr shift
+            buffer.position(rowStride * (cropRect.top shr shift) + pixelStride * (cropRect.left shr shift))
+            for (row in 0 until h) {
+                var length: Int
+                if (pixelStride == 1 && outputStride == 1) {
+                    length = w
+                    buffer.get(data, channelOffset, length)
+                    channelOffset += length
+                } else {
+                    length = (w - 1) * pixelStride + 1
+                    buffer.get(rowData, 0, length)
+                    for (col in 0 until w) {
+                        data[channelOffset] = rowData[col * pixelStride]
+                        channelOffset += outputStride
+                    }
+                }
+                if (row < h - 1) {
+                    buffer.position(buffer.position() + rowStride - length)
+                }
+            }
+        }
+        return data
+    }
+
+}

+ 44 - 0
app/src/main/java/com/grkj/iscs/view/activity/test/face/arcsoft/NV21ToBitmap.java

@@ -0,0 +1,44 @@
+package com.grkj.iscs.view.activity.test.face.arcsoft;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicYuvToRGB;
+import android.renderscript.Type;
+
+public class NV21ToBitmap {
+
+    private RenderScript rs;
+    private ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic;
+    private Type.Builder yuvType, rgbaType;
+    private Allocation in, out;
+
+    public NV21ToBitmap(Context context) {
+        rs = RenderScript.create(context);
+        yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));
+    }
+
+    public Bitmap nv21ToBitmap(byte[] nv21, int width, int height){
+        if (yuvType == null){
+            yuvType = new Type.Builder(rs, Element.U8(rs)).setX(nv21.length);
+            in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);
+
+            rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
+            out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);
+        }
+
+        in.copyFrom(nv21);
+
+        yuvToRgbIntrinsic.setInput(in);
+        yuvToRgbIntrinsic.forEach(out);
+
+        Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        out.copyTo(bmpout);
+
+        return bmpout;
+
+    }
+
+}

BIN
app/src/main/jniLibs/arm64-v8a/libarcsoft_face.so


BIN
app/src/main/jniLibs/arm64-v8a/libarcsoft_face_engine.so


BIN
app/src/main/jniLibs/arm64-v8a/libarcsoft_image_util.so


BIN
app/src/main/jniLibs/armeabi-v7a/libarcsoft_face.so


BIN
app/src/main/jniLibs/armeabi-v7a/libarcsoft_face_engine.so


BIN
app/src/main/jniLibs/armeabi-v7a/libarcsoft_image_util.so


+ 25 - 0
app/src/main/res/layout/activity_arcsoft_test.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".view.activity.test.face.arcsoft.ArcsoftTestActivity">
+
+    <TextureView
+        android:id="@+id/texture_preview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+<!--    <RelativeLayout-->
+<!--        android:layout_width="match_parent"-->
+<!--        android:layout_height="match_parent"-->
+<!--        android:background="@color/white"/>-->
+
+    <ImageView
+        android:id="@+id/iv_capture"
+        android:layout_width="100dp"
+        android:layout_height="100dp"/>
+
+</RelativeLayout>

+ 10 - 0
app/src/main/res/layout/activity_main.xml

@@ -143,6 +143,16 @@
             android:text="Face"
             android:textSize="10sp" />
 
+        <Button
+            android:id="@+id/arcsoft"
+            android:layout_width="wrap_content"
+            android:layout_height="50dp"
+            android:layout_margin="5dp"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Arcsoft"
+            android:textSize="10sp" />
+
         <Button
             android:id="@+id/finger"
             android:layout_width="wrap_content"