diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt new file mode 100644 index 0000000..915c9b8 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt @@ -0,0 +1,232 @@ +package com.casic.br.operationsite.view + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.FaceDetector +import android.os.Handler +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import com.casic.br.operationsite.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.createImageFileDir +import com.pengxh.kt.lite.extensions.setScreenBrightness +import com.pengxh.kt.lite.extensions.toBitmap +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import kotlinx.android.synthetic.main.activity_face_detect.* +import kotlinx.android.synthetic.main.include_base_title.* +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.* +import kotlin.math.abs + + +class FaceDetectActivity : KotlinBaseActivity() { + + private val kTag = "FaceDetectActivity" + private lateinit var cameraExecutor: ExecutorService + private lateinit var cameraProviderFuture: ListenableFuture + private lateinit var imageCapture: ImageCapture + private lateinit var imageAnalysis: ImageAnalysis + private lateinit var weakReferenceHandler: WeakReferenceHandler + private val RATIO_4_3_VALUE = 4.0 / 3.0 + private val RATIO_16_9_VALUE = 16.0 / 9.0 + private val executor: ThreadPoolExecutor = ThreadPoolExecutor( + 16, 16, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(1024), + ThreadFactoryBuilder().setNameFormat("faceDetector-pool-%d").build(), + ThreadPoolExecutor.AbortPolicy() + ) + + override fun initLayoutView(): Int = R.layout.activity_face_detect + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(false).init() + ImmerseStatusBarUtil.setColor(this, R.color.mainThemeColor.convertColor(this)) + + leftBackView.setOnClickListener { finish() } + titleView.text = "入场申请人脸采集" + } + + override fun initData() { + //调节屏幕亮度最大 + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) + + // 初始化相机业务线程池 + cameraExecutor = Executors.newSingleThreadExecutor() + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + // 检查 CameraProvider 可用性 + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + bindPreview(cameraProvider) + } catch (e: ExecutionException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + }, ContextCompat.getMainExecutor(this)) + weakReferenceHandler = WeakReferenceHandler(callback) + } + + private fun bindPreview(@NonNull cameraProvider: ProcessCameraProvider) { + val screenAspectRatio: Int = if (android.os.Build.VERSION.SDK_INT >= 30) { + val metrics: Rect = windowManager.currentWindowMetrics.bounds + aspectRatio(metrics.width(), metrics.height()) + } else { + val outMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(outMetrics) + aspectRatio(outMetrics.widthPixels, outMetrics.heightPixels) + } + + // CameraSelector + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_FRONT) + .build() + + // Preview + val cameraPreViewBuilder = Preview.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageAnalysis + imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + try { + val camera = cameraProvider.bindToLifecycle( + this, + cameraSelector, + imageCapture, + imageAnalysis, + cameraPreViewBuilder + ) + cameraPreViewBuilder.setSurfaceProvider(cameraPreView.surfaceProvider) + observeCameraState(camera.cameraInfo) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun aspectRatio(width: Int, height: Int): Int { + val ratio = width.coerceAtLeast(height).toDouble() / width.coerceAtMost(height) + if (abs(ratio - RATIO_4_3_VALUE) <= abs(ratio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + + @SuppressLint("UnsafeOptInUsageError") + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(this, { + //开始预览之后才人脸检测 + if (it.type == CameraState.Type.OPEN) { + imageAnalysis.setAnalyzer(executor, { imageProxy -> + /** + * 相机源数据是 YUV_420_888 !!! + * */ + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val tempImage = imageProxy.image + if (tempImage != null) { + /** + * Android内置的人脸检测,需要将Bitmap对象转为RGB_565格式,否则无法识别 + * */ + val tempBitmap = tempImage.toBitmap(ImageFormat.YUV_420_888)!! + val faceDetectBitmap = tempBitmap.copy(Bitmap.Config.RGB_565, true) + val faces = arrayOfNulls(3) + // 一次只检测一张人脸 + val faceDetector = + FaceDetector(faceDetectBitmap.width, faceDetectBitmap.height, 1) + val faceCount = faceDetector.findFaces(faceDetectBitmap, faces) + /** + * 检测到人脸之后延迟几秒采集人脸数据 + * */ + if (faceCount > 0) { + weakReferenceHandler.sendEmptyMessageDelayed(2022071401, 3000) + } + } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() + } + } + }) + } + }) + } + + /** + * 人脸特征采集 + * */ + private val callback = Handler.Callback { + if (it.what == 2022071401) { + faceDetectTipsView.text = "人脸特征采集中,请勿晃动手机" + val fileOptions = ImageCapture.OutputFileOptions.Builder(createImageFile()).build() + imageCapture.takePicture(fileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val intent = Intent() + intent.putExtra("ImageUri", outputFileResults.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onError(exception: ImageCaptureException) { + exception.printStackTrace() + } + }) + } + true + } + + override fun initEvent() { + + } + + private fun createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date()) + val imageFile = + File(createImageFileDir().toString() + File.separator + "IMG_" + timeStamp + ".jpg") + if (!imageFile.exists()) { + try { + imageFile.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + } + return imageFile + } + + override fun onDestroy() { + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt new file mode 100644 index 0000000..915c9b8 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt @@ -0,0 +1,232 @@ +package com.casic.br.operationsite.view + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.FaceDetector +import android.os.Handler +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import com.casic.br.operationsite.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.createImageFileDir +import com.pengxh.kt.lite.extensions.setScreenBrightness +import com.pengxh.kt.lite.extensions.toBitmap +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import kotlinx.android.synthetic.main.activity_face_detect.* +import kotlinx.android.synthetic.main.include_base_title.* +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.* +import kotlin.math.abs + + +class FaceDetectActivity : KotlinBaseActivity() { + + private val kTag = "FaceDetectActivity" + private lateinit var cameraExecutor: ExecutorService + private lateinit var cameraProviderFuture: ListenableFuture + private lateinit var imageCapture: ImageCapture + private lateinit var imageAnalysis: ImageAnalysis + private lateinit var weakReferenceHandler: WeakReferenceHandler + private val RATIO_4_3_VALUE = 4.0 / 3.0 + private val RATIO_16_9_VALUE = 16.0 / 9.0 + private val executor: ThreadPoolExecutor = ThreadPoolExecutor( + 16, 16, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(1024), + ThreadFactoryBuilder().setNameFormat("faceDetector-pool-%d").build(), + ThreadPoolExecutor.AbortPolicy() + ) + + override fun initLayoutView(): Int = R.layout.activity_face_detect + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(false).init() + ImmerseStatusBarUtil.setColor(this, R.color.mainThemeColor.convertColor(this)) + + leftBackView.setOnClickListener { finish() } + titleView.text = "入场申请人脸采集" + } + + override fun initData() { + //调节屏幕亮度最大 + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) + + // 初始化相机业务线程池 + cameraExecutor = Executors.newSingleThreadExecutor() + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + // 检查 CameraProvider 可用性 + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + bindPreview(cameraProvider) + } catch (e: ExecutionException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + }, ContextCompat.getMainExecutor(this)) + weakReferenceHandler = WeakReferenceHandler(callback) + } + + private fun bindPreview(@NonNull cameraProvider: ProcessCameraProvider) { + val screenAspectRatio: Int = if (android.os.Build.VERSION.SDK_INT >= 30) { + val metrics: Rect = windowManager.currentWindowMetrics.bounds + aspectRatio(metrics.width(), metrics.height()) + } else { + val outMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(outMetrics) + aspectRatio(outMetrics.widthPixels, outMetrics.heightPixels) + } + + // CameraSelector + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_FRONT) + .build() + + // Preview + val cameraPreViewBuilder = Preview.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageAnalysis + imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + try { + val camera = cameraProvider.bindToLifecycle( + this, + cameraSelector, + imageCapture, + imageAnalysis, + cameraPreViewBuilder + ) + cameraPreViewBuilder.setSurfaceProvider(cameraPreView.surfaceProvider) + observeCameraState(camera.cameraInfo) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun aspectRatio(width: Int, height: Int): Int { + val ratio = width.coerceAtLeast(height).toDouble() / width.coerceAtMost(height) + if (abs(ratio - RATIO_4_3_VALUE) <= abs(ratio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + + @SuppressLint("UnsafeOptInUsageError") + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(this, { + //开始预览之后才人脸检测 + if (it.type == CameraState.Type.OPEN) { + imageAnalysis.setAnalyzer(executor, { imageProxy -> + /** + * 相机源数据是 YUV_420_888 !!! + * */ + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val tempImage = imageProxy.image + if (tempImage != null) { + /** + * Android内置的人脸检测,需要将Bitmap对象转为RGB_565格式,否则无法识别 + * */ + val tempBitmap = tempImage.toBitmap(ImageFormat.YUV_420_888)!! + val faceDetectBitmap = tempBitmap.copy(Bitmap.Config.RGB_565, true) + val faces = arrayOfNulls(3) + // 一次只检测一张人脸 + val faceDetector = + FaceDetector(faceDetectBitmap.width, faceDetectBitmap.height, 1) + val faceCount = faceDetector.findFaces(faceDetectBitmap, faces) + /** + * 检测到人脸之后延迟几秒采集人脸数据 + * */ + if (faceCount > 0) { + weakReferenceHandler.sendEmptyMessageDelayed(2022071401, 3000) + } + } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() + } + } + }) + } + }) + } + + /** + * 人脸特征采集 + * */ + private val callback = Handler.Callback { + if (it.what == 2022071401) { + faceDetectTipsView.text = "人脸特征采集中,请勿晃动手机" + val fileOptions = ImageCapture.OutputFileOptions.Builder(createImageFile()).build() + imageCapture.takePicture(fileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val intent = Intent() + intent.putExtra("ImageUri", outputFileResults.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onError(exception: ImageCaptureException) { + exception.printStackTrace() + } + }) + } + true + } + + override fun initEvent() { + + } + + private fun createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date()) + val imageFile = + File(createImageFileDir().toString() + File.separator + "IMG_" + timeStamp + ".jpg") + if (!imageFile.exists()) { + try { + imageFile.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + } + return imageFile + } + + override fun onDestroy() { + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt index 659dd6b..5627e2e 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt @@ -2,7 +2,6 @@ import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager -import com.amap.api.navi.NaviSetting import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.ViewPagerAdapter import com.casic.br.operationsite.fragment.CompletedFragment @@ -46,10 +45,6 @@ //默认显示实施中页面 mainViewPager.currentItem = 1 segmentedGroup.check(segmentedGroup.getChildAt(1).id) - - //先把导航隐私政策声明,后面导航会用到 - NaviSetting.updatePrivacyShow(this, true, true) - NaviSetting.updatePrivacyAgree(this, true) } override fun initEvent() { diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt new file mode 100644 index 0000000..915c9b8 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt @@ -0,0 +1,232 @@ +package com.casic.br.operationsite.view + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.FaceDetector +import android.os.Handler +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import com.casic.br.operationsite.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.createImageFileDir +import com.pengxh.kt.lite.extensions.setScreenBrightness +import com.pengxh.kt.lite.extensions.toBitmap +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import kotlinx.android.synthetic.main.activity_face_detect.* +import kotlinx.android.synthetic.main.include_base_title.* +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.* +import kotlin.math.abs + + +class FaceDetectActivity : KotlinBaseActivity() { + + private val kTag = "FaceDetectActivity" + private lateinit var cameraExecutor: ExecutorService + private lateinit var cameraProviderFuture: ListenableFuture + private lateinit var imageCapture: ImageCapture + private lateinit var imageAnalysis: ImageAnalysis + private lateinit var weakReferenceHandler: WeakReferenceHandler + private val RATIO_4_3_VALUE = 4.0 / 3.0 + private val RATIO_16_9_VALUE = 16.0 / 9.0 + private val executor: ThreadPoolExecutor = ThreadPoolExecutor( + 16, 16, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(1024), + ThreadFactoryBuilder().setNameFormat("faceDetector-pool-%d").build(), + ThreadPoolExecutor.AbortPolicy() + ) + + override fun initLayoutView(): Int = R.layout.activity_face_detect + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(false).init() + ImmerseStatusBarUtil.setColor(this, R.color.mainThemeColor.convertColor(this)) + + leftBackView.setOnClickListener { finish() } + titleView.text = "入场申请人脸采集" + } + + override fun initData() { + //调节屏幕亮度最大 + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) + + // 初始化相机业务线程池 + cameraExecutor = Executors.newSingleThreadExecutor() + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + // 检查 CameraProvider 可用性 + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + bindPreview(cameraProvider) + } catch (e: ExecutionException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + }, ContextCompat.getMainExecutor(this)) + weakReferenceHandler = WeakReferenceHandler(callback) + } + + private fun bindPreview(@NonNull cameraProvider: ProcessCameraProvider) { + val screenAspectRatio: Int = if (android.os.Build.VERSION.SDK_INT >= 30) { + val metrics: Rect = windowManager.currentWindowMetrics.bounds + aspectRatio(metrics.width(), metrics.height()) + } else { + val outMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(outMetrics) + aspectRatio(outMetrics.widthPixels, outMetrics.heightPixels) + } + + // CameraSelector + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_FRONT) + .build() + + // Preview + val cameraPreViewBuilder = Preview.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageAnalysis + imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + try { + val camera = cameraProvider.bindToLifecycle( + this, + cameraSelector, + imageCapture, + imageAnalysis, + cameraPreViewBuilder + ) + cameraPreViewBuilder.setSurfaceProvider(cameraPreView.surfaceProvider) + observeCameraState(camera.cameraInfo) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun aspectRatio(width: Int, height: Int): Int { + val ratio = width.coerceAtLeast(height).toDouble() / width.coerceAtMost(height) + if (abs(ratio - RATIO_4_3_VALUE) <= abs(ratio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + + @SuppressLint("UnsafeOptInUsageError") + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(this, { + //开始预览之后才人脸检测 + if (it.type == CameraState.Type.OPEN) { + imageAnalysis.setAnalyzer(executor, { imageProxy -> + /** + * 相机源数据是 YUV_420_888 !!! + * */ + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val tempImage = imageProxy.image + if (tempImage != null) { + /** + * Android内置的人脸检测,需要将Bitmap对象转为RGB_565格式,否则无法识别 + * */ + val tempBitmap = tempImage.toBitmap(ImageFormat.YUV_420_888)!! + val faceDetectBitmap = tempBitmap.copy(Bitmap.Config.RGB_565, true) + val faces = arrayOfNulls(3) + // 一次只检测一张人脸 + val faceDetector = + FaceDetector(faceDetectBitmap.width, faceDetectBitmap.height, 1) + val faceCount = faceDetector.findFaces(faceDetectBitmap, faces) + /** + * 检测到人脸之后延迟几秒采集人脸数据 + * */ + if (faceCount > 0) { + weakReferenceHandler.sendEmptyMessageDelayed(2022071401, 3000) + } + } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() + } + } + }) + } + }) + } + + /** + * 人脸特征采集 + * */ + private val callback = Handler.Callback { + if (it.what == 2022071401) { + faceDetectTipsView.text = "人脸特征采集中,请勿晃动手机" + val fileOptions = ImageCapture.OutputFileOptions.Builder(createImageFile()).build() + imageCapture.takePicture(fileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val intent = Intent() + intent.putExtra("ImageUri", outputFileResults.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onError(exception: ImageCaptureException) { + exception.printStackTrace() + } + }) + } + true + } + + override fun initEvent() { + + } + + private fun createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date()) + val imageFile = + File(createImageFileDir().toString() + File.separator + "IMG_" + timeStamp + ".jpg") + if (!imageFile.exists()) { + try { + imageFile.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + } + return imageFile + } + + override fun onDestroy() { + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt index 659dd6b..5627e2e 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt @@ -2,7 +2,6 @@ import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager -import com.amap.api.navi.NaviSetting import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.ViewPagerAdapter import com.casic.br.operationsite.fragment.CompletedFragment @@ -46,10 +45,6 @@ //默认显示实施中页面 mainViewPager.currentItem = 1 segmentedGroup.check(segmentedGroup.getChildAt(1).id) - - //先把导航隐私政策声明,后面导航会用到 - NaviSetting.updatePrivacyShow(this, true, true) - NaviSetting.updatePrivacyAgree(this, true) } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt new file mode 100644 index 0000000..74a7298 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt @@ -0,0 +1,66 @@ +package com.casic.br.operationsite.view + +import com.amap.api.navi.NaviSetting +import com.casic.br.operationsite.R +import com.casic.br.operationsite.utils.LocaleConstant +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.navigatePageTo +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import kotlinx.android.synthetic.main.activity_permssion.* +import pub.devrel.easypermissions.EasyPermissions +import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks + +class PermissionActivity : KotlinBaseActivity(), PermissionCallbacks { + + override fun initLayoutView(): Int = R.layout.activity_permssion + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(true).init() + ImmerseStatusBarUtil.setColor(this, R.color.white.convertColor(this)) + } + + override fun initData() { + //判断是否有权限,如果版本大于5.1才需要判断(即6.0以上),其他则不需要判断。 + if (EasyPermissions.hasPermissions(this, *LocaleConstant.USER_PERMISSIONS)) { + startMainActivity() + } else { + enterMainButton.setOnClickListener { + EasyPermissions.requestPermissions( + this@PermissionActivity, + resources.getString(R.string.app_name) + "需要获取存储相关权限", + LocaleConstant.PERMISSIONS_CODE, + *LocaleConstant.USER_PERMISSIONS + ) + } + } + } + + override fun initEvent() { + + } + + private fun startMainActivity() { + //先把导航隐私政策声明,后面导航会用到 + NaviSetting.updatePrivacyShow(this, true, true) + NaviSetting.updatePrivacyAgree(this, true) + navigatePageTo() + finish() + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + override fun onPermissionsGranted(requestCode: Int, perms: List) { + startMainActivity() + } + + override fun onPermissionsDenied(requestCode: Int, perms: List) { + + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt new file mode 100644 index 0000000..915c9b8 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt @@ -0,0 +1,232 @@ +package com.casic.br.operationsite.view + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.FaceDetector +import android.os.Handler +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import com.casic.br.operationsite.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.createImageFileDir +import com.pengxh.kt.lite.extensions.setScreenBrightness +import com.pengxh.kt.lite.extensions.toBitmap +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import kotlinx.android.synthetic.main.activity_face_detect.* +import kotlinx.android.synthetic.main.include_base_title.* +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.* +import kotlin.math.abs + + +class FaceDetectActivity : KotlinBaseActivity() { + + private val kTag = "FaceDetectActivity" + private lateinit var cameraExecutor: ExecutorService + private lateinit var cameraProviderFuture: ListenableFuture + private lateinit var imageCapture: ImageCapture + private lateinit var imageAnalysis: ImageAnalysis + private lateinit var weakReferenceHandler: WeakReferenceHandler + private val RATIO_4_3_VALUE = 4.0 / 3.0 + private val RATIO_16_9_VALUE = 16.0 / 9.0 + private val executor: ThreadPoolExecutor = ThreadPoolExecutor( + 16, 16, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(1024), + ThreadFactoryBuilder().setNameFormat("faceDetector-pool-%d").build(), + ThreadPoolExecutor.AbortPolicy() + ) + + override fun initLayoutView(): Int = R.layout.activity_face_detect + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(false).init() + ImmerseStatusBarUtil.setColor(this, R.color.mainThemeColor.convertColor(this)) + + leftBackView.setOnClickListener { finish() } + titleView.text = "入场申请人脸采集" + } + + override fun initData() { + //调节屏幕亮度最大 + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) + + // 初始化相机业务线程池 + cameraExecutor = Executors.newSingleThreadExecutor() + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + // 检查 CameraProvider 可用性 + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + bindPreview(cameraProvider) + } catch (e: ExecutionException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + }, ContextCompat.getMainExecutor(this)) + weakReferenceHandler = WeakReferenceHandler(callback) + } + + private fun bindPreview(@NonNull cameraProvider: ProcessCameraProvider) { + val screenAspectRatio: Int = if (android.os.Build.VERSION.SDK_INT >= 30) { + val metrics: Rect = windowManager.currentWindowMetrics.bounds + aspectRatio(metrics.width(), metrics.height()) + } else { + val outMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(outMetrics) + aspectRatio(outMetrics.widthPixels, outMetrics.heightPixels) + } + + // CameraSelector + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_FRONT) + .build() + + // Preview + val cameraPreViewBuilder = Preview.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageAnalysis + imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + try { + val camera = cameraProvider.bindToLifecycle( + this, + cameraSelector, + imageCapture, + imageAnalysis, + cameraPreViewBuilder + ) + cameraPreViewBuilder.setSurfaceProvider(cameraPreView.surfaceProvider) + observeCameraState(camera.cameraInfo) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun aspectRatio(width: Int, height: Int): Int { + val ratio = width.coerceAtLeast(height).toDouble() / width.coerceAtMost(height) + if (abs(ratio - RATIO_4_3_VALUE) <= abs(ratio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + + @SuppressLint("UnsafeOptInUsageError") + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(this, { + //开始预览之后才人脸检测 + if (it.type == CameraState.Type.OPEN) { + imageAnalysis.setAnalyzer(executor, { imageProxy -> + /** + * 相机源数据是 YUV_420_888 !!! + * */ + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val tempImage = imageProxy.image + if (tempImage != null) { + /** + * Android内置的人脸检测,需要将Bitmap对象转为RGB_565格式,否则无法识别 + * */ + val tempBitmap = tempImage.toBitmap(ImageFormat.YUV_420_888)!! + val faceDetectBitmap = tempBitmap.copy(Bitmap.Config.RGB_565, true) + val faces = arrayOfNulls(3) + // 一次只检测一张人脸 + val faceDetector = + FaceDetector(faceDetectBitmap.width, faceDetectBitmap.height, 1) + val faceCount = faceDetector.findFaces(faceDetectBitmap, faces) + /** + * 检测到人脸之后延迟几秒采集人脸数据 + * */ + if (faceCount > 0) { + weakReferenceHandler.sendEmptyMessageDelayed(2022071401, 3000) + } + } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() + } + } + }) + } + }) + } + + /** + * 人脸特征采集 + * */ + private val callback = Handler.Callback { + if (it.what == 2022071401) { + faceDetectTipsView.text = "人脸特征采集中,请勿晃动手机" + val fileOptions = ImageCapture.OutputFileOptions.Builder(createImageFile()).build() + imageCapture.takePicture(fileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val intent = Intent() + intent.putExtra("ImageUri", outputFileResults.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onError(exception: ImageCaptureException) { + exception.printStackTrace() + } + }) + } + true + } + + override fun initEvent() { + + } + + private fun createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date()) + val imageFile = + File(createImageFileDir().toString() + File.separator + "IMG_" + timeStamp + ".jpg") + if (!imageFile.exists()) { + try { + imageFile.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + } + return imageFile + } + + override fun onDestroy() { + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt index 659dd6b..5627e2e 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt @@ -2,7 +2,6 @@ import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager -import com.amap.api.navi.NaviSetting import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.ViewPagerAdapter import com.casic.br.operationsite.fragment.CompletedFragment @@ -46,10 +45,6 @@ //默认显示实施中页面 mainViewPager.currentItem = 1 segmentedGroup.check(segmentedGroup.getChildAt(1).id) - - //先把导航隐私政策声明,后面导航会用到 - NaviSetting.updatePrivacyShow(this, true, true) - NaviSetting.updatePrivacyAgree(this, true) } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt new file mode 100644 index 0000000..74a7298 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt @@ -0,0 +1,66 @@ +package com.casic.br.operationsite.view + +import com.amap.api.navi.NaviSetting +import com.casic.br.operationsite.R +import com.casic.br.operationsite.utils.LocaleConstant +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.navigatePageTo +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import kotlinx.android.synthetic.main.activity_permssion.* +import pub.devrel.easypermissions.EasyPermissions +import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks + +class PermissionActivity : KotlinBaseActivity(), PermissionCallbacks { + + override fun initLayoutView(): Int = R.layout.activity_permssion + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(true).init() + ImmerseStatusBarUtil.setColor(this, R.color.white.convertColor(this)) + } + + override fun initData() { + //判断是否有权限,如果版本大于5.1才需要判断(即6.0以上),其他则不需要判断。 + if (EasyPermissions.hasPermissions(this, *LocaleConstant.USER_PERMISSIONS)) { + startMainActivity() + } else { + enterMainButton.setOnClickListener { + EasyPermissions.requestPermissions( + this@PermissionActivity, + resources.getString(R.string.app_name) + "需要获取存储相关权限", + LocaleConstant.PERMISSIONS_CODE, + *LocaleConstant.USER_PERMISSIONS + ) + } + } + } + + override fun initEvent() { + + } + + private fun startMainActivity() { + //先把导航隐私政策声明,后面导航会用到 + NaviSetting.updatePrivacyShow(this, true, true) + NaviSetting.updatePrivacyAgree(this, true) + navigatePageTo() + finish() + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + override fun onPermissionsGranted(requestCode: Int, perms: List) { + startMainActivity() + } + + override fun onPermissionsDenied(requestCode: Int, perms: List) { + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000..b612ce9 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt new file mode 100644 index 0000000..915c9b8 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt @@ -0,0 +1,232 @@ +package com.casic.br.operationsite.view + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.FaceDetector +import android.os.Handler +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import com.casic.br.operationsite.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.createImageFileDir +import com.pengxh.kt.lite.extensions.setScreenBrightness +import com.pengxh.kt.lite.extensions.toBitmap +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import kotlinx.android.synthetic.main.activity_face_detect.* +import kotlinx.android.synthetic.main.include_base_title.* +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.* +import kotlin.math.abs + + +class FaceDetectActivity : KotlinBaseActivity() { + + private val kTag = "FaceDetectActivity" + private lateinit var cameraExecutor: ExecutorService + private lateinit var cameraProviderFuture: ListenableFuture + private lateinit var imageCapture: ImageCapture + private lateinit var imageAnalysis: ImageAnalysis + private lateinit var weakReferenceHandler: WeakReferenceHandler + private val RATIO_4_3_VALUE = 4.0 / 3.0 + private val RATIO_16_9_VALUE = 16.0 / 9.0 + private val executor: ThreadPoolExecutor = ThreadPoolExecutor( + 16, 16, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(1024), + ThreadFactoryBuilder().setNameFormat("faceDetector-pool-%d").build(), + ThreadPoolExecutor.AbortPolicy() + ) + + override fun initLayoutView(): Int = R.layout.activity_face_detect + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(false).init() + ImmerseStatusBarUtil.setColor(this, R.color.mainThemeColor.convertColor(this)) + + leftBackView.setOnClickListener { finish() } + titleView.text = "入场申请人脸采集" + } + + override fun initData() { + //调节屏幕亮度最大 + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) + + // 初始化相机业务线程池 + cameraExecutor = Executors.newSingleThreadExecutor() + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + // 检查 CameraProvider 可用性 + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + bindPreview(cameraProvider) + } catch (e: ExecutionException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + }, ContextCompat.getMainExecutor(this)) + weakReferenceHandler = WeakReferenceHandler(callback) + } + + private fun bindPreview(@NonNull cameraProvider: ProcessCameraProvider) { + val screenAspectRatio: Int = if (android.os.Build.VERSION.SDK_INT >= 30) { + val metrics: Rect = windowManager.currentWindowMetrics.bounds + aspectRatio(metrics.width(), metrics.height()) + } else { + val outMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(outMetrics) + aspectRatio(outMetrics.widthPixels, outMetrics.heightPixels) + } + + // CameraSelector + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_FRONT) + .build() + + // Preview + val cameraPreViewBuilder = Preview.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageAnalysis + imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + try { + val camera = cameraProvider.bindToLifecycle( + this, + cameraSelector, + imageCapture, + imageAnalysis, + cameraPreViewBuilder + ) + cameraPreViewBuilder.setSurfaceProvider(cameraPreView.surfaceProvider) + observeCameraState(camera.cameraInfo) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun aspectRatio(width: Int, height: Int): Int { + val ratio = width.coerceAtLeast(height).toDouble() / width.coerceAtMost(height) + if (abs(ratio - RATIO_4_3_VALUE) <= abs(ratio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + + @SuppressLint("UnsafeOptInUsageError") + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(this, { + //开始预览之后才人脸检测 + if (it.type == CameraState.Type.OPEN) { + imageAnalysis.setAnalyzer(executor, { imageProxy -> + /** + * 相机源数据是 YUV_420_888 !!! + * */ + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val tempImage = imageProxy.image + if (tempImage != null) { + /** + * Android内置的人脸检测,需要将Bitmap对象转为RGB_565格式,否则无法识别 + * */ + val tempBitmap = tempImage.toBitmap(ImageFormat.YUV_420_888)!! + val faceDetectBitmap = tempBitmap.copy(Bitmap.Config.RGB_565, true) + val faces = arrayOfNulls(3) + // 一次只检测一张人脸 + val faceDetector = + FaceDetector(faceDetectBitmap.width, faceDetectBitmap.height, 1) + val faceCount = faceDetector.findFaces(faceDetectBitmap, faces) + /** + * 检测到人脸之后延迟几秒采集人脸数据 + * */ + if (faceCount > 0) { + weakReferenceHandler.sendEmptyMessageDelayed(2022071401, 3000) + } + } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() + } + } + }) + } + }) + } + + /** + * 人脸特征采集 + * */ + private val callback = Handler.Callback { + if (it.what == 2022071401) { + faceDetectTipsView.text = "人脸特征采集中,请勿晃动手机" + val fileOptions = ImageCapture.OutputFileOptions.Builder(createImageFile()).build() + imageCapture.takePicture(fileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val intent = Intent() + intent.putExtra("ImageUri", outputFileResults.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onError(exception: ImageCaptureException) { + exception.printStackTrace() + } + }) + } + true + } + + override fun initEvent() { + + } + + private fun createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date()) + val imageFile = + File(createImageFileDir().toString() + File.separator + "IMG_" + timeStamp + ".jpg") + if (!imageFile.exists()) { + try { + imageFile.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + } + return imageFile + } + + override fun onDestroy() { + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt index 659dd6b..5627e2e 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt @@ -2,7 +2,6 @@ import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager -import com.amap.api.navi.NaviSetting import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.ViewPagerAdapter import com.casic.br.operationsite.fragment.CompletedFragment @@ -46,10 +45,6 @@ //默认显示实施中页面 mainViewPager.currentItem = 1 segmentedGroup.check(segmentedGroup.getChildAt(1).id) - - //先把导航隐私政策声明,后面导航会用到 - NaviSetting.updatePrivacyShow(this, true, true) - NaviSetting.updatePrivacyAgree(this, true) } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt new file mode 100644 index 0000000..74a7298 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt @@ -0,0 +1,66 @@ +package com.casic.br.operationsite.view + +import com.amap.api.navi.NaviSetting +import com.casic.br.operationsite.R +import com.casic.br.operationsite.utils.LocaleConstant +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.navigatePageTo +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import kotlinx.android.synthetic.main.activity_permssion.* +import pub.devrel.easypermissions.EasyPermissions +import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks + +class PermissionActivity : KotlinBaseActivity(), PermissionCallbacks { + + override fun initLayoutView(): Int = R.layout.activity_permssion + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(true).init() + ImmerseStatusBarUtil.setColor(this, R.color.white.convertColor(this)) + } + + override fun initData() { + //判断是否有权限,如果版本大于5.1才需要判断(即6.0以上),其他则不需要判断。 + if (EasyPermissions.hasPermissions(this, *LocaleConstant.USER_PERMISSIONS)) { + startMainActivity() + } else { + enterMainButton.setOnClickListener { + EasyPermissions.requestPermissions( + this@PermissionActivity, + resources.getString(R.string.app_name) + "需要获取存储相关权限", + LocaleConstant.PERMISSIONS_CODE, + *LocaleConstant.USER_PERMISSIONS + ) + } + } + } + + override fun initEvent() { + + } + + private fun startMainActivity() { + //先把导航隐私政策声明,后面导航会用到 + NaviSetting.updatePrivacyShow(this, true, true) + NaviSetting.updatePrivacyAgree(this, true) + navigatePageTo() + finish() + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + override fun onPermissionsGranted(requestCode: Int, perms: List) { + startMainActivity() + } + + override fun onPermissionsDenied(requestCode: Int, perms: List) { + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000..b612ce9 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_storage.xml b/app/src/main/res/drawable/ic_storage.xml new file mode 100644 index 0000000..898c1cf --- /dev/null +++ b/app/src/main/res/drawable/ic_storage.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt new file mode 100644 index 0000000..915c9b8 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt @@ -0,0 +1,232 @@ +package com.casic.br.operationsite.view + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.FaceDetector +import android.os.Handler +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import com.casic.br.operationsite.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.createImageFileDir +import com.pengxh.kt.lite.extensions.setScreenBrightness +import com.pengxh.kt.lite.extensions.toBitmap +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import kotlinx.android.synthetic.main.activity_face_detect.* +import kotlinx.android.synthetic.main.include_base_title.* +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.* +import kotlin.math.abs + + +class FaceDetectActivity : KotlinBaseActivity() { + + private val kTag = "FaceDetectActivity" + private lateinit var cameraExecutor: ExecutorService + private lateinit var cameraProviderFuture: ListenableFuture + private lateinit var imageCapture: ImageCapture + private lateinit var imageAnalysis: ImageAnalysis + private lateinit var weakReferenceHandler: WeakReferenceHandler + private val RATIO_4_3_VALUE = 4.0 / 3.0 + private val RATIO_16_9_VALUE = 16.0 / 9.0 + private val executor: ThreadPoolExecutor = ThreadPoolExecutor( + 16, 16, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(1024), + ThreadFactoryBuilder().setNameFormat("faceDetector-pool-%d").build(), + ThreadPoolExecutor.AbortPolicy() + ) + + override fun initLayoutView(): Int = R.layout.activity_face_detect + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(false).init() + ImmerseStatusBarUtil.setColor(this, R.color.mainThemeColor.convertColor(this)) + + leftBackView.setOnClickListener { finish() } + titleView.text = "入场申请人脸采集" + } + + override fun initData() { + //调节屏幕亮度最大 + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) + + // 初始化相机业务线程池 + cameraExecutor = Executors.newSingleThreadExecutor() + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + // 检查 CameraProvider 可用性 + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + bindPreview(cameraProvider) + } catch (e: ExecutionException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + }, ContextCompat.getMainExecutor(this)) + weakReferenceHandler = WeakReferenceHandler(callback) + } + + private fun bindPreview(@NonNull cameraProvider: ProcessCameraProvider) { + val screenAspectRatio: Int = if (android.os.Build.VERSION.SDK_INT >= 30) { + val metrics: Rect = windowManager.currentWindowMetrics.bounds + aspectRatio(metrics.width(), metrics.height()) + } else { + val outMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(outMetrics) + aspectRatio(outMetrics.widthPixels, outMetrics.heightPixels) + } + + // CameraSelector + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_FRONT) + .build() + + // Preview + val cameraPreViewBuilder = Preview.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageAnalysis + imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + try { + val camera = cameraProvider.bindToLifecycle( + this, + cameraSelector, + imageCapture, + imageAnalysis, + cameraPreViewBuilder + ) + cameraPreViewBuilder.setSurfaceProvider(cameraPreView.surfaceProvider) + observeCameraState(camera.cameraInfo) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun aspectRatio(width: Int, height: Int): Int { + val ratio = width.coerceAtLeast(height).toDouble() / width.coerceAtMost(height) + if (abs(ratio - RATIO_4_3_VALUE) <= abs(ratio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + + @SuppressLint("UnsafeOptInUsageError") + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(this, { + //开始预览之后才人脸检测 + if (it.type == CameraState.Type.OPEN) { + imageAnalysis.setAnalyzer(executor, { imageProxy -> + /** + * 相机源数据是 YUV_420_888 !!! + * */ + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val tempImage = imageProxy.image + if (tempImage != null) { + /** + * Android内置的人脸检测,需要将Bitmap对象转为RGB_565格式,否则无法识别 + * */ + val tempBitmap = tempImage.toBitmap(ImageFormat.YUV_420_888)!! + val faceDetectBitmap = tempBitmap.copy(Bitmap.Config.RGB_565, true) + val faces = arrayOfNulls(3) + // 一次只检测一张人脸 + val faceDetector = + FaceDetector(faceDetectBitmap.width, faceDetectBitmap.height, 1) + val faceCount = faceDetector.findFaces(faceDetectBitmap, faces) + /** + * 检测到人脸之后延迟几秒采集人脸数据 + * */ + if (faceCount > 0) { + weakReferenceHandler.sendEmptyMessageDelayed(2022071401, 3000) + } + } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() + } + } + }) + } + }) + } + + /** + * 人脸特征采集 + * */ + private val callback = Handler.Callback { + if (it.what == 2022071401) { + faceDetectTipsView.text = "人脸特征采集中,请勿晃动手机" + val fileOptions = ImageCapture.OutputFileOptions.Builder(createImageFile()).build() + imageCapture.takePicture(fileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val intent = Intent() + intent.putExtra("ImageUri", outputFileResults.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onError(exception: ImageCaptureException) { + exception.printStackTrace() + } + }) + } + true + } + + override fun initEvent() { + + } + + private fun createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date()) + val imageFile = + File(createImageFileDir().toString() + File.separator + "IMG_" + timeStamp + ".jpg") + if (!imageFile.exists()) { + try { + imageFile.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + } + return imageFile + } + + override fun onDestroy() { + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt index 659dd6b..5627e2e 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt @@ -2,7 +2,6 @@ import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager -import com.amap.api.navi.NaviSetting import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.ViewPagerAdapter import com.casic.br.operationsite.fragment.CompletedFragment @@ -46,10 +45,6 @@ //默认显示实施中页面 mainViewPager.currentItem = 1 segmentedGroup.check(segmentedGroup.getChildAt(1).id) - - //先把导航隐私政策声明,后面导航会用到 - NaviSetting.updatePrivacyShow(this, true, true) - NaviSetting.updatePrivacyAgree(this, true) } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt new file mode 100644 index 0000000..74a7298 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt @@ -0,0 +1,66 @@ +package com.casic.br.operationsite.view + +import com.amap.api.navi.NaviSetting +import com.casic.br.operationsite.R +import com.casic.br.operationsite.utils.LocaleConstant +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.navigatePageTo +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import kotlinx.android.synthetic.main.activity_permssion.* +import pub.devrel.easypermissions.EasyPermissions +import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks + +class PermissionActivity : KotlinBaseActivity(), PermissionCallbacks { + + override fun initLayoutView(): Int = R.layout.activity_permssion + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(true).init() + ImmerseStatusBarUtil.setColor(this, R.color.white.convertColor(this)) + } + + override fun initData() { + //判断是否有权限,如果版本大于5.1才需要判断(即6.0以上),其他则不需要判断。 + if (EasyPermissions.hasPermissions(this, *LocaleConstant.USER_PERMISSIONS)) { + startMainActivity() + } else { + enterMainButton.setOnClickListener { + EasyPermissions.requestPermissions( + this@PermissionActivity, + resources.getString(R.string.app_name) + "需要获取存储相关权限", + LocaleConstant.PERMISSIONS_CODE, + *LocaleConstant.USER_PERMISSIONS + ) + } + } + } + + override fun initEvent() { + + } + + private fun startMainActivity() { + //先把导航隐私政策声明,后面导航会用到 + NaviSetting.updatePrivacyShow(this, true, true) + NaviSetting.updatePrivacyAgree(this, true) + navigatePageTo() + finish() + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + override fun onPermissionsGranted(requestCode: Int, perms: List) { + startMainActivity() + } + + override fun onPermissionsDenied(requestCode: Int, perms: List) { + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000..b612ce9 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_storage.xml b/app/src/main/res/drawable/ic_storage.xml new file mode 100644 index 0000000..898c1cf --- /dev/null +++ b/app/src/main/res/drawable/ic_storage.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_face_detect.xml b/app/src/main/res/layout/activity_face_detect.xml new file mode 100644 index 0000000..9cb962d --- /dev/null +++ b/app/src/main/res/layout/activity_face_detect.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8dedc1d..019b44e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,4 +87,10 @@ implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' //高德导航(带有地图,无需再依赖地图) implementation 'com.amap.api:navi-3dmap:8.1.0_3dmap8.1.0' + //CameraX + def CameraX_version = '1.1.0' + implementation "androidx.camera:camera-core:${CameraX_version}" + implementation "androidx.camera:camera-camera2:${CameraX_version}" + implementation "androidx.camera:camera-lifecycle:${CameraX_version}" + implementation 'androidx.camera:camera-view:1.0.0-alpha32' } \ No newline at end of file diff --git a/app/libs/lite-release.aar b/app/libs/lite-release.aar index 706b6c7..a6008fa 100644 --- a/app/libs/lite-release.aar +++ b/app/libs/lite-release.aar Binary files differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55e187f..b0bd61a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -9,7 +10,11 @@ + + @@ -32,6 +37,7 @@ + @@ -41,6 +47,10 @@ android:theme="@style/Theme.BigImageActivity" /> + + if (result.resultCode == RESULT_OK) { + val data = result.data!! + val imageUri = data.getStringExtra("ImageUri") + if (imageUri.isNullOrBlank()) { + "人脸注册失败,请重试".show(context) + return@registerForActivityResult + } + val uri = Uri.parse(imageUri) + Log.d(kTag, "uri: $uri") + val realFilePath = uri.realFilePath(this) + Log.d(kTag, "realFilePath: $realFilePath") + + //暂时直接显示图片 + realPaths.add(realFilePath) + imageAdapter.setupImage(realPaths) + + //上传图片 + } + } + override fun initEvent() { imageAdapter.setOnItemClickListener(object : EditableImageAdapter.OnItemClickListener { override fun onAddImageClick() { - PictureSelector.create(context) - .openCamera(SelectMimeType.ofImage()) - .forResult(object : OnResultCallbackListener { - override fun onResult(result: ArrayList?) { - if (result == null) { - "拍照保存失败,请重试".show(context) - return - } - analyticalSelectResults(result[0]) - } - - override fun onCancel() { - - } - }) + //人脸检测 + faceDetectLauncher.launch(Intent(context, FaceDetectActivity::class.java)) } override fun onItemClick(position: Int) { @@ -109,28 +116,4 @@ } } - - private fun analyticalSelectResults(result: LocalMedia) { - //压缩图片 -// val realPath = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -// result.realPath -// } else { -// result.sandboxPath -// } -// Log.d(kTag, "初始路径:" + result.path) -// Log.d(kTag, "绝对路径:" + result.realPath) -// Log.d(kTag, "原图路径:" + result.originalPath) -// Log.d(kTag, "沙盒路径:" + result.sandboxPath) - result.realPath.compressImage(this, object : OnImageCompressListener { - override fun onSuccess(file: File) { - Log.d(kTag, "onSuccess: " + file.absolutePath) - //上传图片 - uploadFileViewModel.uploadImage(file) - } - - override fun onError(e: Throwable) { - e.printStackTrace() - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt new file mode 100644 index 0000000..915c9b8 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/FaceDetectActivity.kt @@ -0,0 +1,232 @@ +package com.casic.br.operationsite.view + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.FaceDetector +import android.os.Handler +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import com.casic.br.operationsite.R +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.createImageFileDir +import com.pengxh.kt.lite.extensions.setScreenBrightness +import com.pengxh.kt.lite.extensions.toBitmap +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import kotlinx.android.synthetic.main.activity_face_detect.* +import kotlinx.android.synthetic.main.include_base_title.* +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.* +import kotlin.math.abs + + +class FaceDetectActivity : KotlinBaseActivity() { + + private val kTag = "FaceDetectActivity" + private lateinit var cameraExecutor: ExecutorService + private lateinit var cameraProviderFuture: ListenableFuture + private lateinit var imageCapture: ImageCapture + private lateinit var imageAnalysis: ImageAnalysis + private lateinit var weakReferenceHandler: WeakReferenceHandler + private val RATIO_4_3_VALUE = 4.0 / 3.0 + private val RATIO_16_9_VALUE = 16.0 / 9.0 + private val executor: ThreadPoolExecutor = ThreadPoolExecutor( + 16, 16, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(1024), + ThreadFactoryBuilder().setNameFormat("faceDetector-pool-%d").build(), + ThreadPoolExecutor.AbortPolicy() + ) + + override fun initLayoutView(): Int = R.layout.activity_face_detect + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(false).init() + ImmerseStatusBarUtil.setColor(this, R.color.mainThemeColor.convertColor(this)) + + leftBackView.setOnClickListener { finish() } + titleView.text = "入场申请人脸采集" + } + + override fun initData() { + //调节屏幕亮度最大 + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL) + + // 初始化相机业务线程池 + cameraExecutor = Executors.newSingleThreadExecutor() + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + // 检查 CameraProvider 可用性 + cameraProviderFuture.addListener({ + try { + val cameraProvider = cameraProviderFuture.get() + bindPreview(cameraProvider) + } catch (e: ExecutionException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + }, ContextCompat.getMainExecutor(this)) + weakReferenceHandler = WeakReferenceHandler(callback) + } + + private fun bindPreview(@NonNull cameraProvider: ProcessCameraProvider) { + val screenAspectRatio: Int = if (android.os.Build.VERSION.SDK_INT >= 30) { + val metrics: Rect = windowManager.currentWindowMetrics.bounds + aspectRatio(metrics.width(), metrics.height()) + } else { + val outMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(outMetrics) + aspectRatio(outMetrics.widthPixels, outMetrics.heightPixels) + } + + // CameraSelector + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_FRONT) + .build() + + // Preview + val cameraPreViewBuilder = Preview.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // ImageAnalysis + imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(Surface.ROTATION_0) + .build() + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + try { + val camera = cameraProvider.bindToLifecycle( + this, + cameraSelector, + imageCapture, + imageAnalysis, + cameraPreViewBuilder + ) + cameraPreViewBuilder.setSurfaceProvider(cameraPreView.surfaceProvider) + observeCameraState(camera.cameraInfo) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun aspectRatio(width: Int, height: Int): Int { + val ratio = width.coerceAtLeast(height).toDouble() / width.coerceAtMost(height) + if (abs(ratio - RATIO_4_3_VALUE) <= abs(ratio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + + @SuppressLint("UnsafeOptInUsageError") + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(this, { + //开始预览之后才人脸检测 + if (it.type == CameraState.Type.OPEN) { + imageAnalysis.setAnalyzer(executor, { imageProxy -> + /** + * 相机源数据是 YUV_420_888 !!! + * */ + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val tempImage = imageProxy.image + if (tempImage != null) { + /** + * Android内置的人脸检测,需要将Bitmap对象转为RGB_565格式,否则无法识别 + * */ + val tempBitmap = tempImage.toBitmap(ImageFormat.YUV_420_888)!! + val faceDetectBitmap = tempBitmap.copy(Bitmap.Config.RGB_565, true) + val faces = arrayOfNulls(3) + // 一次只检测一张人脸 + val faceDetector = + FaceDetector(faceDetectBitmap.width, faceDetectBitmap.height, 1) + val faceCount = faceDetector.findFaces(faceDetectBitmap, faces) + /** + * 检测到人脸之后延迟几秒采集人脸数据 + * */ + if (faceCount > 0) { + weakReferenceHandler.sendEmptyMessageDelayed(2022071401, 3000) + } + } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() + } + } + }) + } + }) + } + + /** + * 人脸特征采集 + * */ + private val callback = Handler.Callback { + if (it.what == 2022071401) { + faceDetectTipsView.text = "人脸特征采集中,请勿晃动手机" + val fileOptions = ImageCapture.OutputFileOptions.Builder(createImageFile()).build() + imageCapture.takePicture(fileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val intent = Intent() + intent.putExtra("ImageUri", outputFileResults.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onError(exception: ImageCaptureException) { + exception.printStackTrace() + } + }) + } + true + } + + override fun initEvent() { + + } + + private fun createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date()) + val imageFile = + File(createImageFileDir().toString() + File.separator + "IMG_" + timeStamp + ".jpg") + if (!imageFile.exists()) { + try { + imageFile.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + } + return imageFile + } + + override fun onDestroy() { + window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt index 659dd6b..5627e2e 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MainActivity.kt @@ -2,7 +2,6 @@ import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager -import com.amap.api.navi.NaviSetting import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.ViewPagerAdapter import com.casic.br.operationsite.fragment.CompletedFragment @@ -46,10 +45,6 @@ //默认显示实施中页面 mainViewPager.currentItem = 1 segmentedGroup.check(segmentedGroup.getChildAt(1).id) - - //先把导航隐私政策声明,后面导航会用到 - NaviSetting.updatePrivacyShow(this, true, true) - NaviSetting.updatePrivacyAgree(this, true) } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt new file mode 100644 index 0000000..74a7298 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/view/PermissionActivity.kt @@ -0,0 +1,66 @@ +package com.casic.br.operationsite.view + +import com.amap.api.navi.NaviSetting +import com.casic.br.operationsite.R +import com.casic.br.operationsite.utils.LocaleConstant +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.navigatePageTo +import com.pengxh.kt.lite.utils.ImmerseStatusBarUtil +import kotlinx.android.synthetic.main.activity_permssion.* +import pub.devrel.easypermissions.EasyPermissions +import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks + +class PermissionActivity : KotlinBaseActivity(), PermissionCallbacks { + + override fun initLayoutView(): Int = R.layout.activity_permssion + + override fun setupTopBarLayout() { + ImmersionBar.with(this).statusBarDarkFont(true).init() + ImmerseStatusBarUtil.setColor(this, R.color.white.convertColor(this)) + } + + override fun initData() { + //判断是否有权限,如果版本大于5.1才需要判断(即6.0以上),其他则不需要判断。 + if (EasyPermissions.hasPermissions(this, *LocaleConstant.USER_PERMISSIONS)) { + startMainActivity() + } else { + enterMainButton.setOnClickListener { + EasyPermissions.requestPermissions( + this@PermissionActivity, + resources.getString(R.string.app_name) + "需要获取存储相关权限", + LocaleConstant.PERMISSIONS_CODE, + *LocaleConstant.USER_PERMISSIONS + ) + } + } + } + + override fun initEvent() { + + } + + private fun startMainActivity() { + //先把导航隐私政策声明,后面导航会用到 + NaviSetting.updatePrivacyShow(this, true, true) + NaviSetting.updatePrivacyAgree(this, true) + navigatePageTo() + finish() + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + override fun onPermissionsGranted(requestCode: Int, perms: List) { + startMainActivity() + } + + override fun onPermissionsDenied(requestCode: Int, perms: List) { + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000..b612ce9 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_storage.xml b/app/src/main/res/drawable/ic_storage.xml new file mode 100644 index 0000000..898c1cf --- /dev/null +++ b/app/src/main/res/drawable/ic_storage.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_face_detect.xml b/app/src/main/res/layout/activity_face_detect.xml new file mode 100644 index 0000000..9cb962d --- /dev/null +++ b/app/src/main/res/layout/activity_face_detect.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_permssion.xml b/app/src/main/res/layout/activity_permssion.xml new file mode 100644 index 0000000..1366064 --- /dev/null +++ b/app/src/main/res/layout/activity_permssion.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +