设备上跑通 人脸检测和 线条绘制
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.vvl_version='1.4.321.0'
|
ext.vvl_version='1.4.321.0'
|
||||||
@@ -57,4 +58,10 @@ dependencies {
|
|||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
androidTestImplementation libs.ext.junit
|
androidTestImplementation libs.ext.junit
|
||||||
androidTestImplementation libs.espresso.core
|
androidTestImplementation libs.espresso.core
|
||||||
|
implementation 'com.google.mediapipe:tasks-vision:0.10.26'
|
||||||
|
def camerax_version = '1.4.2'
|
||||||
|
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:$camerax_version"
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<!-- Declare features -->
|
||||||
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
|
||||||
|
<!-- Declare permissions -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|||||||
Binary file not shown.
@@ -75,4 +75,47 @@ void android_main(struct android_app *pApp) {
|
|||||||
} while (!pApp->destroyRequested);
|
} while (!pApp->destroyRequested);
|
||||||
application.cleanup();
|
application.cleanup();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern void TextureLoadProcessWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize);
|
||||||
|
extern void ReceiveFacePoint(float* pos, int count, int width, int height);
|
||||||
|
|
||||||
|
void processWithVulkan(uint8_t* data, int width, int height, int format,
|
||||||
|
int rowStride, size_t dataSize) {
|
||||||
|
TextureLoadProcessWithVulkan(data, width, height, rowStride, dataSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_hmwl_sample_MainActivity_processImageNative(JNIEnv *env, jobject thiz, jobject buffer,
|
||||||
|
jint width, jint height, jint format,
|
||||||
|
jint row_stride, jint pixel_stride,
|
||||||
|
jint rotation) {
|
||||||
|
|
||||||
|
uint8_t* imageData = static_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
|
||||||
|
jlong capacity = env->GetDirectBufferCapacity(buffer);
|
||||||
|
|
||||||
|
if (imageData == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在这里将图像数据传递给 Vulkan
|
||||||
|
// 创建 Vulkan 图像或更新现有图像
|
||||||
|
processWithVulkan(imageData, width, height, format, row_stride, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_hmwl_sample_MainActivity_passDataToNative(JNIEnv *env, jobject thiz, jobject buffer,
|
||||||
|
jint point_count, jint width, jint height) {
|
||||||
|
float* pos = static_cast<float*>(env->GetDirectBufferAddress(buffer));
|
||||||
|
|
||||||
|
if (pos == nullptr) {
|
||||||
|
// 处理错误
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 或者直接将指针传递给Vulkan
|
||||||
|
ReceiveFacePoint(pos, point_count, width, height);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
|
||||||
|
package com.hmwl.sample;
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.camera.core.ImageProxy
|
||||||
|
import com.google.mediapipe.framework.image.BitmapImageBuilder
|
||||||
|
import com.google.mediapipe.framework.image.MPImage
|
||||||
|
import com.google.mediapipe.tasks.core.BaseOptions
|
||||||
|
import com.google.mediapipe.tasks.core.Delegate
|
||||||
|
import com.google.mediapipe.tasks.vision.core.RunningMode
|
||||||
|
import com.google.mediapipe.tasks.vision.facelandmarker.FaceLandmarker
|
||||||
|
import com.google.mediapipe.tasks.vision.facelandmarker.FaceLandmarkerResult
|
||||||
|
|
||||||
|
class FaceLandmarkerHelper(
|
||||||
|
var minFaceDetectionConfidence: Float = DEFAULT_FACE_DETECTION_CONFIDENCE,
|
||||||
|
var minFaceTrackingConfidence: Float = DEFAULT_FACE_TRACKING_CONFIDENCE,
|
||||||
|
var minFacePresenceConfidence: Float = DEFAULT_FACE_PRESENCE_CONFIDENCE,
|
||||||
|
var maxNumFaces: Int = DEFAULT_NUM_FACES,
|
||||||
|
var currentDelegate: Int = DELEGATE_CPU,
|
||||||
|
var runningMode: RunningMode = RunningMode.IMAGE,
|
||||||
|
val context: Context,
|
||||||
|
// this listener is only used when running in RunningMode.LIVE_STREAM
|
||||||
|
val faceLandmarkerHelperListener: LandmarkerListener? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
// For this example this needs to be a var so it can be reset on changes.
|
||||||
|
// If the Face Landmarker will not change, a lazy val would be preferable.
|
||||||
|
private var faceLandmarker: FaceLandmarker? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
setupFaceLandmarker()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearFaceLandmarker() {
|
||||||
|
faceLandmarker?.close()
|
||||||
|
faceLandmarker = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return running status of FaceLandmarkerHelper
|
||||||
|
fun isClose(): Boolean {
|
||||||
|
return faceLandmarker == null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setupFaceLandmarker() {
|
||||||
|
// Set general face landmarker options
|
||||||
|
val baseOptionBuilder = BaseOptions.builder()
|
||||||
|
|
||||||
|
// Use the specified hardware for running the model. Default to CPU
|
||||||
|
when (currentDelegate) {
|
||||||
|
DELEGATE_CPU -> {
|
||||||
|
baseOptionBuilder.setDelegate(Delegate.CPU)
|
||||||
|
}
|
||||||
|
DELEGATE_GPU -> {
|
||||||
|
baseOptionBuilder.setDelegate(Delegate.GPU)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseOptionBuilder.setModelAssetPath(MP_FACE_LANDMARKER_TASK)
|
||||||
|
|
||||||
|
// Check if runningMode is consistent with faceLandmarkerHelperListener
|
||||||
|
when (runningMode) {
|
||||||
|
RunningMode.LIVE_STREAM -> {
|
||||||
|
if (faceLandmarkerHelperListener == null) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"faceLandmarkerHelperListener must be set when runningMode is LIVE_STREAM."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val baseOptions = baseOptionBuilder.build()
|
||||||
|
// Create an option builder with base options and specific
|
||||||
|
// options only use for Face Landmarker.
|
||||||
|
val optionsBuilder =
|
||||||
|
FaceLandmarker.FaceLandmarkerOptions.builder()
|
||||||
|
.setBaseOptions(baseOptions)
|
||||||
|
.setMinFaceDetectionConfidence(minFaceDetectionConfidence)
|
||||||
|
.setMinTrackingConfidence(minFaceTrackingConfidence)
|
||||||
|
.setMinFacePresenceConfidence(minFacePresenceConfidence)
|
||||||
|
.setNumFaces(maxNumFaces)
|
||||||
|
.setOutputFaceBlendshapes(false)
|
||||||
|
.setOutputFacialTransformationMatrixes(false)
|
||||||
|
.setRunningMode(runningMode)
|
||||||
|
|
||||||
|
// The ResultListener and ErrorListener only use for LIVE_STREAM mode.
|
||||||
|
if (runningMode == RunningMode.LIVE_STREAM) {
|
||||||
|
optionsBuilder
|
||||||
|
.setResultListener(this::returnLivestreamResult)
|
||||||
|
.setErrorListener(this::returnLivestreamError)
|
||||||
|
}
|
||||||
|
|
||||||
|
val options = optionsBuilder.build()
|
||||||
|
faceLandmarker =
|
||||||
|
FaceLandmarker.createFromOptions(context, options)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
faceLandmarkerHelperListener?.onError(
|
||||||
|
"Face Landmarker failed to initialize. See error logs for " +
|
||||||
|
"details"
|
||||||
|
)
|
||||||
|
Log.e(
|
||||||
|
TAG, "MediaPipe failed to load the task with error: " + e
|
||||||
|
.message
|
||||||
|
)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
// This occurs if the model being used does not support GPU
|
||||||
|
faceLandmarkerHelperListener?.onError(
|
||||||
|
"Face Landmarker failed to initialize. See error logs for " +
|
||||||
|
"details", GPU_ERROR
|
||||||
|
)
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Face Landmarker failed to load model with error: " + e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var frameId: Long = 0
|
||||||
|
// 简化的延时跟踪
|
||||||
|
private val frameTimings = mutableMapOf<Long, FrameTiming>()
|
||||||
|
private var lastFpsUpdateTime: Long = 0
|
||||||
|
private var frameCount: Int = 0
|
||||||
|
private var currentFps: Double = 0.0
|
||||||
|
|
||||||
|
// 内部数据类用于存储时间信息
|
||||||
|
private data class FrameTiming(
|
||||||
|
var captureTime: Long = 0,
|
||||||
|
var preprocessStartTime: Long = 0,
|
||||||
|
var preprocessEndTime: Long = 0,
|
||||||
|
var detectionStartTime: Long = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// 辅助方法:记录帧时间信息
|
||||||
|
private fun recordFrameTiming(frameId: Long, block: (FrameTiming) -> Unit) {
|
||||||
|
val timing = frameTimings.getOrPut(frameId) { FrameTiming() }
|
||||||
|
block(timing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:更新FPS计算
|
||||||
|
private fun updateFps() {
|
||||||
|
frameCount++
|
||||||
|
val currentTime = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
|
if (lastFpsUpdateTime == 0L) {
|
||||||
|
lastFpsUpdateTime = currentTime
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeDiff = currentTime - lastFpsUpdateTime
|
||||||
|
if (timeDiff >= 1000) { // 每秒更新一次FPS
|
||||||
|
currentFps = frameCount * 1000.0 / timeDiff
|
||||||
|
frameCount = 0
|
||||||
|
lastFpsUpdateTime = currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:清理旧的时间记录
|
||||||
|
private fun cleanupOldTimings(currentFrameId: Long) {
|
||||||
|
val framesToRemove = frameTimings.keys.filter { it < currentFrameId - 10 }
|
||||||
|
framesToRemove.forEach { frameTimings.remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the ImageProxy to MP Image and feed it to FacelandmakerHelper.
|
||||||
|
fun detectLiveStream(
|
||||||
|
imageProxy: ImageProxy,
|
||||||
|
isFrontCamera: Boolean
|
||||||
|
) {
|
||||||
|
if (runningMode != RunningMode.LIVE_STREAM) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Attempting to call detectLiveStream" +
|
||||||
|
" while not using RunningMode.LIVE_STREAM"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val frameTime = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
|
val currentFrameId = frameId++
|
||||||
|
val captureTime = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
|
// 记录捕获时间
|
||||||
|
recordFrameTiming(currentFrameId) {
|
||||||
|
it.captureTime = captureTime
|
||||||
|
it.preprocessStartTime = SystemClock.uptimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy out RGB bits from the frame to a bitmap buffer
|
||||||
|
val bitmapBuffer =
|
||||||
|
Bitmap.createBitmap(
|
||||||
|
imageProxy.width,
|
||||||
|
imageProxy.height,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
imageProxy.use { bitmapBuffer.copyPixelsFromBuffer(imageProxy.planes[0].buffer) }
|
||||||
|
imageProxy.close()
|
||||||
|
|
||||||
|
val matrix = Matrix().apply {
|
||||||
|
// Rotate the frame received from the camera to be in the same direction as it'll be shown
|
||||||
|
postRotate(imageProxy.imageInfo.rotationDegrees.toFloat())
|
||||||
|
|
||||||
|
// flip image if user use front camera
|
||||||
|
if (isFrontCamera) {
|
||||||
|
postScale(
|
||||||
|
-1f,
|
||||||
|
1f,
|
||||||
|
imageProxy.width.toFloat(),
|
||||||
|
imageProxy.height.toFloat()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val rotatedBitmap = Bitmap.createBitmap(
|
||||||
|
bitmapBuffer, (640-480)/2, 0, bitmapBuffer.height, bitmapBuffer.height,
|
||||||
|
matrix, true
|
||||||
|
)
|
||||||
|
|
||||||
|
//val bitmap = BitmapFactory.decodeFile("/sdcard/Android/data/com.khronos.vulkan_samples/files/assets/face480.png")
|
||||||
|
//val mpImage = BitmapImageBuilder(bitmap).build()
|
||||||
|
|
||||||
|
// Convert the input Bitmap object to an MPImage object to run inference
|
||||||
|
val mpImage = BitmapImageBuilder(rotatedBitmap).build()
|
||||||
|
|
||||||
|
detectAsync(mpImage, frameTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run face face landmark using MediaPipe Face Landmarker API
|
||||||
|
@VisibleForTesting
|
||||||
|
fun detectAsync(mpImage: MPImage, frameTime: Long) {
|
||||||
|
faceLandmarker?.detectAsync(mpImage, frameTime)
|
||||||
|
// As we're using running mode LIVE_STREAM, the landmark result will
|
||||||
|
// be returned in returnLivestreamResult function
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Return the landmark result to this FaceLandmarkerHelper's caller
|
||||||
|
private fun returnLivestreamResult(
|
||||||
|
result: FaceLandmarkerResult,
|
||||||
|
input: MPImage
|
||||||
|
) {
|
||||||
|
val resultTime = SystemClock.uptimeMillis()
|
||||||
|
val frameId = result.timestampMs()
|
||||||
|
if( result.faceLandmarks().size > 0 ) {
|
||||||
|
// 计算各种延时
|
||||||
|
val timings = frameTimings[frameId]
|
||||||
|
if (timings != null) {
|
||||||
|
val totalLatency = resultTime - timings.captureTime
|
||||||
|
val preprocessLatency = timings.preprocessEndTime - timings.preprocessStartTime
|
||||||
|
val detectionLatency = resultTime - timings.detectionStartTime
|
||||||
|
val captureToDetectionLatency = timings.detectionStartTime - timings.captureTime
|
||||||
|
|
||||||
|
Log.i("TimeLatency",
|
||||||
|
"总延时: ${totalLatency}ms | " +
|
||||||
|
"预处理: ${preprocessLatency}ms | " +
|
||||||
|
"检测: ${detectionLatency}ms | " +
|
||||||
|
"捕获到检测: ${captureToDetectionLatency}ms | " +
|
||||||
|
"FPS: ${String.format("%.1f", currentFps)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
val finishTimeMs = SystemClock.uptimeMillis()
|
||||||
|
val inferenceTime = finishTimeMs - result.timestampMs()
|
||||||
|
faceLandmarkerHelperListener?.onResults(
|
||||||
|
ResultBundle(
|
||||||
|
result,
|
||||||
|
inferenceTime, // 主要是检测时间
|
||||||
|
input.height,
|
||||||
|
input.width,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val finishTimeMs = SystemClock.uptimeMillis()
|
||||||
|
val inferenceTime = finishTimeMs - result.timestampMs()
|
||||||
|
Log.i("TimeLatency",
|
||||||
|
"总延时: ${inferenceTime}ms | "
|
||||||
|
)
|
||||||
|
faceLandmarkerHelperListener?.onResults(
|
||||||
|
ResultBundle(
|
||||||
|
result,
|
||||||
|
inferenceTime,
|
||||||
|
input.height,
|
||||||
|
input.width
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
faceLandmarkerHelperListener?.onEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return errors thrown during detection to this FaceLandmarkerHelper's
|
||||||
|
// caller
|
||||||
|
private fun returnLivestreamError(error: RuntimeException) {
|
||||||
|
faceLandmarkerHelperListener?.onError(
|
||||||
|
error.message ?: "An unknown error has occurred"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "FaceLandmarkerHelper"
|
||||||
|
private const val MP_FACE_LANDMARKER_TASK = "face_landmarker.task"
|
||||||
|
|
||||||
|
const val DELEGATE_CPU = 0
|
||||||
|
const val DELEGATE_GPU = 1
|
||||||
|
const val DEFAULT_FACE_DETECTION_CONFIDENCE = 0.5F
|
||||||
|
const val DEFAULT_FACE_TRACKING_CONFIDENCE = 0.5F
|
||||||
|
const val DEFAULT_FACE_PRESENCE_CONFIDENCE = 0.5F
|
||||||
|
const val DEFAULT_NUM_FACES = 1
|
||||||
|
const val OTHER_ERROR = 0
|
||||||
|
const val GPU_ERROR = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ResultBundle(
|
||||||
|
val result: FaceLandmarkerResult,
|
||||||
|
val inferenceTime: Long,
|
||||||
|
val inputImageHeight: Int,
|
||||||
|
val inputImageWidth: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface LandmarkerListener {
|
||||||
|
fun onError(error: String, errorCode: Int = OTHER_ERROR)
|
||||||
|
fun onResults(resultBundle: ResultBundle)
|
||||||
|
|
||||||
|
fun onEmpty() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,202 @@
|
|||||||
package com.hmwl.sample;
|
package com.hmwl.sample;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.google.androidgamesdk.GameActivity;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.camera.core.AspectRatio;
|
||||||
|
import androidx.camera.core.CameraSelector;
|
||||||
|
import androidx.camera.core.ImageAnalysis;
|
||||||
|
import androidx.camera.core.ImageProxy;
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
public class MainActivity extends GameActivity{
|
import com.google.androidgamesdk.GameActivity;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.mediapipe.tasks.components.containers.NormalizedLandmark;
|
||||||
|
import com.google.mediapipe.tasks.vision.core.RunningMode;
|
||||||
|
import com.google.mediapipe.tasks.vision.facelandmarker.FaceLandmarkerResult;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class MainActivity extends GameActivity implements FaceLandmarkerHelper.LandmarkerListener {
|
||||||
private static final String TAG = "MainActivity";
|
private static final String TAG = "MainActivity";
|
||||||
|
|
||||||
static {
|
static {
|
||||||
System.loadLibrary("sample");
|
System.loadLibrary("sample");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ProcessCameraProvider cameraProvider;
|
||||||
|
private void initCamera(){
|
||||||
|
backgroundExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
if (hasCameraPermission()) {
|
||||||
|
startCamera();
|
||||||
|
} else {
|
||||||
|
requestCameraPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCamera() {
|
||||||
|
ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
|
||||||
|
ProcessCameraProvider.getInstance(this);
|
||||||
|
|
||||||
|
cameraProviderFuture.addListener(() -> {
|
||||||
|
try {
|
||||||
|
cameraProvider = cameraProviderFuture.get();
|
||||||
|
|
||||||
|
CameraSelector cameraSelector = new CameraSelector.Builder()
|
||||||
|
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
imageAnalyzer = new ImageAnalysis.Builder()
|
||||||
|
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
imageAnalyzer.setAnalyzer(backgroundExecutor, new ImageAnalysis.Analyzer() {
|
||||||
|
private long lastCaptureTime = 0;
|
||||||
|
private int frameCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void analyze(@NonNull ImageProxy image) {
|
||||||
|
frameCount++;
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
long captureTimeNs = image.getImageInfo().getTimestamp();
|
||||||
|
long analyzeTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// 简单直接的计算方法
|
||||||
|
long captureTimeMs = TimeUnit.NANOSECONDS.toMillis(captureTimeNs);
|
||||||
|
long latency = analyzeTime - captureTimeMs;
|
||||||
|
|
||||||
|
// 帧率计算
|
||||||
|
if (lastCaptureTime > 0) {
|
||||||
|
long frameInterval = captureTimeMs - lastCaptureTime;
|
||||||
|
double fps = 1000.0 / frameInterval;
|
||||||
|
|
||||||
|
if (frameCount % 10 == 0) { // 每30帧打印一次
|
||||||
|
Log.i("TimeLatency", String.format(
|
||||||
|
"Frame %d - Latency: %d ms, FPS: %.1f",
|
||||||
|
frameCount, latency, fps
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCaptureTime = captureTimeMs;
|
||||||
|
|
||||||
|
processImageForVulkan(image);
|
||||||
|
if(frameCount %2 == 0)
|
||||||
|
{
|
||||||
|
detectFace(image);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
image.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
//image.close(); // 确保在这里关闭
|
||||||
|
//Log.d("Camera", "Image closed, ready for next frame");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cameraProvider.bindToLifecycle(this, cameraSelector, imageAnalyzer);
|
||||||
|
|
||||||
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
Log.e("MainActivity", "Error starting camera: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}, ContextCompat.getMainExecutor(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processImageForVulkan(ImageProxy image) {
|
||||||
|
// 获取图像信息
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
int format = image.getFormat();
|
||||||
|
|
||||||
|
// 获取图像数据平面
|
||||||
|
ImageProxy.PlaneProxy[] planes = image.getPlanes();
|
||||||
|
|
||||||
|
// 对于 RGBA_8888 格式,通常只有一个平面
|
||||||
|
if (planes.length > 0) {
|
||||||
|
ImageProxy.PlaneProxy plane = planes[0];
|
||||||
|
ByteBuffer buffer = plane.getBuffer();
|
||||||
|
int rowStride = plane.getRowStride();
|
||||||
|
int pixelStride = plane.getPixelStride();
|
||||||
|
|
||||||
|
// 调用 Native 方法处理图像
|
||||||
|
processImageNative(buffer, width, height, format,
|
||||||
|
rowStride, pixelStride, image.getImageInfo().getRotationDegrees());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native 方法
|
||||||
|
private native void processImageNative(ByteBuffer buffer, int width, int height,
|
||||||
|
int format, int rowStride, int pixelStride,
|
||||||
|
int rotation);
|
||||||
|
|
||||||
|
private void detectFace(ImageProxy imageProxy) {
|
||||||
|
faceLandmarkerHelper.detectLiveStream(
|
||||||
|
imageProxy,false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasCameraPermission() {
|
||||||
|
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestCameraPermission() {
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
new String[]{Manifest.permission.CAMERA},
|
||||||
|
REQUEST_CAMERA_PERMISSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int REQUEST_CAMERA_PERMISSION = 1001;
|
||||||
|
ImageAnalysis imageAnalyzer;
|
||||||
|
ExecutorService backgroundExecutor;
|
||||||
|
FaceLandmarkerHelper faceLandmarkerHelper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
initCamera();
|
||||||
|
|
||||||
|
backgroundExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
faceLandmarkerHelper = new FaceLandmarkerHelper(
|
||||||
|
0.5f,
|
||||||
|
0.5f,
|
||||||
|
0.5f,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
RunningMode.LIVE_STREAM,
|
||||||
|
MainActivity.this,
|
||||||
|
MainActivity.this
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
int action = event.getActionMasked();
|
int action = event.getActionMasked();
|
||||||
@@ -57,4 +241,71 @@ public class MainActivity extends GameActivity{
|
|||||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NotNull String error, int errorCode) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private ByteBuffer nativeBuffer = null;
|
||||||
|
float[] points = new float[480 * 3];
|
||||||
|
|
||||||
|
|
||||||
|
public class Vector {
|
||||||
|
public float x, y, z;
|
||||||
|
|
||||||
|
public Vector(float x, float y, float z) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float z_rate = -1.0f;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResults(FaceLandmarkerHelper.@NotNull ResultBundle resultBundle) {
|
||||||
|
FaceLandmarkerResult faceLandmarkerResult = resultBundle.getResult();
|
||||||
|
int height = resultBundle.getInputImageHeight();
|
||||||
|
int width = resultBundle.getInputImageWidth();
|
||||||
|
int bufferSize = 480 * 3 * 4;
|
||||||
|
|
||||||
|
if(nativeBuffer == null)
|
||||||
|
{
|
||||||
|
// 创建直接ByteBuffer(在native内存中)
|
||||||
|
nativeBuffer = ByteBuffer.allocateDirect(bufferSize);
|
||||||
|
nativeBuffer.order(ByteOrder.nativeOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
long start_time = System.currentTimeMillis();
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
// Iterate through each detected face
|
||||||
|
for (List<NormalizedLandmark> faceLandmarks : faceLandmarkerResult.faceLandmarks()) {
|
||||||
|
//should be 1
|
||||||
|
|
||||||
|
int arrayIndex = 0;
|
||||||
|
for (NormalizedLandmark p : faceLandmarks)
|
||||||
|
{
|
||||||
|
//sb.append("[");
|
||||||
|
points[arrayIndex++] = p.x();
|
||||||
|
//sb.append(""+p.x()+",");
|
||||||
|
points[arrayIndex++] = p.y();
|
||||||
|
//sb.append(""+p.y()+",");
|
||||||
|
points[arrayIndex++] = p.z() * z_rate;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FloatBuffer floatBuffer = nativeBuffer.asFloatBuffer();
|
||||||
|
floatBuffer.put(points);
|
||||||
|
floatBuffer.position(0);
|
||||||
|
long cur_time = System.currentTimeMillis();
|
||||||
|
Log.i("TimeLatency","Result Data ProcessTime:" + (cur_time-start_time));
|
||||||
|
passDataToNative(nativeBuffer, index, width, height);
|
||||||
|
Log.i("TimeLatency","passDataToNative ProcessTime:" + (System.currentTimeMillis() - cur_time));
|
||||||
|
}
|
||||||
|
|
||||||
|
public native void passDataToNative(ByteBuffer buffer, int pointCount, int width, int height);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
|
id 'org.jetbrains.kotlin.android' version '2.2.0' apply false
|
||||||
}
|
}
|
||||||
@@ -626,13 +626,13 @@ uint32_t Application::findMemoryType(VkPhysicalDevice physicalDevice, uint32_t t
|
|||||||
throw std::runtime_error("Failed to find suitable memory type!");
|
throw std::runtime_error("Failed to find suitable memory type!");
|
||||||
}
|
}
|
||||||
|
|
||||||
Texture Application::loadTexture(std::string path, Texture& tex)
|
Texture Application::loadTexture(std::string path, Texture& tex, bool srgb)
|
||||||
{
|
{
|
||||||
std::vector<unsigned char> data = readFileEx(path);
|
std::vector<unsigned char> data = readFileEx(path);
|
||||||
std::vector<unsigned char> image;
|
std::vector<unsigned char> image;
|
||||||
unsigned w, h;
|
unsigned w, h;
|
||||||
unsigned error = lodepng::decode(image, w, h, data, LCT_RGBA, 8);
|
unsigned error = lodepng::decode(image, w, h, data, LCT_RGBA, 8);
|
||||||
processWithVulkan(image.data(), w, h, w * 4, image.size(), tex, true);
|
processWithVulkan(image.data(), w, h, w * 4, image.size(), tex, srgb);
|
||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ public:
|
|||||||
virtual void render(VkCommandBuffer commandBuffer);
|
virtual void render(VkCommandBuffer commandBuffer);
|
||||||
virtual bool isInited() { return inited; }
|
virtual bool isInited() { return inited; }
|
||||||
|
|
||||||
|
void processWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize, Texture& out_texture, bool srgb);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; // 物理设备
|
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; // 物理设备
|
||||||
@@ -69,10 +71,11 @@ protected:
|
|||||||
int currentFrame = 0;
|
int currentFrame = 0;
|
||||||
int MAX_FRAMES_IN_FLIGHT = 2;
|
int MAX_FRAMES_IN_FLIGHT = 2;
|
||||||
bool inited = false;
|
bool inited = false;
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Texture loadTexture(std::string path, Texture& tex);
|
Texture loadTexture(std::string path, Texture& tex, bool srgb);
|
||||||
void processWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize, Texture& out_texture, bool srgb);
|
|
||||||
uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter,
|
uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter,
|
||||||
VkMemoryPropertyFlags properties);
|
VkMemoryPropertyFlags properties);
|
||||||
void createTexture(VkDevice device, VkPhysicalDevice physicalDevice, int width, int height, Texture& texture, bool srgb);
|
void createTexture(VkDevice device, VkPhysicalDevice physicalDevice, int width, int height, Texture& texture, bool srgb);
|
||||||
|
|||||||
+12
-2
@@ -24,6 +24,14 @@ void ReceiveFacePoint(float* pos, int pointCount, int width, int height)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void TextureLoadProcessWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize)
|
||||||
|
{
|
||||||
|
Texture& tex_bg = FaceApp::Get()->tex_bg;
|
||||||
|
FaceApp::Get()->processWithVulkan(data, width, height, rowStride, dataSize, tex_bg, false);
|
||||||
|
}
|
||||||
|
|
||||||
bool FaceApp::LoadOBJ(const std::string& filename,
|
bool FaceApp::LoadOBJ(const std::string& filename,
|
||||||
std::vector<TextureLoadingVertexStructure>& vertices,
|
std::vector<TextureLoadingVertexStructure>& vertices,
|
||||||
std::vector<uint32_t>& indices) {
|
std::vector<uint32_t>& indices) {
|
||||||
@@ -477,6 +485,8 @@ void FaceApp::setup_descriptor_set()
|
|||||||
|
|
||||||
void FaceApp::render(VkCommandBuffer commandBuffer)
|
void FaceApp::render(VkCommandBuffer commandBuffer)
|
||||||
{
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mtx);
|
||||||
|
std::unique_lock<std::mutex> lock_point(mtx_point);
|
||||||
//Application::render(commandBuffer);
|
//Application::render(commandBuffer);
|
||||||
|
|
||||||
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout_bg, 0, 1, &m_descriptor_set_bg, 0, NULL);
|
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout_bg, 0, 1, &m_descriptor_set_bg, 0, NULL);
|
||||||
@@ -539,8 +549,8 @@ void FaceApp::initVulkan()
|
|||||||
Application::initVulkan();
|
Application::initVulkan();
|
||||||
createVmaAllocator();
|
createVmaAllocator();
|
||||||
LoadOBJ("face_picture_3dmax.obj", obj_vertices, obj_indices);
|
LoadOBJ("face_picture_3dmax.obj", obj_vertices, obj_indices);
|
||||||
loadTexture("demo0.png", tex_demo0);
|
loadTexture("demo0.png", tex_demo0, true);
|
||||||
loadTexture("out.png", tex_bg);
|
loadTexture("out.png", tex_bg, false);
|
||||||
createVertexBuffer();
|
createVertexBuffer();
|
||||||
createUniformBuffer();
|
createUniformBuffer();
|
||||||
setup_descriptor_pool();
|
setup_descriptor_pool();
|
||||||
|
|||||||
+4
-1
@@ -44,6 +44,9 @@ public:
|
|||||||
|
|
||||||
virtual bool isInited() override { return inited && faceAppInited; }
|
virtual bool isInited() override { return inited && faceAppInited; }
|
||||||
virtual void cleanup() override;
|
virtual void cleanup() override;
|
||||||
|
|
||||||
|
Texture tex_bg = {};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
glm::vec3 rotation = glm::vec3();
|
glm::vec3 rotation = glm::vec3();
|
||||||
glm::vec3 camera_pos = glm::vec3();
|
glm::vec3 camera_pos = glm::vec3();
|
||||||
@@ -103,7 +106,7 @@ private:
|
|||||||
float myFloatValue = 1.5f;
|
float myFloatValue = 1.5f;
|
||||||
|
|
||||||
|
|
||||||
Texture tex_bg = {};
|
|
||||||
void create_pipelines_bg();
|
void create_pipelines_bg();
|
||||||
void setup_descriptor_set_layout_bg();
|
void setup_descriptor_set_layout_bg();
|
||||||
void setup_descriptor_set_bg();
|
void setup_descriptor_set_bg();
|
||||||
|
|||||||
Reference in New Issue
Block a user