Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6c19885c6 | ||
|
|
cf101e62a9 | ||
|
|
f4e7f6cc48 | ||
|
|
d4bb35fe5a | ||
|
|
1faeaae7e5 | ||
|
|
33d683c671 | ||
|
|
137e1cc763 | ||
|
|
6abec128ed |
@@ -58,6 +58,7 @@ build:android_arm64 --fat_apk_cpu=arm64-v8a
|
|||||||
|
|
||||||
# iOS configs.
|
# iOS configs.
|
||||||
build:ios --apple_platform_type=ios
|
build:ios --apple_platform_type=ios
|
||||||
|
build:ios --copt=-fno-aligned-allocation
|
||||||
|
|
||||||
build:ios_i386 --config=ios
|
build:ios_i386 --config=ios
|
||||||
build:ios_i386 --cpu=ios_i386
|
build:ios_i386 --cpu=ios_i386
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.7.2
|
4.2.1
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
* Bug fixes
|
* Bug fixes
|
||||||
* Documentation fixes
|
* Documentation fixes
|
||||||
|
|
||||||
For new feature additions (e.g., new graphs and calculators), we are currently not planning to accept new feature pull requests into the MediaPipe repository. Instead, we like to get contributors to create their own repositories of the new feature and list it at [Awesome MediaPipe](https://mediapipe.org). This will allow contributors to more quickly get their code out to the community.
|
For new feature additions (e.g., new graphs and calculators), we are currently not planning to accept new feature pull requests into the MediaPipe repository. Instead, we like to get contributors to create their own repositories of the new feature and list it at [Awesome MediaPipe](https://mediapipe.page.link/awesome-mediapipe). This will allow contributors to more quickly get their code out to the community.
|
||||||
|
|
||||||
Before sending your pull requests, make sure you followed this list.
|
Before sending your pull requests, make sure you followed this list.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ RUN pip3 install tf_slim
|
|||||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||||
|
|
||||||
# Install bazel
|
# Install bazel
|
||||||
ARG BAZEL_VERSION=3.7.2
|
ARG BAZEL_VERSION=4.2.1
|
||||||
RUN mkdir /bazel && \
|
RUN mkdir /bazel && \
|
||||||
wget --no-check-certificate -O /bazel/installer.sh "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/b\
|
wget --no-check-certificate -O /bazel/installer.sh "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/b\
|
||||||
azel-${BAZEL_VERSION}-installer-linux-x86_64.sh" && \
|
azel-${BAZEL_VERSION}-installer-linux-x86_64.sh" && \
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ bazel_skylib_workspace()
|
|||||||
load("@bazel_skylib//lib:versions.bzl", "versions")
|
load("@bazel_skylib//lib:versions.bzl", "versions")
|
||||||
versions.check(minimum_bazel_version = "3.7.2")
|
versions.check(minimum_bazel_version = "3.7.2")
|
||||||
|
|
||||||
# ABSL cpp library lts_2020_09_23
|
# ABSL cpp library lts_2021_03_24, patch 2.
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "com_google_absl",
|
name = "com_google_absl",
|
||||||
urls = [
|
urls = [
|
||||||
"https://github.com/abseil/abseil-cpp/archive/20200923.tar.gz",
|
"https://github.com/abseil/abseil-cpp/archive/refs/tags/20210324.2.tar.gz",
|
||||||
],
|
],
|
||||||
# Remove after https://github.com/abseil/abseil-cpp/issues/326 is solved.
|
# Remove after https://github.com/abseil/abseil-cpp/issues/326 is solved.
|
||||||
patches = [
|
patches = [
|
||||||
@@ -29,8 +29,8 @@ http_archive(
|
|||||||
patch_args = [
|
patch_args = [
|
||||||
"-p1",
|
"-p1",
|
||||||
],
|
],
|
||||||
strip_prefix = "abseil-cpp-20200923",
|
strip_prefix = "abseil-cpp-20210324.2",
|
||||||
sha256 = "b3744a4f7a249d5eaf2309daad597631ce77ea62e0fc6abffbab4b4c3dc0fc08"
|
sha256 = "59b862f50e710277f8ede96f083a5bb8d7c9595376146838b9580be90374ee1f"
|
||||||
)
|
)
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
@@ -122,16 +122,16 @@ http_archive(
|
|||||||
# ...but the Java download is currently broken, so we use the "source" download.
|
# ...but the Java download is currently broken, so we use the "source" download.
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "com_google_protobuf_javalite",
|
name = "com_google_protobuf_javalite",
|
||||||
sha256 = "a79d19dcdf9139fa4b81206e318e33d245c4c9da1ffed21c87288ed4380426f9",
|
sha256 = "87407cd28e7a9c95d9f61a098a53cf031109d451a7763e7dd1253abf8b4df422",
|
||||||
strip_prefix = "protobuf-3.11.4",
|
strip_prefix = "protobuf-3.19.1",
|
||||||
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.11.4.tar.gz"],
|
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.1.tar.gz"],
|
||||||
)
|
)
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "com_google_protobuf",
|
name = "com_google_protobuf",
|
||||||
sha256 = "a79d19dcdf9139fa4b81206e318e33d245c4c9da1ffed21c87288ed4380426f9",
|
sha256 = "87407cd28e7a9c95d9f61a098a53cf031109d451a7763e7dd1253abf8b4df422",
|
||||||
strip_prefix = "protobuf-3.11.4",
|
strip_prefix = "protobuf-3.19.1",
|
||||||
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.11.4.tar.gz"],
|
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.1.tar.gz"],
|
||||||
patches = [
|
patches = [
|
||||||
"@//third_party:com_google_protobuf_fixes.diff"
|
"@//third_party:com_google_protobuf_fixes.diff"
|
||||||
],
|
],
|
||||||
@@ -154,28 +154,29 @@ http_archive(
|
|||||||
sha256 = "75922da3a1bdb417d820398eb03d4e9bd067c4905a4246d35a44c01d62154d91",
|
sha256 = "75922da3a1bdb417d820398eb03d4e9bd067c4905a4246d35a44c01d62154d91",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Point to the commit that deprecates the usage of Eigen::MappedSparseMatrix.
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "pybind11",
|
name = "pybind11",
|
||||||
urls = [
|
urls = [
|
||||||
"https://storage.googleapis.com/mirror.tensorflow.org/github.com/pybind/pybind11/archive/v2.4.3.tar.gz",
|
"https://github.com/pybind/pybind11/archive/70a58c577eaf067748c2ec31bfd0b0a614cffba6.zip",
|
||||||
"https://github.com/pybind/pybind11/archive/v2.4.3.tar.gz",
|
|
||||||
],
|
],
|
||||||
sha256 = "1eed57bc6863190e35637290f97a20c81cfe4d9090ac0a24f3bbf08f265eb71d",
|
sha256 = "b971842fab1b5b8f3815a2302331782b7d137fef0e06502422bc4bc360f4956c",
|
||||||
strip_prefix = "pybind11-2.4.3",
|
strip_prefix = "pybind11-70a58c577eaf067748c2ec31bfd0b0a614cffba6",
|
||||||
build_file = "@pybind11_bazel//:pybind11.BUILD",
|
build_file = "@pybind11_bazel//:pybind11.BUILD",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Point to the commit that deprecates the usage of Eigen::MappedSparseMatrix.
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "ceres_solver",
|
name = "ceres_solver",
|
||||||
url = "https://github.com/ceres-solver/ceres-solver/archive/2.0.0.zip",
|
url = "https://github.com/ceres-solver/ceres-solver/archive/123fba61cf2611a3c8bddc9d91416db26b10b558.zip",
|
||||||
patches = [
|
patches = [
|
||||||
"@//third_party:ceres_solver_compatibility_fixes.diff"
|
"@//third_party:ceres_solver_compatibility_fixes.diff"
|
||||||
],
|
],
|
||||||
patch_args = [
|
patch_args = [
|
||||||
"-p1",
|
"-p1",
|
||||||
],
|
],
|
||||||
strip_prefix = "ceres-solver-2.0.0",
|
strip_prefix = "ceres-solver-123fba61cf2611a3c8bddc9d91416db26b10b558",
|
||||||
sha256 = "db12d37b4cebb26353ae5b7746c7985e00877baa8e7b12dc4d3a1512252fff3b"
|
sha256 = "8b7b16ceb363420e0fd499576daf73fa338adb0b1449f58bea7862766baa1ac7"
|
||||||
)
|
)
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
@@ -249,21 +250,12 @@ http_archive(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# You may run setup_android.sh to install Android SDK and NDK.
|
|
||||||
android_ndk_repository(
|
|
||||||
name = "androidndk",
|
|
||||||
)
|
|
||||||
|
|
||||||
android_sdk_repository(
|
|
||||||
name = "androidsdk",
|
|
||||||
)
|
|
||||||
|
|
||||||
# iOS basic build deps.
|
# iOS basic build deps.
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "build_bazel_rules_apple",
|
name = "build_bazel_rules_apple",
|
||||||
sha256 = "7a7afdd4869bb201c9352eed2daf37294d42b093579b70423490c1b4d4f6ce42",
|
sha256 = "77e8bf6fda706f420a55874ae6ee4df0c9d95da6c7838228b26910fc82eea5a2",
|
||||||
url = "https://github.com/bazelbuild/rules_apple/releases/download/0.19.0/rules_apple.0.19.0.tar.gz",
|
url = "https://github.com/bazelbuild/rules_apple/releases/download/0.32.0/rules_apple.0.32.0.tar.gz",
|
||||||
patches = [
|
patches = [
|
||||||
# Bypass checking ios unit test runner when building MP ios applications.
|
# Bypass checking ios unit test runner when building MP ios applications.
|
||||||
"@//third_party:build_bazel_rules_apple_bypass_test_runner_check.diff"
|
"@//third_party:build_bazel_rules_apple_bypass_test_runner_check.diff"
|
||||||
@@ -289,10 +281,9 @@ swift_rules_dependencies()
|
|||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "build_bazel_apple_support",
|
name = "build_bazel_apple_support",
|
||||||
sha256 = "122ebf7fe7d1c8e938af6aeaee0efe788a3a2449ece5a8d6a428cb18d6f88033",
|
sha256 = "741366f79d900c11e11d8efd6cc6c66a31bfb2451178b58e0b5edc6f1db17b35",
|
||||||
urls = [
|
urls = [
|
||||||
"https://storage.googleapis.com/mirror.tensorflow.org/github.com/bazelbuild/apple_support/releases/download/0.7.1/apple_support.0.7.1.tar.gz",
|
"https://github.com/bazelbuild/apple_support/releases/download/0.10.0/apple_support.0.10.0.tar.gz"
|
||||||
"https://github.com/bazelbuild/apple_support/releases/download/0.7.1/apple_support.0.7.1.tar.gz",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -333,6 +324,7 @@ maven_install(
|
|||||||
"androidx.concurrent:concurrent-futures:1.0.0-alpha03",
|
"androidx.concurrent:concurrent-futures:1.0.0-alpha03",
|
||||||
"androidx.lifecycle:lifecycle-common:2.3.1",
|
"androidx.lifecycle:lifecycle-common:2.3.1",
|
||||||
"androidx.activity:activity:1.2.2",
|
"androidx.activity:activity:1.2.2",
|
||||||
|
"androidx.exifinterface:exifinterface:1.3.3",
|
||||||
"androidx.fragment:fragment:1.3.4",
|
"androidx.fragment:fragment:1.3.4",
|
||||||
"androidx.annotation:annotation:aar:1.1.0",
|
"androidx.annotation:annotation:aar:1.1.0",
|
||||||
"androidx.appcompat:appcompat:aar:1.1.0-rc01",
|
"androidx.appcompat:appcompat:aar:1.1.0-rc01",
|
||||||
@@ -349,8 +341,11 @@ maven_install(
|
|||||||
"com.google.auto.value:auto-value:1.8.1",
|
"com.google.auto.value:auto-value:1.8.1",
|
||||||
"com.google.auto.value:auto-value-annotations:1.8.1",
|
"com.google.auto.value:auto-value-annotations:1.8.1",
|
||||||
"com.google.code.findbugs:jsr305:latest.release",
|
"com.google.code.findbugs:jsr305:latest.release",
|
||||||
"com.google.flogger:flogger-system-backend:latest.release",
|
"com.google.android.datatransport:transport-api:3.0.0",
|
||||||
"com.google.flogger:flogger:latest.release",
|
"com.google.android.datatransport:transport-backend-cct:3.1.0",
|
||||||
|
"com.google.android.datatransport:transport-runtime:3.1.0",
|
||||||
|
"com.google.flogger:flogger-system-backend:0.6",
|
||||||
|
"com.google.flogger:flogger:0.6",
|
||||||
"com.google.guava:guava:27.0.1-android",
|
"com.google.guava:guava:27.0.1-android",
|
||||||
"com.google.guava:listenablefuture:1.0",
|
"com.google.guava:listenablefuture:1.0",
|
||||||
"junit:junit:4.12",
|
"junit:junit:4.12",
|
||||||
@@ -378,9 +373,9 @@ http_archive(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Tensorflow repo should always go after the other external dependencies.
|
# Tensorflow repo should always go after the other external dependencies.
|
||||||
# 2021-07-29
|
# 2021-12-02
|
||||||
_TENSORFLOW_GIT_COMMIT = "52a2905cbc21034766c08041933053178c5d10e3"
|
_TENSORFLOW_GIT_COMMIT = "18a1dc0ba806dc023808531f0373d9ec068e64bf"
|
||||||
_TENSORFLOW_SHA256 = "06d4691bcdb700f3275fa0971a1585221c2b9f3dffe867963be565a6643d7f56"
|
_TENSORFLOW_SHA256 = "85b90416f7a11339327777bccd634de00ca0de2cf334f5f0727edcb11ff9289a"
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "org_tensorflow",
|
name = "org_tensorflow",
|
||||||
urls = [
|
urls = [
|
||||||
@@ -389,6 +384,8 @@ http_archive(
|
|||||||
patches = [
|
patches = [
|
||||||
"@//third_party:org_tensorflow_compatibility_fixes.diff",
|
"@//third_party:org_tensorflow_compatibility_fixes.diff",
|
||||||
"@//third_party:org_tensorflow_objc_cxx17.diff",
|
"@//third_party:org_tensorflow_objc_cxx17.diff",
|
||||||
|
# Diff is generated with a script, don't update it manually.
|
||||||
|
"@//third_party:org_tensorflow_custom_ops.diff",
|
||||||
],
|
],
|
||||||
patch_args = [
|
patch_args = [
|
||||||
"-p1",
|
"-p1",
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ APIs (currently in alpha) that are now available in
|
|||||||
|
|
||||||
* Install MediaPipe following these [instructions](./install.md).
|
* Install MediaPipe following these [instructions](./install.md).
|
||||||
* Setup Java Runtime.
|
* Setup Java Runtime.
|
||||||
* Setup Android SDK release 28.0.3 and above.
|
* Setup Android SDK release 30.0.0 and above.
|
||||||
* Setup Android NDK version between 18 and 21.
|
* Setup Android NDK version 18 and above.
|
||||||
|
|
||||||
MediaPipe recommends setting up Android SDK and NDK via Android Studio (and see
|
MediaPipe recommends setting up Android SDK and NDK via Android Studio (and see
|
||||||
below for Android Studio setup). However, if you prefer using MediaPipe without
|
below for Android Studio setup). However, if you prefer using MediaPipe without
|
||||||
@@ -47,6 +47,15 @@ export ANDROID_HOME=<path to the Android SDK>
|
|||||||
export ANDROID_NDK_HOME=<path to the Android NDK>
|
export ANDROID_NDK_HOME=<path to the Android NDK>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
and add android_ndk_repository() and android_sdk_repository() rules into the
|
||||||
|
[`WORKSPACE`](https://github.com/google/mediapipe/blob/master/WORKSPACE) file as
|
||||||
|
the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ echo "android_sdk_repository(name = \"androidsdk\")" >> WORKSPACE
|
||||||
|
$ echo "android_ndk_repository(name = \"androidndk\")" >> WORKSPACE
|
||||||
|
```
|
||||||
|
|
||||||
In order to use MediaPipe on earlier Android versions, MediaPipe needs to switch
|
In order to use MediaPipe on earlier Android versions, MediaPipe needs to switch
|
||||||
to a lower Android API level. You can achieve this by specifying `api_level =
|
to a lower Android API level. You can achieve this by specifying `api_level =
|
||||||
$YOUR_INTENDED_API_LEVEL` in android_ndk_repository() and/or
|
$YOUR_INTENDED_API_LEVEL` in android_ndk_repository() and/or
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ each project.
|
|||||||
implementation 'com.google.flogger:flogger-system-backend:latest.release'
|
implementation 'com.google.flogger:flogger-system-backend:latest.release'
|
||||||
implementation 'com.google.code.findbugs:jsr305:latest.release'
|
implementation 'com.google.code.findbugs:jsr305:latest.release'
|
||||||
implementation 'com.google.guava:guava:27.0.1-android'
|
implementation 'com.google.guava:guava:27.0.1-android'
|
||||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
implementation 'com.google.protobuf:protobuf-javalite:3.19.1'
|
||||||
// CameraX core library
|
// CameraX core library
|
||||||
def camerax_version = "1.0.0-beta10"
|
def camerax_version = "1.0.0-beta10"
|
||||||
implementation "androidx.camera:camera-core:$camerax_version"
|
implementation "androidx.camera:camera-core:$camerax_version"
|
||||||
|
|||||||
@@ -1,58 +1,55 @@
|
|||||||
---
|
---
|
||||||
layout: default
|
layout: default
|
||||||
title: Android Solutions
|
title: MediaPipe Android Solutions
|
||||||
parent: MediaPipe on Android
|
parent: MediaPipe on Android
|
||||||
grand_parent: Getting Started
|
grand_parent: Getting Started
|
||||||
nav_order: 2
|
nav_order: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
# Android Solution APIs
|
# MediaPipe Android Solutions
|
||||||
{: .no_toc }
|
{: .no_toc }
|
||||||
|
|
||||||
1. TOC
|
1. TOC
|
||||||
{:toc}
|
{:toc}
|
||||||
---
|
---
|
||||||
|
|
||||||
Please follow instructions below to use the MediaPipe Solution APIs in Android
|
MediaPipe Android Solution APIs (currently in alpha) are available in:
|
||||||
Studio projects and build the Android example apps in the supported MediaPipe
|
|
||||||
[solutions](../solutions/solutions.md).
|
|
||||||
|
|
||||||
## Integrate MediaPipe Android Solutions in Android Studio
|
* [MediaPipe Face Detection](../solutions/face_detection#android-solution-api)
|
||||||
|
* [MediaPipe Face Mesh](../solutions/face_mesh#android-solution-api)
|
||||||
|
* [MediaPipe Hands](../solutions/hands#android-solution-api)
|
||||||
|
|
||||||
MediaPipe Android Solution APIs (currently in alpha) are now available in
|
## Incorporation in Android Studio
|
||||||
|
|
||||||
|
Prebuilt packages of Android Solution APIs can be found in
|
||||||
[Google's Maven Repository](https://maven.google.com/web/index.html?#com.google.mediapipe).
|
[Google's Maven Repository](https://maven.google.com/web/index.html?#com.google.mediapipe).
|
||||||
To incorporate MediaPipe Android Solutions into an Android Studio project, add
|
To incorporate them into an Android Studio project, add the following into the
|
||||||
the following into the project's Gradle dependencies:
|
project's Gradle dependencies:
|
||||||
|
|
||||||
```
|
```
|
||||||
dependencies {
|
dependencies {
|
||||||
// MediaPipe solution-core is the foundation of any MediaPipe solutions.
|
// MediaPipe solution-core is the foundation of any MediaPipe Solutions.
|
||||||
implementation 'com.google.mediapipe:solution-core:latest.release'
|
implementation 'com.google.mediapipe:solution-core:latest.release'
|
||||||
// Optional: MediaPipe Hands solution.
|
// Optional: MediaPipe Face Detection Solution.
|
||||||
implementation 'com.google.mediapipe:hands:latest.release'
|
implementation 'com.google.mediapipe:facedetection:latest.release'
|
||||||
// Optional: MediaPipe FaceMesh solution.
|
// Optional: MediaPipe Face Mesh Solution.
|
||||||
implementation 'com.google.mediapipe:facemesh:latest.release'
|
implementation 'com.google.mediapipe:facemesh:latest.release'
|
||||||
// MediaPipe deps
|
// Optional: MediaPipe Hands Solution.
|
||||||
implementation 'com.google.flogger:flogger:latest.release'
|
implementation 'com.google.mediapipe:hands:latest.release'
|
||||||
implementation 'com.google.flogger:flogger-system-backend:latest.release'
|
|
||||||
implementation 'com.google.guava:guava:27.0.1-android'
|
|
||||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
|
||||||
// CameraX core library
|
|
||||||
def camerax_version = "1.0.0-beta10"
|
|
||||||
implementation "androidx.camera:camera-core:$camerax_version"
|
|
||||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
|
||||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
See the detailed solutions API usage examples for different use cases in the
|
If you need further customization, instead of using the prebuilt maven packages
|
||||||
solution example apps'
|
consider building a MediaPipe Android Archive library locally from source by
|
||||||
[source code](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/solutions).
|
following these [instructions](./android_archive_library.md).
|
||||||
If the prebuilt maven packages are not sufficient, building the MediaPipe
|
|
||||||
Android archive library locally by following these
|
|
||||||
[instructions](./android_archive_library.md).
|
|
||||||
|
|
||||||
## Build solution example apps in Android Studio
|
## Building solution example apps
|
||||||
|
|
||||||
|
Detailed usage examples of the Android Solution APIs can be found in the
|
||||||
|
[source code](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/solutions)
|
||||||
|
of the solution example apps.
|
||||||
|
|
||||||
|
To build these apps:
|
||||||
|
|
||||||
1. Open Android Studio Arctic Fox on Linux, macOS, or Windows.
|
1. Open Android Studio Arctic Fox on Linux, macOS, or Windows.
|
||||||
|
|
||||||
@@ -73,7 +70,62 @@ Android archive library locally by following these
|
|||||||
|
|
||||||
6. (Optional) Run solutions on CPU.
|
6. (Optional) Run solutions on CPU.
|
||||||
|
|
||||||
MediaPipe solution example apps run the pipeline and the model inference on
|
MediaPipe solution example apps run the pipeline and model inference on GPU
|
||||||
GPU by default. If needed, for example to run the apps on Android Emulator,
|
by default. If needed, for example to run the apps on Android Emulator, set
|
||||||
set the `RUN_ON_GPU` boolean variable to `false` in the app's
|
the `RUN_ON_GPU` boolean variable to `false` in the app's
|
||||||
MainActivity.java to run the pipeline and the model inference on CPU.
|
`MainActivity.java` to run the pipeline and model inference on CPU.
|
||||||
|
|
||||||
|
## MediaPipe Solution APIs Terms of Service
|
||||||
|
|
||||||
|
Last modified: November 12, 2021
|
||||||
|
|
||||||
|
Use of MediaPipe Solution APIs is subject to the
|
||||||
|
[Google APIs Terms of Service](https://developers.google.com/terms),
|
||||||
|
[Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy),
|
||||||
|
and the terms below. Please check back from time to time as these terms and
|
||||||
|
policies are occasionally updated.
|
||||||
|
|
||||||
|
**Privacy**
|
||||||
|
|
||||||
|
When you use MediaPipe Solution APIs, processing of the input data (e.g. images,
|
||||||
|
video, text) fully happens on-device, and **MediaPipe does not send that input
|
||||||
|
data to Google servers**. As a result, you can use our APIs for processing data
|
||||||
|
that should not leave the device.
|
||||||
|
|
||||||
|
MediaPipe Android Solution APIs will contact Google servers from time to time in
|
||||||
|
order to receive things like bug fixes, updated models, and hardware accelerator
|
||||||
|
compatibility information. MediaPipe Android Solution APIs also send metrics
|
||||||
|
about the performance and utilization of the APIs in your app to Google. Google
|
||||||
|
uses this metrics data to measure performance, API usage, debug, maintain and
|
||||||
|
improve the APIs, and detect misuse or abuse, as further described in our
|
||||||
|
[Privacy Policy](https://policies.google.com/privacy).
|
||||||
|
|
||||||
|
**You are responsible for obtaining informed consent from your app users about
|
||||||
|
Google’s processing of MediaPipe metrics data as required by applicable law.**
|
||||||
|
|
||||||
|
Data we collect may include the following, across all MediaPipe Android Solution
|
||||||
|
APIs:
|
||||||
|
|
||||||
|
- Device information (such as manufacturer, model, OS version and build) and
|
||||||
|
available ML hardware accelerators (GPU and DSP). Used for diagnostics and
|
||||||
|
usage analytics.
|
||||||
|
|
||||||
|
- App identification information (package name / bundle id, app version). Used
|
||||||
|
for diagnostics and usage analytics.
|
||||||
|
|
||||||
|
- API configuration (such as image format, resolution, and MediaPipe version
|
||||||
|
used). Used for diagnostics and usage analytics.
|
||||||
|
|
||||||
|
- Event type (such as initialize, download model, update, run, and detection).
|
||||||
|
Used for diagnostics and usage analytics.
|
||||||
|
|
||||||
|
- Error codes. Used for diagnostics.
|
||||||
|
|
||||||
|
- Performance metrics. Used for diagnostics.
|
||||||
|
|
||||||
|
- Per-installation identifiers that do not uniquely identify a user or
|
||||||
|
physical device. Used for operation of remote configuration and usage
|
||||||
|
analytics.
|
||||||
|
|
||||||
|
- Network request sender IP addresses. Used for remote configuration
|
||||||
|
diagnostics. Collected IP addresses are retained temporarily.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ monotonically increasing timestamps. By convention, realtime calculators and
|
|||||||
graphs use the recording time or the presentation time as the timestamp for each
|
graphs use the recording time or the presentation time as the timestamp for each
|
||||||
packet, with each timestamp representing microseconds since
|
packet, with each timestamp representing microseconds since
|
||||||
`Jan/1/1970:00:00:00`. This allows packets from various sources to be processed
|
`Jan/1/1970:00:00:00`. This allows packets from various sources to be processed
|
||||||
in a gloablly consistent order.
|
in a globally consistent order.
|
||||||
|
|
||||||
Normally for offline processing, every input packet is processed and processing
|
Normally for offline processing, every input packet is processed and processing
|
||||||
continues as long as necessary. For online processing, it is often necessary to
|
continues as long as necessary. For online processing, it is often necessary to
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ bazel to build the iOS application. The content of the
|
|||||||
5. `Main.storyboard` and `Launch.storyboard`
|
5. `Main.storyboard` and `Launch.storyboard`
|
||||||
6. `Assets.xcassets` directory.
|
6. `Assets.xcassets` directory.
|
||||||
|
|
||||||
|
Note: In newer versions of Xcode, you may see additional files `SceneDelegate.h`
|
||||||
|
and `SceneDelegate.m`. Make sure to copy them too and add them to the `BUILD`
|
||||||
|
file mentioned below.
|
||||||
|
|
||||||
Copy these files to a directory named `HelloWorld` to a location that can access
|
Copy these files to a directory named `HelloWorld` to a location that can access
|
||||||
the MediaPipe source code. For example, the source code of the application that
|
the MediaPipe source code. For example, the source code of the application that
|
||||||
we will build in this tutorial is located in
|
we will build in this tutorial is located in
|
||||||
@@ -247,6 +251,12 @@ We need to get frames from the `_cameraSource` into our application
|
|||||||
`MPPInputSourceDelegate`. So our application `ViewController` can be a delegate
|
`MPPInputSourceDelegate`. So our application `ViewController` can be a delegate
|
||||||
of `_cameraSource`.
|
of `_cameraSource`.
|
||||||
|
|
||||||
|
Update the interface definition of `ViewController` accordingly:
|
||||||
|
|
||||||
|
```
|
||||||
|
@interface ViewController () <MPPInputSourceDelegate>
|
||||||
|
```
|
||||||
|
|
||||||
To handle camera setup and process incoming frames, we should use a queue
|
To handle camera setup and process incoming frames, we should use a queue
|
||||||
different from the main queue. Add the following to the implementation block of
|
different from the main queue. Add the following to the implementation block of
|
||||||
the `ViewController`:
|
the `ViewController`:
|
||||||
@@ -288,6 +298,12 @@ utility called `MPPLayerRenderer` to display images on the screen. This utility
|
|||||||
can be used to display `CVPixelBufferRef` objects, which is the type of the
|
can be used to display `CVPixelBufferRef` objects, which is the type of the
|
||||||
images provided by `MPPCameraInputSource` to its delegates.
|
images provided by `MPPCameraInputSource` to its delegates.
|
||||||
|
|
||||||
|
In `ViewController.m`, add the following import line:
|
||||||
|
|
||||||
|
```
|
||||||
|
#import "mediapipe/objc/MPPLayerRenderer.h"
|
||||||
|
```
|
||||||
|
|
||||||
To display images of the screen, we need to add a new `UIView` object called
|
To display images of the screen, we need to add a new `UIView` object called
|
||||||
`_liveView` to the `ViewController`.
|
`_liveView` to the `ViewController`.
|
||||||
|
|
||||||
@@ -411,6 +427,12 @@ Objective-C++.
|
|||||||
|
|
||||||
### Use the graph in `ViewController`
|
### Use the graph in `ViewController`
|
||||||
|
|
||||||
|
In `ViewController.m`, add the following import line:
|
||||||
|
|
||||||
|
```
|
||||||
|
#import "mediapipe/objc/MPPGraph.h"
|
||||||
|
```
|
||||||
|
|
||||||
Declare a static constant with the name of the graph, the input stream and the
|
Declare a static constant with the name of the graph, the input stream and the
|
||||||
output stream:
|
output stream:
|
||||||
|
|
||||||
@@ -549,6 +571,12 @@ method to receive packets on this output stream and display them on the screen:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Update the interface definition of `ViewController` with `MPPGraphDelegate`:
|
||||||
|
|
||||||
|
```
|
||||||
|
@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>
|
||||||
|
```
|
||||||
|
|
||||||
And that is all! Build and run the app on your iOS device. You should see the
|
And that is all! Build and run the app on your iOS device. You should see the
|
||||||
results of running the edge detection graph on a live video feed. Congrats!
|
results of running the edge detection graph on a live video feed. Congrats!
|
||||||
|
|
||||||
@@ -560,5 +588,5 @@ appropriate `BUILD` file dependencies for the edge detection graph.
|
|||||||
|
|
||||||
[Bazel]:https://bazel.build/
|
[Bazel]:https://bazel.build/
|
||||||
[`edge_detection_mobile_gpu.pbtxt`]:https://github.com/google/mediapipe/tree/master/mediapipe/graphs/edge_detection/edge_detection_mobile_gpu.pbtxt
|
[`edge_detection_mobile_gpu.pbtxt`]:https://github.com/google/mediapipe/tree/master/mediapipe/graphs/edge_detection/edge_detection_mobile_gpu.pbtxt
|
||||||
[common]:(https://github.com/google/mediapipe/tree/master/mediapipe/examples/ios/common)
|
[common]:https://github.com/google/mediapipe/tree/master/mediapipe/examples/ios/common
|
||||||
[helloworld]:(https://github.com/google/mediapipe/tree/master/mediapipe/examples/ios/helloworld)
|
[helloworld]:https://github.com/google/mediapipe/tree/master/mediapipe/examples/ios/helloworld
|
||||||
|
|||||||
@@ -569,7 +569,7 @@ next section.
|
|||||||
|
|
||||||
Option 1. Follow
|
Option 1. Follow
|
||||||
[the official Bazel documentation](https://docs.bazel.build/versions/master/install-windows.html)
|
[the official Bazel documentation](https://docs.bazel.build/versions/master/install-windows.html)
|
||||||
to install Bazel 3.7.2 or higher.
|
to install Bazel 4.2.1 or higher.
|
||||||
|
|
||||||
Option 2. Follow the official
|
Option 2. Follow the official
|
||||||
[Bazel documentation](https://docs.bazel.build/versions/master/install-bazelisk.html)
|
[Bazel documentation](https://docs.bazel.build/versions/master/install-bazelisk.html)
|
||||||
@@ -657,7 +657,7 @@ cameras. Alternatively, you use a video file as input.
|
|||||||
|
|
||||||
Note: Windows' and WSL’s adb versions must be the same version, e.g., if WSL
|
Note: Windows' and WSL’s adb versions must be the same version, e.g., if WSL
|
||||||
has ADB 1.0.39, you need to download the corresponding Windows ADB from
|
has ADB 1.0.39, you need to download the corresponding Windows ADB from
|
||||||
[here](https://dl.google.com/android/repository/platform-tools_r26.0.1-windows.zip).
|
[here](https://dl.google.com/android/repository/platform-tools_r30.0.3-windows.zip).
|
||||||
|
|
||||||
3. Launch WSL.
|
3. Launch WSL.
|
||||||
|
|
||||||
@@ -796,7 +796,7 @@ This will use a Docker image that will isolate mediapipe's installation from the
|
|||||||
```bash
|
```bash
|
||||||
$ docker run -it --name mediapipe mediapipe:latest
|
$ docker run -it --name mediapipe mediapipe:latest
|
||||||
|
|
||||||
root@bca08b91ff63:/mediapipe# GLOG_logtostderr=1 bazel run --define MEDIAPIPE_DISABLE_GPU=1 mediapipe/examples/desktop/hello_world:hello_world
|
root@bca08b91ff63:/mediapipe# GLOG_logtostderr=1 bazel run --define MEDIAPIPE_DISABLE_GPU=1 mediapipe/examples/desktop/hello_world
|
||||||
|
|
||||||
# Should print:
|
# Should print:
|
||||||
# Hello World!
|
# Hello World!
|
||||||
|
|||||||
@@ -249,12 +249,12 @@ three stages: initialization and setup, graph run, and graph shutdown.
|
|||||||
graph.start_run()
|
graph.start_run()
|
||||||
|
|
||||||
graph.add_packet_to_input_stream(
|
graph.add_packet_to_input_stream(
|
||||||
'in_stream', mp.packet_creator.create_str('abc').at(0))
|
'in_stream', mp.packet_creator.create_string('abc').at(0))
|
||||||
|
|
||||||
rgb_img = cv2.cvtColor(cv2.imread('/path/to/your/image.png'), cv2.COLOR_BGR2RGB)
|
rgb_img = cv2.cvtColor(cv2.imread('/path/to/your/image.png'), cv2.COLOR_BGR2RGB)
|
||||||
graph.add_packet_to_input_stream(
|
graph.add_packet_to_input_stream(
|
||||||
'in_stream',
|
'in_stream',
|
||||||
mp.packet_creator.create_image_frame(format=mp.ImageFormat.SRGB,
|
mp.packet_creator.create_image_frame(image_format=mp.ImageFormat.SRGB,
|
||||||
data=rgb_img).at(1))
|
data=rgb_img).at(1))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -108,14 +108,14 @@ ERROR: No matching distribution found for mediapipe
|
|||||||
|
|
||||||
after running `pip install mediapipe` usually indicates that there is no qualified MediaPipe Python for your system.
|
after running `pip install mediapipe` usually indicates that there is no qualified MediaPipe Python for your system.
|
||||||
Please note that MediaPipe Python PyPI officially supports the **64-bit**
|
Please note that MediaPipe Python PyPI officially supports the **64-bit**
|
||||||
version of Python 3.7 and above on the following OS:
|
version of Python 3.7 to 3.10 on the following OS:
|
||||||
|
|
||||||
- x86_64 Linux
|
- x86_64 Linux
|
||||||
- x86_64 macOS 10.15+
|
- x86_64 macOS 10.15+
|
||||||
- amd64 Windows
|
- amd64 Windows
|
||||||
|
|
||||||
If the OS is currently supported and you still see this error, please make sure
|
If the OS is currently supported and you still see this error, please make sure
|
||||||
that both the Python and pip binary are for Python 3.7 and above. Otherwise,
|
that both the Python and pip binary are for Python 3.7 to 3.10. Otherwise,
|
||||||
please consider building the MediaPipe Python package locally by following the
|
please consider building the MediaPipe Python package locally by following the
|
||||||
instructions [here](python.md#building-mediapipe-python-package).
|
instructions [here](python.md#building-mediapipe-python-package).
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 797 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 77 KiB |
@@ -121,12 +121,10 @@ with mp_face_detection.FaceDetection(
|
|||||||
# If loading a video, use 'break' instead of 'continue'.
|
# If loading a video, use 'break' instead of 'continue'.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Flip the image horizontally for a later selfie-view display, and convert
|
|
||||||
# the BGR image to RGB.
|
|
||||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
|
||||||
# To improve performance, optionally mark the image as not writeable to
|
# To improve performance, optionally mark the image as not writeable to
|
||||||
# pass by reference.
|
# pass by reference.
|
||||||
image.flags.writeable = False
|
image.flags.writeable = False
|
||||||
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
results = face_detection.process(image)
|
results = face_detection.process(image)
|
||||||
|
|
||||||
# Draw the face detection annotations on the image.
|
# Draw the face detection annotations on the image.
|
||||||
@@ -135,7 +133,8 @@ with mp_face_detection.FaceDetection(
|
|||||||
if results.detections:
|
if results.detections:
|
||||||
for detection in results.detections:
|
for detection in results.detections:
|
||||||
mp_drawing.draw_detection(image, detection)
|
mp_drawing.draw_detection(image, detection)
|
||||||
cv2.imshow('MediaPipe Face Detection', image)
|
# Flip the image horizontally for a selfie-view display.
|
||||||
|
cv2.imshow('MediaPipe Face Detection', cv2.flip(image, 1))
|
||||||
if cv2.waitKey(5) & 0xFF == 27:
|
if cv2.waitKey(5) & 0xFF == 27:
|
||||||
break
|
break
|
||||||
cap.release()
|
cap.release()
|
||||||
@@ -200,7 +199,7 @@ const faceDetection = new FaceDetection({locateFile: (file) => {
|
|||||||
return `https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/${file}`;
|
return `https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/${file}`;
|
||||||
}});
|
}});
|
||||||
faceDetection.setOptions({
|
faceDetection.setOptions({
|
||||||
modelSelection: 0
|
modelSelection: 0,
|
||||||
minDetectionConfidence: 0.5
|
minDetectionConfidence: 0.5
|
||||||
});
|
});
|
||||||
faceDetection.onResults(onResults);
|
faceDetection.onResults(onResults);
|
||||||
@@ -216,6 +215,214 @@ camera.start();
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Android Solution API
|
||||||
|
|
||||||
|
Please first follow general
|
||||||
|
[instructions](../getting_started/android_solutions.md) to add MediaPipe Gradle
|
||||||
|
dependencies and try the Android Solution API in the companion
|
||||||
|
[example Android Studio project](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/solutions/facedetection),
|
||||||
|
and learn more in the usage example below.
|
||||||
|
|
||||||
|
Supported configuration options:
|
||||||
|
|
||||||
|
* [staticImageMode](#static_image_mode)
|
||||||
|
* [modelSelection](#model_selection)
|
||||||
|
|
||||||
|
#### Camera Input
|
||||||
|
|
||||||
|
```java
|
||||||
|
// For camera input and result rendering with OpenGL.
|
||||||
|
FaceDetectionOptions faceDetectionOptions =
|
||||||
|
FaceDetectionOptions.builder()
|
||||||
|
.setStaticImageMode(false)
|
||||||
|
.setModelSelection(0).build();
|
||||||
|
FaceDetection faceDetection = new FaceDetection(this, faceDetectionOptions);
|
||||||
|
faceDetection.setErrorListener(
|
||||||
|
(message, e) -> Log.e(TAG, "MediaPipe Face Detection error:" + message));
|
||||||
|
|
||||||
|
// Initializes a new CameraInput instance and connects it to MediaPipe Face Detection Solution.
|
||||||
|
CameraInput cameraInput = new CameraInput(this);
|
||||||
|
cameraInput.setNewFrameListener(
|
||||||
|
textureFrame -> faceDetection.send(textureFrame));
|
||||||
|
|
||||||
|
// Initializes a new GlSurfaceView with a ResultGlRenderer<FaceDetectionResult> instance
|
||||||
|
// that provides the interfaces to run user-defined OpenGL rendering code.
|
||||||
|
// See mediapipe/examples/android/solutions/facedetection/src/main/java/com/google/mediapipe/examples/facedetection/FaceDetectionResultGlRenderer.java
|
||||||
|
// as an example.
|
||||||
|
SolutionGlSurfaceView<FaceDetectionResult> glSurfaceView =
|
||||||
|
new SolutionGlSurfaceView<>(
|
||||||
|
this, faceDetection.getGlContext(), faceDetection.getGlMajorVersion());
|
||||||
|
glSurfaceView.setSolutionResultRenderer(new FaceDetectionResultGlRenderer());
|
||||||
|
glSurfaceView.setRenderInputImage(true);
|
||||||
|
faceDetection.setResultListener(
|
||||||
|
faceDetectionResult -> {
|
||||||
|
if (faceDetectionResult.multiFaceDetections().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RelativeKeypoint noseTip =
|
||||||
|
faceDetectionResult
|
||||||
|
.multiFaceDetections()
|
||||||
|
.get(0)
|
||||||
|
.getLocationData()
|
||||||
|
.getRelativeKeypoints(FaceKeypoint.NOSE_TIP);
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaPipe Face Detection nose tip normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||||
|
noseTip.getX(), noseTip.getY()));
|
||||||
|
// Request GL rendering.
|
||||||
|
glSurfaceView.setRenderData(faceDetectionResult);
|
||||||
|
glSurfaceView.requestRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The runnable to start camera after the GLSurfaceView is attached.
|
||||||
|
glSurfaceView.post(
|
||||||
|
() ->
|
||||||
|
cameraInput.start(
|
||||||
|
this,
|
||||||
|
faceDetection.getGlContext(),
|
||||||
|
CameraInput.CameraFacing.FRONT,
|
||||||
|
glSurfaceView.getWidth(),
|
||||||
|
glSurfaceView.getHeight()));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Image Input
|
||||||
|
|
||||||
|
```java
|
||||||
|
// For reading images from gallery and drawing the output in an ImageView.
|
||||||
|
FaceDetectionOptions faceDetectionOptions =
|
||||||
|
FaceDetectionOptions.builder()
|
||||||
|
.setStaticImageMode(true)
|
||||||
|
.setModelSelection(0).build();
|
||||||
|
FaceDetection faceDetection = new FaceDetection(this, faceDetectionOptions);
|
||||||
|
|
||||||
|
// Connects MediaPipe Face Detection Solution to the user-defined ImageView
|
||||||
|
// instance that allows users to have the custom drawing of the output landmarks
|
||||||
|
// on it. See mediapipe/examples/android/solutions/facedetection/src/main/java/com/google/mediapipe/examples/facedetection/FaceDetectionResultImageView.java
|
||||||
|
// as an example.
|
||||||
|
FaceDetectionResultImageView imageView = new FaceDetectionResultImageView(this);
|
||||||
|
faceDetection.setResultListener(
|
||||||
|
faceDetectionResult -> {
|
||||||
|
if (faceDetectionResult.multiFaceDetections().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int width = faceDetectionResult.inputBitmap().getWidth();
|
||||||
|
int height = faceDetectionResult.inputBitmap().getHeight();
|
||||||
|
RelativeKeypoint noseTip =
|
||||||
|
faceDetectionResult
|
||||||
|
.multiFaceDetections()
|
||||||
|
.get(0)
|
||||||
|
.getLocationData()
|
||||||
|
.getRelativeKeypoints(FaceKeypoint.NOSE_TIP);
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaPipe Face Detection nose tip coordinates (pixel values): x=%f, y=%f",
|
||||||
|
noseTip.getX() * width, noseTip.getY() * height));
|
||||||
|
// Request canvas drawing.
|
||||||
|
imageView.setFaceDetectionResult(faceDetectionResult);
|
||||||
|
runOnUiThread(() -> imageView.update());
|
||||||
|
});
|
||||||
|
faceDetection.setErrorListener(
|
||||||
|
(message, e) -> Log.e(TAG, "MediaPipe Face Detection error:" + message));
|
||||||
|
|
||||||
|
// ActivityResultLauncher to get an image from the gallery as Bitmap.
|
||||||
|
ActivityResultLauncher<Intent> imageGetter =
|
||||||
|
registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
Intent resultIntent = result.getData();
|
||||||
|
if (resultIntent != null && result.getResultCode() == RESULT_OK) {
|
||||||
|
Bitmap bitmap = null;
|
||||||
|
try {
|
||||||
|
bitmap =
|
||||||
|
MediaStore.Images.Media.getBitmap(
|
||||||
|
this.getContentResolver(), resultIntent.getData());
|
||||||
|
// Please also rotate the Bitmap based on its orientation.
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Bitmap reading error:" + e);
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
faceDetection.send(bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Intent pickImageIntent = new Intent(Intent.ACTION_PICK);
|
||||||
|
pickImageIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||||
|
imageGetter.launch(pickImageIntent);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Video Input
|
||||||
|
|
||||||
|
```java
|
||||||
|
// For video input and result rendering with OpenGL.
|
||||||
|
FaceDetectionOptions faceDetectionOptions =
|
||||||
|
FaceDetectionOptions.builder()
|
||||||
|
.setStaticImageMode(false)
|
||||||
|
.setModelSelection(0).build();
|
||||||
|
FaceDetection faceDetection = new FaceDetection(this, faceDetectionOptions);
|
||||||
|
faceDetection.setErrorListener(
|
||||||
|
(message, e) -> Log.e(TAG, "MediaPipe Face Detection error:" + message));
|
||||||
|
|
||||||
|
// Initializes a new VideoInput instance and connects it to MediaPipe Face Detection Solution.
|
||||||
|
VideoInput videoInput = new VideoInput(this);
|
||||||
|
videoInput.setNewFrameListener(
|
||||||
|
textureFrame -> faceDetection.send(textureFrame));
|
||||||
|
|
||||||
|
// Initializes a new GlSurfaceView with a ResultGlRenderer<FaceDetectionResult> instance
|
||||||
|
// that provides the interfaces to run user-defined OpenGL rendering code.
|
||||||
|
// See mediapipe/examples/android/solutions/facedetection/src/main/java/com/google/mediapipe/examples/facedetection/FaceDetectionResultGlRenderer.java
|
||||||
|
// as an example.
|
||||||
|
SolutionGlSurfaceView<FaceDetectionResult> glSurfaceView =
|
||||||
|
new SolutionGlSurfaceView<>(
|
||||||
|
this, faceDetection.getGlContext(), faceDetection.getGlMajorVersion());
|
||||||
|
glSurfaceView.setSolutionResultRenderer(new FaceDetectionResultGlRenderer());
|
||||||
|
glSurfaceView.setRenderInputImage(true);
|
||||||
|
|
||||||
|
faceDetection.setResultListener(
|
||||||
|
faceDetectionResult -> {
|
||||||
|
if (faceDetectionResult.multiFaceDetections().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RelativeKeypoint noseTip =
|
||||||
|
faceDetectionResult
|
||||||
|
.multiFaceDetections()
|
||||||
|
.get(0)
|
||||||
|
.getLocationData()
|
||||||
|
.getRelativeKeypoints(FaceKeypoint.NOSE_TIP);
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaPipe Face Detection nose tip normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||||
|
noseTip.getX(), noseTip.getY()));
|
||||||
|
// Request GL rendering.
|
||||||
|
glSurfaceView.setRenderData(faceDetectionResult);
|
||||||
|
glSurfaceView.requestRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
ActivityResultLauncher<Intent> videoGetter =
|
||||||
|
registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
Intent resultIntent = result.getData();
|
||||||
|
if (resultIntent != null) {
|
||||||
|
if (result.getResultCode() == RESULT_OK) {
|
||||||
|
glSurfaceView.post(
|
||||||
|
() ->
|
||||||
|
videoInput.start(
|
||||||
|
this,
|
||||||
|
resultIntent.getData(),
|
||||||
|
faceDetection.getGlContext(),
|
||||||
|
glSurfaceView.getWidth(),
|
||||||
|
glSurfaceView.getHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Intent pickVideoIntent = new Intent(Intent.ACTION_PICK);
|
||||||
|
pickVideoIntent.setDataAndType(MediaStore.Video.Media.INTERNAL_CONTENT_URI, "video/*");
|
||||||
|
videoGetter.launch(pickVideoIntent);
|
||||||
|
```
|
||||||
|
|
||||||
## Example Apps
|
## Example Apps
|
||||||
|
|
||||||
Please first see general instructions for
|
Please first see general instructions for
|
||||||
|
|||||||
+97
-51
@@ -111,6 +111,23 @@ You can find more information about the face landmark model in this
|
|||||||
:------------------------------------------------------------------------: |
|
:------------------------------------------------------------------------: |
|
||||||
*Fig 2. Face landmarks: the red box indicates the cropped area as input to the landmark model, the red dots represent the 468 landmarks in 3D, and the green lines connecting landmarks illustrate the contours around the eyes, eyebrows, lips and the entire face.* |
|
*Fig 2. Face landmarks: the red box indicates the cropped area as input to the landmark model, the red dots represent the 468 landmarks in 3D, and the green lines connecting landmarks illustrate the contours around the eyes, eyebrows, lips and the entire face.* |
|
||||||
|
|
||||||
|
#### Attention Mesh Model
|
||||||
|
|
||||||
|
In addition to the [Face Landmark Model](#face-landmark-model) we provide
|
||||||
|
another model that applies
|
||||||
|
[attention](https://en.wikipedia.org/wiki/Attention_(machine_learning)) to
|
||||||
|
semantically meaningful face regions, and therefore predicting landmarks more
|
||||||
|
accurately around lips, eyes and irises, at the expense of more compute. It
|
||||||
|
enables applications like AR makeup and AR puppeteering.
|
||||||
|
|
||||||
|
The attention mesh model can be selected in the Solution APIs via the
|
||||||
|
[refine_landmarks](#refine_landmarks) option. You can also find more information
|
||||||
|
about the model in this [paper](https://arxiv.org/abs/2006.10962).
|
||||||
|
|
||||||
|
 |
|
||||||
|
:---------------------------------------------------------------------------: |
|
||||||
|
*Fig 3. Attention Mesh: Overview of model architecture.* |
|
||||||
|
|
||||||
## Face Geometry Module
|
## Face Geometry Module
|
||||||
|
|
||||||
The [Face Landmark Model](#face-landmark-model) performs a single-camera face landmark
|
The [Face Landmark Model](#face-landmark-model) performs a single-camera face landmark
|
||||||
@@ -145,8 +162,8 @@ be set freely, however for better results it is advised to set them as close to
|
|||||||
the *real physical camera parameters* as possible.
|
the *real physical camera parameters* as possible.
|
||||||
|
|
||||||
 |
|
 |
|
||||||
:----------------------------------------------------------------------------: |
|
:-------------------------------------------------------------------------------: |
|
||||||
*Fig 3. A visualization of multiple key elements in the Metric 3D space.* |
|
*Fig 4. A visualization of multiple key elements in the Metric 3D space.* |
|
||||||
|
|
||||||
#### Canonical Face Model
|
#### Canonical Face Model
|
||||||
|
|
||||||
@@ -210,7 +227,7 @@ The effect renderer is implemented as a MediaPipe
|
|||||||
|
|
||||||
|  |
|
|  |
|
||||||
| :---------------------------------------------------------------------: |
|
| :---------------------------------------------------------------------: |
|
||||||
| *Fig 4. An example of face effects rendered by the Face Geometry Effect Renderer.* |
|
| *Fig 5. An example of face effects rendered by the Face Geometry Effect Renderer.* |
|
||||||
|
|
||||||
## Solution APIs
|
## Solution APIs
|
||||||
|
|
||||||
@@ -234,6 +251,12 @@ unrelated, images. Default to `false`.
|
|||||||
|
|
||||||
Maximum number of faces to detect. Default to `1`.
|
Maximum number of faces to detect. Default to `1`.
|
||||||
|
|
||||||
|
#### refine_landmarks
|
||||||
|
|
||||||
|
Whether to further refine the landmark coordinates around the eyes and lips, and
|
||||||
|
output additional landmarks around the irises by applying the
|
||||||
|
[Attention Mesh Model](#attention-mesh-model). Default to `false`.
|
||||||
|
|
||||||
#### min_detection_confidence
|
#### min_detection_confidence
|
||||||
|
|
||||||
Minimum confidence value (`[0.0, 1.0]`) from the face detection model for the
|
Minimum confidence value (`[0.0, 1.0]`) from the face detection model for the
|
||||||
@@ -271,6 +294,7 @@ Supported configuration options:
|
|||||||
|
|
||||||
* [static_image_mode](#static_image_mode)
|
* [static_image_mode](#static_image_mode)
|
||||||
* [max_num_faces](#max_num_faces)
|
* [max_num_faces](#max_num_faces)
|
||||||
|
* [refine_landmarks](#refine_landmarks)
|
||||||
* [min_detection_confidence](#min_detection_confidence)
|
* [min_detection_confidence](#min_detection_confidence)
|
||||||
* [min_tracking_confidence](#min_tracking_confidence)
|
* [min_tracking_confidence](#min_tracking_confidence)
|
||||||
|
|
||||||
@@ -287,6 +311,7 @@ drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
|
|||||||
with mp_face_mesh.FaceMesh(
|
with mp_face_mesh.FaceMesh(
|
||||||
static_image_mode=True,
|
static_image_mode=True,
|
||||||
max_num_faces=1,
|
max_num_faces=1,
|
||||||
|
refine_landmarks=True,
|
||||||
min_detection_confidence=0.5) as face_mesh:
|
min_detection_confidence=0.5) as face_mesh:
|
||||||
for idx, file in enumerate(IMAGE_FILES):
|
for idx, file in enumerate(IMAGE_FILES):
|
||||||
image = cv2.imread(file)
|
image = cv2.imread(file)
|
||||||
@@ -313,12 +338,21 @@ with mp_face_mesh.FaceMesh(
|
|||||||
landmark_drawing_spec=None,
|
landmark_drawing_spec=None,
|
||||||
connection_drawing_spec=mp_drawing_styles
|
connection_drawing_spec=mp_drawing_styles
|
||||||
.get_default_face_mesh_contours_style())
|
.get_default_face_mesh_contours_style())
|
||||||
|
mp_drawing.draw_landmarks(
|
||||||
|
image=annotated_image,
|
||||||
|
landmark_list=face_landmarks,
|
||||||
|
connections=mp_face_mesh.FACEMESH_IRISES,
|
||||||
|
landmark_drawing_spec=None,
|
||||||
|
connection_drawing_spec=mp_drawing_styles
|
||||||
|
.get_default_face_mesh_iris_connections_style())
|
||||||
cv2.imwrite('/tmp/annotated_image' + str(idx) + '.png', annotated_image)
|
cv2.imwrite('/tmp/annotated_image' + str(idx) + '.png', annotated_image)
|
||||||
|
|
||||||
# For webcam input:
|
# For webcam input:
|
||||||
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
|
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
|
||||||
cap = cv2.VideoCapture(0)
|
cap = cv2.VideoCapture(0)
|
||||||
with mp_face_mesh.FaceMesh(
|
with mp_face_mesh.FaceMesh(
|
||||||
|
max_num_faces=1,
|
||||||
|
refine_landmarks=True,
|
||||||
min_detection_confidence=0.5,
|
min_detection_confidence=0.5,
|
||||||
min_tracking_confidence=0.5) as face_mesh:
|
min_tracking_confidence=0.5) as face_mesh:
|
||||||
while cap.isOpened():
|
while cap.isOpened():
|
||||||
@@ -328,12 +362,10 @@ with mp_face_mesh.FaceMesh(
|
|||||||
# If loading a video, use 'break' instead of 'continue'.
|
# If loading a video, use 'break' instead of 'continue'.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Flip the image horizontally for a later selfie-view display, and convert
|
|
||||||
# the BGR image to RGB.
|
|
||||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
|
||||||
# To improve performance, optionally mark the image as not writeable to
|
# To improve performance, optionally mark the image as not writeable to
|
||||||
# pass by reference.
|
# pass by reference.
|
||||||
image.flags.writeable = False
|
image.flags.writeable = False
|
||||||
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
results = face_mesh.process(image)
|
results = face_mesh.process(image)
|
||||||
|
|
||||||
# Draw the face mesh annotations on the image.
|
# Draw the face mesh annotations on the image.
|
||||||
@@ -355,7 +387,15 @@ with mp_face_mesh.FaceMesh(
|
|||||||
landmark_drawing_spec=None,
|
landmark_drawing_spec=None,
|
||||||
connection_drawing_spec=mp_drawing_styles
|
connection_drawing_spec=mp_drawing_styles
|
||||||
.get_default_face_mesh_contours_style())
|
.get_default_face_mesh_contours_style())
|
||||||
cv2.imshow('MediaPipe FaceMesh', image)
|
mp_drawing.draw_landmarks(
|
||||||
|
image=image,
|
||||||
|
landmark_list=face_landmarks,
|
||||||
|
connections=mp_face_mesh.FACEMESH_IRISES,
|
||||||
|
landmark_drawing_spec=None,
|
||||||
|
connection_drawing_spec=mp_drawing_styles
|
||||||
|
.get_default_face_mesh_iris_connections_style())
|
||||||
|
# Flip the image horizontally for a selfie-view display.
|
||||||
|
cv2.imshow('MediaPipe Face Mesh', cv2.flip(image, 1))
|
||||||
if cv2.waitKey(5) & 0xFF == 27:
|
if cv2.waitKey(5) & 0xFF == 27:
|
||||||
break
|
break
|
||||||
cap.release()
|
cap.release()
|
||||||
@@ -370,6 +410,7 @@ and the following usage example.
|
|||||||
Supported configuration options:
|
Supported configuration options:
|
||||||
|
|
||||||
* [maxNumFaces](#max_num_faces)
|
* [maxNumFaces](#max_num_faces)
|
||||||
|
* [refineLandmarks](#refine_landmarks)
|
||||||
* [minDetectionConfidence](#min_detection_confidence)
|
* [minDetectionConfidence](#min_detection_confidence)
|
||||||
* [minTrackingConfidence](#min_tracking_confidence)
|
* [minTrackingConfidence](#min_tracking_confidence)
|
||||||
|
|
||||||
@@ -410,8 +451,10 @@ function onResults(results) {
|
|||||||
{color: '#C0C0C070', lineWidth: 1});
|
{color: '#C0C0C070', lineWidth: 1});
|
||||||
drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYE, {color: '#FF3030'});
|
drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYE, {color: '#FF3030'});
|
||||||
drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYEBROW, {color: '#FF3030'});
|
drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYEBROW, {color: '#FF3030'});
|
||||||
|
drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_IRIS, {color: '#FF3030'});
|
||||||
drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYE, {color: '#30FF30'});
|
drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYE, {color: '#30FF30'});
|
||||||
drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYEBROW, {color: '#30FF30'});
|
drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYEBROW, {color: '#30FF30'});
|
||||||
|
drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_IRIS, {color: '#30FF30'});
|
||||||
drawConnectors(canvasCtx, landmarks, FACEMESH_FACE_OVAL, {color: '#E0E0E0'});
|
drawConnectors(canvasCtx, landmarks, FACEMESH_FACE_OVAL, {color: '#E0E0E0'});
|
||||||
drawConnectors(canvasCtx, landmarks, FACEMESH_LIPS, {color: '#E0E0E0'});
|
drawConnectors(canvasCtx, landmarks, FACEMESH_LIPS, {color: '#E0E0E0'});
|
||||||
}
|
}
|
||||||
@@ -424,6 +467,7 @@ const faceMesh = new FaceMesh({locateFile: (file) => {
|
|||||||
}});
|
}});
|
||||||
faceMesh.setOptions({
|
faceMesh.setOptions({
|
||||||
maxNumFaces: 1,
|
maxNumFaces: 1,
|
||||||
|
refineLandmarks: true,
|
||||||
minDetectionConfidence: 0.5,
|
minDetectionConfidence: 0.5,
|
||||||
minTrackingConfidence: 0.5
|
minTrackingConfidence: 0.5
|
||||||
});
|
});
|
||||||
@@ -443,18 +487,16 @@ camera.start();
|
|||||||
### Android Solution API
|
### Android Solution API
|
||||||
|
|
||||||
Please first follow general
|
Please first follow general
|
||||||
[instructions](../getting_started/android_solutions.md#integrate-mediapipe-android-solutions-api)
|
[instructions](../getting_started/android_solutions.md) to add MediaPipe Gradle
|
||||||
to add MediaPipe Gradle dependencies, then try the FaceMash solution API in the
|
dependencies and try the Android Solution API in the companion
|
||||||
companion
|
[example Android Studio project](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/solutions/facemesh),
|
||||||
[example Android Studio project](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/solutions/facemesh)
|
|
||||||
following
|
|
||||||
[these instructions](../getting_started/android_solutions.md#build-solution-example-apps-in-android-studio)
|
|
||||||
and learn more in the usage example below.
|
and learn more in the usage example below.
|
||||||
|
|
||||||
Supported configuration options:
|
Supported configuration options:
|
||||||
|
|
||||||
* [staticImageMode](#static_image_mode)
|
* [staticImageMode](#static_image_mode)
|
||||||
* [maxNumFaces](#max_num_faces)
|
* [maxNumFaces](#max_num_faces)
|
||||||
|
* [refineLandmarks](#refine_landmarks)
|
||||||
* runOnGpu: Run the pipeline and the model inference on GPU or CPU.
|
* runOnGpu: Run the pipeline and the model inference on GPU or CPU.
|
||||||
|
|
||||||
#### Camera Input
|
#### Camera Input
|
||||||
@@ -463,17 +505,18 @@ Supported configuration options:
|
|||||||
// For camera input and result rendering with OpenGL.
|
// For camera input and result rendering with OpenGL.
|
||||||
FaceMeshOptions faceMeshOptions =
|
FaceMeshOptions faceMeshOptions =
|
||||||
FaceMeshOptions.builder()
|
FaceMeshOptions.builder()
|
||||||
.setMode(FaceMeshOptions.STREAMING_MODE) // API soon to become
|
.setStaticImageMode(false)
|
||||||
.setMaxNumFaces(1) // setStaticImageMode(false)
|
.setRefineLandmarks(true)
|
||||||
|
.setMaxNumFaces(1)
|
||||||
.setRunOnGpu(true).build();
|
.setRunOnGpu(true).build();
|
||||||
FaceMesh facemesh = new FaceMesh(this, faceMeshOptions);
|
FaceMesh faceMesh = new FaceMesh(this, faceMeshOptions);
|
||||||
facemesh.setErrorListener(
|
faceMesh.setErrorListener(
|
||||||
(message, e) -> Log.e(TAG, "MediaPipe FaceMesh error:" + message));
|
(message, e) -> Log.e(TAG, "MediaPipe Face Mesh error:" + message));
|
||||||
|
|
||||||
// Initializes a new CameraInput instance and connects it to MediaPipe FaceMesh.
|
// Initializes a new CameraInput instance and connects it to MediaPipe Face Mesh Solution.
|
||||||
CameraInput cameraInput = new CameraInput(this);
|
CameraInput cameraInput = new CameraInput(this);
|
||||||
cameraInput.setNewFrameListener(
|
cameraInput.setNewFrameListener(
|
||||||
textureFrame -> facemesh.send(textureFrame));
|
textureFrame -> faceMesh.send(textureFrame));
|
||||||
|
|
||||||
// Initializes a new GlSurfaceView with a ResultGlRenderer<FaceMeshResult> instance
|
// Initializes a new GlSurfaceView with a ResultGlRenderer<FaceMeshResult> instance
|
||||||
// that provides the interfaces to run user-defined OpenGL rendering code.
|
// that provides the interfaces to run user-defined OpenGL rendering code.
|
||||||
@@ -481,18 +524,18 @@ cameraInput.setNewFrameListener(
|
|||||||
// as an example.
|
// as an example.
|
||||||
SolutionGlSurfaceView<FaceMeshResult> glSurfaceView =
|
SolutionGlSurfaceView<FaceMeshResult> glSurfaceView =
|
||||||
new SolutionGlSurfaceView<>(
|
new SolutionGlSurfaceView<>(
|
||||||
this, facemesh.getGlContext(), facemesh.getGlMajorVersion());
|
this, faceMesh.getGlContext(), faceMesh.getGlMajorVersion());
|
||||||
glSurfaceView.setSolutionResultRenderer(new FaceMeshResultGlRenderer());
|
glSurfaceView.setSolutionResultRenderer(new FaceMeshResultGlRenderer());
|
||||||
glSurfaceView.setRenderInputImage(true);
|
glSurfaceView.setRenderInputImage(true);
|
||||||
|
|
||||||
facemesh.setResultListener(
|
faceMesh.setResultListener(
|
||||||
faceMeshResult -> {
|
faceMeshResult -> {
|
||||||
NormalizedLandmark noseLandmark =
|
NormalizedLandmark noseLandmark =
|
||||||
result.multiFaceLandmarks().get(0).getLandmarkList().get(1);
|
result.multiFaceLandmarks().get(0).getLandmarkList().get(1);
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
"MediaPipe FaceMesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
"MediaPipe Face Mesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||||
noseLandmark.getX(), noseLandmark.getY()));
|
noseLandmark.getX(), noseLandmark.getY()));
|
||||||
// Request GL rendering.
|
// Request GL rendering.
|
||||||
glSurfaceView.setRenderData(faceMeshResult);
|
glSurfaceView.setRenderData(faceMeshResult);
|
||||||
@@ -504,7 +547,7 @@ glSurfaceView.post(
|
|||||||
() ->
|
() ->
|
||||||
cameraInput.start(
|
cameraInput.start(
|
||||||
this,
|
this,
|
||||||
facemesh.getGlContext(),
|
faceMesh.getGlContext(),
|
||||||
CameraInput.CameraFacing.FRONT,
|
CameraInput.CameraFacing.FRONT,
|
||||||
glSurfaceView.getWidth(),
|
glSurfaceView.getWidth(),
|
||||||
glSurfaceView.getHeight()));
|
glSurfaceView.getHeight()));
|
||||||
@@ -516,17 +559,18 @@ glSurfaceView.post(
|
|||||||
// For reading images from gallery and drawing the output in an ImageView.
|
// For reading images from gallery and drawing the output in an ImageView.
|
||||||
FaceMeshOptions faceMeshOptions =
|
FaceMeshOptions faceMeshOptions =
|
||||||
FaceMeshOptions.builder()
|
FaceMeshOptions.builder()
|
||||||
.setMode(FaceMeshOptions.STATIC_IMAGE_MODE) // API soon to become
|
.setStaticImageMode(true)
|
||||||
.setMaxNumFaces(1) // setStaticImageMode(true)
|
.setRefineLandmarks(true)
|
||||||
|
.setMaxNumFaces(1)
|
||||||
.setRunOnGpu(true).build();
|
.setRunOnGpu(true).build();
|
||||||
FaceMesh facemesh = new FaceMesh(this, faceMeshOptions);
|
FaceMesh faceMesh = new FaceMesh(this, faceMeshOptions);
|
||||||
|
|
||||||
// Connects MediaPipe FaceMesh to the user-defined ImageView instance that allows
|
// Connects MediaPipe Face Mesh Solution to the user-defined ImageView instance
|
||||||
// users to have the custom drawing of the output landmarks on it.
|
// that allows users to have the custom drawing of the output landmarks on it.
|
||||||
// See mediapipe/examples/android/solutions/facemesh/src/main/java/com/google/mediapipe/examples/facemesh/FaceMeshResultImageView.java
|
// See mediapipe/examples/android/solutions/facemesh/src/main/java/com/google/mediapipe/examples/facemesh/FaceMeshResultImageView.java
|
||||||
// as an example.
|
// as an example.
|
||||||
FaceMeshResultImageView imageView = new FaceMeshResultImageView(this);
|
FaceMeshResultImageView imageView = new FaceMeshResultImageView(this);
|
||||||
facemesh.setResultListener(
|
faceMesh.setResultListener(
|
||||||
faceMeshResult -> {
|
faceMeshResult -> {
|
||||||
int width = faceMeshResult.inputBitmap().getWidth();
|
int width = faceMeshResult.inputBitmap().getWidth();
|
||||||
int height = faceMeshResult.inputBitmap().getHeight();
|
int height = faceMeshResult.inputBitmap().getHeight();
|
||||||
@@ -535,14 +579,14 @@ facemesh.setResultListener(
|
|||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
"MediaPipe FaceMesh nose coordinates (pixel values): x=%f, y=%f",
|
"MediaPipe Face Mesh nose coordinates (pixel values): x=%f, y=%f",
|
||||||
noseLandmark.getX() * width, noseLandmark.getY() * height));
|
noseLandmark.getX() * width, noseLandmark.getY() * height));
|
||||||
// Request canvas drawing.
|
// Request canvas drawing.
|
||||||
imageView.setFaceMeshResult(faceMeshResult);
|
imageView.setFaceMeshResult(faceMeshResult);
|
||||||
runOnUiThread(() -> imageView.update());
|
runOnUiThread(() -> imageView.update());
|
||||||
});
|
});
|
||||||
facemesh.setErrorListener(
|
faceMesh.setErrorListener(
|
||||||
(message, e) -> Log.e(TAG, "MediaPipe FaceMesh error:" + message));
|
(message, e) -> Log.e(TAG, "MediaPipe Face Mesh error:" + message));
|
||||||
|
|
||||||
// ActivityResultLauncher to get an image from the gallery as Bitmap.
|
// ActivityResultLauncher to get an image from the gallery as Bitmap.
|
||||||
ActivityResultLauncher<Intent> imageGetter =
|
ActivityResultLauncher<Intent> imageGetter =
|
||||||
@@ -556,17 +600,18 @@ ActivityResultLauncher<Intent> imageGetter =
|
|||||||
bitmap =
|
bitmap =
|
||||||
MediaStore.Images.Media.getBitmap(
|
MediaStore.Images.Media.getBitmap(
|
||||||
this.getContentResolver(), resultIntent.getData());
|
this.getContentResolver(), resultIntent.getData());
|
||||||
|
// Please also rotate the Bitmap based on its orientation.
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Bitmap reading error:" + e);
|
Log.e(TAG, "Bitmap reading error:" + e);
|
||||||
}
|
}
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
facemesh.send(bitmap);
|
faceMesh.send(bitmap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Intent gallery = new Intent(
|
Intent pickImageIntent = new Intent(Intent.ACTION_PICK);
|
||||||
Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
|
pickImageIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||||
imageGetter.launch(gallery);
|
imageGetter.launch(pickImageIntent);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Video Input
|
#### Video Input
|
||||||
@@ -575,17 +620,18 @@ imageGetter.launch(gallery);
|
|||||||
// For video input and result rendering with OpenGL.
|
// For video input and result rendering with OpenGL.
|
||||||
FaceMeshOptions faceMeshOptions =
|
FaceMeshOptions faceMeshOptions =
|
||||||
FaceMeshOptions.builder()
|
FaceMeshOptions.builder()
|
||||||
.setMode(FaceMeshOptions.STREAMING_MODE) // API soon to become
|
.setStaticImageMode(false)
|
||||||
.setMaxNumFaces(1) // setStaticImageMode(false)
|
.setRefineLandmarks(true)
|
||||||
|
.setMaxNumFaces(1)
|
||||||
.setRunOnGpu(true).build();
|
.setRunOnGpu(true).build();
|
||||||
FaceMesh facemesh = new FaceMesh(this, faceMeshOptions);
|
FaceMesh faceMesh = new FaceMesh(this, faceMeshOptions);
|
||||||
facemesh.setErrorListener(
|
faceMesh.setErrorListener(
|
||||||
(message, e) -> Log.e(TAG, "MediaPipe FaceMesh error:" + message));
|
(message, e) -> Log.e(TAG, "MediaPipe Face Mesh error:" + message));
|
||||||
|
|
||||||
// Initializes a new VideoInput instance and connects it to MediaPipe FaceMesh.
|
// Initializes a new VideoInput instance and connects it to MediaPipe Face Mesh Solution.
|
||||||
VideoInput videoInput = new VideoInput(this);
|
VideoInput videoInput = new VideoInput(this);
|
||||||
videoInput.setNewFrameListener(
|
videoInput.setNewFrameListener(
|
||||||
textureFrame -> facemesh.send(textureFrame));
|
textureFrame -> faceMesh.send(textureFrame));
|
||||||
|
|
||||||
// Initializes a new GlSurfaceView with a ResultGlRenderer<FaceMeshResult> instance
|
// Initializes a new GlSurfaceView with a ResultGlRenderer<FaceMeshResult> instance
|
||||||
// that provides the interfaces to run user-defined OpenGL rendering code.
|
// that provides the interfaces to run user-defined OpenGL rendering code.
|
||||||
@@ -593,18 +639,18 @@ videoInput.setNewFrameListener(
|
|||||||
// as an example.
|
// as an example.
|
||||||
SolutionGlSurfaceView<FaceMeshResult> glSurfaceView =
|
SolutionGlSurfaceView<FaceMeshResult> glSurfaceView =
|
||||||
new SolutionGlSurfaceView<>(
|
new SolutionGlSurfaceView<>(
|
||||||
this, facemesh.getGlContext(), facemesh.getGlMajorVersion());
|
this, faceMesh.getGlContext(), faceMesh.getGlMajorVersion());
|
||||||
glSurfaceView.setSolutionResultRenderer(new FaceMeshResultGlRenderer());
|
glSurfaceView.setSolutionResultRenderer(new FaceMeshResultGlRenderer());
|
||||||
glSurfaceView.setRenderInputImage(true);
|
glSurfaceView.setRenderInputImage(true);
|
||||||
|
|
||||||
facemesh.setResultListener(
|
faceMesh.setResultListener(
|
||||||
faceMeshResult -> {
|
faceMeshResult -> {
|
||||||
NormalizedLandmark noseLandmark =
|
NormalizedLandmark noseLandmark =
|
||||||
result.multiFaceLandmarks().get(0).getLandmarkList().get(1);
|
result.multiFaceLandmarks().get(0).getLandmarkList().get(1);
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
"MediaPipe FaceMesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
"MediaPipe Face Mesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||||
noseLandmark.getX(), noseLandmark.getY()));
|
noseLandmark.getX(), noseLandmark.getY()));
|
||||||
// Request GL rendering.
|
// Request GL rendering.
|
||||||
glSurfaceView.setRenderData(faceMeshResult);
|
glSurfaceView.setRenderData(faceMeshResult);
|
||||||
@@ -623,15 +669,15 @@ ActivityResultLauncher<Intent> videoGetter =
|
|||||||
videoInput.start(
|
videoInput.start(
|
||||||
this,
|
this,
|
||||||
resultIntent.getData(),
|
resultIntent.getData(),
|
||||||
facemesh.getGlContext(),
|
faceMesh.getGlContext(),
|
||||||
glSurfaceView.getWidth(),
|
glSurfaceView.getWidth(),
|
||||||
glSurfaceView.getHeight()));
|
glSurfaceView.getHeight()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Intent gallery =
|
Intent pickVideoIntent = new Intent(Intent.ACTION_PICK);
|
||||||
new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI);
|
pickVideoIntent.setDataAndType(MediaStore.Video.Media.INTERNAL_CONTENT_URI, "video/*");
|
||||||
videoGetter.launch(gallery);
|
videoGetter.launch(pickVideoIntent);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example Apps
|
## Example Apps
|
||||||
|
|||||||
+66
-36
@@ -91,8 +91,10 @@ To detect initial hand locations, we designed a
|
|||||||
mobile real-time uses in a manner similar to the face detection model in
|
mobile real-time uses in a manner similar to the face detection model in
|
||||||
[MediaPipe Face Mesh](./face_mesh.md). Detecting hands is a decidedly complex
|
[MediaPipe Face Mesh](./face_mesh.md). Detecting hands is a decidedly complex
|
||||||
task: our
|
task: our
|
||||||
[model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/palm_detection/palm_detection.tflite)
|
[lite model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/palm_detection/palm_detection_lite.tflite)
|
||||||
has to work across a variety of hand sizes with a large scale span (~20x)
|
and
|
||||||
|
[full model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/palm_detection/palm_detection_full.tflite)
|
||||||
|
have to work across a variety of hand sizes with a large scale span (~20x)
|
||||||
relative to the image frame and be able to detect occluded and self-occluded
|
relative to the image frame and be able to detect occluded and self-occluded
|
||||||
hands. Whereas faces have high contrast patterns, e.g., in the eye and mouth
|
hands. Whereas faces have high contrast patterns, e.g., in the eye and mouth
|
||||||
region, the lack of such features in hands makes it comparatively difficult to
|
region, the lack of such features in hands makes it comparatively difficult to
|
||||||
@@ -120,7 +122,7 @@ just 86.22%.
|
|||||||
### Hand Landmark Model
|
### Hand Landmark Model
|
||||||
|
|
||||||
After the palm detection over the whole image our subsequent hand landmark
|
After the palm detection over the whole image our subsequent hand landmark
|
||||||
[model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark/hand_landmark.tflite)
|
[model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark/hand_landmark_full.tflite)
|
||||||
performs precise keypoint localization of 21 3D hand-knuckle coordinates inside
|
performs precise keypoint localization of 21 3D hand-knuckle coordinates inside
|
||||||
the detected hand regions via regression, that is direct coordinate prediction.
|
the detected hand regions via regression, that is direct coordinate prediction.
|
||||||
The model learns a consistent internal hand pose representation and is robust
|
The model learns a consistent internal hand pose representation and is robust
|
||||||
@@ -163,6 +165,11 @@ unrelated, images. Default to `false`.
|
|||||||
|
|
||||||
Maximum number of hands to detect. Default to `2`.
|
Maximum number of hands to detect. Default to `2`.
|
||||||
|
|
||||||
|
#### model_complexity
|
||||||
|
|
||||||
|
Complexity of the hand landmark model: `0` or `1`. Landmark accuracy as well as
|
||||||
|
inference latency generally go up with the model complexity. Default to `1`.
|
||||||
|
|
||||||
#### min_detection_confidence
|
#### min_detection_confidence
|
||||||
|
|
||||||
Minimum confidence value (`[0.0, 1.0]`) from the hand detection model for the
|
Minimum confidence value (`[0.0, 1.0]`) from the hand detection model for the
|
||||||
@@ -190,6 +197,13 @@ of 21 hand landmarks and each landmark is composed of `x`, `y` and `z`. `x` and
|
|||||||
and the smaller the value the closer the landmark is to the camera. The
|
and the smaller the value the closer the landmark is to the camera. The
|
||||||
magnitude of `z` uses roughly the same scale as `x`.
|
magnitude of `z` uses roughly the same scale as `x`.
|
||||||
|
|
||||||
|
#### multi_hand_world_landmarks
|
||||||
|
|
||||||
|
Collection of detected/tracked hands, where each hand is represented as a list
|
||||||
|
of 21 hand landmarks in world coordinates. Each landmark is composed of `x`, `y`
|
||||||
|
and `z`: real-world 3D coordinates in meters with the origin at the hand's
|
||||||
|
approximate geometric center.
|
||||||
|
|
||||||
#### multi_handedness
|
#### multi_handedness
|
||||||
|
|
||||||
Collection of handedness of the detected/tracked hands (i.e. is it a left or
|
Collection of handedness of the detected/tracked hands (i.e. is it a left or
|
||||||
@@ -212,6 +226,7 @@ Supported configuration options:
|
|||||||
|
|
||||||
* [static_image_mode](#static_image_mode)
|
* [static_image_mode](#static_image_mode)
|
||||||
* [max_num_hands](#max_num_hands)
|
* [max_num_hands](#max_num_hands)
|
||||||
|
* [model_complexity](#model_complexity)
|
||||||
* [min_detection_confidence](#min_detection_confidence)
|
* [min_detection_confidence](#min_detection_confidence)
|
||||||
* [min_tracking_confidence](#min_tracking_confidence)
|
* [min_tracking_confidence](#min_tracking_confidence)
|
||||||
|
|
||||||
@@ -256,10 +271,17 @@ with mp_hands.Hands(
|
|||||||
mp_drawing_styles.get_default_hand_connections_style())
|
mp_drawing_styles.get_default_hand_connections_style())
|
||||||
cv2.imwrite(
|
cv2.imwrite(
|
||||||
'/tmp/annotated_image' + str(idx) + '.png', cv2.flip(annotated_image, 1))
|
'/tmp/annotated_image' + str(idx) + '.png', cv2.flip(annotated_image, 1))
|
||||||
|
# Draw hand world landmarks.
|
||||||
|
if not results.multi_hand_world_landmarks:
|
||||||
|
continue
|
||||||
|
for hand_world_landmarks in results.multi_hand_world_landmarks:
|
||||||
|
mp_drawing.plot_landmarks(
|
||||||
|
hand_world_landmarks, mp_hands.HAND_CONNECTIONS, azimuth=5)
|
||||||
|
|
||||||
# For webcam input:
|
# For webcam input:
|
||||||
cap = cv2.VideoCapture(0)
|
cap = cv2.VideoCapture(0)
|
||||||
with mp_hands.Hands(
|
with mp_hands.Hands(
|
||||||
|
model_complexity=0,
|
||||||
min_detection_confidence=0.5,
|
min_detection_confidence=0.5,
|
||||||
min_tracking_confidence=0.5) as hands:
|
min_tracking_confidence=0.5) as hands:
|
||||||
while cap.isOpened():
|
while cap.isOpened():
|
||||||
@@ -269,12 +291,10 @@ with mp_hands.Hands(
|
|||||||
# If loading a video, use 'break' instead of 'continue'.
|
# If loading a video, use 'break' instead of 'continue'.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Flip the image horizontally for a later selfie-view display, and convert
|
|
||||||
# the BGR image to RGB.
|
|
||||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
|
||||||
# To improve performance, optionally mark the image as not writeable to
|
# To improve performance, optionally mark the image as not writeable to
|
||||||
# pass by reference.
|
# pass by reference.
|
||||||
image.flags.writeable = False
|
image.flags.writeable = False
|
||||||
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
results = hands.process(image)
|
results = hands.process(image)
|
||||||
|
|
||||||
# Draw the hand annotations on the image.
|
# Draw the hand annotations on the image.
|
||||||
@@ -288,7 +308,8 @@ with mp_hands.Hands(
|
|||||||
mp_hands.HAND_CONNECTIONS,
|
mp_hands.HAND_CONNECTIONS,
|
||||||
mp_drawing_styles.get_default_hand_landmarks_style(),
|
mp_drawing_styles.get_default_hand_landmarks_style(),
|
||||||
mp_drawing_styles.get_default_hand_connections_style())
|
mp_drawing_styles.get_default_hand_connections_style())
|
||||||
cv2.imshow('MediaPipe Hands', image)
|
# Flip the image horizontally for a selfie-view display.
|
||||||
|
cv2.imshow('MediaPipe Hands', cv2.flip(image, 1))
|
||||||
if cv2.waitKey(5) & 0xFF == 27:
|
if cv2.waitKey(5) & 0xFF == 27:
|
||||||
break
|
break
|
||||||
cap.release()
|
cap.release()
|
||||||
@@ -303,6 +324,7 @@ and a [fun application], and the following usage example.
|
|||||||
Supported configuration options:
|
Supported configuration options:
|
||||||
|
|
||||||
* [maxNumHands](#max_num_hands)
|
* [maxNumHands](#max_num_hands)
|
||||||
|
* [modelComplexity](#model_complexity)
|
||||||
* [minDetectionConfidence](#min_detection_confidence)
|
* [minDetectionConfidence](#min_detection_confidence)
|
||||||
* [minTrackingConfidence](#min_tracking_confidence)
|
* [minTrackingConfidence](#min_tracking_confidence)
|
||||||
|
|
||||||
@@ -352,6 +374,7 @@ const hands = new Hands({locateFile: (file) => {
|
|||||||
}});
|
}});
|
||||||
hands.setOptions({
|
hands.setOptions({
|
||||||
maxNumHands: 2,
|
maxNumHands: 2,
|
||||||
|
modelComplexity: 1,
|
||||||
minDetectionConfidence: 0.5,
|
minDetectionConfidence: 0.5,
|
||||||
minTrackingConfidence: 0.5
|
minTrackingConfidence: 0.5
|
||||||
});
|
});
|
||||||
@@ -371,13 +394,10 @@ camera.start();
|
|||||||
### Android Solution API
|
### Android Solution API
|
||||||
|
|
||||||
Please first follow general
|
Please first follow general
|
||||||
[instructions](../getting_started/android_solutions.md#integrate-mediapipe-android-solutions-api)
|
[instructions](../getting_started/android_solutions.md) to add MediaPipe Gradle
|
||||||
to add MediaPipe Gradle dependencies, then try the Hands solution API in the
|
dependencies and try the Android Solution API in the companion
|
||||||
companion
|
[example Android Studio project](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/solutions/hands),
|
||||||
[example Android Studio project](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/solutions/hands)
|
and learn more in the usage example below.
|
||||||
following
|
|
||||||
[these instructions](../getting_started/android_solutions.md#build-solution-example-apps-in-android-studio)
|
|
||||||
and learn more in usage example below.
|
|
||||||
|
|
||||||
Supported configuration options:
|
Supported configuration options:
|
||||||
|
|
||||||
@@ -391,14 +411,14 @@ Supported configuration options:
|
|||||||
// For camera input and result rendering with OpenGL.
|
// For camera input and result rendering with OpenGL.
|
||||||
HandsOptions handsOptions =
|
HandsOptions handsOptions =
|
||||||
HandsOptions.builder()
|
HandsOptions.builder()
|
||||||
.setMode(HandsOptions.STREAMING_MODE) // API soon to become
|
.setStaticImageMode(false)
|
||||||
.setMaxNumHands(1) // setStaticImageMode(false)
|
.setMaxNumHands(2)
|
||||||
.setRunOnGpu(true).build();
|
.setRunOnGpu(true).build();
|
||||||
Hands hands = new Hands(this, handsOptions);
|
Hands hands = new Hands(this, handsOptions);
|
||||||
hands.setErrorListener(
|
hands.setErrorListener(
|
||||||
(message, e) -> Log.e(TAG, "MediaPipe Hands error:" + message));
|
(message, e) -> Log.e(TAG, "MediaPipe Hands error:" + message));
|
||||||
|
|
||||||
// Initializes a new CameraInput instance and connects it to MediaPipe Hands.
|
// Initializes a new CameraInput instance and connects it to MediaPipe Hands Solution.
|
||||||
CameraInput cameraInput = new CameraInput(this);
|
CameraInput cameraInput = new CameraInput(this);
|
||||||
cameraInput.setNewFrameListener(
|
cameraInput.setNewFrameListener(
|
||||||
textureFrame -> hands.send(textureFrame));
|
textureFrame -> hands.send(textureFrame));
|
||||||
@@ -415,8 +435,11 @@ glSurfaceView.setRenderInputImage(true);
|
|||||||
|
|
||||||
hands.setResultListener(
|
hands.setResultListener(
|
||||||
handsResult -> {
|
handsResult -> {
|
||||||
NormalizedLandmark wristLandmark = Hands.getHandLandmark(
|
if (result.multiHandLandmarks().isEmpty()) {
|
||||||
handsResult, 0, HandLandmark.WRIST);
|
return;
|
||||||
|
}
|
||||||
|
NormalizedLandmark wristLandmark =
|
||||||
|
handsResult.multiHandLandmarks().get(0).getLandmarkList().get(HandLandmark.WRIST);
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
@@ -444,22 +467,25 @@ glSurfaceView.post(
|
|||||||
// For reading images from gallery and drawing the output in an ImageView.
|
// For reading images from gallery and drawing the output in an ImageView.
|
||||||
HandsOptions handsOptions =
|
HandsOptions handsOptions =
|
||||||
HandsOptions.builder()
|
HandsOptions.builder()
|
||||||
.setMode(HandsOptions.STATIC_IMAGE_MODE) // API soon to become
|
.setStaticImageMode(true)
|
||||||
.setMaxNumHands(1) // setStaticImageMode(true)
|
.setMaxNumHands(2)
|
||||||
.setRunOnGpu(true).build();
|
.setRunOnGpu(true).build();
|
||||||
Hands hands = new Hands(this, handsOptions);
|
Hands hands = new Hands(this, handsOptions);
|
||||||
|
|
||||||
// Connects MediaPipe Hands to the user-defined ImageView instance that allows
|
// Connects MediaPipe Hands Solution to the user-defined ImageView instance that
|
||||||
// users to have the custom drawing of the output landmarks on it.
|
// allows users to have the custom drawing of the output landmarks on it.
|
||||||
// See mediapipe/examples/android/solutions/hands/src/main/java/com/google/mediapipe/examples/hands/HandsResultImageView.java
|
// See mediapipe/examples/android/solutions/hands/src/main/java/com/google/mediapipe/examples/hands/HandsResultImageView.java
|
||||||
// as an example.
|
// as an example.
|
||||||
HandsResultImageView imageView = new HandsResultImageView(this);
|
HandsResultImageView imageView = new HandsResultImageView(this);
|
||||||
hands.setResultListener(
|
hands.setResultListener(
|
||||||
handsResult -> {
|
handsResult -> {
|
||||||
|
if (result.multiHandLandmarks().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
int width = handsResult.inputBitmap().getWidth();
|
int width = handsResult.inputBitmap().getWidth();
|
||||||
int height = handsResult.inputBitmap().getHeight();
|
int height = handsResult.inputBitmap().getHeight();
|
||||||
NormalizedLandmark wristLandmark = Hands.getHandLandmark(
|
NormalizedLandmark wristLandmark =
|
||||||
handsResult, 0, HandLandmark.WRIST);
|
handsResult.multiHandLandmarks().get(0).getLandmarkList().get(HandLandmark.WRIST);
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
@@ -484,6 +510,7 @@ ActivityResultLauncher<Intent> imageGetter =
|
|||||||
bitmap =
|
bitmap =
|
||||||
MediaStore.Images.Media.getBitmap(
|
MediaStore.Images.Media.getBitmap(
|
||||||
this.getContentResolver(), resultIntent.getData());
|
this.getContentResolver(), resultIntent.getData());
|
||||||
|
// Please also rotate the Bitmap based on its orientation.
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Bitmap reading error:" + e);
|
Log.e(TAG, "Bitmap reading error:" + e);
|
||||||
}
|
}
|
||||||
@@ -492,9 +519,9 @@ ActivityResultLauncher<Intent> imageGetter =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Intent gallery = new Intent(
|
Intent pickImageIntent = new Intent(Intent.ACTION_PICK);
|
||||||
Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
|
pickImageIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||||
imageGetter.launch(gallery);
|
imageGetter.launch(pickImageIntent);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Video Input
|
#### Video Input
|
||||||
@@ -503,14 +530,14 @@ imageGetter.launch(gallery);
|
|||||||
// For video input and result rendering with OpenGL.
|
// For video input and result rendering with OpenGL.
|
||||||
HandsOptions handsOptions =
|
HandsOptions handsOptions =
|
||||||
HandsOptions.builder()
|
HandsOptions.builder()
|
||||||
.setMode(HandsOptions.STREAMING_MODE) // API soon to become
|
.setStaticImageMode(false)
|
||||||
.setMaxNumHands(1) // setStaticImageMode(false)
|
.setMaxNumHands(2)
|
||||||
.setRunOnGpu(true).build();
|
.setRunOnGpu(true).build();
|
||||||
Hands hands = new Hands(this, handsOptions);
|
Hands hands = new Hands(this, handsOptions);
|
||||||
hands.setErrorListener(
|
hands.setErrorListener(
|
||||||
(message, e) -> Log.e(TAG, "MediaPipe Hands error:" + message));
|
(message, e) -> Log.e(TAG, "MediaPipe Hands error:" + message));
|
||||||
|
|
||||||
// Initializes a new VideoInput instance and connects it to MediaPipe Hands.
|
// Initializes a new VideoInput instance and connects it to MediaPipe Hands Solution.
|
||||||
VideoInput videoInput = new VideoInput(this);
|
VideoInput videoInput = new VideoInput(this);
|
||||||
videoInput.setNewFrameListener(
|
videoInput.setNewFrameListener(
|
||||||
textureFrame -> hands.send(textureFrame));
|
textureFrame -> hands.send(textureFrame));
|
||||||
@@ -527,8 +554,11 @@ glSurfaceView.setRenderInputImage(true);
|
|||||||
|
|
||||||
hands.setResultListener(
|
hands.setResultListener(
|
||||||
handsResult -> {
|
handsResult -> {
|
||||||
NormalizedLandmark wristLandmark = Hands.getHandLandmark(
|
if (result.multiHandLandmarks().isEmpty()) {
|
||||||
handsResult, 0, HandLandmark.WRIST);
|
return;
|
||||||
|
}
|
||||||
|
NormalizedLandmark wristLandmark =
|
||||||
|
handsResult.multiHandLandmarks().get(0).getLandmarkList().get(HandLandmark.WRIST);
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
@@ -557,9 +587,9 @@ ActivityResultLauncher<Intent> videoGetter =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Intent gallery =
|
Intent pickVideoIntent = new Intent(Intent.ACTION_PICK);
|
||||||
new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI);
|
pickVideoIntent.setDataAndType(MediaStore.Video.Media.INTERNAL_CONTENT_URI, "video/*");
|
||||||
videoGetter.launch(gallery);
|
videoGetter.launch(pickVideoIntent);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example Apps
|
## Example Apps
|
||||||
|
|||||||
@@ -147,6 +147,23 @@ If set to `true`, the solution filters pose landmarks across different input
|
|||||||
images to reduce jitter, but ignored if [static_image_mode](#static_image_mode)
|
images to reduce jitter, but ignored if [static_image_mode](#static_image_mode)
|
||||||
is also set to `true`. Default to `true`.
|
is also set to `true`. Default to `true`.
|
||||||
|
|
||||||
|
#### enable_segmentation
|
||||||
|
|
||||||
|
If set to `true`, in addition to the pose, face and hand landmarks the solution
|
||||||
|
also generates the segmentation mask. Default to `false`.
|
||||||
|
|
||||||
|
#### smooth_segmentation
|
||||||
|
|
||||||
|
If set to `true`, the solution filters segmentation masks across different input
|
||||||
|
images to reduce jitter. Ignored if [enable_segmentation](#enable_segmentation)
|
||||||
|
is `false` or [static_image_mode](#static_image_mode) is `true`. Default to
|
||||||
|
`true`.
|
||||||
|
|
||||||
|
#### refine_face_landmarks
|
||||||
|
|
||||||
|
Whether to further refine the landmark coordinates around the eyes and lips, and
|
||||||
|
output additional landmarks around the irises. Default to `false`.
|
||||||
|
|
||||||
#### min_detection_confidence
|
#### min_detection_confidence
|
||||||
|
|
||||||
Minimum confidence value (`[0.0, 1.0]`) from the person-detection model for the
|
Minimum confidence value (`[0.0, 1.0]`) from the person-detection model for the
|
||||||
@@ -207,6 +224,15 @@ the camera. The magnitude of `z` uses roughly the same scale as `x`.
|
|||||||
A list of 21 hand landmarks on the right hand, in the same representation as
|
A list of 21 hand landmarks on the right hand, in the same representation as
|
||||||
[left_hand_landmarks](#left_hand_landmarks).
|
[left_hand_landmarks](#left_hand_landmarks).
|
||||||
|
|
||||||
|
#### segmentation_mask
|
||||||
|
|
||||||
|
The output segmentation mask, predicted only when
|
||||||
|
[enable_segmentation](#enable_segmentation) is set to `true`. The mask has the
|
||||||
|
same width and height as the input image, and contains values in `[0.0, 1.0]`
|
||||||
|
where `1.0` and `0.0` indicate high certainty of a "human" and "background"
|
||||||
|
pixel respectively. Please refer to the platform-specific usage examples below
|
||||||
|
for usage details.
|
||||||
|
|
||||||
### Python Solution API
|
### Python Solution API
|
||||||
|
|
||||||
Please first follow general [instructions](../getting_started/python.md) to
|
Please first follow general [instructions](../getting_started/python.md) to
|
||||||
@@ -218,6 +244,9 @@ Supported configuration options:
|
|||||||
* [static_image_mode](#static_image_mode)
|
* [static_image_mode](#static_image_mode)
|
||||||
* [model_complexity](#model_complexity)
|
* [model_complexity](#model_complexity)
|
||||||
* [smooth_landmarks](#smooth_landmarks)
|
* [smooth_landmarks](#smooth_landmarks)
|
||||||
|
* [enable_segmentation](#enable_segmentation)
|
||||||
|
* [smooth_segmentation](#smooth_segmentation)
|
||||||
|
* [refine_face_landmarks](#refine_face_landmarks)
|
||||||
* [min_detection_confidence](#min_detection_confidence)
|
* [min_detection_confidence](#min_detection_confidence)
|
||||||
* [min_tracking_confidence](#min_tracking_confidence)
|
* [min_tracking_confidence](#min_tracking_confidence)
|
||||||
|
|
||||||
@@ -232,7 +261,9 @@ mp_holistic = mp.solutions.holistic
|
|||||||
IMAGE_FILES = []
|
IMAGE_FILES = []
|
||||||
with mp_holistic.Holistic(
|
with mp_holistic.Holistic(
|
||||||
static_image_mode=True,
|
static_image_mode=True,
|
||||||
model_complexity=2) as holistic:
|
model_complexity=2,
|
||||||
|
enable_segmentation=True,
|
||||||
|
refine_face_landmarks=True) as holistic:
|
||||||
for idx, file in enumerate(IMAGE_FILES):
|
for idx, file in enumerate(IMAGE_FILES):
|
||||||
image = cv2.imread(file)
|
image = cv2.imread(file)
|
||||||
image_height, image_width, _ = image.shape
|
image_height, image_width, _ = image.shape
|
||||||
@@ -245,8 +276,16 @@ with mp_holistic.Holistic(
|
|||||||
f'{results.pose_landmarks.landmark[mp_holistic.PoseLandmark.NOSE].x * image_width}, '
|
f'{results.pose_landmarks.landmark[mp_holistic.PoseLandmark.NOSE].x * image_width}, '
|
||||||
f'{results.pose_landmarks.landmark[mp_holistic.PoseLandmark.NOSE].y * image_height})'
|
f'{results.pose_landmarks.landmark[mp_holistic.PoseLandmark.NOSE].y * image_height})'
|
||||||
)
|
)
|
||||||
# Draw pose, left and right hands, and face landmarks on the image.
|
|
||||||
annotated_image = image.copy()
|
annotated_image = image.copy()
|
||||||
|
# Draw segmentation on the image.
|
||||||
|
# To improve segmentation around boundaries, consider applying a joint
|
||||||
|
# bilateral filter to "results.segmentation_mask" with "image".
|
||||||
|
condition = np.stack((results.segmentation_mask,) * 3, axis=-1) > 0.1
|
||||||
|
bg_image = np.zeros(image.shape, dtype=np.uint8)
|
||||||
|
bg_image[:] = BG_COLOR
|
||||||
|
annotated_image = np.where(condition, annotated_image, bg_image)
|
||||||
|
# Draw pose, left and right hands, and face landmarks on the image.
|
||||||
mp_drawing.draw_landmarks(
|
mp_drawing.draw_landmarks(
|
||||||
annotated_image,
|
annotated_image,
|
||||||
results.face_landmarks,
|
results.face_landmarks,
|
||||||
@@ -277,12 +316,10 @@ with mp_holistic.Holistic(
|
|||||||
# If loading a video, use 'break' instead of 'continue'.
|
# If loading a video, use 'break' instead of 'continue'.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Flip the image horizontally for a later selfie-view display, and convert
|
|
||||||
# the BGR image to RGB.
|
|
||||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
|
||||||
# To improve performance, optionally mark the image as not writeable to
|
# To improve performance, optionally mark the image as not writeable to
|
||||||
# pass by reference.
|
# pass by reference.
|
||||||
image.flags.writeable = False
|
image.flags.writeable = False
|
||||||
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
results = holistic.process(image)
|
results = holistic.process(image)
|
||||||
|
|
||||||
# Draw landmark annotation on the image.
|
# Draw landmark annotation on the image.
|
||||||
@@ -301,7 +338,8 @@ with mp_holistic.Holistic(
|
|||||||
mp_holistic.POSE_CONNECTIONS,
|
mp_holistic.POSE_CONNECTIONS,
|
||||||
landmark_drawing_spec=mp_drawing_styles
|
landmark_drawing_spec=mp_drawing_styles
|
||||||
.get_default_pose_landmarks_style())
|
.get_default_pose_landmarks_style())
|
||||||
cv2.imshow('MediaPipe Holistic', image)
|
# Flip the image horizontally for a selfie-view display.
|
||||||
|
cv2.imshow('MediaPipe Holistic', cv2.flip(image, 1))
|
||||||
if cv2.waitKey(5) & 0xFF == 27:
|
if cv2.waitKey(5) & 0xFF == 27:
|
||||||
break
|
break
|
||||||
cap.release()
|
cap.release()
|
||||||
@@ -317,6 +355,9 @@ Supported configuration options:
|
|||||||
|
|
||||||
* [modelComplexity](#model_complexity)
|
* [modelComplexity](#model_complexity)
|
||||||
* [smoothLandmarks](#smooth_landmarks)
|
* [smoothLandmarks](#smooth_landmarks)
|
||||||
|
* [enableSegmentation](#enable_segmentation)
|
||||||
|
* [smoothSegmentation](#smooth_segmentation)
|
||||||
|
* [refineFaceLandmarks](#refineFaceLandmarks)
|
||||||
* [minDetectionConfidence](#min_detection_confidence)
|
* [minDetectionConfidence](#min_detection_confidence)
|
||||||
* [minTrackingConfidence](#min_tracking_confidence)
|
* [minTrackingConfidence](#min_tracking_confidence)
|
||||||
|
|
||||||
@@ -349,8 +390,20 @@ const canvasCtx = canvasElement.getContext('2d');
|
|||||||
function onResults(results) {
|
function onResults(results) {
|
||||||
canvasCtx.save();
|
canvasCtx.save();
|
||||||
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
||||||
|
canvasCtx.drawImage(results.segmentationMask, 0, 0,
|
||||||
|
canvasElement.width, canvasElement.height);
|
||||||
|
|
||||||
|
// Only overwrite existing pixels.
|
||||||
|
canvasCtx.globalCompositeOperation = 'source-in';
|
||||||
|
canvasCtx.fillStyle = '#00FF00';
|
||||||
|
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
|
||||||
|
|
||||||
|
// Only overwrite missing pixels.
|
||||||
|
canvasCtx.globalCompositeOperation = 'destination-atop';
|
||||||
canvasCtx.drawImage(
|
canvasCtx.drawImage(
|
||||||
results.image, 0, 0, canvasElement.width, canvasElement.height);
|
results.image, 0, 0, canvasElement.width, canvasElement.height);
|
||||||
|
|
||||||
|
canvasCtx.globalCompositeOperation = 'source-over';
|
||||||
drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS,
|
drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS,
|
||||||
{color: '#00FF00', lineWidth: 4});
|
{color: '#00FF00', lineWidth: 4});
|
||||||
drawLandmarks(canvasCtx, results.poseLandmarks,
|
drawLandmarks(canvasCtx, results.poseLandmarks,
|
||||||
@@ -374,6 +427,9 @@ const holistic = new Holistic({locateFile: (file) => {
|
|||||||
holistic.setOptions({
|
holistic.setOptions({
|
||||||
modelComplexity: 1,
|
modelComplexity: 1,
|
||||||
smoothLandmarks: true,
|
smoothLandmarks: true,
|
||||||
|
enableSegmentation: true,
|
||||||
|
smoothSegmentation: true,
|
||||||
|
refineFaceLandmarks: true,
|
||||||
minDetectionConfidence: 0.5,
|
minDetectionConfidence: 0.5,
|
||||||
minTrackingConfidence: 0.5
|
minTrackingConfidence: 0.5
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ one over the other.
|
|||||||
* Face landmark model:
|
* Face landmark model:
|
||||||
[TFLite model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/face_landmark/face_landmark.tflite),
|
[TFLite model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/face_landmark/face_landmark.tflite),
|
||||||
[TF.js model](https://tfhub.dev/mediapipe/facemesh/1)
|
[TF.js model](https://tfhub.dev/mediapipe/facemesh/1)
|
||||||
* [Model card](https://mediapipe.page.link/facemesh-mc)
|
* Face landmark model w/ attention (aka Attention Mesh):
|
||||||
|
[TFLite model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/face_landmark/face_landmark_with_attention.tflite)
|
||||||
|
* [Model card](https://mediapipe.page.link/facemesh-mc),
|
||||||
|
[Model card (w/ attention)](https://mediapipe.page.link/attentionmesh-mc)
|
||||||
|
|
||||||
### [Iris](https://google.github.io/mediapipe/solutions/iris)
|
### [Iris](https://google.github.io/mediapipe/solutions/iris)
|
||||||
|
|
||||||
@@ -52,13 +55,14 @@ one over the other.
|
|||||||
### [Hands](https://google.github.io/mediapipe/solutions/hands)
|
### [Hands](https://google.github.io/mediapipe/solutions/hands)
|
||||||
|
|
||||||
* Palm detection model:
|
* Palm detection model:
|
||||||
[TFLite model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/palm_detection/palm_detection.tflite),
|
[TFLite model (lite)](https://github.com/google/mediapipe/tree/master/mediapipe/modules/palm_detection/palm_detection_lite.tflite),
|
||||||
|
[TFLite model (full)](https://github.com/google/mediapipe/tree/master/mediapipe/modules/palm_detection/palm_detection_full.tflite),
|
||||||
[TF.js model](https://tfhub.dev/mediapipe/handdetector/1)
|
[TF.js model](https://tfhub.dev/mediapipe/handdetector/1)
|
||||||
* Hand landmark model:
|
* Hand landmark model:
|
||||||
[TFLite model](https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark/hand_landmark.tflite),
|
[TFLite model (lite)](https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark/hand_landmark_lite.tflite),
|
||||||
[TFLite model (sparse)](https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark/hand_landmark_sparse.tflite),
|
[TFLite model (full)](https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark/hand_landmark_full.tflite),
|
||||||
[TF.js model](https://tfhub.dev/mediapipe/handskeleton/1)
|
[TF.js model](https://tfhub.dev/mediapipe/handskeleton/1)
|
||||||
* [Model card](https://mediapipe.page.link/handmc), [Model card (sparse)](https://mediapipe.page.link/handmc-sparse)
|
* [Model card](https://mediapipe.page.link/handmc)
|
||||||
|
|
||||||
### [Pose](https://google.github.io/mediapipe/solutions/pose)
|
### [Pose](https://google.github.io/mediapipe/solutions/pose)
|
||||||
|
|
||||||
|
|||||||
@@ -338,11 +338,10 @@ with mp_objectron.Objectron(static_image_mode=False,
|
|||||||
# If loading a video, use 'break' instead of 'continue'.
|
# If loading a video, use 'break' instead of 'continue'.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Convert the BGR image to RGB.
|
|
||||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
||||||
# To improve performance, optionally mark the image as not writeable to
|
# To improve performance, optionally mark the image as not writeable to
|
||||||
# pass by reference.
|
# pass by reference.
|
||||||
image.flags.writeable = False
|
image.flags.writeable = False
|
||||||
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
results = objectron.process(image)
|
results = objectron.process(image)
|
||||||
|
|
||||||
# Draw the box landmarks on the image.
|
# Draw the box landmarks on the image.
|
||||||
@@ -354,7 +353,8 @@ with mp_objectron.Objectron(static_image_mode=False,
|
|||||||
image, detected_object.landmarks_2d, mp_objectron.BOX_CONNECTIONS)
|
image, detected_object.landmarks_2d, mp_objectron.BOX_CONNECTIONS)
|
||||||
mp_drawing.draw_axis(image, detected_object.rotation,
|
mp_drawing.draw_axis(image, detected_object.rotation,
|
||||||
detected_object.translation)
|
detected_object.translation)
|
||||||
cv2.imshow('MediaPipe Objectron', image)
|
# Flip the image horizontally for a selfie-view display.
|
||||||
|
cv2.imshow('MediaPipe Objectron', cv2.flip(image, 1))
|
||||||
if cv2.waitKey(5) & 0xFF == 27:
|
if cv2.waitKey(5) & 0xFF == 27:
|
||||||
break
|
break
|
||||||
cap.release()
|
cap.release()
|
||||||
@@ -529,7 +529,7 @@ Example app bounding boxes are rendered with [GlAnimationOverlayCalculator](http
|
|||||||
> ```
|
> ```
|
||||||
> and then run
|
> and then run
|
||||||
>
|
>
|
||||||
> ```build
|
> ```bash
|
||||||
> bazel run -c opt mediapipe/graphs/object_detection_3d/obj_parser:ObjParser -- input_dir=[INTERMEDIATE_OUTPUT_DIR] output_dir=[OUTPUT_DIR]
|
> bazel run -c opt mediapipe/graphs/object_detection_3d/obj_parser:ObjParser -- input_dir=[INTERMEDIATE_OUTPUT_DIR] output_dir=[OUTPUT_DIR]
|
||||||
> ```
|
> ```
|
||||||
> INPUT_DIR should be the folder with initial asset .obj files to be processed,
|
> INPUT_DIR should be the folder with initial asset .obj files to be processed,
|
||||||
|
|||||||
+20
-19
@@ -88,11 +88,11 @@ from [COCO topology](https://cocodataset.org/#keypoints-2020).
|
|||||||
|
|
||||||
Method | Yoga <br/> [`mAP`] | Yoga <br/> [`[email protected]`] | Dance <br/> [`mAP`] | Dance <br/> [`[email protected]`] | HIIT <br/> [`mAP`] | HIIT <br/> [`[email protected]`]
|
Method | Yoga <br/> [`mAP`] | Yoga <br/> [`[email protected]`] | Dance <br/> [`mAP`] | Dance <br/> [`[email protected]`] | HIIT <br/> [`mAP`] | HIIT <br/> [`[email protected]`]
|
||||||
----------------------------------------------------------------------------------------------------- | -----------------: | ---------------------: | ------------------: | ----------------------: | -----------------: | ---------------------:
|
----------------------------------------------------------------------------------------------------- | -----------------: | ---------------------: | ------------------: | ----------------------: | -----------------: | ---------------------:
|
||||||
BlazePose.Heavy | 68.1 | **96.4** | 73.0 | **97.2** | 74.0 | **97.5**
|
BlazePose GHUM Heavy | 68.1 | **96.4** | 73.0 | **97.2** | 74.0 | **97.5**
|
||||||
BlazePose.Full | 62.6 | **95.5** | 67.4 | **96.3** | 68.0 | **95.7**
|
BlazePose GHUM Full | 62.6 | **95.5** | 67.4 | **96.3** | 68.0 | **95.7**
|
||||||
BlazePose.Lite | 45.0 | **90.2** | 53.6 | **92.5** | 53.8 | **93.5**
|
BlazePose GHUM Lite | 45.0 | **90.2** | 53.6 | **92.5** | 53.8 | **93.5**
|
||||||
[AlphaPose.ResNet50](https://github.com/MVIG-SJTU/AlphaPose) | 63.4 | **96.0** | 57.8 | **95.5** | 63.4 | **96.0**
|
[AlphaPose ResNet50](https://github.com/MVIG-SJTU/AlphaPose) | 63.4 | **96.0** | 57.8 | **95.5** | 63.4 | **96.0**
|
||||||
[Apple.Vision](https://developer.apple.com/documentation/vision/detecting_human_body_poses_in_images) | 32.8 | **82.7** | 36.4 | **91.4** | 44.5 | **88.6**
|
[Apple Vision](https://developer.apple.com/documentation/vision/detecting_human_body_poses_in_images) | 32.8 | **82.7** | 36.4 | **91.4** | 44.5 | **88.6**
|
||||||
|
|
||||||
 |
|
 |
|
||||||
:--------------------------------------------------------------------------: |
|
:--------------------------------------------------------------------------: |
|
||||||
@@ -101,11 +101,11 @@ BlazePose.Lite
|
|||||||
We designed our models specifically for live perception use cases, so all of
|
We designed our models specifically for live perception use cases, so all of
|
||||||
them work in real-time on the majority of modern devices.
|
them work in real-time on the majority of modern devices.
|
||||||
|
|
||||||
Method | Latency <br/> Pixel 3 [TFLite GPU](https://www.tensorflow.org/lite/performance/gpu_advanced) | Latency <br/> MacBook Pro (15-inch 2017)
|
Method | Latency <br/> Pixel 3 [TFLite GPU](https://www.tensorflow.org/lite/performance/gpu_advanced) | Latency <br/> MacBook Pro (15-inch 2017)
|
||||||
--------------- | -------------------------------------------------------------------------------------------: | ---------------------------------------:
|
-------------------- | -------------------------------------------------------------------------------------------: | ---------------------------------------:
|
||||||
BlazePose.Heavy | 53 ms | 38 ms
|
BlazePose GHUM Heavy | 53 ms | 38 ms
|
||||||
BlazePose.Full | 25 ms | 27 ms
|
BlazePose GHUM Full | 25 ms | 27 ms
|
||||||
BlazePose.Lite | 20 ms | 25 ms
|
BlazePose GHUM Lite | 20 ms | 25 ms
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ hip midpoints.
|
|||||||
:----------------------------------------------------------------------------------------------------: |
|
:----------------------------------------------------------------------------------------------------: |
|
||||||
*Fig 3. Vitruvian man aligned via two virtual keypoints predicted by BlazePose detector in addition to the face bounding box.* |
|
*Fig 3. Vitruvian man aligned via two virtual keypoints predicted by BlazePose detector in addition to the face bounding box.* |
|
||||||
|
|
||||||
### Pose Landmark Model (BlazePose GHUM 3D)
|
### Pose Landmark Model (BlazePose [GHUM](https://github.com/google-research/google-research/tree/master/ghum) 3D)
|
||||||
|
|
||||||
The landmark model in MediaPipe Pose predicts the location of 33 pose landmarks
|
The landmark model in MediaPipe Pose predicts the location of 33 pose landmarks
|
||||||
(see figure below).
|
(see figure below).
|
||||||
@@ -141,7 +141,7 @@ Optionally, MediaPipe Pose can predicts a full-body
|
|||||||
Please find more detail in the
|
Please find more detail in the
|
||||||
[BlazePose Google AI Blog](https://ai.googleblog.com/2020/08/on-device-real-time-body-pose-tracking.html),
|
[BlazePose Google AI Blog](https://ai.googleblog.com/2020/08/on-device-real-time-body-pose-tracking.html),
|
||||||
this [paper](https://arxiv.org/abs/2006.10204),
|
this [paper](https://arxiv.org/abs/2006.10204),
|
||||||
[the model card](./models.md#pose) and the [Output](#Output) section below.
|
[the model card](./models.md#pose) and the [Output](#output) section below.
|
||||||
|
|
||||||
## Solution APIs
|
## Solution APIs
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ pixel respectively. Please refer to the platform-specific usage examples below
|
|||||||
for usage details.
|
for usage details.
|
||||||
|
|
||||||
*Fig 6. Example of MediaPipe Pose segmentation mask.* |
|
*Fig 6. Example of MediaPipe Pose segmentation mask.* |
|
||||||
:-----------------------------------------------------------: |
|
:---------------------------------------------------: |
|
||||||
<video autoplay muted loop preload style="height: auto; width: 480px"><source src="../images/mobile/pose_segmentation.mp4" type="video/mp4"></video> |
|
<video autoplay muted loop preload style="height: auto; width: 480px"><source src="../images/mobile/pose_segmentation.mp4" type="video/mp4"></video> |
|
||||||
|
|
||||||
### Python Solution API
|
### Python Solution API
|
||||||
@@ -281,8 +281,8 @@ with mp_pose.Pose(
|
|||||||
continue
|
continue
|
||||||
print(
|
print(
|
||||||
f'Nose coordinates: ('
|
f'Nose coordinates: ('
|
||||||
f'{results.pose_landmarks.landmark[mp_holistic.PoseLandmark.NOSE].x * image_width}, '
|
f'{results.pose_landmarks.landmark[mp_pose.PoseLandmark.NOSE].x * image_width}, '
|
||||||
f'{results.pose_landmarks.landmark[mp_holistic.PoseLandmark.NOSE].y * image_height})'
|
f'{results.pose_landmarks.landmark[mp_pose.PoseLandmark.NOSE].y * image_height})'
|
||||||
)
|
)
|
||||||
|
|
||||||
annotated_image = image.copy()
|
annotated_image = image.copy()
|
||||||
@@ -316,12 +316,10 @@ with mp_pose.Pose(
|
|||||||
# If loading a video, use 'break' instead of 'continue'.
|
# If loading a video, use 'break' instead of 'continue'.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Flip the image horizontally for a later selfie-view display, and convert
|
|
||||||
# the BGR image to RGB.
|
|
||||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
|
||||||
# To improve performance, optionally mark the image as not writeable to
|
# To improve performance, optionally mark the image as not writeable to
|
||||||
# pass by reference.
|
# pass by reference.
|
||||||
image.flags.writeable = False
|
image.flags.writeable = False
|
||||||
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
results = pose.process(image)
|
results = pose.process(image)
|
||||||
|
|
||||||
# Draw the pose annotation on the image.
|
# Draw the pose annotation on the image.
|
||||||
@@ -332,7 +330,8 @@ with mp_pose.Pose(
|
|||||||
results.pose_landmarks,
|
results.pose_landmarks,
|
||||||
mp_pose.POSE_CONNECTIONS,
|
mp_pose.POSE_CONNECTIONS,
|
||||||
landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
|
landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
|
||||||
cv2.imshow('MediaPipe Pose', image)
|
# Flip the image horizontally for a selfie-view display.
|
||||||
|
cv2.imshow('MediaPipe Pose', cv2.flip(image, 1))
|
||||||
if cv2.waitKey(5) & 0xFF == 27:
|
if cv2.waitKey(5) & 0xFF == 27:
|
||||||
break
|
break
|
||||||
cap.release()
|
cap.release()
|
||||||
@@ -369,6 +368,7 @@ Supported configuration options:
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<video class="input_video"></video>
|
<video class="input_video"></video>
|
||||||
<canvas class="output_canvas" width="1280px" height="720px"></canvas>
|
<canvas class="output_canvas" width="1280px" height="720px"></canvas>
|
||||||
|
<div class="landmark-grid-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -486,6 +486,7 @@ on how to build MediaPipe examples.
|
|||||||
[BlazePose: On-device Real-time Body Pose Tracking](https://arxiv.org/abs/2006.10204)
|
[BlazePose: On-device Real-time Body Pose Tracking](https://arxiv.org/abs/2006.10204)
|
||||||
([presentation](https://youtu.be/YPpUOTRn5tA))
|
([presentation](https://youtu.be/YPpUOTRn5tA))
|
||||||
* [Models and model cards](./models.md#pose)
|
* [Models and model cards](./models.md#pose)
|
||||||
|
* [GHUM & GHUML: Generative 3D Human Shape and Articulated Pose Models](https://github.com/google-research/google-research/tree/master/ghum)
|
||||||
* [Web demo](https://code.mediapipe.dev/codepen/pose)
|
* [Web demo](https://code.mediapipe.dev/codepen/pose)
|
||||||
* [Python Colab](https://mediapipe.page.link/pose_py_colab)
|
* [Python Colab](https://mediapipe.page.link/pose_py_colab)
|
||||||
|
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ to visualize its associated subgraphs, please see
|
|||||||
[(or download prebuilt ARM64 APK)](https://drive.google.com/file/d/1DoeyGzMmWUsjfVgZfGGecrn7GKzYcEAo/view?usp=sharing)
|
[(or download prebuilt ARM64 APK)](https://drive.google.com/file/d/1DoeyGzMmWUsjfVgZfGGecrn7GKzYcEAo/view?usp=sharing)
|
||||||
[`mediapipe/examples/android/src/java/com/google/mediapipe/apps/selfiesegmentationgpu:selfiesegmentationgpu`](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/src/java/com/google/mediapipe/apps/selfiesegmentationgpu/BUILD)
|
[`mediapipe/examples/android/src/java/com/google/mediapipe/apps/selfiesegmentationgpu:selfiesegmentationgpu`](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/src/java/com/google/mediapipe/apps/selfiesegmentationgpu/BUILD)
|
||||||
* iOS target:
|
* iOS target:
|
||||||
[`mediapipe/examples/ios/selfiesegmentationgpu:SelfieSegmentationGpuApp`](http:/mediapipe/examples/ios/selfiesegmentationgpu/BUILD)
|
[`mediapipe/examples/ios/selfiesegmentationgpu:SelfieSegmentationGpuApp`](https://github.com/google/mediapipe/tree/master/mediapipe/examples/ios/selfiesegmentationgpu/BUILD)
|
||||||
|
|
||||||
### Desktop
|
### Desktop
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ has_toc: false
|
|||||||
{:toc}
|
{:toc}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
MediaPipe offers open source cross-platform, customizable ML solutions for live
|
||||||
|
and streaming media.
|
||||||
|
|
||||||
<!-- []() in the first cell is needed to preserve table formatting in GitHub Pages. -->
|
<!-- []() in the first cell is needed to preserve table formatting in GitHub Pages. -->
|
||||||
<!-- Whenever this table is updated, paste a copy to ../external_index.md. -->
|
<!-- Whenever this table is updated, paste a copy to ../external_index.md. -->
|
||||||
|
|
||||||
|
|||||||
@@ -531,9 +531,13 @@ cc_test(
|
|||||||
":split_vector_calculator",
|
":split_vector_calculator",
|
||||||
"//mediapipe/framework:calculator_framework",
|
"//mediapipe/framework:calculator_framework",
|
||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
|
"//mediapipe/framework/api2:node",
|
||||||
|
"//mediapipe/framework/api2:port",
|
||||||
"//mediapipe/framework/port:gtest_main",
|
"//mediapipe/framework/port:gtest_main",
|
||||||
"//mediapipe/framework/port:parse_text_proto",
|
"//mediapipe/framework/port:parse_text_proto",
|
||||||
"//mediapipe/framework/port:status",
|
"//mediapipe/framework/port:status",
|
||||||
|
"@com_google_absl//absl/status",
|
||||||
|
"@com_google_absl//absl/types:optional",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1238,7 +1242,6 @@ cc_test(
|
|||||||
"//mediapipe/framework:calculator_profile_cc_proto",
|
"//mediapipe/framework:calculator_profile_cc_proto",
|
||||||
"//mediapipe/framework:test_calculators",
|
"//mediapipe/framework:test_calculators",
|
||||||
"//mediapipe/framework/deps:clock",
|
"//mediapipe/framework/deps:clock",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/port:core_proto",
|
"//mediapipe/framework/port:core_proto",
|
||||||
"//mediapipe/framework/port:gtest_main",
|
"//mediapipe/framework/port:gtest_main",
|
||||||
"//mediapipe/framework/port:integral_types",
|
"//mediapipe/framework/port:integral_types",
|
||||||
|
|||||||
@@ -42,4 +42,13 @@ REGISTER_CALCULATOR(BeginLoopDetectionCalculator);
|
|||||||
typedef BeginLoopCalculator<std::vector<Matrix>> BeginLoopMatrixCalculator;
|
typedef BeginLoopCalculator<std::vector<Matrix>> BeginLoopMatrixCalculator;
|
||||||
REGISTER_CALCULATOR(BeginLoopMatrixCalculator);
|
REGISTER_CALCULATOR(BeginLoopMatrixCalculator);
|
||||||
|
|
||||||
|
// A calculator to process std::vector<std::vector<Matrix>>.
|
||||||
|
typedef BeginLoopCalculator<std::vector<std::vector<Matrix>>>
|
||||||
|
BeginLoopMatrixVectorCalculator;
|
||||||
|
REGISTER_CALCULATOR(BeginLoopMatrixVectorCalculator);
|
||||||
|
|
||||||
|
// A calculator to process std::vector<uint64_t>.
|
||||||
|
typedef BeginLoopCalculator<std::vector<uint64_t>> BeginLoopUint64tCalculator;
|
||||||
|
REGISTER_CALCULATOR(BeginLoopUint64tCalculator);
|
||||||
|
|
||||||
} // namespace mediapipe
|
} // namespace mediapipe
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ message FlowLimiterCalculatorOptions {
|
|||||||
optional int32 max_in_flight = 1 [default = 1];
|
optional int32 max_in_flight = 1 [default = 1];
|
||||||
|
|
||||||
// The maximum number of frames queued waiting for processing.
|
// The maximum number of frames queued waiting for processing.
|
||||||
// The default value limits to 1 frame awaiting processing.
|
// The default value limits to 0 frames awaiting processing.
|
||||||
optional int32 max_in_queue = 2 [default = 0];
|
optional int32 max_in_queue = 2 [default = 0];
|
||||||
|
|
||||||
// The maximum time in microseconds to wait for a frame to finish processing.
|
// The maximum time in microseconds to wait for a frame to finish processing.
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_profile.pb.h"
|
#include "mediapipe/framework/calculator_profile.pb.h"
|
||||||
#include "mediapipe/framework/deps/clock.h"
|
#include "mediapipe/framework/deps/clock.h"
|
||||||
#include "mediapipe/framework/deps/message_matchers.h"
|
|
||||||
#include "mediapipe/framework/port/gmock.h"
|
#include "mediapipe/framework/port/gmock.h"
|
||||||
#include "mediapipe/framework/port/gtest.h"
|
#include "mediapipe/framework/port/gtest.h"
|
||||||
#include "mediapipe/framework/port/integral_types.h"
|
#include "mediapipe/framework/port/integral_types.h"
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "absl/status/status.h"
|
||||||
|
#include "absl/types/optional.h"
|
||||||
#include "mediapipe/calculators/core/split_vector_calculator.h"
|
#include "mediapipe/calculators/core/split_vector_calculator.h"
|
||||||
|
#include "mediapipe/framework/api2/node.h"
|
||||||
|
#include "mediapipe/framework/api2/port.h"
|
||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_runner.h"
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
#include "mediapipe/framework/port/gtest.h"
|
#include "mediapipe/framework/port/gtest.h"
|
||||||
@@ -301,4 +305,99 @@ TEST(MuxCalculatorTest, DiscardSkippedInputs_MuxInputStreamHandler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
class PassThroughAndTsBoundUpdateNode : public mediapipe::api2::Node {
|
||||||
|
public:
|
||||||
|
static constexpr mediapipe::api2::Input<int> kInValue{"VALUE"};
|
||||||
|
static constexpr mediapipe::api2::Output<int> kOutValue{"VALUE"};
|
||||||
|
static constexpr mediapipe::api2::Output<int> kOutTsBoundUpdate{
|
||||||
|
"TS_BOUND_UPDATE"};
|
||||||
|
MEDIAPIPE_NODE_CONTRACT(kInValue, kOutValue, kOutTsBoundUpdate);
|
||||||
|
|
||||||
|
absl::Status Process(CalculatorContext* cc) override {
|
||||||
|
kOutValue(cc).Send(kInValue(cc));
|
||||||
|
kOutTsBoundUpdate(cc).SetNextTimestampBound(
|
||||||
|
cc->InputTimestamp().NextAllowedInStream());
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MEDIAPIPE_REGISTER_NODE(PassThroughAndTsBoundUpdateNode);
|
||||||
|
|
||||||
|
class ToOptionalNode : public mediapipe::api2::Node {
|
||||||
|
public:
|
||||||
|
static constexpr mediapipe::api2::Input<int> kTick{"TICK"};
|
||||||
|
static constexpr mediapipe::api2::Input<int> kInValue{"VALUE"};
|
||||||
|
static constexpr mediapipe::api2::Output<absl::optional<int>> kOutValue{
|
||||||
|
"OUTPUT"};
|
||||||
|
MEDIAPIPE_NODE_CONTRACT(kTick, kInValue, kOutValue);
|
||||||
|
|
||||||
|
absl::Status Process(CalculatorContext* cc) override {
|
||||||
|
if (kInValue(cc).IsEmpty()) {
|
||||||
|
kOutValue(cc).Send(absl::nullopt);
|
||||||
|
} else {
|
||||||
|
kOutValue(cc).Send({kInValue(cc).Get()});
|
||||||
|
}
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MEDIAPIPE_REGISTER_NODE(ToOptionalNode);
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
TEST(MuxCalculatorTest, HandleTimestampBoundUpdates) {
|
||||||
|
CalculatorGraphConfig config =
|
||||||
|
mediapipe::ParseTextProtoOrDie<CalculatorGraphConfig>(
|
||||||
|
R"pb(
|
||||||
|
input_stream: "select"
|
||||||
|
node {
|
||||||
|
calculator: "PassThroughAndTsBoundUpdateNode"
|
||||||
|
input_stream: "VALUE:select"
|
||||||
|
output_stream: "VALUE:select_ps"
|
||||||
|
output_stream: "TS_BOUND_UPDATE:ts_bound_update"
|
||||||
|
}
|
||||||
|
node {
|
||||||
|
calculator: "MuxCalculator"
|
||||||
|
input_stream: "INPUT:0:select_ps"
|
||||||
|
input_stream: "INPUT:1:ts_bound_update"
|
||||||
|
input_stream: "SELECT:select"
|
||||||
|
output_stream: "OUTPUT:select_or_ts_bound_update"
|
||||||
|
}
|
||||||
|
node {
|
||||||
|
calculator: "ToOptionalNode"
|
||||||
|
input_stream: "TICK:select"
|
||||||
|
input_stream: "VALUE:select_or_ts_bound_update"
|
||||||
|
output_stream: "OUTPUT:output"
|
||||||
|
}
|
||||||
|
)pb");
|
||||||
|
std::vector<Packet> output_packets;
|
||||||
|
tool::AddVectorSink("output", &config, &output_packets);
|
||||||
|
|
||||||
|
CalculatorGraph graph;
|
||||||
|
MP_ASSERT_OK(graph.Initialize(config));
|
||||||
|
MP_ASSERT_OK(graph.StartRun({}));
|
||||||
|
|
||||||
|
auto send_value_fn = [&](int value, Timestamp ts) -> absl::Status {
|
||||||
|
MP_RETURN_IF_ERROR(
|
||||||
|
graph.AddPacketToInputStream("select", MakePacket<int>(value).At(ts)));
|
||||||
|
return graph.WaitUntilIdle();
|
||||||
|
};
|
||||||
|
|
||||||
|
MP_ASSERT_OK(send_value_fn(0, Timestamp(1)));
|
||||||
|
ASSERT_EQ(output_packets.size(), 1);
|
||||||
|
EXPECT_EQ(output_packets[0].Get<absl::optional<int>>(), 0);
|
||||||
|
|
||||||
|
MP_ASSERT_OK(send_value_fn(1, Timestamp(2)));
|
||||||
|
ASSERT_EQ(output_packets.size(), 2);
|
||||||
|
EXPECT_EQ(output_packets[1].Get<absl::optional<int>>(), absl::nullopt);
|
||||||
|
|
||||||
|
MP_ASSERT_OK(send_value_fn(0, Timestamp(3)));
|
||||||
|
ASSERT_EQ(output_packets.size(), 3);
|
||||||
|
EXPECT_EQ(output_packets[2].Get<absl::optional<int>>(), 0);
|
||||||
|
|
||||||
|
MP_ASSERT_OK(graph.CloseAllInputStreams());
|
||||||
|
MP_ASSERT_OK(graph.WaitUntilDone());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
} // namespace mediapipe
|
} // namespace mediapipe
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ class PacketClonerCalculator : public CalculatorBase {
|
|||||||
const auto calculator_options =
|
const auto calculator_options =
|
||||||
cc->Options<mediapipe::PacketClonerCalculatorOptions>();
|
cc->Options<mediapipe::PacketClonerCalculatorOptions>();
|
||||||
output_only_when_all_inputs_received_ =
|
output_only_when_all_inputs_received_ =
|
||||||
calculator_options.output_only_when_all_inputs_received();
|
calculator_options.output_only_when_all_inputs_received() ||
|
||||||
|
calculator_options.output_packets_only_when_all_inputs_received();
|
||||||
|
output_empty_packets_before_all_inputs_received_ =
|
||||||
|
calculator_options.output_packets_only_when_all_inputs_received();
|
||||||
|
|
||||||
// Parse input streams.
|
// Parse input streams.
|
||||||
tick_signal_index_ = cc->Inputs().NumEntries() - 1;
|
tick_signal_index_ = cc->Inputs().NumEntries() - 1;
|
||||||
@@ -88,6 +91,9 @@ class PacketClonerCalculator : public CalculatorBase {
|
|||||||
// Return if one of the input is null.
|
// Return if one of the input is null.
|
||||||
for (int i = 0; i < tick_signal_index_; ++i) {
|
for (int i = 0; i < tick_signal_index_; ++i) {
|
||||||
if (current_[i].IsEmpty()) {
|
if (current_[i].IsEmpty()) {
|
||||||
|
if (output_empty_packets_before_all_inputs_received_) {
|
||||||
|
SetAllNextTimestampBounds(cc);
|
||||||
|
}
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,9 +113,17 @@ class PacketClonerCalculator : public CalculatorBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void SetAllNextTimestampBounds(CalculatorContext* cc) {
|
||||||
|
for (int j = 0; j < tick_signal_index_; ++j) {
|
||||||
|
cc->Outputs().Index(j).SetNextTimestampBound(
|
||||||
|
cc->InputTimestamp().NextAllowedInStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Packet> current_;
|
std::vector<Packet> current_;
|
||||||
int tick_signal_index_;
|
int tick_signal_index_;
|
||||||
bool output_only_when_all_inputs_received_;
|
bool output_only_when_all_inputs_received_;
|
||||||
|
bool output_empty_packets_before_all_inputs_received_;
|
||||||
};
|
};
|
||||||
|
|
||||||
REGISTER_CALCULATOR(PacketClonerCalculator);
|
REGISTER_CALCULATOR(PacketClonerCalculator);
|
||||||
|
|||||||
@@ -28,4 +28,9 @@ message PacketClonerCalculatorOptions {
|
|||||||
// When true, this calculator will drop received TICK packets if any input
|
// When true, this calculator will drop received TICK packets if any input
|
||||||
// stream hasn't received a packet yet.
|
// stream hasn't received a packet yet.
|
||||||
optional bool output_only_when_all_inputs_received = 1 [default = false];
|
optional bool output_only_when_all_inputs_received = 1 [default = false];
|
||||||
|
|
||||||
|
// Similar with above, but also transmit empty packet for all streams before
|
||||||
|
// all inputs are received.
|
||||||
|
optional bool output_packets_only_when_all_inputs_received = 2
|
||||||
|
[default = false];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ absl::Status PacketThinnerCalculator::Open(CalculatorContext* cc) {
|
|||||||
header->format = video_header.format;
|
header->format = video_header.format;
|
||||||
header->width = video_header.width;
|
header->width = video_header.width;
|
||||||
header->height = video_header.height;
|
header->height = video_header.height;
|
||||||
|
header->duration = video_header.duration;
|
||||||
header->frame_rate = new_frame_rate;
|
header->frame_rate = new_frame_rate;
|
||||||
cc->Outputs().Index(0).SetHeader(Adopt(header.release()));
|
cc->Outputs().Index(0).SetHeader(Adopt(header.release()));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -80,4 +80,7 @@ typedef SplitVectorCalculator<mediapipe::ClassificationList, false>
|
|||||||
SplitClassificationListVectorCalculator;
|
SplitClassificationListVectorCalculator;
|
||||||
REGISTER_CALCULATOR(SplitClassificationListVectorCalculator);
|
REGISTER_CALCULATOR(SplitClassificationListVectorCalculator);
|
||||||
|
|
||||||
|
typedef SplitVectorCalculator<uint64_t, false> SplitUint64tVectorCalculator;
|
||||||
|
REGISTER_CALCULATOR(SplitUint64tVectorCalculator);
|
||||||
|
|
||||||
} // namespace mediapipe
|
} // namespace mediapipe
|
||||||
|
|||||||
@@ -480,8 +480,7 @@ RectSpec ImageCroppingCalculator::GetCropSpecs(const CalculatorContext* cc,
|
|||||||
if (cc->Inputs().HasTag(kRectTag)) {
|
if (cc->Inputs().HasTag(kRectTag)) {
|
||||||
const auto& rect = cc->Inputs().Tag(kRectTag).Get<Rect>();
|
const auto& rect = cc->Inputs().Tag(kRectTag).Get<Rect>();
|
||||||
// Only use the rect if it is valid.
|
// Only use the rect if it is valid.
|
||||||
if (rect.width() > 0 && rect.height() > 0 && rect.x_center() >= 0 &&
|
if (rect.width() > 0 && rect.height() > 0) {
|
||||||
rect.y_center() >= 0) {
|
|
||||||
x_center = rect.x_center();
|
x_center = rect.x_center();
|
||||||
y_center = rect.y_center();
|
y_center = rect.y_center();
|
||||||
crop_width = rect.width();
|
crop_width = rect.width();
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ namespace mediapipe {
|
|||||||
#if !MEDIAPIPE_DISABLE_GPU
|
#if !MEDIAPIPE_DISABLE_GPU
|
||||||
|
|
||||||
#endif // !MEDIAPIPE_DISABLE_GPU
|
#endif // !MEDIAPIPE_DISABLE_GPU
|
||||||
|
#if defined(MEDIAPIPE_IOS)
|
||||||
|
|
||||||
|
#endif // defined(MEDIAPIPE_IOS)
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char kImageFrameTag[] = "IMAGE";
|
constexpr char kImageFrameTag[] = "IMAGE";
|
||||||
@@ -337,12 +340,15 @@ absl::Status ImageTransformationCalculator::Process(CalculatorContext* cc) {
|
|||||||
!cc->Inputs().Tag("FLIP_VERTICALLY").IsEmpty()) {
|
!cc->Inputs().Tag("FLIP_VERTICALLY").IsEmpty()) {
|
||||||
flip_vertically_ = cc->Inputs().Tag("FLIP_VERTICALLY").Get<bool>();
|
flip_vertically_ = cc->Inputs().Tag("FLIP_VERTICALLY").Get<bool>();
|
||||||
}
|
}
|
||||||
if (cc->Inputs().HasTag("OUTPUT_DIMENSIONS") &&
|
if (cc->Inputs().HasTag("OUTPUT_DIMENSIONS")) {
|
||||||
!cc->Inputs().Tag("OUTPUT_DIMENSIONS").IsEmpty()) {
|
if (cc->Inputs().Tag("OUTPUT_DIMENSIONS").IsEmpty()) {
|
||||||
const auto& image_size =
|
return absl::OkStatus();
|
||||||
cc->Inputs().Tag("OUTPUT_DIMENSIONS").Get<std::pair<int, int>>();
|
} else {
|
||||||
output_width_ = image_size.first;
|
const auto& image_size =
|
||||||
output_height_ = image_size.second;
|
cc->Inputs().Tag("OUTPUT_DIMENSIONS").Get<std::pair<int, int>>();
|
||||||
|
output_width_ = image_size.first;
|
||||||
|
output_height_ = image_size.second;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (use_gpu_) {
|
if (use_gpu_) {
|
||||||
@@ -506,6 +512,14 @@ absl::Status ImageTransformationCalculator::RenderGpu(CalculatorContext* cc) {
|
|||||||
ComputeOutputDimensions(input_width, input_height, &output_width,
|
ComputeOutputDimensions(input_width, input_height, &output_width,
|
||||||
&output_height);
|
&output_height);
|
||||||
|
|
||||||
|
if (scale_mode_ == mediapipe::ScaleMode_Mode_FILL_AND_CROP) {
|
||||||
|
const float scale =
|
||||||
|
std::min(static_cast<float>(output_width_) / input_width,
|
||||||
|
static_cast<float>(output_height_) / input_height);
|
||||||
|
output_width = std::round(input_width * scale);
|
||||||
|
output_height = std::round(input_height * scale);
|
||||||
|
}
|
||||||
|
|
||||||
if (cc->Outputs().HasTag("LETTERBOX_PADDING")) {
|
if (cc->Outputs().HasTag("LETTERBOX_PADDING")) {
|
||||||
auto padding = absl::make_unique<std::array<float, 4>>();
|
auto padding = absl::make_unique<std::array<float, 4>>();
|
||||||
ComputeOutputLetterboxPadding(input_width, input_height, output_width,
|
ComputeOutputLetterboxPadding(input_width, input_height, output_width,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ enum { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, NUM_ATTRIBUTES };
|
|||||||
// The alpha channel can be set to a single value, or come from an image mask.
|
// The alpha channel can be set to a single value, or come from an image mask.
|
||||||
// If the input image has an alpha channel, it will be updated.
|
// If the input image has an alpha channel, it will be updated.
|
||||||
// If the input image doesn't have an alpha channel, one will be added.
|
// If the input image doesn't have an alpha channel, one will be added.
|
||||||
// Adding alpha channel to a Grayscale (single channel) input is not suported.
|
// Adding alpha channel to a Grayscale (single channel) input is not supported.
|
||||||
//
|
//
|
||||||
// Inputs:
|
// Inputs:
|
||||||
// One of the following two IMAGE tags:
|
// One of the following two IMAGE tags:
|
||||||
|
|||||||
@@ -356,6 +356,57 @@ cc_library(
|
|||||||
alwayslink = 1,
|
alwayslink = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mediapipe_proto_library(
|
||||||
|
name = "landmarks_to_tensor_calculator_proto",
|
||||||
|
srcs = ["landmarks_to_tensor_calculator.proto"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//mediapipe/framework:calculator_options_proto",
|
||||||
|
"//mediapipe/framework:calculator_proto",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "landmarks_to_tensor_calculator",
|
||||||
|
srcs = ["landmarks_to_tensor_calculator.cc"],
|
||||||
|
hdrs = ["landmarks_to_tensor_calculator.h"],
|
||||||
|
copts = select({
|
||||||
|
"//mediapipe:apple": [
|
||||||
|
"-x objective-c++",
|
||||||
|
"-fobjc-arc", # enable reference-counting
|
||||||
|
],
|
||||||
|
"//conditions:default": [],
|
||||||
|
}),
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":landmarks_to_tensor_calculator_cc_proto",
|
||||||
|
"//mediapipe/framework:calculator_framework",
|
||||||
|
"//mediapipe/framework/api2:node",
|
||||||
|
"//mediapipe/framework/formats:landmark_cc_proto",
|
||||||
|
"//mediapipe/framework/formats:tensor",
|
||||||
|
"//mediapipe/framework/port:ret_check",
|
||||||
|
],
|
||||||
|
alwayslink = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "landmarks_to_tensor_calculator_test",
|
||||||
|
srcs = ["landmarks_to_tensor_calculator_test.cc"],
|
||||||
|
deps = [
|
||||||
|
":landmarks_to_tensor_calculator",
|
||||||
|
":landmarks_to_tensor_calculator_cc_proto",
|
||||||
|
"//mediapipe/framework:calculator_cc_proto",
|
||||||
|
"//mediapipe/framework:calculator_framework",
|
||||||
|
"//mediapipe/framework:calculator_runner",
|
||||||
|
"//mediapipe/framework/formats:landmark_cc_proto",
|
||||||
|
"//mediapipe/framework/formats:tensor",
|
||||||
|
"//mediapipe/framework/port:gtest_main",
|
||||||
|
"//mediapipe/framework/port:parse_text_proto",
|
||||||
|
"@com_google_absl//absl/memory",
|
||||||
|
"@com_google_googletest//:gtest_main",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
mediapipe_proto_library(
|
mediapipe_proto_library(
|
||||||
name = "tensors_to_floats_calculator_proto",
|
name = "tensors_to_floats_calculator_proto",
|
||||||
srcs = ["tensors_to_floats_calculator.proto"],
|
srcs = ["tensors_to_floats_calculator.proto"],
|
||||||
|
|||||||
@@ -99,13 +99,11 @@ class InferenceCalculator : public NodeIntf {
|
|||||||
kSideInCustomOpResolver{"CUSTOM_OP_RESOLVER"};
|
kSideInCustomOpResolver{"CUSTOM_OP_RESOLVER"};
|
||||||
static constexpr SideInput<TfLiteModelPtr>::Optional kSideInModel{"MODEL"};
|
static constexpr SideInput<TfLiteModelPtr>::Optional kSideInModel{"MODEL"};
|
||||||
static constexpr Output<std::vector<Tensor>> kOutTensors{"TENSORS"};
|
static constexpr Output<std::vector<Tensor>> kOutTensors{"TENSORS"};
|
||||||
static constexpr SideInput<std::string>::Optional kNnApiDelegateCacheDir{
|
static constexpr SideInput<
|
||||||
"NNAPI_CACHE_DIR"};
|
mediapipe::InferenceCalculatorOptions::Delegate>::Optional kDelegate{
|
||||||
static constexpr SideInput<std::string>::Optional kNnApiDelegateModelToken{
|
"DELEGATE"};
|
||||||
"NNAPI_MODEL_TOKEN"};
|
|
||||||
MEDIAPIPE_NODE_CONTRACT(kInTensors, kSideInCustomOpResolver, kSideInModel,
|
MEDIAPIPE_NODE_CONTRACT(kInTensors, kSideInCustomOpResolver, kSideInModel,
|
||||||
kOutTensors, kNnApiDelegateCacheDir,
|
kOutTensors, kDelegate);
|
||||||
kNnApiDelegateModelToken);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
using TfLiteDelegatePtr =
|
using TfLiteDelegatePtr =
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ package mediapipe;
|
|||||||
|
|
||||||
import "mediapipe/framework/calculator.proto";
|
import "mediapipe/framework/calculator.proto";
|
||||||
|
|
||||||
|
option java_package = "com.google.mediapipe.calculator.proto";
|
||||||
|
option java_outer_classname = "InferenceCalculatorProto";
|
||||||
|
|
||||||
// Full Example:
|
// Full Example:
|
||||||
//
|
//
|
||||||
// node {
|
// node {
|
||||||
@@ -31,7 +34,6 @@ import "mediapipe/framework/calculator.proto";
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//
|
|
||||||
message InferenceCalculatorOptions {
|
message InferenceCalculatorOptions {
|
||||||
extend mediapipe.CalculatorOptions {
|
extend mediapipe.CalculatorOptions {
|
||||||
optional InferenceCalculatorOptions ext = 336783863;
|
optional InferenceCalculatorOptions ext = 336783863;
|
||||||
@@ -66,8 +68,30 @@ message InferenceCalculatorOptions {
|
|||||||
// Load pre-compiled serialized binary cache to accelerate init process.
|
// Load pre-compiled serialized binary cache to accelerate init process.
|
||||||
// Only available for OpenCL delegate on Android.
|
// Only available for OpenCL delegate on Android.
|
||||||
// Kernel caching will only be enabled if this path is set.
|
// Kernel caching will only be enabled if this path is set.
|
||||||
|
//
|
||||||
|
// NOTE: binary cache usage may be skipped if valid serialized model,
|
||||||
|
// specified by "serialized_model_dir", exists.
|
||||||
|
//
|
||||||
|
// TODO: update to cached_kernel_dir
|
||||||
optional string cached_kernel_path = 2;
|
optional string cached_kernel_path = 2;
|
||||||
|
|
||||||
|
// A dir to load from and save to a pre-compiled serialized model used to
|
||||||
|
// accelerate init process.
|
||||||
|
//
|
||||||
|
// NOTE: available for OpenCL delegate on Android only when
|
||||||
|
// "use_advanced_gpu_api" is set to true and "model_token" is set
|
||||||
|
// properly.
|
||||||
|
//
|
||||||
|
// NOTE: serialized model takes precedence over binary cache
|
||||||
|
// specified by "cached_kernel_path", which still can be used if
|
||||||
|
// serialized model is invalid or missing.
|
||||||
|
optional string serialized_model_dir = 7;
|
||||||
|
|
||||||
|
// Unique token identifying the model. Used in conjunction with
|
||||||
|
// "serialized_model_dir". It is the caller's responsibility to ensure
|
||||||
|
// there is no clash of the tokens.
|
||||||
|
optional string model_token = 8;
|
||||||
|
|
||||||
// Encapsulated compilation/runtime tradeoffs.
|
// Encapsulated compilation/runtime tradeoffs.
|
||||||
enum InferenceUsage {
|
enum InferenceUsage {
|
||||||
UNSPECIFIED = 0;
|
UNSPECIFIED = 0;
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ int GetXnnpackDefaultNumThreads() {
|
|||||||
// Returns number of threads to configure XNNPACK delegate with.
|
// Returns number of threads to configure XNNPACK delegate with.
|
||||||
// Returns user provided value if specified. Otherwise, tries to choose optimal
|
// Returns user provided value if specified. Otherwise, tries to choose optimal
|
||||||
// number of threads depending on the device.
|
// number of threads depending on the device.
|
||||||
int GetXnnpackNumThreads(const mediapipe::InferenceCalculatorOptions& opts) {
|
int GetXnnpackNumThreads(
|
||||||
|
const bool opts_has_delegate,
|
||||||
|
const mediapipe::InferenceCalculatorOptions::Delegate& opts_delegate) {
|
||||||
static constexpr int kDefaultNumThreads = -1;
|
static constexpr int kDefaultNumThreads = -1;
|
||||||
if (opts.has_delegate() && opts.delegate().has_xnnpack() &&
|
if (opts_has_delegate && opts_delegate.has_xnnpack() &&
|
||||||
opts.delegate().xnnpack().num_threads() != kDefaultNumThreads) {
|
opts_delegate.xnnpack().num_threads() != kDefaultNumThreads) {
|
||||||
return opts.delegate().xnnpack().num_threads();
|
return opts_delegate.xnnpack().num_threads();
|
||||||
}
|
}
|
||||||
return GetXnnpackDefaultNumThreads();
|
return GetXnnpackDefaultNumThreads();
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ class InferenceCalculatorCpuImpl
|
|||||||
private:
|
private:
|
||||||
absl::Status LoadModel(CalculatorContext* cc);
|
absl::Status LoadModel(CalculatorContext* cc);
|
||||||
absl::Status LoadDelegate(CalculatorContext* cc);
|
absl::Status LoadDelegate(CalculatorContext* cc);
|
||||||
|
absl::Status LoadDelegateAndAllocateTensors(CalculatorContext* cc);
|
||||||
|
|
||||||
// TfLite requires us to keep the model alive as long as the interpreter is.
|
// TfLite requires us to keep the model alive as long as the interpreter is.
|
||||||
Packet<TfLiteModelPtr> model_packet_;
|
Packet<TfLiteModelPtr> model_packet_;
|
||||||
@@ -91,8 +94,7 @@ absl::Status InferenceCalculatorCpuImpl::UpdateContract(
|
|||||||
|
|
||||||
absl::Status InferenceCalculatorCpuImpl::Open(CalculatorContext* cc) {
|
absl::Status InferenceCalculatorCpuImpl::Open(CalculatorContext* cc) {
|
||||||
MP_RETURN_IF_ERROR(LoadModel(cc));
|
MP_RETURN_IF_ERROR(LoadModel(cc));
|
||||||
MP_RETURN_IF_ERROR(LoadDelegate(cc));
|
return LoadDelegateAndAllocateTensors(cc);
|
||||||
return absl::OkStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Status InferenceCalculatorCpuImpl::Process(CalculatorContext* cc) {
|
absl::Status InferenceCalculatorCpuImpl::Process(CalculatorContext* cc) {
|
||||||
@@ -156,44 +158,59 @@ absl::Status InferenceCalculatorCpuImpl::LoadModel(CalculatorContext* cc) {
|
|||||||
cc->Options<mediapipe::InferenceCalculatorOptions>().cpu_num_thread());
|
cc->Options<mediapipe::InferenceCalculatorOptions>().cpu_num_thread());
|
||||||
#endif // __EMSCRIPTEN__
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status InferenceCalculatorCpuImpl::LoadDelegateAndAllocateTensors(
|
||||||
|
CalculatorContext* cc) {
|
||||||
|
MP_RETURN_IF_ERROR(LoadDelegate(cc));
|
||||||
|
|
||||||
|
// AllocateTensors() can be called only after ModifyGraphWithDelegate.
|
||||||
RET_CHECK_EQ(interpreter_->AllocateTensors(), kTfLiteOk);
|
RET_CHECK_EQ(interpreter_->AllocateTensors(), kTfLiteOk);
|
||||||
// TODO: Support quantized tensors.
|
// TODO: Support quantized tensors.
|
||||||
CHECK(interpreter_->tensor(interpreter_->inputs()[0])->quantization.type !=
|
RET_CHECK_NE(
|
||||||
kTfLiteAffineQuantization);
|
interpreter_->tensor(interpreter_->inputs()[0])->quantization.type,
|
||||||
|
kTfLiteAffineQuantization);
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Status InferenceCalculatorCpuImpl::LoadDelegate(CalculatorContext* cc) {
|
absl::Status InferenceCalculatorCpuImpl::LoadDelegate(CalculatorContext* cc) {
|
||||||
const auto& calculator_opts =
|
const auto& calculator_opts =
|
||||||
cc->Options<mediapipe::InferenceCalculatorOptions>();
|
cc->Options<mediapipe::InferenceCalculatorOptions>();
|
||||||
if (calculator_opts.has_delegate() &&
|
auto opts_delegate = calculator_opts.delegate();
|
||||||
calculator_opts.delegate().has_tflite()) {
|
if (!kDelegate(cc).IsEmpty()) {
|
||||||
|
mediapipe::InferenceCalculatorOptions::Delegate input_side_packet_delegate =
|
||||||
|
kDelegate(cc).Get();
|
||||||
|
CHECK(input_side_packet_delegate.has_tflite() ||
|
||||||
|
input_side_packet_delegate.has_xnnpack() ||
|
||||||
|
input_side_packet_delegate.has_nnapi() ||
|
||||||
|
input_side_packet_delegate.delegate_case() ==
|
||||||
|
mediapipe::InferenceCalculatorOptions::Delegate::DELEGATE_NOT_SET)
|
||||||
|
<< "inference_calculator_cpu only supports delegate input side packet "
|
||||||
|
<< "for TFLite, XNNPack and Nnapi";
|
||||||
|
opts_delegate.MergeFrom(input_side_packet_delegate);
|
||||||
|
}
|
||||||
|
const bool opts_has_delegate =
|
||||||
|
calculator_opts.has_delegate() || !kDelegate(cc).IsEmpty();
|
||||||
|
if (opts_has_delegate && opts_delegate.has_tflite()) {
|
||||||
// Default tflite inference requeqsted - no need to modify graph.
|
// Default tflite inference requeqsted - no need to modify graph.
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(MEDIAPIPE_ANDROID)
|
#if defined(MEDIAPIPE_ANDROID)
|
||||||
const bool nnapi_requested = calculator_opts.has_delegate()
|
const bool nnapi_requested = opts_has_delegate ? opts_delegate.has_nnapi()
|
||||||
? calculator_opts.delegate().has_nnapi()
|
: calculator_opts.use_nnapi();
|
||||||
: calculator_opts.use_nnapi();
|
|
||||||
if (nnapi_requested) {
|
if (nnapi_requested) {
|
||||||
// Attempt to use NNAPI.
|
// Attempt to use NNAPI.
|
||||||
// If not supported, the default CPU delegate will be created and used.
|
// If not supported, the default CPU delegate will be created and used.
|
||||||
interpreter_->SetAllowFp16PrecisionForFp32(1);
|
interpreter_->SetAllowFp16PrecisionForFp32(1);
|
||||||
tflite::StatefulNnApiDelegate::Options options;
|
tflite::StatefulNnApiDelegate::Options options;
|
||||||
const auto& nnapi = calculator_opts.delegate().nnapi();
|
const auto& nnapi = opts_delegate.nnapi();
|
||||||
// Set up cache_dir and model_token for NNAPI compilation cache.
|
// Set up cache_dir and model_token for NNAPI compilation cache.
|
||||||
options.cache_dir =
|
options.cache_dir =
|
||||||
nnapi.has_cache_dir() ? nnapi.cache_dir().c_str() : nullptr;
|
nnapi.has_cache_dir() ? nnapi.cache_dir().c_str() : nullptr;
|
||||||
if (!kNnApiDelegateCacheDir(cc).IsEmpty()) {
|
|
||||||
options.cache_dir = kNnApiDelegateCacheDir(cc).Get().c_str();
|
|
||||||
}
|
|
||||||
options.model_token =
|
options.model_token =
|
||||||
nnapi.has_model_token() ? nnapi.model_token().c_str() : nullptr;
|
nnapi.has_model_token() ? nnapi.model_token().c_str() : nullptr;
|
||||||
if (!kNnApiDelegateModelToken(cc).IsEmpty()) {
|
|
||||||
options.model_token = kNnApiDelegateModelToken(cc).Get().c_str();
|
|
||||||
}
|
|
||||||
delegate_ = TfLiteDelegatePtr(new tflite::StatefulNnApiDelegate(options),
|
delegate_ = TfLiteDelegatePtr(new tflite::StatefulNnApiDelegate(options),
|
||||||
[](TfLiteDelegate*) {});
|
[](TfLiteDelegate*) {});
|
||||||
RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_.get()),
|
RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_.get()),
|
||||||
@@ -205,13 +222,13 @@ absl::Status InferenceCalculatorCpuImpl::LoadDelegate(CalculatorContext* cc) {
|
|||||||
#if defined(__EMSCRIPTEN__)
|
#if defined(__EMSCRIPTEN__)
|
||||||
const bool use_xnnpack = true;
|
const bool use_xnnpack = true;
|
||||||
#else
|
#else
|
||||||
const bool use_xnnpack = calculator_opts.has_delegate() &&
|
const bool use_xnnpack = opts_has_delegate && opts_delegate.has_xnnpack();
|
||||||
calculator_opts.delegate().has_xnnpack();
|
|
||||||
#endif // defined(__EMSCRIPTEN__)
|
#endif // defined(__EMSCRIPTEN__)
|
||||||
|
|
||||||
if (use_xnnpack) {
|
if (use_xnnpack) {
|
||||||
TfLiteXNNPackDelegateOptions xnnpack_opts{};
|
TfLiteXNNPackDelegateOptions xnnpack_opts{};
|
||||||
xnnpack_opts.num_threads = GetXnnpackNumThreads(calculator_opts);
|
xnnpack_opts.num_threads =
|
||||||
|
GetXnnpackNumThreads(opts_has_delegate, opts_delegate);
|
||||||
delegate_ = TfLiteDelegatePtr(TfLiteXNNPackDelegateCreate(&xnnpack_opts),
|
delegate_ = TfLiteDelegatePtr(TfLiteXNNPackDelegateCreate(&xnnpack_opts),
|
||||||
&TfLiteXNNPackDelegateDelete);
|
&TfLiteXNNPackDelegateDelete);
|
||||||
RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_.get()),
|
RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_.get()),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "absl/memory/memory.h"
|
#include "absl/memory/memory.h"
|
||||||
#include "absl/status/status.h"
|
#include "absl/status/status.h"
|
||||||
#include "mediapipe/calculators/tensor/inference_calculator.h"
|
#include "mediapipe/calculators/tensor/inference_calculator.h"
|
||||||
|
#include "mediapipe/framework/deps/file_path.h"
|
||||||
#include "mediapipe/util/tflite/config.h"
|
#include "mediapipe/util/tflite/config.h"
|
||||||
|
|
||||||
#if MEDIAPIPE_TFLITE_GL_INFERENCE
|
#if MEDIAPIPE_TFLITE_GL_INFERENCE
|
||||||
@@ -49,10 +50,11 @@ class InferenceCalculatorGlImpl
|
|||||||
absl::Status Close(CalculatorContext* cc) override;
|
absl::Status Close(CalculatorContext* cc) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
absl::Status ReadKernelsFromFile();
|
absl::Status ReadGpuCaches();
|
||||||
absl::Status WriteKernelsToFile();
|
absl::Status SaveGpuCaches();
|
||||||
absl::Status LoadModel(CalculatorContext* cc);
|
absl::Status LoadModel(CalculatorContext* cc);
|
||||||
absl::Status LoadDelegate(CalculatorContext* cc);
|
absl::Status LoadDelegate(CalculatorContext* cc);
|
||||||
|
absl::Status LoadDelegateAndAllocateTensors(CalculatorContext* cc);
|
||||||
absl::Status InitTFLiteGPURunner(CalculatorContext* cc);
|
absl::Status InitTFLiteGPURunner(CalculatorContext* cc);
|
||||||
|
|
||||||
// TfLite requires us to keep the model alive as long as the interpreter is.
|
// TfLite requires us to keep the model alive as long as the interpreter is.
|
||||||
@@ -81,6 +83,8 @@ class InferenceCalculatorGlImpl
|
|||||||
|
|
||||||
bool use_kernel_caching_ = false;
|
bool use_kernel_caching_ = false;
|
||||||
std::string cached_kernel_filename_;
|
std::string cached_kernel_filename_;
|
||||||
|
bool use_serialized_model_ = false;
|
||||||
|
std::string serialized_model_path_;
|
||||||
};
|
};
|
||||||
|
|
||||||
absl::Status InferenceCalculatorGlImpl::UpdateContract(CalculatorContract* cc) {
|
absl::Status InferenceCalculatorGlImpl::UpdateContract(CalculatorContract* cc) {
|
||||||
@@ -94,23 +98,43 @@ absl::Status InferenceCalculatorGlImpl::UpdateContract(CalculatorContract* cc) {
|
|||||||
|
|
||||||
absl::Status InferenceCalculatorGlImpl::Open(CalculatorContext* cc) {
|
absl::Status InferenceCalculatorGlImpl::Open(CalculatorContext* cc) {
|
||||||
const auto& options = cc->Options<::mediapipe::InferenceCalculatorOptions>();
|
const auto& options = cc->Options<::mediapipe::InferenceCalculatorOptions>();
|
||||||
use_advanced_gpu_api_ = options.has_delegate() &&
|
mediapipe::InferenceCalculatorOptions::Delegate delegate = options.delegate();
|
||||||
options.delegate().has_gpu() &&
|
if (!kDelegate(cc).IsEmpty()) {
|
||||||
options.delegate().gpu().use_advanced_gpu_api();
|
mediapipe::InferenceCalculatorOptions::Delegate input_side_packet_delegate =
|
||||||
allow_precision_loss_ = options.delegate().gpu().allow_precision_loss();
|
kDelegate(cc).Get();
|
||||||
tflite_gpu_runner_api_ = options.delegate().gpu().api();
|
CHECK(input_side_packet_delegate.has_gpu() ||
|
||||||
tflite_gpu_runner_usage_ = options.delegate().gpu().usage();
|
input_side_packet_delegate.delegate_case() ==
|
||||||
use_kernel_caching_ = use_advanced_gpu_api_ &&
|
mediapipe::InferenceCalculatorOptions::Delegate::DELEGATE_NOT_SET)
|
||||||
options.delegate().gpu().has_cached_kernel_path();
|
<< "inference_calculator_gl only supports delegate input side packet "
|
||||||
|
<< "for Gpu";
|
||||||
|
delegate.MergeFrom(input_side_packet_delegate);
|
||||||
|
}
|
||||||
|
const bool has_delegate = options.has_delegate() || !kDelegate(cc).IsEmpty();
|
||||||
|
use_advanced_gpu_api_ = has_delegate && delegate.has_gpu() &&
|
||||||
|
delegate.gpu().use_advanced_gpu_api();
|
||||||
|
allow_precision_loss_ = delegate.gpu().allow_precision_loss();
|
||||||
|
tflite_gpu_runner_api_ = delegate.gpu().api();
|
||||||
|
tflite_gpu_runner_usage_ = delegate.gpu().usage();
|
||||||
|
use_kernel_caching_ =
|
||||||
|
use_advanced_gpu_api_ && delegate.gpu().has_cached_kernel_path();
|
||||||
|
use_serialized_model_ = use_advanced_gpu_api_ &&
|
||||||
|
delegate.gpu().has_serialized_model_dir() &&
|
||||||
|
delegate.gpu().has_model_token();
|
||||||
use_gpu_delegate_ = !use_advanced_gpu_api_;
|
use_gpu_delegate_ = !use_advanced_gpu_api_;
|
||||||
|
|
||||||
if (use_kernel_caching_) {
|
if (use_kernel_caching_) {
|
||||||
#ifdef MEDIAPIPE_ANDROID
|
#ifdef MEDIAPIPE_ANDROID
|
||||||
cached_kernel_filename_ = options.delegate().gpu().cached_kernel_path() +
|
cached_kernel_filename_ = delegate.gpu().cached_kernel_path() +
|
||||||
mediapipe::File::Basename(options.model_path()) +
|
mediapipe::File::Basename(options.model_path()) +
|
||||||
".ker";
|
".ker";
|
||||||
#endif // MEDIAPIPE_ANDROID
|
#endif // MEDIAPIPE_ANDROID
|
||||||
}
|
}
|
||||||
|
if (use_serialized_model_) {
|
||||||
|
#ifdef MEDIAPIPE_ANDROID
|
||||||
|
serialized_model_path_ = mediapipe::file::JoinPath(
|
||||||
|
delegate.gpu().serialized_model_dir(), delegate.gpu().model_token());
|
||||||
|
#endif // MEDIAPIPE_ANDROID
|
||||||
|
}
|
||||||
|
|
||||||
// When use_advanced_gpu_api_, model loading is handled in InitTFLiteGPURunner
|
// When use_advanced_gpu_api_, model loading is handled in InitTFLiteGPURunner
|
||||||
// for everything.
|
// for everything.
|
||||||
@@ -119,10 +143,11 @@ absl::Status InferenceCalculatorGlImpl::Open(CalculatorContext* cc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MP_RETURN_IF_ERROR(gpu_helper_.Open(cc));
|
MP_RETURN_IF_ERROR(gpu_helper_.Open(cc));
|
||||||
MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this,
|
MP_RETURN_IF_ERROR(
|
||||||
&cc]() -> ::mediapipe::Status {
|
gpu_helper_.RunInGlContext([this, &cc]() -> ::mediapipe::Status {
|
||||||
return use_advanced_gpu_api_ ? InitTFLiteGPURunner(cc) : LoadDelegate(cc);
|
return use_advanced_gpu_api_ ? InitTFLiteGPURunner(cc)
|
||||||
}));
|
: LoadDelegateAndAllocateTensors(cc);
|
||||||
|
}));
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +222,7 @@ absl::Status InferenceCalculatorGlImpl::Process(CalculatorContext* cc) {
|
|||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Status InferenceCalculatorGlImpl::WriteKernelsToFile() {
|
absl::Status InferenceCalculatorGlImpl::SaveGpuCaches() {
|
||||||
#ifdef MEDIAPIPE_ANDROID
|
#ifdef MEDIAPIPE_ANDROID
|
||||||
if (use_kernel_caching_) {
|
if (use_kernel_caching_) {
|
||||||
// Save kernel file.
|
// Save kernel file.
|
||||||
@@ -207,12 +232,22 @@ absl::Status InferenceCalculatorGlImpl::WriteKernelsToFile() {
|
|||||||
MP_RETURN_IF_ERROR(
|
MP_RETURN_IF_ERROR(
|
||||||
mediapipe::file::SetContents(cached_kernel_filename_, cache_str));
|
mediapipe::file::SetContents(cached_kernel_filename_, cache_str));
|
||||||
}
|
}
|
||||||
|
if (use_serialized_model_) {
|
||||||
|
// Save serialized model file.
|
||||||
|
ASSIGN_OR_RETURN(std::vector<uint8_t> serialized_model_vec,
|
||||||
|
tflite_gpu_runner_->GetSerializedModel());
|
||||||
|
absl::string_view serialized_model(
|
||||||
|
reinterpret_cast<char*>(serialized_model_vec.data()),
|
||||||
|
serialized_model_vec.size());
|
||||||
|
MP_RETURN_IF_ERROR(
|
||||||
|
mediapipe::file::SetContents(serialized_model_path_, serialized_model));
|
||||||
|
}
|
||||||
#endif // MEDIAPIPE_ANDROID
|
#endif // MEDIAPIPE_ANDROID
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Status InferenceCalculatorGlImpl::Close(CalculatorContext* cc) {
|
absl::Status InferenceCalculatorGlImpl::Close(CalculatorContext* cc) {
|
||||||
MP_RETURN_IF_ERROR(WriteKernelsToFile());
|
MP_RETURN_IF_ERROR(SaveGpuCaches());
|
||||||
if (use_gpu_delegate_) {
|
if (use_gpu_delegate_) {
|
||||||
MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() -> Status {
|
MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() -> Status {
|
||||||
gpu_buffers_in_.clear();
|
gpu_buffers_in_.clear();
|
||||||
@@ -226,17 +261,24 @@ absl::Status InferenceCalculatorGlImpl::Close(CalculatorContext* cc) {
|
|||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Status InferenceCalculatorGlImpl::ReadKernelsFromFile() {
|
absl::Status InferenceCalculatorGlImpl::ReadGpuCaches() {
|
||||||
#ifdef MEDIAPIPE_ANDROID
|
#ifdef MEDIAPIPE_ANDROID
|
||||||
if (use_kernel_caching_) {
|
if (use_kernel_caching_ && File::Exists(cached_kernel_filename_)) {
|
||||||
// Load pre-compiled kernel file.
|
// Load pre-compiled kernel file.
|
||||||
if (mediapipe::File::Exists(cached_kernel_filename_)) {
|
std::string cache_str;
|
||||||
std::string cache_str;
|
MP_RETURN_IF_ERROR(
|
||||||
MP_RETURN_IF_ERROR(
|
mediapipe::file::GetContents(cached_kernel_filename_, &cache_str));
|
||||||
mediapipe::file::GetContents(cached_kernel_filename_, &cache_str));
|
std::vector<uint8_t> cache_vec(cache_str.begin(), cache_str.end());
|
||||||
std::vector<uint8_t> cache_vec(cache_str.begin(), cache_str.end());
|
tflite_gpu_runner_->SetSerializedBinaryCache(std::move(cache_vec));
|
||||||
tflite_gpu_runner_->SetSerializedBinaryCache(std::move(cache_vec));
|
}
|
||||||
}
|
if (use_serialized_model_ && File::Exists(serialized_model_path_)) {
|
||||||
|
// Load serialized model file.
|
||||||
|
std::string serialized_model_str;
|
||||||
|
MP_RETURN_IF_ERROR(
|
||||||
|
file::GetContents(serialized_model_path_, &serialized_model_str));
|
||||||
|
std::vector<uint8_t> serialized_model_vec(serialized_model_str.begin(),
|
||||||
|
serialized_model_str.end());
|
||||||
|
tflite_gpu_runner_->SetSerializedModel(std::move(serialized_model_vec));
|
||||||
}
|
}
|
||||||
#endif // MEDIAPIPE_ANDROID
|
#endif // MEDIAPIPE_ANDROID
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
@@ -300,7 +342,7 @@ absl::Status InferenceCalculatorGlImpl::InitTFLiteGPURunner(
|
|||||||
tflite_gpu_runner_->GetOutputShapes()[i].c};
|
tflite_gpu_runner_->GetOutputShapes()[i].c};
|
||||||
}
|
}
|
||||||
|
|
||||||
MP_RETURN_IF_ERROR(ReadKernelsFromFile());
|
MP_RETURN_IF_ERROR(ReadGpuCaches());
|
||||||
|
|
||||||
MP_RETURN_IF_ERROR(tflite_gpu_runner_->Build());
|
MP_RETURN_IF_ERROR(tflite_gpu_runner_->Build());
|
||||||
|
|
||||||
@@ -324,11 +366,19 @@ absl::Status InferenceCalculatorGlImpl::LoadModel(CalculatorContext* cc) {
|
|||||||
cc->Options<mediapipe::InferenceCalculatorOptions>().cpu_num_thread());
|
cc->Options<mediapipe::InferenceCalculatorOptions>().cpu_num_thread());
|
||||||
#endif // __EMSCRIPTEN__
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status InferenceCalculatorGlImpl::LoadDelegateAndAllocateTensors(
|
||||||
|
CalculatorContext* cc) {
|
||||||
|
MP_RETURN_IF_ERROR(LoadDelegate(cc));
|
||||||
|
|
||||||
|
// AllocateTensors() can be called only after ModifyGraphWithDelegate.
|
||||||
RET_CHECK_EQ(interpreter_->AllocateTensors(), kTfLiteOk);
|
RET_CHECK_EQ(interpreter_->AllocateTensors(), kTfLiteOk);
|
||||||
// TODO: Support quantized tensors.
|
// TODO: Support quantized tensors.
|
||||||
CHECK(interpreter_->tensor(interpreter_->inputs()[0])->quantization.type !=
|
RET_CHECK_NE(
|
||||||
kTfLiteAffineQuantization);
|
interpreter_->tensor(interpreter_->inputs()[0])->quantization.type,
|
||||||
|
kTfLiteAffineQuantization);
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class InferenceCalculatorMetalImpl
|
|||||||
private:
|
private:
|
||||||
absl::Status LoadModel(CalculatorContext* cc);
|
absl::Status LoadModel(CalculatorContext* cc);
|
||||||
absl::Status LoadDelegate(CalculatorContext* cc);
|
absl::Status LoadDelegate(CalculatorContext* cc);
|
||||||
|
absl::Status LoadDelegateAndAllocateTensors(CalculatorContext* cc);
|
||||||
|
|
||||||
// TfLite requires us to keep the model alive as long as the interpreter is.
|
// TfLite requires us to keep the model alive as long as the interpreter is.
|
||||||
Packet<TfLiteModelPtr> model_packet_;
|
Packet<TfLiteModelPtr> model_packet_;
|
||||||
@@ -130,8 +131,7 @@ absl::Status InferenceCalculatorMetalImpl::Open(CalculatorContext* cc) {
|
|||||||
|
|
||||||
gpu_helper_ = [[MPPMetalHelper alloc] initWithCalculatorContext:cc];
|
gpu_helper_ = [[MPPMetalHelper alloc] initWithCalculatorContext:cc];
|
||||||
RET_CHECK(gpu_helper_);
|
RET_CHECK(gpu_helper_);
|
||||||
MP_RETURN_IF_ERROR(LoadDelegate(cc));
|
return LoadDelegateAndAllocateTensors(cc);
|
||||||
return absl::OkStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Status InferenceCalculatorMetalImpl::Process(CalculatorContext* cc) {
|
absl::Status InferenceCalculatorMetalImpl::Process(CalculatorContext* cc) {
|
||||||
@@ -212,11 +212,19 @@ absl::Status InferenceCalculatorMetalImpl::LoadModel(CalculatorContext* cc) {
|
|||||||
interpreter_->SetNumThreads(
|
interpreter_->SetNumThreads(
|
||||||
cc->Options<mediapipe::InferenceCalculatorOptions>().cpu_num_thread());
|
cc->Options<mediapipe::InferenceCalculatorOptions>().cpu_num_thread());
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status InferenceCalculatorMetalImpl::LoadDelegateAndAllocateTensors(
|
||||||
|
CalculatorContext* cc) {
|
||||||
|
MP_RETURN_IF_ERROR(LoadDelegate(cc));
|
||||||
|
|
||||||
|
// AllocateTensors() can be called only after ModifyGraphWithDelegate.
|
||||||
RET_CHECK_EQ(interpreter_->AllocateTensors(), kTfLiteOk);
|
RET_CHECK_EQ(interpreter_->AllocateTensors(), kTfLiteOk);
|
||||||
// TODO: Support quantized tensors.
|
// TODO: Support quantized tensors.
|
||||||
CHECK(interpreter_->tensor(interpreter_->inputs()[0])->quantization.type !=
|
RET_CHECK_NE(
|
||||||
kTfLiteAffineQuantization);
|
interpreter_->tensor(interpreter_->inputs()[0])->quantization.type,
|
||||||
|
kTfLiteAffineQuantization);
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +244,7 @@ absl::Status InferenceCalculatorMetalImpl::LoadDelegate(CalculatorContext* cc) {
|
|||||||
TfLiteDelegatePtr(TFLGpuDelegateCreate(&options), &TFLGpuDelegateDelete);
|
TfLiteDelegatePtr(TFLGpuDelegateCreate(&options), &TFLGpuDelegateDelete);
|
||||||
RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_.get()),
|
RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_.get()),
|
||||||
kTfLiteOk);
|
kTfLiteOk);
|
||||||
|
|
||||||
id<MTLDevice> device = gpu_helper_.mtlDevice;
|
id<MTLDevice> device = gpu_helper_.mtlDevice;
|
||||||
|
|
||||||
// Get input image sizes.
|
// Get input image sizes.
|
||||||
|
|||||||
@@ -39,8 +39,6 @@
|
|||||||
|
|
||||||
namespace mediapipe {
|
namespace mediapipe {
|
||||||
|
|
||||||
using ::tflite::Interpreter;
|
|
||||||
|
|
||||||
void DoSmokeTest(const std::string& graph_proto) {
|
void DoSmokeTest(const std::string& graph_proto) {
|
||||||
const int width = 8;
|
const int width = 8;
|
||||||
const int height = 8;
|
const int height = 8;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include "mediapipe/calculators/tensor/landmarks_to_tensor_calculator.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "mediapipe/calculators/tensor/landmarks_to_tensor_calculator.pb.h"
|
||||||
|
#include "mediapipe/framework/api2/node.h"
|
||||||
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
|
#include "mediapipe/framework/formats/landmark.pb.h"
|
||||||
|
#include "mediapipe/framework/formats/tensor.h"
|
||||||
|
#include "mediapipe/framework/port/ret_check.h"
|
||||||
|
|
||||||
|
namespace mediapipe {
|
||||||
|
namespace api2 {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
float GetAttribute(
|
||||||
|
const Landmark& landmark,
|
||||||
|
const LandmarksToTensorCalculatorOptions::Attribute& attribute) {
|
||||||
|
switch (attribute) {
|
||||||
|
case LandmarksToTensorCalculatorOptions::X:
|
||||||
|
return landmark.x();
|
||||||
|
case LandmarksToTensorCalculatorOptions::Y:
|
||||||
|
return landmark.y();
|
||||||
|
case LandmarksToTensorCalculatorOptions::Z:
|
||||||
|
return landmark.z();
|
||||||
|
case LandmarksToTensorCalculatorOptions::VISIBILITY:
|
||||||
|
return landmark.visibility();
|
||||||
|
case LandmarksToTensorCalculatorOptions::PRESENCE:
|
||||||
|
return landmark.presence();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class LandmarksToTensorCalculatorImpl
|
||||||
|
: public NodeImpl<LandmarksToTensorCalculator> {
|
||||||
|
public:
|
||||||
|
absl::Status Open(CalculatorContext* cc) override {
|
||||||
|
options_ = cc->Options<LandmarksToTensorCalculatorOptions>();
|
||||||
|
RET_CHECK(options_.attributes_size() > 0)
|
||||||
|
<< "At least one attribute must be specified";
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status Process(CalculatorContext* cc) override {
|
||||||
|
if (kInLandmarkList(cc).IsEmpty()) {
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get input landmarks.
|
||||||
|
const auto& in_landmarks = *kInLandmarkList(cc);
|
||||||
|
|
||||||
|
// Determine tensor shape.
|
||||||
|
const int n_landmarks = in_landmarks.landmark_size();
|
||||||
|
const int n_attributes = options_.attributes_size();
|
||||||
|
auto tensor_shape = options_.flatten()
|
||||||
|
? Tensor::Shape{1, n_landmarks * n_attributes}
|
||||||
|
: Tensor::Shape{1, n_landmarks, n_attributes};
|
||||||
|
|
||||||
|
// Create empty tesnor.
|
||||||
|
Tensor tensor(Tensor::ElementType::kFloat32, tensor_shape);
|
||||||
|
auto* buffer = tensor.GetCpuWriteView().buffer<float>();
|
||||||
|
|
||||||
|
// Fill tensor with landmark attributes.
|
||||||
|
for (int i = 0; i < n_landmarks; ++i) {
|
||||||
|
for (int j = 0; j < n_attributes; ++j) {
|
||||||
|
buffer[i * n_attributes + j] =
|
||||||
|
GetAttribute(in_landmarks.landmark(i), options_.attributes(j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return vector with a single tensor.
|
||||||
|
auto result = std::vector<Tensor>();
|
||||||
|
result.push_back(std::move(tensor));
|
||||||
|
kOutTensors(cc).Send(std::move(result));
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LandmarksToTensorCalculatorOptions options_;
|
||||||
|
};
|
||||||
|
MEDIAPIPE_NODE_IMPLEMENTATION(LandmarksToTensorCalculatorImpl);
|
||||||
|
|
||||||
|
} // namespace api2
|
||||||
|
} // namespace mediapipe
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#ifndef MEDIAPIPE_CALCULATORS_LANDMARKS_TO_TENSOR_CALCULATOR_H_
|
||||||
|
#define MEDIAPIPE_CALCULATORS_LANDMARKS_TO_TENSOR_CALCULATOR_H_
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "mediapipe/framework/api2/node.h"
|
||||||
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
|
#include "mediapipe/framework/formats/landmark.pb.h"
|
||||||
|
#include "mediapipe/framework/formats/tensor.h"
|
||||||
|
|
||||||
|
namespace mediapipe {
|
||||||
|
namespace api2 {
|
||||||
|
|
||||||
|
// A calculator for converting landmars into a Tensor.
|
||||||
|
//
|
||||||
|
// Input:
|
||||||
|
// LANDMARKS - LandmarkList
|
||||||
|
// Landmarks to be converted into a Tensor.
|
||||||
|
//
|
||||||
|
// Output:
|
||||||
|
// TENSORS - std::vector<Tensor>
|
||||||
|
// Vector containing a single Tensor populated with landmark values.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// node {
|
||||||
|
// calculator: "LandmarksToTensorCalculator"
|
||||||
|
// input_stream: "LANDMARKS:landmarks"
|
||||||
|
// output_stream: "TENSORS:tensors"
|
||||||
|
// options: {
|
||||||
|
// [mediapipe.LandmarksToTensorCalculatorOptions.ext] {
|
||||||
|
// attributes: [X, Y, Z, VISIBILITY, PRESENCE]
|
||||||
|
// # flatten: true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
class LandmarksToTensorCalculator : public NodeIntf {
|
||||||
|
public:
|
||||||
|
static constexpr Input<LandmarkList>::Optional kInLandmarkList{"LANDMARKS"};
|
||||||
|
static constexpr Output<std::vector<Tensor>> kOutTensors{"TENSORS"};
|
||||||
|
MEDIAPIPE_NODE_INTERFACE(LandmarksToTensorCalculator, kInLandmarkList,
|
||||||
|
kOutTensors);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace api2
|
||||||
|
} // namespace mediapipe
|
||||||
|
|
||||||
|
#endif // MEDIAPIPE_CALCULATORS_LANDMARKS_TO_TENSOR_CALCULATOR_H_
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// The option proto for the LandmarksToTensorCalculator.
|
||||||
|
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package mediapipe;
|
||||||
|
|
||||||
|
import "mediapipe/framework/calculator.proto";
|
||||||
|
|
||||||
|
message LandmarksToTensorCalculatorOptions {
|
||||||
|
extend mediapipe.CalculatorOptions {
|
||||||
|
optional LandmarksToTensorCalculatorOptions ext = 394810235;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Attribute {
|
||||||
|
X = 0;
|
||||||
|
Y = 1;
|
||||||
|
Z = 2;
|
||||||
|
VISIBILITY = 3;
|
||||||
|
PRESENCE = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subset and order of attributes as they should appear in the output Tensor.
|
||||||
|
// Should contain at least one attribute.
|
||||||
|
repeated Attribute attributes = 1;
|
||||||
|
|
||||||
|
// Collapses all landmark attributes into a one dimensional tensor (i.e.
|
||||||
|
// switches from (n_landmarks, n_attributes) to (n_landmarks * n_attributes)
|
||||||
|
// representation).
|
||||||
|
optional bool flatten = 2 [default = false];
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "absl/memory/memory.h"
|
||||||
|
#include "mediapipe/calculators/tensor/landmarks_to_tensor_calculator.pb.h"
|
||||||
|
#include "mediapipe/framework/calculator.pb.h"
|
||||||
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
|
#include "mediapipe/framework/formats/landmark.pb.h"
|
||||||
|
#include "mediapipe/framework/formats/tensor.h"
|
||||||
|
#include "mediapipe/framework/port/gmock.h"
|
||||||
|
#include "mediapipe/framework/port/gtest.h"
|
||||||
|
#include "mediapipe/framework/port/parse_text_proto.h"
|
||||||
|
#include "mediapipe/framework/port/status_matchers.h"
|
||||||
|
|
||||||
|
namespace mediapipe {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::mediapipe::ParseTextProtoOrDie;
|
||||||
|
using Node = ::mediapipe::CalculatorGraphConfig::Node;
|
||||||
|
|
||||||
|
void RunLandmarks(mediapipe::CalculatorRunner* runner,
|
||||||
|
const LandmarkList& landmarks) {
|
||||||
|
runner->MutableInputs()
|
||||||
|
->Tag("LANDMARKS")
|
||||||
|
.packets.push_back(MakePacket<LandmarkList>(landmarks).At(Timestamp(0)));
|
||||||
|
MP_ASSERT_OK(runner->Run());
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tensor& GetOutputTensor(mediapipe::CalculatorRunner* runner) {
|
||||||
|
const auto& output_packets = runner->Outputs().Tag("TENSORS").packets;
|
||||||
|
EXPECT_EQ(output_packets.size(), 1);
|
||||||
|
|
||||||
|
const auto& tensors = output_packets[0].Get<std::vector<Tensor>>();
|
||||||
|
EXPECT_EQ(tensors.size(), 1);
|
||||||
|
|
||||||
|
return tensors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void ValidateTensor(const Tensor& tensor,
|
||||||
|
const std::vector<int>& expected_shape,
|
||||||
|
const std::vector<float>& expected_values) {
|
||||||
|
EXPECT_EQ(tensor.shape().dims, expected_shape);
|
||||||
|
EXPECT_EQ(tensor.shape().num_elements(), expected_values.size());
|
||||||
|
|
||||||
|
auto* tensor_buffer = tensor.GetCpuReadView().buffer<float>();
|
||||||
|
const std::vector<float> tensor_values(
|
||||||
|
tensor_buffer, tensor_buffer + tensor.shape().num_elements());
|
||||||
|
EXPECT_THAT(tensor_values, testing::ElementsAreArray(expected_values));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LandmarksToTensorCalculatorTest, AllAttributes) {
|
||||||
|
mediapipe::CalculatorRunner runner(ParseTextProtoOrDie<Node>(R"pb(
|
||||||
|
calculator: "LandmarksToTensorCalculator"
|
||||||
|
input_stream: "LANDMARKS:landmarks"
|
||||||
|
output_stream: "TENSORS:tensors"
|
||||||
|
options: {
|
||||||
|
[mediapipe.LandmarksToTensorCalculatorOptions.ext] {
|
||||||
|
attributes: [ X, Y, Z, VISIBILITY, PRESENCE ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)pb"));
|
||||||
|
|
||||||
|
LandmarkList landmarks;
|
||||||
|
auto* landmark1 = landmarks.add_landmark();
|
||||||
|
landmark1->set_x(1.0f);
|
||||||
|
landmark1->set_y(2.0f);
|
||||||
|
landmark1->set_z(3.0f);
|
||||||
|
landmark1->set_visibility(4.0f);
|
||||||
|
landmark1->set_presence(5.0f);
|
||||||
|
auto* landmark2 = landmarks.add_landmark();
|
||||||
|
landmark2->set_x(6.0f);
|
||||||
|
landmark2->set_y(7.0f);
|
||||||
|
landmark2->set_z(8.0f);
|
||||||
|
landmark2->set_visibility(9.0f);
|
||||||
|
landmark2->set_presence(10.0f);
|
||||||
|
|
||||||
|
RunLandmarks(&runner, landmarks);
|
||||||
|
const auto& tensor = GetOutputTensor(&runner);
|
||||||
|
ValidateTensor(tensor, /*expected_shape=*/{1, 2, 5}, /*expected_values=*/
|
||||||
|
{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LandmarksToTensorCalculatorTest, XYZAttributes) {
|
||||||
|
mediapipe::CalculatorRunner runner(ParseTextProtoOrDie<Node>(R"pb(
|
||||||
|
calculator: "LandmarksToTensorCalculator"
|
||||||
|
input_stream: "LANDMARKS:landmarks"
|
||||||
|
output_stream: "TENSORS:tensors"
|
||||||
|
options: {
|
||||||
|
[mediapipe.LandmarksToTensorCalculatorOptions.ext] {
|
||||||
|
attributes: [ X, Y, Z ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)pb"));
|
||||||
|
|
||||||
|
LandmarkList landmarks;
|
||||||
|
auto* landmark1 = landmarks.add_landmark();
|
||||||
|
landmark1->set_x(1.0f);
|
||||||
|
landmark1->set_y(2.0f);
|
||||||
|
landmark1->set_z(3.0f);
|
||||||
|
auto* landmark2 = landmarks.add_landmark();
|
||||||
|
landmark2->set_x(6.0f);
|
||||||
|
landmark2->set_y(7.0f);
|
||||||
|
landmark2->set_z(8.0f);
|
||||||
|
|
||||||
|
RunLandmarks(&runner, landmarks);
|
||||||
|
const auto& tensor = GetOutputTensor(&runner);
|
||||||
|
ValidateTensor(tensor, /*expected_shape=*/{1, 2, 3}, /*expected_values=*/
|
||||||
|
{1.0f, 2.0f, 3.0f, 6.0f, 7.0f, 8.0f});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LandmarksToTensorCalculatorTest, XYZAttributes_Flatten) {
|
||||||
|
mediapipe::CalculatorRunner runner(ParseTextProtoOrDie<Node>(R"pb(
|
||||||
|
calculator: "LandmarksToTensorCalculator"
|
||||||
|
input_stream: "LANDMARKS:landmarks"
|
||||||
|
output_stream: "TENSORS:tensors"
|
||||||
|
options: {
|
||||||
|
[mediapipe.LandmarksToTensorCalculatorOptions.ext] {
|
||||||
|
attributes: [ X, Y, Z ]
|
||||||
|
flatten: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)pb"));
|
||||||
|
|
||||||
|
LandmarkList landmarks;
|
||||||
|
auto* landmark1 = landmarks.add_landmark();
|
||||||
|
landmark1->set_x(1.0f);
|
||||||
|
landmark1->set_y(2.0f);
|
||||||
|
landmark1->set_z(3.0f);
|
||||||
|
auto* landmark2 = landmarks.add_landmark();
|
||||||
|
landmark2->set_x(6.0f);
|
||||||
|
landmark2->set_y(7.0f);
|
||||||
|
landmark2->set_z(8.0f);
|
||||||
|
|
||||||
|
RunLandmarks(&runner, landmarks);
|
||||||
|
const auto& tensor = GetOutputTensor(&runner);
|
||||||
|
ValidateTensor(tensor, /*expected_shape=*/{1, 6}, /*expected_values=*/
|
||||||
|
{1.0f, 2.0f, 3.0f, 6.0f, 7.0f, 8.0f});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace mediapipe
|
||||||
@@ -670,7 +670,8 @@ absl::Status TensorsToDetectionsCalculator::ConvertToDetections(
|
|||||||
detection_boxes[box_offset + 2], detection_boxes[box_offset + 3],
|
detection_boxes[box_offset + 2], detection_boxes[box_offset + 3],
|
||||||
detection_scores[i], detection_classes[i], options_.flip_vertically());
|
detection_scores[i], detection_classes[i], options_.flip_vertically());
|
||||||
const auto& bbox = detection.location_data().relative_bounding_box();
|
const auto& bbox = detection.location_data().relative_bounding_box();
|
||||||
if (bbox.width() < 0 || bbox.height() < 0) {
|
if (bbox.width() < 0 || bbox.height() < 0 || std::isnan(bbox.width()) ||
|
||||||
|
std::isnan(bbox.height())) {
|
||||||
// Decoded detection boxes could have negative values for width/height due
|
// Decoded detection boxes could have negative values for width/height due
|
||||||
// to model prediction. Filter out those boxes since some downstream
|
// to model prediction. Filter out those boxes since some downstream
|
||||||
// calculators may assume non-negative values. (b/171391719)
|
// calculators may assume non-negative values. (b/171391719)
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ using ::tflite::gpu::gl::GlShader;
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Currently only OpenGLES 3.1 and CPU backends supported.
|
|
||||||
// TODO Refactor and add support for other backends/platforms.
|
// TODO Refactor and add support for other backends/platforms.
|
||||||
//
|
//
|
||||||
class TensorsToSegmentationCalculator : public CalculatorBase {
|
class TensorsToSegmentationCalculator : public CalculatorBase {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
#include "tensorflow/core/framework/types.h"
|
#include "tensorflow/core/framework/types.h"
|
||||||
|
|
||||||
namespace mediapipe {
|
namespace mediapipe {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr char kTransposeOptionsString[] =
|
constexpr char kTransposeOptionsString[] =
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ constexpr char kBboxTag[] = "BBOX";
|
|||||||
constexpr char kForwardFlowEncodedTag[] = "FORWARD_FLOW_ENCODED";
|
constexpr char kForwardFlowEncodedTag[] = "FORWARD_FLOW_ENCODED";
|
||||||
constexpr char kImagePrefixTag[] = "IMAGE_PREFIX";
|
constexpr char kImagePrefixTag[] = "IMAGE_PREFIX";
|
||||||
constexpr char kImageTag[] = "IMAGE";
|
constexpr char kImageTag[] = "IMAGE";
|
||||||
|
constexpr char kFloatContextFeatureOtherTag[] = "FLOAT_CONTEXT_FEATURE_OTHER";
|
||||||
|
constexpr char kFloatContextFeatureTestTag[] = "FLOAT_CONTEXT_FEATURE_TEST";
|
||||||
constexpr char kSequenceExampleTag[] = "SEQUENCE_EXAMPLE";
|
constexpr char kSequenceExampleTag[] = "SEQUENCE_EXAMPLE";
|
||||||
|
|
||||||
class UnpackMediaSequenceCalculatorTest : public ::testing::Test {
|
class UnpackMediaSequenceCalculatorTest : public ::testing::Test {
|
||||||
|
|||||||
@@ -24,20 +24,20 @@ message SsdAnchorsCalculatorOptions {
|
|||||||
optional SsdAnchorsCalculatorOptions ext = 247258239;
|
optional SsdAnchorsCalculatorOptions ext = 247258239;
|
||||||
}
|
}
|
||||||
// Size of input images.
|
// Size of input images.
|
||||||
required int32 input_size_width = 1;
|
optional int32 input_size_width = 1; // required
|
||||||
required int32 input_size_height = 2;
|
optional int32 input_size_height = 2; // required
|
||||||
|
|
||||||
// Min and max scales for generating anchor boxes on feature maps.
|
// Min and max scales for generating anchor boxes on feature maps.
|
||||||
required float min_scale = 3;
|
optional float min_scale = 3; // required
|
||||||
required float max_scale = 4;
|
optional float max_scale = 4; // required
|
||||||
|
|
||||||
// The offset for the center of anchors. The value is in the scale of stride.
|
// The offset for the center of anchors. The value is in the scale of stride.
|
||||||
// E.g. 0.5 meaning 0.5 * |current_stride| in pixels.
|
// E.g. 0.5 meaning 0.5 * |current_stride| in pixels.
|
||||||
required float anchor_offset_x = 5 [default = 0.5];
|
optional float anchor_offset_x = 5 [default = 0.5]; // required
|
||||||
required float anchor_offset_y = 6 [default = 0.5];
|
optional float anchor_offset_y = 6 [default = 0.5]; // required
|
||||||
|
|
||||||
// Number of output feature maps to generate the anchors on.
|
// Number of output feature maps to generate the anchors on.
|
||||||
required int32 num_layers = 7;
|
optional int32 num_layers = 7; // required
|
||||||
// Sizes of output feature maps to create anchors. Either feature_map size or
|
// Sizes of output feature maps to create anchors. Either feature_map size or
|
||||||
// stride should be provided.
|
// stride should be provided.
|
||||||
repeated int32 feature_map_width = 8;
|
repeated int32 feature_map_width = 8;
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ message TfLiteTensorsToDetectionsCalculatorOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The number of output classes predicted by the detection model.
|
// The number of output classes predicted by the detection model.
|
||||||
required int32 num_classes = 1;
|
optional int32 num_classes = 1; // required
|
||||||
// The number of output boxes predicted by the detection model.
|
// The number of output boxes predicted by the detection model.
|
||||||
required int32 num_boxes = 2;
|
optional int32 num_boxes = 2; // required
|
||||||
// The number of output values per boxes predicted by the detection model. The
|
// The number of output values per boxes predicted by the detection model. The
|
||||||
// values contain bounding boxes, keypoints, etc.
|
// values contain bounding boxes, keypoints, etc.
|
||||||
required int32 num_coords = 3;
|
optional int32 num_coords = 3; // required
|
||||||
|
|
||||||
// The offset of keypoint coordinates in the location tensor.
|
// The offset of keypoint coordinates in the location tensor.
|
||||||
optional int32 keypoint_coord_offset = 9;
|
optional int32 keypoint_coord_offset = 9;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ message TfLiteTensorsToLandmarksCalculatorOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Number of landmarks from the output of the model.
|
// Number of landmarks from the output of the model.
|
||||||
required int32 num_landmarks = 1;
|
optional int32 num_landmarks = 1; // required
|
||||||
|
|
||||||
// Size of the input image for the model. These options are used only when
|
// Size of the input image for the model. These options are used only when
|
||||||
// normalized landmarks are needed. Z coordinate is scaled as X assuming
|
// normalized landmarks are needed. Z coordinate is scaled as X assuming
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ message TfLiteTensorsToSegmentationCalculatorOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dimensions of input segmentation tensor to process.
|
// Dimensions of input segmentation tensor to process.
|
||||||
required int32 tensor_width = 1;
|
optional int32 tensor_width = 1; // required
|
||||||
required int32 tensor_height = 2;
|
optional int32 tensor_height = 2; // required
|
||||||
required int32 tensor_channels = 3;
|
optional int32 tensor_channels = 3; // required
|
||||||
|
|
||||||
// How much to use previous mask when computing current one; range [0-1].
|
// How much to use previous mask when computing current one; range [0-1].
|
||||||
// This is a tradeoff between responsiveness (0.0) and accuracy (1.0).
|
// This is a tradeoff between responsiveness (0.0) and accuracy (1.0).
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ mediapipe_proto_library(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mediapipe_proto_library(
|
||||||
|
name = "filter_detections_calculator_proto",
|
||||||
|
srcs = ["filter_detections_calculator.proto"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//mediapipe/framework:calculator_options_proto",
|
||||||
|
"//mediapipe/framework:calculator_proto",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
mediapipe_proto_library(
|
mediapipe_proto_library(
|
||||||
name = "timed_box_list_id_to_label_calculator_proto",
|
name = "timed_box_list_id_to_label_calculator_proto",
|
||||||
srcs = ["timed_box_list_id_to_label_calculator.proto"],
|
srcs = ["timed_box_list_id_to_label_calculator.proto"],
|
||||||
@@ -158,6 +168,20 @@ cc_test(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "filter_detections_calculator_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["filter_detections_calculator_test.cc"],
|
||||||
|
deps = [
|
||||||
|
":filter_detections_calculator",
|
||||||
|
"//mediapipe/framework:calculator_framework",
|
||||||
|
"//mediapipe/framework:calculator_runner",
|
||||||
|
"//mediapipe/framework/formats:detection_cc_proto",
|
||||||
|
"//mediapipe/framework/port:gtest_main",
|
||||||
|
"//mediapipe/framework/port:parse_text_proto",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
cc_library(
|
cc_library(
|
||||||
name = "packet_latency_calculator",
|
name = "packet_latency_calculator",
|
||||||
srcs = ["packet_latency_calculator.cc"],
|
srcs = ["packet_latency_calculator.cc"],
|
||||||
@@ -190,7 +214,6 @@ cc_test(
|
|||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
"//mediapipe/framework:timestamp",
|
"//mediapipe/framework:timestamp",
|
||||||
"//mediapipe/framework/deps:clock",
|
"//mediapipe/framework/deps:clock",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/port:gtest_main",
|
"//mediapipe/framework/port:gtest_main",
|
||||||
"//mediapipe/framework/port:parse_text_proto",
|
"//mediapipe/framework/port:parse_text_proto",
|
||||||
"//mediapipe/framework/stream_handler:immediate_input_stream_handler",
|
"//mediapipe/framework/stream_handler:immediate_input_stream_handler",
|
||||||
@@ -372,6 +395,20 @@ cc_library(
|
|||||||
alwayslink = 1,
|
alwayslink = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "filter_detections_calculator",
|
||||||
|
srcs = ["filter_detections_calculator.cc"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":filter_detections_calculator_cc_proto",
|
||||||
|
"//mediapipe/framework:calculator_framework",
|
||||||
|
"//mediapipe/framework/formats:detection_cc_proto",
|
||||||
|
"//mediapipe/framework/port:status",
|
||||||
|
"@com_google_absl//absl/memory",
|
||||||
|
],
|
||||||
|
alwayslink = 1,
|
||||||
|
)
|
||||||
|
|
||||||
cc_library(
|
cc_library(
|
||||||
name = "landmarks_to_detection_calculator",
|
name = "landmarks_to_detection_calculator",
|
||||||
srcs = ["landmarks_to_detection_calculator.cc"],
|
srcs = ["landmarks_to_detection_calculator.cc"],
|
||||||
@@ -449,7 +486,6 @@ cc_test(
|
|||||||
"//mediapipe/framework:calculator_framework",
|
"//mediapipe/framework:calculator_framework",
|
||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
"//mediapipe/framework:packet",
|
"//mediapipe/framework:packet",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/formats:detection_cc_proto",
|
"//mediapipe/framework/formats:detection_cc_proto",
|
||||||
"//mediapipe/framework/formats:location_data_cc_proto",
|
"//mediapipe/framework/formats:location_data_cc_proto",
|
||||||
"//mediapipe/framework/formats:rect_cc_proto",
|
"//mediapipe/framework/formats:rect_cc_proto",
|
||||||
@@ -733,7 +769,6 @@ cc_test(
|
|||||||
"//mediapipe/framework:calculator_framework",
|
"//mediapipe/framework:calculator_framework",
|
||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
"//mediapipe/framework:packet",
|
"//mediapipe/framework:packet",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/formats:detection_cc_proto",
|
"//mediapipe/framework/formats:detection_cc_proto",
|
||||||
"//mediapipe/framework/formats:location_data_cc_proto",
|
"//mediapipe/framework/formats:location_data_cc_proto",
|
||||||
"//mediapipe/framework/port:gtest_main",
|
"//mediapipe/framework/port:gtest_main",
|
||||||
@@ -830,7 +865,6 @@ cc_test(
|
|||||||
"//mediapipe/framework:calculator_cc_proto",
|
"//mediapipe/framework:calculator_cc_proto",
|
||||||
"//mediapipe/framework:calculator_framework",
|
"//mediapipe/framework:calculator_framework",
|
||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/formats:landmark_cc_proto",
|
"//mediapipe/framework/formats:landmark_cc_proto",
|
||||||
"//mediapipe/framework/formats:rect_cc_proto",
|
"//mediapipe/framework/formats:rect_cc_proto",
|
||||||
"//mediapipe/framework/port:gtest_main",
|
"//mediapipe/framework/port:gtest_main",
|
||||||
@@ -1028,7 +1062,6 @@ cc_test(
|
|||||||
"//mediapipe/framework:calculator_framework",
|
"//mediapipe/framework:calculator_framework",
|
||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
"//mediapipe/framework:packet",
|
"//mediapipe/framework:packet",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/port:gtest_main",
|
"//mediapipe/framework/port:gtest_main",
|
||||||
"//mediapipe/framework/port:parse_text_proto",
|
"//mediapipe/framework/port:parse_text_proto",
|
||||||
"//mediapipe/framework/port:status",
|
"//mediapipe/framework/port:status",
|
||||||
@@ -1130,6 +1163,7 @@ cc_library(
|
|||||||
"//mediapipe/framework:collection_item_id",
|
"//mediapipe/framework:collection_item_id",
|
||||||
"//mediapipe/framework/port:rectangle",
|
"//mediapipe/framework/port:rectangle",
|
||||||
"//mediapipe/framework/port:status",
|
"//mediapipe/framework/port:status",
|
||||||
|
"//mediapipe/util:rectangle_util",
|
||||||
"@com_google_absl//absl/memory",
|
"@com_google_absl//absl/memory",
|
||||||
],
|
],
|
||||||
alwayslink = 1,
|
alwayslink = 1,
|
||||||
@@ -1177,7 +1211,6 @@ cc_test(
|
|||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
"//mediapipe/framework:collection_item_id",
|
"//mediapipe/framework:collection_item_id",
|
||||||
"//mediapipe/framework:packet",
|
"//mediapipe/framework:packet",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/formats:detection_cc_proto",
|
"//mediapipe/framework/formats:detection_cc_proto",
|
||||||
"//mediapipe/framework/formats:location_data_cc_proto",
|
"//mediapipe/framework/formats:location_data_cc_proto",
|
||||||
"//mediapipe/framework/formats:rect_cc_proto",
|
"//mediapipe/framework/formats:rect_cc_proto",
|
||||||
@@ -1245,6 +1278,7 @@ cc_library(
|
|||||||
"//mediapipe/framework:calculator_options_cc_proto",
|
"//mediapipe/framework:calculator_options_cc_proto",
|
||||||
"//mediapipe/framework/formats:image_format_cc_proto",
|
"//mediapipe/framework/formats:image_format_cc_proto",
|
||||||
"//mediapipe/framework:calculator_framework",
|
"//mediapipe/framework:calculator_framework",
|
||||||
|
"//mediapipe/framework/api2:node",
|
||||||
"//mediapipe/framework/formats:image_frame",
|
"//mediapipe/framework/formats:image_frame",
|
||||||
"//mediapipe/framework/formats:image",
|
"//mediapipe/framework/formats:image",
|
||||||
"//mediapipe/framework/port:logging",
|
"//mediapipe/framework/port:logging",
|
||||||
@@ -1304,7 +1338,6 @@ cc_test(
|
|||||||
":detection_classifications_merger_calculator",
|
":detection_classifications_merger_calculator",
|
||||||
"//mediapipe/framework:calculator_framework",
|
"//mediapipe/framework:calculator_framework",
|
||||||
"//mediapipe/framework:calculator_runner",
|
"//mediapipe/framework:calculator_runner",
|
||||||
"//mediapipe/framework/deps:message_matchers",
|
|
||||||
"//mediapipe/framework/formats:classification_cc_proto",
|
"//mediapipe/framework/formats:classification_cc_proto",
|
||||||
"//mediapipe/framework/formats:detection_cc_proto",
|
"//mediapipe/framework/formats:detection_cc_proto",
|
||||||
"//mediapipe/framework/port:gtest_main",
|
"//mediapipe/framework/port:gtest_main",
|
||||||
@@ -1345,6 +1378,32 @@ cc_library(
|
|||||||
alwayslink = 1,
|
alwayslink = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mediapipe_proto_library(
|
||||||
|
name = "landmarks_refinement_calculator_proto",
|
||||||
|
srcs = ["landmarks_refinement_calculator.proto"],
|
||||||
|
deps = [
|
||||||
|
"//mediapipe/framework:calculator_options_proto",
|
||||||
|
"//mediapipe/framework:calculator_proto",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "landmarks_refinement_calculator",
|
||||||
|
srcs = ["landmarks_refinement_calculator.cc"],
|
||||||
|
hdrs = ["landmarks_refinement_calculator.h"],
|
||||||
|
deps = [
|
||||||
|
":landmarks_refinement_calculator_cc_proto",
|
||||||
|
"//mediapipe/framework:calculator_framework",
|
||||||
|
"//mediapipe/framework/api2:node",
|
||||||
|
"//mediapipe/framework/api2:port",
|
||||||
|
"//mediapipe/framework/formats:landmark_cc_proto",
|
||||||
|
"//mediapipe/framework/port:core_proto",
|
||||||
|
"//mediapipe/framework/port:ret_check",
|
||||||
|
"@com_google_absl//absl/memory",
|
||||||
|
],
|
||||||
|
alwayslink = 1,
|
||||||
|
)
|
||||||
|
|
||||||
cc_test(
|
cc_test(
|
||||||
name = "refine_landmarks_from_heatmap_calculator_test",
|
name = "refine_landmarks_from_heatmap_calculator_test",
|
||||||
srcs = ["refine_landmarks_from_heatmap_calculator_test.cc"],
|
srcs = ["refine_landmarks_from_heatmap_calculator_test.cc"],
|
||||||
|
|||||||
@@ -26,20 +26,10 @@
|
|||||||
#include "mediapipe/framework/port/canonical_errors.h"
|
#include "mediapipe/framework/port/canonical_errors.h"
|
||||||
#include "mediapipe/framework/port/rectangle.h"
|
#include "mediapipe/framework/port/rectangle.h"
|
||||||
#include "mediapipe/framework/port/status.h"
|
#include "mediapipe/framework/port/status.h"
|
||||||
|
#include "mediapipe/util/rectangle_util.h"
|
||||||
|
|
||||||
namespace mediapipe {
|
namespace mediapipe {
|
||||||
|
|
||||||
// Computes the overlap similarity based on Intersection over Union (IoU) of
|
|
||||||
// two rectangles.
|
|
||||||
inline float OverlapSimilarity(const Rectangle_f& rect1,
|
|
||||||
const Rectangle_f& rect2) {
|
|
||||||
if (!rect1.Intersects(rect2)) return 0.0f;
|
|
||||||
// Compute IoU similarity score.
|
|
||||||
const float intersection_area = Rectangle_f(rect1).Intersect(rect2).Area();
|
|
||||||
const float normalization = rect1.Area() + rect2.Area() - intersection_area;
|
|
||||||
return normalization > 0.0f ? intersection_area / normalization : 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssocationCalculator<T> accepts multiple inputs of vectors of type T that can
|
// AssocationCalculator<T> accepts multiple inputs of vectors of type T that can
|
||||||
// be converted to Rectangle_f. The output is a vector of type T that contains
|
// be converted to Rectangle_f. The output is a vector of type T that contains
|
||||||
// elements from the input vectors that don't overlap with each other. When
|
// elements from the input vectors that don't overlap with each other. When
|
||||||
@@ -187,7 +177,7 @@ class AssociationCalculator : public CalculatorBase {
|
|||||||
|
|
||||||
for (auto uit = current->begin(); uit != current->end();) {
|
for (auto uit = current->begin(); uit != current->end();) {
|
||||||
ASSIGN_OR_RETURN(auto prev_rect, GetRectangle(*uit));
|
ASSIGN_OR_RETURN(auto prev_rect, GetRectangle(*uit));
|
||||||
if (OverlapSimilarity(cur_rect, prev_rect) >
|
if (CalculateIou(cur_rect, prev_rect) >
|
||||||
options_.min_similarity_threshold()) {
|
options_.min_similarity_threshold()) {
|
||||||
std::pair<bool, int> prev_id = GetId(*uit);
|
std::pair<bool, int> prev_id = GetId(*uit);
|
||||||
// If prev_id.first is false when some element doesn't have an ID,
|
// If prev_id.first is false when some element doesn't have an ID,
|
||||||
@@ -232,7 +222,7 @@ class AssociationCalculator : public CalculatorBase {
|
|||||||
}
|
}
|
||||||
const Rectangle_f& prev_rect = get_prev_rectangle.value();
|
const Rectangle_f& prev_rect = get_prev_rectangle.value();
|
||||||
|
|
||||||
if (OverlapSimilarity(cur_rect, prev_rect) >
|
if (CalculateIou(cur_rect, prev_rect) >
|
||||||
options_.min_similarity_threshold()) {
|
options_.min_similarity_threshold()) {
|
||||||
std::pair<bool, int> prev_id = GetId(prev_input_vec[ui]);
|
std::pair<bool, int> prev_id = GetId(prev_input_vec[ui]);
|
||||||
// If prev_id.first is false when some element doesn't have an ID,
|
// If prev_id.first is false when some element doesn't have an ID,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_runner.h"
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
#include "mediapipe/framework/collection_item_id.h"
|
#include "mediapipe/framework/collection_item_id.h"
|
||||||
#include "mediapipe/framework/deps/message_matchers.h"
|
|
||||||
#include "mediapipe/framework/formats/detection.pb.h"
|
#include "mediapipe/framework/formats/detection.pb.h"
|
||||||
#include "mediapipe/framework/formats/location_data.pb.h"
|
#include "mediapipe/framework/formats/location_data.pb.h"
|
||||||
#include "mediapipe/framework/formats/rect.pb.h"
|
#include "mediapipe/framework/formats/rect.pb.h"
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_runner.h"
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
#include "mediapipe/framework/deps/message_matchers.h"
|
|
||||||
#include "mediapipe/framework/formats/classification.pb.h"
|
#include "mediapipe/framework/formats/classification.pb.h"
|
||||||
#include "mediapipe/framework/formats/detection.pb.h"
|
#include "mediapipe/framework/formats/detection.pb.h"
|
||||||
|
#include "mediapipe/framework/port/gmock.h"
|
||||||
#include "mediapipe/framework/port/gtest.h"
|
#include "mediapipe/framework/port/gtest.h"
|
||||||
#include "mediapipe/framework/port/parse_text_proto.h"
|
#include "mediapipe/framework/port/parse_text_proto.h"
|
||||||
#include "mediapipe/framework/port/status_matchers.h"
|
#include "mediapipe/framework/port/status_matchers.h"
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
#include "mediapipe/framework/calculator.pb.h"
|
#include "mediapipe/framework/calculator.pb.h"
|
||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_runner.h"
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
#include "mediapipe/framework/deps/message_matchers.h"
|
|
||||||
#include "mediapipe/framework/formats/detection.pb.h"
|
#include "mediapipe/framework/formats/detection.pb.h"
|
||||||
#include "mediapipe/framework/formats/location_data.pb.h"
|
#include "mediapipe/framework/formats/location_data.pb.h"
|
||||||
#include "mediapipe/framework/formats/rect.pb.h"
|
#include "mediapipe/framework/formats/rect.pb.h"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
#include "mediapipe/framework/calculator.pb.h"
|
#include "mediapipe/framework/calculator.pb.h"
|
||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_runner.h"
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
#include "mediapipe/framework/deps/message_matchers.h"
|
|
||||||
#include "mediapipe/framework/formats/detection.pb.h"
|
#include "mediapipe/framework/formats/detection.pb.h"
|
||||||
#include "mediapipe/framework/formats/location_data.pb.h"
|
#include "mediapipe/framework/formats/location_data.pb.h"
|
||||||
#include "mediapipe/framework/packet.h"
|
#include "mediapipe/framework/packet.h"
|
||||||
@@ -34,8 +33,6 @@ constexpr char kDetectionsTag[] = "DETECTIONS";
|
|||||||
constexpr char kRenderDataTag[] = "RENDER_DATA";
|
constexpr char kRenderDataTag[] = "RENDER_DATA";
|
||||||
constexpr char kDetectionListTag[] = "DETECTION_LIST";
|
constexpr char kDetectionListTag[] = "DETECTION_LIST";
|
||||||
|
|
||||||
using ::testing::DoubleNear;
|
|
||||||
|
|
||||||
// Error tolerance for pixels, distances, etc.
|
// Error tolerance for pixels, distances, etc.
|
||||||
static constexpr double kErrorTolerance = 1e-5;
|
static constexpr double kErrorTolerance = 1e-5;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "absl/memory/memory.h"
|
||||||
|
#include "mediapipe/calculators/util/filter_detections_calculator.pb.h"
|
||||||
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
|
#include "mediapipe/framework/formats/detection.pb.h"
|
||||||
|
#include "mediapipe/framework/port/status.h"
|
||||||
|
|
||||||
|
namespace mediapipe {
|
||||||
|
|
||||||
|
const char kInputDetectionsTag[] = "INPUT_DETECTIONS";
|
||||||
|
const char kOutputDetectionsTag[] = "OUTPUT_DETECTIONS";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Calculator to filter out detections that do not meet the criteria specified
|
||||||
|
// in options.
|
||||||
|
//
|
||||||
|
class FilterDetectionsCalculator : public CalculatorBase {
|
||||||
|
public:
|
||||||
|
static absl::Status GetContract(CalculatorContract* cc) {
|
||||||
|
RET_CHECK(cc->Inputs().HasTag(kInputDetectionsTag));
|
||||||
|
RET_CHECK(cc->Outputs().HasTag(kOutputDetectionsTag));
|
||||||
|
|
||||||
|
cc->Inputs().Tag(kInputDetectionsTag).Set<std::vector<Detection>>();
|
||||||
|
cc->Outputs().Tag(kOutputDetectionsTag).Set<std::vector<Detection>>();
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status Open(CalculatorContext* cc) override {
|
||||||
|
cc->SetOffset(TimestampDiff(0));
|
||||||
|
options_ = cc->Options<mediapipe::FilterDetectionsCalculatorOptions>();
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status Process(CalculatorContext* cc) final {
|
||||||
|
const auto& input_detections =
|
||||||
|
cc->Inputs().Tag(kInputDetectionsTag).Get<std::vector<Detection>>();
|
||||||
|
|
||||||
|
auto output_detections = absl::make_unique<std::vector<Detection>>();
|
||||||
|
|
||||||
|
for (const Detection& detection : input_detections) {
|
||||||
|
RET_CHECK_GT(detection.score_size(), 0);
|
||||||
|
// Note: only score at index 0 supported.
|
||||||
|
if (detection.score(0) >= options_.min_score()) {
|
||||||
|
output_detections->push_back(detection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cc->Outputs()
|
||||||
|
.Tag(kOutputDetectionsTag)
|
||||||
|
.Add(output_detections.release(), cc->InputTimestamp());
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mediapipe::FilterDetectionsCalculatorOptions options_;
|
||||||
|
};
|
||||||
|
|
||||||
|
REGISTER_CALCULATOR(FilterDetectionsCalculator);
|
||||||
|
|
||||||
|
} // namespace mediapipe
|
||||||
+8
-17
@@ -12,26 +12,17 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package com.google.mediapipe.solutioncore;
|
syntax = "proto2";
|
||||||
|
|
||||||
import com.google.auto.value.AutoValue;
|
package mediapipe;
|
||||||
|
|
||||||
/**
|
import "mediapipe/framework/calculator.proto";
|
||||||
* The left, right, bottom, and top boundaries of the visible section on the screen. The boundary
|
|
||||||
* values are typically within the range -1.0 and 1.0.
|
|
||||||
*/
|
|
||||||
@AutoValue
|
|
||||||
public abstract class ResultGlBoundary {
|
|
||||||
|
|
||||||
static ResultGlBoundary create(float left, float right, float bottom, float top) {
|
message FilterDetectionsCalculatorOptions {
|
||||||
return new AutoValue_ResultGlBoundary(left, right, bottom, top);
|
extend mediapipe.CalculatorOptions {
|
||||||
|
optional FilterDetectionsCalculatorOptions ext = 395478132;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract float left();
|
// Detections lower than this score get filtered out.
|
||||||
|
optional float min_score = 1;
|
||||||
public abstract float right();
|
|
||||||
|
|
||||||
public abstract float bottom();
|
|
||||||
|
|
||||||
public abstract float top();
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
|
#include "mediapipe/framework/formats/detection.pb.h"
|
||||||
|
#include "mediapipe/framework/port/gmock.h"
|
||||||
|
#include "mediapipe/framework/port/gtest.h"
|
||||||
|
#include "mediapipe/framework/port/parse_text_proto.h"
|
||||||
|
#include "mediapipe/framework/port/status_matchers.h"
|
||||||
|
|
||||||
|
namespace mediapipe {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::testing::ElementsAre;
|
||||||
|
|
||||||
|
absl::Status RunGraph(std::vector<Detection>& input_detections,
|
||||||
|
std::vector<Detection>* output_detections) {
|
||||||
|
CalculatorRunner runner(R"pb(
|
||||||
|
calculator: "FilterDetectionsCalculator"
|
||||||
|
input_stream: "INPUT_DETECTIONS:input_detections"
|
||||||
|
output_stream: "OUTPUT_DETECTIONS:output_detections"
|
||||||
|
options {
|
||||||
|
[mediapipe.FilterDetectionsCalculatorOptions.ext] { min_score: 0.5 }
|
||||||
|
}
|
||||||
|
)pb");
|
||||||
|
|
||||||
|
const Timestamp input_timestamp = Timestamp(0);
|
||||||
|
runner.MutableInputs()
|
||||||
|
->Tag("INPUT_DETECTIONS")
|
||||||
|
.packets.push_back(MakePacket<std::vector<Detection>>(input_detections)
|
||||||
|
.At(input_timestamp));
|
||||||
|
MP_RETURN_IF_ERROR(runner.Run()) << "Calculator run failed.";
|
||||||
|
|
||||||
|
const std::vector<Packet>& output_packets =
|
||||||
|
runner.Outputs().Tag("OUTPUT_DETECTIONS").packets;
|
||||||
|
RET_CHECK_EQ(output_packets.size(), 1);
|
||||||
|
|
||||||
|
*output_detections = output_packets[0].Get<std::vector<Detection>>();
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FilterDetectionsCalculatorTest, TestFilterDetections) {
|
||||||
|
std::vector<Detection> input_detections;
|
||||||
|
Detection d1, d2;
|
||||||
|
d1.add_score(0.2);
|
||||||
|
d2.add_score(0.8);
|
||||||
|
input_detections.push_back(d1);
|
||||||
|
input_detections.push_back(d2);
|
||||||
|
|
||||||
|
std::vector<Detection> output_detections;
|
||||||
|
MP_EXPECT_OK(RunGraph(input_detections, &output_detections));
|
||||||
|
|
||||||
|
EXPECT_THAT(output_detections, ElementsAre(mediapipe::EqualsProto(d2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FilterDetectionsCalculatorTest, TestFilterDetectionsMultiple) {
|
||||||
|
std::vector<Detection> input_detections;
|
||||||
|
Detection d1, d2, d3, d4;
|
||||||
|
d1.add_score(0.3);
|
||||||
|
d2.add_score(0.4);
|
||||||
|
d3.add_score(0.5);
|
||||||
|
d4.add_score(0.6);
|
||||||
|
input_detections.push_back(d1);
|
||||||
|
input_detections.push_back(d2);
|
||||||
|
input_detections.push_back(d3);
|
||||||
|
input_detections.push_back(d4);
|
||||||
|
|
||||||
|
std::vector<Detection> output_detections;
|
||||||
|
MP_EXPECT_OK(RunGraph(input_detections, &output_detections));
|
||||||
|
|
||||||
|
EXPECT_THAT(output_detections, ElementsAre(mediapipe::EqualsProto(d3),
|
||||||
|
mediapipe::EqualsProto(d4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FilterDetectionsCalculatorTest, TestFilterDetectionsEmpty) {
|
||||||
|
std::vector<Detection> input_detections;
|
||||||
|
|
||||||
|
std::vector<Detection> output_detections;
|
||||||
|
MP_EXPECT_OK(RunGraph(input_detections, &output_detections));
|
||||||
|
|
||||||
|
EXPECT_EQ(output_detections.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace mediapipe
|
||||||
@@ -33,9 +33,17 @@ class InverseMatrixCalculatorImpl : public NodeImpl<InverseMatrixCalculator> {
|
|||||||
kInputMatrix(cc).Get().data());
|
kInputMatrix(cc).Get().data());
|
||||||
|
|
||||||
Eigen::Matrix<float, 4, 4, Eigen::RowMajor> inverse_matrix;
|
Eigen::Matrix<float, 4, 4, Eigen::RowMajor> inverse_matrix;
|
||||||
bool inverse_check;
|
bool inverse_check = false;
|
||||||
matrix.computeInverseWithCheck(inverse_matrix, inverse_check);
|
// The matrix is invertible if the absolute value of its determinant is
|
||||||
RET_CHECK(inverse_check) << "Inverse matrix cannot be calculated.";
|
// greater than this threshold. Quite small threshold is selected to enable
|
||||||
|
// inverting valid matrices containing relatively small values resulting in
|
||||||
|
// a small determinant.
|
||||||
|
constexpr double kAbsDeterminantThreshold =
|
||||||
|
Eigen::NumTraits<double>::epsilon();
|
||||||
|
matrix.computeInverseWithCheck(inverse_matrix, inverse_check,
|
||||||
|
kAbsDeterminantThreshold);
|
||||||
|
RET_CHECK(inverse_check)
|
||||||
|
<< "Inverse matrix cannot be calculated for: " << matrix;
|
||||||
|
|
||||||
std::array<float, 16> output;
|
std::array<float, 16> output;
|
||||||
Eigen::Map<Eigen::Matrix<float, 4, 4, Eigen::RowMajor>>(
|
Eigen::Map<Eigen::Matrix<float, 4, 4, Eigen::RowMajor>>(
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ void RunTest(const std::array<float, 16>& matrix,
|
|||||||
|
|
||||||
const auto& inverse_matrix = output_packets[0].Get<std::array<float, 16>>();
|
const auto& inverse_matrix = output_packets[0].Get<std::array<float, 16>>();
|
||||||
|
|
||||||
EXPECT_THAT(inverse_matrix, testing::Eq(expected_inverse_matrix));
|
EXPECT_THAT(
|
||||||
|
inverse_matrix,
|
||||||
|
testing::Pointwise(testing::FloatEq(),
|
||||||
|
absl::MakeSpan(expected_inverse_matrix.data(),
|
||||||
|
expected_inverse_matrix.size())));
|
||||||
|
|
||||||
// Fully close graph at end, otherwise calculator+tensors are destroyed
|
// Fully close graph at end, otherwise calculator+tensors are destroyed
|
||||||
// after calling WaitUntilDone().
|
// after calling WaitUntilDone().
|
||||||
@@ -122,5 +126,25 @@ TEST(InverseMatrixCalculatorTest, Rotation90) {
|
|||||||
RunTest(matrix, expected_inverse_matrix);
|
RunTest(matrix, expected_inverse_matrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(InverseMatrixCalculatorTest, CheckPrecision) {
|
||||||
|
// clang-format off
|
||||||
|
std::array<float, 16> matrix = {
|
||||||
|
0.00001f, 0.0f, 0.0f, 0.0f,
|
||||||
|
0.0f, 0.00001f, 0.0f, 0.0f,
|
||||||
|
0.0f, 0.0f, 1.0f, 0.0f,
|
||||||
|
0.0f, 0.0f, 0.0f, 1.0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<float, 16> expected_inverse_matrix = {
|
||||||
|
100000.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
0.0f, 100000.0f, 0.0f, 0.0f,
|
||||||
|
0.0f, 0.0f, 1.0f, 0.0f,
|
||||||
|
0.0f, 0.0f, 0.0f, 1.0f,
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
RunTest(matrix, expected_inverse_matrix);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace mediapipe
|
} // namespace mediapipe
|
||||||
|
|||||||
@@ -175,7 +175,8 @@ absl::Status LabelsToRenderDataCalculator::Process(CalculatorContext* cc) {
|
|||||||
|
|
||||||
auto* text = label_annotation->mutable_text();
|
auto* text = label_annotation->mutable_text();
|
||||||
std::string display_text = labels[i];
|
std::string display_text = labels[i];
|
||||||
if (cc->Inputs().HasTag(kScoresTag)) {
|
if (cc->Inputs().HasTag(kScoresTag) ||
|
||||||
|
options_.display_classification_score()) {
|
||||||
absl::StrAppend(&display_text, ":", scores[i]);
|
absl::StrAppend(&display_text, ":", scores[i]);
|
||||||
}
|
}
|
||||||
text->set_display_text(display_text);
|
text->set_display_text(display_text);
|
||||||
|
|||||||
@@ -62,4 +62,7 @@ message LabelsToRenderDataCalculatorOptions {
|
|||||||
|
|
||||||
// Uses Classification.display_name field instead of Classification.label.
|
// Uses Classification.display_name field instead of Classification.label.
|
||||||
optional bool use_display_name = 9 [default = false];
|
optional bool use_display_name = 9 [default = false];
|
||||||
|
|
||||||
|
// Displays Classification score if enabled.
|
||||||
|
optional bool display_classification_score = 10 [default = false];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
#include "mediapipe/framework/calculator.pb.h"
|
#include "mediapipe/framework/calculator.pb.h"
|
||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_runner.h"
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
#include "mediapipe/framework/deps/message_matchers.h"
|
|
||||||
#include "mediapipe/framework/formats/landmark.pb.h"
|
#include "mediapipe/framework/formats/landmark.pb.h"
|
||||||
#include "mediapipe/framework/formats/rect.pb.h"
|
#include "mediapipe/framework/formats/rect.pb.h"
|
||||||
|
#include "mediapipe/framework/port/gmock.h"
|
||||||
#include "mediapipe/framework/port/gtest.h"
|
#include "mediapipe/framework/port/gtest.h"
|
||||||
#include "mediapipe/framework/port/parse_text_proto.h"
|
#include "mediapipe/framework/port/parse_text_proto.h"
|
||||||
#include "mediapipe/framework/port/status_matchers.h"
|
#include "mediapipe/framework/port/status_matchers.h"
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include "mediapipe/calculators/util/landmarks_refinement_calculator.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <set>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "absl/memory/memory.h"
|
||||||
|
#include "mediapipe/calculators/util/landmarks_refinement_calculator.pb.h"
|
||||||
|
#include "mediapipe/framework/api2/node.h"
|
||||||
|
#include "mediapipe/framework/api2/port.h"
|
||||||
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
|
#include "mediapipe/framework/port/proto_ns.h"
|
||||||
|
#include "mediapipe/framework/port/ret_check.h"
|
||||||
|
|
||||||
|
namespace mediapipe {
|
||||||
|
|
||||||
|
namespace api2 {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
absl::StatusOr<int> GetNumberOfRefinedLandmarks(
|
||||||
|
const proto_ns::RepeatedPtrField<
|
||||||
|
LandmarksRefinementCalculatorOptions::Refinement>& refinements) {
|
||||||
|
// Gather all used indexes.
|
||||||
|
std::set<int> idxs;
|
||||||
|
for (int i = 0; i < refinements.size(); ++i) {
|
||||||
|
const auto& refinement = refinements.Get(i);
|
||||||
|
for (int i = 0; i < refinement.indexes_mapping_size(); ++i) {
|
||||||
|
idxs.insert(refinement.indexes_mapping(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that indxes start with 0 and there is no gaps between min and max
|
||||||
|
// indexes.
|
||||||
|
RET_CHECK(!idxs.empty())
|
||||||
|
<< "There should be at least one landmark in indexes mapping";
|
||||||
|
int idxs_min = *idxs.begin();
|
||||||
|
int idxs_max = *idxs.rbegin();
|
||||||
|
int n_idxs = idxs.size();
|
||||||
|
RET_CHECK_EQ(idxs_min, 0)
|
||||||
|
<< "Indexes are expected to start with 0 instead of " << idxs_min;
|
||||||
|
RET_CHECK_EQ(idxs_max, n_idxs - 1)
|
||||||
|
<< "Indexes should have no gaps but " << idxs_max - n_idxs + 1
|
||||||
|
<< " indexes are missing";
|
||||||
|
|
||||||
|
return n_idxs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefineXY(const proto_ns::RepeatedField<int>& indexes_mapping,
|
||||||
|
const NormalizedLandmarkList& landmarks,
|
||||||
|
NormalizedLandmarkList* refined_landmarks) {
|
||||||
|
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||||
|
const auto& landmark = landmarks.landmark(i);
|
||||||
|
auto* refined_landmark =
|
||||||
|
refined_landmarks->mutable_landmark(indexes_mapping.Get(i));
|
||||||
|
refined_landmark->set_x(landmark.x());
|
||||||
|
refined_landmark->set_y(landmark.y());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetZAverage(const NormalizedLandmarkList& landmarks,
|
||||||
|
const proto_ns::RepeatedField<int>& indexes) {
|
||||||
|
double z_sum = 0;
|
||||||
|
for (int i = 0; i < indexes.size(); ++i) {
|
||||||
|
z_sum += landmarks.landmark(indexes.Get(i)).z();
|
||||||
|
}
|
||||||
|
return z_sum / indexes.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefineZ(
|
||||||
|
const proto_ns::RepeatedField<int>& indexes_mapping,
|
||||||
|
const LandmarksRefinementCalculatorOptions::ZRefinement& z_refinement,
|
||||||
|
const NormalizedLandmarkList& landmarks,
|
||||||
|
NormalizedLandmarkList* refined_landmarks) {
|
||||||
|
if (z_refinement.has_none()) {
|
||||||
|
// Do nothing and keep Z that is already in refined landmarks.
|
||||||
|
} else if (z_refinement.has_copy()) {
|
||||||
|
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||||
|
refined_landmarks->mutable_landmark(indexes_mapping.Get(i))
|
||||||
|
->set_z(landmarks.landmark(i).z());
|
||||||
|
}
|
||||||
|
} else if (z_refinement.has_assign_average()) {
|
||||||
|
const float z_average =
|
||||||
|
GetZAverage(*refined_landmarks,
|
||||||
|
z_refinement.assign_average().indexes_for_average());
|
||||||
|
for (int i = 0; i < indexes_mapping.size(); ++i) {
|
||||||
|
refined_landmarks->mutable_landmark(indexes_mapping.Get(i))
|
||||||
|
->set_z(z_average);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CHECK(false) << "Z refinement is either not specified or not supported";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class LandmarksRefinementCalculatorImpl
|
||||||
|
: public NodeImpl<LandmarksRefinementCalculator> {
|
||||||
|
absl::Status Open(CalculatorContext* cc) override {
|
||||||
|
options_ = cc->Options<LandmarksRefinementCalculatorOptions>();
|
||||||
|
|
||||||
|
// Validate refinements.
|
||||||
|
for (int i = 0; i < options_.refinement_size(); ++i) {
|
||||||
|
const auto& refinement = options_.refinement(i);
|
||||||
|
RET_CHECK_GT(refinement.indexes_mapping_size(), 0)
|
||||||
|
<< "Refinement " << i << " has no indexes mapping";
|
||||||
|
RET_CHECK(refinement.has_z_refinement())
|
||||||
|
<< "Refinement " << i << " has no Z refinement specified";
|
||||||
|
RET_CHECK(refinement.z_refinement().has_none() ^
|
||||||
|
refinement.z_refinement().has_copy() ^
|
||||||
|
refinement.z_refinement().has_assign_average())
|
||||||
|
<< "Exactly one Z refinement should be specified";
|
||||||
|
|
||||||
|
const auto z_refinement = refinement.z_refinement();
|
||||||
|
if (z_refinement.has_assign_average()) {
|
||||||
|
RET_CHECK_GT(z_refinement.assign_average().indexes_for_average_size(),
|
||||||
|
0)
|
||||||
|
<< "When using assign average Z refinement at least one index for "
|
||||||
|
"averagin should be specified";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate indexes mapping and get total number of refined landmarks.
|
||||||
|
ASSIGN_OR_RETURN(n_refined_landmarks_,
|
||||||
|
GetNumberOfRefinedLandmarks(options_.refinement()));
|
||||||
|
|
||||||
|
// Validate that number of refinements and landmark streams is the same.
|
||||||
|
RET_CHECK_EQ(kLandmarks(cc).Count(), options_.refinement_size())
|
||||||
|
<< "There are " << options_.refinement_size() << " refinements while "
|
||||||
|
<< kLandmarks(cc).Count() << " landmark streams";
|
||||||
|
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status Process(CalculatorContext* cc) override {
|
||||||
|
// If any of the refinement landmarks is missing - refinement won't happen.
|
||||||
|
for (const auto& landmarks_stream : kLandmarks(cc)) {
|
||||||
|
if (landmarks_stream.IsEmpty()) {
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize refined landmarks list.
|
||||||
|
auto refined_landmarks = absl::make_unique<NormalizedLandmarkList>();
|
||||||
|
for (int i = 0; i < n_refined_landmarks_; ++i) {
|
||||||
|
refined_landmarks->add_landmark();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply input landmarks to outpu refined landmarks in provided order.
|
||||||
|
for (int i = 0; i < kLandmarks(cc).Count(); ++i) {
|
||||||
|
const auto& landmarks = kLandmarks(cc)[i].Get();
|
||||||
|
const auto& refinement = options_.refinement(i);
|
||||||
|
|
||||||
|
// Check number of landmarks in mapping and stream are the same.
|
||||||
|
RET_CHECK_EQ(landmarks.landmark_size(), refinement.indexes_mapping_size())
|
||||||
|
<< "There are " << landmarks.landmark_size()
|
||||||
|
<< " refinement landmarks while mapping has "
|
||||||
|
<< refinement.indexes_mapping_size();
|
||||||
|
|
||||||
|
// Refine X and Y.
|
||||||
|
RefineXY(refinement.indexes_mapping(), landmarks,
|
||||||
|
refined_landmarks.get());
|
||||||
|
|
||||||
|
// Refine Z.
|
||||||
|
RefineZ(refinement.indexes_mapping(), refinement.z_refinement(),
|
||||||
|
landmarks, refined_landmarks.get());
|
||||||
|
|
||||||
|
// Visibility and presence are not currently refined and are left as `0`.
|
||||||
|
}
|
||||||
|
|
||||||
|
kRefinedLandmarks(cc).Send(std::move(refined_landmarks));
|
||||||
|
return absl::OkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LandmarksRefinementCalculatorOptions options_;
|
||||||
|
int n_refined_landmarks_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
MEDIAPIPE_NODE_IMPLEMENTATION(LandmarksRefinementCalculatorImpl);
|
||||||
|
|
||||||
|
} // namespace api2
|
||||||
|
} // namespace mediapipe
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#ifndef MEDIAPIPE_CALCULATORS_UTIL_LANDMARKS_REFINEMENT_CALCULATOR_H_
|
||||||
|
#define MEDIAPIPE_CALCULATORS_UTIL_LANDMARKS_REFINEMENT_CALCULATOR_H_
|
||||||
|
|
||||||
|
#include "mediapipe/framework/api2/node.h"
|
||||||
|
#include "mediapipe/framework/api2/port.h"
|
||||||
|
#include "mediapipe/framework/formats/landmark.pb.h"
|
||||||
|
|
||||||
|
namespace mediapipe {
|
||||||
|
|
||||||
|
namespace api2 {
|
||||||
|
|
||||||
|
// A calculator to refine one set of landmarks with another.
|
||||||
|
//
|
||||||
|
// Inputs:
|
||||||
|
// LANDMARKS: Multiple NormalizedLandmarkList to use for
|
||||||
|
// refinement. They will be applied to the resulting REFINED_LANDMARKS in
|
||||||
|
// the provided order. Each list should be non empty and contain the same
|
||||||
|
// amount of landmarks as indexes in mapping. Number of lists should be the
|
||||||
|
// same as number of refinements in options.
|
||||||
|
//
|
||||||
|
// Outputs:
|
||||||
|
// REFINED_LANDMARKS: A NormalizedLandmarkList with refined landmarks. Number
|
||||||
|
// of produced landmarks is equal to to the maximum index mapping number in
|
||||||
|
// calculator options (calculator verifies that there are no gaps in the
|
||||||
|
// mapping).
|
||||||
|
//
|
||||||
|
// Examples config:
|
||||||
|
// node {
|
||||||
|
// calculator: "LandmarksRefinementCalculator"
|
||||||
|
// input_stream: "LANDMARKS:0:mesh_landmarks"
|
||||||
|
// input_stream: "LANDMARKS:1:lips_landmarks"
|
||||||
|
// input_stream: "LANDMARKS:2:left_eye_landmarks"
|
||||||
|
// input_stream: "LANDMARKS:3:right_eye_landmarks"
|
||||||
|
// output_stream: "REFINED_LANDMARKS:landmarks"
|
||||||
|
// options: {
|
||||||
|
// [mediapipe.LandmarksRefinementCalculatorOptions.ext] {
|
||||||
|
// refinement: {
|
||||||
|
// indexes_mapping: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
// z_refinement: { copy {} }
|
||||||
|
// }
|
||||||
|
// refinement: {
|
||||||
|
// indexes_mapping: [0, 1, 2, 3]
|
||||||
|
// z_refinement: { none {} }
|
||||||
|
// }
|
||||||
|
// refinement: {
|
||||||
|
// indexes_mapping: [4, 5]
|
||||||
|
// z_refinement: { none {} }
|
||||||
|
// }
|
||||||
|
// refinement: {
|
||||||
|
// indexes_mapping: [6, 7]
|
||||||
|
// z_refinement: { none {} }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
class LandmarksRefinementCalculator : public NodeIntf {
|
||||||
|
public:
|
||||||
|
static constexpr Input<::mediapipe::NormalizedLandmarkList>::Multiple
|
||||||
|
kLandmarks{"LANDMARKS"};
|
||||||
|
static constexpr Output<::mediapipe::NormalizedLandmarkList>
|
||||||
|
kRefinedLandmarks{"REFINED_LANDMARKS"};
|
||||||
|
|
||||||
|
MEDIAPIPE_NODE_INTERFACE(LandmarksRefinementCalculator, kLandmarks,
|
||||||
|
kRefinedLandmarks);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace api2
|
||||||
|
} // namespace mediapipe
|
||||||
|
|
||||||
|
#endif // MEDIAPIPE_CALCULATORS_UTIL_LANDMARKS_REFINEMENT_CALCULATOR_H_
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package mediapipe;
|
||||||
|
|
||||||
|
import "mediapipe/framework/calculator.proto";
|
||||||
|
|
||||||
|
message LandmarksRefinementCalculatorOptions {
|
||||||
|
extend CalculatorOptions {
|
||||||
|
optional LandmarksRefinementCalculatorOptions ext = 381914658;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do nothing and keep those Z that are already present in the resulting set
|
||||||
|
// of landmarks.
|
||||||
|
message ZRefinementNone {}
|
||||||
|
|
||||||
|
// Simply copy Z values from the given set of landmarks to the resulting set
|
||||||
|
// of landmarks.
|
||||||
|
message ZRefinementCopy {}
|
||||||
|
|
||||||
|
// Calculate average of the specified set of landmarks in the resulting set
|
||||||
|
// and use it as Z for all given landmarks when assigning their values to the
|
||||||
|
// resulting set of landmarks.
|
||||||
|
message ZRefinementAssignAverage {
|
||||||
|
// Indexes of the resulting landmarks to use for average. Should be non
|
||||||
|
// empty.
|
||||||
|
repeated int32 indexes_for_average = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specifies the set of instructions on assigning z value from the given set
|
||||||
|
// of landmarks to the resulting set of landmarks.
|
||||||
|
message ZRefinement {
|
||||||
|
// Exactly one Z refinement option should be specified.
|
||||||
|
oneof z_refinement_options {
|
||||||
|
ZRefinementNone none = 1;
|
||||||
|
ZRefinementCopy copy = 2;
|
||||||
|
ZRefinementAssignAverage assign_average = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specifies the set of instructions of assigning values to the resulting set
|
||||||
|
// of landmarks.
|
||||||
|
message Refinement {
|
||||||
|
// Maps indexes of the given set of landmarks to indexes of the resulting
|
||||||
|
// set of landmarks. Should be non empty and contain the same amount of
|
||||||
|
// indexes as landmarks in the corresponding input stream.
|
||||||
|
repeated int32 indexes_mapping = 1;
|
||||||
|
|
||||||
|
// Z refinement instructions.
|
||||||
|
optional ZRefinement z_refinement = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refinement instructions for every landmarks input stream. Applied in the
|
||||||
|
// same order as defined. Should be the same amount of refinements as landmark
|
||||||
|
// input streams in the calculator. Union of index mappings should start with
|
||||||
|
// 0 and cover a contineous range.
|
||||||
|
repeated Refinement refinement = 1;
|
||||||
|
}
|
||||||
@@ -86,11 +86,11 @@ inline void GetMinMaxZ(const LandmarkListType& landmarks, float* z_min,
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <class LandmarkType>
|
template <class LandmarkType>
|
||||||
bool IsLandmarkVisibileAndPresent(const LandmarkType& landmark,
|
bool IsLandmarkVisibleAndPresent(const LandmarkType& landmark,
|
||||||
bool utilize_visibility,
|
bool utilize_visibility,
|
||||||
float visibility_threshold,
|
float visibility_threshold,
|
||||||
bool utilize_presence,
|
bool utilize_presence,
|
||||||
float presence_threshold) {
|
float presence_threshold) {
|
||||||
if (utilize_visibility && landmark.has_visibility() &&
|
if (utilize_visibility && landmark.has_visibility() &&
|
||||||
landmark.visibility() < visibility_threshold) {
|
landmark.visibility() < visibility_threshold) {
|
||||||
return false;
|
return false;
|
||||||
@@ -153,12 +153,16 @@ void AddConnectionsWithDepth(const LandmarkListType& landmarks,
|
|||||||
const Color& max_depth_line_color,
|
const Color& max_depth_line_color,
|
||||||
RenderData* render_data) {
|
RenderData* render_data) {
|
||||||
for (int i = 0; i < landmark_connections.size(); i += 2) {
|
for (int i = 0; i < landmark_connections.size(); i += 2) {
|
||||||
|
if (landmark_connections[i] >= landmarks.landmark_size() ||
|
||||||
|
landmark_connections[i + 1] >= landmarks.landmark_size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const auto& ld0 = landmarks.landmark(landmark_connections[i]);
|
const auto& ld0 = landmarks.landmark(landmark_connections[i]);
|
||||||
const auto& ld1 = landmarks.landmark(landmark_connections[i + 1]);
|
const auto& ld1 = landmarks.landmark(landmark_connections[i + 1]);
|
||||||
if (!IsLandmarkVisibileAndPresent<LandmarkType>(
|
if (!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||||
ld0, utilize_visibility, visibility_threshold, utilize_presence,
|
ld0, utilize_visibility, visibility_threshold, utilize_presence,
|
||||||
presence_threshold) ||
|
presence_threshold) ||
|
||||||
!IsLandmarkVisibileAndPresent<LandmarkType>(
|
!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||||
ld1, utilize_visibility, visibility_threshold, utilize_presence,
|
ld1, utilize_visibility, visibility_threshold, utilize_presence,
|
||||||
presence_threshold)) {
|
presence_threshold)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -196,12 +200,16 @@ void AddConnections(const LandmarkListType& landmarks,
|
|||||||
const Color& connection_color, float thickness,
|
const Color& connection_color, float thickness,
|
||||||
bool normalized, RenderData* render_data) {
|
bool normalized, RenderData* render_data) {
|
||||||
for (int i = 0; i < landmark_connections.size(); i += 2) {
|
for (int i = 0; i < landmark_connections.size(); i += 2) {
|
||||||
|
if (landmark_connections[i] >= landmarks.landmark_size() ||
|
||||||
|
landmark_connections[i + 1] >= landmarks.landmark_size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const auto& ld0 = landmarks.landmark(landmark_connections[i]);
|
const auto& ld0 = landmarks.landmark(landmark_connections[i]);
|
||||||
const auto& ld1 = landmarks.landmark(landmark_connections[i + 1]);
|
const auto& ld1 = landmarks.landmark(landmark_connections[i + 1]);
|
||||||
if (!IsLandmarkVisibileAndPresent<LandmarkType>(
|
if (!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||||
ld0, utilize_visibility, visibility_threshold, utilize_presence,
|
ld0, utilize_visibility, visibility_threshold, utilize_presence,
|
||||||
presence_threshold) ||
|
presence_threshold) ||
|
||||||
!IsLandmarkVisibileAndPresent<LandmarkType>(
|
!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||||
ld1, utilize_visibility, visibility_threshold, utilize_presence,
|
ld1, utilize_visibility, visibility_threshold, utilize_presence,
|
||||||
presence_threshold)) {
|
presence_threshold)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -317,7 +325,7 @@ absl::Status LandmarksToRenderDataCalculator::Process(CalculatorContext* cc) {
|
|||||||
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||||
const Landmark& landmark = landmarks.landmark(i);
|
const Landmark& landmark = landmarks.landmark(i);
|
||||||
|
|
||||||
if (!IsLandmarkVisibileAndPresent<Landmark>(
|
if (!IsLandmarkVisibleAndPresent<Landmark>(
|
||||||
landmark, options_.utilize_visibility(),
|
landmark, options_.utilize_visibility(),
|
||||||
options_.visibility_threshold(), options_.utilize_presence(),
|
options_.visibility_threshold(), options_.utilize_presence(),
|
||||||
options_.presence_threshold())) {
|
options_.presence_threshold())) {
|
||||||
@@ -363,7 +371,7 @@ absl::Status LandmarksToRenderDataCalculator::Process(CalculatorContext* cc) {
|
|||||||
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||||
const NormalizedLandmark& landmark = landmarks.landmark(i);
|
const NormalizedLandmark& landmark = landmarks.landmark(i);
|
||||||
|
|
||||||
if (!IsLandmarkVisibileAndPresent<NormalizedLandmark>(
|
if (!IsLandmarkVisibleAndPresent<NormalizedLandmark>(
|
||||||
landmark, options_.utilize_visibility(),
|
landmark, options_.utilize_visibility(),
|
||||||
options_.visibility_threshold(), options_.utilize_presence(),
|
options_.visibility_threshold(), options_.utilize_presence(),
|
||||||
options_.presence_threshold())) {
|
options_.presence_threshold())) {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "mediapipe/calculators/util/latency.pb.h"
|
#include "mediapipe/calculators/util/latency.pb.h"
|
||||||
#include "mediapipe/framework/calculator_runner.h"
|
#include "mediapipe/framework/calculator_runner.h"
|
||||||
#include "mediapipe/framework/deps/clock.h"
|
#include "mediapipe/framework/deps/clock.h"
|
||||||
#include "mediapipe/framework/deps/message_matchers.h"
|
|
||||||
#include "mediapipe/framework/port/gmock.h"
|
#include "mediapipe/framework/port/gmock.h"
|
||||||
#include "mediapipe/framework/port/gtest.h"
|
#include "mediapipe/framework/port/gtest.h"
|
||||||
#include "mediapipe/framework/port/parse_text_proto.h"
|
#include "mediapipe/framework/port/parse_text_proto.h"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ inline float NormalizeRadians(float angle) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Performs geometric transformation to the input Rect or NormalizedRect,
|
// Performs geometric transformation to the input Rect or NormalizedRect,
|
||||||
// correpsonding to input stream RECT or NORM_RECT respectively. When the input
|
// corresponding to input stream RECT or NORM_RECT respectively. When the input
|
||||||
// is NORM_RECT, an addition input stream IMAGE_SIZE is required, which is a
|
// is NORM_RECT, an addition input stream IMAGE_SIZE is required, which is a
|
||||||
// std::pair<int, int> representing the image width and height.
|
// std::pair<int, int> representing the image width and height.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "mediapipe/framework/api2/node.h"
|
||||||
#include "mediapipe/framework/calculator_framework.h"
|
#include "mediapipe/framework/calculator_framework.h"
|
||||||
#include "mediapipe/framework/calculator_options.pb.h"
|
#include "mediapipe/framework/calculator_options.pb.h"
|
||||||
#include "mediapipe/framework/formats/image.h"
|
#include "mediapipe/framework/formats/image.h"
|
||||||
@@ -23,23 +24,23 @@
|
|||||||
#include "mediapipe/framework/port/status.h"
|
#include "mediapipe/framework/port/status.h"
|
||||||
#include "mediapipe/framework/port/vector.h"
|
#include "mediapipe/framework/port/vector.h"
|
||||||
|
|
||||||
#if !MEDIAPIPE_DISABLE_GPU
|
|
||||||
#include "mediapipe/gpu/gl_calculator_helper.h"
|
|
||||||
#endif // !MEDIAPIPE_DISABLE_GPU
|
|
||||||
|
|
||||||
namespace mediapipe {
|
namespace mediapipe {
|
||||||
|
namespace api2 {
|
||||||
|
|
||||||
namespace {
|
#if MEDIAPIPE_DISABLE_GPU
|
||||||
constexpr char kImageFrameTag[] = "IMAGE_CPU";
|
// Just a placeholder to not have to depend on mediapipe::GpuBuffer.
|
||||||
constexpr char kGpuBufferTag[] = "IMAGE_GPU";
|
class Nothing {};
|
||||||
constexpr char kImageTag[] = "IMAGE";
|
using GpuBuffer = Nothing;
|
||||||
} // namespace
|
#else
|
||||||
|
using GpuBuffer = mediapipe::GpuBuffer;
|
||||||
|
#endif // MEDIAPIPE_DISABLE_GPU
|
||||||
|
|
||||||
// A calculator for converting from legacy MediaPipe datatypes into a
|
// A calculator for converting from legacy MediaPipe datatypes into a
|
||||||
// unified image container.
|
// unified image container.
|
||||||
//
|
//
|
||||||
// Inputs:
|
// Inputs:
|
||||||
// One of the following two tags:
|
// One of the following two tags:
|
||||||
|
// IMAGE: An Image, ImageFrame, or GpuBuffer containing input image.
|
||||||
// IMAGE_CPU: An ImageFrame containing input image.
|
// IMAGE_CPU: An ImageFrame containing input image.
|
||||||
// IMAGE_GPU: A GpuBuffer containing input image.
|
// IMAGE_GPU: A GpuBuffer containing input image.
|
||||||
//
|
//
|
||||||
@@ -49,107 +50,44 @@ constexpr char kImageTag[] = "IMAGE";
|
|||||||
// Note:
|
// Note:
|
||||||
// No CPU/GPU conversion is done.
|
// No CPU/GPU conversion is done.
|
||||||
//
|
//
|
||||||
class ToImageCalculator : public CalculatorBase {
|
class ToImageCalculator : public Node {
|
||||||
public:
|
public:
|
||||||
ToImageCalculator() = default;
|
ToImageCalculator() = default;
|
||||||
~ToImageCalculator() override = default;
|
~ToImageCalculator() override = default;
|
||||||
|
|
||||||
static absl::Status GetContract(CalculatorContract* cc);
|
static constexpr Input<
|
||||||
|
OneOf<mediapipe::Image, mediapipe::ImageFrame, GpuBuffer>>::Optional kIn{
|
||||||
|
"IMAGE"};
|
||||||
|
static constexpr Input<mediapipe::ImageFrame>::Optional kInCpu{"IMAGE_CPU"};
|
||||||
|
static constexpr Input<GpuBuffer>::Optional kInGpu{"IMAGE_GPU"};
|
||||||
|
static constexpr Output<mediapipe::Image> kOut{"IMAGE"};
|
||||||
|
MEDIAPIPE_NODE_CONTRACT(kIn, kInCpu, kInGpu, kOut);
|
||||||
|
|
||||||
|
static absl::Status UpdateContract(CalculatorContract* cc);
|
||||||
|
|
||||||
// From Calculator.
|
// From Calculator.
|
||||||
absl::Status Open(CalculatorContext* cc) override;
|
|
||||||
absl::Status Process(CalculatorContext* cc) override;
|
absl::Status Process(CalculatorContext* cc) override;
|
||||||
absl::Status Close(CalculatorContext* cc) override;
|
absl::Status Close(CalculatorContext* cc) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
absl::Status RenderGpu(CalculatorContext* cc);
|
absl::StatusOr<Packet<Image>> GetInputImage(CalculatorContext* cc);
|
||||||
absl::Status RenderCpu(CalculatorContext* cc);
|
|
||||||
|
|
||||||
bool gpu_input_ = false;
|
|
||||||
bool gpu_initialized_ = false;
|
|
||||||
#if !MEDIAPIPE_DISABLE_GPU
|
|
||||||
mediapipe::GlCalculatorHelper gpu_helper_;
|
|
||||||
#endif // !MEDIAPIPE_DISABLE_GPU
|
|
||||||
};
|
};
|
||||||
REGISTER_CALCULATOR(ToImageCalculator);
|
MEDIAPIPE_REGISTER_NODE(ToImageCalculator);
|
||||||
|
|
||||||
absl::Status ToImageCalculator::GetContract(CalculatorContract* cc) {
|
absl::Status ToImageCalculator::UpdateContract(CalculatorContract* cc) {
|
||||||
cc->Outputs().Tag(kImageTag).Set<mediapipe::Image>();
|
int num_inputs = static_cast<int>(kIn(cc).IsConnected()) +
|
||||||
|
static_cast<int>(kInCpu(cc).IsConnected()) +
|
||||||
bool gpu_input = false;
|
static_cast<int>(kInGpu(cc).IsConnected());
|
||||||
|
if (num_inputs != 1) {
|
||||||
if (cc->Inputs().HasTag(kImageFrameTag) &&
|
|
||||||
cc->Inputs().HasTag(kGpuBufferTag)) {
|
|
||||||
return absl::InternalError("Cannot have multiple inputs.");
|
return absl::InternalError("Cannot have multiple inputs.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cc->Inputs().HasTag(kGpuBufferTag)) {
|
|
||||||
#if !MEDIAPIPE_DISABLE_GPU
|
|
||||||
cc->Inputs().Tag(kGpuBufferTag).Set<mediapipe::GpuBuffer>();
|
|
||||||
gpu_input = true;
|
|
||||||
#else
|
|
||||||
RET_CHECK_FAIL() << "GPU is disabled. Cannot use IMAGE_GPU stream.";
|
|
||||||
#endif // !MEDIAPIPE_DISABLE_GPU
|
|
||||||
}
|
|
||||||
if (cc->Inputs().HasTag(kImageFrameTag)) {
|
|
||||||
cc->Inputs().Tag(kImageFrameTag).Set<mediapipe::ImageFrame>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gpu_input) {
|
|
||||||
#if !MEDIAPIPE_DISABLE_GPU
|
|
||||||
MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc));
|
|
||||||
#endif // !MEDIAPIPE_DISABLE_GPU
|
|
||||||
}
|
|
||||||
|
|
||||||
return absl::OkStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
absl::Status ToImageCalculator::Open(CalculatorContext* cc) {
|
|
||||||
cc->SetOffset(TimestampDiff(0));
|
|
||||||
|
|
||||||
if (cc->Inputs().HasTag(kGpuBufferTag)) {
|
|
||||||
gpu_input_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gpu_input_) {
|
|
||||||
#if !MEDIAPIPE_DISABLE_GPU
|
|
||||||
MP_RETURN_IF_ERROR(gpu_helper_.Open(cc));
|
|
||||||
#endif
|
|
||||||
} // !MEDIAPIPE_DISABLE_GPU
|
|
||||||
|
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Status ToImageCalculator::Process(CalculatorContext* cc) {
|
absl::Status ToImageCalculator::Process(CalculatorContext* cc) {
|
||||||
if (gpu_input_) {
|
ASSIGN_OR_RETURN(auto output, GetInputImage(cc));
|
||||||
#if !MEDIAPIPE_DISABLE_GPU
|
kOut(cc).Send(output.At(cc->InputTimestamp()));
|
||||||
MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([&cc]() -> absl::Status {
|
|
||||||
auto& input = cc->Inputs().Tag(kGpuBufferTag).Get<mediapipe::GpuBuffer>();
|
|
||||||
// Wrap texture pointer; shallow copy.
|
|
||||||
auto output = std::make_unique<mediapipe::Image>(input);
|
|
||||||
cc->Outputs().Tag(kImageTag).Add(output.release(), cc->InputTimestamp());
|
|
||||||
return absl::OkStatus();
|
|
||||||
}));
|
|
||||||
#endif // !MEDIAPIPE_DISABLE_GPU
|
|
||||||
} else {
|
|
||||||
// The input ImageFrame.
|
|
||||||
auto& input = cc->Inputs().Tag(kImageFrameTag).Get<mediapipe::ImageFrame>();
|
|
||||||
// Make a copy of the input packet to co-own the input ImageFrame.
|
|
||||||
Packet* packet_copy_ptr =
|
|
||||||
new Packet(cc->Inputs().Tag(kImageFrameTag).Value());
|
|
||||||
// Create an output Image that (co-)owns a new ImageFrame that points to
|
|
||||||
// the same pixel data as the input ImageFrame and also owns the packet
|
|
||||||
// copy. As a result, the output Image indirectly co-owns the input
|
|
||||||
// ImageFrame. This ensures a correct life span of the shared pixel data.
|
|
||||||
std::unique_ptr<mediapipe::Image> output =
|
|
||||||
std::make_unique<mediapipe::Image>(
|
|
||||||
std::make_shared<mediapipe::ImageFrame>(
|
|
||||||
input.Format(), input.Width(), input.Height(),
|
|
||||||
input.WidthStep(), const_cast<uint8*>(input.PixelData()),
|
|
||||||
[packet_copy_ptr](uint8*) { delete packet_copy_ptr; }));
|
|
||||||
cc->Outputs().Tag(kImageTag).Add(output.release(), cc->InputTimestamp());
|
|
||||||
}
|
|
||||||
|
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,4 +95,43 @@ absl::Status ToImageCalculator::Close(CalculatorContext* cc) {
|
|||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap ImageFrameSharedPtr; shallow copy.
|
||||||
|
absl::StatusOr<Packet<Image>> FromImageFrame(Packet<ImageFrame> packet) {
|
||||||
|
return MakePacket<Image, std::shared_ptr<mediapipe::ImageFrame>>(
|
||||||
|
std::const_pointer_cast<mediapipe::ImageFrame>(
|
||||||
|
SharedPtrWithPacket<mediapipe::ImageFrame>(packet)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap texture pointer; shallow copy.
|
||||||
|
absl::StatusOr<Packet<Image>> FromGpuBuffer(Packet<GpuBuffer> packet) {
|
||||||
|
#if !MEDIAPIPE_DISABLE_GPU
|
||||||
|
const GpuBuffer& buffer = *packet;
|
||||||
|
return MakePacket<Image, const GpuBuffer&>(buffer);
|
||||||
|
#else
|
||||||
|
return absl::UnimplementedError("GPU processing is disabled in build flags");
|
||||||
|
#endif // !MEDIAPIPE_DISABLE_GPU
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::StatusOr<Packet<Image>> ToImageCalculator::GetInputImage(
|
||||||
|
CalculatorContext* cc) {
|
||||||
|
if (kIn(cc).IsConnected()) {
|
||||||
|
return kIn(cc).Visit(
|
||||||
|
[&](const mediapipe::Image&) {
|
||||||
|
return absl::StatusOr<Packet<Image>>(kIn(cc).As<Image>());
|
||||||
|
},
|
||||||
|
[&](const mediapipe::ImageFrame&) {
|
||||||
|
return FromImageFrame(kIn(cc).As<ImageFrame>());
|
||||||
|
},
|
||||||
|
[&](const GpuBuffer&) {
|
||||||
|
return FromGpuBuffer(kIn(cc).As<GpuBuffer>());
|
||||||
|
});
|
||||||
|
} else if (kInCpu(cc).IsConnected()) {
|
||||||
|
return FromImageFrame(kInCpu(cc).As<ImageFrame>());
|
||||||
|
} else if (kInGpu(cc).IsConnected()) {
|
||||||
|
return FromGpuBuffer(kInGpu(cc).As<GpuBuffer>());
|
||||||
|
}
|
||||||
|
return absl::InvalidArgumentError("No input found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace api2
|
||||||
} // namespace mediapipe
|
} // namespace mediapipe
|
||||||
|
|||||||
@@ -12,5 +12,12 @@ cd /d %~dp0
|
|||||||
cd facemesh\src\main
|
cd facemesh\src\main
|
||||||
rm res
|
rm res
|
||||||
mklink /d res ..\..\..\res
|
mklink /d res ..\..\..\res
|
||||||
|
|
||||||
|
@rem for face detection example app.
|
||||||
|
cd /d %~dp0
|
||||||
|
cd facedetection\src\main
|
||||||
|
rm res
|
||||||
|
mklink /d res ..\..\..\res
|
||||||
|
|
||||||
dir
|
dir
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 30
|
||||||
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.google.mediapipe.apps.facedetection"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 30
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||||
|
testImplementation 'junit:junit:4.+'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
|
// MediaPipe Face Detection Solution.
|
||||||
|
implementation 'com.google.mediapipe:solution-core:latest.release'
|
||||||
|
implementation 'com.google.mediapipe:facedetection:latest.release'
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.google.mediapipe.examples.facedetection">
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="21"
|
||||||
|
android:targetSdkVersion="30" />
|
||||||
|
|
||||||
|
<!-- For loading images from gallery -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<!-- For using the camera -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<!-- For logging solution events -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="MediaPipe Face Detection"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<activity android:name=".MainActivity"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Copyright 2021 The MediaPipe 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
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:private"])
|
||||||
|
|
||||||
|
android_binary(
|
||||||
|
name = "facedetection",
|
||||||
|
srcs = glob(["**/*.java"]),
|
||||||
|
custom_package = "com.google.mediapipe.examples.facedetection",
|
||||||
|
manifest = "AndroidManifest.xml",
|
||||||
|
manifest_values = {
|
||||||
|
"applicationId": "com.google.mediapipe.examples.facedetection",
|
||||||
|
},
|
||||||
|
multidex = "native",
|
||||||
|
resource_files = ["//mediapipe/examples/android/solutions:resource_files"],
|
||||||
|
deps = [
|
||||||
|
"//mediapipe/framework/formats:detection_java_proto_lite",
|
||||||
|
"//mediapipe/framework/formats:location_data_java_proto_lite",
|
||||||
|
"//mediapipe/java/com/google/mediapipe/solutioncore:camera_input",
|
||||||
|
"//mediapipe/java/com/google/mediapipe/solutioncore:mediapipe_jni_lib",
|
||||||
|
"//mediapipe/java/com/google/mediapipe/solutioncore:solution_rendering",
|
||||||
|
"//mediapipe/java/com/google/mediapipe/solutioncore:video_input",
|
||||||
|
"//mediapipe/java/com/google/mediapipe/solutions/facedetection",
|
||||||
|
"//third_party:androidx_appcompat",
|
||||||
|
"//third_party:androidx_constraint_layout",
|
||||||
|
"//third_party:opencv",
|
||||||
|
"@maven//:androidx_activity_activity",
|
||||||
|
"@maven//:androidx_concurrent_concurrent_futures",
|
||||||
|
"@maven//:androidx_exifinterface_exifinterface",
|
||||||
|
"@maven//:androidx_fragment_fragment",
|
||||||
|
"@maven//:com_google_guava_guava",
|
||||||
|
],
|
||||||
|
)
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.facedetection;
|
||||||
|
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import com.google.mediapipe.formats.proto.DetectionProto.Detection;
|
||||||
|
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceDetectionResult;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceKeypoint;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
|
||||||
|
/** A custom implementation of {@link ResultGlRenderer} to render {@link FaceDetectionResult}. */
|
||||||
|
public class FaceDetectionResultGlRenderer implements ResultGlRenderer<FaceDetectionResult> {
|
||||||
|
private static final String TAG = "FaceDetectionResultGlRenderer";
|
||||||
|
|
||||||
|
private static final float[] KEYPOINT_COLOR = new float[] {1f, 0f, 0f, 1f};
|
||||||
|
private static final float KEYPOINT_SIZE = 16f;
|
||||||
|
private static final float[] BBOX_COLOR = new float[] {0f, 1f, 0f, 1f};
|
||||||
|
private static final int BBOX_THICKNESS = 8;
|
||||||
|
private static final String VERTEX_SHADER =
|
||||||
|
"uniform mat4 uProjectionMatrix;\n"
|
||||||
|
+ "uniform float uPointSize;\n"
|
||||||
|
+ "attribute vec4 vPosition;\n"
|
||||||
|
+ "void main() {\n"
|
||||||
|
+ " gl_Position = uProjectionMatrix * vPosition;\n"
|
||||||
|
+ " gl_PointSize = uPointSize;"
|
||||||
|
+ "}";
|
||||||
|
private static final String FRAGMENT_SHADER =
|
||||||
|
"precision mediump float;\n"
|
||||||
|
+ "uniform vec4 uColor;\n"
|
||||||
|
+ "void main() {\n"
|
||||||
|
+ " gl_FragColor = uColor;\n"
|
||||||
|
+ "}";
|
||||||
|
private int program;
|
||||||
|
private int positionHandle;
|
||||||
|
private int pointSizeHandle;
|
||||||
|
private int projectionMatrixHandle;
|
||||||
|
private int colorHandle;
|
||||||
|
|
||||||
|
private int loadShader(int type, String shaderCode) {
|
||||||
|
int shader = GLES20.glCreateShader(type);
|
||||||
|
GLES20.glShaderSource(shader, shaderCode);
|
||||||
|
GLES20.glCompileShader(shader);
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setupRendering() {
|
||||||
|
program = GLES20.glCreateProgram();
|
||||||
|
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
|
||||||
|
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
|
||||||
|
GLES20.glAttachShader(program, vertexShader);
|
||||||
|
GLES20.glAttachShader(program, fragmentShader);
|
||||||
|
GLES20.glLinkProgram(program);
|
||||||
|
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
||||||
|
pointSizeHandle = GLES20.glGetUniformLocation(program, "uPointSize");
|
||||||
|
projectionMatrixHandle = GLES20.glGetUniformLocation(program, "uProjectionMatrix");
|
||||||
|
colorHandle = GLES20.glGetUniformLocation(program, "uColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderResult(FaceDetectionResult result, float[] projectionMatrix) {
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLES20.glUseProgram(program);
|
||||||
|
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
|
||||||
|
GLES20.glUniform1f(pointSizeHandle, KEYPOINT_SIZE);
|
||||||
|
int numDetectedFaces = result.multiFaceDetections().size();
|
||||||
|
for (int i = 0; i < numDetectedFaces; ++i) {
|
||||||
|
drawDetection(result.multiFaceDetections().get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the shader program.
|
||||||
|
*
|
||||||
|
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
||||||
|
*/
|
||||||
|
public void release() {
|
||||||
|
GLES20.glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawDetection(Detection detection) {
|
||||||
|
if (!detection.hasLocationData()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Draw keypoints.
|
||||||
|
float[] points = new float[FaceKeypoint.NUM_KEY_POINTS * 2];
|
||||||
|
for (int i = 0; i < FaceKeypoint.NUM_KEY_POINTS; ++i) {
|
||||||
|
points[2 * i] = detection.getLocationData().getRelativeKeypoints(i).getX();
|
||||||
|
points[2 * i + 1] = detection.getLocationData().getRelativeKeypoints(i).getY();
|
||||||
|
}
|
||||||
|
GLES20.glUniform4fv(colorHandle, 1, KEYPOINT_COLOR, 0);
|
||||||
|
FloatBuffer vertexBuffer =
|
||||||
|
ByteBuffer.allocateDirect(points.length * 4)
|
||||||
|
.order(ByteOrder.nativeOrder())
|
||||||
|
.asFloatBuffer()
|
||||||
|
.put(points);
|
||||||
|
vertexBuffer.position(0);
|
||||||
|
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||||
|
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, FaceKeypoint.NUM_KEY_POINTS);
|
||||||
|
if (!detection.getLocationData().hasRelativeBoundingBox()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Draw bounding box.
|
||||||
|
float left = detection.getLocationData().getRelativeBoundingBox().getXmin();
|
||||||
|
float top = detection.getLocationData().getRelativeBoundingBox().getYmin();
|
||||||
|
float right = left + detection.getLocationData().getRelativeBoundingBox().getWidth();
|
||||||
|
float bottom = top + detection.getLocationData().getRelativeBoundingBox().getHeight();
|
||||||
|
drawLine(top, left, top, right);
|
||||||
|
drawLine(bottom, left, bottom, right);
|
||||||
|
drawLine(top, left, bottom, left);
|
||||||
|
drawLine(top, right, bottom, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLine(float y1, float x1, float y2, float x2) {
|
||||||
|
GLES20.glUniform4fv(colorHandle, 1, BBOX_COLOR, 0);
|
||||||
|
GLES20.glLineWidth(BBOX_THICKNESS);
|
||||||
|
float[] vertex = {x1, y1, x2, y2};
|
||||||
|
FloatBuffer vertexBuffer =
|
||||||
|
ByteBuffer.allocateDirect(vertex.length * 4)
|
||||||
|
.order(ByteOrder.nativeOrder())
|
||||||
|
.asFloatBuffer()
|
||||||
|
.put(vertex);
|
||||||
|
vertexBuffer.position(0);
|
||||||
|
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||||
|
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_LINES, 0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.facedetection;
|
||||||
|
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView;
|
||||||
|
import com.google.mediapipe.formats.proto.DetectionProto.Detection;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceDetectionResult;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceKeypoint;
|
||||||
|
|
||||||
|
/** An ImageView implementation for displaying {@link FaceDetectionResult}. */
|
||||||
|
public class FaceDetectionResultImageView extends AppCompatImageView {
|
||||||
|
private static final String TAG = "FaceDetectionResultImageView";
|
||||||
|
|
||||||
|
private static final int KEYPOINT_COLOR = Color.RED;
|
||||||
|
private static final int KEYPOINT_RADIUS = 8; // Pixels
|
||||||
|
private static final int BBOX_COLOR = Color.GREEN;
|
||||||
|
private static final int BBOX_THICKNESS = 5; // Pixels
|
||||||
|
private Bitmap latest;
|
||||||
|
|
||||||
|
public FaceDetectionResultImageView(Context context) {
|
||||||
|
super(context);
|
||||||
|
setScaleType(AppCompatImageView.ScaleType.FIT_CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a {@link FaceDetectionResult} to render.
|
||||||
|
*
|
||||||
|
* @param result a {@link FaceDetectionResult} object that contains the solution outputs and the
|
||||||
|
* input {@link Bitmap}.
|
||||||
|
*/
|
||||||
|
public void setFaceDetectionResult(FaceDetectionResult result) {
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bitmap bmInput = result.inputBitmap();
|
||||||
|
int width = bmInput.getWidth();
|
||||||
|
int height = bmInput.getHeight();
|
||||||
|
latest = Bitmap.createBitmap(width, height, bmInput.getConfig());
|
||||||
|
Canvas canvas = new Canvas(latest);
|
||||||
|
|
||||||
|
canvas.drawBitmap(bmInput, new Matrix(), null);
|
||||||
|
int numDetectedFaces = result.multiFaceDetections().size();
|
||||||
|
for (int i = 0; i < numDetectedFaces; ++i) {
|
||||||
|
drawDetectionOnCanvas(result.multiFaceDetections().get(i), canvas, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the image view with the latest {@link FaceDetectionResult}. */
|
||||||
|
public void update() {
|
||||||
|
postInvalidate();
|
||||||
|
if (latest != null) {
|
||||||
|
setImageBitmap(latest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawDetectionOnCanvas(Detection detection, Canvas canvas, int width, int height) {
|
||||||
|
if (!detection.hasLocationData()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Draw keypoints.
|
||||||
|
Paint keypointPaint = new Paint();
|
||||||
|
keypointPaint.setColor(KEYPOINT_COLOR);
|
||||||
|
for (int i = 0; i < FaceKeypoint.NUM_KEY_POINTS; ++i) {
|
||||||
|
int xPixel =
|
||||||
|
min(
|
||||||
|
(int) (detection.getLocationData().getRelativeKeypoints(i).getX() * width),
|
||||||
|
width - 1);
|
||||||
|
int yPixel =
|
||||||
|
min(
|
||||||
|
(int) (detection.getLocationData().getRelativeKeypoints(i).getY() * height),
|
||||||
|
height - 1);
|
||||||
|
canvas.drawCircle(xPixel, yPixel, KEYPOINT_RADIUS, keypointPaint);
|
||||||
|
}
|
||||||
|
if (!detection.getLocationData().hasRelativeBoundingBox()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Draw bounding box.
|
||||||
|
Paint bboxPaint = new Paint();
|
||||||
|
bboxPaint.setColor(BBOX_COLOR);
|
||||||
|
bboxPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
bboxPaint.setStrokeWidth(BBOX_THICKNESS);
|
||||||
|
float left = detection.getLocationData().getRelativeBoundingBox().getXmin() * width;
|
||||||
|
float top = detection.getLocationData().getRelativeBoundingBox().getYmin() * height;
|
||||||
|
float right = left + detection.getLocationData().getRelativeBoundingBox().getWidth() * width;
|
||||||
|
float bottom = top + detection.getLocationData().getRelativeBoundingBox().getHeight() * height;
|
||||||
|
canvas.drawRect(left, top, right, bottom, bboxPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
+364
@@ -0,0 +1,364 @@
|
|||||||
|
// Copyright 2021 The MediaPipe 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
|
||||||
|
//
|
||||||
|
// 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.facedetection;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.exifinterface.media.ExifInterface;
|
||||||
|
// ContentResolver dependency
|
||||||
|
import com.google.mediapipe.solutioncore.CameraInput;
|
||||||
|
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
||||||
|
import com.google.mediapipe.solutioncore.VideoInput;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceDetection;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceDetectionOptions;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceDetectionResult;
|
||||||
|
import com.google.mediapipe.solutions.facedetection.FaceKeypoint;
|
||||||
|
import com.google.mediapipe.formats.proto.LocationDataProto.LocationData.RelativeKeypoint;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/** Main activity of MediaPipe Face Detection app. */
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
private static final String TAG = "MainActivity";
|
||||||
|
|
||||||
|
private FaceDetection faceDetection;
|
||||||
|
|
||||||
|
private enum InputSource {
|
||||||
|
UNKNOWN,
|
||||||
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
|
CAMERA,
|
||||||
|
}
|
||||||
|
private InputSource inputSource = InputSource.UNKNOWN;
|
||||||
|
|
||||||
|
// Image demo UI and image loader components.
|
||||||
|
private ActivityResultLauncher<Intent> imageGetter;
|
||||||
|
private FaceDetectionResultImageView imageView;
|
||||||
|
// Video demo UI and video loader components.
|
||||||
|
private VideoInput videoInput;
|
||||||
|
private ActivityResultLauncher<Intent> videoGetter;
|
||||||
|
// Live camera demo UI and camera components.
|
||||||
|
private CameraInput cameraInput;
|
||||||
|
|
||||||
|
private SolutionGlSurfaceView<FaceDetectionResult> glSurfaceView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
setupStaticImageDemoUiComponents();
|
||||||
|
setupVideoDemoUiComponents();
|
||||||
|
setupLiveDemoUiComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (inputSource == InputSource.CAMERA) {
|
||||||
|
// Restarts the camera and the opengl surface rendering.
|
||||||
|
cameraInput = new CameraInput(this);
|
||||||
|
cameraInput.setNewFrameListener(textureFrame -> faceDetection.send(textureFrame));
|
||||||
|
glSurfaceView.post(this::startCamera);
|
||||||
|
glSurfaceView.setVisibility(View.VISIBLE);
|
||||||
|
} else if (inputSource == InputSource.VIDEO) {
|
||||||
|
videoInput.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (inputSource == InputSource.CAMERA) {
|
||||||
|
glSurfaceView.setVisibility(View.GONE);
|
||||||
|
cameraInput.close();
|
||||||
|
} else if (inputSource == InputSource.VIDEO) {
|
||||||
|
videoInput.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap downscaleBitmap(Bitmap originalBitmap) {
|
||||||
|
double aspectRatio = (double) originalBitmap.getWidth() / originalBitmap.getHeight();
|
||||||
|
int width = imageView.getWidth();
|
||||||
|
int height = imageView.getHeight();
|
||||||
|
if (((double) imageView.getWidth() / imageView.getHeight()) > aspectRatio) {
|
||||||
|
width = (int) (height * aspectRatio);
|
||||||
|
} else {
|
||||||
|
height = (int) (width / aspectRatio);
|
||||||
|
}
|
||||||
|
return Bitmap.createScaledBitmap(originalBitmap, width, height, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap rotateBitmap(Bitmap inputBitmap, InputStream imageData) throws IOException {
|
||||||
|
int orientation =
|
||||||
|
new ExifInterface(imageData)
|
||||||
|
.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||||
|
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
|
||||||
|
return inputBitmap;
|
||||||
|
}
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
switch (orientation) {
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||||
|
matrix.postRotate(90);
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||||
|
matrix.postRotate(180);
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||||
|
matrix.postRotate(270);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
matrix.postRotate(0);
|
||||||
|
}
|
||||||
|
return Bitmap.createBitmap(
|
||||||
|
inputBitmap, 0, 0, inputBitmap.getWidth(), inputBitmap.getHeight(), matrix, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets up the UI components for the static image demo. */
|
||||||
|
private void setupStaticImageDemoUiComponents() {
|
||||||
|
// The Intent to access gallery and read images as bitmap.
|
||||||
|
imageGetter =
|
||||||
|
registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
Intent resultIntent = result.getData();
|
||||||
|
if (resultIntent != null) {
|
||||||
|
if (result.getResultCode() == RESULT_OK) {
|
||||||
|
Bitmap bitmap = null;
|
||||||
|
try {
|
||||||
|
bitmap =
|
||||||
|
downscaleBitmap(
|
||||||
|
MediaStore.Images.Media.getBitmap(
|
||||||
|
this.getContentResolver(), resultIntent.getData()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Bitmap reading error:" + e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
InputStream imageData =
|
||||||
|
this.getContentResolver().openInputStream(resultIntent.getData());
|
||||||
|
bitmap = rotateBitmap(bitmap, imageData);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Bitmap rotation error:" + e);
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
faceDetection.send(bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Button loadImageButton = findViewById(R.id.button_load_picture);
|
||||||
|
loadImageButton.setOnClickListener(
|
||||||
|
v -> {
|
||||||
|
if (inputSource != InputSource.IMAGE) {
|
||||||
|
stopCurrentPipeline();
|
||||||
|
setupStaticImageModePipeline();
|
||||||
|
}
|
||||||
|
// Reads images from gallery.
|
||||||
|
Intent pickImageIntent = new Intent(Intent.ACTION_PICK);
|
||||||
|
pickImageIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||||
|
imageGetter.launch(pickImageIntent);
|
||||||
|
});
|
||||||
|
imageView = new FaceDetectionResultImageView(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets up core workflow for static image mode. */
|
||||||
|
private void setupStaticImageModePipeline() {
|
||||||
|
this.inputSource = InputSource.IMAGE;
|
||||||
|
// Initializes a new MediaPipe Face Detection solution instance in the static image mode.
|
||||||
|
faceDetection =
|
||||||
|
new FaceDetection(
|
||||||
|
this,
|
||||||
|
FaceDetectionOptions.builder()
|
||||||
|
.setStaticImageMode(true)
|
||||||
|
.setModelSelection(0)
|
||||||
|
.setMinDetectionConfidence(0.5f)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Connects MediaPipe Face Detection solution to the user-defined FaceDetectionResultImageView.
|
||||||
|
faceDetection.setResultListener(
|
||||||
|
faceDetectionResult -> {
|
||||||
|
logNoseTipKeypoint(faceDetectionResult, /*faceIndex=*/ 0, /*showPixelValues=*/ true);
|
||||||
|
imageView.setFaceDetectionResult(faceDetectionResult);
|
||||||
|
runOnUiThread(() -> imageView.update());
|
||||||
|
});
|
||||||
|
faceDetection.setErrorListener(
|
||||||
|
(message, e) -> Log.e(TAG, "MediaPipe Face Detection error:" + message));
|
||||||
|
|
||||||
|
// Updates the preview layout.
|
||||||
|
FrameLayout frameLayout = findViewById(R.id.preview_display_layout);
|
||||||
|
frameLayout.removeAllViewsInLayout();
|
||||||
|
imageView.setImageDrawable(null);
|
||||||
|
frameLayout.addView(imageView);
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets up the UI components for the video demo. */
|
||||||
|
private void setupVideoDemoUiComponents() {
|
||||||
|
// The Intent to access gallery and read a video file.
|
||||||
|
videoGetter =
|
||||||
|
registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
Intent resultIntent = result.getData();
|
||||||
|
if (resultIntent != null) {
|
||||||
|
if (result.getResultCode() == RESULT_OK) {
|
||||||
|
glSurfaceView.post(
|
||||||
|
() ->
|
||||||
|
videoInput.start(
|
||||||
|
this,
|
||||||
|
resultIntent.getData(),
|
||||||
|
faceDetection.getGlContext(),
|
||||||
|
glSurfaceView.getWidth(),
|
||||||
|
glSurfaceView.getHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Button loadVideoButton = findViewById(R.id.button_load_video);
|
||||||
|
loadVideoButton.setOnClickListener(
|
||||||
|
v -> {
|
||||||
|
stopCurrentPipeline();
|
||||||
|
setupStreamingModePipeline(InputSource.VIDEO);
|
||||||
|
// Reads video from gallery.
|
||||||
|
Intent pickVideoIntent = new Intent(Intent.ACTION_PICK);
|
||||||
|
pickVideoIntent.setDataAndType(MediaStore.Video.Media.INTERNAL_CONTENT_URI, "video/*");
|
||||||
|
videoGetter.launch(pickVideoIntent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets up the UI components for the live demo with camera input. */
|
||||||
|
private void setupLiveDemoUiComponents() {
|
||||||
|
Button startCameraButton = findViewById(R.id.button_start_camera);
|
||||||
|
startCameraButton.setOnClickListener(
|
||||||
|
v -> {
|
||||||
|
if (inputSource == InputSource.CAMERA) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopCurrentPipeline();
|
||||||
|
setupStreamingModePipeline(InputSource.CAMERA);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets up core workflow for streaming mode. */
|
||||||
|
private void setupStreamingModePipeline(InputSource inputSource) {
|
||||||
|
this.inputSource = inputSource;
|
||||||
|
// Initializes a new MediaPipe Face Detection solution instance in the streaming mode.
|
||||||
|
faceDetection =
|
||||||
|
new FaceDetection(
|
||||||
|
this,
|
||||||
|
FaceDetectionOptions.builder().setStaticImageMode(false).setModelSelection(0).build());
|
||||||
|
faceDetection.setErrorListener(
|
||||||
|
(message, e) -> Log.e(TAG, "MediaPipe Face Detection error:" + message));
|
||||||
|
|
||||||
|
if (inputSource == InputSource.CAMERA) {
|
||||||
|
cameraInput = new CameraInput(this);
|
||||||
|
cameraInput.setNewFrameListener(textureFrame -> faceDetection.send(textureFrame));
|
||||||
|
} else if (inputSource == InputSource.VIDEO) {
|
||||||
|
videoInput = new VideoInput(this);
|
||||||
|
videoInput.setNewFrameListener(textureFrame -> faceDetection.send(textureFrame));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes a new Gl surface view with a user-defined FaceDetectionResultGlRenderer.
|
||||||
|
glSurfaceView =
|
||||||
|
new SolutionGlSurfaceView<>(
|
||||||
|
this, faceDetection.getGlContext(), faceDetection.getGlMajorVersion());
|
||||||
|
glSurfaceView.setSolutionResultRenderer(new FaceDetectionResultGlRenderer());
|
||||||
|
glSurfaceView.setRenderInputImage(true);
|
||||||
|
faceDetection.setResultListener(
|
||||||
|
faceDetectionResult -> {
|
||||||
|
logNoseTipKeypoint(faceDetectionResult, /*faceIndex=*/ 0, /*showPixelValues=*/ false);
|
||||||
|
glSurfaceView.setRenderData(faceDetectionResult);
|
||||||
|
glSurfaceView.requestRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The runnable to start camera after the gl surface view is attached.
|
||||||
|
// For video input source, videoInput.start() will be called when the video uri is available.
|
||||||
|
if (inputSource == InputSource.CAMERA) {
|
||||||
|
glSurfaceView.post(this::startCamera);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the preview layout.
|
||||||
|
FrameLayout frameLayout = findViewById(R.id.preview_display_layout);
|
||||||
|
imageView.setVisibility(View.GONE);
|
||||||
|
frameLayout.removeAllViewsInLayout();
|
||||||
|
frameLayout.addView(glSurfaceView);
|
||||||
|
glSurfaceView.setVisibility(View.VISIBLE);
|
||||||
|
frameLayout.requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCamera() {
|
||||||
|
cameraInput.start(
|
||||||
|
this,
|
||||||
|
faceDetection.getGlContext(),
|
||||||
|
CameraInput.CameraFacing.FRONT,
|
||||||
|
glSurfaceView.getWidth(),
|
||||||
|
glSurfaceView.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopCurrentPipeline() {
|
||||||
|
if (cameraInput != null) {
|
||||||
|
cameraInput.setNewFrameListener(null);
|
||||||
|
cameraInput.close();
|
||||||
|
}
|
||||||
|
if (videoInput != null) {
|
||||||
|
videoInput.setNewFrameListener(null);
|
||||||
|
videoInput.close();
|
||||||
|
}
|
||||||
|
if (glSurfaceView != null) {
|
||||||
|
glSurfaceView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
if (faceDetection != null) {
|
||||||
|
faceDetection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logNoseTipKeypoint(
|
||||||
|
FaceDetectionResult result, int faceIndex, boolean showPixelValues) {
|
||||||
|
if (result.multiFaceDetections().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RelativeKeypoint noseTip =
|
||||||
|
result
|
||||||
|
.multiFaceDetections()
|
||||||
|
.get(faceIndex)
|
||||||
|
.getLocationData()
|
||||||
|
.getRelativeKeypoints(FaceKeypoint.NOSE_TIP);
|
||||||
|
// For Bitmaps, show the pixel values. For texture inputs, show the normalized coordinates.
|
||||||
|
if (showPixelValues) {
|
||||||
|
int width = result.inputBitmap().getWidth();
|
||||||
|
int height = result.inputBitmap().getHeight();
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaPipe Face Detection nose tip coordinates (pixel values): x=%f, y=%f",
|
||||||
|
noseTip.getX() * width, noseTip.getY() * height));
|
||||||
|
} else {
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaPipe Face Detection nose tip normalized coordinates (value range: [0, 1]):"
|
||||||
|
+ " x=%f, y=%f",
|
||||||
|
noseTip.getX(), noseTip.getY()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../res
|
||||||
@@ -7,7 +7,7 @@ android {
|
|||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.google.mediapipe.apps.hands"
|
applicationId "com.google.mediapipe.apps.facemesh"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
@@ -31,20 +31,11 @@ dependencies {
|
|||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||||
testImplementation 'junit:junit:4.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
// MediaPipe hands solution API and solution-core.
|
// MediaPipe Face Mesh Solution.
|
||||||
implementation 'com.google.mediapipe:solution-core:latest.release'
|
implementation 'com.google.mediapipe:solution-core:latest.release'
|
||||||
implementation 'com.google.mediapipe:facemesh:latest.release'
|
implementation 'com.google.mediapipe:facemesh:latest.release'
|
||||||
// MediaPipe deps
|
|
||||||
implementation 'com.google.flogger:flogger:latest.release'
|
|
||||||
implementation 'com.google.flogger:flogger-system-backend:latest.release'
|
|
||||||
implementation 'com.google.guava:guava:27.0.1-android'
|
|
||||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
|
||||||
// CameraX core library
|
|
||||||
def camerax_version = "1.0.0-beta10"
|
|
||||||
implementation "androidx.camera:camera-core:$camerax_version"
|
|
||||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
|
||||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,14 @@
|
|||||||
<!-- For using the camera -->
|
<!-- For using the camera -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<!-- For logging solution events -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="MediaPipe FaceMesh"
|
android:label="MediaPipe Face Mesh"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ android_binary(
|
|||||||
"//third_party:opencv",
|
"//third_party:opencv",
|
||||||
"@maven//:androidx_activity_activity",
|
"@maven//:androidx_activity_activity",
|
||||||
"@maven//:androidx_concurrent_concurrent_futures",
|
"@maven//:androidx_concurrent_concurrent_futures",
|
||||||
|
"@maven//:androidx_exifinterface_exifinterface",
|
||||||
"@maven//:androidx_fragment_fragment",
|
"@maven//:androidx_fragment_fragment",
|
||||||
"@maven//:com_google_guava_guava",
|
"@maven//:com_google_guava_guava",
|
||||||
],
|
],
|
||||||
|
|||||||
+24
-34
@@ -15,11 +15,10 @@
|
|||||||
package com.google.mediapipe.examples.facemesh;
|
package com.google.mediapipe.examples.facemesh;
|
||||||
|
|
||||||
import android.opengl.GLES20;
|
import android.opengl.GLES20;
|
||||||
import android.opengl.Matrix;
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||||
import com.google.mediapipe.solutioncore.ResultGlBoundary;
|
|
||||||
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
||||||
|
import com.google.mediapipe.solutions.facemesh.FaceMesh;
|
||||||
import com.google.mediapipe.solutions.facemesh.FaceMeshConnections;
|
import com.google.mediapipe.solutions.facemesh.FaceMeshConnections;
|
||||||
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
@@ -27,7 +26,7 @@ import java.nio.ByteOrder;
|
|||||||
import java.nio.FloatBuffer;
|
import java.nio.FloatBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A custom implementation of {@link ResultGlRenderer} to render MediaPope FaceMesh results. */
|
/** A custom implementation of {@link ResultGlRenderer} to render {@link FaceMeshResult}. */
|
||||||
public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult> {
|
public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult> {
|
||||||
private static final String TAG = "FaceMeshResultGlRenderer";
|
private static final String TAG = "FaceMeshResultGlRenderer";
|
||||||
|
|
||||||
@@ -46,10 +45,10 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
|||||||
private static final float[] LIPS_COLOR = new float[] {0.9f, 0.9f, 0.9f, 1f};
|
private static final float[] LIPS_COLOR = new float[] {0.9f, 0.9f, 0.9f, 1f};
|
||||||
private static final int LIPS_THICKNESS = 8;
|
private static final int LIPS_THICKNESS = 8;
|
||||||
private static final String VERTEX_SHADER =
|
private static final String VERTEX_SHADER =
|
||||||
"uniform mat4 uTransformMatrix;\n"
|
"uniform mat4 uProjectionMatrix;\n"
|
||||||
+ "attribute vec4 vPosition;\n"
|
+ "attribute vec4 vPosition;\n"
|
||||||
+ "void main() {\n"
|
+ "void main() {\n"
|
||||||
+ " gl_Position = uTransformMatrix * vPosition;\n"
|
+ " gl_Position = uProjectionMatrix * vPosition;\n"
|
||||||
+ "}";
|
+ "}";
|
||||||
private static final String FRAGMENT_SHADER =
|
private static final String FRAGMENT_SHADER =
|
||||||
"precision mediump float;\n"
|
"precision mediump float;\n"
|
||||||
@@ -59,9 +58,8 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
|||||||
+ "}";
|
+ "}";
|
||||||
private int program;
|
private int program;
|
||||||
private int positionHandle;
|
private int positionHandle;
|
||||||
private int transformMatrixHandle;
|
private int projectionMatrixHandle;
|
||||||
private int colorHandle;
|
private int colorHandle;
|
||||||
private final float[] transformMatrix = new float[16];
|
|
||||||
|
|
||||||
private int loadShader(int type, String shaderCode) {
|
private int loadShader(int type, String shaderCode) {
|
||||||
int shader = GLES20.glCreateShader(type);
|
int shader = GLES20.glCreateShader(type);
|
||||||
@@ -79,28 +77,17 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
|||||||
GLES20.glAttachShader(program, fragmentShader);
|
GLES20.glAttachShader(program, fragmentShader);
|
||||||
GLES20.glLinkProgram(program);
|
GLES20.glLinkProgram(program);
|
||||||
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
||||||
transformMatrixHandle = GLES20.glGetUniformLocation(program, "uTransformMatrix");
|
projectionMatrixHandle = GLES20.glGetUniformLocation(program, "uProjectionMatrix");
|
||||||
colorHandle = GLES20.glGetUniformLocation(program, "uColor");
|
colorHandle = GLES20.glGetUniformLocation(program, "uColor");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void renderResult(FaceMeshResult result, ResultGlBoundary boundary) {
|
public void renderResult(FaceMeshResult result, float[] projectionMatrix) {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GLES20.glUseProgram(program);
|
GLES20.glUseProgram(program);
|
||||||
// Sets the transform matrix to align the result rendering with the scaled output texture.
|
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
|
||||||
// Also flips the rendering vertically since OpenGL assumes the coordinate origin is at the
|
|
||||||
// bottom-left corner, whereas MediaPipe landmark data assumes the coordinate origin is at the
|
|
||||||
// top-left corner.
|
|
||||||
Matrix.setIdentityM(transformMatrix, 0);
|
|
||||||
Matrix.scaleM(
|
|
||||||
transformMatrix,
|
|
||||||
0,
|
|
||||||
2 / (boundary.right() - boundary.left()),
|
|
||||||
-2 / (boundary.top() - boundary.bottom()),
|
|
||||||
1.0f);
|
|
||||||
GLES20.glUniformMatrix4fv(transformMatrixHandle, 1, false, transformMatrix, 0);
|
|
||||||
|
|
||||||
int numFaces = result.multiFaceLandmarks().size();
|
int numFaces = result.multiFaceLandmarks().size();
|
||||||
for (int i = 0; i < numFaces; ++i) {
|
for (int i = 0; i < numFaces; ++i) {
|
||||||
@@ -126,7 +113,7 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
|||||||
LEFT_EYE_THICKNESS);
|
LEFT_EYE_THICKNESS);
|
||||||
drawLandmarks(
|
drawLandmarks(
|
||||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||||
FaceMeshConnections.FACEMESH_LEFT_EYEBR0W,
|
FaceMeshConnections.FACEMESH_LEFT_EYEBROW,
|
||||||
LEFT_EYEBROW_COLOR,
|
LEFT_EYEBROW_COLOR,
|
||||||
LEFT_EYEBROW_THICKNESS);
|
LEFT_EYEBROW_THICKNESS);
|
||||||
drawLandmarks(
|
drawLandmarks(
|
||||||
@@ -139,11 +126,24 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
|||||||
FaceMeshConnections.FACEMESH_LIPS,
|
FaceMeshConnections.FACEMESH_LIPS,
|
||||||
LIPS_COLOR,
|
LIPS_COLOR,
|
||||||
LIPS_THICKNESS);
|
LIPS_THICKNESS);
|
||||||
|
if (result.multiFaceLandmarks().get(i).getLandmarkCount()
|
||||||
|
== FaceMesh.FACEMESH_NUM_LANDMARKS_WITH_IRISES) {
|
||||||
|
drawLandmarks(
|
||||||
|
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||||
|
FaceMeshConnections.FACEMESH_RIGHT_IRIS,
|
||||||
|
RIGHT_EYE_COLOR,
|
||||||
|
RIGHT_EYE_THICKNESS);
|
||||||
|
drawLandmarks(
|
||||||
|
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||||
|
FaceMeshConnections.FACEMESH_LEFT_IRIS,
|
||||||
|
LEFT_EYE_COLOR,
|
||||||
|
LEFT_EYE_THICKNESS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls this to delete the shader program.
|
* Deletes the shader program.
|
||||||
*
|
*
|
||||||
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
||||||
*/
|
*/
|
||||||
@@ -159,13 +159,9 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
|||||||
GLES20.glUniform4fv(colorHandle, 1, colorArray, 0);
|
GLES20.glUniform4fv(colorHandle, 1, colorArray, 0);
|
||||||
GLES20.glLineWidth(thickness);
|
GLES20.glLineWidth(thickness);
|
||||||
for (FaceMeshConnections.Connection c : connections) {
|
for (FaceMeshConnections.Connection c : connections) {
|
||||||
float[] vertex = new float[4];
|
|
||||||
NormalizedLandmark start = faceLandmarkList.get(c.start());
|
NormalizedLandmark start = faceLandmarkList.get(c.start());
|
||||||
vertex[0] = normalizedLandmarkValue(start.getX());
|
|
||||||
vertex[1] = normalizedLandmarkValue(start.getY());
|
|
||||||
NormalizedLandmark end = faceLandmarkList.get(c.end());
|
NormalizedLandmark end = faceLandmarkList.get(c.end());
|
||||||
vertex[2] = normalizedLandmarkValue(end.getX());
|
float[] vertex = {start.getX(), start.getY(), end.getX(), end.getY()};
|
||||||
vertex[3] = normalizedLandmarkValue(end.getY());
|
|
||||||
FloatBuffer vertexBuffer =
|
FloatBuffer vertexBuffer =
|
||||||
ByteBuffer.allocateDirect(vertex.length * 4)
|
ByteBuffer.allocateDirect(vertex.length * 4)
|
||||||
.order(ByteOrder.nativeOrder())
|
.order(ByteOrder.nativeOrder())
|
||||||
@@ -177,10 +173,4 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
|||||||
GLES20.glDrawArrays(GLES20.GL_LINES, 0, 2);
|
GLES20.glDrawArrays(GLES20.GL_LINES, 0, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalizes the value from the landmark value range:[0, 1] to the standard OpenGL coordinate
|
|
||||||
// value range: [-1, 1].
|
|
||||||
private float normalizedLandmarkValue(float value) {
|
|
||||||
return value * 2 - 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-11
@@ -24,28 +24,29 @@ import androidx.appcompat.widget.AppCompatImageView;
|
|||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||||
|
import com.google.mediapipe.solutions.facemesh.FaceMesh;
|
||||||
import com.google.mediapipe.solutions.facemesh.FaceMeshConnections;
|
import com.google.mediapipe.solutions.facemesh.FaceMeshConnections;
|
||||||
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** An ImageView implementation for displaying MediaPipe FaceMesh results. */
|
/** An ImageView implementation for displaying {@link FaceMeshResult}. */
|
||||||
public class FaceMeshResultImageView extends AppCompatImageView {
|
public class FaceMeshResultImageView extends AppCompatImageView {
|
||||||
private static final String TAG = "FaceMeshResultImageView";
|
private static final String TAG = "FaceMeshResultImageView";
|
||||||
|
|
||||||
private static final int TESSELATION_COLOR = Color.parseColor("#70C0C0C0");
|
private static final int TESSELATION_COLOR = Color.parseColor("#70C0C0C0");
|
||||||
private static final int TESSELATION_THICKNESS = 5;
|
private static final int TESSELATION_THICKNESS = 3; // Pixels
|
||||||
private static final int RIGHT_EYE_COLOR = Color.parseColor("#FF3030");
|
private static final int RIGHT_EYE_COLOR = Color.parseColor("#FF3030");
|
||||||
private static final int RIGHT_EYE_THICKNESS = 8;
|
private static final int RIGHT_EYE_THICKNESS = 5; // Pixels
|
||||||
private static final int RIGHT_EYEBROW_COLOR = Color.parseColor("#FF3030");
|
private static final int RIGHT_EYEBROW_COLOR = Color.parseColor("#FF3030");
|
||||||
private static final int RIGHT_EYEBROW_THICKNESS = 8;
|
private static final int RIGHT_EYEBROW_THICKNESS = 5; // Pixels
|
||||||
private static final int LEFT_EYE_COLOR = Color.parseColor("#30FF30");
|
private static final int LEFT_EYE_COLOR = Color.parseColor("#30FF30");
|
||||||
private static final int LEFT_EYE_THICKNESS = 8;
|
private static final int LEFT_EYE_THICKNESS = 5; // Pixels
|
||||||
private static final int LEFT_EYEBROW_COLOR = Color.parseColor("#30FF30");
|
private static final int LEFT_EYEBROW_COLOR = Color.parseColor("#30FF30");
|
||||||
private static final int LEFT_EYEBROW_THICKNESS = 8;
|
private static final int LEFT_EYEBROW_THICKNESS = 5; // Pixels
|
||||||
private static final int FACE_OVAL_COLOR = Color.parseColor("#E0E0E0");
|
private static final int FACE_OVAL_COLOR = Color.parseColor("#E0E0E0");
|
||||||
private static final int FACE_OVAL_THICKNESS = 8;
|
private static final int FACE_OVAL_THICKNESS = 5; // Pixels
|
||||||
private static final int LIPS_COLOR = Color.parseColor("#E0E0E0");
|
private static final int LIPS_COLOR = Color.parseColor("#E0E0E0");
|
||||||
private static final int LIPS_THICKNESS = 8;
|
private static final int LIPS_THICKNESS = 5; // Pixels
|
||||||
private Bitmap latest;
|
private Bitmap latest;
|
||||||
|
|
||||||
public FaceMeshResultImageView(Context context) {
|
public FaceMeshResultImageView(Context context) {
|
||||||
@@ -103,7 +104,7 @@ public class FaceMeshResultImageView extends AppCompatImageView {
|
|||||||
drawLandmarksOnCanvas(
|
drawLandmarksOnCanvas(
|
||||||
canvas,
|
canvas,
|
||||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||||
FaceMeshConnections.FACEMESH_LEFT_EYEBR0W,
|
FaceMeshConnections.FACEMESH_LEFT_EYEBROW,
|
||||||
imageSize,
|
imageSize,
|
||||||
LEFT_EYEBROW_COLOR,
|
LEFT_EYEBROW_COLOR,
|
||||||
LEFT_EYEBROW_THICKNESS);
|
LEFT_EYEBROW_THICKNESS);
|
||||||
@@ -121,10 +122,27 @@ public class FaceMeshResultImageView extends AppCompatImageView {
|
|||||||
imageSize,
|
imageSize,
|
||||||
LIPS_COLOR,
|
LIPS_COLOR,
|
||||||
LIPS_THICKNESS);
|
LIPS_THICKNESS);
|
||||||
|
if (result.multiFaceLandmarks().get(i).getLandmarkCount()
|
||||||
|
== FaceMesh.FACEMESH_NUM_LANDMARKS_WITH_IRISES) {
|
||||||
|
drawLandmarksOnCanvas(
|
||||||
|
canvas,
|
||||||
|
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||||
|
FaceMeshConnections.FACEMESH_RIGHT_IRIS,
|
||||||
|
imageSize,
|
||||||
|
RIGHT_EYE_COLOR,
|
||||||
|
RIGHT_EYE_THICKNESS);
|
||||||
|
drawLandmarksOnCanvas(
|
||||||
|
canvas,
|
||||||
|
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||||
|
FaceMeshConnections.FACEMESH_LEFT_IRIS,
|
||||||
|
imageSize,
|
||||||
|
LEFT_EYE_COLOR,
|
||||||
|
LEFT_EYE_THICKNESS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the image view with the latest facemesh result. */
|
/** Updates the image view with the latest {@link FaceMeshResult}. */
|
||||||
public void update() {
|
public void update() {
|
||||||
postInvalidate();
|
postInvalidate();
|
||||||
if (latest != null) {
|
if (latest != null) {
|
||||||
@@ -132,7 +150,6 @@ public class FaceMeshResultImageView extends AppCompatImageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Better hand landmark and hand connection drawing.
|
|
||||||
private void drawLandmarksOnCanvas(
|
private void drawLandmarksOnCanvas(
|
||||||
Canvas canvas,
|
Canvas canvas,
|
||||||
List<NormalizedLandmark> faceLandmarkList,
|
List<NormalizedLandmark> faceLandmarkList,
|
||||||
|
|||||||
+73
-22
@@ -16,6 +16,7 @@ package com.google.mediapipe.examples.facemesh;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -25,6 +26,8 @@ import android.widget.Button;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.exifinterface.media.ExifInterface;
|
||||||
|
// ContentResolver dependency
|
||||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||||
import com.google.mediapipe.solutioncore.CameraInput;
|
import com.google.mediapipe.solutioncore.CameraInput;
|
||||||
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
||||||
@@ -33,8 +36,9 @@ import com.google.mediapipe.solutions.facemesh.FaceMesh;
|
|||||||
import com.google.mediapipe.solutions.facemesh.FaceMeshOptions;
|
import com.google.mediapipe.solutions.facemesh.FaceMeshOptions;
|
||||||
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/** Main activity of MediaPipe FaceMesh app. */
|
/** Main activity of MediaPipe Face Mesh app. */
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
private static final String TAG = "MainActivity";
|
private static final String TAG = "MainActivity";
|
||||||
|
|
||||||
@@ -57,12 +61,14 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private ActivityResultLauncher<Intent> videoGetter;
|
private ActivityResultLauncher<Intent> videoGetter;
|
||||||
// Live camera demo UI and camera components.
|
// Live camera demo UI and camera components.
|
||||||
private CameraInput cameraInput;
|
private CameraInput cameraInput;
|
||||||
|
|
||||||
private SolutionGlSurfaceView<FaceMeshResult> glSurfaceView;
|
private SolutionGlSurfaceView<FaceMeshResult> glSurfaceView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
// TODO: Add a toggle to switch between the original face mesh and attention mesh.
|
||||||
setupStaticImageDemoUiComponents();
|
setupStaticImageDemoUiComponents();
|
||||||
setupVideoDemoUiComponents();
|
setupVideoDemoUiComponents();
|
||||||
setupLiveDemoUiComponents();
|
setupLiveDemoUiComponents();
|
||||||
@@ -93,6 +99,43 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Bitmap downscaleBitmap(Bitmap originalBitmap) {
|
||||||
|
double aspectRatio = (double) originalBitmap.getWidth() / originalBitmap.getHeight();
|
||||||
|
int width = imageView.getWidth();
|
||||||
|
int height = imageView.getHeight();
|
||||||
|
if (((double) imageView.getWidth() / imageView.getHeight()) > aspectRatio) {
|
||||||
|
width = (int) (height * aspectRatio);
|
||||||
|
} else {
|
||||||
|
height = (int) (width / aspectRatio);
|
||||||
|
}
|
||||||
|
return Bitmap.createScaledBitmap(originalBitmap, width, height, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap rotateBitmap(Bitmap inputBitmap, InputStream imageData) throws IOException {
|
||||||
|
int orientation =
|
||||||
|
new ExifInterface(imageData)
|
||||||
|
.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||||
|
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
|
||||||
|
return inputBitmap;
|
||||||
|
}
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
switch (orientation) {
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||||
|
matrix.postRotate(90);
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||||
|
matrix.postRotate(180);
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||||
|
matrix.postRotate(270);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
matrix.postRotate(0);
|
||||||
|
}
|
||||||
|
return Bitmap.createBitmap(
|
||||||
|
inputBitmap, 0, 0, inputBitmap.getWidth(), inputBitmap.getHeight(), matrix, true);
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets up the UI components for the static image demo. */
|
/** Sets up the UI components for the static image demo. */
|
||||||
private void setupStaticImageDemoUiComponents() {
|
private void setupStaticImageDemoUiComponents() {
|
||||||
// The Intent to access gallery and read images as bitmap.
|
// The Intent to access gallery and read images as bitmap.
|
||||||
@@ -106,11 +149,19 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
Bitmap bitmap = null;
|
Bitmap bitmap = null;
|
||||||
try {
|
try {
|
||||||
bitmap =
|
bitmap =
|
||||||
MediaStore.Images.Media.getBitmap(
|
downscaleBitmap(
|
||||||
this.getContentResolver(), resultIntent.getData());
|
MediaStore.Images.Media.getBitmap(
|
||||||
|
this.getContentResolver(), resultIntent.getData()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Bitmap reading error:" + e);
|
Log.e(TAG, "Bitmap reading error:" + e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
InputStream imageData =
|
||||||
|
this.getContentResolver().openInputStream(resultIntent.getData());
|
||||||
|
bitmap = rotateBitmap(bitmap, imageData);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Bitmap rotation error:" + e);
|
||||||
|
}
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
facemesh.send(bitmap);
|
facemesh.send(bitmap);
|
||||||
}
|
}
|
||||||
@@ -125,33 +176,34 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
setupStaticImageModePipeline();
|
setupStaticImageModePipeline();
|
||||||
}
|
}
|
||||||
// Reads images from gallery.
|
// Reads images from gallery.
|
||||||
Intent gallery =
|
Intent pickImageIntent = new Intent(Intent.ACTION_PICK);
|
||||||
new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
|
pickImageIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||||
imageGetter.launch(gallery);
|
imageGetter.launch(pickImageIntent);
|
||||||
});
|
});
|
||||||
imageView = new FaceMeshResultImageView(this);
|
imageView = new FaceMeshResultImageView(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The core MediaPipe FaceMesh setup workflow for its static image mode. */
|
/** Sets up core workflow for static image mode. */
|
||||||
private void setupStaticImageModePipeline() {
|
private void setupStaticImageModePipeline() {
|
||||||
this.inputSource = InputSource.IMAGE;
|
this.inputSource = InputSource.IMAGE;
|
||||||
// Initializes a new MediaPipe FaceMesh instance in the static image mode.
|
// Initializes a new MediaPipe Face Mesh solution instance in the static image mode.
|
||||||
facemesh =
|
facemesh =
|
||||||
new FaceMesh(
|
new FaceMesh(
|
||||||
this,
|
this,
|
||||||
FaceMeshOptions.builder()
|
FaceMeshOptions.builder()
|
||||||
.setMode(FaceMeshOptions.STATIC_IMAGE_MODE)
|
.setStaticImageMode(true)
|
||||||
|
.setRefineLandmarks(true)
|
||||||
.setRunOnGpu(RUN_ON_GPU)
|
.setRunOnGpu(RUN_ON_GPU)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// Connects MediaPipe FaceMesh to the user-defined FaceMeshResultImageView.
|
// Connects MediaPipe Face Mesh solution to the user-defined FaceMeshResultImageView.
|
||||||
facemesh.setResultListener(
|
facemesh.setResultListener(
|
||||||
faceMeshResult -> {
|
faceMeshResult -> {
|
||||||
logNoseLandmark(faceMeshResult, /*showPixelValues=*/ true);
|
logNoseLandmark(faceMeshResult, /*showPixelValues=*/ true);
|
||||||
imageView.setFaceMeshResult(faceMeshResult);
|
imageView.setFaceMeshResult(faceMeshResult);
|
||||||
runOnUiThread(() -> imageView.update());
|
runOnUiThread(() -> imageView.update());
|
||||||
});
|
});
|
||||||
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe FaceMesh error:" + message));
|
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe Face Mesh error:" + message));
|
||||||
|
|
||||||
// Updates the preview layout.
|
// Updates the preview layout.
|
||||||
FrameLayout frameLayout = findViewById(R.id.preview_display_layout);
|
FrameLayout frameLayout = findViewById(R.id.preview_display_layout);
|
||||||
@@ -188,9 +240,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
stopCurrentPipeline();
|
stopCurrentPipeline();
|
||||||
setupStreamingModePipeline(InputSource.VIDEO);
|
setupStreamingModePipeline(InputSource.VIDEO);
|
||||||
// Reads video from gallery.
|
// Reads video from gallery.
|
||||||
Intent gallery =
|
Intent pickVideoIntent = new Intent(Intent.ACTION_PICK);
|
||||||
new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI);
|
pickVideoIntent.setDataAndType(MediaStore.Video.Media.INTERNAL_CONTENT_URI, "video/*");
|
||||||
videoGetter.launch(gallery);
|
videoGetter.launch(pickVideoIntent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,25 +259,24 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The core MediaPipe FaceMesh setup workflow for its streaming mode. */
|
/** Sets up core workflow for streaming mode. */
|
||||||
private void setupStreamingModePipeline(InputSource inputSource) {
|
private void setupStreamingModePipeline(InputSource inputSource) {
|
||||||
this.inputSource = inputSource;
|
this.inputSource = inputSource;
|
||||||
// Initializes a new MediaPipe FaceMesh instance in the streaming mode.
|
// Initializes a new MediaPipe Face Mesh solution instance in the streaming mode.
|
||||||
facemesh =
|
facemesh =
|
||||||
new FaceMesh(
|
new FaceMesh(
|
||||||
this,
|
this,
|
||||||
FaceMeshOptions.builder()
|
FaceMeshOptions.builder()
|
||||||
.setMode(FaceMeshOptions.STREAMING_MODE)
|
.setStaticImageMode(false)
|
||||||
|
.setRefineLandmarks(true)
|
||||||
.setRunOnGpu(RUN_ON_GPU)
|
.setRunOnGpu(RUN_ON_GPU)
|
||||||
.build());
|
.build());
|
||||||
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe FaceMesh error:" + message));
|
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe Face Mesh error:" + message));
|
||||||
|
|
||||||
if (inputSource == InputSource.CAMERA) {
|
if (inputSource == InputSource.CAMERA) {
|
||||||
// Initializes a new CameraInput instance and connects it to MediaPipe FaceMesh.
|
|
||||||
cameraInput = new CameraInput(this);
|
cameraInput = new CameraInput(this);
|
||||||
cameraInput.setNewFrameListener(textureFrame -> facemesh.send(textureFrame));
|
cameraInput.setNewFrameListener(textureFrame -> facemesh.send(textureFrame));
|
||||||
} else if (inputSource == InputSource.VIDEO) {
|
} else if (inputSource == InputSource.VIDEO) {
|
||||||
// Initializes a new VideoInput instance and connects it to MediaPipe FaceMesh.
|
|
||||||
videoInput = new VideoInput(this);
|
videoInput = new VideoInput(this);
|
||||||
videoInput.setNewFrameListener(textureFrame -> facemesh.send(textureFrame));
|
videoInput.setNewFrameListener(textureFrame -> facemesh.send(textureFrame));
|
||||||
}
|
}
|
||||||
@@ -295,13 +346,13 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
"MediaPipe FaceMesh nose coordinates (pixel values): x=%f, y=%f",
|
"MediaPipe Face Mesh nose coordinates (pixel values): x=%f, y=%f",
|
||||||
noseLandmark.getX() * width, noseLandmark.getY() * height));
|
noseLandmark.getX() * width, noseLandmark.getY() * height));
|
||||||
} else {
|
} else {
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
String.format(
|
String.format(
|
||||||
"MediaPipe FaceMesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
"MediaPipe Face Mesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||||
noseLandmark.getX(), noseLandmark.getY()));
|
noseLandmark.getX(), noseLandmark.getY()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,20 +31,11 @@ dependencies {
|
|||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||||
testImplementation 'junit:junit:4.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
// MediaPipe hands solution API and solution-core.
|
// MediaPipe Hands Solution.
|
||||||
implementation 'com.google.mediapipe:solution-core:latest.release'
|
implementation 'com.google.mediapipe:solution-core:latest.release'
|
||||||
implementation 'com.google.mediapipe:hands:latest.release'
|
implementation 'com.google.mediapipe:hands:latest.release'
|
||||||
// MediaPipe deps
|
|
||||||
implementation 'com.google.flogger:flogger:latest.release'
|
|
||||||
implementation 'com.google.flogger:flogger-system-backend:latest.release'
|
|
||||||
implementation 'com.google.guava:guava:27.0.1-android'
|
|
||||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
|
||||||
// CameraX core library
|
|
||||||
def camerax_version = "1.0.0-beta10"
|
|
||||||
implementation "androidx.camera:camera-core:$camerax_version"
|
|
||||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
|
||||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
<!-- For using the camera -->
|
<!-- For using the camera -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<!-- For logging solution events -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ android_binary(
|
|||||||
"//third_party:opencv",
|
"//third_party:opencv",
|
||||||
"@maven//:androidx_activity_activity",
|
"@maven//:androidx_activity_activity",
|
||||||
"@maven//:androidx_concurrent_concurrent_futures",
|
"@maven//:androidx_concurrent_concurrent_futures",
|
||||||
|
"@maven//:androidx_exifinterface_exifinterface",
|
||||||
"@maven//:androidx_fragment_fragment",
|
"@maven//:androidx_fragment_fragment",
|
||||||
"@maven//:com_google_guava_guava",
|
"@maven//:com_google_guava_guava",
|
||||||
],
|
],
|
||||||
|
|||||||
+86
-36
@@ -15,9 +15,7 @@
|
|||||||
package com.google.mediapipe.examples.hands;
|
package com.google.mediapipe.examples.hands;
|
||||||
|
|
||||||
import android.opengl.GLES20;
|
import android.opengl.GLES20;
|
||||||
import android.opengl.Matrix;
|
|
||||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||||
import com.google.mediapipe.solutioncore.ResultGlBoundary;
|
|
||||||
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
||||||
import com.google.mediapipe.solutions.hands.Hands;
|
import com.google.mediapipe.solutions.hands.Hands;
|
||||||
import com.google.mediapipe.solutions.hands.HandsResult;
|
import com.google.mediapipe.solutions.hands.HandsResult;
|
||||||
@@ -26,26 +24,36 @@ import java.nio.ByteOrder;
|
|||||||
import java.nio.FloatBuffer;
|
import java.nio.FloatBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A custom implementation of {@link ResultGlRenderer} to render MediaPope Hands results. */
|
/** A custom implementation of {@link ResultGlRenderer} to render {@link HandsResult}. */
|
||||||
public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
||||||
private static final String TAG = "HandsResultGlRenderer";
|
private static final String TAG = "HandsResultGlRenderer";
|
||||||
|
|
||||||
private static final float CONNECTION_THICKNESS = 20.0f;
|
private static final float[] LEFT_HAND_CONNECTION_COLOR = new float[] {0.2f, 1f, 0.2f, 1f};
|
||||||
|
private static final float[] RIGHT_HAND_CONNECTION_COLOR = new float[] {1f, 0.2f, 0.2f, 1f};
|
||||||
|
private static final float CONNECTION_THICKNESS = 25.0f;
|
||||||
|
private static final float[] LEFT_HAND_HOLLOW_CIRCLE_COLOR = new float[] {0.2f, 1f, 0.2f, 1f};
|
||||||
|
private static final float[] RIGHT_HAND_HOLLOW_CIRCLE_COLOR = new float[] {1f, 0.2f, 0.2f, 1f};
|
||||||
|
private static final float HOLLOW_CIRCLE_RADIUS = 0.01f;
|
||||||
|
private static final float[] LEFT_HAND_LANDMARK_COLOR = new float[] {1f, 0.2f, 0.2f, 1f};
|
||||||
|
private static final float[] RIGHT_HAND_LANDMARK_COLOR = new float[] {0.2f, 1f, 0.2f, 1f};
|
||||||
|
private static final float LANDMARK_RADIUS = 0.008f;
|
||||||
|
private static final int NUM_SEGMENTS = 120;
|
||||||
private static final String VERTEX_SHADER =
|
private static final String VERTEX_SHADER =
|
||||||
"uniform mat4 uTransformMatrix;\n"
|
"uniform mat4 uProjectionMatrix;\n"
|
||||||
+ "attribute vec4 vPosition;\n"
|
+ "attribute vec4 vPosition;\n"
|
||||||
+ "void main() {\n"
|
+ "void main() {\n"
|
||||||
+ " gl_Position = uTransformMatrix * vPosition;\n"
|
+ " gl_Position = uProjectionMatrix * vPosition;\n"
|
||||||
+ "}";
|
+ "}";
|
||||||
private static final String FRAGMENT_SHADER =
|
private static final String FRAGMENT_SHADER =
|
||||||
"precision mediump float;\n"
|
"precision mediump float;\n"
|
||||||
|
+ "uniform vec4 uColor;\n"
|
||||||
+ "void main() {\n"
|
+ "void main() {\n"
|
||||||
+ " gl_FragColor = vec4(0, 1, 0, 1);\n"
|
+ " gl_FragColor = uColor;\n"
|
||||||
+ "}";
|
+ "}";
|
||||||
private int program;
|
private int program;
|
||||||
private int positionHandle;
|
private int positionHandle;
|
||||||
private int transformMatrixHandle;
|
private int projectionMatrixHandle;
|
||||||
private final float[] transformMatrix = new float[16];
|
private int colorHandle;
|
||||||
|
|
||||||
private int loadShader(int type, String shaderCode) {
|
private int loadShader(int type, String shaderCode) {
|
||||||
int shader = GLES20.glCreateShader(type);
|
int shader = GLES20.glCreateShader(type);
|
||||||
@@ -63,37 +71,42 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
|||||||
GLES20.glAttachShader(program, fragmentShader);
|
GLES20.glAttachShader(program, fragmentShader);
|
||||||
GLES20.glLinkProgram(program);
|
GLES20.glLinkProgram(program);
|
||||||
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
||||||
transformMatrixHandle = GLES20.glGetUniformLocation(program, "uTransformMatrix");
|
projectionMatrixHandle = GLES20.glGetUniformLocation(program, "uProjectionMatrix");
|
||||||
|
colorHandle = GLES20.glGetUniformLocation(program, "uColor");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void renderResult(HandsResult result, ResultGlBoundary boundary) {
|
public void renderResult(HandsResult result, float[] projectionMatrix) {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GLES20.glUseProgram(program);
|
GLES20.glUseProgram(program);
|
||||||
// Sets the transform matrix to align the result rendering with the scaled output texture.
|
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
|
||||||
// Also flips the rendering vertically since OpenGL assumes the coordinate origin is at the
|
|
||||||
// bottom-left corner, whereas MediaPipe landmark data assumes the coordinate origin is at the
|
|
||||||
// top-left corner.
|
|
||||||
Matrix.setIdentityM(transformMatrix, 0);
|
|
||||||
Matrix.scaleM(
|
|
||||||
transformMatrix,
|
|
||||||
0,
|
|
||||||
2 / (boundary.right() - boundary.left()),
|
|
||||||
-2 / (boundary.top() - boundary.bottom()),
|
|
||||||
1.0f);
|
|
||||||
GLES20.glUniformMatrix4fv(transformMatrixHandle, 1, false, transformMatrix, 0);
|
|
||||||
GLES20.glLineWidth(CONNECTION_THICKNESS);
|
GLES20.glLineWidth(CONNECTION_THICKNESS);
|
||||||
|
|
||||||
int numHands = result.multiHandLandmarks().size();
|
int numHands = result.multiHandLandmarks().size();
|
||||||
for (int i = 0; i < numHands; ++i) {
|
for (int i = 0; i < numHands; ++i) {
|
||||||
drawLandmarks(result.multiHandLandmarks().get(i).getLandmarkList());
|
boolean isLeftHand = result.multiHandedness().get(i).getLabel().equals("Left");
|
||||||
|
drawConnections(
|
||||||
|
result.multiHandLandmarks().get(i).getLandmarkList(),
|
||||||
|
isLeftHand ? LEFT_HAND_CONNECTION_COLOR : RIGHT_HAND_CONNECTION_COLOR);
|
||||||
|
for (NormalizedLandmark landmark : result.multiHandLandmarks().get(i).getLandmarkList()) {
|
||||||
|
// Draws the landmark.
|
||||||
|
drawCircle(
|
||||||
|
landmark.getX(),
|
||||||
|
landmark.getY(),
|
||||||
|
isLeftHand ? LEFT_HAND_LANDMARK_COLOR : RIGHT_HAND_LANDMARK_COLOR);
|
||||||
|
// Draws a hollow circle around the landmark.
|
||||||
|
drawHollowCircle(
|
||||||
|
landmark.getX(),
|
||||||
|
landmark.getY(),
|
||||||
|
isLeftHand ? LEFT_HAND_HOLLOW_CIRCLE_COLOR : RIGHT_HAND_HOLLOW_CIRCLE_COLOR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls this to delete the shader program.
|
* Deletes the shader program.
|
||||||
*
|
*
|
||||||
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
||||||
*/
|
*/
|
||||||
@@ -101,16 +114,12 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
|||||||
GLES20.glDeleteProgram(program);
|
GLES20.glDeleteProgram(program);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Better hand landmark and hand connection drawing.
|
private void drawConnections(List<NormalizedLandmark> handLandmarkList, float[] colorArray) {
|
||||||
private void drawLandmarks(List<NormalizedLandmark> handLandmarkList) {
|
GLES20.glUniform4fv(colorHandle, 1, colorArray, 0);
|
||||||
for (Hands.Connection c : Hands.HAND_CONNECTIONS) {
|
for (Hands.Connection c : Hands.HAND_CONNECTIONS) {
|
||||||
float[] vertex = new float[4];
|
|
||||||
NormalizedLandmark start = handLandmarkList.get(c.start());
|
NormalizedLandmark start = handLandmarkList.get(c.start());
|
||||||
vertex[0] = normalizedLandmarkValue(start.getX());
|
|
||||||
vertex[1] = normalizedLandmarkValue(start.getY());
|
|
||||||
NormalizedLandmark end = handLandmarkList.get(c.end());
|
NormalizedLandmark end = handLandmarkList.get(c.end());
|
||||||
vertex[2] = normalizedLandmarkValue(end.getX());
|
float[] vertex = {start.getX(), start.getY(), end.getX(), end.getY()};
|
||||||
vertex[3] = normalizedLandmarkValue(end.getY());
|
|
||||||
FloatBuffer vertexBuffer =
|
FloatBuffer vertexBuffer =
|
||||||
ByteBuffer.allocateDirect(vertex.length * 4)
|
ByteBuffer.allocateDirect(vertex.length * 4)
|
||||||
.order(ByteOrder.nativeOrder())
|
.order(ByteOrder.nativeOrder())
|
||||||
@@ -123,9 +132,50 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalizes the value from the landmark value range:[0, 1] to the standard OpenGL coordinate
|
private void drawCircle(float x, float y, float[] colorArray) {
|
||||||
// value range: [-1, 1].
|
GLES20.glUniform4fv(colorHandle, 1, colorArray, 0);
|
||||||
private float normalizedLandmarkValue(float value) {
|
int vertexCount = NUM_SEGMENTS + 2;
|
||||||
return value * 2 - 1;
|
float[] vertices = new float[vertexCount * 3];
|
||||||
|
vertices[0] = x;
|
||||||
|
vertices[1] = y;
|
||||||
|
vertices[2] = 0;
|
||||||
|
for (int i = 1; i < vertexCount; i++) {
|
||||||
|
float angle = 2.0f * i * (float) Math.PI / NUM_SEGMENTS;
|
||||||
|
int currentIndex = 3 * i;
|
||||||
|
vertices[currentIndex] = x + (float) (LANDMARK_RADIUS * Math.cos(angle));
|
||||||
|
vertices[currentIndex + 1] = y + (float) (LANDMARK_RADIUS * Math.sin(angle));
|
||||||
|
vertices[currentIndex + 2] = 0;
|
||||||
|
}
|
||||||
|
FloatBuffer vertexBuffer =
|
||||||
|
ByteBuffer.allocateDirect(vertices.length * 4)
|
||||||
|
.order(ByteOrder.nativeOrder())
|
||||||
|
.asFloatBuffer()
|
||||||
|
.put(vertices);
|
||||||
|
vertexBuffer.position(0);
|
||||||
|
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||||
|
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawHollowCircle(float x, float y, float[] colorArray) {
|
||||||
|
GLES20.glUniform4fv(colorHandle, 1, colorArray, 0);
|
||||||
|
int vertexCount = NUM_SEGMENTS + 1;
|
||||||
|
float[] vertices = new float[vertexCount * 3];
|
||||||
|
for (int i = 0; i < vertexCount; i++) {
|
||||||
|
float angle = 2.0f * i * (float) Math.PI / NUM_SEGMENTS;
|
||||||
|
int currentIndex = 3 * i;
|
||||||
|
vertices[currentIndex] = x + (float) (HOLLOW_CIRCLE_RADIUS * Math.cos(angle));
|
||||||
|
vertices[currentIndex + 1] = y + (float) (HOLLOW_CIRCLE_RADIUS * Math.sin(angle));
|
||||||
|
vertices[currentIndex + 2] = 0;
|
||||||
|
}
|
||||||
|
FloatBuffer vertexBuffer =
|
||||||
|
ByteBuffer.allocateDirect(vertices.length * 4)
|
||||||
|
.order(ByteOrder.nativeOrder())
|
||||||
|
.asFloatBuffer()
|
||||||
|
.put(vertices);
|
||||||
|
vertexBuffer.position(0);
|
||||||
|
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||||
|
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, vertexCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-12
@@ -27,14 +27,19 @@ import com.google.mediapipe.solutions.hands.Hands;
|
|||||||
import com.google.mediapipe.solutions.hands.HandsResult;
|
import com.google.mediapipe.solutions.hands.HandsResult;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** An ImageView implementation for displaying MediaPipe Hands results. */
|
/** An ImageView implementation for displaying {@link HandsResult}. */
|
||||||
public class HandsResultImageView extends AppCompatImageView {
|
public class HandsResultImageView extends AppCompatImageView {
|
||||||
private static final String TAG = "HandsResultImageView";
|
private static final String TAG = "HandsResultImageView";
|
||||||
|
|
||||||
private static final int LANDMARK_COLOR = Color.RED;
|
private static final int LEFT_HAND_CONNECTION_COLOR = Color.parseColor("#30FF30");
|
||||||
private static final int LANDMARK_RADIUS = 15;
|
private static final int RIGHT_HAND_CONNECTION_COLOR = Color.parseColor("#FF3030");
|
||||||
private static final int CONNECTION_COLOR = Color.GREEN;
|
private static final int CONNECTION_THICKNESS = 8; // Pixels
|
||||||
private static final int CONNECTION_THICKNESS = 10;
|
private static final int LEFT_HAND_HOLLOW_CIRCLE_COLOR = Color.parseColor("#30FF30");
|
||||||
|
private static final int RIGHT_HAND_HOLLOW_CIRCLE_COLOR = Color.parseColor("#FF3030");
|
||||||
|
private static final int HOLLOW_CIRCLE_WIDTH = 5; // Pixels
|
||||||
|
private static final int LEFT_HAND_LANDMARK_COLOR = Color.parseColor("#FF3030");
|
||||||
|
private static final int RIGHT_HAND_LANDMARK_COLOR = Color.parseColor("#30FF30");
|
||||||
|
private static final int LANDMARK_RADIUS = 10; // Pixels
|
||||||
private Bitmap latest;
|
private Bitmap latest;
|
||||||
|
|
||||||
public HandsResultImageView(Context context) {
|
public HandsResultImageView(Context context) {
|
||||||
@@ -62,11 +67,15 @@ public class HandsResultImageView extends AppCompatImageView {
|
|||||||
int numHands = result.multiHandLandmarks().size();
|
int numHands = result.multiHandLandmarks().size();
|
||||||
for (int i = 0; i < numHands; ++i) {
|
for (int i = 0; i < numHands; ++i) {
|
||||||
drawLandmarksOnCanvas(
|
drawLandmarksOnCanvas(
|
||||||
result.multiHandLandmarks().get(i).getLandmarkList(), canvas, width, height);
|
result.multiHandLandmarks().get(i).getLandmarkList(),
|
||||||
|
result.multiHandedness().get(i).getLabel().equals("Left"),
|
||||||
|
canvas,
|
||||||
|
width,
|
||||||
|
height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the image view with the latest hands result. */
|
/** Updates the image view with the latest {@link HandsResult}. */
|
||||||
public void update() {
|
public void update() {
|
||||||
postInvalidate();
|
postInvalidate();
|
||||||
if (latest != null) {
|
if (latest != null) {
|
||||||
@@ -74,13 +83,17 @@ public class HandsResultImageView extends AppCompatImageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Better hand landmark and hand connection drawing.
|
|
||||||
private void drawLandmarksOnCanvas(
|
private void drawLandmarksOnCanvas(
|
||||||
List<NormalizedLandmark> handLandmarkList, Canvas canvas, int width, int height) {
|
List<NormalizedLandmark> handLandmarkList,
|
||||||
|
boolean isLeftHand,
|
||||||
|
Canvas canvas,
|
||||||
|
int width,
|
||||||
|
int height) {
|
||||||
// Draw connections.
|
// Draw connections.
|
||||||
for (Hands.Connection c : Hands.HAND_CONNECTIONS) {
|
for (Hands.Connection c : Hands.HAND_CONNECTIONS) {
|
||||||
Paint connectionPaint = new Paint();
|
Paint connectionPaint = new Paint();
|
||||||
connectionPaint.setColor(CONNECTION_COLOR);
|
connectionPaint.setColor(
|
||||||
|
isLeftHand ? LEFT_HAND_CONNECTION_COLOR : RIGHT_HAND_CONNECTION_COLOR);
|
||||||
connectionPaint.setStrokeWidth(CONNECTION_THICKNESS);
|
connectionPaint.setStrokeWidth(CONNECTION_THICKNESS);
|
||||||
NormalizedLandmark start = handLandmarkList.get(c.start());
|
NormalizedLandmark start = handLandmarkList.get(c.start());
|
||||||
NormalizedLandmark end = handLandmarkList.get(c.end());
|
NormalizedLandmark end = handLandmarkList.get(c.end());
|
||||||
@@ -92,11 +105,23 @@ public class HandsResultImageView extends AppCompatImageView {
|
|||||||
connectionPaint);
|
connectionPaint);
|
||||||
}
|
}
|
||||||
Paint landmarkPaint = new Paint();
|
Paint landmarkPaint = new Paint();
|
||||||
landmarkPaint.setColor(LANDMARK_COLOR);
|
landmarkPaint.setColor(isLeftHand ? LEFT_HAND_LANDMARK_COLOR : RIGHT_HAND_LANDMARK_COLOR);
|
||||||
// Draw landmarks.
|
// Draws landmarks.
|
||||||
for (LandmarkProto.NormalizedLandmark landmark : handLandmarkList) {
|
for (LandmarkProto.NormalizedLandmark landmark : handLandmarkList) {
|
||||||
canvas.drawCircle(
|
canvas.drawCircle(
|
||||||
landmark.getX() * width, landmark.getY() * height, LANDMARK_RADIUS, landmarkPaint);
|
landmark.getX() * width, landmark.getY() * height, LANDMARK_RADIUS, landmarkPaint);
|
||||||
}
|
}
|
||||||
|
// Draws hollow circles around landmarks.
|
||||||
|
landmarkPaint.setColor(
|
||||||
|
isLeftHand ? LEFT_HAND_HOLLOW_CIRCLE_COLOR : RIGHT_HAND_HOLLOW_CIRCLE_COLOR);
|
||||||
|
landmarkPaint.setStrokeWidth(HOLLOW_CIRCLE_WIDTH);
|
||||||
|
landmarkPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
for (LandmarkProto.NormalizedLandmark landmark : handLandmarkList) {
|
||||||
|
canvas.drawCircle(
|
||||||
|
landmark.getX() * width,
|
||||||
|
landmark.getY() * height,
|
||||||
|
LANDMARK_RADIUS + HOLLOW_CIRCLE_WIDTH,
|
||||||
|
landmarkPaint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-20
@@ -16,6 +16,7 @@ package com.google.mediapipe.examples.hands;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -25,6 +26,9 @@ import android.widget.Button;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.exifinterface.media.ExifInterface;
|
||||||
|
// ContentResolver dependency
|
||||||
|
import com.google.mediapipe.formats.proto.LandmarkProto.Landmark;
|
||||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||||
import com.google.mediapipe.solutioncore.CameraInput;
|
import com.google.mediapipe.solutioncore.CameraInput;
|
||||||
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
||||||
@@ -34,6 +38,7 @@ import com.google.mediapipe.solutions.hands.Hands;
|
|||||||
import com.google.mediapipe.solutions.hands.HandsOptions;
|
import com.google.mediapipe.solutions.hands.HandsOptions;
|
||||||
import com.google.mediapipe.solutions.hands.HandsResult;
|
import com.google.mediapipe.solutions.hands.HandsResult;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/** Main activity of MediaPipe Hands app. */
|
/** Main activity of MediaPipe Hands app. */
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
@@ -59,6 +64,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private ActivityResultLauncher<Intent> videoGetter;
|
private ActivityResultLauncher<Intent> videoGetter;
|
||||||
// Live camera demo UI and camera components.
|
// Live camera demo UI and camera components.
|
||||||
private CameraInput cameraInput;
|
private CameraInput cameraInput;
|
||||||
|
|
||||||
private SolutionGlSurfaceView<HandsResult> glSurfaceView;
|
private SolutionGlSurfaceView<HandsResult> glSurfaceView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -95,6 +101,43 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Bitmap downscaleBitmap(Bitmap originalBitmap) {
|
||||||
|
double aspectRatio = (double) originalBitmap.getWidth() / originalBitmap.getHeight();
|
||||||
|
int width = imageView.getWidth();
|
||||||
|
int height = imageView.getHeight();
|
||||||
|
if (((double) imageView.getWidth() / imageView.getHeight()) > aspectRatio) {
|
||||||
|
width = (int) (height * aspectRatio);
|
||||||
|
} else {
|
||||||
|
height = (int) (width / aspectRatio);
|
||||||
|
}
|
||||||
|
return Bitmap.createScaledBitmap(originalBitmap, width, height, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap rotateBitmap(Bitmap inputBitmap, InputStream imageData) throws IOException {
|
||||||
|
int orientation =
|
||||||
|
new ExifInterface(imageData)
|
||||||
|
.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||||
|
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
|
||||||
|
return inputBitmap;
|
||||||
|
}
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
switch (orientation) {
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||||
|
matrix.postRotate(90);
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||||
|
matrix.postRotate(180);
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||||
|
matrix.postRotate(270);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
matrix.postRotate(0);
|
||||||
|
}
|
||||||
|
return Bitmap.createBitmap(
|
||||||
|
inputBitmap, 0, 0, inputBitmap.getWidth(), inputBitmap.getHeight(), matrix, true);
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets up the UI components for the static image demo. */
|
/** Sets up the UI components for the static image demo. */
|
||||||
private void setupStaticImageDemoUiComponents() {
|
private void setupStaticImageDemoUiComponents() {
|
||||||
// The Intent to access gallery and read images as bitmap.
|
// The Intent to access gallery and read images as bitmap.
|
||||||
@@ -108,11 +151,19 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
Bitmap bitmap = null;
|
Bitmap bitmap = null;
|
||||||
try {
|
try {
|
||||||
bitmap =
|
bitmap =
|
||||||
MediaStore.Images.Media.getBitmap(
|
downscaleBitmap(
|
||||||
this.getContentResolver(), resultIntent.getData());
|
MediaStore.Images.Media.getBitmap(
|
||||||
|
this.getContentResolver(), resultIntent.getData()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Bitmap reading error:" + e);
|
Log.e(TAG, "Bitmap reading error:" + e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
InputStream imageData =
|
||||||
|
this.getContentResolver().openInputStream(resultIntent.getData());
|
||||||
|
bitmap = rotateBitmap(bitmap, imageData);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Bitmap rotation error:" + e);
|
||||||
|
}
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
hands.send(bitmap);
|
hands.send(bitmap);
|
||||||
}
|
}
|
||||||
@@ -127,27 +178,27 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
setupStaticImageModePipeline();
|
setupStaticImageModePipeline();
|
||||||
}
|
}
|
||||||
// Reads images from gallery.
|
// Reads images from gallery.
|
||||||
Intent gallery =
|
Intent pickImageIntent = new Intent(Intent.ACTION_PICK);
|
||||||
new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
|
pickImageIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||||
imageGetter.launch(gallery);
|
imageGetter.launch(pickImageIntent);
|
||||||
});
|
});
|
||||||
imageView = new HandsResultImageView(this);
|
imageView = new HandsResultImageView(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The core MediaPipe Hands setup workflow for its static image mode. */
|
/** Sets up core workflow for static image mode. */
|
||||||
private void setupStaticImageModePipeline() {
|
private void setupStaticImageModePipeline() {
|
||||||
this.inputSource = InputSource.IMAGE;
|
this.inputSource = InputSource.IMAGE;
|
||||||
// Initializes a new MediaPipe Hands instance in the static image mode.
|
// Initializes a new MediaPipe Hands solution instance in the static image mode.
|
||||||
hands =
|
hands =
|
||||||
new Hands(
|
new Hands(
|
||||||
this,
|
this,
|
||||||
HandsOptions.builder()
|
HandsOptions.builder()
|
||||||
.setMode(HandsOptions.STATIC_IMAGE_MODE)
|
.setStaticImageMode(true)
|
||||||
.setMaxNumHands(1)
|
.setMaxNumHands(2)
|
||||||
.setRunOnGpu(RUN_ON_GPU)
|
.setRunOnGpu(RUN_ON_GPU)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// Connects MediaPipe Hands to the user-defined HandsResultImageView.
|
// Connects MediaPipe Hands solution to the user-defined HandsResultImageView.
|
||||||
hands.setResultListener(
|
hands.setResultListener(
|
||||||
handsResult -> {
|
handsResult -> {
|
||||||
logWristLandmark(handsResult, /*showPixelValues=*/ true);
|
logWristLandmark(handsResult, /*showPixelValues=*/ true);
|
||||||
@@ -191,9 +242,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
stopCurrentPipeline();
|
stopCurrentPipeline();
|
||||||
setupStreamingModePipeline(InputSource.VIDEO);
|
setupStreamingModePipeline(InputSource.VIDEO);
|
||||||
// Reads video from gallery.
|
// Reads video from gallery.
|
||||||
Intent gallery =
|
Intent pickVideoIntent = new Intent(Intent.ACTION_PICK);
|
||||||
new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI);
|
pickVideoIntent.setDataAndType(MediaStore.Video.Media.INTERNAL_CONTENT_URI, "video/*");
|
||||||
videoGetter.launch(gallery);
|
videoGetter.launch(pickVideoIntent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,26 +261,24 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The core MediaPipe Hands setup workflow for its streaming mode. */
|
/** Sets up core workflow for streaming mode. */
|
||||||
private void setupStreamingModePipeline(InputSource inputSource) {
|
private void setupStreamingModePipeline(InputSource inputSource) {
|
||||||
this.inputSource = inputSource;
|
this.inputSource = inputSource;
|
||||||
// Initializes a new MediaPipe Hands instance in the streaming mode.
|
// Initializes a new MediaPipe Hands solution instance in the streaming mode.
|
||||||
hands =
|
hands =
|
||||||
new Hands(
|
new Hands(
|
||||||
this,
|
this,
|
||||||
HandsOptions.builder()
|
HandsOptions.builder()
|
||||||
.setMode(HandsOptions.STREAMING_MODE)
|
.setStaticImageMode(false)
|
||||||
.setMaxNumHands(1)
|
.setMaxNumHands(2)
|
||||||
.setRunOnGpu(RUN_ON_GPU)
|
.setRunOnGpu(RUN_ON_GPU)
|
||||||
.build());
|
.build());
|
||||||
hands.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe Hands error:" + message));
|
hands.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe Hands error:" + message));
|
||||||
|
|
||||||
if (inputSource == InputSource.CAMERA) {
|
if (inputSource == InputSource.CAMERA) {
|
||||||
// Initializes a new CameraInput instance and connects it to MediaPipe Hands.
|
|
||||||
cameraInput = new CameraInput(this);
|
cameraInput = new CameraInput(this);
|
||||||
cameraInput.setNewFrameListener(textureFrame -> hands.send(textureFrame));
|
cameraInput.setNewFrameListener(textureFrame -> hands.send(textureFrame));
|
||||||
} else if (inputSource == InputSource.VIDEO) {
|
} else if (inputSource == InputSource.VIDEO) {
|
||||||
// Initializes a new VideoInput instance and connects it to MediaPipe Hands.
|
|
||||||
videoInput = new VideoInput(this);
|
videoInput = new VideoInput(this);
|
||||||
videoInput.setNewFrameListener(textureFrame -> hands.send(textureFrame));
|
videoInput.setNewFrameListener(textureFrame -> hands.send(textureFrame));
|
||||||
}
|
}
|
||||||
@@ -288,7 +337,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void logWristLandmark(HandsResult result, boolean showPixelValues) {
|
private void logWristLandmark(HandsResult result, boolean showPixelValues) {
|
||||||
NormalizedLandmark wristLandmark = Hands.getHandLandmark(result, 0, HandLandmark.WRIST);
|
if (result.multiHandLandmarks().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NormalizedLandmark wristLandmark =
|
||||||
|
result.multiHandLandmarks().get(0).getLandmarkList().get(HandLandmark.WRIST);
|
||||||
// For Bitmaps, show the pixel values. For texture inputs, show the normalized coordinates.
|
// For Bitmaps, show the pixel values. For texture inputs, show the normalized coordinates.
|
||||||
if (showPixelValues) {
|
if (showPixelValues) {
|
||||||
int width = result.inputBitmap().getWidth();
|
int width = result.inputBitmap().getWidth();
|
||||||
@@ -305,5 +358,16 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
"MediaPipe Hand wrist normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
"MediaPipe Hand wrist normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||||
wristLandmark.getX(), wristLandmark.getY()));
|
wristLandmark.getX(), wristLandmark.getY()));
|
||||||
}
|
}
|
||||||
|
if (result.multiHandWorldLandmarks().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Landmark wristWorldLandmark =
|
||||||
|
result.multiHandWorldLandmarks().get(0).getLandmarkList().get(HandLandmark.WRIST);
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaPipe Hand wrist world coordinates (in meters with the origin at the hand's"
|
||||||
|
+ " approximate geometric center): x=%f m, y=%f m, z=%f m",
|
||||||
|
wristWorldLandmark.getX(), wristWorldLandmark.getY(), wristWorldLandmark.getZ()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
rootProject.name = "mediapipe-solutions-examples"
|
rootProject.name = "mediapipe-solutions-examples"
|
||||||
include ':hands'
|
include ':facedetection'
|
||||||
include ':facemesh'
|
include ':facemesh'
|
||||||
|
include ':hands'
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ android_binary(
|
|||||||
srcs = glob(["*.java"]),
|
srcs = glob(["*.java"]),
|
||||||
assets = [
|
assets = [
|
||||||
"//mediapipe/graphs/face_mesh:face_mesh_mobile_gpu.binarypb",
|
"//mediapipe/graphs/face_mesh:face_mesh_mobile_gpu.binarypb",
|
||||||
"//mediapipe/modules/face_landmark:face_landmark.tflite",
|
"//mediapipe/modules/face_landmark:face_landmark_with_attention.tflite",
|
||||||
"//mediapipe/modules/face_detection:face_detection_short_range.tflite",
|
"//mediapipe/modules/face_detection:face_detection_short_range.tflite",
|
||||||
],
|
],
|
||||||
assets_dir = "",
|
assets_dir = "",
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ android_binary(
|
|||||||
srcs = glob(["*.java"]),
|
srcs = glob(["*.java"]),
|
||||||
assets = [
|
assets = [
|
||||||
"//mediapipe/graphs/hand_tracking:hand_detection_mobile_gpu.binarypb",
|
"//mediapipe/graphs/hand_tracking:hand_detection_mobile_gpu.binarypb",
|
||||||
"//mediapipe/modules/palm_detection:palm_detection.tflite",
|
"//mediapipe/modules/palm_detection:palm_detection_full.tflite",
|
||||||
],
|
],
|
||||||
assets_dir = "",
|
assets_dir = "",
|
||||||
manifest = "//mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic:AndroidManifest.xml",
|
manifest = "//mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic:AndroidManifest.xml",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user