diff --git a/examples/holistic_landmarker/android/.gitignore b/examples/holistic_landmarker/android/.gitignore new file mode 100644 index 00000000..ab234627 --- /dev/null +++ b/examples/holistic_landmarker/android/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +*.task diff --git a/examples/holistic_landmarker/android/README.md b/examples/holistic_landmarker/android/README.md new file mode 100644 index 00000000..33b4c496 --- /dev/null +++ b/examples/holistic_landmarker/android/README.md @@ -0,0 +1,48 @@ +# MediaPipe Tasks Holistic Landmark Detection Android Demo + +### Overview + +This is a camera app that continuously detects the body, hand, and face +landmarks in the frames seen by your device's back camera, using a +custom **task** file. + +The task file is downloaded by a Gradle script when you build and run the app. +You don't need to do any additional steps to download task files into the +project explicitly unless you wish to use your own landmark detection task. If +you do use your own task file, place it into the app's assets/tasks directory. + +This application should be run on physical Android devices with a back camera. + +![Holistic Landmarker Demo](screenshot.jpg?raw=true "Holistic Landmarker Demo") + +## Build the demo using Android Studio + +### Prerequisites + +* The **[Android Studio](https://developer.android.com/studio/index.html)** IDE. + This sample has been tested on Android Studio Giraffe. + +* A physical Android device with a minimum OS version of SDK 24 (Android 7.0 - + Nougat) with developer mode enabled. The process of enabling developer mode + may vary by device. + +### Building + +* Open Android Studio. From the Welcome screen, select Open an existing + Android Studio project. + +* From the Open File or Project window that appears, navigate to and select + the mediapipe/examples/holistic_landmarker/android directory. Click OK. You + may + be asked if you trust the project. Select Trust. + +* If it asks you to do a Gradle Sync, click OK. + +* With your Android device connected to your computer and developer mode + enabled, click on the green Run arrow in Android Studio. + +### Models used + +Downloading, extraction, and placing the models into the *assets/tasks* folder +is +managed automatically by the **app/build.gradle.kts** file. \ No newline at end of file diff --git a/examples/holistic_landmarker/android/app/.gitignore b/examples/holistic_landmarker/android/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/examples/holistic_landmarker/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/holistic_landmarker/android/app/build.gradle.kts b/examples/holistic_landmarker/android/app/build.gradle.kts new file mode 100644 index 00000000..acf83fc2 --- /dev/null +++ b/examples/holistic_landmarker/android/app/build.gradle.kts @@ -0,0 +1,78 @@ +import de.undercouch.gradle.tasks.download.Download + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("de.undercouch.download") +} + +android { + namespace = "com.google.mediapipe.examples.holisticlandmarker" + compileSdk = 34 + + defaultConfig { + applicationId = "com.google.mediapipe.examples.holisticlandmarker" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } +} + +val downloadTaskFile = tasks.register("downloadTaskFile") { + src("https://storage.googleapis.com/mediapipe-models/holistic_landmarker/holistic_landmarker/float16/latest/holistic_landmarker.task") + dest("$projectDir/src/main/assets/tasks/holistic_landmarker.task") + overwrite(false) +} + +tasks.named("preBuild") { + dependsOn(downloadTaskFile) +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") + implementation("androidx.navigation:navigation-ui-ktx:2.7.5") + implementation("androidx.fragment:fragment-ktx:1.6.2") + + // CameraX core library + val cameraxVersion = "1.3.0" + implementation("androidx.camera:camera-core:$cameraxVersion") + // CameraX Camera2 extensions + implementation("androidx.camera:camera-camera2:$cameraxVersion") + // CameraX Lifecycle library + implementation("androidx.camera:camera-lifecycle:$cameraxVersion") + // CameraX View class + implementation("androidx.camera:camera-view:$cameraxVersion") + + implementation("com.google.mediapipe:tasks-vision:0.10.9") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/examples/holistic_landmarker/android/app/proguard-rules.pro b/examples/holistic_landmarker/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/examples/holistic_landmarker/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/holistic_landmarker/android/app/src/main/AndroidManifest.xml b/examples/holistic_landmarker/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1fd92efb --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/HolisticLandmarkerHelper.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/HolisticLandmarkerHelper.kt new file mode 100644 index 00000000..2f84c66d --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/HolisticLandmarkerHelper.kt @@ -0,0 +1,366 @@ +package com.google.mediapipe.examples.holisticlandmarker + +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Matrix +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.SystemClock +import android.util.Log +import androidx.camera.core.ImageProxy +import com.google.mediapipe.framework.MediaPipeException +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.holisticlandmarker.HolisticLandmarker +import com.google.mediapipe.tasks.vision.holisticlandmarker.HolisticLandmarkerResult + +class HolisticLandmarkerHelper( + var currentDelegate: Int = DELEGATE_CPU, + var runningMode: RunningMode = RunningMode.IMAGE, + val context: Context, + val minFaceLandmarksConfidence: Float, + val minHandLandmarksConfidence: Float, + val minPoseLandmarksConfidence: Float, + val minFaceDetectionConfidence: Float, + val minPoseDetectionConfidence: Float, + val minFaceSuppressionThreshold: Float, + val minPoseSuppressionThreshold: Float, + val isFaceBlendShapes: Boolean, + val isPoseSegmentationMark: Boolean, + // this listener is only used when running in RunningMode.LIVE_STREAM + val landmarkerHelperListener: LandmarkerListener? = null +) { + private var holisticLandmarker: HolisticLandmarker? = null + + init { + setUpHolisticLandmarker() + } + + fun setUpHolisticLandmarker() { + 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) + } + } + // Check if runningMode is consistent with landmarkerHelperListener + when (runningMode) { + RunningMode.LIVE_STREAM -> { + if (landmarkerHelperListener == null) { + throw IllegalStateException( + "holisticLandmarkerHelperListener must be set when runningMode is LIVE_STREAM." + ) + } + } + + else -> { + // no-op + } + } + try { + baseOptionBuilder.setModelAssetPath(MP_HOLISTIC_LANDMARKER_TASK) + val baseOptions = baseOptionBuilder.build() + val optionsBuilder = + HolisticLandmarker.HolisticLandmarkerOptions.builder() + .setBaseOptions(baseOptions) + .setRunningMode(runningMode) + .setMinFaceLandmarksConfidence(minFaceLandmarksConfidence) + .setMinHandLandmarksConfidence(minHandLandmarksConfidence) + .setMinPoseLandmarksConfidence(minPoseLandmarksConfidence) + .setMinFaceDetectionConfidence(minFaceDetectionConfidence) + .setMinPoseDetectionConfidence(minPoseDetectionConfidence) + .setMinFaceSuppressionThreshold(minFaceSuppressionThreshold) + .setMinPoseSuppressionThreshold(minPoseSuppressionThreshold) + .setOutputFaceBlendshapes(isFaceBlendShapes) + .setOutputPoseSegmentationMasks(isPoseSegmentationMark) + + // 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() + holisticLandmarker = + HolisticLandmarker.createFromOptions(context, options) + } catch (e: IllegalStateException) { + landmarkerHelperListener?.onError( + "Holistic 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) { + landmarkerHelperListener?.onError( + "Holistic Landmarker failed to initialize. See error logs for " + + "details" + ) + Log.e( + TAG, "MediaPipe failed to load the task with error: " + e + .message + ) + } + } + + fun isClose(): Boolean { + return holisticLandmarker == null + } + + fun detectVideoFile( + videoUri: Uri, + inferenceIntervalMs: Long + ): VideoResultBundle? { + if (runningMode != RunningMode.VIDEO) { + throw IllegalArgumentException( + "Attempting to call detectVideoFile" + + " while not using RunningMode.VIDEO" + ) + } + + // Inference time is the difference between the system time at the start and finish of the + // process + val startTime = SystemClock.uptimeMillis() + + // Load frames from the video and run the holistic landmarker. + val retriever = MediaMetadataRetriever() + retriever.setDataSource(context, videoUri) + val videoLengthMs = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + ?.toLong() + + // Note: We need to read width/height from frame instead of getting the width/height + // of the video directly because MediaRetriever returns frames that are smaller than the + // actual dimension of the video file. + val firstFrame = retriever.getFrameAtTime(0) + val width = firstFrame?.width + val height = firstFrame?.height + + // If the video is invalid, returns a null detection result + if ((videoLengthMs == null) || (width == null) || (height == null)) return null + + // Next, we'll get one frame every frameInterval ms, then run detection on these frames. + val resultList = mutableListOf() + val numberOfFrameToRead = videoLengthMs.div(inferenceIntervalMs) + + for (i in 0..numberOfFrameToRead) { + val timestampMs = i * inferenceIntervalMs // ms + + retriever + .getFrameAtTime( + timestampMs * 1000, // convert from ms to micro-s + MediaMetadataRetriever.OPTION_CLOSEST + ) + ?.let { frame -> + // Convert the video frame to ARGB_8888 which is required by the MediaPipe + val argb8888Frame = + if (frame.config == Bitmap.Config.ARGB_8888) frame + else frame.copy(Bitmap.Config.ARGB_8888, false) + + // Convert the input Bitmap object to an MPImage object to run inference + val mpImage = BitmapImageBuilder(argb8888Frame).build() + + // Run holistic landmarker using MediaPipe Holistic Landmarker API + try { + holisticLandmarker?.detectForVideo(mpImage, timestampMs) + ?.let { detectionResult -> + resultList.add(detectionResult) + } + } catch (e: MediaPipeException) { + resultList.add(null) + } + } ?: kotlin.run { + resultList.add(null) + } + } + + retriever.release() + + val inferenceTimePerFrameMs = + (SystemClock.uptimeMillis() - startTime).div(numberOfFrameToRead) + + return VideoResultBundle( + resultList, + inferenceTimePerFrameMs, + height, + width + ) + } + + fun detectImage(image: Bitmap): ResultBundle? { + if (runningMode != RunningMode.IMAGE) { + throw IllegalArgumentException( + "Attempting to call detectImage" + + " while not using RunningMode.IMAGE" + ) + } + + // Inference time is the difference between the system time at the + // start and finish of the process + val startTime = SystemClock.uptimeMillis() + + // Convert the input Bitmap object to an MPImage object to run inference + val mpImage = BitmapImageBuilder(image).build() + + // Run holistic landmarker using MediaPipe Holistic Landmarker API + holisticLandmarker?.detect(mpImage)?.also { landmarkResult -> + val inferenceTimeMs = SystemClock.uptimeMillis() - startTime + return ResultBundle( + landmarkResult, + inferenceTimeMs, + image.height, + image.width + ) + } + + // If holisticLandmarker?.detect() returns null, this is likely an error. Returning null + // to indicate this. + landmarkerHelperListener?.onError( + "Holistic Landmarker failed to detect." + ) + return null + } + + fun detectLiveStreamCamera(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() + + // 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, 0, 0, bitmapBuffer.width, bitmapBuffer.height, + matrix, true + ) + + // Convert the input Bitmap object to an MPImage object to run inference + val mpImage = BitmapImageBuilder(rotatedBitmap).build() + + detectAsync(mpImage, frameTime) + } + + private fun detectAsync(mpImage: MPImage?, frameTime: Long) { + holisticLandmarker?.detectAsync(mpImage, frameTime) + } + + fun clearHolisticLandmarker() { + holisticLandmarker?.close() + holisticLandmarker = null + } + + private fun returnLivestreamResult( + result: HolisticLandmarkerResult, + input: MPImage + ) { + val finishTimeMs = SystemClock.uptimeMillis() + val inferenceTime = finishTimeMs - result.timestampMs() + + landmarkerHelperListener?.onResults( + ResultBundle( + result, + inferenceTime, + input.height, + input.width + ) + ) + } + + // Return errors thrown during detection to this HolisticLandmarkerHelper's + // caller + private fun returnLivestreamError(error: RuntimeException) { + landmarkerHelperListener?.onError( + error = error.message ?: "Unknown error" + ) + } + + data class ResultBundle( + val result: HolisticLandmarkerResult, + val inferenceTime: Long, + val inputImageHeight: Int, + val inputImageWidth: Int, + ) + + data class VideoResultBundle( + val results: List, + val inferenceTime: Long, + val inputImageHeight: Int, + val inputImageWidth: Int, + ) + + companion object { + private const val MP_HOLISTIC_LANDMARKER_TASK = + "tasks/holistic_landmarker.task" + const val TAG = "HolisticLandmarkerHelper" + const val OTHER_ERROR = 0 + const val GPU_ERROR = 1 + const val DELEGATE_CPU = 0 + const val DELEGATE_GPU = 1 + const val DEFAULT_MIN_FACE_LANDMARKS_CONFIDENCE = 0.5F + const val DEFAULT_MIN_HAND_LANDMARKS_CONFIDENCE = 0.5F + const val DEFAULT_MIN_POSE_LANDMARKS_CONFIDENCE = 0.5F + const val DEFAULT_MIN_FACE_DETECTION_CONFIDENCE = 0.5F + const val DEFAULT_MIN_POSE_DETECTION_CONFIDENCE = 0.5F + const val DEFAULT_MIN_FACE_SUPPRESSION_THRESHOLD = 0.5F + const val DEFAULT_MIN_POSE_SUPPRESSION_THRESHOLD = 0.5F + const val DEFAULT_FACE_BLEND_SHAPES = false + const val DEFAULT_POSE_SEGMENTATION_MARK = false + } + + interface LandmarkerListener { + fun onError(error: String, errorCode: Int = OTHER_ERROR) + fun onResults(resultBundle: ResultBundle) + } +} diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/MainActivity.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/MainActivity.kt new file mode 100644 index 00000000..5cd2185a --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/MainActivity.kt @@ -0,0 +1,42 @@ +package com.google.mediapipe.examples.holisticlandmarker + +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.activity.viewModels +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.google.mediapipe.examples.holisticlandmarker.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var activityMainBinding: ActivityMainBinding + private val viewModel: MainViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityMainBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(activityMainBinding.root) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment + val navController = navHostFragment.navController + activityMainBinding.navigation.setupWithNavController(navController) + activityMainBinding.navigation.setOnNavigationItemReselectedListener { + // ignore the reselection + } + } +} diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/MainViewModel.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/MainViewModel.kt new file mode 100644 index 00000000..f181b69d --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/MainViewModel.kt @@ -0,0 +1,135 @@ +package com.google.mediapipe.examples.holisticlandmarker + +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class MainViewModel : ViewModel() { + val helperState: MutableLiveData = MutableLiveData( + HelperState() + ) + + fun setMinFaceLandmarkConfidence(confidence: Float) { + val currentLandmarkConfidence = + helperState.value?.minFaceLandmarkThreshold ?: 0f + helperState.value = helperState.value?.copy( + minFaceLandmarkThreshold = ((currentLandmarkConfidence.toBigDecimal() + confidence.toBigDecimal()).toFloat()).coerceIn( + 0f, + 1f + ) + ) + } + + fun setMinHandLandmarkConfidence(confidence: Float) { + val currentLandmarkConfidence = + helperState.value?.minHandLandmarkThreshold ?: 0f + helperState.value = + helperState.value?.copy( + minHandLandmarkThreshold = ((currentLandmarkConfidence.toBigDecimal() + confidence.toBigDecimal()).toFloat()).coerceIn( + 0f, + 1f + ) + ) + } + + fun setMinPoseLandmarkConfidence(confidence: Float) { + val currentLandmarkConfidence = + helperState.value?.minPoseLandmarkThreshold ?: 0f + helperState.value = + helperState.value?.copy( + minPoseLandmarkThreshold = ((currentLandmarkConfidence.toBigDecimal() + confidence.toBigDecimal()).toFloat()).coerceIn( + 0f, 1f + ) + ) + } + + fun setMinFaceDetectionConfidence(confidence: Float) { + val currentDetectionConfidence = + helperState.value?.minFaceDetectionThreshold ?: 0f + helperState.value = + helperState.value?.copy( + minFaceDetectionThreshold = ((currentDetectionConfidence.toBigDecimal() + confidence.toBigDecimal()).toFloat()).coerceIn( + 0f, + 1f + ) + ) + } + + fun setMinPoseDetectionConfidence(confidence: Float) { + val currentDetectionConfidence = + helperState.value?.minPoseDetectionThreshold ?: 0f + helperState.value = + helperState.value?.copy( + minPoseDetectionThreshold = ((currentDetectionConfidence.toBigDecimal() + confidence.toBigDecimal()).toFloat()).coerceIn( + 0f, + 1f + ) + ) + } + + fun setMinPoseSuppressionConfidence(confidence: Float) { + val currentSuppressionConfidence = + helperState.value?.minPoseSuppressionThreshold ?: 0f + helperState.value = + helperState.value?.copy( + minPoseSuppressionThreshold = ((currentSuppressionConfidence.toBigDecimal() + confidence.toBigDecimal()).toFloat()).coerceIn( + 0f, + 1f + ) + ) + } + + fun setMinFaceSuppressionConfidence(confidence: Float) { + val currentSuppressionConfidence = + helperState.value?.minFaceSuppressionThreshold ?: 0f + helperState.value = + helperState.value?.copy( + minFaceSuppressionThreshold = ((currentSuppressionConfidence.toBigDecimal() + confidence.toBigDecimal()).toFloat()).coerceIn( + 0f, + 1f + ) + ) + } + + fun setFaceBlendMode(faceBlendMode: Boolean) { + helperState.value = + helperState.value?.copy(isFaceBlendMode = faceBlendMode) + } + + fun setPoseSegmentationMarks(poseSegmentationMarks: Boolean) { + helperState.value = + helperState.value?.copy(isPoseSegmentationMarks = poseSegmentationMarks) + } + + fun setDelegate(delegate: Int) { + helperState.value = helperState.value?.copy(delegate = delegate) + } +} + +data class HelperState( + val delegate: Int = HolisticLandmarkerHelper.DELEGATE_CPU, + val minFaceLandmarkThreshold: Float = HolisticLandmarkerHelper.DEFAULT_MIN_FACE_LANDMARKS_CONFIDENCE, + val minHandLandmarkThreshold: Float = HolisticLandmarkerHelper.DEFAULT_MIN_HAND_LANDMARKS_CONFIDENCE, + val minPoseLandmarkThreshold: Float = HolisticLandmarkerHelper.DEFAULT_MIN_POSE_LANDMARKS_CONFIDENCE, + val minFaceDetectionThreshold: Float = HolisticLandmarkerHelper.DEFAULT_MIN_FACE_DETECTION_CONFIDENCE, + val minPoseDetectionThreshold: Float = HolisticLandmarkerHelper.DEFAULT_MIN_POSE_DETECTION_CONFIDENCE, + val minPoseSuppressionThreshold: Float = HolisticLandmarkerHelper.DEFAULT_MIN_POSE_SUPPRESSION_THRESHOLD, + val minFaceSuppressionThreshold: Float = HolisticLandmarkerHelper.DEFAULT_MIN_FACE_SUPPRESSION_THRESHOLD, + val isFaceBlendMode: Boolean = HolisticLandmarkerHelper.DEFAULT_FACE_BLEND_SHAPES, + val isPoseSegmentationMarks: Boolean = HolisticLandmarkerHelper.DEFAULT_POSE_SEGMENTATION_MARK, +) diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/OverlayView.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/OverlayView.kt new file mode 100644 index 00000000..d487c4e5 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/OverlayView.kt @@ -0,0 +1,207 @@ +package com.google.mediapipe.examples.holisticlandmarker + +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import com.google.mediapipe.framework.image.ByteBufferExtractor +import com.google.mediapipe.tasks.vision.core.RunningMode +import com.google.mediapipe.tasks.vision.facelandmarker.FaceLandmarker +import com.google.mediapipe.tasks.vision.handlandmarker.HandLandmarker +import com.google.mediapipe.tasks.vision.holisticlandmarker.HolisticLandmarkerResult +import com.google.mediapipe.tasks.vision.poselandmarker.PoseLandmarker +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +class OverlayView(context: Context?, attrs: AttributeSet?) : + View(context, attrs) { + private var results: HolisticLandmarkerResult? = null + private var facePaint = Paint() + private var handPaint = Paint() + private var posePaint = Paint() + + private var scaleFactor: Float = 1f + private var imageWidth: Int = 1 + private var imageHeight: Int = 1 + + init { + initPaints() + } + + fun clear() { + results = null + facePaint.reset() + invalidate() + initPaints() + } + + private fun initPaints() { + facePaint.color = + ContextCompat.getColor(context!!, R.color.mp_color_primary) + facePaint.strokeWidth = LANDMARK_STROKE_WIDTH + facePaint.style = Paint.Style.STROKE + + handPaint.color = + ContextCompat.getColor(context!!, R.color.color_right_hand) + handPaint.strokeWidth = LANDMARK_STROKE_WIDTH + handPaint.style = Paint.Style.STROKE + + posePaint.color = + ContextCompat.getColor(context!!, R.color.color_pose) + posePaint.strokeWidth = LANDMARK_STROKE_WIDTH + posePaint.style = Paint.Style.STROKE + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + if (results == null || results!!.faceLandmarks().isEmpty()) { + clear() + return + } + // draw segmentation mask if present + if (results?.segmentationMask()?.isPresent == true) { + val buffer = ByteBufferExtractor.extract( + results?.segmentationMask()!!.get() + ) + // convert bytebuffer to bitmap + val bitmap = Bitmap.createBitmap( + imageWidth, + imageHeight, + Bitmap.Config.ARGB_8888 + ) + bitmap.copyPixelsFromBuffer(buffer) + // scale + val scaledBitmap = Bitmap.createScaledBitmap( + bitmap, + (bitmap.width * scaleFactor).roundToInt(), + (bitmap.height * scaleFactor).roundToInt(), + false + ) + // draw bitmap on canvas + canvas.drawBitmap( + scaledBitmap, + 0f, + 0f, + null + ) + } + + // draw pose landmarks + results?.poseLandmarks()?.let { poseLandmarkerResult -> + PoseLandmarker.POSE_LANDMARKS.forEach { + canvas.drawLine( + poseLandmarkerResult[it!!.start()] + .x() * imageWidth * scaleFactor, + poseLandmarkerResult[it.start()] + .y() * imageHeight * scaleFactor, + poseLandmarkerResult[it.end()] + .x() * imageWidth * scaleFactor, + poseLandmarkerResult[it.end()] + .y() * imageHeight * scaleFactor, + posePaint + ) + } + } + + // draw hand landmarks + results?.leftHandLandmarks()?.let { leftHandLandmarkerResult -> + HandLandmarker.HAND_CONNECTIONS.forEach { + canvas.drawLine( + leftHandLandmarkerResult[it!!.start()] + .x() * imageWidth * scaleFactor, + leftHandLandmarkerResult[it.start()] + .y() * imageHeight * scaleFactor, + leftHandLandmarkerResult[it.end()] + .x() * imageWidth * scaleFactor, + leftHandLandmarkerResult[it.end()] + .y() * imageHeight * scaleFactor, + handPaint + ) + } + } + + results?.rightHandLandmarks()?.let { rightHandLandmarkerResult -> + HandLandmarker.HAND_CONNECTIONS.forEach { + canvas.drawLine( + rightHandLandmarkerResult[it!!.start()] + .x() * imageWidth * scaleFactor, + rightHandLandmarkerResult[it.start()] + .y() * imageHeight * scaleFactor, + rightHandLandmarkerResult[it.end()] + .x() * imageWidth * scaleFactor, + rightHandLandmarkerResult[it.end()] + .y() * imageHeight * scaleFactor, + handPaint + ) + } + } + + results?.faceLandmarks()?.let { faceLandmarkerResult -> + + FaceLandmarker.FACE_LANDMARKS_CONNECTORS.forEach { + canvas.drawLine( + faceLandmarkerResult[it!!.start()] + .x() * imageWidth * scaleFactor, + faceLandmarkerResult[it.start()] + .y() * imageHeight * scaleFactor, + faceLandmarkerResult[it.end()] + .x() * imageWidth * scaleFactor, + faceLandmarkerResult[it.end()] + .y() * imageHeight * scaleFactor, + facePaint + ) + } + } + } + + fun setResults( + holisticLandmarkerResults: HolisticLandmarkerResult?, + imageHeight: Int, + imageWidth: Int, + runningMode: RunningMode = RunningMode.IMAGE + ) { + results = holisticLandmarkerResults + + this.imageHeight = imageHeight + this.imageWidth = imageWidth + + scaleFactor = when (runningMode) { + RunningMode.IMAGE, + RunningMode.VIDEO -> { + min(width * 1f / imageWidth, height * 1f / imageHeight) + } + + RunningMode.LIVE_STREAM -> { + // PreviewView is in FILL_START mode. So we need to scale up the + // landmarks to match with the size that the captured images will be + // displayed. + max(width * 1f / imageWidth, height * 1f / imageHeight) + } + } + invalidate() + } + + companion object { + private const val LANDMARK_STROKE_WIDTH = 8F + } +} diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/CameraFragment.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/CameraFragment.kt new file mode 100644 index 00000000..059bf4b3 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/CameraFragment.kt @@ -0,0 +1,385 @@ +package com.google.mediapipe.examples.holisticlandmarker.fragments + +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import androidx.camera.core.AspectRatio +import androidx.camera.core.Camera +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.Navigation +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.viewpager2.widget.ViewPager2 +import com.google.mediapipe.examples.holisticlandmarker.HolisticLandmarkerHelper +import com.google.mediapipe.examples.holisticlandmarker.MainViewModel +import com.google.mediapipe.examples.holisticlandmarker.R +import com.google.mediapipe.examples.holisticlandmarker.HelperState +import com.google.mediapipe.examples.holisticlandmarker.databinding.FragmentCameraBinding +import com.google.mediapipe.tasks.vision.core.RunningMode +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class CameraFragment() : Fragment(), + HolisticLandmarkerHelper.LandmarkerListener { + companion object { + private const val TAG = "Holistic Landmarker" + } + + private var _fragmentCameraBinding: FragmentCameraBinding? = null + + private val fragmentCameraBinding + get() = _fragmentCameraBinding!! + + private val viewModel: MainViewModel by activityViewModels() + + private var preview: Preview? = null + private var imageAnalyzer: ImageAnalysis? = null + private var camera: Camera? = null + private var cameraProvider: ProcessCameraProvider? = null + private var cameraFacing = CameraSelector.LENS_FACING_BACK + private lateinit var holisticLandmarkerHelper: HolisticLandmarkerHelper; + private val faceBlendshapesResultAdapter by lazy { + FaceBlendshapesResultAdapter() + } + + /** Blocking ML operations are performed using this executor */ + private lateinit var backgroundExecutor: ExecutorService + + override fun onResume() { + super.onResume() + // Make sure that all permissions are still present, since the + // user could have removed them while the app was in paused state. + if (!PermissionsFragment.hasPermissions(requireContext())) { + Navigation.findNavController( + requireActivity(), R.id.fragment_container + ).navigate(R.id.action_camera_to_permissions) + } + + // Start the HolisticLandmarkerHelper again when users come back + // to the foreground. + backgroundExecutor.execute { + if (holisticLandmarkerHelper.isClose()) { + holisticLandmarkerHelper.setUpHolisticLandmarker() + } + } + } + + override fun onPause() { + super.onPause() + if (this::holisticLandmarkerHelper.isInitialized) { + // Close the HolisticLandmarkerHelper and release resources + backgroundExecutor.execute { + holisticLandmarkerHelper.clearHolisticLandmarker() + } + } + } + + override fun onDestroyView() { + _fragmentCameraBinding = null + super.onDestroyView() + + // Shut down our background executor + backgroundExecutor.shutdown() + backgroundExecutor.awaitTermination( + Long.MAX_VALUE, TimeUnit.NANOSECONDS + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _fragmentCameraBinding = + FragmentCameraBinding.inflate(inflater, container, false) + + return fragmentCameraBinding.root + } + + @SuppressLint("MissingPermission") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(fragmentCameraBinding.recyclerviewResults) { + layoutManager = LinearLayoutManager(requireContext()) + adapter = faceBlendshapesResultAdapter + } + + // Initialize our background executor + backgroundExecutor = Executors.newSingleThreadExecutor() + + // Wait for the views to be properly laid out + fragmentCameraBinding.viewFinder.post { + // Set up the camera and its use cases + setUpCamera() + } + setUpListener() + viewModel.helperState.observe(viewLifecycleOwner) { helperState -> + updateBottomSheetControlsUi(helperState) + } + } + + private fun setUpListener() { + with(fragmentCameraBinding.bottomSheetLayout) { + faceLandmarksThresholdMinus.setOnClickListener { + viewModel.setMinFaceLandmarkConfidence(-0.1f) + } + faceLandmarksThresholdPlus.setOnClickListener { + viewModel.setMinFaceLandmarkConfidence(0.1f) + } + poseLandmarksThresholdMinus.setOnClickListener { + viewModel.setMinPoseLandmarkConfidence(-0.1f) + } + poseLandmarksThresholdPlus.setOnClickListener { + viewModel.setMinPoseLandmarkConfidence(0.1f) + } + handLandmarksThresholdMinus.setOnClickListener { + viewModel.setMinHandLandmarkConfidence(-0.1f) + } + handLandmarksThresholdPlus.setOnClickListener { + viewModel.setMinHandLandmarkConfidence(0.1f) + } + faceDetectionThresholdMinus.setOnClickListener { + viewModel.setMinFaceDetectionConfidence(-0.1f) + } + faceDetectionThresholdPlus.setOnClickListener { + viewModel.setMinFaceDetectionConfidence(0.1f) + } + poseDetectionThresholdMinus.setOnClickListener { + viewModel.setMinPoseDetectionConfidence(-0.1f) + } + poseDetectionThresholdPlus.setOnClickListener { + viewModel.setMinPoseDetectionConfidence(0.1f) + } + faceSuppressionMinus.setOnClickListener { + viewModel.setMinFaceSuppressionConfidence(-0.1f) + } + faceSuppressionPlus.setOnClickListener { + viewModel.setMinFaceSuppressionConfidence(0.1f) + } + poseSuppressionMinus.setOnClickListener { + viewModel.setMinPoseSuppressionConfidence(-0.1f) + } + poseSuppressionPlus.setOnClickListener { + viewModel.setMinPoseSuppressionConfidence(0.1f) + } + switchFaceBlendShapes.setOnCheckedChangeListener { _, isChecked -> + viewModel.setFaceBlendMode(isChecked) + } + switchPoseSegmentationMarks.setOnCheckedChangeListener { _, isChecked -> + viewModel.setPoseSegmentationMarks(isChecked) + } + spinnerDelegate.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long + ) { + try { + viewModel.setDelegate(p2) + } catch (e: UninitializedPropertyAccessException) { + Log.e( + TAG, + "HolisticLandmarkerHelper has not been initialized yet." + ) + } + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + /* no op */ + } + } + } + } + + private fun updateBottomSheetControlsUi(helperState: HelperState) { + val isFaceBlendShapes = + if (helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU) helperState.isFaceBlendMode else false + val isPoseSegmentationMarks = + if (helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU) helperState.isPoseSegmentationMarks else false + + // update bottom sheet settings + with(fragmentCameraBinding.bottomSheetLayout) { + faceLandmarksThresholdValue.text = + helperState.minFaceLandmarkThreshold.toString() + poseLandmarksThresholdValue.text = + helperState.minPoseLandmarkThreshold.toString() + handLandmarksThresholdValue.text = + helperState.minHandLandmarkThreshold.toString() + faceDetectionThresholdValue.text = + helperState.minFaceDetectionThreshold.toString() + poseDetectionThresholdValue.text = + helperState.minPoseDetectionThreshold.toString() + faceSuppressionValue.text = + helperState.minFaceSuppressionThreshold.toString() + poseSuppressionValue.text = + helperState.minPoseSuppressionThreshold.toString() + // enable with CPU delegate + switchFaceBlendShapes.isChecked = isFaceBlendShapes + switchPoseSegmentationMarks.isChecked = isPoseSegmentationMarks + switchFaceBlendShapes.isEnabled = + helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU + switchPoseSegmentationMarks.isEnabled = + helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU + } + // Create the HolisticLandmarkerHelper that will handle the inference + backgroundExecutor.execute { + // clear it and recreate with new settings + holisticLandmarkerHelper = HolisticLandmarkerHelper( + context = requireContext(), + runningMode = RunningMode.LIVE_STREAM, + currentDelegate = helperState.delegate, + minFaceLandmarksConfidence = helperState.minFaceLandmarkThreshold, + minHandLandmarksConfidence = helperState.minHandLandmarkThreshold, + minPoseLandmarksConfidence = helperState.minPoseLandmarkThreshold, + minFaceDetectionConfidence = helperState.minFaceDetectionThreshold, + minPoseDetectionConfidence = helperState.minPoseDetectionThreshold, + minFaceSuppressionThreshold = helperState.minFaceSuppressionThreshold, + minPoseSuppressionThreshold = helperState.minPoseSuppressionThreshold, + isFaceBlendShapes = isFaceBlendShapes, + isPoseSegmentationMark = isPoseSegmentationMarks, + landmarkerHelperListener = this + ) + _fragmentCameraBinding?.overlay?.clear() + } + } + + // Initialize CameraX, and prepare to bind the camera use cases + private fun setUpCamera() { + val cameraProviderFuture = + ProcessCameraProvider.getInstance(requireContext()) + cameraProviderFuture.addListener( + { + // CameraProvider + cameraProvider = cameraProviderFuture.get() + + // Build and bind the camera use cases + bindCameraUseCases() + }, ContextCompat.getMainExecutor(requireContext()) + ) + } + + // Declare and bind preview, capture and analysis use cases + @SuppressLint("UnsafeOptInUsageError") + private fun bindCameraUseCases() { + + // CameraProvider + val cameraProvider = cameraProvider + ?: throw IllegalStateException("Camera initialization failed.") + + val cameraSelector = + CameraSelector.Builder().requireLensFacing(cameraFacing).build() + + // Preview. Only using the 4:3 ratio because this is the closest to our models + preview = Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3) + .setTargetRotation(fragmentCameraBinding.viewFinder.display.rotation) + .build() + + // ImageAnalysis. Using RGBA 8888 to match how our models work + imageAnalyzer = + ImageAnalysis.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3) + .setTargetRotation(fragmentCameraBinding.viewFinder.display.rotation) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) + .build() + // The analyzer can then be assigned to the instance + .also { + it.setAnalyzer(backgroundExecutor) { image -> + detectFace(image) + } + } + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + + try { + // A variable number of use-cases can be passed here - + // camera provides access to CameraControl & CameraInfo + camera = cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageAnalyzer + ) + + // Attach the viewfinder's surface provider to preview use case + preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider) + } catch (exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + } + + private fun detectFace(imageProxy: ImageProxy) { + holisticLandmarkerHelper.detectLiveStreamCamera( + imageProxy = imageProxy, + isFrontCamera = cameraFacing == CameraSelector.LENS_FACING_FRONT + ) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + imageAnalyzer?.targetRotation = + fragmentCameraBinding.viewFinder.display.rotation + } + + override fun onError(error: String, errorCode: Int) { + Log.e(TAG, "An error $error, code $errorCode occurred") + activity?.runOnUiThread { + faceBlendshapesResultAdapter.updateResults(null) + faceBlendshapesResultAdapter.notifyDataSetChanged() + } + } + + // Update UI after face have been detected. Extracts original + // image height/width to scale and place the landmarks properly through + // OverlayView + override fun onResults( + resultBundle: HolisticLandmarkerHelper.ResultBundle + ) { + activity?.runOnUiThread { + if (_fragmentCameraBinding != null) { + if (fragmentCameraBinding.recyclerviewResults.scrollState != ViewPager2.SCROLL_STATE_DRAGGING) { + faceBlendshapesResultAdapter.updateResults(resultBundle.result) + faceBlendshapesResultAdapter.notifyDataSetChanged() + } + + fragmentCameraBinding.bottomSheetLayout.inferenceTimeVal.text = + String.format("%d ms", resultBundle.inferenceTime) + + // Pass necessary information to OverlayView for drawing on the canvas + fragmentCameraBinding.overlay.setResults( + resultBundle.result, + resultBundle.inputImageHeight, + resultBundle.inputImageWidth, + RunningMode.LIVE_STREAM + ) + // Force a redraw + fragmentCameraBinding.overlay.invalidate() + } + } + } +} \ No newline at end of file diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/FaceBlendshapesResultAdapter.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/FaceBlendshapesResultAdapter.kt new file mode 100644 index 00000000..b774f521 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/FaceBlendshapesResultAdapter.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.mediapipe.examples.holisticlandmarker.fragments + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.google.mediapipe.examples.holisticlandmarker.databinding.FaceBlendshapesResultBinding +import com.google.mediapipe.tasks.components.containers.Category +import com.google.mediapipe.tasks.vision.holisticlandmarker.HolisticLandmarkerResult + +class FaceBlendshapesResultAdapter : + RecyclerView.Adapter() { + companion object { + private const val NO_VALUE = "--" + } + + private var categories: MutableList = MutableList(52) { null } + + fun updateResults(faceLandmarkerResult: HolisticLandmarkerResult? = null) { + categories = MutableList(52) { null } + if (faceLandmarkerResult?.faceBlendshapes()?.isPresent == true) { + val sortedCategories = + faceLandmarkerResult.faceBlendshapes().get() + .sortedBy { -it.score() } + val min = + kotlin.math.min(sortedCategories.size, categories.size) + for (i in 0 until min) { + categories[i] = sortedCategories[i] + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val binding = FaceBlendshapesResultBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + categories[position].let { category -> + holder.bind(category?.categoryName(), category?.score()) + } + } + + override fun getItemCount(): Int = categories.size + + inner class ViewHolder(private val binding: FaceBlendshapesResultBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(label: String?, score: Float?) { + with(binding) { + tvLabel.text = label ?: NO_VALUE + tvScore.text = if (score != null) String.format( + "%.2f", + score + ) else NO_VALUE + } + } + } +} diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/GalleryFragment.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/GalleryFragment.kt new file mode 100644 index 00000000..68d183cd --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/GalleryFragment.kt @@ -0,0 +1,485 @@ +package com.google.mediapipe.examples.holisticlandmarker.fragments + +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.SystemClock +import android.provider.MediaStore +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.viewpager2.widget.ViewPager2 +import com.google.mediapipe.examples.holisticlandmarker.HelperState +import com.google.mediapipe.examples.holisticlandmarker.HolisticLandmarkerHelper +import com.google.mediapipe.examples.holisticlandmarker.MainViewModel +import com.google.mediapipe.examples.holisticlandmarker.R +import com.google.mediapipe.examples.holisticlandmarker.databinding.FragmentGalleryBinding +import com.google.mediapipe.tasks.vision.core.RunningMode +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +class GalleryFragment : Fragment(), + HolisticLandmarkerHelper.LandmarkerListener { + enum class MediaType { + IMAGE, VIDEO, UNKNOWN + } + + private var _fragmentGalleryBinding: FragmentGalleryBinding? = null + private val fragmentGalleryBinding + get() = _fragmentGalleryBinding!! + private lateinit var holisticLandmarkerHelper: HolisticLandmarkerHelper + private val viewModel: MainViewModel by activityViewModels() + private val faceBlendshapesResultAdapter by lazy { + FaceBlendshapesResultAdapter() + } + private lateinit var backgroundExecutor: ScheduledExecutorService + + private val getContent = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + // Handle the returned Uri + uri?.let { mediaUri -> + when (val mediaType = loadMediaType(mediaUri)) { + MediaType.IMAGE -> runDetectionOnImage(mediaUri) + MediaType.VIDEO -> runDetectionOnVideo(mediaUri) + MediaType.UNKNOWN -> { + updateDisplayView(mediaType) + Toast.makeText( + requireContext(), + "Unsupported data type.", + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _fragmentGalleryBinding = + FragmentGalleryBinding.inflate(inflater, container, false) + + return fragmentGalleryBinding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(fragmentGalleryBinding.recyclerviewResults) { + layoutManager = LinearLayoutManager(requireContext()) + adapter = faceBlendshapesResultAdapter + } + fragmentGalleryBinding.fabGetContent.setOnClickListener { + getContent.launch(arrayOf("image/*", "video/*")) + // reset the view + clearView() + } + setUpListener() + viewModel.helperState.observe(viewLifecycleOwner) { + updateBottomSheetControlsUi(it) + } + } + + override fun onPause() { + super.onPause() + clearView() + } + + private fun setUpListener() { + with(fragmentGalleryBinding.bottomSheetLayout) { + faceLandmarksThresholdMinus.setOnClickListener { + viewModel.setMinFaceLandmarkConfidence(-0.1f) + } + faceLandmarksThresholdPlus.setOnClickListener { + viewModel.setMinFaceLandmarkConfidence(0.1f) + } + poseLandmarksThresholdMinus.setOnClickListener { + viewModel.setMinPoseLandmarkConfidence(-0.1f) + } + poseLandmarksThresholdPlus.setOnClickListener { + viewModel.setMinPoseLandmarkConfidence(0.1f) + } + handLandmarksThresholdMinus.setOnClickListener { + viewModel.setMinHandLandmarkConfidence(-0.1f) + } + handLandmarksThresholdPlus.setOnClickListener { + viewModel.setMinHandLandmarkConfidence(0.1f) + } + faceDetectionThresholdMinus.setOnClickListener { + viewModel.setMinFaceDetectionConfidence(-0.1f) + } + faceDetectionThresholdPlus.setOnClickListener { + viewModel.setMinFaceDetectionConfidence(0.1f) + } + poseDetectionThresholdMinus.setOnClickListener { + viewModel.setMinPoseDetectionConfidence(-0.1f) + } + poseDetectionThresholdPlus.setOnClickListener { + viewModel.setMinPoseDetectionConfidence(0.1f) + } + faceSuppressionMinus.setOnClickListener { + viewModel.setMinFaceSuppressionConfidence(-0.1f) + } + faceSuppressionPlus.setOnClickListener { + viewModel.setMinFaceSuppressionConfidence(0.1f) + } + poseSuppressionMinus.setOnClickListener { + viewModel.setMinPoseSuppressionConfidence(-0.1f) + } + poseSuppressionPlus.setOnClickListener { + viewModel.setMinPoseSuppressionConfidence(0.1f) + } + switchFaceBlendShapes.setOnCheckedChangeListener { _, isChecked -> + viewModel.setFaceBlendMode(isChecked) + } + switchPoseSegmentationMarks.setOnCheckedChangeListener { _, isChecked -> + viewModel.setPoseSegmentationMarks(isChecked) + } + + spinnerDelegate.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long + ) { + try { + viewModel.setDelegate(p2) + } catch (e: UninitializedPropertyAccessException) { + Log.e( + TAG, + "HolisticLandmarkerHelper has not been initialized yet." + ) + } + } + + override fun onNothingSelected(p0: AdapterView<*>?) {/* no op */ + } + } + } + } + + private var isFaceBlendShapes = false + private var isPoseSegmentationMarks = false + + private fun updateBottomSheetControlsUi(helperState: HelperState) { + isFaceBlendShapes = + if (helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU) helperState.isFaceBlendMode else false + isPoseSegmentationMarks = + if (helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU) helperState.isPoseSegmentationMarks else false + + + // init bottom sheet settings + with(fragmentGalleryBinding.bottomSheetLayout) { + faceLandmarksThresholdValue.text = + helperState.minFaceLandmarkThreshold.toString() + poseLandmarksThresholdValue.text = + helperState.minPoseLandmarkThreshold.toString() + handLandmarksThresholdValue.text = + helperState.minHandLandmarkThreshold.toString() + faceDetectionThresholdValue.text = + helperState.minFaceDetectionThreshold.toString() + poseDetectionThresholdValue.text = + helperState.minPoseDetectionThreshold.toString() + faceSuppressionValue.text = + helperState.minFaceSuppressionThreshold.toString() + poseSuppressionValue.text = + helperState.minPoseSuppressionThreshold.toString() + // enable with CPU delegate + switchFaceBlendShapes.isChecked = isFaceBlendShapes + switchPoseSegmentationMarks.isChecked = isPoseSegmentationMarks + switchFaceBlendShapes.isEnabled = + helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU + switchPoseSegmentationMarks.isEnabled = + helperState.delegate == HolisticLandmarkerHelper.DELEGATE_CPU + } + clearView() + } + + // Load and display the image. + private fun runDetectionOnImage(uri: Uri) { + setUiEnabled(false) + updateDisplayView(MediaType.IMAGE) + backgroundExecutor = Executors.newSingleThreadScheduledExecutor() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val source = ImageDecoder.createSource( + requireActivity().contentResolver, uri + ) + ImageDecoder.decodeBitmap(source) + } else { + MediaStore.Images.Media.getBitmap( + requireActivity().contentResolver, uri + ) + }.copy(Bitmap.Config.ARGB_8888, true)?.let { bitmap -> + fragmentGalleryBinding.imageResult.setImageBitmap(bitmap) + + // Run holistic landmarker on the input image + backgroundExecutor.execute { + viewModel.helperState.value?.let { + holisticLandmarkerHelper = HolisticLandmarkerHelper( + context = requireContext(), + runningMode = RunningMode.IMAGE, + currentDelegate = it.delegate, + minFaceLandmarksConfidence = it.minFaceLandmarkThreshold, + minHandLandmarksConfidence = it.minHandLandmarkThreshold, + minPoseLandmarksConfidence = it.minPoseLandmarkThreshold, + minFaceDetectionConfidence = it.minFaceDetectionThreshold, + minPoseDetectionConfidence = it.minPoseDetectionThreshold, + minFaceSuppressionThreshold = it.minFaceSuppressionThreshold, + minPoseSuppressionThreshold = it.minPoseSuppressionThreshold, + isFaceBlendShapes = isFaceBlendShapes, + isPoseSegmentationMark = isPoseSegmentationMarks, + landmarkerHelperListener = this + ) + } + + holisticLandmarkerHelper.detectImage(bitmap) + ?.let { result -> + activity?.runOnUiThread { + if (fragmentGalleryBinding.recyclerviewResults.scrollState != ViewPager2.SCROLL_STATE_DRAGGING) { + faceBlendshapesResultAdapter.updateResults( + result.result + ) + faceBlendshapesResultAdapter.notifyDataSetChanged() + } + + fragmentGalleryBinding.overlay.setResults( + result.result, + bitmap.height, + bitmap.width, + RunningMode.IMAGE + ) + + setUiEnabled(true) + + fragmentGalleryBinding.bottomSheetLayout.inferenceTimeVal.text = + String.format("%d ms", result.inferenceTime) + } + } ?: run { + activity?.runOnUiThread { + setUiEnabled(true) + } + Log.e( + TAG, "Error running holistic landmarker." + ) + } + + holisticLandmarkerHelper.clearHolisticLandmarker() + } + } + } + + // clear view when switching between image and video + private fun clearView() { + with(fragmentGalleryBinding) { + tvPlaceholder.visibility = View.VISIBLE + bottomSheetLayout.inferenceTimeVal.text = + getString(R.string.tv_default_inference_time) + if (videoView.isPlaying) { + videoView.stopPlayback() + } + videoView.visibility = View.GONE + imageResult.visibility = View.GONE + overlay.clear() + } + faceBlendshapesResultAdapter.updateResults(null) + faceBlendshapesResultAdapter.notifyDataSetChanged() + } + + private fun runDetectionOnVideo(uri: Uri) { + setUiEnabled(false) + updateDisplayView(MediaType.VIDEO) + + with(fragmentGalleryBinding.videoView) { + setVideoURI(uri) + // mute the audio + setOnPreparedListener { it.setVolume(0f, 0f) } + requestFocus() + } + backgroundExecutor = Executors.newSingleThreadScheduledExecutor() + + backgroundExecutor.execute { + + viewModel.helperState.value?.let { + holisticLandmarkerHelper = HolisticLandmarkerHelper( + context = requireContext(), + runningMode = RunningMode.VIDEO, + currentDelegate = it.delegate, + minFaceLandmarksConfidence = it.minFaceLandmarkThreshold, + minHandLandmarksConfidence = it.minHandLandmarkThreshold, + minPoseLandmarksConfidence = it.minPoseLandmarkThreshold, + minFaceDetectionConfidence = it.minFaceDetectionThreshold, + minPoseDetectionConfidence = it.minPoseDetectionThreshold, + minFaceSuppressionThreshold = it.minFaceSuppressionThreshold, + minPoseSuppressionThreshold = it.minPoseSuppressionThreshold, + isFaceBlendShapes = isFaceBlendShapes, + isPoseSegmentationMark = isPoseSegmentationMarks, + landmarkerHelperListener = this + ) + } + + activity?.runOnUiThread { + fragmentGalleryBinding.videoView.visibility = View.GONE + fragmentGalleryBinding.progress.visibility = View.VISIBLE + } + + holisticLandmarkerHelper.detectVideoFile(uri, VIDEO_INTERVAL_MS) + ?.let { resultBundle -> + activity?.runOnUiThread { displayVideoResult(resultBundle) } + } ?: run { + setUiEnabled(true) + Log.e(TAG, "Error running holistic landmarker.") + } + + holisticLandmarkerHelper.clearHolisticLandmarker() + } + } + + // Setup and display the video. + private fun displayVideoResult(result: HolisticLandmarkerHelper.VideoResultBundle) { + + fragmentGalleryBinding.videoView.visibility = View.VISIBLE + fragmentGalleryBinding.progress.visibility = View.GONE + + fragmentGalleryBinding.videoView.start() + val videoStartTimeMs = SystemClock.uptimeMillis() + + backgroundExecutor.scheduleAtFixedRate( + { + activity?.runOnUiThread { + val videoElapsedTimeMs = + SystemClock.uptimeMillis() - videoStartTimeMs + val resultIndex = + videoElapsedTimeMs.div(VIDEO_INTERVAL_MS).toInt() + + if (resultIndex >= result.results.size || fragmentGalleryBinding.videoView.visibility == View.GONE) { + // The video playback has finished so we stop drawing bounding boxes + backgroundExecutor.shutdown() + } else { + fragmentGalleryBinding.overlay.setResults( + result.results[resultIndex], + result.inputImageHeight, + result.inputImageWidth, + RunningMode.VIDEO + ) + + if (fragmentGalleryBinding.recyclerviewResults.scrollState != ViewPager2.SCROLL_STATE_DRAGGING) { + faceBlendshapesResultAdapter.updateResults(result.results[resultIndex]) + faceBlendshapesResultAdapter.notifyDataSetChanged() + } + + setUiEnabled(true) + + fragmentGalleryBinding.bottomSheetLayout.inferenceTimeVal.text = + String.format("%d ms", result.inferenceTime) + } + } + }, 0, VIDEO_INTERVAL_MS, TimeUnit.MILLISECONDS + ) + } + + private fun updateDisplayView(mediaType: MediaType) { + fragmentGalleryBinding.imageResult.visibility = + if (mediaType == MediaType.IMAGE) View.VISIBLE else View.GONE + fragmentGalleryBinding.videoView.visibility = + if (mediaType == MediaType.VIDEO) View.VISIBLE else View.GONE + fragmentGalleryBinding.tvPlaceholder.visibility = + if (mediaType == MediaType.UNKNOWN) View.VISIBLE else View.GONE + } + + // Check the type of media that user selected. + private fun loadMediaType(uri: Uri): MediaType { + val mimeType = context?.contentResolver?.getType(uri) + mimeType?.let { + if (mimeType.startsWith("image")) return MediaType.IMAGE + if (mimeType.startsWith("video")) return MediaType.VIDEO + } + + return MediaType.UNKNOWN + } + + private fun setUiEnabled(enabled: Boolean) { + fragmentGalleryBinding.progress.visibility = + if (enabled) View.GONE else View.VISIBLE + fragmentGalleryBinding.fabGetContent.isEnabled = enabled + with(fragmentGalleryBinding.bottomSheetLayout) { + faceLandmarksThresholdMinus.isEnabled = enabled + faceLandmarksThresholdPlus.isEnabled = enabled + poseLandmarksThresholdMinus.isEnabled = enabled + poseLandmarksThresholdPlus.isEnabled = enabled + handLandmarksThresholdMinus.isEnabled = enabled + handLandmarksThresholdPlus.isEnabled = enabled + faceDetectionThresholdMinus.isEnabled = enabled + faceDetectionThresholdPlus.isEnabled = enabled + poseDetectionThresholdMinus.isEnabled = enabled + poseDetectionThresholdPlus.isEnabled = enabled + faceSuppressionMinus.isEnabled = enabled + faceSuppressionPlus.isEnabled = enabled + poseSuppressionMinus.isEnabled = enabled + poseSuppressionPlus.isEnabled = enabled + switchFaceBlendShapes.isEnabled = enabled + // only enable with CPU delegate + switchPoseSegmentationMarks.isEnabled = + if (viewModel.helperState.value?.delegate == HolisticLandmarkerHelper.DELEGATE_CPU) enabled else false + spinnerDelegate.isEnabled = + if (viewModel.helperState.value?.delegate == HolisticLandmarkerHelper.DELEGATE_CPU) enabled else false + } + } + + private fun classifyingError() { + activity?.runOnUiThread { + fragmentGalleryBinding.progress.visibility = View.GONE + setUiEnabled(true) + updateDisplayView(MediaType.UNKNOWN) + } + } + + override fun onError(error: String, errorCode: Int) { + classifyingError() + activity?.runOnUiThread { + Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show() + if (errorCode == HolisticLandmarkerHelper.GPU_ERROR) { + fragmentGalleryBinding.bottomSheetLayout.spinnerDelegate.setSelection( + HolisticLandmarkerHelper.DELEGATE_CPU, false + ) + } + } + } + + override fun onResults(resultBundle: HolisticLandmarkerHelper.ResultBundle) { + // no-op + } + + companion object { + private const val TAG = "GalleryFragment" + + // Value used to get frames at specific intervals for inference (e.g. every 300ms) + private const val VIDEO_INTERVAL_MS = 300L + } + +} diff --git a/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/PermissionsFragment.kt b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/PermissionsFragment.kt new file mode 100644 index 00000000..48528c8a --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/java/com/google/mediapipe/examples/holisticlandmarker/fragments/PermissionsFragment.kt @@ -0,0 +1,92 @@ +package com.google.mediapipe.examples.holisticlandmarker.fragments + +/* + * Copyright 2024 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.navigation.Navigation +import com.google.mediapipe.examples.holisticlandmarker.R + +private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA) + +class PermissionsFragment: Fragment() { + private val requestPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + Toast.makeText( + context, + "Permission request granted", + Toast.LENGTH_LONG + ).show() + navigateToCamera() + } else { + Toast.makeText( + context, + "Permission request denied", + Toast.LENGTH_LONG + ).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) -> { + navigateToCamera() + } + else -> { + requestPermissionLauncher.launch( + Manifest.permission.CAMERA + ) + } + } + } + + private fun navigateToCamera() { + lifecycleScope.launchWhenStarted { + Navigation.findNavController( + requireActivity(), + R.id.fragment_container + ).navigate( + R.id.action_permissions_to_camera + ) + } + } + + companion object { + + /** Convenience method used to check if all permissions required by this app are granted */ + fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all { + ContextCompat.checkSelfPermission( + context, + it + ) == PackageManager.PERMISSION_GRANTED + } + } +} diff --git a/examples/holistic_landmarker/android/app/src/main/res/color/bg_nav_item.xml b/examples/holistic_landmarker/android/app/src/main/res/color/bg_nav_item.xml new file mode 100644 index 00000000..d6336b54 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/color/bg_nav_item.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_add_24.xml b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_add_24.xml new file mode 100644 index 00000000..b7395fa7 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_add_24.xml @@ -0,0 +1,20 @@ + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml new file mode 100644 index 00000000..6a1e5a21 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_photo_library_24.xml b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_photo_library_24.xml new file mode 100644 index 00000000..c6401426 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_baseline_photo_library_24.xml @@ -0,0 +1,20 @@ + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_minus.xml b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 00000000..1d636f53 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,24 @@ + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_plus.xml b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 00000000..212f2693 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,24 @@ + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/drawable/icn_chevron_up.png b/examples/holistic_landmarker/android/app/src/main/res/drawable/icn_chevron_up.png new file mode 100644 index 00000000..67e1975a Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/drawable/icn_chevron_up.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/drawable/media_pipe_banner.xml b/examples/holistic_landmarker/android/app/src/main/res/drawable/media_pipe_banner.xml new file mode 100644 index 00000000..2f67ac7c --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/drawable/media_pipe_banner.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/layout/activity_main.xml b/examples/holistic_landmarker/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..dd8342f0 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/layout/face_blendshapes_result.xml b/examples/holistic_landmarker/android/app/src/main/res/layout/face_blendshapes_result.xml new file mode 100644 index 00000000..e7e0bfa2 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/layout/face_blendshapes_result.xml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/layout/fragment_camera.xml b/examples/holistic_landmarker/android/app/src/main/res/layout/fragment_camera.xml new file mode 100644 index 00000000..0436c4c4 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/layout/fragment_camera.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/layout/fragment_gallery.xml b/examples/holistic_landmarker/android/app/src/main/res/layout/fragment_gallery.xml new file mode 100644 index 00000000..5f8e41a4 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/layout/fragment_gallery.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/layout/info_bottom_sheet.xml b/examples/holistic_landmarker/android/app/src/main/res/layout/info_bottom_sheet.xml new file mode 100644 index 00000000..971fbf2c --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/layout/info_bottom_sheet.xml @@ -0,0 +1,457 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/menu/menu_bottom_nav.xml b/examples/holistic_landmarker/android/app/src/main/res/menu/menu_bottom_nav.xml new file mode 100644 index 00000000..a81355b9 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/menu/menu_bottom_nav.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..2d2fd07d Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..50f1d73b Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..afbe3ddf Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..7cdb3cd7 Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..c8c00e92 Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..f5bcd8c9 Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..fab12be5 Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..49905959 Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..c834177f Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..6bffe6ad Binary files /dev/null and b/examples/holistic_landmarker/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/examples/holistic_landmarker/android/app/src/main/res/navigation/nav_graph.xml b/examples/holistic_landmarker/android/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 00000000..27598774 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/holistic_landmarker/android/app/src/main/res/values/colors.xml b/examples/holistic_landmarker/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..e12b673f --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/values/colors.xml @@ -0,0 +1,19 @@ + + + #007F8B + #12B5CB + #00676D + #FBBC04 + #F9AB00 + #B00020 + #FF0000 + #FF9800 + #03A9F4 + #E0E0E0 + #FFFFFF + #EEEEEE + @android:color/black + #FFFFFFFF + #DDFFFFFF + #AAFFFFFF + diff --git a/examples/holistic_landmarker/android/app/src/main/res/values/dimens.xml b/examples/holistic_landmarker/android/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..9d6b4e90 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,18 @@ + + + 4dp + 64dp + + + 14sp + 16dp + 50dp + 0dp + 48dp + 10dp + 160dp + 240dp + + 3 + 16dp + diff --git a/examples/holistic_landmarker/android/app/src/main/res/values/strings.xml b/examples/holistic_landmarker/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..dffd93b7 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/values/strings.xml @@ -0,0 +1,35 @@ + + Holistic Landmarker + MediaPipe + + Camera + Gallery + Click + to add an image or a video + to begin running the holistic landmarker. + + Inference Time + Frames per Second + Detection + Threshold + Tracking + Threshold + Presence + Threshold + Number of Faces + Delegate + Face landmarks threshold + Pose landmarks threshold + Hand landmarks threshold + Face detection threshold + Pose detection threshold + Face suppression threshold + Pose suppression threshold + Face Blend Shapes + Pose Segmentation marks + 0 ms + + + CPU + GPU + + diff --git a/examples/holistic_landmarker/android/app/src/main/res/values/styles.xml b/examples/holistic_landmarker/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..5f2a61a6 --- /dev/null +++ b/examples/holistic_landmarker/android/app/src/main/res/values/styles.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/examples/holistic_landmarker/android/build.gradle.kts b/examples/holistic_landmarker/android/build.gradle.kts new file mode 100644 index 00000000..d2c9dcc9 --- /dev/null +++ b/examples/holistic_landmarker/android/build.gradle.kts @@ -0,0 +1,6 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id("com.android.application") version "8.1.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.0" apply false + id("de.undercouch.download") version "4.1.2" apply false +} diff --git a/examples/holistic_landmarker/android/gradle.properties b/examples/holistic_landmarker/android/gradle.properties new file mode 100644 index 00000000..3c5031eb --- /dev/null +++ b/examples/holistic_landmarker/android/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/examples/holistic_landmarker/android/gradle/wrapper/gradle-wrapper.jar b/examples/holistic_landmarker/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/examples/holistic_landmarker/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/holistic_landmarker/android/gradle/wrapper/gradle-wrapper.properties b/examples/holistic_landmarker/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..c7639a2a --- /dev/null +++ b/examples/holistic_landmarker/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 04 10:39:01 ICT 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/holistic_landmarker/android/gradlew b/examples/holistic_landmarker/android/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/examples/holistic_landmarker/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/examples/holistic_landmarker/android/gradlew.bat b/examples/holistic_landmarker/android/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/examples/holistic_landmarker/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/holistic_landmarker/android/screenshot.jpg b/examples/holistic_landmarker/android/screenshot.jpg new file mode 100644 index 00000000..21a67ce8 Binary files /dev/null and b/examples/holistic_landmarker/android/screenshot.jpg differ diff --git a/examples/holistic_landmarker/android/settings.gradle.kts b/examples/holistic_landmarker/android/settings.gradle.kts new file mode 100644 index 00000000..042bb7cf --- /dev/null +++ b/examples/holistic_landmarker/android/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Holistic Landmarker" +include(":app")