diff --git a/README.md b/README.md index e10952bc..012ea3a2 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,17 @@ ML solutions for live and streaming media. ![ready_to_use.png](https://mediapipe.dev/images/ready_to_use_small.png) | ![open_source.png](https://mediapipe.dev/images/open_source_small.png) ***Ready-to-use solutions***: *Cutting-edge ML solutions demonstrating full power of the framework* | ***Free and open source***: *Framework and solutions both under Apache 2.0, fully extensible and customizable* +---- + +**Attention:** *Thanks for your interest in MediaPipe! We are moving to +[https://developers.google.com/mediapipe](https://developers.google.com/mediapipe) +as the primary developer documentation +site for MediaPipe starting April 3, 2023.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## ML solutions in MediaPipe Face Detection | Face Mesh | Iris | Hands | Pose | Holistic diff --git a/docs/_layouts/forward.html b/docs/_layouts/forward.html new file mode 100644 index 00000000..ec97e98c --- /dev/null +++ b/docs/_layouts/forward.html @@ -0,0 +1,13 @@ + + + + + + Redirecting + + +

This page now lives on https://developers.google.com/mediapipe/. If you aren't automatically + redirected, follow this + link.

+ + diff --git a/docs/framework_concepts/building_graphs_cpp.md b/docs/framework_concepts/building_graphs_cpp.md index c415711e..250cd89e 100644 --- a/docs/framework_concepts/building_graphs_cpp.md +++ b/docs/framework_concepts/building_graphs_cpp.md @@ -593,3 +593,105 @@ CalculatorGraphConfig BuildGraph() { return graph.GetConfig(); } ``` + +### Separate nodes for better readability + +```c++ {.bad} +CalculatorGraphConfig BuildGraph() { + Graph graph; + + // Inputs. + Stream a = graph.In(0).Cast(); + auto& node1 = graph.AddNode("Calculator1"); + a.ConnectTo(node1.In("INPUT")); + Stream b = node1.Out("OUTPUT").Cast(); + auto& node2 = graph.AddNode("Calculator2"); + b.ConnectTo(node2.In("INPUT")); + Stream c = node2.Out("OUTPUT").Cast(); + auto& node3 = graph.AddNode("Calculator3"); + b.ConnectTo(node3.In("INPUT_B")); + c.ConnectTo(node3.In("INPUT_C")); + Stream d = node3.Out("OUTPUT").Cast(); + auto& node4 = graph.AddNode("Calculator4"); + b.ConnectTo(node4.In("INPUT_B")); + c.ConnectTo(node4.In("INPUT_C")); + d.ConnectTo(node4.In("INPUT_D")); + Stream e = node4.Out("OUTPUT").Cast(); + // Outputs. + b.SetName("b").ConnectTo(graph.Out(0)); + c.SetName("c").ConnectTo(graph.Out(1)); + d.SetName("d").ConnectTo(graph.Out(2)); + e.SetName("e").ConnectTo(graph.Out(3)); + + return graph.GetConfig(); +} +``` + +In the above code, it can be hard to grasp the idea where each node begins and +ends. To improve this and help your code readers, you can simply have blank +lines before and after each node: + +```c++ {.good} +CalculatorGraphConfig BuildGraph() { + Graph graph; + + // Inputs. + Stream a = graph.In(0).Cast(); + + auto& node1 = graph.AddNode("Calculator1"); + a.ConnectTo(node1.In("INPUT")); + Stream b = node1.Out("OUTPUT").Cast(); + + auto& node2 = graph.AddNode("Calculator2"); + b.ConnectTo(node2.In("INPUT")); + Stream c = node2.Out("OUTPUT").Cast(); + + auto& node3 = graph.AddNode("Calculator3"); + b.ConnectTo(node3.In("INPUT_B")); + c.ConnectTo(node3.In("INPUT_C")); + Stream d = node3.Out("OUTPUT").Cast(); + + auto& node4 = graph.AddNode("Calculator4"); + b.ConnectTo(node4.In("INPUT_B")); + c.ConnectTo(node4.In("INPUT_C")); + d.ConnectTo(node4.In("INPUT_D")); + Stream e = node4.Out("OUTPUT").Cast(); + + // Outputs. + b.SetName("b").ConnectTo(graph.Out(0)); + c.SetName("c").ConnectTo(graph.Out(1)); + d.SetName("d").ConnectTo(graph.Out(2)); + e.SetName("e").ConnectTo(graph.Out(3)); + + return graph.GetConfig(); +} +``` + +Also, the above representation matches `CalculatorGraphConfig` proto +representation better. + +If you extract nodes into utility functions, they are scoped within functions +already and it's clear where they begin and end, so it's completely fine to +have: + +```c++ {.good} +CalculatorGraphConfig BuildGraph() { + Graph graph; + + // Inputs. + Stream a = graph.In(0).Cast(); + + Stream b = RunCalculator1(a, graph); + Stream c = RunCalculator2(b, graph); + Stream d = RunCalculator3(b, c, graph); + Stream e = RunCalculator4(b, c, d, graph); + + // Outputs. + b.SetName("b").ConnectTo(graph.Out(0)); + c.SetName("c").ConnectTo(graph.Out(1)); + d.SetName("d").ConnectTo(graph.Out(2)); + e.SetName("e").ConnectTo(graph.Out(3)); + + return graph.GetConfig(); +} +``` diff --git a/docs/framework_concepts/calculators.md b/docs/framework_concepts/calculators.md index 614abbbf..5c51a3ec 100644 --- a/docs/framework_concepts/calculators.md +++ b/docs/framework_concepts/calculators.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/framework_concepts/calculators title: Calculators parent: Framework Concepts nav_order: 1 diff --git a/docs/framework_concepts/framework_concepts.md b/docs/framework_concepts/framework_concepts.md index dd43d830..5d953480 100644 --- a/docs/framework_concepts/framework_concepts.md +++ b/docs/framework_concepts/framework_concepts.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/framework_concepts/overview title: Framework Concepts nav_order: 5 has_children: true diff --git a/docs/framework_concepts/gpu.md b/docs/framework_concepts/gpu.md index b089dd6f..3c411d55 100644 --- a/docs/framework_concepts/gpu.md +++ b/docs/framework_concepts/gpu.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/framework_concepts/gpu title: GPU parent: Framework Concepts nav_order: 5 diff --git a/docs/framework_concepts/graphs.md b/docs/framework_concepts/graphs.md index 5166f313..0d38c75f 100644 --- a/docs/framework_concepts/graphs.md +++ b/docs/framework_concepts/graphs.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/framework_concepts/graphs title: Graphs parent: Framework Concepts nav_order: 2 diff --git a/docs/framework_concepts/packets.md b/docs/framework_concepts/packets.md index bf5cd4ea..100bc6b0 100644 --- a/docs/framework_concepts/packets.md +++ b/docs/framework_concepts/packets.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/framework_concepts/packets title: Packets parent: Framework Concepts nav_order: 3 diff --git a/docs/framework_concepts/realtime_streams.md b/docs/framework_concepts/realtime_streams.md index 03808145..43d147f5 100644 --- a/docs/framework_concepts/realtime_streams.md +++ b/docs/framework_concepts/realtime_streams.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/framework_concepts/realtime_streams title: Real-time Streams parent: Framework Concepts nav_order: 6 diff --git a/docs/framework_concepts/synchronization.md b/docs/framework_concepts/synchronization.md index be92130b..e35e1032 100644 --- a/docs/framework_concepts/synchronization.md +++ b/docs/framework_concepts/synchronization.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/framework_concepts/synchronization title: Synchronization parent: Framework Concepts nav_order: 4 diff --git a/docs/getting_started/android_solutions.md b/docs/getting_started/android_solutions.md index 2333cd66..0c492c1b 100644 --- a/docs/getting_started/android_solutions.md +++ b/docs/getting_started/android_solutions.md @@ -13,6 +13,17 @@ nav_order: 2 {:toc} --- +**Attention:** *Thanks for your interest in MediaPipe! We are moving to +[https://developers.google.com/mediapipe](https://developers.google.com/mediapipe) +as the primary developer documentation +site for MediaPipe starting April 3, 2023. This content will not be moved to +the new site, but will remain available in the source code repository on an +as-is basis.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + MediaPipe Android Solution APIs (currently in alpha) are available in: * [MediaPipe Face Detection](../solutions/face_detection#android-solution-api) diff --git a/docs/getting_started/building_examples.md b/docs/getting_started/building_examples.md index 2244b273..20c30bef 100644 --- a/docs/getting_started/building_examples.md +++ b/docs/getting_started/building_examples.md @@ -12,6 +12,17 @@ nav_exclude: true {:toc} --- +**Attention:** *Thanks for your interest in MediaPipe! We are moving to +[https://developers.google.com/mediapipe](https://developers.google.com/mediapipe) +as the primary developer documentation +site for MediaPipe starting April 3, 2023. This content will not be moved to +the new site, but will remain available in the source code repository on an +as-is basis.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ### Android Please see these [instructions](./android.md). diff --git a/docs/getting_started/faq.md b/docs/getting_started/faq.md index c42ef898..b7c24e6e 100644 --- a/docs/getting_started/faq.md +++ b/docs/getting_started/faq.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/getting_started/faq title: FAQ parent: Getting Started nav_order: 9 @@ -59,7 +60,7 @@ The second approach allows up to [`max_in_flight`] invocations of the packets from [`CalculatorBase::Process`] are automatically ordered by timestamp before they are passed along to downstream calculators. -With either aproach, you must be aware that the calculator running in parallel +With either approach, you must be aware that the calculator running in parallel cannot maintain internal state in the same way as a normal sequential calculator. diff --git a/docs/getting_started/getting_started.md b/docs/getting_started/getting_started.md index be715054..fea9cfa7 100644 --- a/docs/getting_started/getting_started.md +++ b/docs/getting_started/getting_started.md @@ -11,3 +11,14 @@ has_children: true 1. TOC {:toc} --- + +**Attention:** *Thanks for your interest in MediaPipe! We are moving to +[https://developers.google.com/mediapipe](https://developers.google.com/mediapipe) +as the primary developer documentation +site for MediaPipe starting April 3, 2023. This content will not be moved to +the new site, but will remain available in the source code repository on an +as-is basis.* + +*This notice and web page will be removed on April 3, 2023.* + +---- diff --git a/docs/getting_started/gpu_support.md b/docs/getting_started/gpu_support.md index b4f4aa18..4bd1efeb 100644 --- a/docs/getting_started/gpu_support.md +++ b/docs/getting_started/gpu_support.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/getting_started/gpu_support title: GPU Support parent: Getting Started nav_order: 7 diff --git a/docs/getting_started/help.md b/docs/getting_started/help.md index a9d2ba7b..3ba05274 100644 --- a/docs/getting_started/help.md +++ b/docs/getting_started/help.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/getting_started/help title: Getting Help parent: Getting Started nav_order: 8 @@ -37,8 +38,8 @@ If you open a GitHub issue, here is our policy: - **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: - **Mobile device (e.g. iPhone 8, Pixel 2, Samsung Galaxy) if the issue happens on mobile device**: - **Bazel version**: -- **Android Studio, NDK, SDK versions (if issue is related to building in mobile dev enviroment)**: -- **Xcode & Tulsi version (if issue is related to building in mobile dev enviroment)**: +- **Android Studio, NDK, SDK versions (if issue is related to building in mobile dev environment)**: +- **Xcode & Tulsi version (if issue is related to building in mobile dev environment)**: - **Exact steps to reproduce**: ### Describe the problem diff --git a/docs/getting_started/install.md b/docs/getting_started/install.md index 17f2dfd4..cc5c0241 100644 --- a/docs/getting_started/install.md +++ b/docs/getting_started/install.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/getting_started/install title: Installation parent: Getting Started nav_order: 6 diff --git a/docs/getting_started/javascript.md b/docs/getting_started/javascript.md index 71cec263..79269827 100644 --- a/docs/getting_started/javascript.md +++ b/docs/getting_started/javascript.md @@ -12,6 +12,17 @@ nav_order: 4 {:toc} --- +**Attention:** *Thanks for your interest in MediaPipe! We are moving to +[https://developers.google.com/mediapipe](https://developers.google.com/mediapipe) +as the primary developer documentation +site for MediaPipe starting April 3, 2023. This content will not be moved to +the new site, but will remain available in the source code repository on an +as-is basis.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Ready-to-use JavaScript Solutions MediaPipe currently offers the following solutions: @@ -33,7 +44,7 @@ snippets. | Browser | Platform | Notes | | ------- | ----------------------- | -------------------------------------- | -| Chrome | Android / Windows / Mac | Pixel 4 and older unsupported. Fuschia | +| Chrome | Android / Windows / Mac | Pixel 4 and older unsupported. Fuchsia | | | | unsupported. | | Chrome | iOS | Camera unavailable in Chrome on iOS. | | Safari | iPad/iPhone/Mac | iOS and Safari on iPad / iPhone / | diff --git a/docs/getting_started/troubleshooting.md b/docs/getting_started/troubleshooting.md index a4f347aa..0da25497 100644 --- a/docs/getting_started/troubleshooting.md +++ b/docs/getting_started/troubleshooting.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/getting_started/troubleshooting title: Troubleshooting parent: Getting Started nav_order: 10 @@ -65,7 +66,7 @@ WARNING: Download from https://storage.googleapis.com/mirror.tensorflow.org/gith ``` usually indicates that Bazel fails to download necessary dependency repositories -that MediaPipe needs. MedaiPipe has several dependency repositories that are +that MediaPipe needs. MediaPipe has several dependency repositories that are hosted by Google sites. In some regions, you may need to set up a network proxy or use a VPN to access those resources. You may also need to append `--host_jvm_args "-DsocksProxyHost= -DsocksProxyPort="` diff --git a/docs/index.md b/docs/index.md index e10952bc..012ea3a2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,6 +19,17 @@ ML solutions for live and streaming media. ![ready_to_use.png](https://mediapipe.dev/images/ready_to_use_small.png) | ![open_source.png](https://mediapipe.dev/images/open_source_small.png) ***Ready-to-use solutions***: *Cutting-edge ML solutions demonstrating full power of the framework* | ***Free and open source***: *Framework and solutions both under Apache 2.0, fully extensible and customizable* +---- + +**Attention:** *Thanks for your interest in MediaPipe! We are moving to +[https://developers.google.com/mediapipe](https://developers.google.com/mediapipe) +as the primary developer documentation +site for MediaPipe starting April 3, 2023.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## ML solutions in MediaPipe Face Detection | Face Mesh | Iris | Hands | Pose | Holistic diff --git a/docs/solutions/autoflip.md b/docs/solutions/autoflip.md index 820478dc..d0a76343 100644 --- a/docs/solutions/autoflip.md +++ b/docs/solutions/autoflip.md @@ -18,6 +18,16 @@ nav_order: 14 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for this MediaPipe Legacy Solution as of March 1, 2023. +For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview AutoFlip is an automatic video cropping pipeline built on top of MediaPipe. This diff --git a/docs/solutions/box_tracking.md b/docs/solutions/box_tracking.md index 0f65fbfc..4fecc515 100644 --- a/docs/solutions/box_tracking.md +++ b/docs/solutions/box_tracking.md @@ -18,6 +18,16 @@ nav_order: 10 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for this MediaPipe Legacy Solution as of March 1, 2023. +For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview MediaPipe Box Tracking has been powering real-time tracking in diff --git a/docs/solutions/face_detection.md b/docs/solutions/face_detection.md index 3e9700c7..789d9b3d 100644 --- a/docs/solutions/face_detection.md +++ b/docs/solutions/face_detection.md @@ -18,6 +18,16 @@ nav_order: 1 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview MediaPipe Face Detection is an ultrafast face detection solution that comes with diff --git a/docs/solutions/face_mesh.md b/docs/solutions/face_mesh.md index 24ee760f..84fbb22a 100644 --- a/docs/solutions/face_mesh.md +++ b/docs/solutions/face_mesh.md @@ -18,6 +18,16 @@ nav_order: 2 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview MediaPipe Face Mesh is a solution that estimates 468 3D face landmarks in @@ -133,7 +143,7 @@ about the model in this [paper](https://arxiv.org/abs/2006.10962). The [Face Landmark Model](#face-landmark-model) performs a single-camera face landmark detection in the screen coordinate space: the X- and Y- coordinates are normalized screen coordinates, while the Z coordinate is relative and is scaled -as the X coodinate under the +as the X coordinate under the [weak perspective projection camera model](https://en.wikipedia.org/wiki/3D_projection#Weak_perspective_projection). This format is well-suited for some applications, however it does not directly enable the full spectrum of augmented reality (AR) features like aligning a diff --git a/docs/solutions/hair_segmentation.md b/docs/solutions/hair_segmentation.md index e94a4c79..481cd005 100644 --- a/docs/solutions/hair_segmentation.md +++ b/docs/solutions/hair_segmentation.md @@ -18,6 +18,16 @@ nav_order: 8 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ![hair_segmentation_android_gpu_gif](https://mediapipe.dev/images/mobile/hair_segmentation_android_gpu.gif) ## Example Apps diff --git a/docs/solutions/hands.md b/docs/solutions/hands.md index d3c245b7..a4cd90ba 100644 --- a/docs/solutions/hands.md +++ b/docs/solutions/hands.md @@ -18,6 +18,16 @@ nav_order: 4 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview The ability to perceive the shape and motion of hands can be a vital component diff --git a/docs/solutions/holistic.md b/docs/solutions/holistic.md index 11589425..876a8857 100644 --- a/docs/solutions/holistic.md +++ b/docs/solutions/holistic.md @@ -18,6 +18,16 @@ nav_order: 6 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview Live perception of simultaneous [human pose](./pose.md), diff --git a/docs/solutions/instant_motion_tracking.md b/docs/solutions/instant_motion_tracking.md index 6bdbe5e0..1e714bdc 100644 --- a/docs/solutions/instant_motion_tracking.md +++ b/docs/solutions/instant_motion_tracking.md @@ -18,6 +18,16 @@ nav_order: 11 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for this MediaPipe Legacy Solution as of March 1, 2023. +For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview Augmented Reality (AR) technology creates fun, engaging, and immersive user diff --git a/docs/solutions/iris.md b/docs/solutions/iris.md index 1d36f74c..b8459a0e 100644 --- a/docs/solutions/iris.md +++ b/docs/solutions/iris.md @@ -18,6 +18,16 @@ nav_order: 3 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview A wide range of real-world applications, including computational photography @@ -38,7 +48,7 @@ camera, in real-time, without the need for specialized hardware. Through use of iris landmarks, the solution is also able to determine the metric distance between the subject and the camera with relative error less than 10%. Note that iris tracking does not infer the location at which people are looking, nor does -it provide any form of identity recognition. With the cross-platfrom capability +it provide any form of identity recognition. With the cross-platform capability of the MediaPipe framework, MediaPipe Iris can run on most modern [mobile phones](#mobile), [desktops/laptops](#desktop) and even on the [web](#web). @@ -99,7 +109,7 @@ You can also find more details in this ### Iris Landmark Model The iris model takes an image patch of the eye region and estimates both the eye -landmarks (along the eyelid) and iris landmarks (along ths iris contour). You +landmarks (along the eyelid) and iris landmarks (along this iris contour). You can find more details in this [paper](https://arxiv.org/abs/2006.11341). ![iris_tracking_eye_and_iris_landmarks.png](https://mediapipe.dev/images/mobile/iris_tracking_eye_and_iris_landmarks.png) | diff --git a/docs/solutions/knift.md b/docs/solutions/knift.md index f2ec398f..ad5d39f2 100644 --- a/docs/solutions/knift.md +++ b/docs/solutions/knift.md @@ -18,6 +18,16 @@ nav_order: 13 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for this MediaPipe Legacy Solution as of March 1, 2023. +For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview MediaPipe KNIFT is a template-based feature matching solution using KNIFT diff --git a/docs/solutions/media_sequence.md b/docs/solutions/media_sequence.md index e6bd5fd4..5c479ea4 100644 --- a/docs/solutions/media_sequence.md +++ b/docs/solutions/media_sequence.md @@ -18,6 +18,16 @@ nav_order: 15 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for this MediaPipe Legacy Solution as of March 1, 2023. +For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview MediaPipe is a useful and general framework for media processing that can @@ -85,7 +95,7 @@ process new data sets, in the documentation of MediaSequence uses SequenceExamples as the format of both inputs and outputs. Annotations are encoded as inputs in a SequenceExample of metadata - that defines the labels and the path to the cooresponding video file. This + that defines the labels and the path to the corresponding video file. This metadata is passed as input to the C++ `media_sequence_demo` binary, and the output is a SequenceExample filled with images and annotations ready for model training. diff --git a/docs/solutions/models.md b/docs/solutions/models.md index 325c41f1..1172f2cf 100644 --- a/docs/solutions/models.md +++ b/docs/solutions/models.md @@ -12,6 +12,20 @@ nav_order: 30 {:toc} --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for +[these MediaPipe Legacy Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +as of March 1, 2023. All other +[MediaPipe Legacy Solutions will be upgraded](https://developers.google.com/mediapipe/solutions/guide#legacy) +to a new MediaPipe Solution. The code repository and prebuilt binaries for all +MediaPipe Legacy Solutions will continue to be provided on an as-is basis. +We encourage you to check out the new MediaPipe Solutions at: +[https://developers.google.com/mediapipe/solutions](https://developers.google.com/mediapipe/solutions)* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ### [Face Detection](https://google.github.io/mediapipe/solutions/face_detection) * Short-range model (best for faces within 2 meters from the camera): diff --git a/docs/solutions/object_detection.md b/docs/solutions/object_detection.md index 71d6063b..7b18c0b0 100644 --- a/docs/solutions/object_detection.md +++ b/docs/solutions/object_detection.md @@ -18,6 +18,16 @@ nav_order: 9 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ![object_detection_android_gpu.gif](https://mediapipe.dev/images/mobile/object_detection_android_gpu.gif) ## Example Apps diff --git a/docs/solutions/objectron.md b/docs/solutions/objectron.md index 10483e49..4ffb27bd 100644 --- a/docs/solutions/objectron.md +++ b/docs/solutions/objectron.md @@ -18,6 +18,16 @@ nav_order: 12 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for this MediaPipe Legacy Solution as of March 1, 2023. +For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview MediaPipe Objectron is a mobile real-time 3D object detection solution for @@ -170,7 +180,7 @@ and a The detection subgraph performs ML inference only once every few frames to reduce computation load, and decodes the output tensor to a FrameAnnotation that contains nine keypoints: the 3D bounding box's center and its eight vertices. -The tracking subgraph runs every frame, using the box traker in +The tracking subgraph runs every frame, using the box tracker in [MediaPipe Box Tracking](./box_tracking.md) to track the 2D box tightly enclosing the projection of the 3D bounding box, and lifts the tracked 2D keypoints to 3D with @@ -613,7 +623,7 @@ z_ndc = 1 / Z ### Pixel Space -In this API we set upper-left coner of an image as the origin of pixel +In this API we set upper-left corner of an image as the origin of pixel coordinate. One can convert from NDC to pixel space as follows: ``` diff --git a/docs/solutions/pose.md b/docs/solutions/pose.md index 90580022..3226ddc2 100644 --- a/docs/solutions/pose.md +++ b/docs/solutions/pose.md @@ -20,6 +20,16 @@ nav_order: 5 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview Human pose estimation from video plays a critical role in various applications diff --git a/docs/solutions/pose_classification.md b/docs/solutions/pose_classification.md index 38cb4f80..24f20f72 100644 --- a/docs/solutions/pose_classification.md +++ b/docs/solutions/pose_classification.md @@ -19,6 +19,16 @@ nav_order: 1 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview One of the applications diff --git a/docs/solutions/selfie_segmentation.md b/docs/solutions/selfie_segmentation.md index d8b17487..5febf29f 100644 --- a/docs/solutions/selfie_segmentation.md +++ b/docs/solutions/selfie_segmentation.md @@ -18,6 +18,16 @@ nav_order: 7 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +As of March 1, 2023, this solution is planned to be upgraded to a new MediaPipe +Solution. For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + ## Overview *Fig 1. Example of MediaPipe Selfie Segmentation.* | diff --git a/docs/solutions/solutions.md b/docs/solutions/solutions.md index 05036938..b65390af 100644 --- a/docs/solutions/solutions.md +++ b/docs/solutions/solutions.md @@ -13,7 +13,21 @@ has_toc: false {:toc} --- -Note: These solutions are no longer actively maintained. Consider using or migrating to the new [MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide). +**Attention:** *Thank you for your interest in MediaPipe Solutions. We have +ended support for +[these MediaPipe Legacy Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +as of March 1, 2023. All other +[MediaPipe Legacy Solutions will be upgraded](https://developers.google.com/mediapipe/solutions/guide#legacy) +to a new MediaPipe Solution. The +[code repository](https://github.com/google/mediapipe/tree/master/mediapipe) +and prebuilt binaries for all MediaPipe Legacy Solutions will continue to +be provided on an as-is basis. We encourage you to check out the new MediaPipe +Solutions at: +[https://developers.google.com/mediapipe/solutions](https://developers.google.com/mediapipe/solutions)* + +*This notice and web page will be removed on June 1, 2023.* + +---- MediaPipe offers open source cross-platform, customizable ML solutions for live and streaming media. diff --git a/docs/solutions/youtube_8m.md b/docs/solutions/youtube_8m.md index 5415c146..2e82b85d 100644 --- a/docs/solutions/youtube_8m.md +++ b/docs/solutions/youtube_8m.md @@ -18,6 +18,16 @@ nav_order: 16 --- +**Attention:** *Thank you for your interest in MediaPipe Solutions. +We have ended support for this MediaPipe Legacy Solution as of March 1, 2023. +For more information, see the new +[MediaPipe Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +site.* + +*This notice and web page will be removed on April 3, 2023.* + +---- + MediaPipe is a useful and general framework for media processing that can assist with research, development, and deployment of ML models. This example focuses on model development by demonstrating how to prepare training data and do model diff --git a/docs/tools/visualizer.md b/docs/tools/visualizer.md index 5ed2de2d..45111a36 100644 --- a/docs/tools/visualizer.md +++ b/docs/tools/visualizer.md @@ -1,5 +1,6 @@ --- -layout: default +layout: forward +target: https://developers.google.com/mediapipe/framework/tools/visualizer title: Visualizer parent: Tools nav_order: 1 diff --git a/mediapipe/calculators/core/merge_to_vector_calculator.h b/mediapipe/calculators/core/merge_to_vector_calculator.h index f63d86ee..b4f7a37c 100644 --- a/mediapipe/calculators/core/merge_to_vector_calculator.h +++ b/mediapipe/calculators/core/merge_to_vector_calculator.h @@ -48,7 +48,6 @@ class MergeToVectorCalculator : public Node { } absl::Status Process(CalculatorContext* cc) { - const int input_num = kIn(cc).Count(); std::vector output_vector; for (auto it = kIn(cc).begin(); it != kIn(cc).end(); it++) { const auto& elem = *it; diff --git a/mediapipe/framework/formats/BUILD b/mediapipe/framework/formats/BUILD index 26525b5d..bab5ecd7 100644 --- a/mediapipe/framework/formats/BUILD +++ b/mediapipe/framework/formats/BUILD @@ -13,6 +13,7 @@ # limitations under the License. # +load("@bazel_skylib//lib:selects.bzl", "selects") load("//mediapipe/framework/port:build_config.bzl", "mediapipe_proto_library") load("//mediapipe/framework:mediapipe_register_type.bzl", "mediapipe_register_type") @@ -23,6 +24,14 @@ package( licenses(["notice"]) +selects.config_setting_group( + name = "ios_or_disable_gpu", + match_any = [ + "//mediapipe/gpu:disable_gpu", + "//mediapipe:ios", + ], +) + mediapipe_proto_library( name = "detection_proto", srcs = ["detection.proto"], @@ -336,9 +345,7 @@ cc_library( "//conditions:default": [ "//mediapipe/gpu:gl_texture_buffer", ], - "//mediapipe:ios": [ - ], - "//mediapipe/gpu:disable_gpu": [], + "ios_or_disable_gpu": [], }) + select({ "//conditions:default": [], "//mediapipe:apple": [ diff --git a/mediapipe/framework/tool/status_util.cc b/mediapipe/framework/tool/status_util.cc index 0e3a5924..401a1b63 100644 --- a/mediapipe/framework/tool/status_util.cc +++ b/mediapipe/framework/tool/status_util.cc @@ -18,15 +18,16 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" namespace mediapipe { namespace tool { -absl::Status StatusInvalid(const std::string& message) { +absl::Status StatusInvalid(absl::string_view message) { return absl::Status(absl::StatusCode::kInvalidArgument, message); } -absl::Status StatusFail(const std::string& message) { +absl::Status StatusFail(absl::string_view message) { return absl::Status(absl::StatusCode::kUnknown, message); } @@ -35,12 +36,12 @@ absl::Status StatusStop() { "mediapipe::tool::StatusStop()"); } -absl::Status AddStatusPrefix(const std::string& prefix, +absl::Status AddStatusPrefix(absl::string_view prefix, const absl::Status& status) { return absl::Status(status.code(), absl::StrCat(prefix, status.message())); } -absl::Status CombinedStatus(const std::string& general_comment, +absl::Status CombinedStatus(absl::string_view general_comment, const std::vector& statuses) { // The final error code is absl::StatusCode::kUnknown if not all // the error codes are the same. Otherwise it is the same error code diff --git a/mediapipe/framework/tool/status_util.h b/mediapipe/framework/tool/status_util.h index 8b4bc02d..0db03ec4 100644 --- a/mediapipe/framework/tool/status_util.h +++ b/mediapipe/framework/tool/status_util.h @@ -19,6 +19,7 @@ #include #include "absl/base/macros.h" +#include "absl/strings/string_view.h" #include "mediapipe/framework/port/status.h" namespace mediapipe { @@ -34,16 +35,16 @@ absl::Status StatusStop(); // Return a status which signals an invalid initial condition (for // example an InputSidePacket does not include all necessary fields). ABSL_DEPRECATED("Use absl::InvalidArgumentError(error_message) instead.") -absl::Status StatusInvalid(const std::string& error_message); +absl::Status StatusInvalid(absl::string_view error_message); // Return a status which signals that something unexpectedly failed. ABSL_DEPRECATED("Use absl::UnknownError(error_message) instead.") -absl::Status StatusFail(const std::string& error_message); +absl::Status StatusFail(absl::string_view error_message); // Prefixes the given string to the error message in status. // This function should be considered internal to the framework. // TODO Replace usage of AddStatusPrefix with util::Annotate(). -absl::Status AddStatusPrefix(const std::string& prefix, +absl::Status AddStatusPrefix(absl::string_view prefix, const absl::Status& status); // Combine a vector of absl::Status into a single composite status. @@ -51,7 +52,7 @@ absl::Status AddStatusPrefix(const std::string& prefix, // will be returned. // This function should be considered internal to the framework. // TODO Move this function to somewhere with less visibility. -absl::Status CombinedStatus(const std::string& general_comment, +absl::Status CombinedStatus(absl::string_view general_comment, const std::vector& statuses); } // namespace tool diff --git a/mediapipe/java/com/google/mediapipe/components/GlSurfaceViewRenderer.java b/mediapipe/java/com/google/mediapipe/components/GlSurfaceViewRenderer.java index 7a6c547a..23178b26 100644 --- a/mediapipe/java/com/google/mediapipe/components/GlSurfaceViewRenderer.java +++ b/mediapipe/java/com/google/mediapipe/components/GlSurfaceViewRenderer.java @@ -15,7 +15,9 @@ package com.google.mediapipe.components; import static java.lang.Math.max; +import static java.lang.Math.min; +import android.graphics.Bitmap; import android.graphics.SurfaceTexture; import android.opengl.GLES11Ext; import android.opengl.GLES20; @@ -25,9 +27,12 @@ import android.util.Log; import com.google.mediapipe.framework.TextureFrame; import com.google.mediapipe.glutil.CommonShaders; import com.google.mediapipe.glutil.ShaderUtil; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -44,6 +49,13 @@ import javax.microedition.khronos.opengles.GL10; * {@link TextureFrame} (call {@link #setNextFrame(TextureFrame)}). */ public class GlSurfaceViewRenderer implements GLSurfaceView.Renderer { + /** + * Listener for Bitmap capture requests. + */ + public interface BitmapCaptureListener { + void onBitmapCaptured(Bitmap result); + } + private static final String TAG = "DemoRenderer"; private static final int ATTRIB_POSITION = 1; private static final int ATTRIB_TEXTURE_COORDINATE = 2; @@ -56,12 +68,32 @@ public class GlSurfaceViewRenderer implements GLSurfaceView.Renderer { private int frameUniform; private int textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; private int textureTransformUniform; + private boolean shouldFitToWidth = false; // Controls the alignment between frame size and surface size, 0.5f default is centered. private float alignmentHorizontal = 0.5f; private float alignmentVertical = 0.5f; private float[] textureTransformMatrix = new float[16]; private SurfaceTexture surfaceTexture = null; private final AtomicReference nextFrame = new AtomicReference<>(); + private final AtomicBoolean captureNextFrameBitmap = new AtomicBoolean(); + private BitmapCaptureListener bitmapCaptureListener; + + /** + * Sets the {@link BitmapCaptureListener}. + */ + public void setBitmapCaptureListener(BitmapCaptureListener bitmapCaptureListener) { + this.bitmapCaptureListener = bitmapCaptureListener; + } + + /** + * Request to capture Bitmap of the next frame. + * + * The result will be provided to the {@link BitmapCaptureListener} if one is set. Please note + * this is an expensive operation and the result may not be available for a while. + */ + public void captureNextFrameBitmap() { + captureNextFrameBitmap.set(true); + } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { @@ -147,6 +179,31 @@ public class GlSurfaceViewRenderer implements GLSurfaceView.Renderer { GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); ShaderUtil.checkGlError("glDrawArrays"); + + // Capture Bitmap if requested. + BitmapCaptureListener bitmapCaptureListener = this.bitmapCaptureListener; + if (captureNextFrameBitmap.getAndSet(false) && bitmapCaptureListener != null) { + int bitmapSize = surfaceWidth * surfaceHeight; + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bitmapSize * 4); + byteBuffer.order(ByteOrder.nativeOrder()); + GLES20.glReadPixels( + 0, 0, surfaceWidth, surfaceHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, byteBuffer); + int[] pixelBuffer = new int[bitmapSize]; + byteBuffer.asIntBuffer().get(pixelBuffer); + for (int i = 0; i < bitmapSize; i++) { + // Swap R and B channels. + pixelBuffer[i] = + (pixelBuffer[i] & 0xff00ff00) + | ((pixelBuffer[i] & 0x000000ff) << 16) + | ((pixelBuffer[i] & 0x00ff0000) >> 16); + } + Bitmap bitmap = Bitmap.createBitmap(surfaceWidth, surfaceHeight, Bitmap.Config.ARGB_8888); + bitmap.setPixels( + pixelBuffer, /* offset= */bitmapSize - surfaceWidth, /* stride= */-surfaceWidth, + /* x= */0, /* y= */0, surfaceWidth, surfaceHeight); + bitmapCaptureListener.onBitmapCaptured(bitmap); + } + GLES20.glBindTexture(textureTarget, 0); ShaderUtil.checkGlError("unbind surfaceTexture"); @@ -158,13 +215,17 @@ public class GlSurfaceViewRenderer implements GLSurfaceView.Renderer { // TODO: compute scale from surfaceTexture size. float scaleWidth = frameWidth > 0 ? (float) surfaceWidth / (float) frameWidth : 1.0f; float scaleHeight = frameHeight > 0 ? (float) surfaceHeight / (float) frameHeight : 1.0f; - // Whichever of the two scales is greater corresponds to the dimension where the image - // is proportionally smaller than the view. Dividing both scales by that number results + // By default whichever of the two scales is greater corresponds to the dimension where the + // image is proportionally smaller than the view. Dividing both scales by that number results // in that dimension having scale 1.0, and thus touching the edges of the view, while the - // other is cropped proportionally. - float maxScale = max(scaleWidth, scaleHeight); - scaleWidth /= maxScale; - scaleHeight /= maxScale; + // other is cropped proportionally. If shouldFitToWidth is set as true, use the min scale + // if frame width is greater than frame height. + float scale = max(scaleWidth, scaleHeight); + if (shouldFitToWidth && (frameWidth > frameHeight)) { + scale = min(scaleWidth, scaleHeight); + } + scaleWidth /= scale; + scaleHeight /= scale; // Alignment controls where the visible section is placed within the full camera frame, with // (0, 0) being the bottom left, and (1, 1) being the top right. @@ -232,6 +293,11 @@ public class GlSurfaceViewRenderer implements GLSurfaceView.Renderer { frameHeight = height; } + /** Supports fit to width when the frame width is greater than the frame height. */ + public void setShouldFitToWidth(boolean shouldFitToWidth) { + this.shouldFitToWidth = shouldFitToWidth; + } + /** * When the aspect ratios between the camera frame and the surface size are mismatched, this * controls how the image is aligned. 0.0 means aligning the left/bottom edges; 1.0 means aligning diff --git a/mediapipe/tasks/cc/audio/audio_embedder/BUILD b/mediapipe/tasks/cc/audio/audio_embedder/BUILD index 1dfdd6f1..d79a6f01 100644 --- a/mediapipe/tasks/cc/audio/audio_embedder/BUILD +++ b/mediapipe/tasks/cc/audio/audio_embedder/BUILD @@ -35,7 +35,6 @@ cc_library( "//mediapipe/tasks/cc/components/containers/proto:embeddings_cc_proto", "//mediapipe/tasks/cc/components/processors:embedder_options", "//mediapipe/tasks/cc/components/processors/proto:embedder_options_cc_proto", - "//mediapipe/tasks/cc/components/utils:cosine_similarity", "//mediapipe/tasks/cc/core:base_options", "//mediapipe/tasks/cc/core:task_runner", "//mediapipe/tasks/cc/core/proto:inference_subgraph_cc_proto", diff --git a/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.cc b/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.cc index 1c4a524d..8dd384c4 100644 --- a/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.cc +++ b/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.cc @@ -29,7 +29,6 @@ limitations under the License. #include "mediapipe/tasks/cc/components/containers/proto/embeddings.pb.h" #include "mediapipe/tasks/cc/components/processors/embedder_options.h" #include "mediapipe/tasks/cc/components/processors/proto/embedder_options.pb.h" -#include "mediapipe/tasks/cc/components/utils/cosine_similarity.h" #include "mediapipe/tasks/cc/core/proto/inference_subgraph.pb.h" #include "mediapipe/tasks/cc/core/task_runner.h" #include "tensorflow/lite/core/api/op_resolver.h" @@ -147,10 +146,4 @@ absl::Status AudioEmbedder::EmbedAsync(Matrix audio_block, .At(Timestamp(timestamp_ms * kMicroSecondsPerMilliSecond))}}); } -absl::StatusOr AudioEmbedder::CosineSimilarity( - const components::containers::Embedding& u, - const components::containers::Embedding& v) { - return components::utils::CosineSimilarity(u, v); -} - } // namespace mediapipe::tasks::audio::audio_embedder diff --git a/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.h b/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.h index 31cb6142..c5f548a6 100644 --- a/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.h +++ b/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder.h @@ -125,16 +125,6 @@ class AudioEmbedder : core::BaseAudioTaskApi { // Shuts down the AudioEmbedder when all works are done. absl::Status Close() { return runner_->Close(); } - - // Utility function to compute cosine similarity [1] between two embeddings. - // May return an InvalidArgumentError if e.g. the embeddings are of different - // types (quantized vs. float), have different sizes, or have a an L2-norm of - // 0. - // - // [1]: https://en.wikipedia.org/wiki/Cosine_similarity - static absl::StatusOr CosineSimilarity( - const components::containers::Embedding& u, - const components::containers::Embedding& v); }; } // namespace mediapipe::tasks::audio::audio_embedder diff --git a/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder_test.cc b/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder_test.cc index 749066ea..e388423b 100644 --- a/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder_test.cc +++ b/mediapipe/tasks/cc/audio/audio_embedder/audio_embedder_test.cc @@ -54,8 +54,6 @@ constexpr char kModelWithMetadata[] = "yamnet_embedding_metadata.tflite"; constexpr char k16kTestWavFilename[] = "speech_16000_hz_mono.wav"; constexpr char k48kTestWavFilename[] = "speech_48000_hz_mono.wav"; constexpr char k16kTestWavForTwoHeadsFilename[] = "two_heads_16000_hz_mono.wav"; -constexpr float kSpeechSimilarities[] = {0.985359, 0.994349, 0.993227, 0.996658, - 0.996384}; constexpr int kMilliSecondsPerSecond = 1000; constexpr int kYamnetNumOfAudioSamples = 15600; constexpr int kYamnetAudioSampleRate = 16000; @@ -163,15 +161,9 @@ TEST_F(EmbedTest, SucceedsWithSameAudioAtDifferentSampleRates) { audio_embedder->Embed(audio_buffer1, 16000)); MP_ASSERT_OK_AND_ASSIGN(auto result2, audio_embedder->Embed(audio_buffer2, 48000)); - int expected_size = sizeof(kSpeechSimilarities) / sizeof(float); + int expected_size = 5; ASSERT_EQ(result1.size(), expected_size); ASSERT_EQ(result2.size(), expected_size); - for (int i = 0; i < expected_size; ++i) { - MP_ASSERT_OK_AND_ASSIGN(double similarity, AudioEmbedder::CosineSimilarity( - result1[i].embeddings[0], - result2[i].embeddings[0])); - EXPECT_NEAR(similarity, kSpeechSimilarities[i], 1e-6); - } MP_EXPECT_OK(audio_embedder->Close()); } @@ -192,10 +184,6 @@ TEST_F(EmbedTest, SucceedsWithDifferentAudios) { audio_embedder->Embed(audio_buffer2, kYamnetAudioSampleRate)); ASSERT_EQ(result1.size(), 5); ASSERT_EQ(result2.size(), 1); - MP_ASSERT_OK_AND_ASSIGN(double similarity, AudioEmbedder::CosineSimilarity( - result1[0].embeddings[0], - result2[0].embeddings[0])); - EXPECT_NEAR(similarity, 0.09017f, 1e-6); MP_EXPECT_OK(audio_embedder->Close()); } @@ -258,15 +246,9 @@ TEST_F(EmbedAsyncTest, SucceedsWithSameAudioAtDifferentSampleRates) { RunAudioEmbedderInStreamMode(k16kTestWavFilename, 16000, &result1); std::vector result2; RunAudioEmbedderInStreamMode(k48kTestWavFilename, 48000, &result2); - int expected_size = sizeof(kSpeechSimilarities) / sizeof(float); + int expected_size = 5; ASSERT_EQ(result1.size(), expected_size); ASSERT_EQ(result2.size(), expected_size); - for (int i = 0; i < expected_size; ++i) { - MP_ASSERT_OK_AND_ASSIGN(double similarity, AudioEmbedder::CosineSimilarity( - result1[i].embeddings[0], - result2[i].embeddings[0])); - EXPECT_NEAR(similarity, kSpeechSimilarities[i], 1e-6); - } } TEST_F(EmbedAsyncTest, SucceedsWithDifferentAudios) { @@ -276,10 +258,6 @@ TEST_F(EmbedAsyncTest, SucceedsWithDifferentAudios) { RunAudioEmbedderInStreamMode(k16kTestWavForTwoHeadsFilename, 16000, &result2); ASSERT_EQ(result1.size(), 5); ASSERT_EQ(result2.size(), 1); - MP_ASSERT_OK_AND_ASSIGN(double similarity, AudioEmbedder::CosineSimilarity( - result1[0].embeddings[0], - result2[0].embeddings[0])); - EXPECT_NEAR(similarity, 0.09017f, 1e-6); } } // namespace diff --git a/mediapipe/tasks/cc/components/calculators/score_calibration_calculator_test.cc b/mediapipe/tasks/cc/components/calculators/score_calibration_calculator_test.cc index 8134d86d..b42c1fa1 100644 --- a/mediapipe/tasks/cc/components/calculators/score_calibration_calculator_test.cc +++ b/mediapipe/tasks/cc/components/calculators/score_calibration_calculator_test.cc @@ -185,15 +185,15 @@ TEST_P(CalibrationWithoutIndicesTest, Succeeds) { INSTANTIATE_TEST_SUITE_P( ScoreCalibrationCalculatorTest, CalibrationWithoutIndicesTest, - Values(CalibrationTestParams{.score_transformation = "IDENTITY", - .expected_results = {0.4948505976, - 0.5059588508, 0.2, 0.2}}, + Values(CalibrationTestParams{ + /* score_transformation= */ "IDENTITY", + /* expected_results= */ {0.4948505976, 0.5059588508, 0.2, 0.2}}, CalibrationTestParams{ - .score_transformation = "LOG", - .expected_results = {0.2976901255, 0.3393665735, 0.2, 0.2}}, + /* score_transformation= */ "LOG", + /* expected_results= */ {0.2976901255, 0.3393665735, 0.2, 0.2}}, CalibrationTestParams{ - .score_transformation = "INVERSE_LOGISTIC", - .expected_results = {0.3203217641, 0.3778080605, 0.2, 0.2}}), + /* score_transformation= */ "INVERSE_LOGISTIC", + /* expected_results= */ {0.3203217641, 0.3778080605, 0.2, 0.2}}), [](const TestParamInfo& info) { return info.param.score_transformation; }); diff --git a/mediapipe/tasks/cc/components/containers/landmark.h b/mediapipe/tasks/cc/components/containers/landmark.h index 15b73000..5cb57bfb 100644 --- a/mediapipe/tasks/cc/components/containers/landmark.h +++ b/mediapipe/tasks/cc/components/containers/landmark.h @@ -17,6 +17,7 @@ limitations under the License. #define MEDIAPIPE_TASKS_CC_COMPONENTS_CONTAINERS_LANDMARK_H_ #include +#include #include #include diff --git a/mediapipe/tasks/cc/core/BUILD b/mediapipe/tasks/cc/core/BUILD index 9deb38f4..3d01639c 100644 --- a/mediapipe/tasks/cc/core/BUILD +++ b/mediapipe/tasks/cc/core/BUILD @@ -332,9 +332,11 @@ cc_library( "//mediapipe/tasks:internal", ], deps = [ + ":external_file_handler", "//mediapipe/calculators/core:flow_limiter_calculator_cc_proto", "//mediapipe/framework:calculator_cc_proto", "//mediapipe/framework/api2:builder", + "//mediapipe/tasks/cc/core/proto:external_file_cc_proto", "//mediapipe/tasks/metadata:metadata_schema_cc", "@com_google_absl//absl/strings", "@flatbuffers//:runtime_cc", @@ -375,6 +377,5 @@ cc_test( "//mediapipe/tasks/cc:common", "//mediapipe/tasks/cc/core/proto:external_file_cc_proto", "//mediapipe/tasks/cc/metadata/utils:zip_utils", - "@org_tensorflow//tensorflow/lite/c:common", ], ) diff --git a/mediapipe/tasks/cc/core/external_file_handler.cc b/mediapipe/tasks/cc/core/external_file_handler.cc index a95b8e74..907a19f8 100644 --- a/mediapipe/tasks/cc/core/external_file_handler.cc +++ b/mediapipe/tasks/cc/core/external_file_handler.cc @@ -29,7 +29,7 @@ limitations under the License. #include #else #include -#endif +#endif // _WIN32 #include #include @@ -102,9 +102,13 @@ absl::StatusOr PathToResourceAsFile(std::string path) { #else if (absl::StartsWith(path, "./")) { path = "mediapipe" + path.substr(1); + } else if (path[0] != '/') { + path = "mediapipe/" + path; } std::string error; + // TODO: We should ideally use `CreateForTests` when this is + // accessed from unit tests. std::unique_ptr<::bazel::tools::cpp::runfiles::Runfiles> runfiles( ::bazel::tools::cpp::runfiles::Runfiles::Create("", &error)); if (!runfiles) { diff --git a/mediapipe/tasks/cc/core/model_asset_bundle_resources_test.cc b/mediapipe/tasks/cc/core/model_asset_bundle_resources_test.cc index bcf88713..359deef9 100644 --- a/mediapipe/tasks/cc/core/model_asset_bundle_resources_test.cc +++ b/mediapipe/tasks/cc/core/model_asset_bundle_resources_test.cc @@ -88,6 +88,7 @@ TEST(ModelAssetBundleResourcesTest, CreateFromFile) { .status()); } +#ifndef _WIN32 TEST(ModelAssetBundleResourcesTest, CreateFromFileDescriptor) { const int model_file_descriptor = open(kTestModelBundlePath, O_RDONLY); auto model_file = std::make_unique(); @@ -103,6 +104,7 @@ TEST(ModelAssetBundleResourcesTest, CreateFromFileDescriptor) { model_bundle_resources->GetModelFile("dummy_gesture_recognizer.tflite") .status()); } +#endif // _WIN32 TEST(ModelAssetBundleResourcesTest, CreateFromFilePointer) { auto file_content = LoadBinaryContent(kTestModelBundlePath); diff --git a/mediapipe/tasks/cc/core/model_resources_test.cc b/mediapipe/tasks/cc/core/model_resources_test.cc index de480c5a..3bc5ff06 100644 --- a/mediapipe/tasks/cc/core/model_resources_test.cc +++ b/mediapipe/tasks/cc/core/model_resources_test.cc @@ -136,6 +136,7 @@ TEST_F(ModelResourcesTest, CreateFromFile) { CheckModelResourcesPackets(model_resources.get()); } +#ifndef _WIN32 TEST_F(ModelResourcesTest, CreateFromFileDescriptor) { const int model_file_descriptor = open(kTestModelPath, O_RDONLY); auto model_file = std::make_unique(); @@ -145,6 +146,7 @@ TEST_F(ModelResourcesTest, CreateFromFileDescriptor) { ModelResources::Create(kTestModelResourcesTag, std::move(model_file))); CheckModelResourcesPackets(model_resources.get()); } +#endif // _WIN32 TEST_F(ModelResourcesTest, CreateFromInvalidFile) { auto model_file = std::make_unique(); @@ -168,6 +170,15 @@ TEST_F(ModelResourcesTest, CreateFromInvalidFileDescriptor) { auto status_or_model_resources = ModelResources::Create(kTestModelResourcesTag, std::move(model_file)); +#ifdef _WIN32 + EXPECT_EQ(status_or_model_resources.status().code(), + absl::StatusCode::kFailedPrecondition); + EXPECT_THAT( + status_or_model_resources.status().message(), + testing::HasSubstr("File descriptors are not supported on Windows.")); + AssertStatusHasMediaPipeTasksStatusCode(status_or_model_resources.status(), + MediaPipeTasksStatus::kFileReadError); +#else EXPECT_EQ(status_or_model_resources.status().code(), absl::StatusCode::kInvalidArgument); EXPECT_THAT( @@ -176,6 +187,7 @@ TEST_F(ModelResourcesTest, CreateFromInvalidFileDescriptor) { AssertStatusHasMediaPipeTasksStatusCode( status_or_model_resources.status(), MediaPipeTasksStatus::kInvalidArgumentError); +#endif // _WIN32 } TEST_F(ModelResourcesTest, CreateFailWithCorruptedFile) { diff --git a/mediapipe/tasks/cc/core/utils.cc b/mediapipe/tasks/cc/core/utils.cc index a840ba62..1e44109c 100644 --- a/mediapipe/tasks/cc/core/utils.cc +++ b/mediapipe/tasks/cc/core/utils.cc @@ -23,6 +23,8 @@ limitations under the License. #include "absl/strings/string_view.h" #include "flatbuffers/flatbuffers.h" #include "mediapipe/calculators/core/flow_limiter_calculator.pb.h" +#include "mediapipe/tasks/cc/core/external_file_handler.h" +#include "mediapipe/tasks/cc/core/proto/external_file.pb.h" namespace mediapipe { namespace tasks { @@ -34,13 +36,11 @@ constexpr char kFlowLimiterCalculatorName[] = "FlowLimiterCalculator"; } // namespace std::string LoadBinaryContent(const char* filename) { - std::ifstream input_file(filename, std::ios::binary | std::ios::ate); - // Find buffer size from input file, and load the buffer. - size_t buffer_size = input_file.tellg(); - std::string buffer(buffer_size, '\0'); - input_file.seekg(0, std::ios::beg); - input_file.read(const_cast(buffer.c_str()), buffer_size); - return buffer; + proto::ExternalFile external_file; + external_file.set_file_name(filename); + auto file_handler = + ExternalFileHandler::CreateFromExternalFile(&external_file); + return std::string{(*file_handler)->GetFileContent()}; } int FindTensorIndexByMetadataName( diff --git a/mediapipe/tasks/cc/metadata/tests/BUILD b/mediapipe/tasks/cc/metadata/tests/BUILD index 2b0e0bc2..33cbf6b5 100644 --- a/mediapipe/tasks/cc/metadata/tests/BUILD +++ b/mediapipe/tasks/cc/metadata/tests/BUILD @@ -16,6 +16,7 @@ cc_test( "//mediapipe/framework/port:gtest_main", "//mediapipe/framework/port:status", "//mediapipe/tasks/cc:common", + "//mediapipe/tasks/cc/core:utils", "//mediapipe/tasks/cc/metadata:metadata_extractor", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", diff --git a/mediapipe/tasks/cc/metadata/tests/metadata_extractor_test.cc b/mediapipe/tasks/cc/metadata/tests/metadata_extractor_test.cc index 4dacc7b8..41f66415 100644 --- a/mediapipe/tasks/cc/metadata/tests/metadata_extractor_test.cc +++ b/mediapipe/tasks/cc/metadata/tests/metadata_extractor_test.cc @@ -25,12 +25,14 @@ limitations under the License. #include "mediapipe/framework/port/status_macros.h" #include "mediapipe/framework/port/status_matchers.h" #include "mediapipe/tasks/cc/common.h" +#include "mediapipe/tasks/cc/core/utils.h" namespace mediapipe { namespace tasks { namespace metadata { namespace { +using core::LoadBinaryContent; using ::testing::Optional; constexpr char kTestDataDirectory[] = "mediapipe/tasks/testdata/metadata"; @@ -53,8 +55,8 @@ constexpr char kRandomTextFile[] = "external_file"; absl::StatusOr> CreateMetadataExtractor( std::string model_name, std::string* file_contents) { - MP_RETURN_IF_ERROR(file::GetContents( - file::JoinPath("./", kTestDataDirectory, model_name), file_contents)); + *file_contents = LoadBinaryContent( + file::JoinPath("./", kTestDataDirectory, model_name).c_str()); return ModelMetadataExtractor::CreateFromModelBuffer(file_contents->data(), file_contents->length()); } diff --git a/mediapipe/tasks/cc/metadata/tests/metadata_parser_test.cc b/mediapipe/tasks/cc/metadata/tests/metadata_parser_test.cc index 3605648e..1d2e22cc 100644 --- a/mediapipe/tasks/cc/metadata/tests/metadata_parser_test.cc +++ b/mediapipe/tasks/cc/metadata/tests/metadata_parser_test.cc @@ -26,7 +26,11 @@ using ::testing::MatchesRegex; TEST(MetadataParserTest, MatadataParserVersionIsWellFormed) { // Validates that the version is well-formed (x.y.z). +#ifdef _WIN32 + EXPECT_THAT(kMatadataParserVersion, MatchesRegex("\\d+\\.\\d+\\.\\d+")); +#else EXPECT_THAT(kMatadataParserVersion, MatchesRegex("[0-9]+\\.[0-9]+\\.[0-9]+")); +#endif // _WIN32 } } // namespace diff --git a/mediapipe/tasks/cc/metadata/tests/metadata_version_test.cc b/mediapipe/tasks/cc/metadata/tests/metadata_version_test.cc index bf6206f3..96785302 100644 --- a/mediapipe/tasks/cc/metadata/tests/metadata_version_test.cc +++ b/mediapipe/tasks/cc/metadata/tests/metadata_version_test.cc @@ -83,7 +83,11 @@ TEST(MetadataVersionTest, builder.GetSize(), &min_version), kTfLiteOk); // Validates that the version is well-formed (x.y.z). +#ifdef _WIN32 + EXPECT_THAT(min_version, MatchesRegex("\\d+\\.\\d+\\.\\d+")); +#else EXPECT_THAT(min_version, MatchesRegex("[0-9]+\\.[0-9]+\\.[0-9]+")); +#endif // _WIN32 } TEST(MetadataVersionTest, diff --git a/mediapipe/tasks/cc/vision/face_geometry/calculators/BUILD b/mediapipe/tasks/cc/vision/face_geometry/calculators/BUILD new file mode 100644 index 00000000..7a504129 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/calculators/BUILD @@ -0,0 +1,49 @@ +# Copyright 2023 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. + +load("//mediapipe/framework/port:build_config.bzl", "mediapipe_proto_library") + +licenses(["notice"]) + +package(default_visibility = ["//mediapipe/tasks:internal"]) + +mediapipe_proto_library( + name = "geometry_pipeline_calculator_proto", + srcs = ["geometry_pipeline_calculator.proto"], + deps = [ + "//mediapipe/framework:calculator_options_proto", + ], +) + +cc_library( + name = "geometry_pipeline_calculator", + srcs = ["geometry_pipeline_calculator.cc"], + deps = [ + ":geometry_pipeline_calculator_cc_proto", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/formats:landmark_cc_proto", + "//mediapipe/framework/port:logging", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/framework/port:statusor", + "//mediapipe/tasks/cc/vision/face_geometry/libs:geometry_pipeline", + "//mediapipe/tasks/cc/vision/face_geometry/libs:validation_utils", + "//mediapipe/tasks/cc/vision/face_geometry/proto:environment_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:face_geometry_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:geometry_pipeline_metadata_cc_proto", + "//mediapipe/util:resource_util", + "@com_google_absl//absl/memory", + ], + alwayslink = 1, +) diff --git a/mediapipe/tasks/cc/vision/face_geometry/calculators/geometry_pipeline_calculator.cc b/mediapipe/tasks/cc/vision/face_geometry/calculators/geometry_pipeline_calculator.cc new file mode 100644 index 00000000..d6082e62 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/calculators/geometry_pipeline_calculator.cc @@ -0,0 +1,194 @@ +// Copyright 2023 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 +#include +#include +#include + +#include "absl/memory/memory.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/formats/landmark.pb.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/framework/port/status_macros.h" +#include "mediapipe/framework/port/statusor.h" +#include "mediapipe/tasks/cc/vision/face_geometry/calculators/geometry_pipeline_calculator.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/libs/geometry_pipeline.h" +#include "mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/environment.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/face_geometry.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.pb.h" +#include "mediapipe/util/resource_util.h" + +namespace mediapipe::tasks::vision::face_geometry { +namespace { + +static constexpr char kEnvironmentTag[] = "ENVIRONMENT"; +static constexpr char kImageSizeTag[] = "IMAGE_SIZE"; +static constexpr char kMultiFaceGeometryTag[] = "MULTI_FACE_GEOMETRY"; +static constexpr char kMultiFaceLandmarksTag[] = "MULTI_FACE_LANDMARKS"; + +using ::mediapipe::tasks::vision::face_geometry::proto::Environment; +using ::mediapipe::tasks::vision::face_geometry::proto::FaceGeometry; +using ::mediapipe::tasks::vision::face_geometry::proto:: + GeometryPipelineMetadata; + +// A calculator that renders a visual effect for multiple faces. +// +// Inputs: +// IMAGE_SIZE (`std::pair`, required): +// The size of the current frame. The first element of the pair is the frame +// width; the other one is the frame height. +// +// The face landmarks should have been detected on a frame with the same +// ratio. If used as-is, the resulting face geometry visualization should be +// happening on a frame with the same ratio as well. +// +// MULTI_FACE_LANDMARKS (`std::vector`, required): +// A vector of face landmark lists. +// +// Input side packets: +// ENVIRONMENT (`proto::Environment`, required) +// Describes an environment; includes the camera frame origin point location +// as well as virtual camera parameters. +// +// Output: +// MULTI_FACE_GEOMETRY (`std::vector`, required): +// A vector of face geometry data. +// +// Options: +// metadata_path (`string`, optional): +// Defines a path for the geometry pipeline metadata file. +// +// The geometry pipeline metadata file format must be the binary +// `GeometryPipelineMetadata` proto. +// +class GeometryPipelineCalculator : public CalculatorBase { + public: + static absl::Status GetContract(CalculatorContract* cc) { + cc->InputSidePackets().Tag(kEnvironmentTag).Set(); + cc->Inputs().Tag(kImageSizeTag).Set>(); + cc->Inputs() + .Tag(kMultiFaceLandmarksTag) + .Set>(); + cc->Outputs().Tag(kMultiFaceGeometryTag).Set>(); + + return absl::OkStatus(); + } + + absl::Status Open(CalculatorContext* cc) override { + cc->SetOffset(mediapipe::TimestampDiff(0)); + + const auto& options = cc->Options(); + + ASSIGN_OR_RETURN( + GeometryPipelineMetadata metadata, + ReadMetadataFromFile(options.metadata_path()), + _ << "Failed to read the geometry pipeline metadata from file!"); + + MP_RETURN_IF_ERROR(ValidateGeometryPipelineMetadata(metadata)) + << "Invalid geometry pipeline metadata!"; + + const Environment& environment = + cc->InputSidePackets().Tag(kEnvironmentTag).Get(); + + MP_RETURN_IF_ERROR(ValidateEnvironment(environment)) + << "Invalid environment!"; + + ASSIGN_OR_RETURN(geometry_pipeline_, + CreateGeometryPipeline(environment, metadata), + _ << "Failed to create a geometry pipeline!"); + + return absl::OkStatus(); + } + + absl::Status Process(CalculatorContext* cc) override { + // Both the `IMAGE_SIZE` and the `MULTI_FACE_LANDMARKS` streams are required + // to have a non-empty packet. In case this requirement is not met, there's + // nothing to be processed at the current timestamp. + if (cc->Inputs().Tag(kImageSizeTag).IsEmpty() || + cc->Inputs().Tag(kMultiFaceLandmarksTag).IsEmpty()) { + return absl::OkStatus(); + } + + const auto& image_size = + cc->Inputs().Tag(kImageSizeTag).Get>(); + const auto& multi_face_landmarks = + cc->Inputs() + .Tag(kMultiFaceLandmarksTag) + .Get>(); + + auto multi_face_geometry = absl::make_unique>(); + + ASSIGN_OR_RETURN( + *multi_face_geometry, + geometry_pipeline_->EstimateFaceGeometry( + multi_face_landmarks, // + /*frame_width*/ image_size.first, + /*frame_height*/ image_size.second), + _ << "Failed to estimate face geometry for multiple faces!"); + + cc->Outputs() + .Tag(kMultiFaceGeometryTag) + .AddPacket(mediapipe::Adopt>( + multi_face_geometry.release()) + .At(cc->InputTimestamp())); + + return absl::OkStatus(); + } + + absl::Status Close(CalculatorContext* cc) override { + return absl::OkStatus(); + } + + private: + static absl::StatusOr ReadMetadataFromFile( + const std::string& metadata_path) { + ASSIGN_OR_RETURN(std::string metadata_blob, + ReadContentBlobFromFile(metadata_path), + _ << "Failed to read a metadata blob from file!"); + + GeometryPipelineMetadata metadata; + RET_CHECK(metadata.ParseFromString(metadata_blob)) + << "Failed to parse a metadata proto from a binary blob!"; + + return metadata; + } + + static absl::StatusOr ReadContentBlobFromFile( + const std::string& unresolved_path) { + ASSIGN_OR_RETURN(std::string resolved_path, + mediapipe::PathToResourceAsFile(unresolved_path), + _ << "Failed to resolve path! Path = " << unresolved_path); + + std::string content_blob; + MP_RETURN_IF_ERROR( + mediapipe::GetResourceContents(resolved_path, &content_blob)) + << "Failed to read content blob! Resolved path = " << resolved_path; + + return content_blob; + } + + std::unique_ptr geometry_pipeline_; +}; + +} // namespace + +using FaceGeometryPipelineCalculator = GeometryPipelineCalculator; + +REGISTER_CALCULATOR( + ::mediapipe::tasks::vision::face_geometry::FaceGeometryPipelineCalculator); + +} // namespace mediapipe::tasks::vision::face_geometry diff --git a/mediapipe/tasks/cc/vision/face_geometry/calculators/geometry_pipeline_calculator.proto b/mediapipe/tasks/cc/vision/face_geometry/calculators/geometry_pipeline_calculator.proto new file mode 100644 index 00000000..afcc20a1 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/calculators/geometry_pipeline_calculator.proto @@ -0,0 +1,27 @@ +// Copyright 2023 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.tasks.vision.face_geometry; + +import "mediapipe/framework/calculator_options.proto"; + +message FaceGeometryPipelineCalculatorOptions { + extend mediapipe.CalculatorOptions { + optional FaceGeometryPipelineCalculatorOptions ext = 512499200; + } + + optional string metadata_path = 1; +} diff --git a/mediapipe/tasks/cc/vision/face_geometry/data/BUILD b/mediapipe/tasks/cc/vision/face_geometry/data/BUILD new file mode 100644 index 00000000..19dc5a58 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/data/BUILD @@ -0,0 +1,59 @@ +# Copyright 2023 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. + +load("//mediapipe/framework:encode_binary_proto.bzl", "encode_binary_proto") + +licenses(["notice"]) + +package(default_visibility = ["//visibility:public"]) + +encode_binary_proto( + name = "geometry_pipeline_metadata_detection", + input = "geometry_pipeline_metadata_detection.pbtxt", + message_type = "mediapipe.tasks.vision.face_geometry.proto.GeometryPipelineMetadata", + output = "geometry_pipeline_metadata_detection.binarypb", + deps = [ + "//mediapipe/tasks/cc/vision/face_geometry/proto:geometry_pipeline_metadata_proto", + ], +) + +encode_binary_proto( + name = "geometry_pipeline_metadata_landmarks", + input = "geometry_pipeline_metadata_landmarks.pbtxt", + message_type = "mediapipe.tasks.vision.face_geometry.proto.GeometryPipelineMetadata", + output = "geometry_pipeline_metadata_landmarks.binarypb", + deps = [ + "//mediapipe/tasks/cc/vision/face_geometry/proto:geometry_pipeline_metadata_proto", + ], +) + +# For backward-compatibility reasons, generate `geometry_pipeline_metadata.binarypb` from +# the `geometry_pipeline_metadata_landmarks.pbtxt` definition. +encode_binary_proto( + name = "geometry_pipeline_metadata", + input = "geometry_pipeline_metadata_landmarks.pbtxt", + message_type = "mediapipe.tasks.vision.face_geometry.proto.GeometryPipelineMetadata", + output = "geometry_pipeline_metadata.binarypb", + deps = [ + "//mediapipe/tasks/cc/vision/face_geometry/proto:geometry_pipeline_metadata_proto", + ], +) + +# These canonical face model files are not meant to be used in runtime, but rather for asset +# creation and/or reference. +exports_files([ + "canonical_face_model.fbx", + "canonical_face_model.obj", + "canonical_face_model_uv_visualization.png", +]) diff --git a/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model.fbx b/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model.fbx new file mode 100644 index 00000000..8e9d24ac Binary files /dev/null and b/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model.fbx differ diff --git a/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model.obj b/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model.obj new file mode 100644 index 00000000..0e666d1c --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model.obj @@ -0,0 +1,1834 @@ +v 0.000000 -3.406404 5.979507 +v 0.000000 -1.126865 7.475604 +v 0.000000 -2.089024 6.058267 +v -0.463928 0.955357 6.633583 +v 0.000000 -0.463170 7.586580 +v 0.000000 0.365669 7.242870 +v 0.000000 2.473255 5.788627 +v -4.253081 2.577646 3.279702 +v 0.000000 4.019042 5.284764 +v 0.000000 4.885979 5.385258 +v 0.000000 8.261778 4.481535 +v 0.000000 -3.706811 5.864924 +v 0.000000 -3.918301 5.569430 +v 0.000000 -3.994436 5.219482 +v 0.000000 -4.542400 5.404754 +v 0.000000 -4.745577 5.529457 +v 0.000000 -5.019567 5.601448 +v 0.000000 -5.365123 5.535441 +v 0.000000 -6.149624 5.071372 +v 0.000000 -1.501095 7.112196 +v -0.416106 -1.466449 6.447657 +v -7.087960 5.434801 0.099620 +v -2.628639 2.035898 3.848121 +v -3.198363 1.985815 3.796952 +v -3.775151 2.039402 3.646194 +v -4.465819 2.422950 3.155168 +v -2.164289 2.189867 3.851822 +v -3.208229 3.223926 4.115822 +v -2.673803 3.205337 4.092203 +v -3.745193 3.165286 3.972409 +v -4.161018 3.059069 3.719554 +v -5.062006 1.934418 2.776093 +v -2.266659 -7.425768 4.389812 +v -4.445859 2.663991 3.173422 +v -7.214530 2.263009 0.073150 +v -5.799793 2.349546 2.204059 +v -2.844939 -0.720868 4.433130 +v -0.711452 -3.329355 5.877044 +v -0.606033 -3.924562 5.444923 +v -1.431615 -3.500953 5.496189 +v -1.914910 -3.803146 5.028930 +v -1.131043 -3.973937 5.189648 +v -1.563548 -4.082763 4.842263 +v -2.650112 -5.003649 4.188483 +v -0.427049 -1.094134 7.360529 +v -0.496396 -0.475659 7.440358 +v -5.253307 3.881582 3.363159 +v -1.718698 0.974609 4.558359 +v -1.608635 -0.942516 5.814193 +v -1.651267 -0.610868 5.581319 +v -4.765501 -0.701554 3.534632 +v -0.478306 0.295766 7.101013 +v -3.734964 4.508230 4.550454 +v -4.588603 4.302037 4.048484 +v -6.279331 6.615427 1.425850 +v -1.220941 4.142165 5.106035 +v -2.193489 3.100317 4.000575 +v -3.102642 -4.352984 4.095905 +v -6.719682 -4.788645 -1.745401 +v -1.193824 -1.306795 5.737747 +v -0.729766 -1.593712 5.833208 +v -2.456206 -4.342621 4.283884 +v -2.204823 -4.304508 4.162499 +v -4.985894 4.802461 3.751977 +v -1.592294 -1.257709 5.456949 +v -2.644548 4.524654 4.921559 +v -2.760292 5.100971 5.015990 +v -3.523964 8.005976 3.729163 +v -5.599763 5.715470 2.724259 +v -3.063932 6.566144 4.529981 +v -5.720968 4.254584 2.830852 +v -6.374393 4.785590 1.591691 +v -0.672728 -3.688016 5.737804 +v -1.262560 -3.787691 5.417779 +v -1.732553 -3.952767 5.000579 +v -1.043625 -1.464973 5.662455 +v -2.321234 -4.329069 4.258156 +v -2.056846 -4.477671 4.520883 +v -2.153084 -4.276322 4.038093 +v -0.946874 -1.035249 6.512274 +v -1.469132 -4.036351 4.604908 +v -1.024340 -3.989851 4.926693 +v -0.533422 -3.993222 5.138202 +v -0.769720 -6.095394 4.985883 +v -0.699606 -5.291850 5.448304 +v -0.669687 -4.949770 5.509612 +v -0.630947 -4.695101 5.449371 +v -0.583218 -4.517982 5.339869 +v -1.537170 -4.423206 4.745470 +v -1.615600 -4.475942 4.813632 +v -1.729053 -4.618680 4.854463 +v -1.838624 -4.828746 4.823737 +v -2.368250 -3.106237 4.868096 +v -7.542244 -1.049282 -2.431321 +v 0.000000 -1.724003 6.601390 +v -1.826614 -4.399531 4.399021 +v -1.929558 -4.411831 4.497052 +v -0.597442 -2.013686 5.866456 +v -1.405627 -1.714196 5.241087 +v -0.662449 -1.819321 5.863759 +v -2.342340 0.572222 4.294303 +v -3.327324 0.104863 4.113860 +v -1.726175 -0.919165 5.273355 +v -5.133204 7.485602 2.660442 +v -4.538641 6.319907 3.683424 +v -3.986562 5.109487 4.466315 +v -2.169681 -5.440433 4.455874 +v -1.395634 5.011963 5.316032 +v -1.619500 6.599217 4.921106 +v -1.891399 8.236377 4.274997 +v -4.195832 2.235205 3.375099 +v -5.733342 1.411738 2.431726 +v -1.859887 2.355757 3.843181 +v -4.988612 3.074654 3.083858 +v -1.303263 1.416453 4.831091 +v -1.305757 -0.672779 6.415959 +v -6.465170 0.937119 1.689873 +v -5.258659 0.945811 2.974312 +v -4.432338 0.722096 3.522615 +v -3.300681 0.861641 3.872784 +v -2.430178 1.131492 4.039035 +v -1.820731 1.467954 4.224124 +v -0.563221 2.307693 5.566789 +v -6.338145 -0.529279 1.881175 +v -5.587698 3.208071 2.687839 +v -0.242624 -1.462857 7.071491 +v -1.611251 0.339326 4.895421 +v -7.743095 2.364999 -2.005167 +v -1.391142 1.851048 4.448999 +v -1.785794 -0.978284 4.850470 +v -4.670959 2.664461 3.084075 +v -1.333970 -0.283761 6.097047 +v -7.270895 -2.890917 -2.252455 +v -1.856432 2.585245 3.757904 +v -0.923388 0.073076 6.671944 +v -5.000589 -6.135128 1.892523 +v -5.085276 -7.178590 0.714711 +v -7.159291 -0.811820 -0.072044 +v -5.843051 -5.248023 0.924091 +v -6.847258 3.662916 0.724695 +v -2.412942 -8.258853 4.119213 +v -0.179909 -1.689864 6.573301 +v -2.103655 -0.163946 4.566119 +v -6.407571 2.236021 1.560843 +v -3.670075 2.360153 3.635230 +v -3.177186 2.294265 3.775704 +v -2.196121 -4.598322 4.479786 +v -6.234883 -1.944430 1.663542 +v -1.292924 -9.295920 4.094063 +v -3.210651 -8.533278 2.802001 +v -4.068926 -7.993109 1.925119 +v 0.000000 6.545390 5.027311 +v 0.000000 -9.403378 4.264492 +v -2.724032 2.315802 3.777151 +v -2.288460 2.398891 3.697603 +v -1.998311 2.496547 3.689148 +v -6.130040 3.399261 2.038516 +v -2.288460 2.886504 3.775031 +v -2.724032 2.961810 3.871767 +v -3.177186 2.964136 3.876973 +v -3.670075 2.927714 3.724325 +v -4.018389 2.857357 3.482983 +v -7.555811 4.106811 -0.991917 +v -4.018389 2.483695 3.440898 +v 0.000000 -2.521945 5.932265 +v -1.776217 -2.683946 5.213116 +v -1.222237 -1.182444 5.952465 +v -0.731493 -2.536683 5.815343 +v 0.000000 3.271027 5.236015 +v -4.135272 -6.996638 2.671970 +v -3.311811 -7.660815 3.382963 +v -1.313701 -8.639995 4.702456 +v -5.940524 -6.223629 -0.631468 +v -1.998311 2.743838 3.744030 +v -0.901447 1.236992 5.754256 +v 0.000000 -8.765243 4.891441 +v -2.308977 -8.974196 3.609070 +v -6.954154 -2.439843 -0.131163 +v -1.098819 -4.458788 5.120727 +v -1.181124 -4.579996 5.189564 +v -1.255818 -4.787901 5.237051 +v -1.325085 -5.106507 5.205010 +v -1.546388 -5.819392 4.757893 +v -1.953754 -4.183892 4.431713 +v -2.117802 -4.137093 4.555096 +v -2.285339 -4.051196 4.582438 +v -2.850160 -3.665720 4.484994 +v -5.278538 -2.238942 2.861224 +v -0.946709 1.907628 5.196779 +v -1.314173 3.104912 4.231404 +v -1.780000 2.860000 3.881555 +v -1.845110 -4.098880 4.247264 +v -5.436187 -4.030482 2.109852 +v -0.766444 3.182131 4.861453 +v -1.938616 -6.614410 4.521085 +v 0.000000 1.059413 6.774605 +v -0.516573 1.583572 6.148363 +v 0.000000 1.728369 6.316750 +v -1.246815 0.230297 5.681036 +v 0.000000 -7.942194 5.181173 +v 0.000000 -6.991499 5.153478 +v -0.997827 -6.930921 4.979576 +v -3.288807 -5.382514 3.795752 +v -2.311631 -1.566237 4.590085 +v -2.680250 -6.111567 4.096152 +v -3.832928 -1.537326 4.137731 +v -2.961860 -2.274215 4.440943 +v -4.386901 -2.683286 3.643886 +v -1.217295 -7.834465 4.969286 +v -1.542374 -0.136843 5.201008 +v -3.878377 -6.041764 3.311079 +v -3.084037 -6.809842 3.814195 +v -3.747321 -4.503545 3.726453 +v -6.094129 -3.205991 1.473482 +v -4.588995 -4.728726 2.983221 +v -6.583231 -3.941269 0.070268 +v -3.492580 -3.195820 4.130198 +v -1.255543 0.802341 5.307551 +v -1.126122 -0.933602 6.538785 +v -1.443109 -1.142774 5.905127 +v -0.923043 -0.529042 7.003423 +v -1.755386 3.529117 4.327696 +v -2.632589 3.713828 4.364629 +v -3.388062 3.721976 4.309028 +v -4.075766 3.675413 4.076063 +v -4.622910 3.474691 3.646321 +v -5.171755 2.535753 2.670867 +v -7.297331 0.763172 -0.048769 +v -4.706828 1.651000 3.109532 +v -4.071712 1.476821 3.476944 +v -3.269817 1.470659 3.731945 +v -2.527572 1.617311 3.865444 +v -1.970894 1.858505 3.961782 +v -1.579543 2.097941 4.084996 +v -7.664182 0.673132 -2.435867 +v -1.397041 -1.340139 5.630378 +v -0.884838 0.658740 6.233232 +v -0.767097 -0.968035 7.077932 +v -0.460213 -1.334106 6.787447 +v -0.748618 -1.067994 6.798303 +v -1.236408 -1.585568 5.480490 +v -0.387306 -1.409990 6.957705 +v -0.319925 -1.607931 6.508676 +v -1.639633 2.556298 3.863736 +v -1.255645 2.467144 4.203800 +v -1.031362 2.382663 4.615849 +v -4.253081 2.772296 3.315305 +v -4.530000 2.910000 3.339685 +v 0.463928 0.955357 6.633583 +v 4.253081 2.577646 3.279702 +v 0.416106 -1.466449 6.447657 +v 7.087960 5.434801 0.099620 +v 2.628639 2.035898 3.848121 +v 3.198363 1.985815 3.796952 +v 3.775151 2.039402 3.646194 +v 4.465819 2.422950 3.155168 +v 2.164289 2.189867 3.851822 +v 3.208229 3.223926 4.115822 +v 2.673803 3.205337 4.092203 +v 3.745193 3.165286 3.972409 +v 4.161018 3.059069 3.719554 +v 5.062006 1.934418 2.776093 +v 2.266659 -7.425768 4.389812 +v 4.445859 2.663991 3.173422 +v 7.214530 2.263009 0.073150 +v 5.799793 2.349546 2.204059 +v 2.844939 -0.720868 4.433130 +v 0.711452 -3.329355 5.877044 +v 0.606033 -3.924562 5.444923 +v 1.431615 -3.500953 5.496189 +v 1.914910 -3.803146 5.028930 +v 1.131043 -3.973937 5.189648 +v 1.563548 -4.082763 4.842263 +v 2.650112 -5.003649 4.188483 +v 0.427049 -1.094134 7.360529 +v 0.496396 -0.475659 7.440358 +v 5.253307 3.881582 3.363159 +v 1.718698 0.974609 4.558359 +v 1.608635 -0.942516 5.814193 +v 1.651267 -0.610868 5.581319 +v 4.765501 -0.701554 3.534632 +v 0.478306 0.295766 7.101013 +v 3.734964 4.508230 4.550454 +v 4.588603 4.302037 4.048484 +v 6.279331 6.615427 1.425850 +v 1.220941 4.142165 5.106035 +v 2.193489 3.100317 4.000575 +v 3.102642 -4.352984 4.095905 +v 6.719682 -4.788645 -1.745401 +v 1.193824 -1.306795 5.737747 +v 0.729766 -1.593712 5.833208 +v 2.456206 -4.342621 4.283884 +v 2.204823 -4.304508 4.162499 +v 4.985894 4.802461 3.751977 +v 1.592294 -1.257709 5.456949 +v 2.644548 4.524654 4.921559 +v 2.760292 5.100971 5.015990 +v 3.523964 8.005976 3.729163 +v 5.599763 5.715470 2.724259 +v 3.063932 6.566144 4.529981 +v 5.720968 4.254584 2.830852 +v 6.374393 4.785590 1.591691 +v 0.672728 -3.688016 5.737804 +v 1.262560 -3.787691 5.417779 +v 1.732553 -3.952767 5.000579 +v 1.043625 -1.464973 5.662455 +v 2.321234 -4.329069 4.258156 +v 2.056846 -4.477671 4.520883 +v 2.153084 -4.276322 4.038093 +v 0.946874 -1.035249 6.512274 +v 1.469132 -4.036351 4.604908 +v 1.024340 -3.989851 4.926693 +v 0.533422 -3.993222 5.138202 +v 0.769720 -6.095394 4.985883 +v 0.699606 -5.291850 5.448304 +v 0.669687 -4.949770 5.509612 +v 0.630947 -4.695101 5.449371 +v 0.583218 -4.517982 5.339869 +v 1.537170 -4.423206 4.745470 +v 1.615600 -4.475942 4.813632 +v 1.729053 -4.618680 4.854463 +v 1.838624 -4.828746 4.823737 +v 2.368250 -3.106237 4.868096 +v 7.542244 -1.049282 -2.431321 +v 1.826614 -4.399531 4.399021 +v 1.929558 -4.411831 4.497052 +v 0.597442 -2.013686 5.866456 +v 1.405627 -1.714196 5.241087 +v 0.662449 -1.819321 5.863759 +v 2.342340 0.572222 4.294303 +v 3.327324 0.104863 4.113860 +v 1.726175 -0.919165 5.273355 +v 5.133204 7.485602 2.660442 +v 4.538641 6.319907 3.683424 +v 3.986562 5.109487 4.466315 +v 2.169681 -5.440433 4.455874 +v 1.395634 5.011963 5.316032 +v 1.619500 6.599217 4.921106 +v 1.891399 8.236377 4.274997 +v 4.195832 2.235205 3.375099 +v 5.733342 1.411738 2.431726 +v 1.859887 2.355757 3.843181 +v 4.988612 3.074654 3.083858 +v 1.303263 1.416453 4.831091 +v 1.305757 -0.672779 6.415959 +v 6.465170 0.937119 1.689873 +v 5.258659 0.945811 2.974312 +v 4.432338 0.722096 3.522615 +v 3.300681 0.861641 3.872784 +v 2.430178 1.131492 4.039035 +v 1.820731 1.467954 4.224124 +v 0.563221 2.307693 5.566789 +v 6.338145 -0.529279 1.881175 +v 5.587698 3.208071 2.687839 +v 0.242624 -1.462857 7.071491 +v 1.611251 0.339326 4.895421 +v 7.743095 2.364999 -2.005167 +v 1.391142 1.851048 4.448999 +v 1.785794 -0.978284 4.850470 +v 4.670959 2.664461 3.084075 +v 1.333970 -0.283761 6.097047 +v 7.270895 -2.890917 -2.252455 +v 1.856432 2.585245 3.757904 +v 0.923388 0.073076 6.671944 +v 5.000589 -6.135128 1.892523 +v 5.085276 -7.178590 0.714711 +v 7.159291 -0.811820 -0.072044 +v 5.843051 -5.248023 0.924091 +v 6.847258 3.662916 0.724695 +v 2.412942 -8.258853 4.119213 +v 0.179909 -1.689864 6.573301 +v 2.103655 -0.163946 4.566119 +v 6.407571 2.236021 1.560843 +v 3.670075 2.360153 3.635230 +v 3.177186 2.294265 3.775704 +v 2.196121 -4.598322 4.479786 +v 6.234883 -1.944430 1.663542 +v 1.292924 -9.295920 4.094063 +v 3.210651 -8.533278 2.802001 +v 4.068926 -7.993109 1.925119 +v 2.724032 2.315802 3.777151 +v 2.288460 2.398891 3.697603 +v 1.998311 2.496547 3.689148 +v 6.130040 3.399261 2.038516 +v 2.288460 2.886504 3.775031 +v 2.724032 2.961810 3.871767 +v 3.177186 2.964136 3.876973 +v 3.670075 2.927714 3.724325 +v 4.018389 2.857357 3.482983 +v 7.555811 4.106811 -0.991917 +v 4.018389 2.483695 3.440898 +v 1.776217 -2.683946 5.213116 +v 1.222237 -1.182444 5.952465 +v 0.731493 -2.536683 5.815343 +v 4.135272 -6.996638 2.671970 +v 3.311811 -7.660815 3.382963 +v 1.313701 -8.639995 4.702456 +v 5.940524 -6.223629 -0.631468 +v 1.998311 2.743838 3.744030 +v 0.901447 1.236992 5.754256 +v 2.308977 -8.974196 3.609070 +v 6.954154 -2.439843 -0.131163 +v 1.098819 -4.458788 5.120727 +v 1.181124 -4.579996 5.189564 +v 1.255818 -4.787901 5.237051 +v 1.325085 -5.106507 5.205010 +v 1.546388 -5.819392 4.757893 +v 1.953754 -4.183892 4.431713 +v 2.117802 -4.137093 4.555096 +v 2.285339 -4.051196 4.582438 +v 2.850160 -3.665720 4.484994 +v 5.278538 -2.238942 2.861224 +v 0.946709 1.907628 5.196779 +v 1.314173 3.104912 4.231404 +v 1.780000 2.860000 3.881555 +v 1.845110 -4.098880 4.247264 +v 5.436187 -4.030482 2.109852 +v 0.766444 3.182131 4.861453 +v 1.938616 -6.614410 4.521085 +v 0.516573 1.583572 6.148363 +v 1.246815 0.230297 5.681036 +v 0.997827 -6.930921 4.979576 +v 3.288807 -5.382514 3.795752 +v 2.311631 -1.566237 4.590085 +v 2.680250 -6.111567 4.096152 +v 3.832928 -1.537326 4.137731 +v 2.961860 -2.274215 4.440943 +v 4.386901 -2.683286 3.643886 +v 1.217295 -7.834465 4.969286 +v 1.542374 -0.136843 5.201008 +v 3.878377 -6.041764 3.311079 +v 3.084037 -6.809842 3.814195 +v 3.747321 -4.503545 3.726453 +v 6.094129 -3.205991 1.473482 +v 4.588995 -4.728726 2.983221 +v 6.583231 -3.941269 0.070268 +v 3.492580 -3.195820 4.130198 +v 1.255543 0.802341 5.307551 +v 1.126122 -0.933602 6.538785 +v 1.443109 -1.142774 5.905127 +v 0.923043 -0.529042 7.003423 +v 1.755386 3.529117 4.327696 +v 2.632589 3.713828 4.364629 +v 3.388062 3.721976 4.309028 +v 4.075766 3.675413 4.076063 +v 4.622910 3.474691 3.646321 +v 5.171755 2.535753 2.670867 +v 7.297331 0.763172 -0.048769 +v 4.706828 1.651000 3.109532 +v 4.071712 1.476821 3.476944 +v 3.269817 1.470659 3.731945 +v 2.527572 1.617311 3.865444 +v 1.970894 1.858505 3.961782 +v 1.579543 2.097941 4.084996 +v 7.664182 0.673132 -2.435867 +v 1.397041 -1.340139 5.630378 +v 0.884838 0.658740 6.233232 +v 0.767097 -0.968035 7.077932 +v 0.460213 -1.334106 6.787447 +v 0.748618 -1.067994 6.798303 +v 1.236408 -1.585568 5.480490 +v 0.387306 -1.409990 6.957705 +v 0.319925 -1.607931 6.508676 +v 1.639633 2.556298 3.863736 +v 1.255645 2.467144 4.203800 +v 1.031362 2.382663 4.615849 +v 4.253081 2.772296 3.315305 +v 4.530000 2.910000 3.339685 +vt 0.427942 0.304722 +vt 0.526878 0.295374 +vt 0.444832 0.269206 +vt 0.607600 0.322297 +vt 0.377046 0.677222 +vt 0.473033 0.304722 +vt 0.526913 0.282143 +vt 0.447112 0.284192 +vt 0.599262 0.318931 +vt 0.414712 0.664780 +vt 0.473122 0.295374 +vt 0.527671 0.263774 +vt 0.448020 0.295368 +vt 0.593203 0.314324 +vt 0.467288 0.470075 +vt 0.473087 0.282143 +vt 0.534090 0.220859 +vt 0.448662 0.304722 +vt 0.569944 0.232965 +vt 0.437114 0.441104 +vt 0.472329 0.263774 +vt 0.524613 0.307634 +vt 0.114210 0.384978 +vt 0.555168 0.269206 +vt 0.455528 0.451377 +vt 0.465828 0.220810 +vt 0.547818 0.307634 +vt 0.375437 0.075808 +vt 0.552888 0.284192 +vt 0.429884 0.533478 +vt 0.475387 0.307634 +vt 0.568842 0.307634 +vt 0.499877 0.091010 +vt 0.551980 0.295368 +vt 0.336768 0.355267 +vt 0.452182 0.307634 +vt 0.539958 0.442861 +vt 0.455607 0.548199 +vt 0.551338 0.304722 +vt 0.133823 0.317299 +vt 0.431158 0.307634 +vt 0.596371 0.306047 +vt 0.408772 0.626106 +vt 0.885770 0.384971 +vt 0.279777 0.285342 +vt 0.460042 0.442861 +vt 0.596961 0.293460 +vt 0.128294 0.208059 +vt 0.624563 0.075808 +vt 0.189096 0.353700 +vt 0.403629 0.306047 +vt 0.611897 0.306039 +vt 0.440512 0.097581 +vt 0.544341 0.548416 +vt 0.324548 0.296007 +vt 0.403039 0.293460 +vt 0.554692 0.419934 +vt 0.335279 0.147180 +vt 0.591234 0.626106 +vt 0.354128 0.187447 +vt 0.388103 0.306039 +vt 0.577238 0.326110 +vt 0.288719 0.180054 +vt 0.871706 0.208059 +vt 0.445308 0.419934 +vt 0.553172 0.331473 +vt 0.499923 0.648476 +vt 0.559100 0.097368 +vt 0.422762 0.326110 +vt 0.527121 0.333802 +vt 0.465844 0.379359 +vt 0.664630 0.147129 +vt 0.446828 0.331473 +vt 0.826722 0.721245 +vt 0.445682 0.433923 +vt 0.711218 0.180025 +vt 0.472879 0.333802 +vt 0.770391 0.700444 +vt 0.415838 0.375804 +vt 0.534154 0.379360 +vt 0.173287 0.721252 +vt 0.635536 0.810751 +vt 0.499988 0.381566 +vt 0.554318 0.433923 +vt 0.229622 0.700459 +vt 0.770092 0.767979 +vt 0.301415 0.612551 +vt 0.584177 0.375893 +vt 0.364501 0.810886 +vt 0.668509 0.880086 +vt 0.058133 0.680924 +vt 0.698585 0.612551 +vt 0.229924 0.767997 +vt 0.616907 0.744114 +vt 0.301415 0.636844 +vt 0.941867 0.680924 +vt 0.331431 0.880286 +vt 0.614083 0.718613 +vt 0.318785 0.641660 +vt 0.698585 0.636844 +vt 0.383103 0.744160 +vt 0.577414 0.436833 +vt 0.343364 0.644643 +vt 0.681215 0.641660 +vt 0.385919 0.718636 +vt 0.722943 0.728037 +vt 0.365962 0.644029 +vt 0.656636 0.644643 +vt 0.422552 0.436767 +vt 0.607591 0.305797 +vt 0.388665 0.637716 +vt 0.634038 0.644029 +vt 0.277076 0.728068 +vt 0.618026 0.305289 +vt 0.194993 0.657898 +vt 0.611335 0.637716 +vt 0.392389 0.305797 +vt 0.542902 0.415208 +vt 0.410373 0.608920 +vt 0.805016 0.657892 +vt 0.381974 0.305289 +vt 0.557261 0.427174 +vt 0.393207 0.604463 +vt 0.589660 0.608938 +vt 0.457098 0.415208 +vt 0.932695 0.269895 +vt 0.366170 0.601178 +vt 0.606793 0.604463 +vt 0.442739 0.427174 +vt 0.645429 0.303293 +vt 0.499977 0.045547 +vt 0.633830 0.601178 +vt 0.067305 0.269895 +vt 0.607610 0.646112 +vt 0.500023 0.809424 +vt 0.733752 0.130299 +vt 0.354490 0.303216 +vt 0.552386 0.697432 +vt 0.266248 0.130299 +vt 0.681008 0.101715 +vt 0.392390 0.646112 +vt 0.830705 0.806186 +vt 0.318993 0.101715 +vt 0.568013 0.055435 +vt 0.447580 0.697390 +vt 0.703624 0.706729 +vt 0.430987 0.055935 +vt 0.812086 0.411461 +vt 0.169295 0.806186 +vt 0.662801 0.717082 +vt 0.187885 0.411462 +vt 0.603900 0.289783 +vt 0.296392 0.706757 +vt 0.516446 0.500361 +vt 0.396100 0.289783 +vt 0.656636 0.599403 +vt 0.337212 0.717117 +vt 0.723330 0.636627 +vt 0.723087 0.467946 +vt 0.343364 0.599403 +vt 0.681215 0.603765 +vt 0.483370 0.500413 +vt 0.710288 0.631747 +vt 0.578632 0.466377 +vt 0.318785 0.603765 +vt 0.825608 0.602325 +vt 0.276896 0.467943 +vt 0.549756 0.600249 +vt 0.570338 0.451425 +vt 0.174399 0.602329 +vt 0.617942 0.491684 +vt 0.421352 0.466259 +vt 0.560698 0.604668 +vt 0.598631 0.545021 +vt 0.382385 0.491427 +vt 0.508953 0.420562 +vt 0.429819 0.451385 +vt 0.573595 0.610193 +vt 0.742247 0.685493 +vt 0.490967 0.420622 +vt 0.614074 0.116754 +vt 0.401223 0.544828 +vt 0.517472 0.422123 +vt 0.515097 0.472748 +vt 0.385764 0.116846 +vt 0.865595 0.666313 +vt 0.257765 0.685510 +vt 0.516311 0.436946 +vt 0.513050 0.452718 +vt 0.134410 0.666317 +vt 0.816351 0.259740 +vt 0.485301 0.472605 +vt 0.566036 0.417671 +vt 0.624852 0.271901 +vt 0.183610 0.259743 +vt 0.892441 0.459239 +vt 0.486717 0.452371 +vt 0.531529 0.444943 +vt 0.571228 0.317308 +vt 0.107550 0.459245 +vt 0.801779 0.168062 +vt 0.374971 0.272195 +vt 0.523913 0.436170 +vt 0.549626 0.319139 +vt 0.198221 0.168062 +vt 0.760966 0.220247 +vt 0.428771 0.317309 +vt 0.526564 0.453882 +vt 0.585384 0.333459 +vt 0.238979 0.220255 +vt 0.537728 0.494615 +vt 0.450374 0.319139 +vt 0.541366 0.521101 +vt 0.560215 0.342771 +vt 0.462783 0.494253 +vt 0.580985 0.612840 +vt 0.414617 0.333459 +vt 0.567192 0.430580 +vt 0.525850 0.319809 +vt 0.419054 0.612845 +vt 0.967686 0.355643 +vt 0.439785 0.342771 +vt 0.992440 0.519223 +vt 0.528249 0.349596 +vt 0.032314 0.355643 +vt 0.560611 0.480983 +vt 0.474155 0.319808 +vt 0.579658 0.590055 +vt 0.643998 0.465512 +vt 0.439121 0.481042 +vt 0.733530 0.623023 +vt 0.471751 0.349596 +vt 0.603876 0.583413 +vt 0.790082 0.608646 +vt 0.266470 0.623023 +vt 0.602995 0.451312 +vt 0.355808 0.465594 +vt 0.633505 0.573912 +vt 0.893693 0.600040 +vt 0.396993 0.451203 +vt 0.573500 0.580000 +vt 0.209925 0.608647 +vt 0.666525 0.566134 +vt 0.719902 0.624400 +vt 0.426243 0.579569 +vt 0.980531 0.598436 +vt 0.106310 0.600044 +vt 0.702114 0.566837 +vt 0.602918 0.157137 +vt 0.019469 0.598436 +vt 0.595293 0.514976 +vt 0.280098 0.624400 +vt 0.732392 0.575453 +vt 0.752212 0.589195 +vt 0.404670 0.514867 +vt 0.509127 0.437282 +vt 0.396889 0.157245 +vt 0.897013 0.531231 +vt 0.702097 0.646409 +vt 0.490726 0.437599 +vt 0.771046 0.651041 +vt 0.247792 0.589190 +vt 0.758757 0.617213 +vt 0.680678 0.652735 +vt 0.228962 0.651049 +vt 0.810748 0.476074 +vt 0.297903 0.646409 +vt 0.716482 0.666799 +vt 0.629906 0.653924 +vt 0.189241 0.476076 +vt 0.523481 0.594373 +vt 0.319322 0.652735 +vt 0.687132 0.677654 +vt 0.654766 0.655989 +vt 0.476410 0.594194 +vt 0.600862 0.567527 +vt 0.370094 0.653924 +vt 0.655896 0.679837 +vt 0.606630 0.596295 +vt 0.398964 0.567345 +vt 0.631101 0.552846 +vt 0.345234 0.655989 +vt 0.622953 0.677221 +vt 0.725342 0.610869 +vt 0.368756 0.552793 +vt 0.667113 0.539327 +vt 0.393362 0.596294 +vt 0.585271 0.664823 +vt 0.688880 0.590540 +vt 0.332828 0.539288 +vt 0.713757 0.532373 +vt 0.274658 0.610869 +vt 0.531987 0.469860 +vt 0.661242 0.586975 +vt 0.286267 0.532325 +vt 0.752702 0.542818 +vt 0.311120 0.590540 +vt 0.562759 0.441215 +vt 0.634070 0.590424 +vt 0.247308 0.542806 +vt 0.821442 0.542444 +vt 0.313951 0.224692 +vt 0.338758 0.586975 +vt 0.544562 0.451624 +vt 0.895093 0.745859 +vt 0.178560 0.542446 +vt 0.551868 0.463430 +vt 0.410986 0.491277 +vt 0.365930 0.590424 +vt 0.570082 0.533674 +vt 0.526227 0.426090 +vt 0.448340 0.463064 +vt 0.572156 0.562348 +vt 0.447750 0.137523 +vt 0.104907 0.745859 +vt 0.663187 0.355403 +vt 0.710288 0.619236 +vt 0.427685 0.562039 +vt 0.742870 0.644554 +vt 0.295284 0.378419 +vt 0.473773 0.426090 +vt 0.866152 0.317295 +vt 0.517862 0.528052 +vt 0.257135 0.644560 +vt 0.587247 0.601068 +vt 0.357155 0.395730 +vt 0.499816 0.437019 +vt 0.720122 0.285333 +vt 0.276670 0.636627 +vt 0.412782 0.601030 +vt 0.781070 0.564595 +vt 0.319688 0.429262 +vt 0.499968 0.218629 +vt 0.810858 0.353695 +vt 0.289712 0.631747 +vt 0.218937 0.564589 +vt 0.711045 0.601048 +vt 0.374293 0.219815 +vt 0.499977 0.262981 +vt 0.675343 0.296022 +vt 0.450067 0.599566 +vt 0.288955 0.601048 +vt 0.588166 0.890956 +vt 0.378909 0.425990 +vt 0.499977 0.280615 +vt 0.645735 0.187360 +vt 0.438999 0.603505 +vt 0.412198 0.891099 +vt 0.570304 0.812129 +vt 0.344549 0.254561 +vt 0.499977 0.294066 +vt 0.685945 0.224643 +vt 0.426450 0.610201 +vt 0.429765 0.812166 +vt 0.558266 0.738328 +vt 0.456549 0.180799 +vt 0.499977 0.304722 +vt 0.589072 0.491363 +vt 0.482483 0.422151 +vt 0.441728 0.738324 +vt 0.600409 0.250995 +vt 0.499913 0.178271 +vt 0.500023 0.307652 +vt 0.552012 0.137408 +vt 0.483518 0.437016 +vt 0.399510 0.251079 +vt 0.672684 0.743419 +vt 0.499886 0.133083 +vt 0.500016 0.320776 +vt 0.704663 0.378470 +vt 0.433991 0.417638 +vt 0.327338 0.743473 +vt 0.709250 0.798492 +vt 0.432112 0.506411 +vt 0.500023 0.333766 +vt 0.642764 0.395662 +vt 0.468472 0.444943 +vt 0.290777 0.798554 +vt 0.757824 0.852324 +vt 0.499974 0.560363 +vt 0.500023 0.892950 +vt 0.680198 0.429281 +vt 0.476088 0.436170 +vt 0.242176 0.852324 +vt 0.588354 0.453138 +vt 0.479154 0.557346 +vt 0.499987 0.730081 +vt 0.625560 0.219688 +vt 0.473466 0.454256 +vt 0.411671 0.453035 +vt 0.665586 0.504049 +vt 0.499989 0.530175 +vt 0.499955 0.687602 +vt 0.621009 0.425982 +vt 0.458639 0.520911 +vt 0.334562 0.503927 +vt 0.627543 0.526648 +vt 0.411362 0.195673 +vt 0.289712 0.619236 +vt 0.655317 0.254485 +vt 0.432949 0.430482 +vt 0.372120 0.526586 +vt 0.536915 0.406214 +vt 0.468268 0.647329 +vt 0.499523 0.598938 +vt 0.543283 0.180745 +vt 0.007561 0.519223 +vt 0.463080 0.406216 +vt 0.577268 0.414065 +vt 0.228018 0.316428 +vt 0.499910 0.501747 +vt 0.567985 0.506521 +vt 0.420121 0.589772 +vt 0.422729 0.414015 +vt 0.531915 0.398463 +vt 0.413386 0.307634 +vt 0.500151 0.472844 +vt 0.520797 0.557435 +vt 0.396012 0.583304 +vt 0.468080 0.398465 +vt 0.590372 0.298177 +vt 0.416164 0.631286 +vt 0.482113 0.528021 +vt 0.588371 0.195559 +vt 0.366427 0.573884 +vt 0.409626 0.298177 +vt 0.586800 0.304600 +vt 0.436392 0.640113 +vt 0.499974 0.397628 +vt 0.531597 0.647517 +vt 0.333434 0.566122 +vt 0.413200 0.304600 +vt 0.986046 0.439966 +vt 0.452770 0.579150 +vt 0.500026 0.452513 +vt 0.771915 0.316422 +vt 0.297879 0.566824 +vt 0.499914 0.419853 +vt 0.609945 0.360090 +vt 0.247923 0.398667 +vt 0.499977 0.347466 +vt 0.586614 0.307634 +vt 0.267612 0.575440 +vt 0.013954 0.439966 +vt 0.581691 0.279937 +vt 0.367856 0.336081 +vt 0.583841 0.631286 +vt 0.102986 0.531237 +vt 0.390095 0.360427 +vt 0.576838 0.288154 +vt 0.392400 0.322297 +vt 0.563544 0.640172 +vt 0.241246 0.617214 +vt 0.418309 0.279937 +vt 0.573521 0.296460 +vt 0.400738 0.318931 +vt 0.547226 0.579605 +vt 0.283526 0.666810 +vt 0.423162 0.288154 +vt 0.572058 0.304722 +vt 0.406787 0.314327 +vt 0.752033 0.398685 +vt 0.312876 0.677668 +vt 0.426479 0.296460 +vt 0.526967 0.304722 +vt 0.430012 0.233191 +vt 0.631938 0.336500 +vt 0.344108 0.679849 +f 174/43 156/119 134/220 +f 247/335 34/252 8/399 +f 383/124 399/59 363/216 +f 264/244 467/163 250/317 +f 309/42 416/442 325/427 +f 79/51 96/432 192/416 +f 357/246 390/96 265/239 +f 128/250 35/247 163/91 +f 369/186 265/239 390/96 +f 140/190 163/91 35/247 +f 268/224 1/441 303/70 +f 38/232 73/77 1/441 +f 12/375 303/70 1/441 +f 12/375 1/441 73/77 +f 350/281 452/238 351/276 +f 121/285 122/280 232/425 +f 453/233 351/276 452/238 +f 233/419 232/425 122/280 +f 268/224 303/70 270/214 +f 38/232 40/222 73/77 +f 304/66 270/214 303/70 +f 74/73 73/77 40/222 +f 358/241 344/313 351/276 +f 129/245 122/280 115/318 +f 278/174 351/276 344/313 +f 48/182 115/318 122/280 +f 351/276 453/233 358/241 +f 122/280 129/245 233/419 +f 454/228 358/241 453/233 +f 234/413 233/419 129/245 +f 300/82 334/373 298/90 +f 70/89 68/97 105/378 +f 333/379 298/90 334/373 +f 104/384 105/378 68/97 +f 176/33 153/131 397/68 +f 176/33 172/53 153/131 +f 378/144 397/68 153/131 +f 149/147 153/131 172/53 +f 382/128 385/116 383/124 +f 155/123 156/119 158/111 +f 399/59 383/124 385/116 +f 174/43 158/111 156/119 +f 281/159 348/291 331/391 +f 51/167 102/396 119/295 +f 349/286 331/391 348/291 +f 120/290 119/295 102/396 +f 270/214 304/66 271/209 +f 40/222 41/217 74/73 +f 305/62 271/209 304/66 +f 75/69 74/73 41/217 +f 10/387 337/355 152/135 +f 10/387 152/135 108/360 +f 338/349 152/135 337/355 +f 109/354 108/360 152/135 +f 345/307 279/169 361/226 +f 116/312 132/230 49/177 +f 280/164 361/226 279/169 +f 50/172 49/177 132/230 +f 263/249 432/346 419/424 +f 33/257 195/398 212/60 +f 425/388 419/424 432/346 +f 205/338 212/60 195/398 +f 305/62 409/9 271/209 +f 75/69 41/217 185/456 +f 410/4 271/209 409/9 +f 186/451 185/456 41/217 +f 273/199 311/32 408/14 +f 43/207 184/461 81/41 +f 416/442 408/14 311/32 +f 192/416 81/41 184/461 +f 323/439 271/209 411/467 +f 93/449 187/446 41/217 +f 410/4 411/467 271/209 +f 186/451 41/217 187/446 +f 348/291 450/248 349/286 +f 119/295 120/290 230/437 +f 451/243 349/286 450/248 +f 231/431 230/437 120/290 +f 435/328 433/340 431/352 +f 215/45 211/302 213/55 +f 423/400 431/352 433/340 +f 203/350 213/55 211/302 +f 314/17 315/12 19/333 +f 84/26 19/333 85/21 +f 18/339 19/333 315/12 +f 18/339 85/21 19/333 +f 308/47 376/152 307/52 +f 78/56 77/61 147/155 +f 292/114 307/52 376/152 +f 62/121 147/155 77/61 +f 260/264 388/104 261/259 +f 30/272 31/267 161/99 +f 389/100 261/259 388/104 +f 162/95 161/99 31/267 +f 287/134 415/447 385/116 +f 57/141 158/111 191/422 +f 399/59 385/116 415/447 +f 174/43 191/422 158/111 +f 419/424 425/388 407/19 +f 195/398 183/466 205/338 +f 336/361 407/19 425/388 +f 107/366 205/338 183/466 +f 368/191 417/436 365/206 +f 139/195 136/210 193/410 +f 435/328 365/206 417/436 +f 215/45 193/410 136/210 +f 392/88 424/394 328/409 +f 166/79 99/414 204/344 +f 359/236 328/409 424/394 +f 130/240 204/344 99/414 +f 299/86 302/74 285/142 +f 69/93 55/149 72/81 +f 252/305 285/142 302/74 +f 22/315 72/81 55/149 +f 5/417 276/184 6/411 +f 5/417 6/411 46/192 +f 282/154 6/411 276/184 +f 52/162 46/192 6/411 +f 255/289 374/161 254/294 +f 25/297 24/303 145/165 +f 375/156 254/294 374/161 +f 146/160 145/165 24/303 +f 321/450 322/445 308/47 +f 91/459 78/56 92/454 +f 376/152 308/47 322/445 +f 147/155 92/454 78/56 +f 281/159 426/382 412/462 +f 51/167 188/440 206/332 +f 428/370 412/462 426/382 +f 208/320 206/332 188/440 +f 422/406 314/17 201/362 +f 202/356 201/362 84/26 +f 19/333 201/362 314/17 +f 19/333 84/26 201/362 +f 336/361 322/445 407/19 +f 107/366 183/466 92/454 +f 406/24 407/19 322/445 +f 182/3 92/454 183/466 +f 406/24 322/445 405/29 +f 182/3 181/8 92/454 +f 321/450 405/29 322/445 +f 91/459 92/454 181/8 +f 18/339 315/12 17/345 +f 18/339 17/345 85/21 +f 316/7 17/345 315/12 +f 86/16 85/21 17/345 +f 426/382 267/229 427/376 +f 206/332 207/326 37/237 +f 424/394 427/376 267/229 +f 204/344 37/237 207/326 +f 370/181 397/68 401/49 +f 141/185 177/28 172/53 +f 378/144 401/49 397/68 +f 149/147 172/53 177/28 +f 392/88 270/214 323/439 +f 166/79 93/449 40/222 +f 271/209 323/439 270/214 +f 41/217 40/222 93/449 +f 418/430 466/168 414/452 +f 194/404 190/428 246/341 +f 465/173 414/452 466/168 +f 245/347 246/341 190/428 +f 258/274 259/269 387/108 +f 28/282 160/103 29/277 +f 386/112 387/108 259/269 +f 159/107 29/277 160/103 +f 261/259 389/100 468/158 +f 31/267 248/329 162/95 +f 467/163 468/158 389/100 +f 247/335 162/95 248/329 +f 249/323 457/213 420/418 +f 4/423 197/386 237/395 +f 400/54 420/418 457/213 +f 175/38 237/395 197/386 +f 334/373 299/86 333/379 +f 105/378 104/384 69/93 +f 285/142 333/379 299/86 +f 55/149 69/93 104/384 +f 286/138 9/393 418/430 +f 56/145 194/404 9/393 +f 169/67 418/430 9/393 +f 169/67 9/393 194/404 +f 341/331 262/254 347/296 +f 112/336 118/300 32/262 +f 449/253 347/296 262/254 +f 229/443 32/262 118/300 +f 286/138 418/430 442/288 +f 56/145 222/10 194/404 +f 414/452 442/288 418/430 +f 190/428 194/404 222/10 +f 328/409 461/193 327/415 +f 99/414 98/420 241/371 +f 329/403 327/415 461/193 +f 100/408 241/371 98/420 +f 278/174 356/251 330/397 +f 48/182 101/402 127/255 +f 372/171 330/397 356/251 +f 143/175 127/255 101/402 +f 310/37 393/84 439/304 +f 80/46 219/25 167/75 +f 440/298 439/304 393/84 +f 220/20 167/75 219/25 +f 382/128 383/124 257/279 +f 155/123 27/287 156/119 +f 342/325 257/279 383/124 +f 113/330 156/119 27/287 +f 361/226 280/164 421/412 +f 132/230 199/374 50/172 +f 430/358 421/412 280/164 +f 210/308 50/172 199/374 +f 366/201 365/206 380/136 +f 137/205 151/139 136/210 +f 395/76 380/136 365/206 +f 170/63 136/210 151/139 +f 356/251 278/174 438/310 +f 127/255 218/30 48/182 +f 344/313 438/310 278/174 +f 115/318 48/182 218/30 +f 444/278 445/273 283/150 +f 224/468 53/157 225/463 +f 284/146 283/150 445/273 +f 54/153 225/463 53/157 +f 282/154 276/184 364/211 +f 52/162 135/215 46/192 +f 441/293 364/211 276/184 +f 221/15 46/192 135/215 +f 432/346 263/249 396/72 +f 212/60 171/58 33/257 +f 370/181 396/72 263/249 +f 141/185 33/257 171/58 +f 338/349 300/82 339/343 +f 109/354 110/348 70/89 +f 298/90 339/343 300/82 +f 68/97 70/89 110/348 +f 336/361 274/194 322/445 +f 107/366 92/454 44/202 +f 376/152 322/445 274/194 +f 147/155 44/202 92/454 +f 349/286 451/243 350/281 +f 120/290 121/285 231/431 +f 452/238 350/281 451/243 +f 232/425 231/431 121/285 +f 468/158 360/231 343/319 +f 248/329 114/324 131/235 +f 447/263 343/319 360/231 +f 227/453 131/235 114/324 +f 283/150 284/146 335/367 +f 53/157 106/372 54/153 +f 294/106 335/367 284/146 +f 64/113 54/153 106/372 +f 251/311 459/203 463/183 +f 21/321 243/359 239/383 +f 462/188 463/183 459/203 +f 242/365 239/383 243/359 +f 277/179 354/261 301/78 +f 47/187 71/85 125/265 +f 384/120 301/78 354/261 +f 157/115 125/265 71/85 +f 326/421 293/110 325/427 +f 97/426 96/432 63/117 +f 309/42 325/427 293/110 +f 79/51 63/117 96/432 +f 284/146 277/179 294/106 +f 54/153 64/113 47/187 +f 301/78 294/106 277/179 +f 71/85 47/187 64/113 +f 448/258 265/239 346/301 +f 228/448 117/306 35/247 +f 373/166 346/301 265/239 +f 144/170 35/247 117/306 +f 353/266 346/301 347/296 +f 124/270 118/300 117/306 +f 341/331 347/296 346/301 +f 112/336 117/306 118/300 +f 2/435 20/327 275/189 +f 2/435 45/197 20/327 +f 355/256 275/189 20/327 +f 126/260 20/327 45/197 +f 249/323 282/154 457/213 +f 4/423 237/395 52/162 +f 364/211 457/213 282/154 +f 135/215 52/162 237/395 +f 426/382 427/376 428/370 +f 206/332 208/320 207/326 +f 437/316 428/370 427/376 +f 217/35 207/326 208/320 +f 381/132 382/128 253/299 +f 154/127 23/309 155/123 +f 257/279 253/299 382/128 +f 27/287 155/123 23/309 +f 392/88 394/80 270/214 +f 166/79 40/222 168/71 +f 268/224 270/214 394/80 +f 38/232 168/71 40/222 +f 200/368 429/364 201/362 +f 200/368 201/362 209/314 +f 422/406 201/362 429/364 +f 202/356 209/314 201/362 +f 331/391 330/397 267/229 +f 102/396 37/237 101/402 +f 372/171 267/229 330/397 +f 143/175 101/402 37/237 +f 423/400 433/340 274/194 +f 203/350 44/202 213/55 +f 288/130 274/194 433/340 +f 58/137 213/55 44/202 +f 291/118 251/311 329/403 +f 61/125 100/408 21/321 +f 463/183 329/403 251/311 +f 243/359 21/321 100/408 +f 259/269 287/134 386/112 +f 29/277 159/107 57/141 +f 385/116 386/112 287/134 +f 158/111 57/141 159/107 +f 343/319 447/263 354/261 +f 114/324 125/265 227/453 +f 266/234 354/261 447/263 +f 36/242 227/453 125/265 +f 258/274 387/108 260/264 +f 28/282 30/272 160/103 +f 388/104 260/264 387/108 +f 161/99 160/103 30/272 +f 431/352 423/400 432/346 +f 211/302 212/60 203/350 +f 425/388 432/346 423/400 +f 205/338 203/350 212/60 +f 446/268 343/319 277/179 +f 226/458 47/187 114/324 +f 354/261 277/179 343/319 +f 125/265 114/324 47/187 +f 425/388 423/400 336/361 +f 205/338 107/366 203/350 +f 274/194 336/361 423/400 +f 44/202 203/350 107/366 +f 307/52 293/110 308/47 +f 77/61 78/56 63/117 +f 326/421 308/47 293/110 +f 97/426 63/117 78/56 +f 367/196 448/258 353/266 +f 138/200 124/270 228/448 +f 346/301 353/266 448/258 +f 117/306 228/448 124/270 +f 303/70 269/219 304/66 +f 73/77 74/73 39/227 +f 272/204 304/66 269/219 +f 42/212 39/227 74/73 +f 372/171 359/236 267/229 +f 143/175 37/237 130/240 +f 424/394 267/229 359/236 +f 204/344 130/240 37/237 +f 328/409 295/102 461/193 +f 99/414 241/371 65/109 +f 456/218 461/193 295/102 +f 236/401 65/109 241/371 +f 295/102 332/385 279/169 +f 65/109 49/177 103/390 +f 280/164 279/169 332/385 +f 50/172 103/390 49/177 +f 304/66 272/204 305/62 +f 74/73 75/69 42/212 +f 273/199 305/62 272/204 +f 43/207 42/212 75/69 +f 428/370 437/316 435/328 +f 208/320 215/45 217/35 +f 433/340 435/328 437/316 +f 213/55 217/35 215/45 +f 305/62 273/199 409/9 +f 75/69 185/456 43/207 +f 408/14 409/9 273/199 +f 184/461 43/207 185/456 +f 395/76 431/352 396/72 +f 170/63 171/58 211/302 +f 432/346 396/72 431/352 +f 212/60 211/302 171/58 +f 396/72 370/181 379/140 +f 171/58 150/143 141/185 +f 401/49 379/140 370/181 +f 177/28 141/185 150/143 +f 297/94 335/367 300/82 +f 67/101 70/89 106/372 +f 334/373 300/82 335/367 +f 105/378 106/372 70/89 +f 418/430 169/67 352/271 +f 194/404 123/275 169/67 +f 7/405 352/271 169/67 +f 7/405 169/67 123/275 +f 281/159 412/462 353/266 +f 51/167 124/270 188/440 +f 377/148 353/266 412/462 +f 148/151 188/440 124/270 +f 320/455 321/450 326/421 +f 90/464 97/426 91/459 +f 308/47 326/421 321/450 +f 78/56 91/459 97/426 +f 286/138 296/98 337/355 +f 56/145 108/360 66/105 +f 297/94 337/355 296/98 +f 67/101 66/105 108/360 +f 405/29 321/450 404/34 +f 181/8 180/13 91/459 +f 320/455 404/34 321/450 +f 90/464 91/459 180/13 +f 331/391 349/286 330/397 +f 102/396 101/402 120/290 +f 350/281 330/397 349/286 +f 121/285 120/290 101/402 +f 335/367 294/106 334/373 +f 106/372 105/378 64/113 +f 299/86 334/373 294/106 +f 69/93 64/113 105/378 +f 324/433 455/223 367/196 +f 94/444 138/200 235/407 +f 448/258 367/196 455/223 +f 228/448 235/407 138/200 +f 17/345 316/7 16/351 +f 17/345 16/351 86/16 +f 317/2 16/351 316/7 +f 87/11 86/16 16/351 +f 430/358 280/164 359/236 +f 210/308 130/240 50/172 +f 332/385 359/236 280/164 +f 103/390 50/172 130/240 +f 16/351 317/2 15/357 +f 16/351 15/357 87/11 +f 318/465 15/357 317/2 +f 88/6 87/11 15/357 +f 9/393 286/138 10/387 +f 9/393 10/387 56/145 +f 337/355 10/387 286/138 +f 108/360 56/145 10/387 +f 330/397 350/281 278/174 +f 101/402 48/182 121/285 +f 351/276 278/174 350/281 +f 122/280 121/285 48/182 +f 253/299 254/294 381/132 +f 23/309 154/127 24/303 +f 375/156 381/132 254/294 +f 146/160 24/303 154/127 +f 403/39 404/34 319/460 +f 179/18 89/1 180/13 +f 320/455 319/460 404/34 +f 90/464 180/13 89/1 +f 352/271 7/405 420/418 +f 123/275 197/386 7/405 +f 198/380 420/418 7/405 +f 198/380 7/405 197/386 +f 325/427 319/460 326/421 +f 96/432 97/426 89/1 +f 320/455 326/421 319/460 +f 90/464 89/1 97/426 +f 398/64 368/191 366/201 +f 173/48 137/205 139/195 +f 365/206 366/201 368/191 +f 136/210 139/195 137/205 +f 289/126 436/322 398/64 +f 59/133 173/48 216/40 +f 368/191 398/64 436/322 +f 139/195 216/40 173/48 +f 439/304 440/298 345/307 +f 219/25 116/312 220/20 +f 279/169 345/307 440/298 +f 49/177 220/20 116/312 +f 272/204 312/27 273/199 +f 42/212 43/207 82/36 +f 311/32 273/199 312/27 +f 81/41 82/36 43/207 +f 6/411 282/154 196/392 +f 6/411 196/392 52/162 +f 249/323 196/392 282/154 +f 4/423 52/162 196/392 +f 274/194 288/130 376/152 +f 44/202 147/155 58/137 +f 292/114 376/152 288/130 +f 62/121 58/137 147/155 +f 397/68 429/364 176/33 +f 172/53 176/33 209/314 +f 200/368 176/33 429/364 +f 200/368 209/314 176/33 +f 269/219 313/22 272/204 +f 39/227 42/212 83/31 +f 312/27 272/204 313/22 +f 82/36 83/31 42/212 +f 445/273 446/268 284/146 +f 225/463 54/153 226/458 +f 277/179 284/146 446/268 +f 47/187 226/458 54/153 +f 255/289 340/337 374/161 +f 25/297 145/165 111/342 +f 391/92 374/161 340/337 +f 164/87 111/342 145/165 +f 296/98 283/150 297/94 +f 66/105 67/101 53/157 +f 335/367 297/94 283/150 +f 106/372 53/157 67/101 +f 347/296 449/253 348/291 +f 118/300 119/295 229/443 +f 450/248 348/291 449/253 +f 230/437 229/443 119/295 +f 455/223 357/246 448/258 +f 235/407 228/448 128/250 +f 265/239 448/258 357/246 +f 35/247 128/250 228/448 +f 337/355 297/94 338/349 +f 108/360 109/354 67/101 +f 300/82 338/349 297/94 +f 70/89 67/101 109/354 +f 152/135 338/349 11/381 +f 152/135 11/381 109/354 +f 339/343 11/381 338/349 +f 110/348 109/354 11/381 +f 279/169 440/298 295/102 +f 49/177 65/109 220/20 +f 456/218 295/102 440/298 +f 236/401 220/20 65/109 +f 408/14 416/442 293/110 +f 184/461 63/117 192/416 +f 309/42 293/110 416/442 +f 79/51 192/416 63/117 +f 359/236 372/171 430/358 +f 130/240 210/308 143/175 +f 356/251 430/358 372/171 +f 127/255 143/175 210/308 +f 346/301 373/166 341/331 +f 117/306 112/336 144/170 +f 266/234 341/331 373/166 +f 36/242 144/170 112/336 +f 389/100 391/92 467/163 +f 162/95 247/335 164/87 +f 250/317 467/163 391/92 +f 8/399 164/87 247/335 +f 353/266 347/296 281/159 +f 124/270 51/167 118/300 +f 348/291 281/159 347/296 +f 119/295 118/300 51/167 +f 296/98 443/283 283/150 +f 66/105 53/157 223/5 +f 444/278 283/150 443/283 +f 224/468 223/5 53/157 +f 20/327 95/438 355/256 +f 20/327 126/260 95/438 +f 371/176 355/256 95/438 +f 142/180 95/438 126/260 +f 296/98 286/138 443/283 +f 66/105 223/5 56/145 +f 442/288 443/283 286/138 +f 222/10 56/145 223/5 +f 420/418 198/380 249/323 +f 197/386 4/423 198/380 +f 196/392 249/323 198/380 +f 196/392 198/380 4/423 +f 360/231 264/244 256/284 +f 131/235 26/292 34/252 +f 250/317 256/284 264/244 +f 8/399 34/252 26/292 +f 276/184 275/189 441/293 +f 46/192 221/15 45/197 +f 458/208 441/293 275/189 +f 238/389 45/197 221/15 +f 301/78 384/120 302/74 +f 71/85 72/81 157/115 +f 369/186 302/74 384/120 +f 140/190 157/115 72/81 +f 418/430 352/271 466/168 +f 194/404 246/341 123/275 +f 413/457 466/168 352/271 +f 189/434 123/275 246/341 +f 467/163 264/244 468/158 +f 247/335 248/329 34/252 +f 360/231 468/158 264/244 +f 131/235 34/252 248/329 +f 390/96 252/305 369/186 +f 163/91 140/190 22/315 +f 302/74 369/186 252/305 +f 72/81 22/315 140/190 +f 375/156 387/108 381/132 +f 146/160 154/127 160/103 +f 386/112 381/132 387/108 +f 159/107 160/103 154/127 +f 380/136 395/76 379/140 +f 151/139 150/143 170/63 +f 396/72 379/140 395/76 +f 171/58 170/63 150/143 +f 352/271 420/418 413/457 +f 123/275 189/434 197/386 +f 400/54 413/457 420/418 +f 175/38 197/386 189/434 +f 427/376 323/439 437/316 +f 207/326 217/35 93/449 +f 411/467 437/316 323/439 +f 187/446 93/449 217/35 +f 388/104 374/161 389/100 +f 161/99 162/95 145/165 +f 391/92 389/100 374/161 +f 164/87 145/165 162/95 +f 394/80 327/415 165/83 +f 168/71 165/83 98/420 +f 3/429 165/83 327/415 +f 3/429 98/420 165/83 +f 355/256 371/176 462/188 +f 126/260 242/365 142/180 +f 463/183 462/188 371/176 +f 243/359 142/180 242/365 +f 1/441 268/224 165/83 +f 1/441 165/83 38/232 +f 394/80 165/83 268/224 +f 168/71 38/232 165/83 +f 12/375 13/369 303/70 +f 12/375 73/77 13/369 +f 269/219 303/70 13/369 +f 39/227 13/369 73/77 +f 387/108 375/156 388/104 +f 160/103 161/99 146/160 +f 374/161 388/104 375/156 +f 145/165 146/160 161/99 +f 13/369 14/363 269/219 +f 13/369 39/227 14/363 +f 313/22 269/219 14/363 +f 83/31 14/363 39/227 +f 294/106 301/78 299/86 +f 64/113 69/93 71/85 +f 302/74 299/86 301/78 +f 72/81 71/85 69/93 +f 341/331 266/234 262/254 +f 112/336 32/262 36/242 +f 447/263 262/254 266/234 +f 227/453 36/242 32/262 +f 381/132 386/112 382/128 +f 154/127 155/123 159/107 +f 385/116 382/128 386/112 +f 158/111 159/107 155/123 +f 281/159 331/391 426/382 +f 51/167 206/332 102/396 +f 267/229 426/382 331/391 +f 37/237 102/396 206/332 +f 424/394 392/88 427/376 +f 204/344 207/326 166/79 +f 323/439 427/376 392/88 +f 93/449 166/79 207/326 +f 430/358 356/251 421/412 +f 210/308 199/374 127/255 +f 438/310 421/412 356/251 +f 218/30 127/255 199/374 +f 392/88 328/409 394/80 +f 166/79 168/71 99/414 +f 327/415 394/80 328/409 +f 98/420 99/414 168/71 +f 458/208 439/304 441/293 +f 238/389 221/15 219/25 +f 345/307 441/293 439/304 +f 116/312 219/25 221/15 +f 383/124 363/216 342/325 +f 156/119 113/330 134/220 +f 464/178 342/325 363/216 +f 244/353 134/220 113/330 +f 458/208 462/188 460/198 +f 238/389 240/377 242/365 +f 459/203 460/198 462/188 +f 239/383 242/365 240/377 +f 435/328 431/352 365/206 +f 215/45 136/210 211/302 +f 395/76 365/206 431/352 +f 170/63 211/302 136/210 +f 415/447 464/178 399/59 +f 191/422 174/43 244/353 +f 363/216 399/59 464/178 +f 134/220 244/353 174/43 +f 263/249 429/364 370/181 +f 33/257 141/185 209/314 +f 397/68 370/181 429/364 +f 172/53 209/314 141/185 +f 458/208 275/189 462/188 +f 238/389 242/365 45/197 +f 355/256 462/188 275/189 +f 126/260 45/197 242/365 +f 317/2 404/34 318/465 +f 87/11 88/6 180/13 +f 403/39 318/465 404/34 +f 179/18 180/13 88/6 +f 316/7 405/29 317/2 +f 86/16 87/11 181/8 +f 404/34 317/2 405/29 +f 180/13 181/8 87/11 +f 315/12 406/24 316/7 +f 85/21 86/16 182/3 +f 405/29 316/7 406/24 +f 181/8 182/3 86/16 +f 314/17 407/19 315/12 +f 84/26 85/21 183/466 +f 406/24 315/12 407/19 +f 182/3 183/466 85/21 +f 419/424 407/19 422/406 +f 195/398 202/356 183/466 +f 314/17 422/406 407/19 +f 84/26 183/466 202/356 +f 367/196 402/44 324/433 +f 138/200 94/444 178/23 +f 362/221 324/433 402/44 +f 133/225 178/23 94/444 +f 409/9 408/14 307/52 +f 185/456 77/61 184/461 +f 293/110 307/52 408/14 +f 63/117 184/461 77/61 +f 409/9 307/52 410/4 +f 185/456 186/451 77/61 +f 292/114 410/4 307/52 +f 62/121 77/61 186/451 +f 411/467 410/4 288/130 +f 187/446 58/137 186/451 +f 292/114 288/130 410/4 +f 62/121 186/451 58/137 +f 437/316 411/467 433/340 +f 217/35 213/55 187/446 +f 288/130 433/340 411/467 +f 58/137 187/446 213/55 +f 435/328 417/436 428/370 +f 215/45 208/320 193/410 +f 412/462 428/370 417/436 +f 188/440 193/410 208/320 +f 265/239 369/186 373/166 +f 35/247 144/170 140/190 +f 384/120 373/166 369/186 +f 157/115 140/190 144/170 +f 458/208 460/198 439/304 +f 238/389 219/25 240/377 +f 310/37 439/304 460/198 +f 80/46 240/377 219/25 +f 353/266 377/148 367/196 +f 124/270 138/200 148/151 +f 402/44 367/196 377/148 +f 178/23 148/151 138/200 +f 5/417 2/435 276/184 +f 5/417 46/192 2/435 +f 275/189 276/184 2/435 +f 45/197 2/435 46/192 +f 429/364 263/249 422/406 +f 209/314 202/356 33/257 +f 419/424 422/406 263/249 +f 195/398 33/257 202/356 +f 328/409 359/236 295/102 +f 99/414 65/109 130/240 +f 332/385 295/102 359/236 +f 103/390 130/240 65/109 +f 368/191 436/322 417/436 +f 139/195 193/410 216/40 +f 434/334 417/436 436/322 +f 214/50 216/40 193/410 +f 456/218 440/298 290/122 +f 236/401 60/129 220/20 +f 393/84 290/122 440/298 +f 167/75 220/20 60/129 +f 329/403 463/183 327/415 +f 100/408 98/420 243/359 +f 371/176 327/415 463/183 +f 142/180 243/359 98/420 +f 327/415 371/176 3/429 +f 98/420 3/429 142/180 +f 95/438 3/429 371/176 +f 95/438 142/180 3/429 +f 461/193 456/218 306/57 +f 241/371 76/65 236/401 +f 290/122 306/57 456/218 +f 60/129 236/401 76/65 +f 449/253 340/337 450/248 +f 229/443 230/437 111/342 +f 255/289 450/248 340/337 +f 25/297 111/342 230/437 +f 262/254 447/263 256/284 +f 32/262 26/292 227/453 +f 360/231 256/284 447/263 +f 131/235 227/453 26/292 +f 450/248 255/289 451/243 +f 230/437 231/431 25/297 +f 254/294 451/243 255/289 +f 24/303 25/297 231/431 +f 451/243 254/294 452/238 +f 231/431 232/425 24/303 +f 253/299 452/238 254/294 +f 23/309 24/303 232/425 +f 452/238 253/299 453/233 +f 232/425 233/419 23/309 +f 257/279 453/233 253/299 +f 27/287 23/309 233/419 +f 257/279 342/325 453/233 +f 27/287 233/419 113/330 +f 454/228 453/233 342/325 +f 234/413 113/330 233/419 +f 414/452 465/173 415/447 +f 190/428 191/422 245/347 +f 464/178 415/447 465/173 +f 244/353 245/347 191/422 +f 442/288 414/452 287/134 +f 222/10 57/141 190/428 +f 415/447 287/134 414/452 +f 191/422 190/428 57/141 +f 442/288 287/134 443/283 +f 222/10 223/5 57/141 +f 259/269 443/283 287/134 +f 29/277 57/141 223/5 +f 443/283 259/269 444/278 +f 223/5 224/468 29/277 +f 258/274 444/278 259/269 +f 28/282 29/277 224/468 +f 445/273 444/278 260/264 +f 225/463 30/272 224/468 +f 258/274 260/264 444/278 +f 28/282 224/468 30/272 +f 260/264 261/259 445/273 +f 30/272 225/463 31/267 +f 446/268 445/273 261/259 +f 226/458 31/267 225/463 +f 261/259 468/158 446/268 +f 31/267 226/458 248/329 +f 343/319 446/268 468/158 +f 114/324 248/329 226/458 +f 251/311 310/37 459/203 +f 21/321 239/383 80/46 +f 460/198 459/203 310/37 +f 240/377 80/46 239/383 +f 291/118 306/57 393/84 +f 61/125 167/75 76/65 +f 290/122 393/84 306/57 +f 60/129 76/65 167/75 +f 461/193 306/57 329/403 +f 241/371 100/408 76/65 +f 291/118 329/403 306/57 +f 61/125 76/65 100/408 +f 377/148 434/334 402/44 +f 148/151 178/23 214/50 +f 436/322 402/44 434/334 +f 216/40 214/50 178/23 +f 251/311 291/118 310/37 +f 21/321 80/46 61/125 +f 393/84 310/37 291/118 +f 167/75 61/125 80/46 +f 412/462 417/436 377/148 +f 188/440 148/151 193/410 +f 434/334 377/148 417/436 +f 214/50 193/410 148/151 +f 342/325 464/178 454/228 +f 113/330 234/413 244/353 +f 465/173 454/228 464/178 +f 245/347 244/353 234/413 +f 454/228 465/173 358/241 +f 234/413 129/245 245/347 +f 466/168 358/241 465/173 +f 246/341 245/347 129/245 +f 413/457 344/313 466/168 +f 189/434 246/341 115/318 +f 358/241 466/168 344/313 +f 129/245 115/318 246/341 +f 438/310 344/313 400/54 +f 218/30 175/38 115/318 +f 413/457 400/54 344/313 +f 189/434 115/318 175/38 +f 364/211 441/293 361/226 +f 135/215 132/230 221/15 +f 345/307 361/226 441/293 +f 116/312 221/15 132/230 +f 457/213 421/412 400/54 +f 237/395 175/38 199/374 +f 438/310 400/54 421/412 +f 218/30 199/374 175/38 +f 457/213 364/211 421/412 +f 237/395 199/374 135/215 +f 361/226 421/412 364/211 +f 132/230 135/215 199/374 +f 362/221 402/44 289/126 +f 133/225 59/133 178/23 +f 436/322 289/126 402/44 +f 216/40 178/23 59/133 +f 354/261 266/234 384/120 +f 125/265 157/115 36/242 +f 373/166 384/120 266/234 +f 144/170 36/242 157/115 +f 256/284 250/317 340/337 +f 26/292 111/342 8/399 +f 391/92 340/337 250/317 +f 164/87 8/399 111/342 +f 262/254 256/284 449/253 +f 32/262 229/443 26/292 +f 340/337 449/253 256/284 +f 111/342 26/292 229/443 +f 15/357 318/465 14/363 +f 15/357 14/363 88/6 +f 313/22 14/363 318/465 +f 83/31 88/6 14/363 +f 318/465 403/39 313/22 +f 88/6 83/31 179/18 +f 312/27 313/22 403/39 +f 82/36 179/18 83/31 +f 403/39 319/460 312/27 +f 179/18 82/36 89/1 +f 311/32 312/27 319/460 +f 81/41 89/1 82/36 +f 319/460 325/427 311/32 +f 89/1 81/41 96/432 +f 416/442 311/32 325/427 +f 192/416 96/432 81/41 diff --git a/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model_uv_visualization.png b/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model_uv_visualization.png new file mode 100644 index 00000000..2acd991e Binary files /dev/null and b/mediapipe/tasks/cc/vision/face_geometry/data/canonical_face_model_uv_visualization.png differ diff --git a/mediapipe/tasks/cc/vision/face_geometry/data/geometry_pipeline_metadata_detection.pbtxt b/mediapipe/tasks/cc/vision/face_geometry/data/geometry_pipeline_metadata_detection.pbtxt new file mode 100644 index 00000000..d221d082 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/data/geometry_pipeline_metadata_detection.pbtxt @@ -0,0 +1,78 @@ +# Copyright 2023 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. + +input_source: FACE_DETECTION_PIPELINE +procrustes_landmark_basis { landmark_id: 0 weight: 1.0 } +procrustes_landmark_basis { landmark_id: 1 weight: 1.0 } +procrustes_landmark_basis { landmark_id: 2 weight: 1.0 } +procrustes_landmark_basis { landmark_id: 3 weight: 1.0 } +procrustes_landmark_basis { landmark_id: 4 weight: 1.0 } +procrustes_landmark_basis { landmark_id: 5 weight: 1.0 } +# NOTE: the triangular topology of the face meshes is only useful when derived +# from the 468 face landmarks, not from the 6 face detection landmarks +# (keypoints). The former don't cover the entire face and this mesh is +# defined here only to comply with the API. It should be considered as +# a placeholder and/or for debugging purposes. +# +# Use the face geometry derived from the face detection landmarks +# (keypoints) for the face pose transformation matrix, not the mesh. +canonical_mesh: { + vertex_type: VERTEX_PT + primitive_type: TRIANGLE + vertex_buffer: -3.1511454582214355 + vertex_buffer: 2.6246179342269897 + vertex_buffer: 3.4656630754470825 + vertex_buffer: 0.349575996398926 + vertex_buffer: 0.38137748837470997 + vertex_buffer: 3.1511454582214355 + vertex_buffer: 2.6246179342269897 + vertex_buffer: 3.4656630754470825 + vertex_buffer: 0.650443494319916 + vertex_buffer: 0.38137999176979054 + vertex_buffer: 0.0 + vertex_buffer: -1.126865029335022 + vertex_buffer: 7.475604057312012 + vertex_buffer: 0.500025987625122 + vertex_buffer: 0.547487020492554 + vertex_buffer: 0.0 + vertex_buffer: -4.304508209228516 + vertex_buffer: 4.162498950958252 + vertex_buffer: 0.499989986419678 + vertex_buffer: 0.694203019142151 + vertex_buffer: -7.664182186126709 + vertex_buffer: 0.673132002353668 + vertex_buffer: -2.435867071151733 + vertex_buffer: 0.007561000064015 + vertex_buffer: 0.480777025222778 + vertex_buffer: 7.664182186126709 + vertex_buffer: 0.673132002353668 + vertex_buffer: -2.435867071151733 + vertex_buffer: 0.992439985275269 + vertex_buffer: 0.480777025222778 + index_buffer: 0 + index_buffer: 1 + index_buffer: 2 + index_buffer: 1 + index_buffer: 5 + index_buffer: 2 + index_buffer: 4 + index_buffer: 0 + index_buffer: 2 + index_buffer: 4 + index_buffer: 2 + index_buffer: 3 + index_buffer: 2 + index_buffer: 5 + index_buffer: 3 +} diff --git a/mediapipe/tasks/cc/vision/face_geometry/data/geometry_pipeline_metadata_landmarks.pbtxt b/mediapipe/tasks/cc/vision/face_geometry/data/geometry_pipeline_metadata_landmarks.pbtxt new file mode 100644 index 00000000..252a7b05 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/data/geometry_pipeline_metadata_landmarks.pbtxt @@ -0,0 +1,5086 @@ +# Copyright 2023 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. + +input_source: FACE_LANDMARK_PIPELINE +procrustes_landmark_basis { landmark_id: 4 weight: 0.070909939706326 } +procrustes_landmark_basis { landmark_id: 6 weight: 0.032100144773722 } +procrustes_landmark_basis { landmark_id: 10 weight: 0.008446550928056 } +procrustes_landmark_basis { landmark_id: 33 weight: 0.058724168688059 } +procrustes_landmark_basis { landmark_id: 54 weight: 0.007667080033571 } +procrustes_landmark_basis { landmark_id: 67 weight: 0.009078059345484 } +procrustes_landmark_basis { landmark_id: 117 weight: 0.009791937656701 } +procrustes_landmark_basis { landmark_id: 119 weight: 0.014565368182957 } +procrustes_landmark_basis { landmark_id: 121 weight: 0.018591361120343 } +procrustes_landmark_basis { landmark_id: 127 weight: 0.005197994410992 } +procrustes_landmark_basis { landmark_id: 129 weight: 0.120625205338001 } +procrustes_landmark_basis { landmark_id: 132 weight: 0.005560018587857 } +procrustes_landmark_basis { landmark_id: 133 weight: 0.05328618362546 } +procrustes_landmark_basis { landmark_id: 136 weight: 0.066890455782413 } +procrustes_landmark_basis { landmark_id: 143 weight: 0.014816547743976 } +procrustes_landmark_basis { landmark_id: 147 weight: 0.014262833632529 } +procrustes_landmark_basis { landmark_id: 198 weight: 0.025462191551924 } +procrustes_landmark_basis { landmark_id: 205 weight: 0.047252278774977 } +procrustes_landmark_basis { landmark_id: 263 weight: 0.058724168688059 } +procrustes_landmark_basis { landmark_id: 284 weight: 0.007667080033571 } +procrustes_landmark_basis { landmark_id: 297 weight: 0.009078059345484 } +procrustes_landmark_basis { landmark_id: 346 weight: 0.009791937656701 } +procrustes_landmark_basis { landmark_id: 348 weight: 0.014565368182957 } +procrustes_landmark_basis { landmark_id: 350 weight: 0.018591361120343 } +procrustes_landmark_basis { landmark_id: 356 weight: 0.005197994410992 } +procrustes_landmark_basis { landmark_id: 358 weight: 0.120625205338001 } +procrustes_landmark_basis { landmark_id: 361 weight: 0.005560018587857 } +procrustes_landmark_basis { landmark_id: 362 weight: 0.05328618362546 } +procrustes_landmark_basis { landmark_id: 365 weight: 0.066890455782413 } +procrustes_landmark_basis { landmark_id: 372 weight: 0.014816547743976 } +procrustes_landmark_basis { landmark_id: 376 weight: 0.014262833632529 } +procrustes_landmark_basis { landmark_id: 420 weight: 0.025462191551924 } +procrustes_landmark_basis { landmark_id: 425 weight: 0.047252278774977 } +canonical_mesh: { + vertex_type: VERTEX_PT + primitive_type: TRIANGLE + vertex_buffer: 0.000000 + vertex_buffer: -3.406404 + vertex_buffer: 5.979507 + vertex_buffer: 0.499977 + vertex_buffer: 0.652534 + vertex_buffer: 0.000000 + vertex_buffer: -1.126865 + vertex_buffer: 7.475604 + vertex_buffer: 0.500026 + vertex_buffer: 0.547487 + vertex_buffer: 0.000000 + vertex_buffer: -2.089024 + vertex_buffer: 6.058267 + vertex_buffer: 0.499974 + vertex_buffer: 0.602372 + vertex_buffer: -0.463928 + vertex_buffer: 0.955357 + vertex_buffer: 6.633583 + vertex_buffer: 0.482113 + vertex_buffer: 0.471979 + vertex_buffer: 0.000000 + vertex_buffer: -0.463170 + vertex_buffer: 7.586580 + vertex_buffer: 0.500151 + vertex_buffer: 0.527156 + vertex_buffer: 0.000000 + vertex_buffer: 0.365669 + vertex_buffer: 7.242870 + vertex_buffer: 0.499910 + vertex_buffer: 0.498253 + vertex_buffer: 0.000000 + vertex_buffer: 2.473255 + vertex_buffer: 5.788627 + vertex_buffer: 0.499523 + vertex_buffer: 0.401062 + vertex_buffer: -4.253081 + vertex_buffer: 2.577646 + vertex_buffer: 3.279702 + vertex_buffer: 0.289712 + vertex_buffer: 0.380764 + vertex_buffer: 0.000000 + vertex_buffer: 4.019042 + vertex_buffer: 5.284764 + vertex_buffer: 0.499955 + vertex_buffer: 0.312398 + vertex_buffer: 0.000000 + vertex_buffer: 4.885979 + vertex_buffer: 5.385258 + vertex_buffer: 0.499987 + vertex_buffer: 0.269919 + vertex_buffer: 0.000000 + vertex_buffer: 8.261778 + vertex_buffer: 4.481535 + vertex_buffer: 0.500023 + vertex_buffer: 0.107050 + vertex_buffer: 0.000000 + vertex_buffer: -3.706811 + vertex_buffer: 5.864924 + vertex_buffer: 0.500023 + vertex_buffer: 0.666234 + vertex_buffer: 0.000000 + vertex_buffer: -3.918301 + vertex_buffer: 5.569430 + vertex_buffer: 0.500016 + vertex_buffer: 0.679224 + vertex_buffer: 0.000000 + vertex_buffer: -3.994436 + vertex_buffer: 5.219482 + vertex_buffer: 0.500023 + vertex_buffer: 0.692348 + vertex_buffer: 0.000000 + vertex_buffer: -4.542400 + vertex_buffer: 5.404754 + vertex_buffer: 0.499977 + vertex_buffer: 0.695278 + vertex_buffer: 0.000000 + vertex_buffer: -4.745577 + vertex_buffer: 5.529457 + vertex_buffer: 0.499977 + vertex_buffer: 0.705934 + vertex_buffer: 0.000000 + vertex_buffer: -5.019567 + vertex_buffer: 5.601448 + vertex_buffer: 0.499977 + vertex_buffer: 0.719385 + vertex_buffer: 0.000000 + vertex_buffer: -5.365123 + vertex_buffer: 5.535441 + vertex_buffer: 0.499977 + vertex_buffer: 0.737019 + vertex_buffer: 0.000000 + vertex_buffer: -6.149624 + vertex_buffer: 5.071372 + vertex_buffer: 0.499968 + vertex_buffer: 0.781371 + vertex_buffer: 0.000000 + vertex_buffer: -1.501095 + vertex_buffer: 7.112196 + vertex_buffer: 0.499816 + vertex_buffer: 0.562981 + vertex_buffer: -0.416106 + vertex_buffer: -1.466449 + vertex_buffer: 6.447657 + vertex_buffer: 0.473773 + vertex_buffer: 0.573910 + vertex_buffer: -7.087960 + vertex_buffer: 5.434801 + vertex_buffer: 0.099620 + vertex_buffer: 0.104907 + vertex_buffer: 0.254141 + vertex_buffer: -2.628639 + vertex_buffer: 2.035898 + vertex_buffer: 3.848121 + vertex_buffer: 0.365930 + vertex_buffer: 0.409576 + vertex_buffer: -3.198363 + vertex_buffer: 1.985815 + vertex_buffer: 3.796952 + vertex_buffer: 0.338758 + vertex_buffer: 0.413025 + vertex_buffer: -3.775151 + vertex_buffer: 2.039402 + vertex_buffer: 3.646194 + vertex_buffer: 0.311120 + vertex_buffer: 0.409460 + vertex_buffer: -4.465819 + vertex_buffer: 2.422950 + vertex_buffer: 3.155168 + vertex_buffer: 0.274658 + vertex_buffer: 0.389131 + vertex_buffer: -2.164289 + vertex_buffer: 2.189867 + vertex_buffer: 3.851822 + vertex_buffer: 0.393362 + vertex_buffer: 0.403706 + vertex_buffer: -3.208229 + vertex_buffer: 3.223926 + vertex_buffer: 4.115822 + vertex_buffer: 0.345234 + vertex_buffer: 0.344011 + vertex_buffer: -2.673803 + vertex_buffer: 3.205337 + vertex_buffer: 4.092203 + vertex_buffer: 0.370094 + vertex_buffer: 0.346076 + vertex_buffer: -3.745193 + vertex_buffer: 3.165286 + vertex_buffer: 3.972409 + vertex_buffer: 0.319322 + vertex_buffer: 0.347265 + vertex_buffer: -4.161018 + vertex_buffer: 3.059069 + vertex_buffer: 3.719554 + vertex_buffer: 0.297903 + vertex_buffer: 0.353591 + vertex_buffer: -5.062006 + vertex_buffer: 1.934418 + vertex_buffer: 2.776093 + vertex_buffer: 0.247792 + vertex_buffer: 0.410810 + vertex_buffer: -2.266659 + vertex_buffer: -7.425768 + vertex_buffer: 4.389812 + vertex_buffer: 0.396889 + vertex_buffer: 0.842755 + vertex_buffer: -4.445859 + vertex_buffer: 2.663991 + vertex_buffer: 3.173422 + vertex_buffer: 0.280098 + vertex_buffer: 0.375600 + vertex_buffer: -7.214530 + vertex_buffer: 2.263009 + vertex_buffer: 0.073150 + vertex_buffer: 0.106310 + vertex_buffer: 0.399956 + vertex_buffer: -5.799793 + vertex_buffer: 2.349546 + vertex_buffer: 2.204059 + vertex_buffer: 0.209925 + vertex_buffer: 0.391353 + vertex_buffer: -2.844939 + vertex_buffer: -0.720868 + vertex_buffer: 4.433130 + vertex_buffer: 0.355808 + vertex_buffer: 0.534406 + vertex_buffer: -0.711452 + vertex_buffer: -3.329355 + vertex_buffer: 5.877044 + vertex_buffer: 0.471751 + vertex_buffer: 0.650404 + vertex_buffer: -0.606033 + vertex_buffer: -3.924562 + vertex_buffer: 5.444923 + vertex_buffer: 0.474155 + vertex_buffer: 0.680192 + vertex_buffer: -1.431615 + vertex_buffer: -3.500953 + vertex_buffer: 5.496189 + vertex_buffer: 0.439785 + vertex_buffer: 0.657229 + vertex_buffer: -1.914910 + vertex_buffer: -3.803146 + vertex_buffer: 5.028930 + vertex_buffer: 0.414617 + vertex_buffer: 0.666541 + vertex_buffer: -1.131043 + vertex_buffer: -3.973937 + vertex_buffer: 5.189648 + vertex_buffer: 0.450374 + vertex_buffer: 0.680861 + vertex_buffer: -1.563548 + vertex_buffer: -4.082763 + vertex_buffer: 4.842263 + vertex_buffer: 0.428771 + vertex_buffer: 0.682691 + vertex_buffer: -2.650112 + vertex_buffer: -5.003649 + vertex_buffer: 4.188483 + vertex_buffer: 0.374971 + vertex_buffer: 0.727805 + vertex_buffer: -0.427049 + vertex_buffer: -1.094134 + vertex_buffer: 7.360529 + vertex_buffer: 0.486717 + vertex_buffer: 0.547629 + vertex_buffer: -0.496396 + vertex_buffer: -0.475659 + vertex_buffer: 7.440358 + vertex_buffer: 0.485301 + vertex_buffer: 0.527395 + vertex_buffer: -5.253307 + vertex_buffer: 3.881582 + vertex_buffer: 3.363159 + vertex_buffer: 0.257765 + vertex_buffer: 0.314490 + vertex_buffer: -1.718698 + vertex_buffer: 0.974609 + vertex_buffer: 4.558359 + vertex_buffer: 0.401223 + vertex_buffer: 0.455172 + vertex_buffer: -1.608635 + vertex_buffer: -0.942516 + vertex_buffer: 5.814193 + vertex_buffer: 0.429819 + vertex_buffer: 0.548615 + vertex_buffer: -1.651267 + vertex_buffer: -0.610868 + vertex_buffer: 5.581319 + vertex_buffer: 0.421352 + vertex_buffer: 0.533741 + vertex_buffer: -4.765501 + vertex_buffer: -0.701554 + vertex_buffer: 3.534632 + vertex_buffer: 0.276896 + vertex_buffer: 0.532057 + vertex_buffer: -0.478306 + vertex_buffer: 0.295766 + vertex_buffer: 7.101013 + vertex_buffer: 0.483370 + vertex_buffer: 0.499587 + vertex_buffer: -3.734964 + vertex_buffer: 4.508230 + vertex_buffer: 4.550454 + vertex_buffer: 0.337212 + vertex_buffer: 0.282883 + vertex_buffer: -4.588603 + vertex_buffer: 4.302037 + vertex_buffer: 4.048484 + vertex_buffer: 0.296392 + vertex_buffer: 0.293243 + vertex_buffer: -6.279331 + vertex_buffer: 6.615427 + vertex_buffer: 1.425850 + vertex_buffer: 0.169295 + vertex_buffer: 0.193814 + vertex_buffer: -1.220941 + vertex_buffer: 4.142165 + vertex_buffer: 5.106035 + vertex_buffer: 0.447580 + vertex_buffer: 0.302610 + vertex_buffer: -2.193489 + vertex_buffer: 3.100317 + vertex_buffer: 4.000575 + vertex_buffer: 0.392390 + vertex_buffer: 0.353888 + vertex_buffer: -3.102642 + vertex_buffer: -4.352984 + vertex_buffer: 4.095905 + vertex_buffer: 0.354490 + vertex_buffer: 0.696784 + vertex_buffer: -6.719682 + vertex_buffer: -4.788645 + vertex_buffer: -1.745401 + vertex_buffer: 0.067305 + vertex_buffer: 0.730105 + vertex_buffer: -1.193824 + vertex_buffer: -1.306795 + vertex_buffer: 5.737747 + vertex_buffer: 0.442739 + vertex_buffer: 0.572826 + vertex_buffer: -0.729766 + vertex_buffer: -1.593712 + vertex_buffer: 5.833208 + vertex_buffer: 0.457098 + vertex_buffer: 0.584792 + vertex_buffer: -2.456206 + vertex_buffer: -4.342621 + vertex_buffer: 4.283884 + vertex_buffer: 0.381974 + vertex_buffer: 0.694711 + vertex_buffer: -2.204823 + vertex_buffer: -4.304508 + vertex_buffer: 4.162499 + vertex_buffer: 0.392389 + vertex_buffer: 0.694203 + vertex_buffer: -4.985894 + vertex_buffer: 4.802461 + vertex_buffer: 3.751977 + vertex_buffer: 0.277076 + vertex_buffer: 0.271932 + vertex_buffer: -1.592294 + vertex_buffer: -1.257709 + vertex_buffer: 5.456949 + vertex_buffer: 0.422552 + vertex_buffer: 0.563233 + vertex_buffer: -2.644548 + vertex_buffer: 4.524654 + vertex_buffer: 4.921559 + vertex_buffer: 0.385919 + vertex_buffer: 0.281364 + vertex_buffer: -2.760292 + vertex_buffer: 5.100971 + vertex_buffer: 5.015990 + vertex_buffer: 0.383103 + vertex_buffer: 0.255840 + vertex_buffer: -3.523964 + vertex_buffer: 8.005976 + vertex_buffer: 3.729163 + vertex_buffer: 0.331431 + vertex_buffer: 0.119714 + vertex_buffer: -5.599763 + vertex_buffer: 5.715470 + vertex_buffer: 2.724259 + vertex_buffer: 0.229924 + vertex_buffer: 0.232003 + vertex_buffer: -3.063932 + vertex_buffer: 6.566144 + vertex_buffer: 4.529981 + vertex_buffer: 0.364501 + vertex_buffer: 0.189114 + vertex_buffer: -5.720968 + vertex_buffer: 4.254584 + vertex_buffer: 2.830852 + vertex_buffer: 0.229622 + vertex_buffer: 0.299541 + vertex_buffer: -6.374393 + vertex_buffer: 4.785590 + vertex_buffer: 1.591691 + vertex_buffer: 0.173287 + vertex_buffer: 0.278748 + vertex_buffer: -0.672728 + vertex_buffer: -3.688016 + vertex_buffer: 5.737804 + vertex_buffer: 0.472879 + vertex_buffer: 0.666198 + vertex_buffer: -1.262560 + vertex_buffer: -3.787691 + vertex_buffer: 5.417779 + vertex_buffer: 0.446828 + vertex_buffer: 0.668527 + vertex_buffer: -1.732553 + vertex_buffer: -3.952767 + vertex_buffer: 5.000579 + vertex_buffer: 0.422762 + vertex_buffer: 0.673890 + vertex_buffer: -1.043625 + vertex_buffer: -1.464973 + vertex_buffer: 5.662455 + vertex_buffer: 0.445308 + vertex_buffer: 0.580066 + vertex_buffer: -2.321234 + vertex_buffer: -4.329069 + vertex_buffer: 4.258156 + vertex_buffer: 0.388103 + vertex_buffer: 0.693961 + vertex_buffer: -2.056846 + vertex_buffer: -4.477671 + vertex_buffer: 4.520883 + vertex_buffer: 0.403039 + vertex_buffer: 0.706540 + vertex_buffer: -2.153084 + vertex_buffer: -4.276322 + vertex_buffer: 4.038093 + vertex_buffer: 0.403629 + vertex_buffer: 0.693953 + vertex_buffer: -0.946874 + vertex_buffer: -1.035249 + vertex_buffer: 6.512274 + vertex_buffer: 0.460042 + vertex_buffer: 0.557139 + vertex_buffer: -1.469132 + vertex_buffer: -4.036351 + vertex_buffer: 4.604908 + vertex_buffer: 0.431158 + vertex_buffer: 0.692366 + vertex_buffer: -1.024340 + vertex_buffer: -3.989851 + vertex_buffer: 4.926693 + vertex_buffer: 0.452182 + vertex_buffer: 0.692366 + vertex_buffer: -0.533422 + vertex_buffer: -3.993222 + vertex_buffer: 5.138202 + vertex_buffer: 0.475387 + vertex_buffer: 0.692366 + vertex_buffer: -0.769720 + vertex_buffer: -6.095394 + vertex_buffer: 4.985883 + vertex_buffer: 0.465828 + vertex_buffer: 0.779190 + vertex_buffer: -0.699606 + vertex_buffer: -5.291850 + vertex_buffer: 5.448304 + vertex_buffer: 0.472329 + vertex_buffer: 0.736226 + vertex_buffer: -0.669687 + vertex_buffer: -4.949770 + vertex_buffer: 5.509612 + vertex_buffer: 0.473087 + vertex_buffer: 0.717857 + vertex_buffer: -0.630947 + vertex_buffer: -4.695101 + vertex_buffer: 5.449371 + vertex_buffer: 0.473122 + vertex_buffer: 0.704626 + vertex_buffer: -0.583218 + vertex_buffer: -4.517982 + vertex_buffer: 5.339869 + vertex_buffer: 0.473033 + vertex_buffer: 0.695278 + vertex_buffer: -1.537170 + vertex_buffer: -4.423206 + vertex_buffer: 4.745470 + vertex_buffer: 0.427942 + vertex_buffer: 0.695278 + vertex_buffer: -1.615600 + vertex_buffer: -4.475942 + vertex_buffer: 4.813632 + vertex_buffer: 0.426479 + vertex_buffer: 0.703540 + vertex_buffer: -1.729053 + vertex_buffer: -4.618680 + vertex_buffer: 4.854463 + vertex_buffer: 0.423162 + vertex_buffer: 0.711846 + vertex_buffer: -1.838624 + vertex_buffer: -4.828746 + vertex_buffer: 4.823737 + vertex_buffer: 0.418309 + vertex_buffer: 0.720063 + vertex_buffer: -2.368250 + vertex_buffer: -3.106237 + vertex_buffer: 4.868096 + vertex_buffer: 0.390095 + vertex_buffer: 0.639573 + vertex_buffer: -7.542244 + vertex_buffer: -1.049282 + vertex_buffer: -2.431321 + vertex_buffer: 0.013954 + vertex_buffer: 0.560034 + vertex_buffer: 0.000000 + vertex_buffer: -1.724003 + vertex_buffer: 6.601390 + vertex_buffer: 0.499914 + vertex_buffer: 0.580147 + vertex_buffer: -1.826614 + vertex_buffer: -4.399531 + vertex_buffer: 4.399021 + vertex_buffer: 0.413200 + vertex_buffer: 0.695400 + vertex_buffer: -1.929558 + vertex_buffer: -4.411831 + vertex_buffer: 4.497052 + vertex_buffer: 0.409626 + vertex_buffer: 0.701823 + vertex_buffer: -0.597442 + vertex_buffer: -2.013686 + vertex_buffer: 5.866456 + vertex_buffer: 0.468080 + vertex_buffer: 0.601535 + vertex_buffer: -1.405627 + vertex_buffer: -1.714196 + vertex_buffer: 5.241087 + vertex_buffer: 0.422729 + vertex_buffer: 0.585985 + vertex_buffer: -0.662449 + vertex_buffer: -1.819321 + vertex_buffer: 5.863759 + vertex_buffer: 0.463080 + vertex_buffer: 0.593784 + vertex_buffer: -2.342340 + vertex_buffer: 0.572222 + vertex_buffer: 4.294303 + vertex_buffer: 0.372120 + vertex_buffer: 0.473414 + vertex_buffer: -3.327324 + vertex_buffer: 0.104863 + vertex_buffer: 4.113860 + vertex_buffer: 0.334562 + vertex_buffer: 0.496073 + vertex_buffer: -1.726175 + vertex_buffer: -0.919165 + vertex_buffer: 5.273355 + vertex_buffer: 0.411671 + vertex_buffer: 0.546965 + vertex_buffer: -5.133204 + vertex_buffer: 7.485602 + vertex_buffer: 2.660442 + vertex_buffer: 0.242176 + vertex_buffer: 0.147676 + vertex_buffer: -4.538641 + vertex_buffer: 6.319907 + vertex_buffer: 3.683424 + vertex_buffer: 0.290777 + vertex_buffer: 0.201446 + vertex_buffer: -3.986562 + vertex_buffer: 5.109487 + vertex_buffer: 4.466315 + vertex_buffer: 0.327338 + vertex_buffer: 0.256527 + vertex_buffer: -2.169681 + vertex_buffer: -5.440433 + vertex_buffer: 4.455874 + vertex_buffer: 0.399510 + vertex_buffer: 0.748921 + vertex_buffer: -1.395634 + vertex_buffer: 5.011963 + vertex_buffer: 5.316032 + vertex_buffer: 0.441728 + vertex_buffer: 0.261676 + vertex_buffer: -1.619500 + vertex_buffer: 6.599217 + vertex_buffer: 4.921106 + vertex_buffer: 0.429765 + vertex_buffer: 0.187834 + vertex_buffer: -1.891399 + vertex_buffer: 8.236377 + vertex_buffer: 4.274997 + vertex_buffer: 0.412198 + vertex_buffer: 0.108901 + vertex_buffer: -4.195832 + vertex_buffer: 2.235205 + vertex_buffer: 3.375099 + vertex_buffer: 0.288955 + vertex_buffer: 0.398952 + vertex_buffer: -5.733342 + vertex_buffer: 1.411738 + vertex_buffer: 2.431726 + vertex_buffer: 0.218937 + vertex_buffer: 0.435411 + vertex_buffer: -1.859887 + vertex_buffer: 2.355757 + vertex_buffer: 3.843181 + vertex_buffer: 0.412782 + vertex_buffer: 0.398970 + vertex_buffer: -4.988612 + vertex_buffer: 3.074654 + vertex_buffer: 3.083858 + vertex_buffer: 0.257135 + vertex_buffer: 0.355440 + vertex_buffer: -1.303263 + vertex_buffer: 1.416453 + vertex_buffer: 4.831091 + vertex_buffer: 0.427685 + vertex_buffer: 0.437961 + vertex_buffer: -1.305757 + vertex_buffer: -0.672779 + vertex_buffer: 6.415959 + vertex_buffer: 0.448340 + vertex_buffer: 0.536936 + vertex_buffer: -6.465170 + vertex_buffer: 0.937119 + vertex_buffer: 1.689873 + vertex_buffer: 0.178560 + vertex_buffer: 0.457554 + vertex_buffer: -5.258659 + vertex_buffer: 0.945811 + vertex_buffer: 2.974312 + vertex_buffer: 0.247308 + vertex_buffer: 0.457194 + vertex_buffer: -4.432338 + vertex_buffer: 0.722096 + vertex_buffer: 3.522615 + vertex_buffer: 0.286267 + vertex_buffer: 0.467675 + vertex_buffer: -3.300681 + vertex_buffer: 0.861641 + vertex_buffer: 3.872784 + vertex_buffer: 0.332828 + vertex_buffer: 0.460712 + vertex_buffer: -2.430178 + vertex_buffer: 1.131492 + vertex_buffer: 4.039035 + vertex_buffer: 0.368756 + vertex_buffer: 0.447207 + vertex_buffer: -1.820731 + vertex_buffer: 1.467954 + vertex_buffer: 4.224124 + vertex_buffer: 0.398964 + vertex_buffer: 0.432655 + vertex_buffer: -0.563221 + vertex_buffer: 2.307693 + vertex_buffer: 5.566789 + vertex_buffer: 0.476410 + vertex_buffer: 0.405806 + vertex_buffer: -6.338145 + vertex_buffer: -0.529279 + vertex_buffer: 1.881175 + vertex_buffer: 0.189241 + vertex_buffer: 0.523924 + vertex_buffer: -5.587698 + vertex_buffer: 3.208071 + vertex_buffer: 2.687839 + vertex_buffer: 0.228962 + vertex_buffer: 0.348951 + vertex_buffer: -0.242624 + vertex_buffer: -1.462857 + vertex_buffer: 7.071491 + vertex_buffer: 0.490726 + vertex_buffer: 0.562401 + vertex_buffer: -1.611251 + vertex_buffer: 0.339326 + vertex_buffer: 4.895421 + vertex_buffer: 0.404670 + vertex_buffer: 0.485133 + vertex_buffer: -7.743095 + vertex_buffer: 2.364999 + vertex_buffer: -2.005167 + vertex_buffer: 0.019469 + vertex_buffer: 0.401564 + vertex_buffer: -1.391142 + vertex_buffer: 1.851048 + vertex_buffer: 4.448999 + vertex_buffer: 0.426243 + vertex_buffer: 0.420431 + vertex_buffer: -1.785794 + vertex_buffer: -0.978284 + vertex_buffer: 4.850470 + vertex_buffer: 0.396993 + vertex_buffer: 0.548797 + vertex_buffer: -4.670959 + vertex_buffer: 2.664461 + vertex_buffer: 3.084075 + vertex_buffer: 0.266470 + vertex_buffer: 0.376977 + vertex_buffer: -1.333970 + vertex_buffer: -0.283761 + vertex_buffer: 6.097047 + vertex_buffer: 0.439121 + vertex_buffer: 0.518958 + vertex_buffer: -7.270895 + vertex_buffer: -2.890917 + vertex_buffer: -2.252455 + vertex_buffer: 0.032314 + vertex_buffer: 0.644357 + vertex_buffer: -1.856432 + vertex_buffer: 2.585245 + vertex_buffer: 3.757904 + vertex_buffer: 0.419054 + vertex_buffer: 0.387155 + vertex_buffer: -0.923388 + vertex_buffer: 0.073076 + vertex_buffer: 6.671944 + vertex_buffer: 0.462783 + vertex_buffer: 0.505747 + vertex_buffer: -5.000589 + vertex_buffer: -6.135128 + vertex_buffer: 1.892523 + vertex_buffer: 0.238979 + vertex_buffer: 0.779745 + vertex_buffer: -5.085276 + vertex_buffer: -7.178590 + vertex_buffer: 0.714711 + vertex_buffer: 0.198221 + vertex_buffer: 0.831938 + vertex_buffer: -7.159291 + vertex_buffer: -0.811820 + vertex_buffer: -0.072044 + vertex_buffer: 0.107550 + vertex_buffer: 0.540755 + vertex_buffer: -5.843051 + vertex_buffer: -5.248023 + vertex_buffer: 0.924091 + vertex_buffer: 0.183610 + vertex_buffer: 0.740257 + vertex_buffer: -6.847258 + vertex_buffer: 3.662916 + vertex_buffer: 0.724695 + vertex_buffer: 0.134410 + vertex_buffer: 0.333683 + vertex_buffer: -2.412942 + vertex_buffer: -8.258853 + vertex_buffer: 4.119213 + vertex_buffer: 0.385764 + vertex_buffer: 0.883154 + vertex_buffer: -0.179909 + vertex_buffer: -1.689864 + vertex_buffer: 6.573301 + vertex_buffer: 0.490967 + vertex_buffer: 0.579378 + vertex_buffer: -2.103655 + vertex_buffer: -0.163946 + vertex_buffer: 4.566119 + vertex_buffer: 0.382385 + vertex_buffer: 0.508573 + vertex_buffer: -6.407571 + vertex_buffer: 2.236021 + vertex_buffer: 1.560843 + vertex_buffer: 0.174399 + vertex_buffer: 0.397671 + vertex_buffer: -3.670075 + vertex_buffer: 2.360153 + vertex_buffer: 3.635230 + vertex_buffer: 0.318785 + vertex_buffer: 0.396235 + vertex_buffer: -3.177186 + vertex_buffer: 2.294265 + vertex_buffer: 3.775704 + vertex_buffer: 0.343364 + vertex_buffer: 0.400597 + vertex_buffer: -2.196121 + vertex_buffer: -4.598322 + vertex_buffer: 4.479786 + vertex_buffer: 0.396100 + vertex_buffer: 0.710217 + vertex_buffer: -6.234883 + vertex_buffer: -1.944430 + vertex_buffer: 1.663542 + vertex_buffer: 0.187885 + vertex_buffer: 0.588538 + vertex_buffer: -1.292924 + vertex_buffer: -9.295920 + vertex_buffer: 4.094063 + vertex_buffer: 0.430987 + vertex_buffer: 0.944065 + vertex_buffer: -3.210651 + vertex_buffer: -8.533278 + vertex_buffer: 2.802001 + vertex_buffer: 0.318993 + vertex_buffer: 0.898285 + vertex_buffer: -4.068926 + vertex_buffer: -7.993109 + vertex_buffer: 1.925119 + vertex_buffer: 0.266248 + vertex_buffer: 0.869701 + vertex_buffer: 0.000000 + vertex_buffer: 6.545390 + vertex_buffer: 5.027311 + vertex_buffer: 0.500023 + vertex_buffer: 0.190576 + vertex_buffer: 0.000000 + vertex_buffer: -9.403378 + vertex_buffer: 4.264492 + vertex_buffer: 0.499977 + vertex_buffer: 0.954453 + vertex_buffer: -2.724032 + vertex_buffer: 2.315802 + vertex_buffer: 3.777151 + vertex_buffer: 0.366170 + vertex_buffer: 0.398822 + vertex_buffer: -2.288460 + vertex_buffer: 2.398891 + vertex_buffer: 3.697603 + vertex_buffer: 0.393207 + vertex_buffer: 0.395537 + vertex_buffer: -1.998311 + vertex_buffer: 2.496547 + vertex_buffer: 3.689148 + vertex_buffer: 0.410373 + vertex_buffer: 0.391080 + vertex_buffer: -6.130040 + vertex_buffer: 3.399261 + vertex_buffer: 2.038516 + vertex_buffer: 0.194993 + vertex_buffer: 0.342102 + vertex_buffer: -2.288460 + vertex_buffer: 2.886504 + vertex_buffer: 3.775031 + vertex_buffer: 0.388665 + vertex_buffer: 0.362284 + vertex_buffer: -2.724032 + vertex_buffer: 2.961810 + vertex_buffer: 3.871767 + vertex_buffer: 0.365962 + vertex_buffer: 0.355971 + vertex_buffer: -3.177186 + vertex_buffer: 2.964136 + vertex_buffer: 3.876973 + vertex_buffer: 0.343364 + vertex_buffer: 0.355357 + vertex_buffer: -3.670075 + vertex_buffer: 2.927714 + vertex_buffer: 3.724325 + vertex_buffer: 0.318785 + vertex_buffer: 0.358340 + vertex_buffer: -4.018389 + vertex_buffer: 2.857357 + vertex_buffer: 3.482983 + vertex_buffer: 0.301415 + vertex_buffer: 0.363156 + vertex_buffer: -7.555811 + vertex_buffer: 4.106811 + vertex_buffer: -0.991917 + vertex_buffer: 0.058133 + vertex_buffer: 0.319076 + vertex_buffer: -4.018389 + vertex_buffer: 2.483695 + vertex_buffer: 3.440898 + vertex_buffer: 0.301415 + vertex_buffer: 0.387449 + vertex_buffer: 0.000000 + vertex_buffer: -2.521945 + vertex_buffer: 5.932265 + vertex_buffer: 0.499988 + vertex_buffer: 0.618434 + vertex_buffer: -1.776217 + vertex_buffer: -2.683946 + vertex_buffer: 5.213116 + vertex_buffer: 0.415838 + vertex_buffer: 0.624196 + vertex_buffer: -1.222237 + vertex_buffer: -1.182444 + vertex_buffer: 5.952465 + vertex_buffer: 0.445682 + vertex_buffer: 0.566077 + vertex_buffer: -0.731493 + vertex_buffer: -2.536683 + vertex_buffer: 5.815343 + vertex_buffer: 0.465844 + vertex_buffer: 0.620641 + vertex_buffer: 0.000000 + vertex_buffer: 3.271027 + vertex_buffer: 5.236015 + vertex_buffer: 0.499923 + vertex_buffer: 0.351524 + vertex_buffer: -4.135272 + vertex_buffer: -6.996638 + vertex_buffer: 2.671970 + vertex_buffer: 0.288719 + vertex_buffer: 0.819946 + vertex_buffer: -3.311811 + vertex_buffer: -7.660815 + vertex_buffer: 3.382963 + vertex_buffer: 0.335279 + vertex_buffer: 0.852820 + vertex_buffer: -1.313701 + vertex_buffer: -8.639995 + vertex_buffer: 4.702456 + vertex_buffer: 0.440512 + vertex_buffer: 0.902419 + vertex_buffer: -5.940524 + vertex_buffer: -6.223629 + vertex_buffer: -0.631468 + vertex_buffer: 0.128294 + vertex_buffer: 0.791941 + vertex_buffer: -1.998311 + vertex_buffer: 2.743838 + vertex_buffer: 3.744030 + vertex_buffer: 0.408772 + vertex_buffer: 0.373894 + vertex_buffer: -0.901447 + vertex_buffer: 1.236992 + vertex_buffer: 5.754256 + vertex_buffer: 0.455607 + vertex_buffer: 0.451801 + vertex_buffer: 0.000000 + vertex_buffer: -8.765243 + vertex_buffer: 4.891441 + vertex_buffer: 0.499877 + vertex_buffer: 0.908990 + vertex_buffer: -2.308977 + vertex_buffer: -8.974196 + vertex_buffer: 3.609070 + vertex_buffer: 0.375437 + vertex_buffer: 0.924192 + vertex_buffer: -6.954154 + vertex_buffer: -2.439843 + vertex_buffer: -0.131163 + vertex_buffer: 0.114210 + vertex_buffer: 0.615022 + vertex_buffer: -1.098819 + vertex_buffer: -4.458788 + vertex_buffer: 5.120727 + vertex_buffer: 0.448662 + vertex_buffer: 0.695278 + vertex_buffer: -1.181124 + vertex_buffer: -4.579996 + vertex_buffer: 5.189564 + vertex_buffer: 0.448020 + vertex_buffer: 0.704632 + vertex_buffer: -1.255818 + vertex_buffer: -4.787901 + vertex_buffer: 5.237051 + vertex_buffer: 0.447112 + vertex_buffer: 0.715808 + vertex_buffer: -1.325085 + vertex_buffer: -5.106507 + vertex_buffer: 5.205010 + vertex_buffer: 0.444832 + vertex_buffer: 0.730794 + vertex_buffer: -1.546388 + vertex_buffer: -5.819392 + vertex_buffer: 4.757893 + vertex_buffer: 0.430012 + vertex_buffer: 0.766809 + vertex_buffer: -1.953754 + vertex_buffer: -4.183892 + vertex_buffer: 4.431713 + vertex_buffer: 0.406787 + vertex_buffer: 0.685673 + vertex_buffer: -2.117802 + vertex_buffer: -4.137093 + vertex_buffer: 4.555096 + vertex_buffer: 0.400738 + vertex_buffer: 0.681069 + vertex_buffer: -2.285339 + vertex_buffer: -4.051196 + vertex_buffer: 4.582438 + vertex_buffer: 0.392400 + vertex_buffer: 0.677703 + vertex_buffer: -2.850160 + vertex_buffer: -3.665720 + vertex_buffer: 4.484994 + vertex_buffer: 0.367856 + vertex_buffer: 0.663919 + vertex_buffer: -5.278538 + vertex_buffer: -2.238942 + vertex_buffer: 2.861224 + vertex_buffer: 0.247923 + vertex_buffer: 0.601333 + vertex_buffer: -0.946709 + vertex_buffer: 1.907628 + vertex_buffer: 5.196779 + vertex_buffer: 0.452770 + vertex_buffer: 0.420850 + vertex_buffer: -1.314173 + vertex_buffer: 3.104912 + vertex_buffer: 4.231404 + vertex_buffer: 0.436392 + vertex_buffer: 0.359887 + vertex_buffer: -1.780000 + vertex_buffer: 2.860000 + vertex_buffer: 3.881555 + vertex_buffer: 0.416164 + vertex_buffer: 0.368714 + vertex_buffer: -1.845110 + vertex_buffer: -4.098880 + vertex_buffer: 4.247264 + vertex_buffer: 0.413386 + vertex_buffer: 0.692366 + vertex_buffer: -5.436187 + vertex_buffer: -4.030482 + vertex_buffer: 2.109852 + vertex_buffer: 0.228018 + vertex_buffer: 0.683572 + vertex_buffer: -0.766444 + vertex_buffer: 3.182131 + vertex_buffer: 4.861453 + vertex_buffer: 0.468268 + vertex_buffer: 0.352671 + vertex_buffer: -1.938616 + vertex_buffer: -6.614410 + vertex_buffer: 4.521085 + vertex_buffer: 0.411362 + vertex_buffer: 0.804327 + vertex_buffer: 0.000000 + vertex_buffer: 1.059413 + vertex_buffer: 6.774605 + vertex_buffer: 0.499989 + vertex_buffer: 0.469825 + vertex_buffer: -0.516573 + vertex_buffer: 1.583572 + vertex_buffer: 6.148363 + vertex_buffer: 0.479154 + vertex_buffer: 0.442654 + vertex_buffer: 0.000000 + vertex_buffer: 1.728369 + vertex_buffer: 6.316750 + vertex_buffer: 0.499974 + vertex_buffer: 0.439637 + vertex_buffer: -1.246815 + vertex_buffer: 0.230297 + vertex_buffer: 5.681036 + vertex_buffer: 0.432112 + vertex_buffer: 0.493589 + vertex_buffer: 0.000000 + vertex_buffer: -7.942194 + vertex_buffer: 5.181173 + vertex_buffer: 0.499886 + vertex_buffer: 0.866917 + vertex_buffer: 0.000000 + vertex_buffer: -6.991499 + vertex_buffer: 5.153478 + vertex_buffer: 0.499913 + vertex_buffer: 0.821729 + vertex_buffer: -0.997827 + vertex_buffer: -6.930921 + vertex_buffer: 4.979576 + vertex_buffer: 0.456549 + vertex_buffer: 0.819201 + vertex_buffer: -3.288807 + vertex_buffer: -5.382514 + vertex_buffer: 3.795752 + vertex_buffer: 0.344549 + vertex_buffer: 0.745439 + vertex_buffer: -2.311631 + vertex_buffer: -1.566237 + vertex_buffer: 4.590085 + vertex_buffer: 0.378909 + vertex_buffer: 0.574010 + vertex_buffer: -2.680250 + vertex_buffer: -6.111567 + vertex_buffer: 4.096152 + vertex_buffer: 0.374293 + vertex_buffer: 0.780185 + vertex_buffer: -3.832928 + vertex_buffer: -1.537326 + vertex_buffer: 4.137731 + vertex_buffer: 0.319688 + vertex_buffer: 0.570738 + vertex_buffer: -2.961860 + vertex_buffer: -2.274215 + vertex_buffer: 4.440943 + vertex_buffer: 0.357155 + vertex_buffer: 0.604270 + vertex_buffer: -4.386901 + vertex_buffer: -2.683286 + vertex_buffer: 3.643886 + vertex_buffer: 0.295284 + vertex_buffer: 0.621581 + vertex_buffer: -1.217295 + vertex_buffer: -7.834465 + vertex_buffer: 4.969286 + vertex_buffer: 0.447750 + vertex_buffer: 0.862477 + vertex_buffer: -1.542374 + vertex_buffer: -0.136843 + vertex_buffer: 5.201008 + vertex_buffer: 0.410986 + vertex_buffer: 0.508723 + vertex_buffer: -3.878377 + vertex_buffer: -6.041764 + vertex_buffer: 3.311079 + vertex_buffer: 0.313951 + vertex_buffer: 0.775308 + vertex_buffer: -3.084037 + vertex_buffer: -6.809842 + vertex_buffer: 3.814195 + vertex_buffer: 0.354128 + vertex_buffer: 0.812553 + vertex_buffer: -3.747321 + vertex_buffer: -4.503545 + vertex_buffer: 3.726453 + vertex_buffer: 0.324548 + vertex_buffer: 0.703993 + vertex_buffer: -6.094129 + vertex_buffer: -3.205991 + vertex_buffer: 1.473482 + vertex_buffer: 0.189096 + vertex_buffer: 0.646300 + vertex_buffer: -4.588995 + vertex_buffer: -4.728726 + vertex_buffer: 2.983221 + vertex_buffer: 0.279777 + vertex_buffer: 0.714658 + vertex_buffer: -6.583231 + vertex_buffer: -3.941269 + vertex_buffer: 0.070268 + vertex_buffer: 0.133823 + vertex_buffer: 0.682701 + vertex_buffer: -3.492580 + vertex_buffer: -3.195820 + vertex_buffer: 4.130198 + vertex_buffer: 0.336768 + vertex_buffer: 0.644733 + vertex_buffer: -1.255543 + vertex_buffer: 0.802341 + vertex_buffer: 5.307551 + vertex_buffer: 0.429884 + vertex_buffer: 0.466522 + vertex_buffer: -1.126122 + vertex_buffer: -0.933602 + vertex_buffer: 6.538785 + vertex_buffer: 0.455528 + vertex_buffer: 0.548623 + vertex_buffer: -1.443109 + vertex_buffer: -1.142774 + vertex_buffer: 5.905127 + vertex_buffer: 0.437114 + vertex_buffer: 0.558896 + vertex_buffer: -0.923043 + vertex_buffer: -0.529042 + vertex_buffer: 7.003423 + vertex_buffer: 0.467288 + vertex_buffer: 0.529925 + vertex_buffer: -1.755386 + vertex_buffer: 3.529117 + vertex_buffer: 4.327696 + vertex_buffer: 0.414712 + vertex_buffer: 0.335220 + vertex_buffer: -2.632589 + vertex_buffer: 3.713828 + vertex_buffer: 4.364629 + vertex_buffer: 0.377046 + vertex_buffer: 0.322778 + vertex_buffer: -3.388062 + vertex_buffer: 3.721976 + vertex_buffer: 4.309028 + vertex_buffer: 0.344108 + vertex_buffer: 0.320151 + vertex_buffer: -4.075766 + vertex_buffer: 3.675413 + vertex_buffer: 4.076063 + vertex_buffer: 0.312876 + vertex_buffer: 0.322332 + vertex_buffer: -4.622910 + vertex_buffer: 3.474691 + vertex_buffer: 3.646321 + vertex_buffer: 0.283526 + vertex_buffer: 0.333190 + vertex_buffer: -5.171755 + vertex_buffer: 2.535753 + vertex_buffer: 2.670867 + vertex_buffer: 0.241246 + vertex_buffer: 0.382786 + vertex_buffer: -7.297331 + vertex_buffer: 0.763172 + vertex_buffer: -0.048769 + vertex_buffer: 0.102986 + vertex_buffer: 0.468763 + vertex_buffer: -4.706828 + vertex_buffer: 1.651000 + vertex_buffer: 3.109532 + vertex_buffer: 0.267612 + vertex_buffer: 0.424560 + vertex_buffer: -4.071712 + vertex_buffer: 1.476821 + vertex_buffer: 3.476944 + vertex_buffer: 0.297879 + vertex_buffer: 0.433176 + vertex_buffer: -3.269817 + vertex_buffer: 1.470659 + vertex_buffer: 3.731945 + vertex_buffer: 0.333434 + vertex_buffer: 0.433878 + vertex_buffer: -2.527572 + vertex_buffer: 1.617311 + vertex_buffer: 3.865444 + vertex_buffer: 0.366427 + vertex_buffer: 0.426116 + vertex_buffer: -1.970894 + vertex_buffer: 1.858505 + vertex_buffer: 3.961782 + vertex_buffer: 0.396012 + vertex_buffer: 0.416696 + vertex_buffer: -1.579543 + vertex_buffer: 2.097941 + vertex_buffer: 4.084996 + vertex_buffer: 0.420121 + vertex_buffer: 0.410228 + vertex_buffer: -7.664182 + vertex_buffer: 0.673132 + vertex_buffer: -2.435867 + vertex_buffer: 0.007561 + vertex_buffer: 0.480777 + vertex_buffer: -1.397041 + vertex_buffer: -1.340139 + vertex_buffer: 5.630378 + vertex_buffer: 0.432949 + vertex_buffer: 0.569518 + vertex_buffer: -0.884838 + vertex_buffer: 0.658740 + vertex_buffer: 6.233232 + vertex_buffer: 0.458639 + vertex_buffer: 0.479089 + vertex_buffer: -0.767097 + vertex_buffer: -0.968035 + vertex_buffer: 7.077932 + vertex_buffer: 0.473466 + vertex_buffer: 0.545744 + vertex_buffer: -0.460213 + vertex_buffer: -1.334106 + vertex_buffer: 6.787447 + vertex_buffer: 0.476088 + vertex_buffer: 0.563830 + vertex_buffer: -0.748618 + vertex_buffer: -1.067994 + vertex_buffer: 6.798303 + vertex_buffer: 0.468472 + vertex_buffer: 0.555057 + vertex_buffer: -1.236408 + vertex_buffer: -1.585568 + vertex_buffer: 5.480490 + vertex_buffer: 0.433991 + vertex_buffer: 0.582362 + vertex_buffer: -0.387306 + vertex_buffer: -1.409990 + vertex_buffer: 6.957705 + vertex_buffer: 0.483518 + vertex_buffer: 0.562984 + vertex_buffer: -0.319925 + vertex_buffer: -1.607931 + vertex_buffer: 6.508676 + vertex_buffer: 0.482483 + vertex_buffer: 0.577849 + vertex_buffer: -1.639633 + vertex_buffer: 2.556298 + vertex_buffer: 3.863736 + vertex_buffer: 0.426450 + vertex_buffer: 0.389799 + vertex_buffer: -1.255645 + vertex_buffer: 2.467144 + vertex_buffer: 4.203800 + vertex_buffer: 0.438999 + vertex_buffer: 0.396495 + vertex_buffer: -1.031362 + vertex_buffer: 2.382663 + vertex_buffer: 4.615849 + vertex_buffer: 0.450067 + vertex_buffer: 0.400434 + vertex_buffer: -4.253081 + vertex_buffer: 2.772296 + vertex_buffer: 3.315305 + vertex_buffer: 0.289712 + vertex_buffer: 0.368253 + vertex_buffer: -4.530000 + vertex_buffer: 2.910000 + vertex_buffer: 3.339685 + vertex_buffer: 0.276670 + vertex_buffer: 0.363373 + vertex_buffer: 0.463928 + vertex_buffer: 0.955357 + vertex_buffer: 6.633583 + vertex_buffer: 0.517862 + vertex_buffer: 0.471948 + vertex_buffer: 4.253081 + vertex_buffer: 2.577646 + vertex_buffer: 3.279702 + vertex_buffer: 0.710288 + vertex_buffer: 0.380764 + vertex_buffer: 0.416106 + vertex_buffer: -1.466449 + vertex_buffer: 6.447657 + vertex_buffer: 0.526227 + vertex_buffer: 0.573910 + vertex_buffer: 7.087960 + vertex_buffer: 5.434801 + vertex_buffer: 0.099620 + vertex_buffer: 0.895093 + vertex_buffer: 0.254141 + vertex_buffer: 2.628639 + vertex_buffer: 2.035898 + vertex_buffer: 3.848121 + vertex_buffer: 0.634070 + vertex_buffer: 0.409576 + vertex_buffer: 3.198363 + vertex_buffer: 1.985815 + vertex_buffer: 3.796952 + vertex_buffer: 0.661242 + vertex_buffer: 0.413025 + vertex_buffer: 3.775151 + vertex_buffer: 2.039402 + vertex_buffer: 3.646194 + vertex_buffer: 0.688880 + vertex_buffer: 0.409460 + vertex_buffer: 4.465819 + vertex_buffer: 2.422950 + vertex_buffer: 3.155168 + vertex_buffer: 0.725342 + vertex_buffer: 0.389131 + vertex_buffer: 2.164289 + vertex_buffer: 2.189867 + vertex_buffer: 3.851822 + vertex_buffer: 0.606630 + vertex_buffer: 0.403705 + vertex_buffer: 3.208229 + vertex_buffer: 3.223926 + vertex_buffer: 4.115822 + vertex_buffer: 0.654766 + vertex_buffer: 0.344011 + vertex_buffer: 2.673803 + vertex_buffer: 3.205337 + vertex_buffer: 4.092203 + vertex_buffer: 0.629906 + vertex_buffer: 0.346076 + vertex_buffer: 3.745193 + vertex_buffer: 3.165286 + vertex_buffer: 3.972409 + vertex_buffer: 0.680678 + vertex_buffer: 0.347265 + vertex_buffer: 4.161018 + vertex_buffer: 3.059069 + vertex_buffer: 3.719554 + vertex_buffer: 0.702097 + vertex_buffer: 0.353591 + vertex_buffer: 5.062006 + vertex_buffer: 1.934418 + vertex_buffer: 2.776093 + vertex_buffer: 0.752212 + vertex_buffer: 0.410805 + vertex_buffer: 2.266659 + vertex_buffer: -7.425768 + vertex_buffer: 4.389812 + vertex_buffer: 0.602918 + vertex_buffer: 0.842863 + vertex_buffer: 4.445859 + vertex_buffer: 2.663991 + vertex_buffer: 3.173422 + vertex_buffer: 0.719902 + vertex_buffer: 0.375600 + vertex_buffer: 7.214530 + vertex_buffer: 2.263009 + vertex_buffer: 0.073150 + vertex_buffer: 0.893693 + vertex_buffer: 0.399960 + vertex_buffer: 5.799793 + vertex_buffer: 2.349546 + vertex_buffer: 2.204059 + vertex_buffer: 0.790082 + vertex_buffer: 0.391354 + vertex_buffer: 2.844939 + vertex_buffer: -0.720868 + vertex_buffer: 4.433130 + vertex_buffer: 0.643998 + vertex_buffer: 0.534488 + vertex_buffer: 0.711452 + vertex_buffer: -3.329355 + vertex_buffer: 5.877044 + vertex_buffer: 0.528249 + vertex_buffer: 0.650404 + vertex_buffer: 0.606033 + vertex_buffer: -3.924562 + vertex_buffer: 5.444923 + vertex_buffer: 0.525850 + vertex_buffer: 0.680191 + vertex_buffer: 1.431615 + vertex_buffer: -3.500953 + vertex_buffer: 5.496189 + vertex_buffer: 0.560215 + vertex_buffer: 0.657229 + vertex_buffer: 1.914910 + vertex_buffer: -3.803146 + vertex_buffer: 5.028930 + vertex_buffer: 0.585384 + vertex_buffer: 0.666541 + vertex_buffer: 1.131043 + vertex_buffer: -3.973937 + vertex_buffer: 5.189648 + vertex_buffer: 0.549626 + vertex_buffer: 0.680861 + vertex_buffer: 1.563548 + vertex_buffer: -4.082763 + vertex_buffer: 4.842263 + vertex_buffer: 0.571228 + vertex_buffer: 0.682692 + vertex_buffer: 2.650112 + vertex_buffer: -5.003649 + vertex_buffer: 4.188483 + vertex_buffer: 0.624852 + vertex_buffer: 0.728099 + vertex_buffer: 0.427049 + vertex_buffer: -1.094134 + vertex_buffer: 7.360529 + vertex_buffer: 0.513050 + vertex_buffer: 0.547282 + vertex_buffer: 0.496396 + vertex_buffer: -0.475659 + vertex_buffer: 7.440358 + vertex_buffer: 0.515097 + vertex_buffer: 0.527252 + vertex_buffer: 5.253307 + vertex_buffer: 3.881582 + vertex_buffer: 3.363159 + vertex_buffer: 0.742247 + vertex_buffer: 0.314507 + vertex_buffer: 1.718698 + vertex_buffer: 0.974609 + vertex_buffer: 4.558359 + vertex_buffer: 0.598631 + vertex_buffer: 0.454979 + vertex_buffer: 1.608635 + vertex_buffer: -0.942516 + vertex_buffer: 5.814193 + vertex_buffer: 0.570338 + vertex_buffer: 0.548575 + vertex_buffer: 1.651267 + vertex_buffer: -0.610868 + vertex_buffer: 5.581319 + vertex_buffer: 0.578632 + vertex_buffer: 0.533623 + vertex_buffer: 4.765501 + vertex_buffer: -0.701554 + vertex_buffer: 3.534632 + vertex_buffer: 0.723087 + vertex_buffer: 0.532054 + vertex_buffer: 0.478306 + vertex_buffer: 0.295766 + vertex_buffer: 7.101013 + vertex_buffer: 0.516446 + vertex_buffer: 0.499639 + vertex_buffer: 3.734964 + vertex_buffer: 4.508230 + vertex_buffer: 4.550454 + vertex_buffer: 0.662801 + vertex_buffer: 0.282918 + vertex_buffer: 4.588603 + vertex_buffer: 4.302037 + vertex_buffer: 4.048484 + vertex_buffer: 0.703624 + vertex_buffer: 0.293271 + vertex_buffer: 6.279331 + vertex_buffer: 6.615427 + vertex_buffer: 1.425850 + vertex_buffer: 0.830705 + vertex_buffer: 0.193814 + vertex_buffer: 1.220941 + vertex_buffer: 4.142165 + vertex_buffer: 5.106035 + vertex_buffer: 0.552386 + vertex_buffer: 0.302568 + vertex_buffer: 2.193489 + vertex_buffer: 3.100317 + vertex_buffer: 4.000575 + vertex_buffer: 0.607610 + vertex_buffer: 0.353888 + vertex_buffer: 3.102642 + vertex_buffer: -4.352984 + vertex_buffer: 4.095905 + vertex_buffer: 0.645429 + vertex_buffer: 0.696707 + vertex_buffer: 6.719682 + vertex_buffer: -4.788645 + vertex_buffer: -1.745401 + vertex_buffer: 0.932695 + vertex_buffer: 0.730105 + vertex_buffer: 1.193824 + vertex_buffer: -1.306795 + vertex_buffer: 5.737747 + vertex_buffer: 0.557261 + vertex_buffer: 0.572826 + vertex_buffer: 0.729766 + vertex_buffer: -1.593712 + vertex_buffer: 5.833208 + vertex_buffer: 0.542902 + vertex_buffer: 0.584792 + vertex_buffer: 2.456206 + vertex_buffer: -4.342621 + vertex_buffer: 4.283884 + vertex_buffer: 0.618026 + vertex_buffer: 0.694711 + vertex_buffer: 2.204823 + vertex_buffer: -4.304508 + vertex_buffer: 4.162499 + vertex_buffer: 0.607591 + vertex_buffer: 0.694203 + vertex_buffer: 4.985894 + vertex_buffer: 4.802461 + vertex_buffer: 3.751977 + vertex_buffer: 0.722943 + vertex_buffer: 0.271963 + vertex_buffer: 1.592294 + vertex_buffer: -1.257709 + vertex_buffer: 5.456949 + vertex_buffer: 0.577414 + vertex_buffer: 0.563167 + vertex_buffer: 2.644548 + vertex_buffer: 4.524654 + vertex_buffer: 4.921559 + vertex_buffer: 0.614083 + vertex_buffer: 0.281387 + vertex_buffer: 2.760292 + vertex_buffer: 5.100971 + vertex_buffer: 5.015990 + vertex_buffer: 0.616907 + vertex_buffer: 0.255886 + vertex_buffer: 3.523964 + vertex_buffer: 8.005976 + vertex_buffer: 3.729163 + vertex_buffer: 0.668509 + vertex_buffer: 0.119914 + vertex_buffer: 5.599763 + vertex_buffer: 5.715470 + vertex_buffer: 2.724259 + vertex_buffer: 0.770092 + vertex_buffer: 0.232021 + vertex_buffer: 3.063932 + vertex_buffer: 6.566144 + vertex_buffer: 4.529981 + vertex_buffer: 0.635536 + vertex_buffer: 0.189249 + vertex_buffer: 5.720968 + vertex_buffer: 4.254584 + vertex_buffer: 2.830852 + vertex_buffer: 0.770391 + vertex_buffer: 0.299556 + vertex_buffer: 6.374393 + vertex_buffer: 4.785590 + vertex_buffer: 1.591691 + vertex_buffer: 0.826722 + vertex_buffer: 0.278755 + vertex_buffer: 0.672728 + vertex_buffer: -3.688016 + vertex_buffer: 5.737804 + vertex_buffer: 0.527121 + vertex_buffer: 0.666198 + vertex_buffer: 1.262560 + vertex_buffer: -3.787691 + vertex_buffer: 5.417779 + vertex_buffer: 0.553172 + vertex_buffer: 0.668527 + vertex_buffer: 1.732553 + vertex_buffer: -3.952767 + vertex_buffer: 5.000579 + vertex_buffer: 0.577238 + vertex_buffer: 0.673890 + vertex_buffer: 1.043625 + vertex_buffer: -1.464973 + vertex_buffer: 5.662455 + vertex_buffer: 0.554692 + vertex_buffer: 0.580066 + vertex_buffer: 2.321234 + vertex_buffer: -4.329069 + vertex_buffer: 4.258156 + vertex_buffer: 0.611897 + vertex_buffer: 0.693961 + vertex_buffer: 2.056846 + vertex_buffer: -4.477671 + vertex_buffer: 4.520883 + vertex_buffer: 0.596961 + vertex_buffer: 0.706540 + vertex_buffer: 2.153084 + vertex_buffer: -4.276322 + vertex_buffer: 4.038093 + vertex_buffer: 0.596371 + vertex_buffer: 0.693953 + vertex_buffer: 0.946874 + vertex_buffer: -1.035249 + vertex_buffer: 6.512274 + vertex_buffer: 0.539958 + vertex_buffer: 0.557139 + vertex_buffer: 1.469132 + vertex_buffer: -4.036351 + vertex_buffer: 4.604908 + vertex_buffer: 0.568842 + vertex_buffer: 0.692366 + vertex_buffer: 1.024340 + vertex_buffer: -3.989851 + vertex_buffer: 4.926693 + vertex_buffer: 0.547818 + vertex_buffer: 0.692366 + vertex_buffer: 0.533422 + vertex_buffer: -3.993222 + vertex_buffer: 5.138202 + vertex_buffer: 0.524613 + vertex_buffer: 0.692366 + vertex_buffer: 0.769720 + vertex_buffer: -6.095394 + vertex_buffer: 4.985883 + vertex_buffer: 0.534090 + vertex_buffer: 0.779141 + vertex_buffer: 0.699606 + vertex_buffer: -5.291850 + vertex_buffer: 5.448304 + vertex_buffer: 0.527671 + vertex_buffer: 0.736226 + vertex_buffer: 0.669687 + vertex_buffer: -4.949770 + vertex_buffer: 5.509612 + vertex_buffer: 0.526913 + vertex_buffer: 0.717857 + vertex_buffer: 0.630947 + vertex_buffer: -4.695101 + vertex_buffer: 5.449371 + vertex_buffer: 0.526878 + vertex_buffer: 0.704626 + vertex_buffer: 0.583218 + vertex_buffer: -4.517982 + vertex_buffer: 5.339869 + vertex_buffer: 0.526967 + vertex_buffer: 0.695278 + vertex_buffer: 1.537170 + vertex_buffer: -4.423206 + vertex_buffer: 4.745470 + vertex_buffer: 0.572058 + vertex_buffer: 0.695278 + vertex_buffer: 1.615600 + vertex_buffer: -4.475942 + vertex_buffer: 4.813632 + vertex_buffer: 0.573521 + vertex_buffer: 0.703540 + vertex_buffer: 1.729053 + vertex_buffer: -4.618680 + vertex_buffer: 4.854463 + vertex_buffer: 0.576838 + vertex_buffer: 0.711846 + vertex_buffer: 1.838624 + vertex_buffer: -4.828746 + vertex_buffer: 4.823737 + vertex_buffer: 0.581691 + vertex_buffer: 0.720063 + vertex_buffer: 2.368250 + vertex_buffer: -3.106237 + vertex_buffer: 4.868096 + vertex_buffer: 0.609945 + vertex_buffer: 0.639910 + vertex_buffer: 7.542244 + vertex_buffer: -1.049282 + vertex_buffer: -2.431321 + vertex_buffer: 0.986046 + vertex_buffer: 0.560034 + vertex_buffer: 1.826614 + vertex_buffer: -4.399531 + vertex_buffer: 4.399021 + vertex_buffer: 0.586800 + vertex_buffer: 0.695400 + vertex_buffer: 1.929558 + vertex_buffer: -4.411831 + vertex_buffer: 4.497052 + vertex_buffer: 0.590372 + vertex_buffer: 0.701823 + vertex_buffer: 0.597442 + vertex_buffer: -2.013686 + vertex_buffer: 5.866456 + vertex_buffer: 0.531915 + vertex_buffer: 0.601537 + vertex_buffer: 1.405627 + vertex_buffer: -1.714196 + vertex_buffer: 5.241087 + vertex_buffer: 0.577268 + vertex_buffer: 0.585935 + vertex_buffer: 0.662449 + vertex_buffer: -1.819321 + vertex_buffer: 5.863759 + vertex_buffer: 0.536915 + vertex_buffer: 0.593786 + vertex_buffer: 2.342340 + vertex_buffer: 0.572222 + vertex_buffer: 4.294303 + vertex_buffer: 0.627543 + vertex_buffer: 0.473352 + vertex_buffer: 3.327324 + vertex_buffer: 0.104863 + vertex_buffer: 4.113860 + vertex_buffer: 0.665586 + vertex_buffer: 0.495951 + vertex_buffer: 1.726175 + vertex_buffer: -0.919165 + vertex_buffer: 5.273355 + vertex_buffer: 0.588354 + vertex_buffer: 0.546862 + vertex_buffer: 5.133204 + vertex_buffer: 7.485602 + vertex_buffer: 2.660442 + vertex_buffer: 0.757824 + vertex_buffer: 0.147676 + vertex_buffer: 4.538641 + vertex_buffer: 6.319907 + vertex_buffer: 3.683424 + vertex_buffer: 0.709250 + vertex_buffer: 0.201508 + vertex_buffer: 3.986562 + vertex_buffer: 5.109487 + vertex_buffer: 4.466315 + vertex_buffer: 0.672684 + vertex_buffer: 0.256581 + vertex_buffer: 2.169681 + vertex_buffer: -5.440433 + vertex_buffer: 4.455874 + vertex_buffer: 0.600409 + vertex_buffer: 0.749005 + vertex_buffer: 1.395634 + vertex_buffer: 5.011963 + vertex_buffer: 5.316032 + vertex_buffer: 0.558266 + vertex_buffer: 0.261672 + vertex_buffer: 1.619500 + vertex_buffer: 6.599217 + vertex_buffer: 4.921106 + vertex_buffer: 0.570304 + vertex_buffer: 0.187871 + vertex_buffer: 1.891399 + vertex_buffer: 8.236377 + vertex_buffer: 4.274997 + vertex_buffer: 0.588166 + vertex_buffer: 0.109044 + vertex_buffer: 4.195832 + vertex_buffer: 2.235205 + vertex_buffer: 3.375099 + vertex_buffer: 0.711045 + vertex_buffer: 0.398952 + vertex_buffer: 5.733342 + vertex_buffer: 1.411738 + vertex_buffer: 2.431726 + vertex_buffer: 0.781070 + vertex_buffer: 0.435405 + vertex_buffer: 1.859887 + vertex_buffer: 2.355757 + vertex_buffer: 3.843181 + vertex_buffer: 0.587247 + vertex_buffer: 0.398932 + vertex_buffer: 4.988612 + vertex_buffer: 3.074654 + vertex_buffer: 3.083858 + vertex_buffer: 0.742870 + vertex_buffer: 0.355446 + vertex_buffer: 1.303263 + vertex_buffer: 1.416453 + vertex_buffer: 4.831091 + vertex_buffer: 0.572156 + vertex_buffer: 0.437652 + vertex_buffer: 1.305757 + vertex_buffer: -0.672779 + vertex_buffer: 6.415959 + vertex_buffer: 0.551868 + vertex_buffer: 0.536570 + vertex_buffer: 6.465170 + vertex_buffer: 0.937119 + vertex_buffer: 1.689873 + vertex_buffer: 0.821442 + vertex_buffer: 0.457556 + vertex_buffer: 5.258659 + vertex_buffer: 0.945811 + vertex_buffer: 2.974312 + vertex_buffer: 0.752702 + vertex_buffer: 0.457182 + vertex_buffer: 4.432338 + vertex_buffer: 0.722096 + vertex_buffer: 3.522615 + vertex_buffer: 0.713757 + vertex_buffer: 0.467627 + vertex_buffer: 3.300681 + vertex_buffer: 0.861641 + vertex_buffer: 3.872784 + vertex_buffer: 0.667113 + vertex_buffer: 0.460673 + vertex_buffer: 2.430178 + vertex_buffer: 1.131492 + vertex_buffer: 4.039035 + vertex_buffer: 0.631101 + vertex_buffer: 0.447154 + vertex_buffer: 1.820731 + vertex_buffer: 1.467954 + vertex_buffer: 4.224124 + vertex_buffer: 0.600862 + vertex_buffer: 0.432473 + vertex_buffer: 0.563221 + vertex_buffer: 2.307693 + vertex_buffer: 5.566789 + vertex_buffer: 0.523481 + vertex_buffer: 0.405627 + vertex_buffer: 6.338145 + vertex_buffer: -0.529279 + vertex_buffer: 1.881175 + vertex_buffer: 0.810748 + vertex_buffer: 0.523926 + vertex_buffer: 5.587698 + vertex_buffer: 3.208071 + vertex_buffer: 2.687839 + vertex_buffer: 0.771046 + vertex_buffer: 0.348959 + vertex_buffer: 0.242624 + vertex_buffer: -1.462857 + vertex_buffer: 7.071491 + vertex_buffer: 0.509127 + vertex_buffer: 0.562718 + vertex_buffer: 1.611251 + vertex_buffer: 0.339326 + vertex_buffer: 4.895421 + vertex_buffer: 0.595293 + vertex_buffer: 0.485024 + vertex_buffer: 7.743095 + vertex_buffer: 2.364999 + vertex_buffer: -2.005167 + vertex_buffer: 0.980531 + vertex_buffer: 0.401564 + vertex_buffer: 1.391142 + vertex_buffer: 1.851048 + vertex_buffer: 4.448999 + vertex_buffer: 0.573500 + vertex_buffer: 0.420000 + vertex_buffer: 1.785794 + vertex_buffer: -0.978284 + vertex_buffer: 4.850470 + vertex_buffer: 0.602995 + vertex_buffer: 0.548688 + vertex_buffer: 4.670959 + vertex_buffer: 2.664461 + vertex_buffer: 3.084075 + vertex_buffer: 0.733530 + vertex_buffer: 0.376977 + vertex_buffer: 1.333970 + vertex_buffer: -0.283761 + vertex_buffer: 6.097047 + vertex_buffer: 0.560611 + vertex_buffer: 0.519017 + vertex_buffer: 7.270895 + vertex_buffer: -2.890917 + vertex_buffer: -2.252455 + vertex_buffer: 0.967686 + vertex_buffer: 0.644357 + vertex_buffer: 1.856432 + vertex_buffer: 2.585245 + vertex_buffer: 3.757904 + vertex_buffer: 0.580985 + vertex_buffer: 0.387160 + vertex_buffer: 0.923388 + vertex_buffer: 0.073076 + vertex_buffer: 6.671944 + vertex_buffer: 0.537728 + vertex_buffer: 0.505385 + vertex_buffer: 5.000589 + vertex_buffer: -6.135128 + vertex_buffer: 1.892523 + vertex_buffer: 0.760966 + vertex_buffer: 0.779753 + vertex_buffer: 5.085276 + vertex_buffer: -7.178590 + vertex_buffer: 0.714711 + vertex_buffer: 0.801779 + vertex_buffer: 0.831938 + vertex_buffer: 7.159291 + vertex_buffer: -0.811820 + vertex_buffer: -0.072044 + vertex_buffer: 0.892441 + vertex_buffer: 0.540761 + vertex_buffer: 5.843051 + vertex_buffer: -5.248023 + vertex_buffer: 0.924091 + vertex_buffer: 0.816351 + vertex_buffer: 0.740260 + vertex_buffer: 6.847258 + vertex_buffer: 3.662916 + vertex_buffer: 0.724695 + vertex_buffer: 0.865595 + vertex_buffer: 0.333687 + vertex_buffer: 2.412942 + vertex_buffer: -8.258853 + vertex_buffer: 4.119213 + vertex_buffer: 0.614074 + vertex_buffer: 0.883246 + vertex_buffer: 0.179909 + vertex_buffer: -1.689864 + vertex_buffer: 6.573301 + vertex_buffer: 0.508953 + vertex_buffer: 0.579438 + vertex_buffer: 2.103655 + vertex_buffer: -0.163946 + vertex_buffer: 4.566119 + vertex_buffer: 0.617942 + vertex_buffer: 0.508316 + vertex_buffer: 6.407571 + vertex_buffer: 2.236021 + vertex_buffer: 1.560843 + vertex_buffer: 0.825608 + vertex_buffer: 0.397675 + vertex_buffer: 3.670075 + vertex_buffer: 2.360153 + vertex_buffer: 3.635230 + vertex_buffer: 0.681215 + vertex_buffer: 0.396235 + vertex_buffer: 3.177186 + vertex_buffer: 2.294265 + vertex_buffer: 3.775704 + vertex_buffer: 0.656636 + vertex_buffer: 0.400597 + vertex_buffer: 2.196121 + vertex_buffer: -4.598322 + vertex_buffer: 4.479786 + vertex_buffer: 0.603900 + vertex_buffer: 0.710217 + vertex_buffer: 6.234883 + vertex_buffer: -1.944430 + vertex_buffer: 1.663542 + vertex_buffer: 0.812086 + vertex_buffer: 0.588539 + vertex_buffer: 1.292924 + vertex_buffer: -9.295920 + vertex_buffer: 4.094063 + vertex_buffer: 0.568013 + vertex_buffer: 0.944565 + vertex_buffer: 3.210651 + vertex_buffer: -8.533278 + vertex_buffer: 2.802001 + vertex_buffer: 0.681008 + vertex_buffer: 0.898285 + vertex_buffer: 4.068926 + vertex_buffer: -7.993109 + vertex_buffer: 1.925119 + vertex_buffer: 0.733752 + vertex_buffer: 0.869701 + vertex_buffer: 2.724032 + vertex_buffer: 2.315802 + vertex_buffer: 3.777151 + vertex_buffer: 0.633830 + vertex_buffer: 0.398822 + vertex_buffer: 2.288460 + vertex_buffer: 2.398891 + vertex_buffer: 3.697603 + vertex_buffer: 0.606793 + vertex_buffer: 0.395537 + vertex_buffer: 1.998311 + vertex_buffer: 2.496547 + vertex_buffer: 3.689148 + vertex_buffer: 0.589660 + vertex_buffer: 0.391062 + vertex_buffer: 6.130040 + vertex_buffer: 3.399261 + vertex_buffer: 2.038516 + vertex_buffer: 0.805016 + vertex_buffer: 0.342108 + vertex_buffer: 2.288460 + vertex_buffer: 2.886504 + vertex_buffer: 3.775031 + vertex_buffer: 0.611335 + vertex_buffer: 0.362284 + vertex_buffer: 2.724032 + vertex_buffer: 2.961810 + vertex_buffer: 3.871767 + vertex_buffer: 0.634038 + vertex_buffer: 0.355971 + vertex_buffer: 3.177186 + vertex_buffer: 2.964136 + vertex_buffer: 3.876973 + vertex_buffer: 0.656636 + vertex_buffer: 0.355357 + vertex_buffer: 3.670075 + vertex_buffer: 2.927714 + vertex_buffer: 3.724325 + vertex_buffer: 0.681215 + vertex_buffer: 0.358340 + vertex_buffer: 4.018389 + vertex_buffer: 2.857357 + vertex_buffer: 3.482983 + vertex_buffer: 0.698585 + vertex_buffer: 0.363156 + vertex_buffer: 7.555811 + vertex_buffer: 4.106811 + vertex_buffer: -0.991917 + vertex_buffer: 0.941867 + vertex_buffer: 0.319076 + vertex_buffer: 4.018389 + vertex_buffer: 2.483695 + vertex_buffer: 3.440898 + vertex_buffer: 0.698585 + vertex_buffer: 0.387449 + vertex_buffer: 1.776217 + vertex_buffer: -2.683946 + vertex_buffer: 5.213116 + vertex_buffer: 0.584177 + vertex_buffer: 0.624107 + vertex_buffer: 1.222237 + vertex_buffer: -1.182444 + vertex_buffer: 5.952465 + vertex_buffer: 0.554318 + vertex_buffer: 0.566077 + vertex_buffer: 0.731493 + vertex_buffer: -2.536683 + vertex_buffer: 5.815343 + vertex_buffer: 0.534154 + vertex_buffer: 0.620640 + vertex_buffer: 4.135272 + vertex_buffer: -6.996638 + vertex_buffer: 2.671970 + vertex_buffer: 0.711218 + vertex_buffer: 0.819975 + vertex_buffer: 3.311811 + vertex_buffer: -7.660815 + vertex_buffer: 3.382963 + vertex_buffer: 0.664630 + vertex_buffer: 0.852871 + vertex_buffer: 1.313701 + vertex_buffer: -8.639995 + vertex_buffer: 4.702456 + vertex_buffer: 0.559100 + vertex_buffer: 0.902632 + vertex_buffer: 5.940524 + vertex_buffer: -6.223629 + vertex_buffer: -0.631468 + vertex_buffer: 0.871706 + vertex_buffer: 0.791941 + vertex_buffer: 1.998311 + vertex_buffer: 2.743838 + vertex_buffer: 3.744030 + vertex_buffer: 0.591234 + vertex_buffer: 0.373894 + vertex_buffer: 0.901447 + vertex_buffer: 1.236992 + vertex_buffer: 5.754256 + vertex_buffer: 0.544341 + vertex_buffer: 0.451584 + vertex_buffer: 2.308977 + vertex_buffer: -8.974196 + vertex_buffer: 3.609070 + vertex_buffer: 0.624563 + vertex_buffer: 0.924192 + vertex_buffer: 6.954154 + vertex_buffer: -2.439843 + vertex_buffer: -0.131163 + vertex_buffer: 0.885770 + vertex_buffer: 0.615029 + vertex_buffer: 1.098819 + vertex_buffer: -4.458788 + vertex_buffer: 5.120727 + vertex_buffer: 0.551338 + vertex_buffer: 0.695278 + vertex_buffer: 1.181124 + vertex_buffer: -4.579996 + vertex_buffer: 5.189564 + vertex_buffer: 0.551980 + vertex_buffer: 0.704632 + vertex_buffer: 1.255818 + vertex_buffer: -4.787901 + vertex_buffer: 5.237051 + vertex_buffer: 0.552888 + vertex_buffer: 0.715808 + vertex_buffer: 1.325085 + vertex_buffer: -5.106507 + vertex_buffer: 5.205010 + vertex_buffer: 0.555168 + vertex_buffer: 0.730794 + vertex_buffer: 1.546388 + vertex_buffer: -5.819392 + vertex_buffer: 4.757893 + vertex_buffer: 0.569944 + vertex_buffer: 0.767035 + vertex_buffer: 1.953754 + vertex_buffer: -4.183892 + vertex_buffer: 4.431713 + vertex_buffer: 0.593203 + vertex_buffer: 0.685676 + vertex_buffer: 2.117802 + vertex_buffer: -4.137093 + vertex_buffer: 4.555096 + vertex_buffer: 0.599262 + vertex_buffer: 0.681069 + vertex_buffer: 2.285339 + vertex_buffer: -4.051196 + vertex_buffer: 4.582438 + vertex_buffer: 0.607600 + vertex_buffer: 0.677703 + vertex_buffer: 2.850160 + vertex_buffer: -3.665720 + vertex_buffer: 4.484994 + vertex_buffer: 0.631938 + vertex_buffer: 0.663500 + vertex_buffer: 5.278538 + vertex_buffer: -2.238942 + vertex_buffer: 2.861224 + vertex_buffer: 0.752033 + vertex_buffer: 0.601315 + vertex_buffer: 0.946709 + vertex_buffer: 1.907628 + vertex_buffer: 5.196779 + vertex_buffer: 0.547226 + vertex_buffer: 0.420395 + vertex_buffer: 1.314173 + vertex_buffer: 3.104912 + vertex_buffer: 4.231404 + vertex_buffer: 0.563544 + vertex_buffer: 0.359828 + vertex_buffer: 1.780000 + vertex_buffer: 2.860000 + vertex_buffer: 3.881555 + vertex_buffer: 0.583841 + vertex_buffer: 0.368714 + vertex_buffer: 1.845110 + vertex_buffer: -4.098880 + vertex_buffer: 4.247264 + vertex_buffer: 0.586614 + vertex_buffer: 0.692366 + vertex_buffer: 5.436187 + vertex_buffer: -4.030482 + vertex_buffer: 2.109852 + vertex_buffer: 0.771915 + vertex_buffer: 0.683578 + vertex_buffer: 0.766444 + vertex_buffer: 3.182131 + vertex_buffer: 4.861453 + vertex_buffer: 0.531597 + vertex_buffer: 0.352483 + vertex_buffer: 1.938616 + vertex_buffer: -6.614410 + vertex_buffer: 4.521085 + vertex_buffer: 0.588371 + vertex_buffer: 0.804441 + vertex_buffer: 0.516573 + vertex_buffer: 1.583572 + vertex_buffer: 6.148363 + vertex_buffer: 0.520797 + vertex_buffer: 0.442565 + vertex_buffer: 1.246815 + vertex_buffer: 0.230297 + vertex_buffer: 5.681036 + vertex_buffer: 0.567985 + vertex_buffer: 0.493479 + vertex_buffer: 0.997827 + vertex_buffer: -6.930921 + vertex_buffer: 4.979576 + vertex_buffer: 0.543283 + vertex_buffer: 0.819255 + vertex_buffer: 3.288807 + vertex_buffer: -5.382514 + vertex_buffer: 3.795752 + vertex_buffer: 0.655317 + vertex_buffer: 0.745515 + vertex_buffer: 2.311631 + vertex_buffer: -1.566237 + vertex_buffer: 4.590085 + vertex_buffer: 0.621009 + vertex_buffer: 0.574018 + vertex_buffer: 2.680250 + vertex_buffer: -6.111567 + vertex_buffer: 4.096152 + vertex_buffer: 0.625560 + vertex_buffer: 0.780312 + vertex_buffer: 3.832928 + vertex_buffer: -1.537326 + vertex_buffer: 4.137731 + vertex_buffer: 0.680198 + vertex_buffer: 0.570719 + vertex_buffer: 2.961860 + vertex_buffer: -2.274215 + vertex_buffer: 4.440943 + vertex_buffer: 0.642764 + vertex_buffer: 0.604338 + vertex_buffer: 4.386901 + vertex_buffer: -2.683286 + vertex_buffer: 3.643886 + vertex_buffer: 0.704663 + vertex_buffer: 0.621530 + vertex_buffer: 1.217295 + vertex_buffer: -7.834465 + vertex_buffer: 4.969286 + vertex_buffer: 0.552012 + vertex_buffer: 0.862592 + vertex_buffer: 1.542374 + vertex_buffer: -0.136843 + vertex_buffer: 5.201008 + vertex_buffer: 0.589072 + vertex_buffer: 0.508637 + vertex_buffer: 3.878377 + vertex_buffer: -6.041764 + vertex_buffer: 3.311079 + vertex_buffer: 0.685945 + vertex_buffer: 0.775357 + vertex_buffer: 3.084037 + vertex_buffer: -6.809842 + vertex_buffer: 3.814195 + vertex_buffer: 0.645735 + vertex_buffer: 0.812640 + vertex_buffer: 3.747321 + vertex_buffer: -4.503545 + vertex_buffer: 3.726453 + vertex_buffer: 0.675343 + vertex_buffer: 0.703978 + vertex_buffer: 6.094129 + vertex_buffer: -3.205991 + vertex_buffer: 1.473482 + vertex_buffer: 0.810858 + vertex_buffer: 0.646305 + vertex_buffer: 4.588995 + vertex_buffer: -4.728726 + vertex_buffer: 2.983221 + vertex_buffer: 0.720122 + vertex_buffer: 0.714667 + vertex_buffer: 6.583231 + vertex_buffer: -3.941269 + vertex_buffer: 0.070268 + vertex_buffer: 0.866152 + vertex_buffer: 0.682705 + vertex_buffer: 3.492580 + vertex_buffer: -3.195820 + vertex_buffer: 4.130198 + vertex_buffer: 0.663187 + vertex_buffer: 0.644597 + vertex_buffer: 1.255543 + vertex_buffer: 0.802341 + vertex_buffer: 5.307551 + vertex_buffer: 0.570082 + vertex_buffer: 0.466326 + vertex_buffer: 1.126122 + vertex_buffer: -0.933602 + vertex_buffer: 6.538785 + vertex_buffer: 0.544562 + vertex_buffer: 0.548376 + vertex_buffer: 1.443109 + vertex_buffer: -1.142774 + vertex_buffer: 5.905127 + vertex_buffer: 0.562759 + vertex_buffer: 0.558785 + vertex_buffer: 0.923043 + vertex_buffer: -0.529042 + vertex_buffer: 7.003423 + vertex_buffer: 0.531987 + vertex_buffer: 0.530140 + vertex_buffer: 1.755386 + vertex_buffer: 3.529117 + vertex_buffer: 4.327696 + vertex_buffer: 0.585271 + vertex_buffer: 0.335177 + vertex_buffer: 2.632589 + vertex_buffer: 3.713828 + vertex_buffer: 4.364629 + vertex_buffer: 0.622953 + vertex_buffer: 0.322779 + vertex_buffer: 3.388062 + vertex_buffer: 3.721976 + vertex_buffer: 4.309028 + vertex_buffer: 0.655896 + vertex_buffer: 0.320163 + vertex_buffer: 4.075766 + vertex_buffer: 3.675413 + vertex_buffer: 4.076063 + vertex_buffer: 0.687132 + vertex_buffer: 0.322346 + vertex_buffer: 4.622910 + vertex_buffer: 3.474691 + vertex_buffer: 3.646321 + vertex_buffer: 0.716482 + vertex_buffer: 0.333201 + vertex_buffer: 5.171755 + vertex_buffer: 2.535753 + vertex_buffer: 2.670867 + vertex_buffer: 0.758757 + vertex_buffer: 0.382787 + vertex_buffer: 7.297331 + vertex_buffer: 0.763172 + vertex_buffer: -0.048769 + vertex_buffer: 0.897013 + vertex_buffer: 0.468769 + vertex_buffer: 4.706828 + vertex_buffer: 1.651000 + vertex_buffer: 3.109532 + vertex_buffer: 0.732392 + vertex_buffer: 0.424547 + vertex_buffer: 4.071712 + vertex_buffer: 1.476821 + vertex_buffer: 3.476944 + vertex_buffer: 0.702114 + vertex_buffer: 0.433163 + vertex_buffer: 3.269817 + vertex_buffer: 1.470659 + vertex_buffer: 3.731945 + vertex_buffer: 0.666525 + vertex_buffer: 0.433866 + vertex_buffer: 2.527572 + vertex_buffer: 1.617311 + vertex_buffer: 3.865444 + vertex_buffer: 0.633505 + vertex_buffer: 0.426088 + vertex_buffer: 1.970894 + vertex_buffer: 1.858505 + vertex_buffer: 3.961782 + vertex_buffer: 0.603876 + vertex_buffer: 0.416587 + vertex_buffer: 1.579543 + vertex_buffer: 2.097941 + vertex_buffer: 4.084996 + vertex_buffer: 0.579658 + vertex_buffer: 0.409945 + vertex_buffer: 7.664182 + vertex_buffer: 0.673132 + vertex_buffer: -2.435867 + vertex_buffer: 0.992440 + vertex_buffer: 0.480777 + vertex_buffer: 1.397041 + vertex_buffer: -1.340139 + vertex_buffer: 5.630378 + vertex_buffer: 0.567192 + vertex_buffer: 0.569420 + vertex_buffer: 0.884838 + vertex_buffer: 0.658740 + vertex_buffer: 6.233232 + vertex_buffer: 0.541366 + vertex_buffer: 0.478899 + vertex_buffer: 0.767097 + vertex_buffer: -0.968035 + vertex_buffer: 7.077932 + vertex_buffer: 0.526564 + vertex_buffer: 0.546118 + vertex_buffer: 0.460213 + vertex_buffer: -1.334106 + vertex_buffer: 6.787447 + vertex_buffer: 0.523913 + vertex_buffer: 0.563830 + vertex_buffer: 0.748618 + vertex_buffer: -1.067994 + vertex_buffer: 6.798303 + vertex_buffer: 0.531529 + vertex_buffer: 0.555057 + vertex_buffer: 1.236408 + vertex_buffer: -1.585568 + vertex_buffer: 5.480490 + vertex_buffer: 0.566036 + vertex_buffer: 0.582329 + vertex_buffer: 0.387306 + vertex_buffer: -1.409990 + vertex_buffer: 6.957705 + vertex_buffer: 0.516311 + vertex_buffer: 0.563054 + vertex_buffer: 0.319925 + vertex_buffer: -1.607931 + vertex_buffer: 6.508676 + vertex_buffer: 0.517472 + vertex_buffer: 0.577877 + vertex_buffer: 1.639633 + vertex_buffer: 2.556298 + vertex_buffer: 3.863736 + vertex_buffer: 0.573595 + vertex_buffer: 0.389807 + vertex_buffer: 1.255645 + vertex_buffer: 2.467144 + vertex_buffer: 4.203800 + vertex_buffer: 0.560698 + vertex_buffer: 0.395332 + vertex_buffer: 1.031362 + vertex_buffer: 2.382663 + vertex_buffer: 4.615849 + vertex_buffer: 0.549756 + vertex_buffer: 0.399751 + vertex_buffer: 4.253081 + vertex_buffer: 2.772296 + vertex_buffer: 3.315305 + vertex_buffer: 0.710288 + vertex_buffer: 0.368253 + vertex_buffer: 4.530000 + vertex_buffer: 2.910000 + vertex_buffer: 3.339685 + vertex_buffer: 0.723330 + vertex_buffer: 0.363373 + index_buffer: 173 + index_buffer: 155 + index_buffer: 133 + index_buffer: 246 + index_buffer: 33 + index_buffer: 7 + index_buffer: 382 + index_buffer: 398 + index_buffer: 362 + index_buffer: 263 + index_buffer: 466 + index_buffer: 249 + index_buffer: 308 + index_buffer: 415 + index_buffer: 324 + index_buffer: 78 + index_buffer: 95 + index_buffer: 191 + index_buffer: 356 + index_buffer: 389 + index_buffer: 264 + index_buffer: 127 + index_buffer: 34 + index_buffer: 162 + index_buffer: 368 + index_buffer: 264 + index_buffer: 389 + index_buffer: 139 + index_buffer: 162 + index_buffer: 34 + index_buffer: 267 + index_buffer: 0 + index_buffer: 302 + index_buffer: 37 + index_buffer: 72 + index_buffer: 0 + index_buffer: 11 + index_buffer: 302 + index_buffer: 0 + index_buffer: 11 + index_buffer: 0 + index_buffer: 72 + index_buffer: 349 + index_buffer: 451 + index_buffer: 350 + index_buffer: 120 + index_buffer: 121 + index_buffer: 231 + index_buffer: 452 + index_buffer: 350 + index_buffer: 451 + index_buffer: 232 + index_buffer: 231 + index_buffer: 121 + index_buffer: 267 + index_buffer: 302 + index_buffer: 269 + index_buffer: 37 + index_buffer: 39 + index_buffer: 72 + index_buffer: 303 + index_buffer: 269 + index_buffer: 302 + index_buffer: 73 + index_buffer: 72 + index_buffer: 39 + index_buffer: 357 + index_buffer: 343 + index_buffer: 350 + index_buffer: 128 + index_buffer: 121 + index_buffer: 114 + index_buffer: 277 + index_buffer: 350 + index_buffer: 343 + index_buffer: 47 + index_buffer: 114 + index_buffer: 121 + index_buffer: 350 + index_buffer: 452 + index_buffer: 357 + index_buffer: 121 + index_buffer: 128 + index_buffer: 232 + index_buffer: 453 + index_buffer: 357 + index_buffer: 452 + index_buffer: 233 + index_buffer: 232 + index_buffer: 128 + index_buffer: 299 + index_buffer: 333 + index_buffer: 297 + index_buffer: 69 + index_buffer: 67 + index_buffer: 104 + index_buffer: 332 + index_buffer: 297 + index_buffer: 333 + index_buffer: 103 + index_buffer: 104 + index_buffer: 67 + index_buffer: 175 + index_buffer: 152 + index_buffer: 396 + index_buffer: 175 + index_buffer: 171 + index_buffer: 152 + index_buffer: 377 + index_buffer: 396 + index_buffer: 152 + index_buffer: 148 + index_buffer: 152 + index_buffer: 171 + index_buffer: 381 + index_buffer: 384 + index_buffer: 382 + index_buffer: 154 + index_buffer: 155 + index_buffer: 157 + index_buffer: 398 + index_buffer: 382 + index_buffer: 384 + index_buffer: 173 + index_buffer: 157 + index_buffer: 155 + index_buffer: 280 + index_buffer: 347 + index_buffer: 330 + index_buffer: 50 + index_buffer: 101 + index_buffer: 118 + index_buffer: 348 + index_buffer: 330 + index_buffer: 347 + index_buffer: 119 + index_buffer: 118 + index_buffer: 101 + index_buffer: 269 + index_buffer: 303 + index_buffer: 270 + index_buffer: 39 + index_buffer: 40 + index_buffer: 73 + index_buffer: 304 + index_buffer: 270 + index_buffer: 303 + index_buffer: 74 + index_buffer: 73 + index_buffer: 40 + index_buffer: 9 + index_buffer: 336 + index_buffer: 151 + index_buffer: 9 + index_buffer: 151 + index_buffer: 107 + index_buffer: 337 + index_buffer: 151 + index_buffer: 336 + index_buffer: 108 + index_buffer: 107 + index_buffer: 151 + index_buffer: 344 + index_buffer: 278 + index_buffer: 360 + index_buffer: 115 + index_buffer: 131 + index_buffer: 48 + index_buffer: 279 + index_buffer: 360 + index_buffer: 278 + index_buffer: 49 + index_buffer: 48 + index_buffer: 131 + index_buffer: 262 + index_buffer: 431 + index_buffer: 418 + index_buffer: 32 + index_buffer: 194 + index_buffer: 211 + index_buffer: 424 + index_buffer: 418 + index_buffer: 431 + index_buffer: 204 + index_buffer: 211 + index_buffer: 194 + index_buffer: 304 + index_buffer: 408 + index_buffer: 270 + index_buffer: 74 + index_buffer: 40 + index_buffer: 184 + index_buffer: 409 + index_buffer: 270 + index_buffer: 408 + index_buffer: 185 + index_buffer: 184 + index_buffer: 40 + index_buffer: 272 + index_buffer: 310 + index_buffer: 407 + index_buffer: 42 + index_buffer: 183 + index_buffer: 80 + index_buffer: 415 + index_buffer: 407 + index_buffer: 310 + index_buffer: 191 + index_buffer: 80 + index_buffer: 183 + index_buffer: 322 + index_buffer: 270 + index_buffer: 410 + index_buffer: 92 + index_buffer: 186 + index_buffer: 40 + index_buffer: 409 + index_buffer: 410 + index_buffer: 270 + index_buffer: 185 + index_buffer: 40 + index_buffer: 186 + index_buffer: 347 + index_buffer: 449 + index_buffer: 348 + index_buffer: 118 + index_buffer: 119 + index_buffer: 229 + index_buffer: 450 + index_buffer: 348 + index_buffer: 449 + index_buffer: 230 + index_buffer: 229 + index_buffer: 119 + index_buffer: 434 + index_buffer: 432 + index_buffer: 430 + index_buffer: 214 + index_buffer: 210 + index_buffer: 212 + index_buffer: 422 + index_buffer: 430 + index_buffer: 432 + index_buffer: 202 + index_buffer: 212 + index_buffer: 210 + index_buffer: 313 + index_buffer: 314 + index_buffer: 18 + index_buffer: 83 + index_buffer: 18 + index_buffer: 84 + index_buffer: 17 + index_buffer: 18 + index_buffer: 314 + index_buffer: 17 + index_buffer: 84 + index_buffer: 18 + index_buffer: 307 + index_buffer: 375 + index_buffer: 306 + index_buffer: 77 + index_buffer: 76 + index_buffer: 146 + index_buffer: 291 + index_buffer: 306 + index_buffer: 375 + index_buffer: 61 + index_buffer: 146 + index_buffer: 76 + index_buffer: 259 + index_buffer: 387 + index_buffer: 260 + index_buffer: 29 + index_buffer: 30 + index_buffer: 160 + index_buffer: 388 + index_buffer: 260 + index_buffer: 387 + index_buffer: 161 + index_buffer: 160 + index_buffer: 30 + index_buffer: 286 + index_buffer: 414 + index_buffer: 384 + index_buffer: 56 + index_buffer: 157 + index_buffer: 190 + index_buffer: 398 + index_buffer: 384 + index_buffer: 414 + index_buffer: 173 + index_buffer: 190 + index_buffer: 157 + index_buffer: 418 + index_buffer: 424 + index_buffer: 406 + index_buffer: 194 + index_buffer: 182 + index_buffer: 204 + index_buffer: 335 + index_buffer: 406 + index_buffer: 424 + index_buffer: 106 + index_buffer: 204 + index_buffer: 182 + index_buffer: 367 + index_buffer: 416 + index_buffer: 364 + index_buffer: 138 + index_buffer: 135 + index_buffer: 192 + index_buffer: 434 + index_buffer: 364 + index_buffer: 416 + index_buffer: 214 + index_buffer: 192 + index_buffer: 135 + index_buffer: 391 + index_buffer: 423 + index_buffer: 327 + index_buffer: 165 + index_buffer: 98 + index_buffer: 203 + index_buffer: 358 + index_buffer: 327 + index_buffer: 423 + index_buffer: 129 + index_buffer: 203 + index_buffer: 98 + index_buffer: 298 + index_buffer: 301 + index_buffer: 284 + index_buffer: 68 + index_buffer: 54 + index_buffer: 71 + index_buffer: 251 + index_buffer: 284 + index_buffer: 301 + index_buffer: 21 + index_buffer: 71 + index_buffer: 54 + index_buffer: 4 + index_buffer: 275 + index_buffer: 5 + index_buffer: 4 + index_buffer: 5 + index_buffer: 45 + index_buffer: 281 + index_buffer: 5 + index_buffer: 275 + index_buffer: 51 + index_buffer: 45 + index_buffer: 5 + index_buffer: 254 + index_buffer: 373 + index_buffer: 253 + index_buffer: 24 + index_buffer: 23 + index_buffer: 144 + index_buffer: 374 + index_buffer: 253 + index_buffer: 373 + index_buffer: 145 + index_buffer: 144 + index_buffer: 23 + index_buffer: 320 + index_buffer: 321 + index_buffer: 307 + index_buffer: 90 + index_buffer: 77 + index_buffer: 91 + index_buffer: 375 + index_buffer: 307 + index_buffer: 321 + index_buffer: 146 + index_buffer: 91 + index_buffer: 77 + index_buffer: 280 + index_buffer: 425 + index_buffer: 411 + index_buffer: 50 + index_buffer: 187 + index_buffer: 205 + index_buffer: 427 + index_buffer: 411 + index_buffer: 425 + index_buffer: 207 + index_buffer: 205 + index_buffer: 187 + index_buffer: 421 + index_buffer: 313 + index_buffer: 200 + index_buffer: 201 + index_buffer: 200 + index_buffer: 83 + index_buffer: 18 + index_buffer: 200 + index_buffer: 313 + index_buffer: 18 + index_buffer: 83 + index_buffer: 200 + index_buffer: 335 + index_buffer: 321 + index_buffer: 406 + index_buffer: 106 + index_buffer: 182 + index_buffer: 91 + index_buffer: 405 + index_buffer: 406 + index_buffer: 321 + index_buffer: 181 + index_buffer: 91 + index_buffer: 182 + index_buffer: 405 + index_buffer: 321 + index_buffer: 404 + index_buffer: 181 + index_buffer: 180 + index_buffer: 91 + index_buffer: 320 + index_buffer: 404 + index_buffer: 321 + index_buffer: 90 + index_buffer: 91 + index_buffer: 180 + index_buffer: 17 + index_buffer: 314 + index_buffer: 16 + index_buffer: 17 + index_buffer: 16 + index_buffer: 84 + index_buffer: 315 + index_buffer: 16 + index_buffer: 314 + index_buffer: 85 + index_buffer: 84 + index_buffer: 16 + index_buffer: 425 + index_buffer: 266 + index_buffer: 426 + index_buffer: 205 + index_buffer: 206 + index_buffer: 36 + index_buffer: 423 + index_buffer: 426 + index_buffer: 266 + index_buffer: 203 + index_buffer: 36 + index_buffer: 206 + index_buffer: 369 + index_buffer: 396 + index_buffer: 400 + index_buffer: 140 + index_buffer: 176 + index_buffer: 171 + index_buffer: 377 + index_buffer: 400 + index_buffer: 396 + index_buffer: 148 + index_buffer: 171 + index_buffer: 176 + index_buffer: 391 + index_buffer: 269 + index_buffer: 322 + index_buffer: 165 + index_buffer: 92 + index_buffer: 39 + index_buffer: 270 + index_buffer: 322 + index_buffer: 269 + index_buffer: 40 + index_buffer: 39 + index_buffer: 92 + index_buffer: 417 + index_buffer: 465 + index_buffer: 413 + index_buffer: 193 + index_buffer: 189 + index_buffer: 245 + index_buffer: 464 + index_buffer: 413 + index_buffer: 465 + index_buffer: 244 + index_buffer: 245 + index_buffer: 189 + index_buffer: 257 + index_buffer: 258 + index_buffer: 386 + index_buffer: 27 + index_buffer: 159 + index_buffer: 28 + index_buffer: 385 + index_buffer: 386 + index_buffer: 258 + index_buffer: 158 + index_buffer: 28 + index_buffer: 159 + index_buffer: 260 + index_buffer: 388 + index_buffer: 467 + index_buffer: 30 + index_buffer: 247 + index_buffer: 161 + index_buffer: 466 + index_buffer: 467 + index_buffer: 388 + index_buffer: 246 + index_buffer: 161 + index_buffer: 247 + index_buffer: 248 + index_buffer: 456 + index_buffer: 419 + index_buffer: 3 + index_buffer: 196 + index_buffer: 236 + index_buffer: 399 + index_buffer: 419 + index_buffer: 456 + index_buffer: 174 + index_buffer: 236 + index_buffer: 196 + index_buffer: 333 + index_buffer: 298 + index_buffer: 332 + index_buffer: 104 + index_buffer: 103 + index_buffer: 68 + index_buffer: 284 + index_buffer: 332 + index_buffer: 298 + index_buffer: 54 + index_buffer: 68 + index_buffer: 103 + index_buffer: 285 + index_buffer: 8 + index_buffer: 417 + index_buffer: 55 + index_buffer: 193 + index_buffer: 8 + index_buffer: 168 + index_buffer: 417 + index_buffer: 8 + index_buffer: 168 + index_buffer: 8 + index_buffer: 193 + index_buffer: 340 + index_buffer: 261 + index_buffer: 346 + index_buffer: 111 + index_buffer: 117 + index_buffer: 31 + index_buffer: 448 + index_buffer: 346 + index_buffer: 261 + index_buffer: 228 + index_buffer: 31 + index_buffer: 117 + index_buffer: 285 + index_buffer: 417 + index_buffer: 441 + index_buffer: 55 + index_buffer: 221 + index_buffer: 193 + index_buffer: 413 + index_buffer: 441 + index_buffer: 417 + index_buffer: 189 + index_buffer: 193 + index_buffer: 221 + index_buffer: 327 + index_buffer: 460 + index_buffer: 326 + index_buffer: 98 + index_buffer: 97 + index_buffer: 240 + index_buffer: 328 + index_buffer: 326 + index_buffer: 460 + index_buffer: 99 + index_buffer: 240 + index_buffer: 97 + index_buffer: 277 + index_buffer: 355 + index_buffer: 329 + index_buffer: 47 + index_buffer: 100 + index_buffer: 126 + index_buffer: 371 + index_buffer: 329 + index_buffer: 355 + index_buffer: 142 + index_buffer: 126 + index_buffer: 100 + index_buffer: 309 + index_buffer: 392 + index_buffer: 438 + index_buffer: 79 + index_buffer: 218 + index_buffer: 166 + index_buffer: 439 + index_buffer: 438 + index_buffer: 392 + index_buffer: 219 + index_buffer: 166 + index_buffer: 218 + index_buffer: 381 + index_buffer: 382 + index_buffer: 256 + index_buffer: 154 + index_buffer: 26 + index_buffer: 155 + index_buffer: 341 + index_buffer: 256 + index_buffer: 382 + index_buffer: 112 + index_buffer: 155 + index_buffer: 26 + index_buffer: 360 + index_buffer: 279 + index_buffer: 420 + index_buffer: 131 + index_buffer: 198 + index_buffer: 49 + index_buffer: 429 + index_buffer: 420 + index_buffer: 279 + index_buffer: 209 + index_buffer: 49 + index_buffer: 198 + index_buffer: 365 + index_buffer: 364 + index_buffer: 379 + index_buffer: 136 + index_buffer: 150 + index_buffer: 135 + index_buffer: 394 + index_buffer: 379 + index_buffer: 364 + index_buffer: 169 + index_buffer: 135 + index_buffer: 150 + index_buffer: 355 + index_buffer: 277 + index_buffer: 437 + index_buffer: 126 + index_buffer: 217 + index_buffer: 47 + index_buffer: 343 + index_buffer: 437 + index_buffer: 277 + index_buffer: 114 + index_buffer: 47 + index_buffer: 217 + index_buffer: 443 + index_buffer: 444 + index_buffer: 282 + index_buffer: 223 + index_buffer: 52 + index_buffer: 224 + index_buffer: 283 + index_buffer: 282 + index_buffer: 444 + index_buffer: 53 + index_buffer: 224 + index_buffer: 52 + index_buffer: 281 + index_buffer: 275 + index_buffer: 363 + index_buffer: 51 + index_buffer: 134 + index_buffer: 45 + index_buffer: 440 + index_buffer: 363 + index_buffer: 275 + index_buffer: 220 + index_buffer: 45 + index_buffer: 134 + index_buffer: 431 + index_buffer: 262 + index_buffer: 395 + index_buffer: 211 + index_buffer: 170 + index_buffer: 32 + index_buffer: 369 + index_buffer: 395 + index_buffer: 262 + index_buffer: 140 + index_buffer: 32 + index_buffer: 170 + index_buffer: 337 + index_buffer: 299 + index_buffer: 338 + index_buffer: 108 + index_buffer: 109 + index_buffer: 69 + index_buffer: 297 + index_buffer: 338 + index_buffer: 299 + index_buffer: 67 + index_buffer: 69 + index_buffer: 109 + index_buffer: 335 + index_buffer: 273 + index_buffer: 321 + index_buffer: 106 + index_buffer: 91 + index_buffer: 43 + index_buffer: 375 + index_buffer: 321 + index_buffer: 273 + index_buffer: 146 + index_buffer: 43 + index_buffer: 91 + index_buffer: 348 + index_buffer: 450 + index_buffer: 349 + index_buffer: 119 + index_buffer: 120 + index_buffer: 230 + index_buffer: 451 + index_buffer: 349 + index_buffer: 450 + index_buffer: 231 + index_buffer: 230 + index_buffer: 120 + index_buffer: 467 + index_buffer: 359 + index_buffer: 342 + index_buffer: 247 + index_buffer: 113 + index_buffer: 130 + index_buffer: 446 + index_buffer: 342 + index_buffer: 359 + index_buffer: 226 + index_buffer: 130 + index_buffer: 113 + index_buffer: 282 + index_buffer: 283 + index_buffer: 334 + index_buffer: 52 + index_buffer: 105 + index_buffer: 53 + index_buffer: 293 + index_buffer: 334 + index_buffer: 283 + index_buffer: 63 + index_buffer: 53 + index_buffer: 105 + index_buffer: 250 + index_buffer: 458 + index_buffer: 462 + index_buffer: 20 + index_buffer: 242 + index_buffer: 238 + index_buffer: 461 + index_buffer: 462 + index_buffer: 458 + index_buffer: 241 + index_buffer: 238 + index_buffer: 242 + index_buffer: 276 + index_buffer: 353 + index_buffer: 300 + index_buffer: 46 + index_buffer: 70 + index_buffer: 124 + index_buffer: 383 + index_buffer: 300 + index_buffer: 353 + index_buffer: 156 + index_buffer: 124 + index_buffer: 70 + index_buffer: 325 + index_buffer: 292 + index_buffer: 324 + index_buffer: 96 + index_buffer: 95 + index_buffer: 62 + index_buffer: 308 + index_buffer: 324 + index_buffer: 292 + index_buffer: 78 + index_buffer: 62 + index_buffer: 95 + index_buffer: 283 + index_buffer: 276 + index_buffer: 293 + index_buffer: 53 + index_buffer: 63 + index_buffer: 46 + index_buffer: 300 + index_buffer: 293 + index_buffer: 276 + index_buffer: 70 + index_buffer: 46 + index_buffer: 63 + index_buffer: 447 + index_buffer: 264 + index_buffer: 345 + index_buffer: 227 + index_buffer: 116 + index_buffer: 34 + index_buffer: 372 + index_buffer: 345 + index_buffer: 264 + index_buffer: 143 + index_buffer: 34 + index_buffer: 116 + index_buffer: 352 + index_buffer: 345 + index_buffer: 346 + index_buffer: 123 + index_buffer: 117 + index_buffer: 116 + index_buffer: 340 + index_buffer: 346 + index_buffer: 345 + index_buffer: 111 + index_buffer: 116 + index_buffer: 117 + index_buffer: 1 + index_buffer: 19 + index_buffer: 274 + index_buffer: 1 + index_buffer: 44 + index_buffer: 19 + index_buffer: 354 + index_buffer: 274 + index_buffer: 19 + index_buffer: 125 + index_buffer: 19 + index_buffer: 44 + index_buffer: 248 + index_buffer: 281 + index_buffer: 456 + index_buffer: 3 + index_buffer: 236 + index_buffer: 51 + index_buffer: 363 + index_buffer: 456 + index_buffer: 281 + index_buffer: 134 + index_buffer: 51 + index_buffer: 236 + index_buffer: 425 + index_buffer: 426 + index_buffer: 427 + index_buffer: 205 + index_buffer: 207 + index_buffer: 206 + index_buffer: 436 + index_buffer: 427 + index_buffer: 426 + index_buffer: 216 + index_buffer: 206 + index_buffer: 207 + index_buffer: 380 + index_buffer: 381 + index_buffer: 252 + index_buffer: 153 + index_buffer: 22 + index_buffer: 154 + index_buffer: 256 + index_buffer: 252 + index_buffer: 381 + index_buffer: 26 + index_buffer: 154 + index_buffer: 22 + index_buffer: 391 + index_buffer: 393 + index_buffer: 269 + index_buffer: 165 + index_buffer: 39 + index_buffer: 167 + index_buffer: 267 + index_buffer: 269 + index_buffer: 393 + index_buffer: 37 + index_buffer: 167 + index_buffer: 39 + index_buffer: 199 + index_buffer: 428 + index_buffer: 200 + index_buffer: 199 + index_buffer: 200 + index_buffer: 208 + index_buffer: 421 + index_buffer: 200 + index_buffer: 428 + index_buffer: 201 + index_buffer: 208 + index_buffer: 200 + index_buffer: 330 + index_buffer: 329 + index_buffer: 266 + index_buffer: 101 + index_buffer: 36 + index_buffer: 100 + index_buffer: 371 + index_buffer: 266 + index_buffer: 329 + index_buffer: 142 + index_buffer: 100 + index_buffer: 36 + index_buffer: 422 + index_buffer: 432 + index_buffer: 273 + index_buffer: 202 + index_buffer: 43 + index_buffer: 212 + index_buffer: 287 + index_buffer: 273 + index_buffer: 432 + index_buffer: 57 + index_buffer: 212 + index_buffer: 43 + index_buffer: 290 + index_buffer: 250 + index_buffer: 328 + index_buffer: 60 + index_buffer: 99 + index_buffer: 20 + index_buffer: 462 + index_buffer: 328 + index_buffer: 250 + index_buffer: 242 + index_buffer: 20 + index_buffer: 99 + index_buffer: 258 + index_buffer: 286 + index_buffer: 385 + index_buffer: 28 + index_buffer: 158 + index_buffer: 56 + index_buffer: 384 + index_buffer: 385 + index_buffer: 286 + index_buffer: 157 + index_buffer: 56 + index_buffer: 158 + index_buffer: 342 + index_buffer: 446 + index_buffer: 353 + index_buffer: 113 + index_buffer: 124 + index_buffer: 226 + index_buffer: 265 + index_buffer: 353 + index_buffer: 446 + index_buffer: 35 + index_buffer: 226 + index_buffer: 124 + index_buffer: 257 + index_buffer: 386 + index_buffer: 259 + index_buffer: 27 + index_buffer: 29 + index_buffer: 159 + index_buffer: 387 + index_buffer: 259 + index_buffer: 386 + index_buffer: 160 + index_buffer: 159 + index_buffer: 29 + index_buffer: 430 + index_buffer: 422 + index_buffer: 431 + index_buffer: 210 + index_buffer: 211 + index_buffer: 202 + index_buffer: 424 + index_buffer: 431 + index_buffer: 422 + index_buffer: 204 + index_buffer: 202 + index_buffer: 211 + index_buffer: 445 + index_buffer: 342 + index_buffer: 276 + index_buffer: 225 + index_buffer: 46 + index_buffer: 113 + index_buffer: 353 + index_buffer: 276 + index_buffer: 342 + index_buffer: 124 + index_buffer: 113 + index_buffer: 46 + index_buffer: 424 + index_buffer: 422 + index_buffer: 335 + index_buffer: 204 + index_buffer: 106 + index_buffer: 202 + index_buffer: 273 + index_buffer: 335 + index_buffer: 422 + index_buffer: 43 + index_buffer: 202 + index_buffer: 106 + index_buffer: 306 + index_buffer: 292 + index_buffer: 307 + index_buffer: 76 + index_buffer: 77 + index_buffer: 62 + index_buffer: 325 + index_buffer: 307 + index_buffer: 292 + index_buffer: 96 + index_buffer: 62 + index_buffer: 77 + index_buffer: 366 + index_buffer: 447 + index_buffer: 352 + index_buffer: 137 + index_buffer: 123 + index_buffer: 227 + index_buffer: 345 + index_buffer: 352 + index_buffer: 447 + index_buffer: 116 + index_buffer: 227 + index_buffer: 123 + index_buffer: 302 + index_buffer: 268 + index_buffer: 303 + index_buffer: 72 + index_buffer: 73 + index_buffer: 38 + index_buffer: 271 + index_buffer: 303 + index_buffer: 268 + index_buffer: 41 + index_buffer: 38 + index_buffer: 73 + index_buffer: 371 + index_buffer: 358 + index_buffer: 266 + index_buffer: 142 + index_buffer: 36 + index_buffer: 129 + index_buffer: 423 + index_buffer: 266 + index_buffer: 358 + index_buffer: 203 + index_buffer: 129 + index_buffer: 36 + index_buffer: 327 + index_buffer: 294 + index_buffer: 460 + index_buffer: 98 + index_buffer: 240 + index_buffer: 64 + index_buffer: 455 + index_buffer: 460 + index_buffer: 294 + index_buffer: 235 + index_buffer: 64 + index_buffer: 240 + index_buffer: 294 + index_buffer: 331 + index_buffer: 278 + index_buffer: 64 + index_buffer: 48 + index_buffer: 102 + index_buffer: 279 + index_buffer: 278 + index_buffer: 331 + index_buffer: 49 + index_buffer: 102 + index_buffer: 48 + index_buffer: 303 + index_buffer: 271 + index_buffer: 304 + index_buffer: 73 + index_buffer: 74 + index_buffer: 41 + index_buffer: 272 + index_buffer: 304 + index_buffer: 271 + index_buffer: 42 + index_buffer: 41 + index_buffer: 74 + index_buffer: 427 + index_buffer: 436 + index_buffer: 434 + index_buffer: 207 + index_buffer: 214 + index_buffer: 216 + index_buffer: 432 + index_buffer: 434 + index_buffer: 436 + index_buffer: 212 + index_buffer: 216 + index_buffer: 214 + index_buffer: 304 + index_buffer: 272 + index_buffer: 408 + index_buffer: 74 + index_buffer: 184 + index_buffer: 42 + index_buffer: 407 + index_buffer: 408 + index_buffer: 272 + index_buffer: 183 + index_buffer: 42 + index_buffer: 184 + index_buffer: 394 + index_buffer: 430 + index_buffer: 395 + index_buffer: 169 + index_buffer: 170 + index_buffer: 210 + index_buffer: 431 + index_buffer: 395 + index_buffer: 430 + index_buffer: 211 + index_buffer: 210 + index_buffer: 170 + index_buffer: 395 + index_buffer: 369 + index_buffer: 378 + index_buffer: 170 + index_buffer: 149 + index_buffer: 140 + index_buffer: 400 + index_buffer: 378 + index_buffer: 369 + index_buffer: 176 + index_buffer: 140 + index_buffer: 149 + index_buffer: 296 + index_buffer: 334 + index_buffer: 299 + index_buffer: 66 + index_buffer: 69 + index_buffer: 105 + index_buffer: 333 + index_buffer: 299 + index_buffer: 334 + index_buffer: 104 + index_buffer: 105 + index_buffer: 69 + index_buffer: 417 + index_buffer: 168 + index_buffer: 351 + index_buffer: 193 + index_buffer: 122 + index_buffer: 168 + index_buffer: 6 + index_buffer: 351 + index_buffer: 168 + index_buffer: 6 + index_buffer: 168 + index_buffer: 122 + index_buffer: 280 + index_buffer: 411 + index_buffer: 352 + index_buffer: 50 + index_buffer: 123 + index_buffer: 187 + index_buffer: 376 + index_buffer: 352 + index_buffer: 411 + index_buffer: 147 + index_buffer: 187 + index_buffer: 123 + index_buffer: 319 + index_buffer: 320 + index_buffer: 325 + index_buffer: 89 + index_buffer: 96 + index_buffer: 90 + index_buffer: 307 + index_buffer: 325 + index_buffer: 320 + index_buffer: 77 + index_buffer: 90 + index_buffer: 96 + index_buffer: 285 + index_buffer: 295 + index_buffer: 336 + index_buffer: 55 + index_buffer: 107 + index_buffer: 65 + index_buffer: 296 + index_buffer: 336 + index_buffer: 295 + index_buffer: 66 + index_buffer: 65 + index_buffer: 107 + index_buffer: 404 + index_buffer: 320 + index_buffer: 403 + index_buffer: 180 + index_buffer: 179 + index_buffer: 90 + index_buffer: 319 + index_buffer: 403 + index_buffer: 320 + index_buffer: 89 + index_buffer: 90 + index_buffer: 179 + index_buffer: 330 + index_buffer: 348 + index_buffer: 329 + index_buffer: 101 + index_buffer: 100 + index_buffer: 119 + index_buffer: 349 + index_buffer: 329 + index_buffer: 348 + index_buffer: 120 + index_buffer: 119 + index_buffer: 100 + index_buffer: 334 + index_buffer: 293 + index_buffer: 333 + index_buffer: 105 + index_buffer: 104 + index_buffer: 63 + index_buffer: 298 + index_buffer: 333 + index_buffer: 293 + index_buffer: 68 + index_buffer: 63 + index_buffer: 104 + index_buffer: 323 + index_buffer: 454 + index_buffer: 366 + index_buffer: 93 + index_buffer: 137 + index_buffer: 234 + index_buffer: 447 + index_buffer: 366 + index_buffer: 454 + index_buffer: 227 + index_buffer: 234 + index_buffer: 137 + index_buffer: 16 + index_buffer: 315 + index_buffer: 15 + index_buffer: 16 + index_buffer: 15 + index_buffer: 85 + index_buffer: 316 + index_buffer: 15 + index_buffer: 315 + index_buffer: 86 + index_buffer: 85 + index_buffer: 15 + index_buffer: 429 + index_buffer: 279 + index_buffer: 358 + index_buffer: 209 + index_buffer: 129 + index_buffer: 49 + index_buffer: 331 + index_buffer: 358 + index_buffer: 279 + index_buffer: 102 + index_buffer: 49 + index_buffer: 129 + index_buffer: 15 + index_buffer: 316 + index_buffer: 14 + index_buffer: 15 + index_buffer: 14 + index_buffer: 86 + index_buffer: 317 + index_buffer: 14 + index_buffer: 316 + index_buffer: 87 + index_buffer: 86 + index_buffer: 14 + index_buffer: 8 + index_buffer: 285 + index_buffer: 9 + index_buffer: 8 + index_buffer: 9 + index_buffer: 55 + index_buffer: 336 + index_buffer: 9 + index_buffer: 285 + index_buffer: 107 + index_buffer: 55 + index_buffer: 9 + index_buffer: 329 + index_buffer: 349 + index_buffer: 277 + index_buffer: 100 + index_buffer: 47 + index_buffer: 120 + index_buffer: 350 + index_buffer: 277 + index_buffer: 349 + index_buffer: 121 + index_buffer: 120 + index_buffer: 47 + index_buffer: 252 + index_buffer: 253 + index_buffer: 380 + index_buffer: 22 + index_buffer: 153 + index_buffer: 23 + index_buffer: 374 + index_buffer: 380 + index_buffer: 253 + index_buffer: 145 + index_buffer: 23 + index_buffer: 153 + index_buffer: 402 + index_buffer: 403 + index_buffer: 318 + index_buffer: 178 + index_buffer: 88 + index_buffer: 179 + index_buffer: 319 + index_buffer: 318 + index_buffer: 403 + index_buffer: 89 + index_buffer: 179 + index_buffer: 88 + index_buffer: 351 + index_buffer: 6 + index_buffer: 419 + index_buffer: 122 + index_buffer: 196 + index_buffer: 6 + index_buffer: 197 + index_buffer: 419 + index_buffer: 6 + index_buffer: 197 + index_buffer: 6 + index_buffer: 196 + index_buffer: 324 + index_buffer: 318 + index_buffer: 325 + index_buffer: 95 + index_buffer: 96 + index_buffer: 88 + index_buffer: 319 + index_buffer: 325 + index_buffer: 318 + index_buffer: 89 + index_buffer: 88 + index_buffer: 96 + index_buffer: 397 + index_buffer: 367 + index_buffer: 365 + index_buffer: 172 + index_buffer: 136 + index_buffer: 138 + index_buffer: 364 + index_buffer: 365 + index_buffer: 367 + index_buffer: 135 + index_buffer: 138 + index_buffer: 136 + index_buffer: 288 + index_buffer: 435 + index_buffer: 397 + index_buffer: 58 + index_buffer: 172 + index_buffer: 215 + index_buffer: 367 + index_buffer: 397 + index_buffer: 435 + index_buffer: 138 + index_buffer: 215 + index_buffer: 172 + index_buffer: 438 + index_buffer: 439 + index_buffer: 344 + index_buffer: 218 + index_buffer: 115 + index_buffer: 219 + index_buffer: 278 + index_buffer: 344 + index_buffer: 439 + index_buffer: 48 + index_buffer: 219 + index_buffer: 115 + index_buffer: 271 + index_buffer: 311 + index_buffer: 272 + index_buffer: 41 + index_buffer: 42 + index_buffer: 81 + index_buffer: 310 + index_buffer: 272 + index_buffer: 311 + index_buffer: 80 + index_buffer: 81 + index_buffer: 42 + index_buffer: 5 + index_buffer: 281 + index_buffer: 195 + index_buffer: 5 + index_buffer: 195 + index_buffer: 51 + index_buffer: 248 + index_buffer: 195 + index_buffer: 281 + index_buffer: 3 + index_buffer: 51 + index_buffer: 195 + index_buffer: 273 + index_buffer: 287 + index_buffer: 375 + index_buffer: 43 + index_buffer: 146 + index_buffer: 57 + index_buffer: 291 + index_buffer: 375 + index_buffer: 287 + index_buffer: 61 + index_buffer: 57 + index_buffer: 146 + index_buffer: 396 + index_buffer: 428 + index_buffer: 175 + index_buffer: 171 + index_buffer: 175 + index_buffer: 208 + index_buffer: 199 + index_buffer: 175 + index_buffer: 428 + index_buffer: 199 + index_buffer: 208 + index_buffer: 175 + index_buffer: 268 + index_buffer: 312 + index_buffer: 271 + index_buffer: 38 + index_buffer: 41 + index_buffer: 82 + index_buffer: 311 + index_buffer: 271 + index_buffer: 312 + index_buffer: 81 + index_buffer: 82 + index_buffer: 41 + index_buffer: 444 + index_buffer: 445 + index_buffer: 283 + index_buffer: 224 + index_buffer: 53 + index_buffer: 225 + index_buffer: 276 + index_buffer: 283 + index_buffer: 445 + index_buffer: 46 + index_buffer: 225 + index_buffer: 53 + index_buffer: 254 + index_buffer: 339 + index_buffer: 373 + index_buffer: 24 + index_buffer: 144 + index_buffer: 110 + index_buffer: 390 + index_buffer: 373 + index_buffer: 339 + index_buffer: 163 + index_buffer: 110 + index_buffer: 144 + index_buffer: 295 + index_buffer: 282 + index_buffer: 296 + index_buffer: 65 + index_buffer: 66 + index_buffer: 52 + index_buffer: 334 + index_buffer: 296 + index_buffer: 282 + index_buffer: 105 + index_buffer: 52 + index_buffer: 66 + index_buffer: 346 + index_buffer: 448 + index_buffer: 347 + index_buffer: 117 + index_buffer: 118 + index_buffer: 228 + index_buffer: 449 + index_buffer: 347 + index_buffer: 448 + index_buffer: 229 + index_buffer: 228 + index_buffer: 118 + index_buffer: 454 + index_buffer: 356 + index_buffer: 447 + index_buffer: 234 + index_buffer: 227 + index_buffer: 127 + index_buffer: 264 + index_buffer: 447 + index_buffer: 356 + index_buffer: 34 + index_buffer: 127 + index_buffer: 227 + index_buffer: 336 + index_buffer: 296 + index_buffer: 337 + index_buffer: 107 + index_buffer: 108 + index_buffer: 66 + index_buffer: 299 + index_buffer: 337 + index_buffer: 296 + index_buffer: 69 + index_buffer: 66 + index_buffer: 108 + index_buffer: 151 + index_buffer: 337 + index_buffer: 10 + index_buffer: 151 + index_buffer: 10 + index_buffer: 108 + index_buffer: 338 + index_buffer: 10 + index_buffer: 337 + index_buffer: 109 + index_buffer: 108 + index_buffer: 10 + index_buffer: 278 + index_buffer: 439 + index_buffer: 294 + index_buffer: 48 + index_buffer: 64 + index_buffer: 219 + index_buffer: 455 + index_buffer: 294 + index_buffer: 439 + index_buffer: 235 + index_buffer: 219 + index_buffer: 64 + index_buffer: 407 + index_buffer: 415 + index_buffer: 292 + index_buffer: 183 + index_buffer: 62 + index_buffer: 191 + index_buffer: 308 + index_buffer: 292 + index_buffer: 415 + index_buffer: 78 + index_buffer: 191 + index_buffer: 62 + index_buffer: 358 + index_buffer: 371 + index_buffer: 429 + index_buffer: 129 + index_buffer: 209 + index_buffer: 142 + index_buffer: 355 + index_buffer: 429 + index_buffer: 371 + index_buffer: 126 + index_buffer: 142 + index_buffer: 209 + index_buffer: 345 + index_buffer: 372 + index_buffer: 340 + index_buffer: 116 + index_buffer: 111 + index_buffer: 143 + index_buffer: 265 + index_buffer: 340 + index_buffer: 372 + index_buffer: 35 + index_buffer: 143 + index_buffer: 111 + index_buffer: 388 + index_buffer: 390 + index_buffer: 466 + index_buffer: 161 + index_buffer: 246 + index_buffer: 163 + index_buffer: 249 + index_buffer: 466 + index_buffer: 390 + index_buffer: 7 + index_buffer: 163 + index_buffer: 246 + index_buffer: 352 + index_buffer: 346 + index_buffer: 280 + index_buffer: 123 + index_buffer: 50 + index_buffer: 117 + index_buffer: 347 + index_buffer: 280 + index_buffer: 346 + index_buffer: 118 + index_buffer: 117 + index_buffer: 50 + index_buffer: 295 + index_buffer: 442 + index_buffer: 282 + index_buffer: 65 + index_buffer: 52 + index_buffer: 222 + index_buffer: 443 + index_buffer: 282 + index_buffer: 442 + index_buffer: 223 + index_buffer: 222 + index_buffer: 52 + index_buffer: 19 + index_buffer: 94 + index_buffer: 354 + index_buffer: 19 + index_buffer: 125 + index_buffer: 94 + index_buffer: 370 + index_buffer: 354 + index_buffer: 94 + index_buffer: 141 + index_buffer: 94 + index_buffer: 125 + index_buffer: 295 + index_buffer: 285 + index_buffer: 442 + index_buffer: 65 + index_buffer: 222 + index_buffer: 55 + index_buffer: 441 + index_buffer: 442 + index_buffer: 285 + index_buffer: 221 + index_buffer: 55 + index_buffer: 222 + index_buffer: 419 + index_buffer: 197 + index_buffer: 248 + index_buffer: 196 + index_buffer: 3 + index_buffer: 197 + index_buffer: 195 + index_buffer: 248 + index_buffer: 197 + index_buffer: 195 + index_buffer: 197 + index_buffer: 3 + index_buffer: 359 + index_buffer: 263 + index_buffer: 255 + index_buffer: 130 + index_buffer: 25 + index_buffer: 33 + index_buffer: 249 + index_buffer: 255 + index_buffer: 263 + index_buffer: 7 + index_buffer: 33 + index_buffer: 25 + index_buffer: 275 + index_buffer: 274 + index_buffer: 440 + index_buffer: 45 + index_buffer: 220 + index_buffer: 44 + index_buffer: 457 + index_buffer: 440 + index_buffer: 274 + index_buffer: 237 + index_buffer: 44 + index_buffer: 220 + index_buffer: 300 + index_buffer: 383 + index_buffer: 301 + index_buffer: 70 + index_buffer: 71 + index_buffer: 156 + index_buffer: 368 + index_buffer: 301 + index_buffer: 383 + index_buffer: 139 + index_buffer: 156 + index_buffer: 71 + index_buffer: 417 + index_buffer: 351 + index_buffer: 465 + index_buffer: 193 + index_buffer: 245 + index_buffer: 122 + index_buffer: 412 + index_buffer: 465 + index_buffer: 351 + index_buffer: 188 + index_buffer: 122 + index_buffer: 245 + index_buffer: 466 + index_buffer: 263 + index_buffer: 467 + index_buffer: 246 + index_buffer: 247 + index_buffer: 33 + index_buffer: 359 + index_buffer: 467 + index_buffer: 263 + index_buffer: 130 + index_buffer: 33 + index_buffer: 247 + index_buffer: 389 + index_buffer: 251 + index_buffer: 368 + index_buffer: 162 + index_buffer: 139 + index_buffer: 21 + index_buffer: 301 + index_buffer: 368 + index_buffer: 251 + index_buffer: 71 + index_buffer: 21 + index_buffer: 139 + index_buffer: 374 + index_buffer: 386 + index_buffer: 380 + index_buffer: 145 + index_buffer: 153 + index_buffer: 159 + index_buffer: 385 + index_buffer: 380 + index_buffer: 386 + index_buffer: 158 + index_buffer: 159 + index_buffer: 153 + index_buffer: 379 + index_buffer: 394 + index_buffer: 378 + index_buffer: 150 + index_buffer: 149 + index_buffer: 169 + index_buffer: 395 + index_buffer: 378 + index_buffer: 394 + index_buffer: 170 + index_buffer: 169 + index_buffer: 149 + index_buffer: 351 + index_buffer: 419 + index_buffer: 412 + index_buffer: 122 + index_buffer: 188 + index_buffer: 196 + index_buffer: 399 + index_buffer: 412 + index_buffer: 419 + index_buffer: 174 + index_buffer: 196 + index_buffer: 188 + index_buffer: 426 + index_buffer: 322 + index_buffer: 436 + index_buffer: 206 + index_buffer: 216 + index_buffer: 92 + index_buffer: 410 + index_buffer: 436 + index_buffer: 322 + index_buffer: 186 + index_buffer: 92 + index_buffer: 216 + index_buffer: 387 + index_buffer: 373 + index_buffer: 388 + index_buffer: 160 + index_buffer: 161 + index_buffer: 144 + index_buffer: 390 + index_buffer: 388 + index_buffer: 373 + index_buffer: 163 + index_buffer: 144 + index_buffer: 161 + index_buffer: 393 + index_buffer: 326 + index_buffer: 164 + index_buffer: 167 + index_buffer: 164 + index_buffer: 97 + index_buffer: 2 + index_buffer: 164 + index_buffer: 326 + index_buffer: 2 + index_buffer: 97 + index_buffer: 164 + index_buffer: 354 + index_buffer: 370 + index_buffer: 461 + index_buffer: 125 + index_buffer: 241 + index_buffer: 141 + index_buffer: 462 + index_buffer: 461 + index_buffer: 370 + index_buffer: 242 + index_buffer: 141 + index_buffer: 241 + index_buffer: 0 + index_buffer: 267 + index_buffer: 164 + index_buffer: 0 + index_buffer: 164 + index_buffer: 37 + index_buffer: 393 + index_buffer: 164 + index_buffer: 267 + index_buffer: 167 + index_buffer: 37 + index_buffer: 164 + index_buffer: 11 + index_buffer: 12 + index_buffer: 302 + index_buffer: 11 + index_buffer: 72 + index_buffer: 12 + index_buffer: 268 + index_buffer: 302 + index_buffer: 12 + index_buffer: 38 + index_buffer: 12 + index_buffer: 72 + index_buffer: 386 + index_buffer: 374 + index_buffer: 387 + index_buffer: 159 + index_buffer: 160 + index_buffer: 145 + index_buffer: 373 + index_buffer: 387 + index_buffer: 374 + index_buffer: 144 + index_buffer: 145 + index_buffer: 160 + index_buffer: 12 + index_buffer: 13 + index_buffer: 268 + index_buffer: 12 + index_buffer: 38 + index_buffer: 13 + index_buffer: 312 + index_buffer: 268 + index_buffer: 13 + index_buffer: 82 + index_buffer: 13 + index_buffer: 38 + index_buffer: 293 + index_buffer: 300 + index_buffer: 298 + index_buffer: 63 + index_buffer: 68 + index_buffer: 70 + index_buffer: 301 + index_buffer: 298 + index_buffer: 300 + index_buffer: 71 + index_buffer: 70 + index_buffer: 68 + index_buffer: 340 + index_buffer: 265 + index_buffer: 261 + index_buffer: 111 + index_buffer: 31 + index_buffer: 35 + index_buffer: 446 + index_buffer: 261 + index_buffer: 265 + index_buffer: 226 + index_buffer: 35 + index_buffer: 31 + index_buffer: 380 + index_buffer: 385 + index_buffer: 381 + index_buffer: 153 + index_buffer: 154 + index_buffer: 158 + index_buffer: 384 + index_buffer: 381 + index_buffer: 385 + index_buffer: 157 + index_buffer: 158 + index_buffer: 154 + index_buffer: 280 + index_buffer: 330 + index_buffer: 425 + index_buffer: 50 + index_buffer: 205 + index_buffer: 101 + index_buffer: 266 + index_buffer: 425 + index_buffer: 330 + index_buffer: 36 + index_buffer: 101 + index_buffer: 205 + index_buffer: 423 + index_buffer: 391 + index_buffer: 426 + index_buffer: 203 + index_buffer: 206 + index_buffer: 165 + index_buffer: 322 + index_buffer: 426 + index_buffer: 391 + index_buffer: 92 + index_buffer: 165 + index_buffer: 206 + index_buffer: 429 + index_buffer: 355 + index_buffer: 420 + index_buffer: 209 + index_buffer: 198 + index_buffer: 126 + index_buffer: 437 + index_buffer: 420 + index_buffer: 355 + index_buffer: 217 + index_buffer: 126 + index_buffer: 198 + index_buffer: 391 + index_buffer: 327 + index_buffer: 393 + index_buffer: 165 + index_buffer: 167 + index_buffer: 98 + index_buffer: 326 + index_buffer: 393 + index_buffer: 327 + index_buffer: 97 + index_buffer: 98 + index_buffer: 167 + index_buffer: 457 + index_buffer: 438 + index_buffer: 440 + index_buffer: 237 + index_buffer: 220 + index_buffer: 218 + index_buffer: 344 + index_buffer: 440 + index_buffer: 438 + index_buffer: 115 + index_buffer: 218 + index_buffer: 220 + index_buffer: 382 + index_buffer: 362 + index_buffer: 341 + index_buffer: 155 + index_buffer: 112 + index_buffer: 133 + index_buffer: 463 + index_buffer: 341 + index_buffer: 362 + index_buffer: 243 + index_buffer: 133 + index_buffer: 112 + index_buffer: 457 + index_buffer: 461 + index_buffer: 459 + index_buffer: 237 + index_buffer: 239 + index_buffer: 241 + index_buffer: 458 + index_buffer: 459 + index_buffer: 461 + index_buffer: 238 + index_buffer: 241 + index_buffer: 239 + index_buffer: 434 + index_buffer: 430 + index_buffer: 364 + index_buffer: 214 + index_buffer: 135 + index_buffer: 210 + index_buffer: 394 + index_buffer: 364 + index_buffer: 430 + index_buffer: 169 + index_buffer: 210 + index_buffer: 135 + index_buffer: 414 + index_buffer: 463 + index_buffer: 398 + index_buffer: 190 + index_buffer: 173 + index_buffer: 243 + index_buffer: 362 + index_buffer: 398 + index_buffer: 463 + index_buffer: 133 + index_buffer: 243 + index_buffer: 173 + index_buffer: 262 + index_buffer: 428 + index_buffer: 369 + index_buffer: 32 + index_buffer: 140 + index_buffer: 208 + index_buffer: 396 + index_buffer: 369 + index_buffer: 428 + index_buffer: 171 + index_buffer: 208 + index_buffer: 140 + index_buffer: 457 + index_buffer: 274 + index_buffer: 461 + index_buffer: 237 + index_buffer: 241 + index_buffer: 44 + index_buffer: 354 + index_buffer: 461 + index_buffer: 274 + index_buffer: 125 + index_buffer: 44 + index_buffer: 241 + index_buffer: 316 + index_buffer: 403 + index_buffer: 317 + index_buffer: 86 + index_buffer: 87 + index_buffer: 179 + index_buffer: 402 + index_buffer: 317 + index_buffer: 403 + index_buffer: 178 + index_buffer: 179 + index_buffer: 87 + index_buffer: 315 + index_buffer: 404 + index_buffer: 316 + index_buffer: 85 + index_buffer: 86 + index_buffer: 180 + index_buffer: 403 + index_buffer: 316 + index_buffer: 404 + index_buffer: 179 + index_buffer: 180 + index_buffer: 86 + index_buffer: 314 + index_buffer: 405 + index_buffer: 315 + index_buffer: 84 + index_buffer: 85 + index_buffer: 181 + index_buffer: 404 + index_buffer: 315 + index_buffer: 405 + index_buffer: 180 + index_buffer: 181 + index_buffer: 85 + index_buffer: 313 + index_buffer: 406 + index_buffer: 314 + index_buffer: 83 + index_buffer: 84 + index_buffer: 182 + index_buffer: 405 + index_buffer: 314 + index_buffer: 406 + index_buffer: 181 + index_buffer: 182 + index_buffer: 84 + index_buffer: 418 + index_buffer: 406 + index_buffer: 421 + index_buffer: 194 + index_buffer: 201 + index_buffer: 182 + index_buffer: 313 + index_buffer: 421 + index_buffer: 406 + index_buffer: 83 + index_buffer: 182 + index_buffer: 201 + index_buffer: 366 + index_buffer: 401 + index_buffer: 323 + index_buffer: 137 + index_buffer: 93 + index_buffer: 177 + index_buffer: 361 + index_buffer: 323 + index_buffer: 401 + index_buffer: 132 + index_buffer: 177 + index_buffer: 93 + index_buffer: 408 + index_buffer: 407 + index_buffer: 306 + index_buffer: 184 + index_buffer: 76 + index_buffer: 183 + index_buffer: 292 + index_buffer: 306 + index_buffer: 407 + index_buffer: 62 + index_buffer: 183 + index_buffer: 76 + index_buffer: 408 + index_buffer: 306 + index_buffer: 409 + index_buffer: 184 + index_buffer: 185 + index_buffer: 76 + index_buffer: 291 + index_buffer: 409 + index_buffer: 306 + index_buffer: 61 + index_buffer: 76 + index_buffer: 185 + index_buffer: 410 + index_buffer: 409 + index_buffer: 287 + index_buffer: 186 + index_buffer: 57 + index_buffer: 185 + index_buffer: 291 + index_buffer: 287 + index_buffer: 409 + index_buffer: 61 + index_buffer: 185 + index_buffer: 57 + index_buffer: 436 + index_buffer: 410 + index_buffer: 432 + index_buffer: 216 + index_buffer: 212 + index_buffer: 186 + index_buffer: 287 + index_buffer: 432 + index_buffer: 410 + index_buffer: 57 + index_buffer: 186 + index_buffer: 212 + index_buffer: 434 + index_buffer: 416 + index_buffer: 427 + index_buffer: 214 + index_buffer: 207 + index_buffer: 192 + index_buffer: 411 + index_buffer: 427 + index_buffer: 416 + index_buffer: 187 + index_buffer: 192 + index_buffer: 207 + index_buffer: 264 + index_buffer: 368 + index_buffer: 372 + index_buffer: 34 + index_buffer: 143 + index_buffer: 139 + index_buffer: 383 + index_buffer: 372 + index_buffer: 368 + index_buffer: 156 + index_buffer: 139 + index_buffer: 143 + index_buffer: 457 + index_buffer: 459 + index_buffer: 438 + index_buffer: 237 + index_buffer: 218 + index_buffer: 239 + index_buffer: 309 + index_buffer: 438 + index_buffer: 459 + index_buffer: 79 + index_buffer: 239 + index_buffer: 218 + index_buffer: 352 + index_buffer: 376 + index_buffer: 366 + index_buffer: 123 + index_buffer: 137 + index_buffer: 147 + index_buffer: 401 + index_buffer: 366 + index_buffer: 376 + index_buffer: 177 + index_buffer: 147 + index_buffer: 137 + index_buffer: 4 + index_buffer: 1 + index_buffer: 275 + index_buffer: 4 + index_buffer: 45 + index_buffer: 1 + index_buffer: 274 + index_buffer: 275 + index_buffer: 1 + index_buffer: 44 + index_buffer: 1 + index_buffer: 45 + index_buffer: 428 + index_buffer: 262 + index_buffer: 421 + index_buffer: 208 + index_buffer: 201 + index_buffer: 32 + index_buffer: 418 + index_buffer: 421 + index_buffer: 262 + index_buffer: 194 + index_buffer: 32 + index_buffer: 201 + index_buffer: 327 + index_buffer: 358 + index_buffer: 294 + index_buffer: 98 + index_buffer: 64 + index_buffer: 129 + index_buffer: 331 + index_buffer: 294 + index_buffer: 358 + index_buffer: 102 + index_buffer: 129 + index_buffer: 64 + index_buffer: 367 + index_buffer: 435 + index_buffer: 416 + index_buffer: 138 + index_buffer: 192 + index_buffer: 215 + index_buffer: 433 + index_buffer: 416 + index_buffer: 435 + index_buffer: 213 + index_buffer: 215 + index_buffer: 192 + index_buffer: 455 + index_buffer: 439 + index_buffer: 289 + index_buffer: 235 + index_buffer: 59 + index_buffer: 219 + index_buffer: 392 + index_buffer: 289 + index_buffer: 439 + index_buffer: 166 + index_buffer: 219 + index_buffer: 59 + index_buffer: 328 + index_buffer: 462 + index_buffer: 326 + index_buffer: 99 + index_buffer: 97 + index_buffer: 242 + index_buffer: 370 + index_buffer: 326 + index_buffer: 462 + index_buffer: 141 + index_buffer: 242 + index_buffer: 97 + index_buffer: 326 + index_buffer: 370 + index_buffer: 2 + index_buffer: 97 + index_buffer: 2 + index_buffer: 141 + index_buffer: 94 + index_buffer: 2 + index_buffer: 370 + index_buffer: 94 + index_buffer: 141 + index_buffer: 2 + index_buffer: 460 + index_buffer: 455 + index_buffer: 305 + index_buffer: 240 + index_buffer: 75 + index_buffer: 235 + index_buffer: 289 + index_buffer: 305 + index_buffer: 455 + index_buffer: 59 + index_buffer: 235 + index_buffer: 75 + index_buffer: 448 + index_buffer: 339 + index_buffer: 449 + index_buffer: 228 + index_buffer: 229 + index_buffer: 110 + index_buffer: 254 + index_buffer: 449 + index_buffer: 339 + index_buffer: 24 + index_buffer: 110 + index_buffer: 229 + index_buffer: 261 + index_buffer: 446 + index_buffer: 255 + index_buffer: 31 + index_buffer: 25 + index_buffer: 226 + index_buffer: 359 + index_buffer: 255 + index_buffer: 446 + index_buffer: 130 + index_buffer: 226 + index_buffer: 25 + index_buffer: 449 + index_buffer: 254 + index_buffer: 450 + index_buffer: 229 + index_buffer: 230 + index_buffer: 24 + index_buffer: 253 + index_buffer: 450 + index_buffer: 254 + index_buffer: 23 + index_buffer: 24 + index_buffer: 230 + index_buffer: 450 + index_buffer: 253 + index_buffer: 451 + index_buffer: 230 + index_buffer: 231 + index_buffer: 23 + index_buffer: 252 + index_buffer: 451 + index_buffer: 253 + index_buffer: 22 + index_buffer: 23 + index_buffer: 231 + index_buffer: 451 + index_buffer: 252 + index_buffer: 452 + index_buffer: 231 + index_buffer: 232 + index_buffer: 22 + index_buffer: 256 + index_buffer: 452 + index_buffer: 252 + index_buffer: 26 + index_buffer: 22 + index_buffer: 232 + index_buffer: 256 + index_buffer: 341 + index_buffer: 452 + index_buffer: 26 + index_buffer: 232 + index_buffer: 112 + index_buffer: 453 + index_buffer: 452 + index_buffer: 341 + index_buffer: 233 + index_buffer: 112 + index_buffer: 232 + index_buffer: 413 + index_buffer: 464 + index_buffer: 414 + index_buffer: 189 + index_buffer: 190 + index_buffer: 244 + index_buffer: 463 + index_buffer: 414 + index_buffer: 464 + index_buffer: 243 + index_buffer: 244 + index_buffer: 190 + index_buffer: 441 + index_buffer: 413 + index_buffer: 286 + index_buffer: 221 + index_buffer: 56 + index_buffer: 189 + index_buffer: 414 + index_buffer: 286 + index_buffer: 413 + index_buffer: 190 + index_buffer: 189 + index_buffer: 56 + index_buffer: 441 + index_buffer: 286 + index_buffer: 442 + index_buffer: 221 + index_buffer: 222 + index_buffer: 56 + index_buffer: 258 + index_buffer: 442 + index_buffer: 286 + index_buffer: 28 + index_buffer: 56 + index_buffer: 222 + index_buffer: 442 + index_buffer: 258 + index_buffer: 443 + index_buffer: 222 + index_buffer: 223 + index_buffer: 28 + index_buffer: 257 + index_buffer: 443 + index_buffer: 258 + index_buffer: 27 + index_buffer: 28 + index_buffer: 223 + index_buffer: 444 + index_buffer: 443 + index_buffer: 259 + index_buffer: 224 + index_buffer: 29 + index_buffer: 223 + index_buffer: 257 + index_buffer: 259 + index_buffer: 443 + index_buffer: 27 + index_buffer: 223 + index_buffer: 29 + index_buffer: 259 + index_buffer: 260 + index_buffer: 444 + index_buffer: 29 + index_buffer: 224 + index_buffer: 30 + index_buffer: 445 + index_buffer: 444 + index_buffer: 260 + index_buffer: 225 + index_buffer: 30 + index_buffer: 224 + index_buffer: 260 + index_buffer: 467 + index_buffer: 445 + index_buffer: 30 + index_buffer: 225 + index_buffer: 247 + index_buffer: 342 + index_buffer: 445 + index_buffer: 467 + index_buffer: 113 + index_buffer: 247 + index_buffer: 225 + index_buffer: 250 + index_buffer: 309 + index_buffer: 458 + index_buffer: 20 + index_buffer: 238 + index_buffer: 79 + index_buffer: 459 + index_buffer: 458 + index_buffer: 309 + index_buffer: 239 + index_buffer: 79 + index_buffer: 238 + index_buffer: 290 + index_buffer: 305 + index_buffer: 392 + index_buffer: 60 + index_buffer: 166 + index_buffer: 75 + index_buffer: 289 + index_buffer: 392 + index_buffer: 305 + index_buffer: 59 + index_buffer: 75 + index_buffer: 166 + index_buffer: 460 + index_buffer: 305 + index_buffer: 328 + index_buffer: 240 + index_buffer: 99 + index_buffer: 75 + index_buffer: 290 + index_buffer: 328 + index_buffer: 305 + index_buffer: 60 + index_buffer: 75 + index_buffer: 99 + index_buffer: 376 + index_buffer: 433 + index_buffer: 401 + index_buffer: 147 + index_buffer: 177 + index_buffer: 213 + index_buffer: 435 + index_buffer: 401 + index_buffer: 433 + index_buffer: 215 + index_buffer: 213 + index_buffer: 177 + index_buffer: 250 + index_buffer: 290 + index_buffer: 309 + index_buffer: 20 + index_buffer: 79 + index_buffer: 60 + index_buffer: 392 + index_buffer: 309 + index_buffer: 290 + index_buffer: 166 + index_buffer: 60 + index_buffer: 79 + index_buffer: 411 + index_buffer: 416 + index_buffer: 376 + index_buffer: 187 + index_buffer: 147 + index_buffer: 192 + index_buffer: 433 + index_buffer: 376 + index_buffer: 416 + index_buffer: 213 + index_buffer: 192 + index_buffer: 147 + index_buffer: 341 + index_buffer: 463 + index_buffer: 453 + index_buffer: 112 + index_buffer: 233 + index_buffer: 243 + index_buffer: 464 + index_buffer: 453 + index_buffer: 463 + index_buffer: 244 + index_buffer: 243 + index_buffer: 233 + index_buffer: 453 + index_buffer: 464 + index_buffer: 357 + index_buffer: 233 + index_buffer: 128 + index_buffer: 244 + index_buffer: 465 + index_buffer: 357 + index_buffer: 464 + index_buffer: 245 + index_buffer: 244 + index_buffer: 128 + index_buffer: 412 + index_buffer: 343 + index_buffer: 465 + index_buffer: 188 + index_buffer: 245 + index_buffer: 114 + index_buffer: 357 + index_buffer: 465 + index_buffer: 343 + index_buffer: 128 + index_buffer: 114 + index_buffer: 245 + index_buffer: 437 + index_buffer: 343 + index_buffer: 399 + index_buffer: 217 + index_buffer: 174 + index_buffer: 114 + index_buffer: 412 + index_buffer: 399 + index_buffer: 343 + index_buffer: 188 + index_buffer: 114 + index_buffer: 174 + index_buffer: 363 + index_buffer: 440 + index_buffer: 360 + index_buffer: 134 + index_buffer: 131 + index_buffer: 220 + index_buffer: 344 + index_buffer: 360 + index_buffer: 440 + index_buffer: 115 + index_buffer: 220 + index_buffer: 131 + index_buffer: 456 + index_buffer: 420 + index_buffer: 399 + index_buffer: 236 + index_buffer: 174 + index_buffer: 198 + index_buffer: 437 + index_buffer: 399 + index_buffer: 420 + index_buffer: 217 + index_buffer: 198 + index_buffer: 174 + index_buffer: 456 + index_buffer: 363 + index_buffer: 420 + index_buffer: 236 + index_buffer: 198 + index_buffer: 134 + index_buffer: 360 + index_buffer: 420 + index_buffer: 363 + index_buffer: 131 + index_buffer: 134 + index_buffer: 198 + index_buffer: 361 + index_buffer: 401 + index_buffer: 288 + index_buffer: 132 + index_buffer: 58 + index_buffer: 177 + index_buffer: 435 + index_buffer: 288 + index_buffer: 401 + index_buffer: 215 + index_buffer: 177 + index_buffer: 58 + index_buffer: 353 + index_buffer: 265 + index_buffer: 383 + index_buffer: 124 + index_buffer: 156 + index_buffer: 35 + index_buffer: 372 + index_buffer: 383 + index_buffer: 265 + index_buffer: 143 + index_buffer: 35 + index_buffer: 156 + index_buffer: 255 + index_buffer: 249 + index_buffer: 339 + index_buffer: 25 + index_buffer: 110 + index_buffer: 7 + index_buffer: 390 + index_buffer: 339 + index_buffer: 249 + index_buffer: 163 + index_buffer: 7 + index_buffer: 110 + index_buffer: 261 + index_buffer: 255 + index_buffer: 448 + index_buffer: 31 + index_buffer: 228 + index_buffer: 25 + index_buffer: 339 + index_buffer: 448 + index_buffer: 255 + index_buffer: 110 + index_buffer: 25 + index_buffer: 228 + index_buffer: 14 + index_buffer: 317 + index_buffer: 13 + index_buffer: 14 + index_buffer: 13 + index_buffer: 87 + index_buffer: 312 + index_buffer: 13 + index_buffer: 317 + index_buffer: 82 + index_buffer: 87 + index_buffer: 13 + index_buffer: 317 + index_buffer: 402 + index_buffer: 312 + index_buffer: 87 + index_buffer: 82 + index_buffer: 178 + index_buffer: 311 + index_buffer: 312 + index_buffer: 402 + index_buffer: 81 + index_buffer: 178 + index_buffer: 82 + index_buffer: 402 + index_buffer: 318 + index_buffer: 311 + index_buffer: 178 + index_buffer: 81 + index_buffer: 88 + index_buffer: 310 + index_buffer: 311 + index_buffer: 318 + index_buffer: 80 + index_buffer: 88 + index_buffer: 81 + index_buffer: 318 + index_buffer: 324 + index_buffer: 310 + index_buffer: 88 + index_buffer: 80 + index_buffer: 95 + index_buffer: 415 + index_buffer: 310 + index_buffer: 324 + index_buffer: 191 + index_buffer: 95 + index_buffer: 80 +} diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/BUILD b/mediapipe/tasks/cc/vision/face_geometry/libs/BUILD new file mode 100644 index 00000000..4c37953e --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/BUILD @@ -0,0 +1,80 @@ +# Copyright 2023 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:public"]) + +cc_library( + name = "geometry_pipeline", + srcs = ["geometry_pipeline.cc"], + hdrs = ["geometry_pipeline.h"], + deps = [ + ":mesh_3d_utils", + ":procrustes_solver", + ":validation_utils", + "//mediapipe/framework/formats:landmark_cc_proto", + "//mediapipe/framework/formats:matrix", + "//mediapipe/framework/formats:matrix_data_cc_proto", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/framework/port:statusor", + "//mediapipe/tasks/cc/vision/face_geometry/proto:environment_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:face_geometry_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:geometry_pipeline_metadata_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:mesh_3d_cc_proto", + "@com_google_absl//absl/memory", + "@eigen_archive//:eigen3", + ], +) + +cc_library( + name = "mesh_3d_utils", + srcs = ["mesh_3d_utils.cc"], + hdrs = ["mesh_3d_utils.h"], + deps = [ + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:statusor", + "//mediapipe/tasks/cc/vision/face_geometry/proto:mesh_3d_cc_proto", + ], +) + +cc_library( + name = "procrustes_solver", + srcs = ["procrustes_solver.cc"], + hdrs = ["procrustes_solver.h"], + deps = [ + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/framework/port:statusor", + "@com_google_absl//absl/memory", + "@eigen_archive//:eigen3", + ], +) + +cc_library( + name = "validation_utils", + srcs = ["validation_utils.cc"], + hdrs = ["validation_utils.h"], + deps = [ + ":mesh_3d_utils", + "//mediapipe/framework/formats:matrix_data_cc_proto", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/tasks/cc/vision/face_geometry/proto:environment_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:face_geometry_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:geometry_pipeline_metadata_cc_proto", + "//mediapipe/tasks/cc/vision/face_geometry/proto:mesh_3d_cc_proto", + ], +) diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/geometry_pipeline.cc b/mediapipe/tasks/cc/vision/face_geometry/libs/geometry_pipeline.cc new file mode 100644 index 00000000..c7ac7c63 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/geometry_pipeline.cc @@ -0,0 +1,471 @@ +// Copyright 2023 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/tasks/cc/vision/face_geometry/libs/geometry_pipeline.h" + +#include +#include +#include +#include +#include + +#include "Eigen/Core" +#include "absl/memory/memory.h" +#include "mediapipe/framework/formats/landmark.pb.h" +#include "mediapipe/framework/formats/matrix.h" +#include "mediapipe/framework/formats/matrix_data.pb.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/framework/port/status_macros.h" +#include "mediapipe/framework/port/statusor.h" +#include "mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.h" +#include "mediapipe/tasks/cc/vision/face_geometry/libs/procrustes_solver.h" +#include "mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/environment.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/face_geometry.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.pb.h" + +namespace mediapipe::tasks::vision::face_geometry { +namespace { + +struct PerspectiveCameraFrustum { + // NOTE: all arguments must be validated prior to calling this constructor. + PerspectiveCameraFrustum(const proto::PerspectiveCamera& perspective_camera, + int frame_width, int frame_height) { + static constexpr float kDegreesToRadians = 3.14159265358979323846f / 180.f; + + const float height_at_near = + 2.f * perspective_camera.near() * + std::tan(0.5f * kDegreesToRadians * + perspective_camera.vertical_fov_degrees()); + + const float width_at_near = frame_width * height_at_near / frame_height; + + left = -0.5f * width_at_near; + right = 0.5f * width_at_near; + bottom = -0.5f * height_at_near; + top = 0.5f * height_at_near; + near = perspective_camera.near(); + far = perspective_camera.far(); + } + + float left; + float right; + float bottom; + float top; + float near; + float far; +}; + +class ScreenToMetricSpaceConverter { + public: + ScreenToMetricSpaceConverter( + proto::OriginPointLocation origin_point_location, // + proto::InputSource input_source, // + Eigen::Matrix3Xf&& canonical_metric_landmarks, // + Eigen::VectorXf&& landmark_weights, // + std::unique_ptr procrustes_solver) + : origin_point_location_(origin_point_location), + input_source_(input_source), + canonical_metric_landmarks_(std::move(canonical_metric_landmarks)), + landmark_weights_(std::move(landmark_weights)), + procrustes_solver_(std::move(procrustes_solver)) {} + + // Converts `screen_landmark_list` into `metric_landmark_list` and estimates + // the `pose_transform_mat`. + // + // Here's the algorithm summary: + // + // (1) Project X- and Y- screen landmark coordinates at the Z near plane. + // + // (2) Estimate a canonical-to-runtime landmark set scale by running the + // Procrustes solver using the screen runtime landmarks. + // + // On this iteration, screen landmarks are used instead of unprojected + // metric landmarks as it is not safe to unproject due to the relative + // nature of the input screen landmark Z coordinate. + // + // (3) Use the canonical-to-runtime scale from (2) to unproject the screen + // landmarks. The result is referenced as "intermediate landmarks" because + // they are the first estimation of the resuling metric landmarks, but are + // not quite there yet. + // + // (4) Estimate a canonical-to-runtime landmark set scale by running the + // Procrustes solver using the intermediate runtime landmarks. + // + // (5) Use the product of the scale factors from (2) and (4) to unproject + // the screen landmarks the second time. This is the second and the final + // estimation of the metric landmarks. + // + // (6) Multiply each of the metric landmarks by the inverse pose + // transformation matrix to align the runtime metric face landmarks with + // the canonical metric face landmarks. + // + // Note: the input screen landmarks are in the left-handed coordinate system, + // however any metric landmarks - including the canonical metric + // landmarks, the final runtime metric landmarks and any intermediate + // runtime metric landmarks - are in the right-handed coordinate system. + // + // To keep the logic correct, the landmark set handedness is changed any + // time the screen-to-metric semantic barrier is passed. + absl::Status Convert( + const mediapipe::NormalizedLandmarkList& screen_landmark_list, // + const PerspectiveCameraFrustum& pcf, // + mediapipe::LandmarkList& metric_landmark_list, // + Eigen::Matrix4f& pose_transform_mat) const { + RET_CHECK_EQ(screen_landmark_list.landmark_size(), + canonical_metric_landmarks_.cols()) + << "The number of landmarks doesn't match the number passed upon " + "initialization!"; + + Eigen::Matrix3Xf screen_landmarks; + ConvertLandmarkListToEigenMatrix(screen_landmark_list, screen_landmarks); + + ProjectXY(pcf, screen_landmarks); + const float depth_offset = screen_landmarks.row(2).mean(); + + // 1st iteration: don't unproject XY because it's unsafe to do so due to + // the relative nature of the Z coordinate. Instead, run the + // first estimation on the projected XY and use that scale to + // unproject for the 2nd iteration. + Eigen::Matrix3Xf intermediate_landmarks(screen_landmarks); + ChangeHandedness(intermediate_landmarks); + + ASSIGN_OR_RETURN(const float first_iteration_scale, + EstimateScale(intermediate_landmarks), + _ << "Failed to estimate first iteration scale!"); + + // 2nd iteration: unproject XY using the scale from the 1st iteration. + intermediate_landmarks = screen_landmarks; + MoveAndRescaleZ(pcf, depth_offset, first_iteration_scale, + intermediate_landmarks); + UnprojectXY(pcf, intermediate_landmarks); + ChangeHandedness(intermediate_landmarks); + + // For face detection input landmarks, re-write Z-coord from the canonical + // landmarks. + if (input_source_ == proto::InputSource::FACE_DETECTION_PIPELINE) { + Eigen::Matrix4f intermediate_pose_transform_mat; + MP_RETURN_IF_ERROR(procrustes_solver_->SolveWeightedOrthogonalProblem( + canonical_metric_landmarks_, intermediate_landmarks, + landmark_weights_, intermediate_pose_transform_mat)) + << "Failed to estimate pose transform matrix!"; + + intermediate_landmarks.row(2) = + (intermediate_pose_transform_mat * + canonical_metric_landmarks_.colwise().homogeneous()) + .row(2); + } + ASSIGN_OR_RETURN(const float second_iteration_scale, + EstimateScale(intermediate_landmarks), + _ << "Failed to estimate second iteration scale!"); + + // Use the total scale to unproject the screen landmarks. + const float total_scale = first_iteration_scale * second_iteration_scale; + MoveAndRescaleZ(pcf, depth_offset, total_scale, screen_landmarks); + UnprojectXY(pcf, screen_landmarks); + ChangeHandedness(screen_landmarks); + + // At this point, screen landmarks are converted into metric landmarks. + Eigen::Matrix3Xf& metric_landmarks = screen_landmarks; + + MP_RETURN_IF_ERROR(procrustes_solver_->SolveWeightedOrthogonalProblem( + canonical_metric_landmarks_, metric_landmarks, landmark_weights_, + pose_transform_mat)) + << "Failed to estimate pose transform matrix!"; + + // For face detection input landmarks, re-write Z-coord from the canonical + // landmarks and run the pose transform estimation again. + if (input_source_ == proto::InputSource::FACE_DETECTION_PIPELINE) { + metric_landmarks.row(2) = + (pose_transform_mat * + canonical_metric_landmarks_.colwise().homogeneous()) + .row(2); + + MP_RETURN_IF_ERROR(procrustes_solver_->SolveWeightedOrthogonalProblem( + canonical_metric_landmarks_, metric_landmarks, landmark_weights_, + pose_transform_mat)) + << "Failed to estimate pose transform matrix!"; + } + + // Multiply each of the metric landmarks by the inverse pose + // transformation matrix to align the runtime metric face landmarks with + // the canonical metric face landmarks. + metric_landmarks = (pose_transform_mat.inverse() * + metric_landmarks.colwise().homogeneous()) + .topRows(3); + + ConvertEigenMatrixToLandmarkList(metric_landmarks, metric_landmark_list); + + return absl::OkStatus(); + } + + private: + void ProjectXY(const PerspectiveCameraFrustum& pcf, + Eigen::Matrix3Xf& landmarks) const { + float x_scale = pcf.right - pcf.left; + float y_scale = pcf.top - pcf.bottom; + float x_translation = pcf.left; + float y_translation = pcf.bottom; + + if (origin_point_location_ == proto::OriginPointLocation::TOP_LEFT_CORNER) { + landmarks.row(1) = 1.f - landmarks.row(1).array(); + } + + landmarks = + landmarks.array().colwise() * Eigen::Array3f(x_scale, y_scale, x_scale); + landmarks.colwise() += Eigen::Vector3f(x_translation, y_translation, 0.f); + } + + absl::StatusOr EstimateScale(Eigen::Matrix3Xf& landmarks) const { + Eigen::Matrix4f transform_mat; + MP_RETURN_IF_ERROR(procrustes_solver_->SolveWeightedOrthogonalProblem( + canonical_metric_landmarks_, landmarks, landmark_weights_, + transform_mat)) + << "Failed to estimate canonical-to-runtime landmark set transform!"; + + return transform_mat.col(0).norm(); + } + + static void MoveAndRescaleZ(const PerspectiveCameraFrustum& pcf, + float depth_offset, float scale, + Eigen::Matrix3Xf& landmarks) { + landmarks.row(2) = + (landmarks.array().row(2) - depth_offset + pcf.near) / scale; + } + + static void UnprojectXY(const PerspectiveCameraFrustum& pcf, + Eigen::Matrix3Xf& landmarks) { + landmarks.row(0) = + landmarks.row(0).cwiseProduct(landmarks.row(2)) / pcf.near; + landmarks.row(1) = + landmarks.row(1).cwiseProduct(landmarks.row(2)) / pcf.near; + } + + static void ChangeHandedness(Eigen::Matrix3Xf& landmarks) { + landmarks.row(2) *= -1.f; + } + + static void ConvertLandmarkListToEigenMatrix( + const mediapipe::NormalizedLandmarkList& landmark_list, + Eigen::Matrix3Xf& eigen_matrix) { + eigen_matrix = Eigen::Matrix3Xf(3, landmark_list.landmark_size()); + for (int i = 0; i < landmark_list.landmark_size(); ++i) { + const auto& landmark = landmark_list.landmark(i); + eigen_matrix(0, i) = landmark.x(); + eigen_matrix(1, i) = landmark.y(); + eigen_matrix(2, i) = landmark.z(); + } + } + + static void ConvertEigenMatrixToLandmarkList( + const Eigen::Matrix3Xf& eigen_matrix, + mediapipe::LandmarkList& landmark_list) { + landmark_list.Clear(); + + for (int i = 0; i < eigen_matrix.cols(); ++i) { + auto& landmark = *landmark_list.add_landmark(); + landmark.set_x(eigen_matrix(0, i)); + landmark.set_y(eigen_matrix(1, i)); + landmark.set_z(eigen_matrix(2, i)); + } + } + + const proto::OriginPointLocation origin_point_location_; + const proto::InputSource input_source_; + Eigen::Matrix3Xf canonical_metric_landmarks_; + Eigen::VectorXf landmark_weights_; + + std::unique_ptr procrustes_solver_; +}; + +class GeometryPipelineImpl : public GeometryPipeline { + public: + GeometryPipelineImpl( + const proto::PerspectiveCamera& perspective_camera, // + const proto::Mesh3d& canonical_mesh, // + uint32_t canonical_mesh_vertex_size, // + uint32_t canonical_mesh_num_vertices, + uint32_t canonical_mesh_vertex_position_offset, + std::unique_ptr space_converter) + : perspective_camera_(perspective_camera), + canonical_mesh_(canonical_mesh), + canonical_mesh_vertex_size_(canonical_mesh_vertex_size), + canonical_mesh_num_vertices_(canonical_mesh_num_vertices), + canonical_mesh_vertex_position_offset_( + canonical_mesh_vertex_position_offset), + space_converter_(std::move(space_converter)) {} + + absl::StatusOr> EstimateFaceGeometry( + const std::vector& + multi_face_landmarks, + int frame_width, int frame_height) const override { + MP_RETURN_IF_ERROR(ValidateFrameDimensions(frame_width, frame_height)) + << "Invalid frame dimensions!"; + + // Create a perspective camera frustum to be shared for geometry estimation + // per each face. + PerspectiveCameraFrustum pcf(perspective_camera_, frame_width, + frame_height); + + std::vector multi_face_geometry; + + // From this point, the meaning of "face landmarks" is clarified further as + // "screen face landmarks". This is done do distinguish from "metric face + // landmarks" that are derived during the face geometry estimation process. + for (const mediapipe::NormalizedLandmarkList& screen_face_landmarks : + multi_face_landmarks) { + // Having a too compact screen landmark list will result in numerical + // instabilities, therefore such faces are filtered. + if (IsScreenLandmarkListTooCompact(screen_face_landmarks)) { + continue; + } + + // Convert the screen landmarks into the metric landmarks and get the pose + // transformation matrix. + mediapipe::LandmarkList metric_face_landmarks; + Eigen::Matrix4f pose_transform_mat; + MP_RETURN_IF_ERROR(space_converter_->Convert(screen_face_landmarks, pcf, + metric_face_landmarks, + pose_transform_mat)) + << "Failed to convert landmarks from the screen to the metric space!"; + + // Pack geometry data for this face. + proto::FaceGeometry face_geometry; + proto::Mesh3d* mutable_mesh = face_geometry.mutable_mesh(); + // Copy the canonical face mesh as the face geometry mesh. + mutable_mesh->CopyFrom(canonical_mesh_); + // Replace XYZ vertex mesh coodinates with the metric landmark positions. + for (int i = 0; i < canonical_mesh_num_vertices_; ++i) { + uint32_t vertex_buffer_offset = canonical_mesh_vertex_size_ * i + + canonical_mesh_vertex_position_offset_; + + mutable_mesh->set_vertex_buffer(vertex_buffer_offset, + metric_face_landmarks.landmark(i).x()); + mutable_mesh->set_vertex_buffer(vertex_buffer_offset + 1, + metric_face_landmarks.landmark(i).y()); + mutable_mesh->set_vertex_buffer(vertex_buffer_offset + 2, + metric_face_landmarks.landmark(i).z()); + } + // Populate the face pose transformation matrix. + mediapipe::MatrixDataProtoFromMatrix( + pose_transform_mat, face_geometry.mutable_pose_transform_matrix()); + + multi_face_geometry.push_back(face_geometry); + } + + return multi_face_geometry; + } + + private: + static bool IsScreenLandmarkListTooCompact( + const mediapipe::NormalizedLandmarkList& screen_landmarks) { + float mean_x = 0.f; + float mean_y = 0.f; + for (int i = 0; i < screen_landmarks.landmark_size(); ++i) { + const auto& landmark = screen_landmarks.landmark(i); + mean_x += (landmark.x() - mean_x) / static_cast(i + 1); + mean_y += (landmark.y() - mean_y) / static_cast(i + 1); + } + + float max_sq_dist = 0.f; + for (const auto& landmark : screen_landmarks.landmark()) { + const float d_x = landmark.x() - mean_x; + const float d_y = landmark.y() - mean_y; + max_sq_dist = std::max(max_sq_dist, d_x * d_x + d_y * d_y); + } + + static constexpr float kIsScreenLandmarkListTooCompactThreshold = 1e-3f; + return std::sqrt(max_sq_dist) <= kIsScreenLandmarkListTooCompactThreshold; + } + + const proto::PerspectiveCamera perspective_camera_; + const proto::Mesh3d canonical_mesh_; + const uint32_t canonical_mesh_vertex_size_; + const uint32_t canonical_mesh_num_vertices_; + const uint32_t canonical_mesh_vertex_position_offset_; + + std::unique_ptr space_converter_; +}; + +} // namespace + +absl::StatusOr> CreateGeometryPipeline( + const proto::Environment& environment, + const proto::GeometryPipelineMetadata& metadata) { + MP_RETURN_IF_ERROR(ValidateEnvironment(environment)) + << "Invalid environment!"; + MP_RETURN_IF_ERROR(ValidateGeometryPipelineMetadata(metadata)) + << "Invalid geometry pipeline metadata!"; + + const auto& canonical_mesh = metadata.canonical_mesh(); + RET_CHECK(HasVertexComponent(canonical_mesh.vertex_type(), + VertexComponent::POSITION)) + << "Canonical face mesh must have the `POSITION` vertex component!"; + RET_CHECK(HasVertexComponent(canonical_mesh.vertex_type(), + VertexComponent::TEX_COORD)) + << "Canonical face mesh must have the `TEX_COORD` vertex component!"; + + uint32_t canonical_mesh_vertex_size = + GetVertexSize(canonical_mesh.vertex_type()); + uint32_t canonical_mesh_num_vertices = + canonical_mesh.vertex_buffer_size() / canonical_mesh_vertex_size; + uint32_t canonical_mesh_vertex_position_offset = + GetVertexComponentOffset(canonical_mesh.vertex_type(), + VertexComponent::POSITION) + .value(); + + // Put the Procrustes landmark basis into Eigen matrices for an easier access. + Eigen::Matrix3Xf canonical_metric_landmarks = + Eigen::Matrix3Xf::Zero(3, canonical_mesh_num_vertices); + Eigen::VectorXf landmark_weights = + Eigen::VectorXf::Zero(canonical_mesh_num_vertices); + + for (int i = 0; i < canonical_mesh_num_vertices; ++i) { + uint32_t vertex_buffer_offset = + canonical_mesh_vertex_size * i + canonical_mesh_vertex_position_offset; + + canonical_metric_landmarks(0, i) = + canonical_mesh.vertex_buffer(vertex_buffer_offset); + canonical_metric_landmarks(1, i) = + canonical_mesh.vertex_buffer(vertex_buffer_offset + 1); + canonical_metric_landmarks(2, i) = + canonical_mesh.vertex_buffer(vertex_buffer_offset + 2); + } + + for (const proto::WeightedLandmarkRef& wlr : + metadata.procrustes_landmark_basis()) { + uint32_t landmark_id = wlr.landmark_id(); + landmark_weights(landmark_id) = wlr.weight(); + } + + std::unique_ptr result = + absl::make_unique( + environment.perspective_camera(), canonical_mesh, + canonical_mesh_vertex_size, canonical_mesh_num_vertices, + canonical_mesh_vertex_position_offset, + absl::make_unique( + environment.origin_point_location(), + metadata.input_source() == proto::InputSource::DEFAULT + ? proto::InputSource::FACE_LANDMARK_PIPELINE + : metadata.input_source(), + std::move(canonical_metric_landmarks), + std::move(landmark_weights), + CreateFloatPrecisionProcrustesSolver())); + + return result; +} + +} // namespace mediapipe::tasks::vision::face_geometry diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/geometry_pipeline.h b/mediapipe/tasks/cc/vision/face_geometry/libs/geometry_pipeline.h new file mode 100644 index 00000000..85a047a5 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/geometry_pipeline.h @@ -0,0 +1,69 @@ +// Copyright 2023 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_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_GEOMETRY_PIPELINE_H_ +#define MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_GEOMETRY_PIPELINE_H_ + +#include +#include + +#include "mediapipe/framework/formats/landmark.pb.h" +#include "mediapipe/framework/port/statusor.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/environment.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/face_geometry.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.pb.h" + +namespace mediapipe::tasks::vision::face_geometry { + +// Encapsulates a stateless estimator of facial geometry in a Metric space based +// on the normalized face landmarks in the Screen space. +class GeometryPipeline { + public: + virtual ~GeometryPipeline() = default; + + // Estimates geometry data for multiple faces. + // + // Returns an error status if any of the passed arguments is invalid. + // + // The result includes face geometry data for a subset of the input faces, + // however geometry data for some faces might be missing. This may happen if + // it'd be unstable to estimate the facial geometry based on a corresponding + // face landmark list for any reason (for example, if the landmark list is too + // compact). + // + // Each face landmark list must have the same number of landmarks as was + // passed upon initialization via the canonical face mesh (as a part of the + // geometry pipeline metadata). + // + // Both `frame_width` and `frame_height` must be positive. + virtual absl::StatusOr> EstimateFaceGeometry( + const std::vector& + multi_face_landmarks, + int frame_width, int frame_height) const = 0; +}; + +// Creates an instance of `GeometryPipeline`. +// +// Both `environment` and `metadata` must be valid (for details, please refer to +// the proto message definition comments and/or `validation_utils.h/cc`). +// +// Canonical face mesh (defined as a part of `metadata`) must have the +// `POSITION` and the `TEX_COORD` vertex components. +absl::StatusOr> CreateGeometryPipeline( + const proto::Environment& environment, + const proto::GeometryPipelineMetadata& metadata); + +} // namespace mediapipe::tasks::vision::face_geometry + +#endif // MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_GEOMETRY_PIPELINE_H_ diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.cc b/mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.cc new file mode 100644 index 00000000..81a1402c --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.cc @@ -0,0 +1,103 @@ +// Copyright 2023 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/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.h" + +#include +#include + +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/statusor.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.pb.h" + +namespace mediapipe::tasks::vision::face_geometry { +namespace { + +bool HasVertexComponentVertexPT(VertexComponent vertex_component) { + switch (vertex_component) { + case VertexComponent::POSITION: + case VertexComponent::TEX_COORD: + return true; + + default: + return false; + } +} + +uint32_t GetVertexComponentSizeVertexPT(VertexComponent vertex_component) { + switch (vertex_component) { + case VertexComponent::POSITION: + return 3; + case VertexComponent::TEX_COORD: + return 2; + } +} + +uint32_t GetVertexComponentOffsetVertexPT(VertexComponent vertex_component) { + switch (vertex_component) { + case VertexComponent::POSITION: + return 0; + case VertexComponent::TEX_COORD: + return GetVertexComponentSizeVertexPT(VertexComponent::POSITION); + } +} + +} // namespace + +std::size_t GetVertexSize(proto::Mesh3d::VertexType vertex_type) { + switch (vertex_type) { + case proto::Mesh3d::VERTEX_PT: + return GetVertexComponentSizeVertexPT(VertexComponent::POSITION) + + GetVertexComponentSizeVertexPT(VertexComponent::TEX_COORD); + } +} + +std::size_t GetPrimitiveSize(proto::Mesh3d::PrimitiveType primitive_type) { + switch (primitive_type) { + case proto::Mesh3d::TRIANGLE: + return 3; + } +} + +bool HasVertexComponent(proto::Mesh3d::VertexType vertex_type, + VertexComponent vertex_component) { + switch (vertex_type) { + case proto::Mesh3d::VERTEX_PT: + return HasVertexComponentVertexPT(vertex_component); + } +} + +absl::StatusOr GetVertexComponentOffset( + proto::Mesh3d::VertexType vertex_type, VertexComponent vertex_component) { + RET_CHECK(HasVertexComponentVertexPT(vertex_component)) + << "A given vertex type doesn't have the requested component!"; + + switch (vertex_type) { + case proto::Mesh3d::VERTEX_PT: + return GetVertexComponentOffsetVertexPT(vertex_component); + } +} + +absl::StatusOr GetVertexComponentSize( + proto::Mesh3d::VertexType vertex_type, VertexComponent vertex_component) { + RET_CHECK(HasVertexComponentVertexPT(vertex_component)) + << "A given vertex type doesn't have the requested component!"; + + switch (vertex_type) { + case proto::Mesh3d::VERTEX_PT: + return GetVertexComponentSizeVertexPT(vertex_component); + } +} + +} // namespace mediapipe::tasks::vision::face_geometry diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.h b/mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.h new file mode 100644 index 00000000..934c0cb2 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.h @@ -0,0 +1,51 @@ +// Copyright 2023 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_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_MESH_3D_UTILS_H_ +#define MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_MESH_3D_UTILS_H_ + +#include +#include + +#include "mediapipe/framework/port/statusor.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.pb.h" + +namespace mediapipe::tasks::vision::face_geometry { + +enum class VertexComponent { POSITION, TEX_COORD }; + +std::size_t GetVertexSize(proto::Mesh3d::VertexType vertex_type); + +std::size_t GetPrimitiveSize(proto::Mesh3d::PrimitiveType primitive_type); + +bool HasVertexComponent(proto::Mesh3d::VertexType vertex_type, + VertexComponent vertex_component); + +// Computes the vertex component offset. +// +// Returns an error status if a given vertex type doesn't have the requested +// component. +absl::StatusOr GetVertexComponentOffset( + proto::Mesh3d::VertexType vertex_type, VertexComponent vertex_component); + +// Computes the vertex component size. +// +// Returns an error status if a given vertex type doesn't have the requested +// component. +absl::StatusOr GetVertexComponentSize( + proto::Mesh3d::VertexType vertex_type, VertexComponent vertex_component); + +} // namespace mediapipe::tasks::vision::face_geometry + +#endif // MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_MESH_3D_UTILS_H_ diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/procrustes_solver.cc b/mediapipe/tasks/cc/vision/face_geometry/libs/procrustes_solver.cc new file mode 100644 index 00000000..f2550749 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/procrustes_solver.cc @@ -0,0 +1,264 @@ +// Copyright 2023 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/tasks/cc/vision/face_geometry/libs/procrustes_solver.h" + +#include +#include + +#include "Eigen/Dense" +#include "absl/memory/memory.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/framework/port/status_macros.h" +#include "mediapipe/framework/port/statusor.h" + +namespace mediapipe::tasks::vision::face_geometry { +namespace { + +class FloatPrecisionProcrustesSolver : public ProcrustesSolver { + public: + FloatPrecisionProcrustesSolver() = default; + + absl::Status SolveWeightedOrthogonalProblem( + const Eigen::Matrix3Xf& source_points, // + const Eigen::Matrix3Xf& target_points, // + const Eigen::VectorXf& point_weights, + Eigen::Matrix4f& transform_mat) const override { + // Validate inputs. + MP_RETURN_IF_ERROR(ValidateInputPoints(source_points, target_points)) + << "Failed to validate weighted orthogonal problem input points!"; + MP_RETURN_IF_ERROR( + ValidatePointWeights(source_points.cols(), point_weights)) + << "Failed to validate weighted orthogonal problem point weights!"; + + // Extract square root from the point weights. + Eigen::VectorXf sqrt_weights = ExtractSquareRoot(point_weights); + + // Try to solve the WEOP problem. + MP_RETURN_IF_ERROR(InternalSolveWeightedOrthogonalProblem( + source_points, target_points, sqrt_weights, transform_mat)) + << "Failed to solve the WEOP problem!"; + + return absl::OkStatus(); + } + + private: + static constexpr float kAbsoluteErrorEps = 1e-9f; + + static absl::Status ValidateInputPoints( + const Eigen::Matrix3Xf& source_points, + const Eigen::Matrix3Xf& target_points) { + RET_CHECK_GT(source_points.cols(), 0) + << "The number of source points must be positive!"; + + RET_CHECK_EQ(source_points.cols(), target_points.cols()) + << "The number of source and target points must be equal!"; + + return absl::OkStatus(); + } + + static absl::Status ValidatePointWeights( + int num_points, const Eigen::VectorXf& point_weights) { + RET_CHECK_GT(point_weights.size(), 0) + << "The number of point weights must be positive!"; + + RET_CHECK_EQ(point_weights.size(), num_points) + << "The number of points and point weights must be equal!"; + + float total_weight = 0.f; + for (int i = 0; i < num_points; ++i) { + RET_CHECK_GE(point_weights(i), 0.f) + << "Each point weight must be non-negative!"; + + total_weight += point_weights(i); + } + + RET_CHECK_GT(total_weight, kAbsoluteErrorEps) + << "The total point weight is too small!"; + + return absl::OkStatus(); + } + + static Eigen::VectorXf ExtractSquareRoot( + const Eigen::VectorXf& point_weights) { + Eigen::VectorXf sqrt_weights(point_weights); + for (int i = 0; i < sqrt_weights.size(); ++i) { + sqrt_weights(i) = std::sqrt(sqrt_weights(i)); + } + + return sqrt_weights; + } + + // Combines a 3x3 rotation-and-scale matrix and a 3x1 translation vector into + // a single 4x4 transformation matrix. + static Eigen::Matrix4f CombineTransformMatrix(const Eigen::Matrix3f& r_and_s, + const Eigen::Vector3f& t) { + Eigen::Matrix4f result = Eigen::Matrix4f::Identity(); + result.leftCols(3).topRows(3) = r_and_s; + result.col(3).topRows(3) = t; + + return result; + } + + // The weighted problem is thoroughly addressed in Section 2.4 of: + // D. Akca, Generalized Procrustes analysis and its applications + // in photogrammetry, 2003, https://doi.org/10.3929/ethz-a-004656648 + // + // Notable differences in the code presented here are: + // + // * In the paper, the weights matrix W_p is Cholesky-decomposed as Q^T Q. + // Our W_p is diagonal (equal to diag(sqrt_weights^2)), + // so we can just set Q = diag(sqrt_weights) instead. + // + // * In the paper, the problem is presented as + // (for W_k = I and W_p = tranposed(Q) Q): + // || Q (c A T + j tranposed(t) - B) || -> min. + // + // We reformulate it as an equivalent minimization of the transpose's + // norm: + // || (c tranposed(T) tranposed(A) - tranposed(B)) tranposed(Q) || -> min, + // where tranposed(A) and tranposed(B) are the source and the target point + // clouds, respectively, c tranposed(T) is the rotation+scaling R sought + // for, and Q is diag(sqrt_weights). + // + // Most of the derivations are therefore transposed. + // + // Note: the output `transform_mat` argument is used instead of `StatusOr<>` + // return type in order to avoid Eigen memory alignment issues. Details: + // https://eigen.tuxfamily.org/dox/group__TopicStructHavingEigenMembers.html + static absl::Status InternalSolveWeightedOrthogonalProblem( + const Eigen::Matrix3Xf& sources, const Eigen::Matrix3Xf& targets, + const Eigen::VectorXf& sqrt_weights, Eigen::Matrix4f& transform_mat) { + // tranposed(A_w). + Eigen::Matrix3Xf weighted_sources = + sources.array().rowwise() * sqrt_weights.array().transpose(); + // tranposed(B_w). + Eigen::Matrix3Xf weighted_targets = + targets.array().rowwise() * sqrt_weights.array().transpose(); + + // w = tranposed(j_w) j_w. + float total_weight = sqrt_weights.cwiseProduct(sqrt_weights).sum(); + + // Let C = (j_w tranposed(j_w)) / (tranposed(j_w) j_w). + // Note that C = tranposed(C), hence (I - C) = tranposed(I - C). + // + // tranposed(A_w) C = tranposed(A_w) j_w tranposed(j_w) / w = + // (tranposed(A_w) j_w) tranposed(j_w) / w = c_w tranposed(j_w), + // + // where c_w = tranposed(A_w) j_w / w is a k x 1 vector calculated here: + Eigen::Matrix3Xf twice_weighted_sources = + weighted_sources.array().rowwise() * sqrt_weights.array().transpose(); + Eigen::Vector3f source_center_of_mass = + twice_weighted_sources.rowwise().sum() / total_weight; + // tranposed((I - C) A_w) = tranposed(A_w) (I - C) = + // tranposed(A_w) - tranposed(A_w) C = tranposed(A_w) - c_w tranposed(j_w). + Eigen::Matrix3Xf centered_weighted_sources = + weighted_sources - source_center_of_mass * sqrt_weights.transpose(); + + Eigen::Matrix3f rotation; + MP_RETURN_IF_ERROR(ComputeOptimalRotation( + weighted_targets * centered_weighted_sources.transpose(), rotation)) + << "Failed to compute the optimal rotation!"; + ASSIGN_OR_RETURN( + float scale, + ComputeOptimalScale(centered_weighted_sources, weighted_sources, + weighted_targets, rotation), + _ << "Failed to compute the optimal scale!"); + + // R = c tranposed(T). + Eigen::Matrix3f rotation_and_scale = scale * rotation; + + // Compute optimal translation for the weighted problem. + + // tranposed(B_w - c A_w T) = tranposed(B_w) - R tranposed(A_w) in (54). + const auto pointwise_diffs = + weighted_targets - rotation_and_scale * weighted_sources; + // Multiplication by j_w is a respectively weighted column sum. + // (54) from the paper. + const auto weighted_pointwise_diffs = + pointwise_diffs.array().rowwise() * sqrt_weights.array().transpose(); + Eigen::Vector3f translation = + weighted_pointwise_diffs.rowwise().sum() / total_weight; + + transform_mat = CombineTransformMatrix(rotation_and_scale, translation); + + return absl::OkStatus(); + } + + // `design_matrix` is a transposed LHS of (51) in the paper. + // + // Note: the output `rotation` argument is used instead of `StatusOr<>` + // return type in order to avoid Eigen memory alignment issues. Details: + // https://eigen.tuxfamily.org/dox/group__TopicStructHavingEigenMembers.html + static absl::Status ComputeOptimalRotation( + const Eigen::Matrix3f& design_matrix, Eigen::Matrix3f& rotation) { + RET_CHECK_GT(design_matrix.norm(), kAbsoluteErrorEps) + << "Design matrix norm is too small!"; + + Eigen::JacobiSVD svd( + design_matrix, Eigen::ComputeFullU | Eigen::ComputeFullV); + + Eigen::Matrix3f postrotation = svd.matrixU(); + Eigen::Matrix3f prerotation = svd.matrixV().transpose(); + + // Disallow reflection by ensuring that det(`rotation`) = +1 (and not -1), + // see "4.6 Constrained orthogonal Procrustes problems" + // in the Gower & Dijksterhuis's book "Procrustes Analysis". + // We flip the sign of the least singular value along with a column in W. + // + // Note that now the sum of singular values doesn't work for scale + // estimation due to this sign flip. + if (postrotation.determinant() * prerotation.determinant() < + static_cast(0)) { + postrotation.col(2) *= static_cast(-1); + } + + // Transposed (52) from the paper. + rotation = postrotation * prerotation; + return absl::OkStatus(); + } + + static absl::StatusOr ComputeOptimalScale( + const Eigen::Matrix3Xf& centered_weighted_sources, + const Eigen::Matrix3Xf& weighted_sources, + const Eigen::Matrix3Xf& weighted_targets, + const Eigen::Matrix3f& rotation) { + // tranposed(T) tranposed(A_w) (I - C). + const auto rotated_centered_weighted_sources = + rotation * centered_weighted_sources; + // Use the identity trace(A B) = sum(A * B^T) + // to avoid building large intermediate matrices (* is Hadamard product). + // (53) from the paper. + float numerator = + rotated_centered_weighted_sources.cwiseProduct(weighted_targets).sum(); + float denominator = + centered_weighted_sources.cwiseProduct(weighted_sources).sum(); + + RET_CHECK_GT(denominator, kAbsoluteErrorEps) + << "Scale expression denominator is too small!"; + RET_CHECK_GT(numerator / denominator, kAbsoluteErrorEps) + << "Scale is too small!"; + + return numerator / denominator; + } +}; + +} // namespace + +std::unique_ptr CreateFloatPrecisionProcrustesSolver() { + return absl::make_unique(); +} + +} // namespace mediapipe::tasks::vision::face_geometry diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/procrustes_solver.h b/mediapipe/tasks/cc/vision/face_geometry/libs/procrustes_solver.h new file mode 100644 index 00000000..0ae417db --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/procrustes_solver.h @@ -0,0 +1,70 @@ +// Copyright 2023 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_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_PROCRUSTES_SOLVER_H_ +#define MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_PROCRUSTES_SOLVER_H_ + +#include + +#include "Eigen/Dense" +#include "mediapipe/framework/port/status.h" + +namespace mediapipe::tasks::vision::face_geometry { + +// Encapsulates a stateless solver for the Weighted Extended Orthogonal +// Procrustes (WEOP) Problem, as defined in Section 2.4 of +// https://doi.org/10.3929/ethz-a-004656648. +// +// Given the source and the target point clouds, the algorithm estimates +// a 4x4 transformation matrix featuring the following semantic components: +// +// * Uniform scale +// * Rotation +// * Translation +// +// The matrix maps the source point cloud into the target point cloud minimizing +// the Mean Squared Error. +class ProcrustesSolver { + public: + virtual ~ProcrustesSolver() = default; + + // Solves the Weighted Extended Orthogonal Procrustes (WEOP) Problem. + // + // All `source_points`, `target_points` and `point_weights` must define the + // same number of points. Elements of `point_weights` must be non-negative. + // + // A too small diameter of either of the point clouds will likely lead to + // numerical instabilities and failure to estimate the transformation. + // + // A too small point cloud total weight will likely lead to numerical + // instabilities and failure to estimate the transformation too. + // + // Small point coordinate deviation for either of the point cloud will likely + // result in a failure as it will make the solution very unstable if possible. + // + // Note: the output `transform_mat` argument is used instead of `StatusOr<>` + // return type in order to avoid Eigen memory alignment issues. Details: + // https://eigen.tuxfamily.org/dox/group__TopicStructHavingEigenMembers.html + virtual absl::Status SolveWeightedOrthogonalProblem( + const Eigen::Matrix3Xf& source_points, // + const Eigen::Matrix3Xf& target_points, // + const Eigen::VectorXf& point_weights, // + Eigen::Matrix4f& transform_mat) const = 0; +}; + +std::unique_ptr CreateFloatPrecisionProcrustesSolver(); + +} // namespace mediapipe::tasks::vision::face_geometry + +#endif // MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_PROCRUSTES_SOLVER_H_ diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.cc b/mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.cc new file mode 100644 index 00000000..879ae3fa --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.cc @@ -0,0 +1,127 @@ +// Copyright 2023 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/tasks/cc/vision/face_geometry/libs/validation_utils.h" + +#include +#include + +#include "mediapipe/framework/formats/matrix_data.pb.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/framework/port/status_macros.h" +#include "mediapipe/tasks/cc/vision/face_geometry/libs/mesh_3d_utils.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/environment.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.pb.h" + +namespace mediapipe::tasks::vision::face_geometry { + +absl::Status ValidatePerspectiveCamera( + const proto::PerspectiveCamera& perspective_camera) { + static constexpr float kAbsoluteErrorEps = 1e-9f; + + RET_CHECK_GT(perspective_camera.near(), kAbsoluteErrorEps) + << "Near Z must be greater than 0 with a margin of 10^{-9}!"; + + RET_CHECK_GT(perspective_camera.far(), + perspective_camera.near() + kAbsoluteErrorEps) + << "Far Z must be greater than Near Z with a margin of 10^{-9}!"; + + RET_CHECK_GT(perspective_camera.vertical_fov_degrees(), kAbsoluteErrorEps) + << "Vertical FOV must be positive with a margin of 10^{-9}!"; + + RET_CHECK_LT(perspective_camera.vertical_fov_degrees() + kAbsoluteErrorEps, + 180.f) + << "Vertical FOV must be less than 180 degrees with a margin of 10^{-9}"; + + return absl::OkStatus(); +} + +absl::Status ValidateEnvironment(const proto::Environment& environment) { + MP_RETURN_IF_ERROR( + ValidatePerspectiveCamera(environment.perspective_camera())) + << "Invalid perspective camera!"; + + return absl::OkStatus(); +} + +absl::Status ValidateMesh3d(const proto::Mesh3d& mesh_3d) { + const std::size_t vertex_size = GetVertexSize(mesh_3d.vertex_type()); + const std::size_t primitive_type = GetPrimitiveSize(mesh_3d.primitive_type()); + + RET_CHECK_EQ(mesh_3d.vertex_buffer_size() % vertex_size, 0) + << "Vertex buffer size must a multiple of the vertex size!"; + + RET_CHECK_EQ(mesh_3d.index_buffer_size() % primitive_type, 0) + << "Index buffer size must a multiple of the primitive size!"; + + const int num_vertices = mesh_3d.vertex_buffer_size() / vertex_size; + for (uint32_t idx : mesh_3d.index_buffer()) { + RET_CHECK_LT(idx, num_vertices) + << "All mesh indices must refer to an existing vertex!"; + } + + return absl::OkStatus(); +} + +absl::Status ValidateFaceGeometry(const proto::FaceGeometry& face_geometry) { + MP_RETURN_IF_ERROR(ValidateMesh3d(face_geometry.mesh())) << "Invalid mesh!"; + + static constexpr char kInvalid4x4MatrixMessage[] = + "Pose transformation matrix must be a 4x4 matrix!"; + + const mediapipe::MatrixData& pose_transform_matrix = + face_geometry.pose_transform_matrix(); + RET_CHECK_EQ(pose_transform_matrix.rows(), 4) << kInvalid4x4MatrixMessage; + RET_CHECK_EQ(pose_transform_matrix.rows(), 4) << kInvalid4x4MatrixMessage; + RET_CHECK_EQ(pose_transform_matrix.packed_data_size(), 16) + << kInvalid4x4MatrixMessage; + + return absl::OkStatus(); +} + +absl::Status ValidateGeometryPipelineMetadata( + const proto::GeometryPipelineMetadata& metadata) { + MP_RETURN_IF_ERROR(ValidateMesh3d(metadata.canonical_mesh())) + << "Invalid canonical mesh!"; + + RET_CHECK_GT(metadata.procrustes_landmark_basis_size(), 0) + + << "Procrustes landmark basis must be non-empty!"; + + const int num_vertices = + metadata.canonical_mesh().vertex_buffer_size() / + GetVertexSize(metadata.canonical_mesh().vertex_type()); + for (const proto::WeightedLandmarkRef& wlr : + metadata.procrustes_landmark_basis()) { + RET_CHECK_LT(wlr.landmark_id(), num_vertices) + << "All Procrustes basis indices must refer to an existing canonical " + "mesh vertex!"; + + RET_CHECK_GE(wlr.weight(), 0.f) + << "All Procrustes basis landmarks must have a non-negative weight!"; + } + + return absl::OkStatus(); +} + +absl::Status ValidateFrameDimensions(int frame_width, int frame_height) { + RET_CHECK_GT(frame_width, 0) << "Frame width must be positive!"; + RET_CHECK_GT(frame_height, 0) << "Frame height must be positive!"; + + return absl::OkStatus(); +} + +} // namespace mediapipe::tasks::vision::face_geometry diff --git a/mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.h b/mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.h new file mode 100644 index 00000000..fc7fbed9 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/libs/validation_utils.h @@ -0,0 +1,70 @@ +// Copyright 2023 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_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_VALIDATION_UTILS_H_ +#define MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_VALIDATION_UTILS_H_ + +#include "mediapipe/framework/port/status.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/environment.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/face_geometry.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.pb.h" +#include "mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.pb.h" + +namespace mediapipe::tasks::vision::face_geometry { + +// Validates `perspective_camera`. +// +// Near Z must be greater than 0 with a margin of `1e-9`. +// Far Z must be greater than Near Z with a margin of `1e-9`. +// Vertical FOV must be in range (0, 180) with a margin of `1e-9` on the range +// edges. +absl::Status ValidatePerspectiveCamera( + const proto::PerspectiveCamera& perspective_camera); + +// Validates `environment`. +// +// Environment's perspective camera must be valid. +absl::Status ValidateEnvironment(const proto::Environment& environment); + +// Validates `mesh_3d`. +// +// Mesh vertex buffer size must a multiple of the vertex size. +// Mesh index buffer size must a multiple of the primitive size. +// All mesh indices must reference an existing mesh vertex. +absl::Status ValidateMesh3d(const proto::Mesh3d& mesh_3d); + +// Validates `face_geometry`. +// +// Face mesh must be valid. +// Face pose transformation matrix must be a 4x4 matrix. +absl::Status ValidateFaceGeometry(const proto::FaceGeometry& face_geometry); + +// Validates `metadata`. +// +// Canonical face mesh must be valid. +// Procrustes landmark basis must be non-empty. +// All Procrustes basis indices must reference an existing canonical mesh +// vertex. +// All Procrustes basis landmarks must have a non-negative weight. +absl::Status ValidateGeometryPipelineMetadata( + const proto::GeometryPipelineMetadata& metadata); + +// Validates frame dimensions. +// +// Both frame width and frame height must be positive. +absl::Status ValidateFrameDimensions(int frame_width, int frame_height); + +} // namespace mediapipe::tasks::vision::face_geometry + +#endif // MEDIAPIPE_TASKS_CC_VISION_FACE_GEOMETRY_LIBS_VALIDATION_UTILS_H_ diff --git a/mediapipe/tasks/cc/vision/face_geometry/proto/BUILD b/mediapipe/tasks/cc/vision/face_geometry/proto/BUILD new file mode 100644 index 00000000..9559448f --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/proto/BUILD @@ -0,0 +1,46 @@ +# Copyright 2023 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. + +load("//mediapipe/framework/port:build_config.bzl", "mediapipe_proto_library") + +licenses(["notice"]) + +package(default_visibility = ["//visibility:public"]) + +mediapipe_proto_library( + name = "environment_proto", + srcs = ["environment.proto"], +) + +mediapipe_proto_library( + name = "face_geometry_proto", + srcs = ["face_geometry.proto"], + deps = [ + ":mesh_3d_proto", + "//mediapipe/framework/formats:matrix_data_proto", + ], +) + +mediapipe_proto_library( + name = "geometry_pipeline_metadata_proto", + srcs = ["geometry_pipeline_metadata.proto"], + deps = [ + ":mesh_3d_proto", + ], +) + +mediapipe_proto_library( + name = "mesh_3d_proto", + srcs = ["mesh_3d.proto"], +) diff --git a/mediapipe/tasks/cc/vision/face_geometry/proto/environment.proto b/mediapipe/tasks/cc/vision/face_geometry/proto/environment.proto new file mode 100644 index 00000000..e60f3c1e --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/proto/environment.proto @@ -0,0 +1,84 @@ +// Copyright 2023 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.tasks.vision.face_geometry.proto; + +option java_package = "mediapipe.tasks.vision.facegeometry.proto"; +option java_outer_classname = "EnvironmentProto"; + +// Defines the (0, 0) origin point location of the environment. +// +// The variation in the origin point location can be traced back to the memory +// layout of the camera video frame buffers. +// +// Usually, the memory layout for most CPU (and also some GPU) camera video +// frame buffers results in having the (0, 0) origin point located in the +// Top Left corner. +// +// On the contrary, the memory layout for most GPU camera video frame buffers +// results in having the (0, 0) origin point located in the Bottom Left corner. +// +// Let's consider the following example: +// +// (A) ---------------+ +// ___ | +// | (1) | | | +// | / \ | | | +// | |---|===|-| | +// | |---| | | | +// | / \ | | | +// | | | | | | +// | | (2) |=| | | +// | | | | | | +// | |_______| |_| | +// | |@| |@| | | | +// | ___________|_|_ | +// | +// (B) ---------------+ +// +// On this example, (1) and (2) have the same X coordinate regardless of the +// origin point location. However, having the origin point located at (A) +// (Top Left corner) results in (1) having a smaller Y coordinate if compared to +// (2). Similarly, having the origin point located at (B) (Bottom Left corner) +// results in (1) having a greater Y coordinate if compared to (2). +// +// Providing the correct origin point location for your environment and making +// sure all the input landmarks are in-sync with this location is crucial +// for receiving the correct output face geometry and visual renders. +enum OriginPointLocation { + BOTTOM_LEFT_CORNER = 1; + TOP_LEFT_CORNER = 2; +} + +// The perspective camera is defined through its vertical FOV angle and the +// Z-clipping planes. The aspect ratio is a runtime variable for the face +// geometry module and should be provided alongside the face landmarks in order +// to estimate the face geometry on a given frame. +// +// More info on Perspective Cameras: +// http://www.songho.ca/opengl/gl_projectionmatrix.html#perspective +message PerspectiveCamera { + // `0 < vertical_fov_degrees < 180`. + optional float vertical_fov_degrees = 1; + // `0 < near < far`. + optional float near = 2; + optional float far = 3; +} + +message Environment { + optional OriginPointLocation origin_point_location = 1; + optional PerspectiveCamera perspective_camera = 2; +} diff --git a/mediapipe/tasks/cc/vision/face_geometry/proto/face_geometry.proto b/mediapipe/tasks/cc/vision/face_geometry/proto/face_geometry.proto new file mode 100644 index 00000000..1934828c --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/proto/face_geometry.proto @@ -0,0 +1,60 @@ +// Copyright 2023 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.tasks.vision.face_geometry.proto; + +import "mediapipe/framework/formats/matrix_data.proto"; +import "mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.proto"; + +option java_package = "mediapipe.tasks.vision.facegeometry.proto"; +option java_outer_classname = "FaceGeometryProto"; + +// Defines the face geometry pipeline estimation result format. +message FaceGeometry { + // Defines a mesh surface for a face. The face mesh vertex IDs are the same as + // the face landmark IDs. + // + // XYZ coordinates exist in the right-handed Metric 3D space configured by an + // environment. UV coodinates are taken from the canonical face mesh model. + // + // XY coordinates are guaranteed to match the screen positions of + // the input face landmarks after (1) being multiplied by the face pose + // transformation matrix and then (2) being projected with a perspective + // camera matrix of the same environment. + // + // NOTE: the triangular topology of the face mesh is only useful when derived + // from the 468 face landmarks, not from the 6 face detection landmarks + // (keypoints). The former don't cover the entire face and this mesh is + // defined here only to comply with the API. It should be considered as + // a placeholder and/or for debugging purposes. + // + // Use the face geometry derived from the face detection landmarks + // (keypoints) for the face pose transformation matrix, not the mesh. + optional Mesh3d mesh = 1; + + // Defines a face pose transformation matrix, which provides mapping from + // the static canonical face model to the runtime face. Tries to distinguish + // a head pose change from a facial expression change and to only reflect the + // former. + // + // Is a 4x4 matrix and contains only the following components: + // * Uniform scale + // * Rotation + // * Translation + // + // The last row is guaranteed to be `[0 0 0 1]`. + optional mediapipe.MatrixData pose_transform_matrix = 2; +} diff --git a/mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.proto b/mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.proto new file mode 100644 index 00000000..54fcaf23 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/proto/geometry_pipeline_metadata.proto @@ -0,0 +1,63 @@ +// Copyright 2023 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.tasks.vision.face_geometry.proto; + +import "mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.proto"; + +option java_package = "mediapipe.tasks.vision.facegeometry.proto"; +option java_outer_classname = "GeometryPipelineMetadataProto"; + +enum InputSource { + DEFAULT = 0; // FACE_LANDMARK_PIPELINE + FACE_LANDMARK_PIPELINE = 1; + FACE_DETECTION_PIPELINE = 2; +} + +message WeightedLandmarkRef { + // Defines the landmark ID. References an existing face landmark ID. + optional uint32 landmark_id = 1; + // Defines the landmark weight. The larger the weight the more influence this + // landmark has in the basis. + // + // Is positive. + optional float weight = 2; +} + +// Next field ID: 4 +message GeometryPipelineMetadata { + // Defines the source of the input landmarks to let the underlying geometry + // pipeline to adjust in order to produce the best results. + // + // Face landmark pipeline is expected to produce 3D landmarks with relative Z + // coordinate, which is scaled as the X coordinate assuming the weak + // perspective projection camera model. + // + // Face landmark pipeline is expected to produce 2D landmarks with Z + // coordinate being equal to 0. + optional InputSource input_source = 3; + // Defines a mesh surface for a canonical face. The canonical face mesh vertex + // IDs are the same as the face landmark IDs. + // + // XYZ coordinates are defined in centimeter units. + optional Mesh3d canonical_mesh = 1; + // Defines a weighted landmark basis for running the Procrustes solver + // algorithm inside the geometry pipeline. + // + // A good basis sets face landmark weights in way to distinguish a head pose + // change from a facial expression change and to only respond to the former. + repeated WeightedLandmarkRef procrustes_landmark_basis = 2; +} diff --git a/mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.proto b/mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.proto new file mode 100644 index 00000000..45913cf0 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_geometry/proto/mesh_3d.proto @@ -0,0 +1,41 @@ +// Copyright 2023 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.tasks.vision.face_geometry.proto; + +option java_package = "mediapipe.tasks.vision.facegeometry.proto"; +option java_outer_classname = "Mesh3dProto"; + +message Mesh3d { + enum VertexType { + // Is defined by 5 coordinates: Position (XYZ) + Texture coordinate (UV). + VERTEX_PT = 0; + } + + enum PrimitiveType { + // Is defined by 3 indices: triangle vertex IDs. + TRIANGLE = 0; + } + + optional VertexType vertex_type = 1; + optional PrimitiveType primitive_type = 2; + // Vertex buffer size is a multiple of the vertex size (e.g., 5 for + // VERTEX_PT). + repeated float vertex_buffer = 3; + // Index buffer size is a multiple of the primitive size (e.g., 3 for + // TRIANGLE). + repeated uint32 index_buffer = 4; +} diff --git a/mediapipe/tasks/cc/vision/face_stylizer/calculators/BUILD b/mediapipe/tasks/cc/vision/face_stylizer/calculators/BUILD new file mode 100644 index 00000000..be1ce9b3 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_stylizer/calculators/BUILD @@ -0,0 +1,108 @@ +# Copyright 2023 The MediaPipe Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("//mediapipe/framework/port:build_config.bzl", "mediapipe_proto_library") + +licenses(["notice"]) + +package(default_visibility = ["//mediapipe/tasks:internal"]) + +mediapipe_proto_library( + name = "tensors_to_image_calculator_proto", + srcs = ["tensors_to_image_calculator.proto"], + deps = [ + "//mediapipe/framework:calculator_options_proto", + "//mediapipe/framework:calculator_proto", + "//mediapipe/gpu:gpu_origin_proto", + ], +) + +cc_library( + name = "tensors_to_image_calculator", + srcs = ["tensors_to_image_calculator.cc"], + copts = select({ + "//mediapipe:apple": [ + "-x objective-c++", + "-fobjc-arc", # enable reference-counting + ], + "//conditions:default": [], + }), + features = ["-layering_check"], # allow depending on tensor_to_image_calculator_gpu_deps + linkopts = select({ + "//mediapipe:apple": [ + "-framework CoreVideo", + "-framework MetalKit", + ], + "//conditions:default": [], + }), + deps = [ + ":tensors_to_image_calculator_cc_proto", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework:calculator_options_cc_proto", + "//mediapipe/framework/api2:builder", + "//mediapipe/framework/api2:node", + "//mediapipe/framework/api2:packet", + "//mediapipe/framework/api2:port", + "//mediapipe/framework/formats:image", + "//mediapipe/framework/formats:tensor", + "//mediapipe/framework/port:logging", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/framework/port:vector", + "//mediapipe/gpu:gpu_origin_cc_proto", + ] + select({ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": ["tensor_to_image_calculator_gpu_deps"], + }), + alwayslink = 1, +) + +cc_library( + name = "tensor_to_image_calculator_gpu_deps", + visibility = ["//visibility:private"], + deps = select({ + "//mediapipe:android": [ + "//mediapipe/gpu:gl_calculator_helper", + "//mediapipe/gpu:gl_quad_renderer", + "//mediapipe/gpu:gl_simple_shaders", + "//mediapipe/gpu:gpu_buffer", + "@org_tensorflow//tensorflow/lite/delegates/gpu:gl_delegate", + "@org_tensorflow//tensorflow/lite/delegates/gpu/common:util", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_program", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_shader", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_texture", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl/converters:util", + ], + "//mediapipe:ios": [ + "//mediapipe/gpu:MPPMetalHelper", + "//mediapipe/gpu:MPPMetalUtil", + "//mediapipe/gpu:gl_calculator_helper", + "//mediapipe/gpu:gpu_buffer", + ], + "//mediapipe:macos": [], + "//conditions:default": [ + "//mediapipe/gpu:gl_calculator_helper", + "//mediapipe/gpu:gl_quad_renderer", + "//mediapipe/gpu:gpu_buffer", + "@org_tensorflow//tensorflow/lite/delegates/gpu:gl_delegate", + "@org_tensorflow//tensorflow/lite/delegates/gpu/common:util", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_program", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_shader", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_texture", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl/converters:util", + ], + }), +) diff --git a/mediapipe/tasks/cc/vision/face_stylizer/calculators/tensors_to_image_calculator.cc b/mediapipe/tasks/cc/vision/face_stylizer/calculators/tensors_to_image_calculator.cc new file mode 100644 index 00000000..ac4f1743 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_stylizer/calculators/tensors_to_image_calculator.cc @@ -0,0 +1,439 @@ +// Copyright 2023 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 +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "mediapipe/framework/api2/node.h" +#include "mediapipe/framework/api2/packet.h" +#include "mediapipe/framework/api2/port.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/calculator_options.pb.h" +#include "mediapipe/framework/formats/image.h" +#include "mediapipe/framework/formats/tensor.h" +#include "mediapipe/framework/port/logging.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/gpu/gpu_origin.pb.h" +#include "mediapipe/tasks/cc/vision/face_stylizer/calculators/tensors_to_image_calculator.pb.h" + +#if !MEDIAPIPE_DISABLE_GPU +#include "mediapipe/gpu/gpu_buffer.h" +#if MEDIAPIPE_METAL_ENABLED +#import +#import +#import + +#include "mediapipe/framework/formats/tensor_mtl_buffer_view.h" +#import "mediapipe/gpu/MPPMetalHelper.h" +#else +#include "mediapipe/gpu/gl_calculator_helper.h" +#include "mediapipe/gpu/gl_quad_renderer.h" +#include "mediapipe/gpu/gl_simple_shaders.h" +#if MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 +#include "tensorflow/lite/delegates/gpu/common/util.h" +#include "tensorflow/lite/delegates/gpu/gl/converters/util.h" +#include "tensorflow/lite/delegates/gpu/gl/gl_program.h" +#include "tensorflow/lite/delegates/gpu/gl/gl_shader.h" +#include "tensorflow/lite/delegates/gpu/gl/gl_texture.h" +#include "tensorflow/lite/delegates/gpu/gl_delegate.h" +#endif // MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 +#endif // MEDIAPIPE_METAL_ENABLED +#endif // !MEDIAPIPE_DISABLE_GPU + +namespace mediapipe { +namespace tasks { +namespace { + +using ::mediapipe::api2::Input; +using ::mediapipe::api2::Node; +using ::mediapipe::api2::Output; + +#if MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 +using ::tflite::gpu::gl::GlProgram; +using ::tflite::gpu::gl::GlShader; +#endif // MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + +enum { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, NUM_ATTRIBUTES }; + +// Commonly used to compute the number of blocks to launch in a kernel. +static int NumGroups(const int size, const int group_size) { // NOLINT + return (size + group_size - 1) / group_size; +} + +} // namespace + +// Converts a MediaPipe tensor to a MediaPipe Image. +// +// Input streams: +// TENSORS - std::vector that only contains one element. +// +// Output streams: +// OUTPUT - mediapipe::Image. +// +// TODO: Enable TensorsToImageCalculator to run on CPU. +class TensorsToImageCalculator : public Node { + public: + static constexpr Input> kInputTensors{"TENSORS"}; + static constexpr Output kOutputImage{"IMAGE"}; + + MEDIAPIPE_NODE_CONTRACT(kInputTensors, kOutputImage); + + static absl::Status UpdateContract(CalculatorContract* cc); + absl::Status Open(CalculatorContext* cc); + absl::Status Process(CalculatorContext* cc); + absl::Status Close(CalculatorContext* cc); + + private: +#if !MEDIAPIPE_DISABLE_GPU +#if MEDIAPIPE_METAL_ENABLED + bool metal_initialized_ = false; + MPPMetalHelper* gpu_helper_ = nullptr; + id to_buffer_program_; + + absl::Status MetalSetup(CalculatorContext* cc); + absl::Status MetalProcess(CalculatorContext* cc); +#else + absl::Status GlSetup(CalculatorContext* cc); + + GlCalculatorHelper gl_helper_; + + bool gl_initialized_ = false; +#if MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + std::unique_ptr gl_compute_program_; + const tflite::gpu::uint3 workgroup_size_ = {8, 8, 1}; +#else + GLuint program_ = 0; + std::unique_ptr gl_renderer_; +#endif // MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 +#endif // MEDIAPIPE_METAL_ENABLED +#endif // !MEDIAPIPE_DISABLE_GPU +}; +MEDIAPIPE_REGISTER_NODE(::mediapipe::tasks::TensorsToImageCalculator); + +absl::Status TensorsToImageCalculator::UpdateContract(CalculatorContract* cc) { +#if !MEDIAPIPE_DISABLE_GPU +#if MEDIAPIPE_METAL_ENABLED + MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); +#else + return GlCalculatorHelper::UpdateContract(cc); +#endif // MEDIAPIPE_METAL_ENABLED +#endif // !MEDIAPIPE_DISABLE_GPU + return absl::OkStatus(); +} + +absl::Status TensorsToImageCalculator::Open(CalculatorContext* cc) { +#if !MEDIAPIPE_DISABLE_GPU +#if MEDIAPIPE_METAL_ENABLED + gpu_helper_ = [[MPPMetalHelper alloc] initWithCalculatorContext:cc]; + RET_CHECK(gpu_helper_); +#else + MP_RETURN_IF_ERROR(gl_helper_.Open(cc)); +#endif // MEDIAPIPE_METAL_ENABLED +#endif // !MEDIAPIPE_DISABLE_GPU + + return absl::OkStatus(); +} + +absl::Status TensorsToImageCalculator::Process(CalculatorContext* cc) { +#if !MEDIAPIPE_DISABLE_GPU +#if MEDIAPIPE_METAL_ENABLED + + return MetalProcess(cc); + +#else + + return gl_helper_.RunInGlContext([this, cc]() -> absl::Status { + if (!gl_initialized_) { + MP_RETURN_IF_ERROR(GlSetup(cc)); + gl_initialized_ = true; + } + + if (kInputTensors(cc).IsEmpty()) { + return absl::OkStatus(); + } + const auto& input_tensors = kInputTensors(cc).Get(); + RET_CHECK_EQ(input_tensors.size(), 1) + << "Expect 1 input tensor, but have " << input_tensors.size(); + const int tensor_width = input_tensors[0].shape().dims[2]; + const int tensor_height = input_tensors[0].shape().dims[1]; + +#if MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + + auto out_texture = std::make_unique(); + MP_RETURN_IF_ERROR(CreateReadWriteRgbaImageTexture( + tflite::gpu::DataType::UINT8, // GL_RGBA8 + {tensor_width, tensor_height}, out_texture.get())); + + const int output_index = 0; + glBindImageTexture(output_index, out_texture->id(), 0, GL_FALSE, 0, + GL_WRITE_ONLY, GL_RGBA8); + + auto read_view = input_tensors[0].GetOpenGlBufferReadView(); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, read_view.name()); + + const tflite::gpu::uint3 workload = {tensor_width, tensor_height, 1}; + const tflite::gpu::uint3 workgroups = + tflite::gpu::DivideRoundUp(workload, workgroup_size_); + + glUseProgram(gl_compute_program_->id()); + glUniform2i(glGetUniformLocation(gl_compute_program_->id(), "out_size"), + tensor_width, tensor_height); + + MP_RETURN_IF_ERROR(gl_compute_program_->Dispatch(workgroups)); + + auto texture_buffer = mediapipe::GlTextureBuffer::Wrap( + out_texture->target(), out_texture->id(), tensor_width, tensor_height, + mediapipe::GpuBufferFormat::kBGRA32, + [ptr = out_texture.release()]( + std::shared_ptr sync_token) mutable { + delete ptr; + }); + + auto output = + std::make_unique(std::move(texture_buffer)); + kOutputImage(cc).Send(Image(*output)); + ; + +#else + + if (!input_tensors[0].ready_as_opengl_texture_2d()) { + (void)input_tensors[0].GetCpuReadView(); + } + + auto output_texture = + gl_helper_.CreateDestinationTexture(tensor_width, tensor_height); + gl_helper_.BindFramebuffer(output_texture); // GL_TEXTURE0 + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, + input_tensors[0].GetOpenGlTexture2dReadView().name()); + + MP_RETURN_IF_ERROR(gl_renderer_->GlRender( + tensor_width, tensor_height, output_texture.width(), + output_texture.height(), mediapipe::FrameScaleMode::kStretch, + mediapipe::FrameRotation::kNone, + /*flip_horizontal=*/false, /*flip_vertical=*/false, + /*flip_texture=*/false)); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, 0); + + auto output = output_texture.GetFrame(); + kOutputImage(cc).Send(Image(*output)); + +#endif // MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + + return mediapipe::OkStatus(); + }); + +#endif // MEDIAPIPE_METAL_ENABLED +#endif // !MEDIAPIPE_DISABLE_GPU + return absl::OkStatus(); +} + +absl::Status TensorsToImageCalculator::Close(CalculatorContext* cc) { +#if !MEDIAPIPE_DISABLE_GPU && !MEDIAPIPE_METAL_ENABLED + gl_helper_.RunInGlContext([this] { +#if MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + gl_compute_program_.reset(); +#else + if (program_) glDeleteProgram(program_); + program_ = 0; +#endif // MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + }); +#endif // !MEDIAPIPE_DISABLE_GPU && !MEDIAPIPE_METAL_ENABLED + return absl::OkStatus(); +} + +#if MEDIAPIPE_METAL_ENABLED + +absl::Status TensorsToImageCalculator::MetalProcess(CalculatorContext* cc) { + if (!metal_initialized_) { + MP_RETURN_IF_ERROR(MetalSetup(cc)); + metal_initialized_ = true; + } + + if (kInputTensors(cc).IsEmpty()) { + return absl::OkStatus(); + } + const auto& input_tensors = kInputTensors(cc).Get(); + RET_CHECK_EQ(input_tensors.size(), 1) + << "Expect 1 input tensor, but have " << input_tensors.size(); + const int tensor_width = input_tensors[0].shape().dims[2]; + const int tensor_height = input_tensors[0].shape().dims[1]; + + // TODO: Fix unused variable + [[maybe_unused]] id device = gpu_helper_.mtlDevice; + id command_buffer = [gpu_helper_ commandBuffer]; + command_buffer.label = @"TensorsToImageCalculatorConvert"; + id compute_encoder = + [command_buffer computeCommandEncoder]; + [compute_encoder setComputePipelineState:to_buffer_program_]; + + auto input_view = + mediapipe::MtlBufferView::GetReadView(input_tensors[0], command_buffer); + [compute_encoder setBuffer:input_view.buffer() offset:0 atIndex:0]; + + mediapipe::GpuBuffer output = + [gpu_helper_ mediapipeGpuBufferWithWidth:tensor_width + height:tensor_height]; + id dst_texture = [gpu_helper_ metalTextureWithGpuBuffer:output]; + [compute_encoder setTexture:dst_texture atIndex:1]; + + MTLSize threads_per_group = MTLSizeMake(8, 8, 1); + MTLSize threadgroups = + MTLSizeMake(NumGroups(tensor_width, 8), NumGroups(tensor_height, 8), 1); + [compute_encoder dispatchThreadgroups:threadgroups + threadsPerThreadgroup:threads_per_group]; + [compute_encoder endEncoding]; + [command_buffer commit]; + + kOutputImage(cc).Send(Image(output)); + return absl::OkStatus(); +} + +absl::Status TensorsToImageCalculator::MetalSetup(CalculatorContext* cc) { + id device = gpu_helper_.mtlDevice; + const std::string shader_source = + R"( + #include + + using namespace metal; + + kernel void convertKernel( + device float* in_buf [[ buffer(0) ]], + texture2d out_tex [[ texture(1) ]], + uint2 gid [[ thread_position_in_grid ]]) { + if (gid.x >= out_tex.get_width() || gid.y >= out_tex.get_height()) return; + uint linear_index = 3 * (gid.y * out_tex.get_width() + gid.x); + float4 out_value = float4(in_buf[linear_index], in_buf[linear_index + 1], in_buf[linear_index + 2], 1.0); + out_tex.write(out_value, gid); + } + )"; + NSString* library_source = + [NSString stringWithUTF8String:shader_source.c_str()]; + NSError* error = nil; + id library = + [device newLibraryWithSource:library_source options:nullptr error:&error]; + RET_CHECK(library != nil) << "Couldn't create shader library " + << [[error localizedDescription] UTF8String]; + id kernel_func = nil; + kernel_func = [library newFunctionWithName:@"convertKernel"]; + RET_CHECK(kernel_func != nil) << "Couldn't create kernel function."; + to_buffer_program_ = + [device newComputePipelineStateWithFunction:kernel_func error:&error]; + RET_CHECK(to_buffer_program_ != nil) << "Couldn't create pipeline state " << + [[error localizedDescription] UTF8String]; + + return mediapipe::OkStatus(); +} + +#endif // MEDIAPIPE_METAL_ENABLED + +#if !MEDIAPIPE_DISABLE_GPU && !MEDIAPIPE_METAL_ENABLED +absl::Status TensorsToImageCalculator::GlSetup(CalculatorContext* cc) { + std::string maybe_flip_y_define; +#if !defined(__APPLE__) + const auto& options = cc->Options(); + if (options.gpu_origin() != mediapipe::GpuOrigin::TOP_LEFT) { + maybe_flip_y_define = R"( + #define FLIP_Y_COORD + )"; + } +#endif // !defined(__APPLE__) + +#if MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + + const std::string shader_header = + absl::StrCat(tflite::gpu::gl::GetShaderHeader(workgroup_size_), R"( + precision highp float; + layout(rgba8, binding = 0) writeonly uniform highp image2D output_texture; + uniform ivec2 out_size; + )"); + + const std::string shader_body = R"( + layout(std430, binding = 2) readonly buffer B0 { + float elements[]; + } input_data; // data tensor + + void main() { + int out_width = out_size.x; + int out_height = out_size.y; + + ivec2 gid = ivec2(gl_GlobalInvocationID.xy); + if (gid.x >= out_width || gid.y >= out_height) { return; } + int linear_index = 3 * (gid.y * out_width + gid.x); + +#ifdef FLIP_Y_COORD + int y_coord = out_height - gid.y - 1; +#else + int y_coord = gid.y; +#endif // defined(FLIP_Y_COORD) + + ivec2 out_coordinate = ivec2(gid.x, y_coord); + vec4 out_value = vec4(input_data.elements[linear_index], input_data.elements[linear_index + 1], input_data.elements[linear_index + 2], 1.0); + imageStore(output_texture, out_coordinate, out_value); + })"; + + const std::string shader_full = + absl::StrCat(shader_header, maybe_flip_y_define, shader_body); + + GlShader shader; + MP_RETURN_IF_ERROR( + GlShader::CompileShader(GL_COMPUTE_SHADER, shader_full, &shader)); + gl_compute_program_ = std::make_unique(); + MP_RETURN_IF_ERROR( + GlProgram::CreateWithShader(shader, gl_compute_program_.get())); + +#else + constexpr GLchar kFragColorOutputDeclaration[] = R"( + #ifdef GL_ES + #define fragColor gl_FragColor + #else + out vec4 fragColor; + #endif // defined(GL_ES); +)"; + + constexpr GLchar kBody[] = R"( + DEFAULT_PRECISION(mediump, float) + in vec2 sample_coordinate; + uniform sampler2D tensor; + void main() { +#ifdef FLIP_Y_COORD + float y_coord = 1.0 - sample_coordinate.y; +#else + float y_coord = sample_coordinate.y; +#endif // defined(FLIP_Y_COORD) + vec3 color = texture2D(tensor, vec2(sample_coordinate.x, y_coord)).rgb; + fragColor = vec4(color, 1.0); + } + )"; + + const std::string src = + absl::StrCat(mediapipe::kMediaPipeFragmentShaderPreamble, + kFragColorOutputDeclaration, maybe_flip_y_define, kBody); + gl_renderer_ = std::make_unique(); + MP_RETURN_IF_ERROR(gl_renderer_->GlSetup(src.c_str(), {"tensor"})); + +#endif // MEDIAPIPE_OPENGL_ES_VERSION >= MEDIAPIPE_OPENGL_ES_31 + + return mediapipe::OkStatus(); +} + +#endif // !MEDIAPIPE_DISABLE_GPU && !MEDIAPIPE_METAL_ENABLED + +} // namespace tasks +} // namespace mediapipe diff --git a/mediapipe/tasks/cc/vision/face_stylizer/calculators/tensors_to_image_calculator.proto b/mediapipe/tasks/cc/vision/face_stylizer/calculators/tensors_to_image_calculator.proto new file mode 100644 index 00000000..08bd7b08 --- /dev/null +++ b/mediapipe/tasks/cc/vision/face_stylizer/calculators/tensors_to_image_calculator.proto @@ -0,0 +1,31 @@ +// Copyright 2023 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.tasks; + +import "mediapipe/framework/calculator.proto"; +import "mediapipe/gpu/gpu_origin.proto"; + +message TensorsToImageCalculatorOptions { + extend mediapipe.CalculatorOptions { + optional TensorsToImageCalculatorOptions ext = 511831156; + } + + // For CONVENTIONAL mode for OpenGL, input image starts at bottom and needs + // to be flipped vertically as tensors are expected to start at top. + // (DEFAULT or unset interpreted as CONVENTIONAL.) + optional mediapipe.GpuOrigin.Mode gpu_origin = 1; +} diff --git a/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/combined_prediction_calculator_test.cc b/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/combined_prediction_calculator_test.cc index ecf49795..509fac5f 100644 --- a/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/combined_prediction_calculator_test.cc +++ b/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/combined_prediction_calculator_test.cc @@ -203,106 +203,111 @@ INSTANTIATE_TEST_CASE_P( CombinedPredictionCalculatorTests, CombinedPredictionCalculatorTest, testing::ValuesIn({ { - .test_name = "TestCustomDramaWinnnerWith_HighCanned_Thresh", - .custom_negative_score = 0.1, - .drama_score = 0.5, - .llama_score = 0.3, - .drama_thresh = 0.25, - .llama_thresh = 0.7, - .canned_negative_score = 0.1, - .bazinga_score = 0.3, - .joy_score = 0.3, - .peace_score = 0.3, - .bazinga_thresh = 0.7, - .joy_thresh = 0.7, - .peace_thresh = 0.7, - .max_scoring_label = "CustomDrama", - .max_score = 0.5, + /* test_name= */ "TestCustomDramaWinnnerWith_HighCanned_Thresh", + /* custom_negative_score= */ 0.1, + /* drama_score= */ 0.5, + /* llama_score= */ 0.3, + /* drama_thresh= */ 0.25, + /* llama_thresh= */ 0.7, + /* canned_negative_score= */ 0.1, + /* bazinga_score= */ 0.3, + /* joy_score= */ 0.3, + /* peace_score= */ 0.3, + /* bazinga_thresh= */ 0.7, + /* joy_thresh= */ 0.7, + /* peace_thresh= */ 0.7, + /* max_scoring_label= */ "CustomDrama", + /* max_score= */ 0.5, }, { - .test_name = "TestCannedWinnerWith_HighCustom_ZeroCanned_Thresh", - .custom_negative_score = 0.1, - .drama_score = 0.3, - .llama_score = 0.6, - .drama_thresh = 0.4, - .llama_thresh = 0.8, - .canned_negative_score = 0.1, - .bazinga_score = 0.4, - .joy_score = 0.3, - .peace_score = 0.2, - .bazinga_thresh = 0.0, - .joy_thresh = 0.0, - .peace_thresh = 0.0, - .max_scoring_label = "CannedBazinga", - .max_score = 0.4, + /* test_name= */ "TestCannedWinnerWith_HighCustom_ZeroCanned_" + "Thresh", + /* custom_negative_score= */ 0.1, + /* drama_score= */ 0.3, + /* llama_score= */ 0.6, + /* drama_thresh= */ 0.4, + /* llama_thresh= */ 0.8, + /* canned_negative_score= */ 0.1, + /* bazinga_score= */ 0.4, + /* joy_score= */ 0.3, + /* peace_score= */ 0.2, + /* bazinga_thresh= */ 0.0, + /* joy_thresh= */ 0.0, + /* peace_thresh= */ 0.0, + /* max_scoring_label= */ "CannedBazinga", + /* max_score= */ 0.4, }, { - .test_name = "TestNegativeWinnerWith_LowCustom_HighCanned_Thresh", - .custom_negative_score = 0.5, - .drama_score = 0.1, - .llama_score = 0.4, - .drama_thresh = 0.1, - .llama_thresh = 0.05, - .canned_negative_score = 0.1, - .bazinga_score = 0.3, - .joy_score = 0.3, - .peace_score = 0.3, - .bazinga_thresh = 0.7, - .joy_thresh = 0.7, - .peace_thresh = 0.7, - .max_scoring_label = "Negative", - .max_score = 0.5, + /* test_name= */ "TestNegativeWinnerWith_LowCustom_HighCanned_" + "Thresh", + /* custom_negative_score= */ 0.5, + /* drama_score= */ 0.1, + /* llama_score= */ 0.4, + /* drama_thresh= */ 0.1, + /* llama_thresh= */ 0.05, + /* canned_negative_score= */ 0.1, + /* bazinga_score= */ 0.3, + /* joy_score= */ 0.3, + /* peace_score= */ 0.3, + /* bazinga_thresh= */ 0.7, + /* joy_thresh= */ 0.7, + /* peace_thresh= */ 0.7, + /* max_scoring_label= */ "Negative", + /* max_score= */ 0.5, }, { - .test_name = "TestNegativeWinnerWith_HighCustom_HighCanned_Thresh", - .custom_negative_score = 0.8, - .drama_score = 0.1, - .llama_score = 0.1, - .drama_thresh = 0.25, - .llama_thresh = 0.7, - .canned_negative_score = 0.1, - .bazinga_score = 0.3, - .joy_score = 0.3, - .peace_score = 0.3, - .bazinga_thresh = 0.7, - .joy_thresh = 0.7, - .peace_thresh = 0.7, - .max_scoring_label = "Negative", - .max_score = 0.8, + /* test_name= */ "TestNegativeWinnerWith_HighCustom_HighCanned_" + "Thresh", + /* custom_negative_score= */ 0.8, + /* drama_score= */ 0.1, + /* llama_score= */ 0.1, + /* drama_thresh= */ 0.25, + /* llama_thresh= */ 0.7, + /* canned_negative_score= */ 0.1, + /* bazinga_score= */ 0.3, + /* joy_score= */ 0.3, + /* peace_score= */ 0.3, + /* bazinga_thresh= */ 0.7, + /* joy_thresh= */ 0.7, + /* peace_thresh= */ 0.7, + /* max_scoring_label= */ "Negative", + /* max_score= */ 0.8, }, { - .test_name = "TestNegativeWinnerWith_HighCustom_HighCannedThresh2", - .custom_negative_score = 0.1, - .drama_score = 0.2, - .llama_score = 0.7, - .drama_thresh = 1.1, - .llama_thresh = 1.1, - .canned_negative_score = 0.1, - .bazinga_score = 0.3, - .joy_score = 0.3, - .peace_score = 0.3, - .bazinga_thresh = 0.7, - .joy_thresh = 0.7, - .peace_thresh = 0.7, - .max_scoring_label = "Negative", - .max_score = 0.1, + /* test_name= */ "TestNegativeWinnerWith_HighCustom_" + "HighCannedThresh2", + /* custom_negative_score= */ 0.1, + /* drama_score= */ 0.2, + /* llama_score= */ 0.7, + /* drama_thresh= */ 1.1, + /* llama_thresh= */ 1.1, + /* canned_negative_score= */ 0.1, + /* bazinga_score= */ 0.3, + /* joy_score= */ 0.3, + /* peace_score= */ 0.3, + /* bazinga_thresh= */ 0.7, + /* joy_thresh= */ 0.7, + /* peace_thresh= */ 0.7, + /* max_scoring_label= */ "Negative", + /* max_score= */ 0.1, }, { - .test_name = "TestNegativeWinnerWith_HighCustom_HighCanned_Thresh3", - .custom_negative_score = 0.1, - .drama_score = 0.3, - .llama_score = 0.6, - .drama_thresh = 0.4, - .llama_thresh = 0.8, - .canned_negative_score = 0.3, - .bazinga_score = 0.2, - .joy_score = 0.3, - .peace_score = 0.2, - .bazinga_thresh = 0.5, - .joy_thresh = 0.5, - .peace_thresh = 0.5, - .max_scoring_label = "Negative", - .max_score = 0.1, + /* test_name= */ "TestNegativeWinnerWith_HighCustom_HighCanned_" + "Thresh3", + /* custom_negative_score= */ 0.1, + /* drama_score= */ 0.3, + /* llama_score= */ 0.6, + /* drama_thresh= */ 0.4, + /* llama_thresh= */ 0.8, + /* canned_negative_score= */ 0.3, + /* bazinga_score= */ 0.2, + /* joy_score= */ 0.3, + /* peace_score= */ 0.2, + /* bazinga_thresh= */ 0.5, + /* joy_thresh= */ 0.5, + /* peace_thresh= */ 0.5, + /* max_scoring_label= */ "Negative", + /* max_score= */ 0.1, }, }), [](const testing::TestParamInfo< diff --git a/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/landmarks_to_matrix_calculator_test.cc b/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/landmarks_to_matrix_calculator_test.cc index a1a44c8d..70234a32 100644 --- a/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/landmarks_to_matrix_calculator_test.cc +++ b/mediapipe/tasks/cc/vision/gesture_recognizer/calculators/landmarks_to_matrix_calculator_test.cc @@ -117,24 +117,24 @@ TEST_P(Landmarks2dToMatrixCalculatorTest, OutputsCorrectResult) { INSTANTIATE_TEST_CASE_P( LandmarksToMatrixCalculatorTests, Landmarks2dToMatrixCalculatorTest, testing::ValuesIn( - {{.test_name = "TestWithOffset0", - .base_offset = 0, - .object_normalization_origin_offset = 0, - .expected_cell_0_2 = 0.1f, - .expected_cell_1_5 = 0.1875f, - .rotation = 0}, - {.test_name = "TestWithOffset21", - .base_offset = 21, - .object_normalization_origin_offset = 0, - .expected_cell_0_2 = 0.1f, - .expected_cell_1_5 = 0.1875f, - .rotation = 0}, - {.test_name = "TestWithRotation", - .base_offset = 0, - .object_normalization_origin_offset = 0, - .expected_cell_0_2 = 0.075f, - .expected_cell_1_5 = -0.25f, - .rotation = M_PI / 2.0}}), + {{/* test_name= */ "TestWithOffset0", + /* base_offset= */ 0, + /* object_normalization_origin_offset= */ 0, + /* expected_cell_0_2= */ 0.1f, + /* expected_cell_1_5= */ 0.1875f, + /* rotation= */ 0}, + {/* test_name= */ "TestWithOffset21", + /* base_offset= */ 21, + /* object_normalization_origin_offset= */ 0, + /* expected_cell_0_2= */ 0.1f, + /* expected_cell_1_5= */ 0.1875f, + /* rotation= */ 0}, + {/* test_name= */ "TestWithRotation", + /* base_offset= */ 0, + /* object_normalization_origin_offset= */ 0, + /* expected_cell_0_2= */ 0.075f, + /* expected_cell_1_5= */ -0.25f, + /* rotation= */ M_PI / 2.0}}), [](const testing::TestParamInfo< Landmarks2dToMatrixCalculatorTest::ParamType>& info) { return info.param.test_name; @@ -203,30 +203,30 @@ TEST_P(LandmarksWorld3dToMatrixCalculatorTest, OutputsCorrectResult) { INSTANTIATE_TEST_CASE_P( LandmarksToMatrixCalculatorTests, LandmarksWorld3dToMatrixCalculatorTest, testing::ValuesIn( - {{.test_name = "TestWithOffset0", - .base_offset = 0, - .object_normalization_origin_offset = 0, - .expected_cell_0_2 = 0.1f, - .expected_cell_1_5 = 0.25, - .rotation = 0}, - {.test_name = "TestWithOffset21", - .base_offset = 21, - .object_normalization_origin_offset = 0, - .expected_cell_0_2 = 0.1f, - .expected_cell_1_5 = 0.25, - .rotation = 0}, - {.test_name = "NoObjectNormalization", - .base_offset = 0, - .object_normalization_origin_offset = -1, - .expected_cell_0_2 = 0.021f, - .expected_cell_1_5 = 0.052f, - .rotation = 0}, - {.test_name = "TestWithRotation", - .base_offset = 0, - .object_normalization_origin_offset = 0, - .expected_cell_0_2 = 0.1f, - .expected_cell_1_5 = -0.25f, - .rotation = M_PI / 2.0}}), + {{/* test_name= */ "TestWithOffset0", + /* base_offset= */ 0, + /* object_normalization_origin_offset= */ 0, + /* expected_cell_0_2= */ 0.1f, + /* expected_cell_1_5= */ 0.25, + /* rotation= */ 0}, + {/* test_name= */ "TestWithOffset21", + /* base_offset= */ 21, + /* object_normalization_origin_offset= */ 0, + /* expected_cell_0_2= */ 0.1f, + /* expected_cell_1_5= */ 0.25, + /* rotation= */ 0}, + {/* test_name= */ "NoObjectNormalization", + /* base_offset= */ 0, + /* object_normalization_origin_offset= */ -1, + /* expected_cell_0_2= */ 0.021f, + /* expected_cell_1_5= */ 0.052f, + /* rotation= */ 0}, + {/* test_name= */ "TestWithRotation", + /* base_offset= */ 0, + /* object_normalization_origin_offset= */ 0, + /* expected_cell_0_2= */ 0.1f, + /* expected_cell_1_5= */ -0.25f, + /* rotation= */ M_PI / 2.0}}), [](const testing::TestParamInfo< LandmarksWorld3dToMatrixCalculatorTest::ParamType>& info) { return info.param.test_name; diff --git a/mediapipe/tasks/cc/vision/hand_detector/hand_detector_graph.cc b/mediapipe/tasks/cc/vision/hand_detector/hand_detector_graph.cc index d7163e33..923eab1c 100644 --- a/mediapipe/tasks/cc/vision/hand_detector/hand_detector_graph.cc +++ b/mediapipe/tasks/cc/vision/hand_detector/hand_detector_graph.cc @@ -257,19 +257,28 @@ class HandDetectorGraph : public core::ModelTaskGraph { preprocessed_tensors >> inference.In("TENSORS"); auto model_output_tensors = inference.Out("TENSORS"); + // TODO: support hand detection metadata. + bool has_metadata = false; + // Generates a single side packet containing a vector of SSD anchors. auto& ssd_anchor = graph.AddNode("SsdAnchorsCalculator"); - ConfigureSsdAnchorsCalculator( - &ssd_anchor.GetOptions()); + auto& ssd_anchor_options = + ssd_anchor.GetOptions(); + if (!has_metadata) { + ConfigureSsdAnchorsCalculator(&ssd_anchor_options); + } auto anchors = ssd_anchor.SideOut(""); // Converts output tensors to Detections. auto& tensors_to_detections = graph.AddNode("TensorsToDetectionsCalculator"); - ConfigureTensorsToDetectionsCalculator( - subgraph_options, - &tensors_to_detections - .GetOptions()); + if (!has_metadata) { + ConfigureTensorsToDetectionsCalculator( + subgraph_options, + &tensors_to_detections + .GetOptions()); + } + model_output_tensors >> tensors_to_detections.In("TENSORS"); anchors >> tensors_to_detections.SideIn("ANCHORS"); auto detections = tensors_to_detections.Out("DETECTIONS"); diff --git a/mediapipe/tasks/cc/vision/hand_landmarker/BUILD b/mediapipe/tasks/cc/vision/hand_landmarker/BUILD index 2552e7a1..7a83816b 100644 --- a/mediapipe/tasks/cc/vision/hand_landmarker/BUILD +++ b/mediapipe/tasks/cc/vision/hand_landmarker/BUILD @@ -148,6 +148,7 @@ cc_library( "//mediapipe/tasks/cc/vision/hand_landmarker/calculators:hand_landmarks_deduplication_calculator", "//mediapipe/tasks/cc/vision/hand_landmarker/proto:hand_landmarker_graph_options_cc_proto", "//mediapipe/tasks/cc/vision/hand_landmarker/proto:hand_landmarks_detector_graph_options_cc_proto", + "//mediapipe/util:graph_builder_utils", ], alwayslink = 1, ) diff --git a/mediapipe/tasks/cc/vision/hand_landmarker/hand_landmarker_graph.cc b/mediapipe/tasks/cc/vision/hand_landmarker/hand_landmarker_graph.cc index 74d288ac..4a3db9f4 100644 --- a/mediapipe/tasks/cc/vision/hand_landmarker/hand_landmarker_graph.cc +++ b/mediapipe/tasks/cc/vision/hand_landmarker/hand_landmarker_graph.cc @@ -14,6 +14,7 @@ limitations under the License. ==============================================================================*/ #include +#include #include #include #include @@ -41,6 +42,7 @@ limitations under the License. #include "mediapipe/tasks/cc/vision/hand_landmarker/calculators/hand_association_calculator.pb.h" #include "mediapipe/tasks/cc/vision/hand_landmarker/proto/hand_landmarker_graph_options.pb.h" #include "mediapipe/tasks/cc/vision/hand_landmarker/proto/hand_landmarks_detector_graph_options.pb.h" +#include "mediapipe/util/graph_builder_utils.h" namespace mediapipe { namespace tasks { @@ -53,7 +55,7 @@ using ::mediapipe::NormalizedRect; using ::mediapipe::api2::Input; using ::mediapipe::api2::Output; using ::mediapipe::api2::builder::Graph; -using ::mediapipe::api2::builder::Source; +using ::mediapipe::api2::builder::Stream; using ::mediapipe::tasks::components::utils::DisallowIf; using ::mediapipe::tasks::core::ModelAssetBundleResources; using ::mediapipe::tasks::metadata::SetExternalFile; @@ -78,40 +80,46 @@ constexpr char kHandLandmarksDetectorTFLiteName[] = "hand_landmarks_detector.tflite"; struct HandLandmarkerOutputs { - Source> landmark_lists; - Source> world_landmark_lists; - Source> hand_rects_next_frame; - Source> handednesses; - Source> palm_rects; - Source> palm_detections; - Source image; + Stream> landmark_lists; + Stream> world_landmark_lists; + Stream> hand_rects_next_frame; + Stream> handednesses; + Stream> palm_rects; + Stream> palm_detections; + Stream image; }; // Sets the base options in the sub tasks. absl::Status SetSubTaskBaseOptions(const ModelAssetBundleResources& resources, HandLandmarkerGraphOptions* options, bool is_copy) { - ASSIGN_OR_RETURN(const auto hand_detector_file, - resources.GetModelFile(kHandDetectorTFLiteName)); auto* hand_detector_graph_options = options->mutable_hand_detector_graph_options(); - SetExternalFile(hand_detector_file, - hand_detector_graph_options->mutable_base_options() - ->mutable_model_asset(), - is_copy); + if (!hand_detector_graph_options->base_options().has_model_asset()) { + ASSIGN_OR_RETURN(const auto hand_detector_file, + resources.GetModelFile(kHandDetectorTFLiteName)); + SetExternalFile(hand_detector_file, + hand_detector_graph_options->mutable_base_options() + ->mutable_model_asset(), + is_copy); + } hand_detector_graph_options->mutable_base_options() ->mutable_acceleration() ->CopyFrom(options->base_options().acceleration()); hand_detector_graph_options->mutable_base_options()->set_use_stream_mode( options->base_options().use_stream_mode()); - ASSIGN_OR_RETURN(const auto hand_landmarks_detector_file, - resources.GetModelFile(kHandLandmarksDetectorTFLiteName)); auto* hand_landmarks_detector_graph_options = options->mutable_hand_landmarks_detector_graph_options(); - SetExternalFile(hand_landmarks_detector_file, - hand_landmarks_detector_graph_options->mutable_base_options() - ->mutable_model_asset(), - is_copy); + if (!hand_landmarks_detector_graph_options->base_options() + .has_model_asset()) { + ASSIGN_OR_RETURN(const auto hand_landmarks_detector_file, + resources.GetModelFile(kHandLandmarksDetectorTFLiteName)); + SetExternalFile( + hand_landmarks_detector_file, + hand_landmarks_detector_graph_options->mutable_base_options() + ->mutable_model_asset(), + is_copy); + } hand_landmarks_detector_graph_options->mutable_base_options() ->mutable_acceleration() ->CopyFrom(options->base_options().acceleration()); @@ -119,7 +127,6 @@ absl::Status SetSubTaskBaseOptions(const ModelAssetBundleResources& resources, ->set_use_stream_mode(options->base_options().use_stream_mode()); return absl::OkStatus(); } - } // namespace // A "mediapipe.tasks.vision.hand_landmarker.HandLandmarkerGraph" performs hand @@ -219,12 +226,15 @@ class HandLandmarkerGraph : public core::ModelTaskGraph { !sc->Service(::mediapipe::tasks::core::kModelResourcesCacheService) .IsAvailable())); } + Stream image_in = graph.In(kImageTag).Cast(); + std::optional> norm_rect_in; + if (HasInput(sc->OriginalNode(), kNormRectTag)) { + norm_rect_in = graph.In(kNormRectTag).Cast(); + } ASSIGN_OR_RETURN( auto hand_landmarker_outputs, - BuildHandLandmarkerGraph( - sc->Options(), - graph[Input(kImageTag)], - graph[Input::Optional(kNormRectTag)], graph)); + BuildHandLandmarkerGraph(sc->Options(), + image_in, norm_rect_in, graph)); hand_landmarker_outputs.landmark_lists >> graph[Output>(kLandmarksTag)]; hand_landmarker_outputs.world_landmark_lists >> @@ -262,8 +272,8 @@ class HandLandmarkerGraph : public core::ModelTaskGraph { // image_in: (mediapipe::Image) stream to run hand landmark detection on. // graph: the mediapipe graph instance to be updated. absl::StatusOr BuildHandLandmarkerGraph( - const HandLandmarkerGraphOptions& tasks_options, Source image_in, - Source norm_rect_in, Graph& graph) { + const HandLandmarkerGraphOptions& tasks_options, Stream image_in, + std::optional> norm_rect_in, Graph& graph) { const int max_num_hands = tasks_options.hand_detector_graph_options().num_hands(); @@ -293,10 +303,15 @@ class HandLandmarkerGraph : public core::ModelTaskGraph { // track the hands from the last frame. auto image_for_hand_detector = DisallowIf(image_in, has_enough_hands, graph); - auto norm_rect_in_for_hand_detector = - DisallowIf(norm_rect_in, has_enough_hands, graph); + std::optional> norm_rect_in_for_hand_detector; + if (norm_rect_in) { + norm_rect_in_for_hand_detector = + DisallowIf(norm_rect_in.value(), has_enough_hands, graph); + } image_for_hand_detector >> hand_detector.In("IMAGE"); - norm_rect_in_for_hand_detector >> hand_detector.In("NORM_RECT"); + if (norm_rect_in_for_hand_detector) { + norm_rect_in_for_hand_detector.value() >> hand_detector.In("NORM_RECT"); + } auto hand_rects_from_hand_detector = hand_detector.Out("HAND_RECTS"); auto& hand_association = graph.AddNode("HandAssociationCalculator"); hand_association.GetOptions() @@ -313,7 +328,9 @@ class HandLandmarkerGraph : public core::ModelTaskGraph { // series, and we don't want to enable the tracking and hand associations // between input images. Always use the hand detector graph. image_in >> hand_detector.In("IMAGE"); - norm_rect_in >> hand_detector.In("NORM_RECT"); + if (norm_rect_in) { + norm_rect_in.value() >> hand_detector.In("NORM_RECT"); + } auto hand_rects_from_hand_detector = hand_detector.Out("HAND_RECTS"); hand_rects_from_hand_detector >> clip_hand_rects.In(""); } diff --git a/mediapipe/tasks/ios/test/vision/core/BUILD b/mediapipe/tasks/ios/test/vision/core/BUILD index ef95e468..92954d06 100644 --- a/mediapipe/tasks/ios/test/vision/core/BUILD +++ b/mediapipe/tasks/ios/test/vision/core/BUILD @@ -34,16 +34,14 @@ objc_library( data = [ "//mediapipe/tasks/testdata/vision:test_images", ], - sdk_frameworks = [ - "CoreMedia", - "CoreVideo", - "CoreGraphics", - "UIKit", - "Accelerate", - ], deps = [ "//mediapipe/tasks/ios/common:MPPCommon", "//mediapipe/tasks/ios/vision/core:MPPImage", + "//third_party/apple_frameworks:Accelerate", + "//third_party/apple_frameworks:CoreGraphics", + "//third_party/apple_frameworks:CoreMedia", + "//third_party/apple_frameworks:CoreVideo", + "//third_party/apple_frameworks:UIKit", ], ) diff --git a/mediapipe/tasks/ios/vision/core/BUILD b/mediapipe/tasks/ios/vision/core/BUILD index 91b9078a..1961ca6b 100644 --- a/mediapipe/tasks/ios/vision/core/BUILD +++ b/mediapipe/tasks/ios/vision/core/BUILD @@ -11,11 +11,6 @@ objc_library( "-std=c++17", ], module_name = "MPPImage", - sdk_frameworks = [ - "CoreMedia", - "CoreVideo", - "UIKit", - ], deps = [ "//mediapipe/tasks/ios/common:MPPCommon", "//mediapipe/tasks/ios/common/utils:MPPCommonUtils", diff --git a/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h new file mode 100644 index 00000000..cf597ec2 --- /dev/null +++ b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h @@ -0,0 +1,27 @@ +// Copyright 2023 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. + +#import + +#include "mediapipe/framework/packet.h" +#import "mediapipe/tasks/ios/vision/core/sources/MPPImage.h" + +/** + * This class helps create various kinds of packets for Mediapipe Vision Tasks. + */ +@interface MPPVisionPacketCreator : NSObject + ++ (mediapipe::Packet)createPacketWithMPPImage:(MPPImage *)image error:(NSError **)error; + +@end diff --git a/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.mm b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.mm new file mode 100644 index 00000000..01e583e6 --- /dev/null +++ b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.mm @@ -0,0 +1,43 @@ +// Copyright 2023 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. + +#import "mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h" +#import "mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.h" + +#include "mediapipe/framework/formats/image.h" + +namespace { +using ::mediapipe::Image; +using ::mediapipe::ImageFrame; +using ::mediapipe::MakePacket; +using ::mediapipe::Packet; +} // namespace + +struct freeDeleter { + void operator()(void *ptr) { free(ptr); } +}; + +@implementation MPPVisionPacketCreator + ++ (Packet)createPacketWithMPPImage:(MPPImage *)image error:(NSError **)error { + std::unique_ptr imageFrame = [image imageFrameWithError:error]; + + if (!imageFrame) { + return Packet(); + } + + return MakePacket(std::move(imageFrame)); +} + +@end diff --git a/mediapipe/tasks/ios/vision/core/utils/BUILD b/mediapipe/tasks/ios/vision/core/utils/BUILD index 540c2753..33dfc13b 100644 --- a/mediapipe/tasks/ios/vision/core/utils/BUILD +++ b/mediapipe/tasks/ios/vision/core/utils/BUILD @@ -4,23 +4,22 @@ licenses(["notice"]) objc_library( name = "MPPImageUtils", - srcs = ["sources/MPPImage+Utils.m"], + srcs = ["sources/MPPImage+Utils.mm"], hdrs = ["sources/MPPImage+Utils.h"], copts = [ "-ObjC++", "-std=c++17", ], module_name = "MPPImageUtils", - sdk_frameworks = [ - "Accelerate", - "CoreGraphics", - "CoreImage", - "CoreVideo", - "UIKit", - ], deps = [ + "//mediapipe/framework/formats:image_format_cc_proto", + "//mediapipe/framework/formats:image_frame", + "//mediapipe/tasks/ios/common:MPPCommon", "//mediapipe/tasks/ios/common/utils:MPPCommonUtils", "//mediapipe/tasks/ios/vision/core:MPPImage", - "//third_party/apple_frameworks:UIKit", + "//third_party/apple_frameworks:Accelerate", + "//third_party/apple_frameworks:CoreGraphics", + "//third_party/apple_frameworks:CoreImage", + "//third_party/apple_frameworks:CoreVideo", ], ) diff --git a/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.h b/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.h index a9c371d5..e683d73f 100644 --- a/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.h +++ b/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.h @@ -14,30 +14,27 @@ #import +#include "mediapipe/framework/formats/image_frame.h" #import "mediapipe/tasks/ios/vision/core/sources/MPPImage.h" NS_ASSUME_NONNULL_BEGIN /** - * Helper utility for performing operations on MPPImage specific to the MediaPipe Vision library. + * Helper utility for converting `MPPImage` into a `mediapipe::ImageFrame`. */ @interface MPPImage (Utils) - -/** Bitmap size of the image. */ -@property(nonatomic, readonly) CGSize bitmapSize; - /** - * Returns the underlying uint8 pixel buffer of an `MPPImage`. - * Irrespective of whether the underlying buffer is grayscale, RGB, RGBA, BGRA etc., the pixel - * data is converted to an RGB format. In case of grayscale images, the mono channel is duplicated - * in the R, G, B channels. + * Converts the `MPPImage` into a `mediapipe::ImageFrame`. + * Irrespective of whether the underlying buffer is grayscale, RGB, RGBA, BGRA etc., the MPPImage is + * converted to an RGB format. In case of grayscale images, the mono channel is duplicated in the R, + * G, B channels. * * @param error Pointer to the memory location where errors if any should be saved. If @c NULL, no * error will be saved. * - * @return The underlying pixel buffer of the `MPPImage` or nil in case of errors. + * @return An std::unique_ptr or `nullptr` in case of errors. */ -- (nullable uint8_t *)rgbPixelDataWithError:(NSError **)error; +- (std::unique_ptr)imageFrameWithError:(NSError **)error; @end diff --git a/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.m b/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.mm similarity index 68% rename from mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.m rename to mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.mm index 01ac9912..87f9a8a3 100644 --- a/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.m +++ b/mediapipe/tasks/ios/vision/core/utils/sources/MPPImage+Utils.mm @@ -22,6 +22,12 @@ #import #import +#include "mediapipe/framework/formats/image_format.pb.h" + +namespace { +using ::mediapipe::ImageFrame; +} + @interface MPPPixelDataUtils : NSObject + (uint8_t *)rgbPixelDataFromPixelData:(uint8_t *)pixelData @@ -35,21 +41,20 @@ @interface MPPCVPixelBufferUtils : NSObject -+ (uint8_t *)pixelDataFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer error:(NSError **)error; ++ (std::unique_ptr)imageFrameFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer + error:(NSError **)error; @end @interface MPPCGImageUtils : NSObject -+ (UInt8 *_Nullable)pixelDataFromCGImage:(CGImageRef)cgImage error:(NSError **)error; ++ (std::unique_ptr)imageFrameFromCGImage:(CGImageRef)cgImage error:(NSError **)error; @end -@interface UIImage (RawPixelDataUtils) +@interface UIImage (ImageFrameUtils) -@property(nonatomic, readonly) CGSize bitmapSize; - -- (uint8_t *)pixelDataWithError:(NSError **)error; +- (std::unique_ptr)imageFrameWithError:(NSError **)error; @end @@ -120,9 +125,14 @@ @implementation MPPCVPixelBufferUtils -+ (uint8_t *)rgbPixelDataFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer error:(NSError **)error { ++ (std::unique_ptr)rgbImageFrameFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer + error:(NSError **)error { CVPixelBufferLockBaseAddress(pixelBuffer, 0); + size_t width = CVPixelBufferGetWidth(pixelBuffer); + size_t height = CVPixelBufferGetHeight(pixelBuffer); + size_t stride = CVPixelBufferGetBytesPerRow(pixelBuffer); + uint8_t *rgbPixelData = [MPPPixelDataUtils rgbPixelDataFromPixelData:(uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer) withWidth:CVPixelBufferGetWidth(pixelBuffer) @@ -133,19 +143,24 @@ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); - return rgbPixelData; + if (!rgbPixelData) { + return nullptr; + } + + std::unique_ptr imageFrame = absl::make_unique( + ::mediapipe::ImageFormat::SRGB, width, height, stride, static_cast(rgbPixelData), + /*deleter=*/free); + + return imageFrame; } -+ (nullable uint8_t *)pixelDataFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer - error:(NSError **)error { - uint8_t *pixelData = NULL; - ++ (std::unique_ptr)imageFrameFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer + error:(NSError **)error { OSType pixelBufferFormat = CVPixelBufferGetPixelFormatType(pixelBuffer); switch (pixelBufferFormat) { case kCVPixelFormatType_32BGRA: { - pixelData = [MPPCVPixelBufferUtils rgbPixelDataFromCVPixelBuffer:pixelBuffer error:error]; - break; + return [MPPCVPixelBufferUtils rgbImageFrameFromCVPixelBuffer:pixelBuffer error:error]; } default: { [MPPCommonUtils createCustomError:error @@ -155,20 +170,20 @@ } } - return pixelData; + return nullptr; } @end @implementation MPPCGImageUtils -+ (UInt8 *_Nullable)pixelDataFromCGImage:(CGImageRef)cgImage error:(NSError **)error { ++ (std::unique_ptr)imageFrameFromCGImage:(CGImageRef)cgImage error:(NSError **)error { size_t width = CGImageGetWidth(cgImage); size_t height = CGImageGetHeight(cgImage); NSInteger bitsPerComponent = 8; NSInteger channelCount = 4; - UInt8 *pixel_data_to_return = NULL; + UInt8 *pixelDataToReturn = NULL; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); size_t bytesPerRow = channelCount * width; @@ -191,12 +206,12 @@ if (srcData) { // We have drawn the image as an RGBA image with 8 bitsPerComponent and hence can safely input // a pixel format of type kCVPixelFormatType_32RGBA for conversion by vImage. - pixel_data_to_return = [MPPPixelDataUtils rgbPixelDataFromPixelData:srcData - withWidth:width - height:height - stride:bytesPerRow - pixelBufferFormat:kCVPixelFormatType_32RGBA - error:error]; + pixelDataToReturn = [MPPPixelDataUtils rgbPixelDataFromPixelData:srcData + withWidth:width + height:height + stride:bytesPerRow + pixelBufferFormat:kCVPixelFormatType_32RGBA + error:error]; } CGContextRelease(context); @@ -204,38 +219,38 @@ CGColorSpaceRelease(colorSpace); - return pixel_data_to_return; + std::unique_ptr imageFrame = + absl::make_unique(mediapipe::ImageFormat::SRGB, (int)width, (int)height, + (int)bytesPerRow, static_cast(pixelDataToReturn), + /*deleter=*/free); + + return imageFrame; } @end -@implementation UIImage (RawPixelDataUtils) - -- (uint8_t *)pixelDataFromCIImageWithError:(NSError **)error { - uint8_t *pixelData = NULL; +@implementation UIImage (ImageFrameUtils) +- (std::unique_ptr)imageFrameFromCIImageWithError:(NSError **)error { if (self.CIImage.pixelBuffer) { - pixelData = [MPPCVPixelBufferUtils pixelDataFromCVPixelBuffer:self.CIImage.pixelBuffer - error:error]; + return [MPPCVPixelBufferUtils imageFrameFromCVPixelBuffer:self.CIImage.pixelBuffer error:error]; } else if (self.CIImage.CGImage) { - pixelData = [MPPCGImageUtils pixelDataFromCGImage:self.CIImage.CGImage error:error]; + return [MPPCGImageUtils imageFrameFromCGImage:self.CIImage.CGImage error:error]; } else { [MPPCommonUtils createCustomError:error withCode:MPPTasksErrorCodeInvalidArgumentError description:@"CIImage should have CGImage or CVPixelBuffer info."]; } - return pixelData; + return nullptr; } -- (uint8_t *)pixelDataWithError:(NSError **)error { - uint8_t *pixelData = nil; - +- (std::unique_ptr)imageFrameWithError:(NSError **)error { if (self.CGImage) { - pixelData = [MPPCGImageUtils pixelDataFromCGImage:self.CGImage error:error]; + return [MPPCGImageUtils imageFrameFromCGImage:self.CGImage error:error]; } else if (self.CIImage) { - pixelData = [self pixelDataFromCIImageWithError:error]; + return [self imageFrameFromCIImageWithError:error]; } else { [MPPCommonUtils createCustomError:error withCode:MPPTasksErrorCodeInvalidArgumentError @@ -243,46 +258,24 @@ " CIImage or CGImage."]; } - return pixelData; + return nullptr; } -- (CGSize)bitmapSize { - CGFloat width = 0; - CGFloat height = 0; - - if (self.CGImage) { - width = CGImageGetWidth(self.CGImage); - height = CGImageGetHeight(self.CGImage); - } else if (self.CIImage.pixelBuffer) { - width = CVPixelBufferGetWidth(self.CIImage.pixelBuffer); - height = CVPixelBufferGetHeight(self.CIImage.pixelBuffer); - } else if (self.CIImage.CGImage) { - width = CGImageGetWidth(self.CIImage.CGImage); - height = CGImageGetHeight(self.CIImage.CGImage); - } - return CGSizeMake(width, height); -} @end @implementation MPPImage (Utils) -- (nullable uint8_t *)rgbPixelDataWithError:(NSError **)error { - uint8_t *pixelData = NULL; - +- (std::unique_ptr)imageFrameWithError:(NSError **)error { switch (self.imageSourceType) { case MPPImageSourceTypeSampleBuffer: { CVPixelBufferRef sampleImagePixelBuffer = CMSampleBufferGetImageBuffer(self.sampleBuffer); - pixelData = [MPPCVPixelBufferUtils pixelDataFromCVPixelBuffer:sampleImagePixelBuffer - error:error]; - break; + return [MPPCVPixelBufferUtils imageFrameFromCVPixelBuffer:sampleImagePixelBuffer error:error]; } case MPPImageSourceTypePixelBuffer: { - pixelData = [MPPCVPixelBufferUtils pixelDataFromCVPixelBuffer:self.pixelBuffer error:error]; - break; + return [MPPCVPixelBufferUtils imageFrameFromCVPixelBuffer:self.pixelBuffer error:error]; } case MPPImageSourceTypeImage: { - pixelData = [self.image pixelDataWithError:error]; - break; + return [self.image imageFrameWithError:error]; } default: [MPPCommonUtils createCustomError:error @@ -290,35 +283,7 @@ description:@"Invalid source type for MPPImage."]; } - return pixelData; -} - -- (CGSize)bitmapSize { - CGFloat width = 0; - CGFloat height = 0; - - switch (self.imageSourceType) { - case MPPImageSourceTypeSampleBuffer: { - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(self.sampleBuffer); - width = CVPixelBufferGetWidth(pixelBuffer); - height = CVPixelBufferGetHeight(pixelBuffer); - break; - } - case MPPImageSourceTypePixelBuffer: { - width = CVPixelBufferGetWidth(self.pixelBuffer); - height = CVPixelBufferGetHeight(self.pixelBuffer); - break; - } - case MPPImageSourceTypeImage: { - width = self.image.bitmapSize.width; - height = self.image.bitmapSize.height; - break; - } - default: - break; - } - - return CGSizeMake(width, height); + return nullptr; } @end diff --git a/mediapipe/tasks/ios/vision/image_classifier/BUILD b/mediapipe/tasks/ios/vision/image_classifier/BUILD new file mode 100644 index 00000000..45e6e215 --- /dev/null +++ b/mediapipe/tasks/ios/vision/image_classifier/BUILD @@ -0,0 +1,38 @@ +# Copyright 2023 The MediaPipe Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//mediapipe/tasks:internal"]) + +licenses(["notice"]) + +objc_library( + name = "MPPImageClassifierResult", + srcs = ["sources/MPPImageClassifierResult.m"], + hdrs = ["sources/MPPImageClassifierResult.h"], + deps = [ + "//mediapipe/tasks/ios/components/containers:MPPClassificationResult", + "//mediapipe/tasks/ios/core:MPPTaskResult", + ], +) + +objc_library( + name = "MPPImageClassifierOptions", + srcs = ["sources/MPPImageClassifierOptions.m"], + hdrs = ["sources/MPPImageClassifierOptions.h"], + deps = [ + ":MPPImageClassifierResult", + "//mediapipe/tasks/ios/core:MPPTaskOptions", + "//mediapipe/tasks/ios/vision/core:MPPRunningMode", + ], +) diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierOptions.h b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierOptions.h new file mode 100644 index 00000000..f7e9a629 --- /dev/null +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierOptions.h @@ -0,0 +1,71 @@ +// Copyright 2023 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. + +#import + +#import "mediapipe/tasks/ios/core/sources/MPPTaskOptions.h" +#import "mediapipe/tasks/ios/vision/core/sources/MPPRunningMode.h" +#import "mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Options for setting up a `MPPImageClassifier`. + */ +NS_SWIFT_NAME(ImageClassifierOptions) +@interface MPPImageClassifierOptions : MPPTaskOptions + +@property(nonatomic) MPPRunningMode runningMode; + +/** + * The user-defined result callback for processing live stream data. The result callback should only + * be specified when the running mode is set to the live stream mode. + */ +@property(nonatomic, copy) void (^completion)(MPPImageClassifierResult *result, NSError *error); + +/** + * The locale to use for display names specified through the TFLite Model Metadata, if any. Defaults + * to English. + */ +@property(nonatomic, copy) NSString *displayNamesLocale; + +/** + * The maximum number of top-scored classification results to return. If < 0, all available results + * will be returned. If 0, an invalid argument error is returned. + */ +@property(nonatomic) NSInteger maxResults; + +/** + * Score threshold to override the one provided in the model metadata (if any). Results below this + * value are rejected. + */ +@property(nonatomic) float scoreThreshold; + +/** + * The allowlist of category names. If non-empty, detection results whose category name is not in + * this set will be filtered out. Duplicate or unknown category names are ignored. Mutually + * exclusive with categoryDenylist. + */ +@property(nonatomic, copy) NSArray *categoryAllowlist; + +/** + * The denylist of category names. If non-empty, detection results whose category name is in this + * set will be filtered out. Duplicate or unknown category names are ignored. Mutually exclusive + * with categoryAllowlist. + */ +@property(nonatomic, copy) NSArray *categoryDenylist; + +@end + +NS_ASSUME_NONNULL_END diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierOptions.m b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierOptions.m new file mode 100644 index 00000000..e109dcc3 --- /dev/null +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierOptions.m @@ -0,0 +1,41 @@ +// Copyright 2023 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. + +#import "mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierOptions.h" + +@implementation MPPImageClassifierOptions + +- (instancetype)init { + self = [super init]; + if (self) { + _maxResults = -1; + _scoreThreshold = 0; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + MPPImageClassifierOptions *imageClassifierOptions = [super copyWithZone:zone]; + + imageClassifierOptions.scoreThreshold = self.scoreThreshold; + imageClassifierOptions.maxResults = self.maxResults; + imageClassifierOptions.categoryDenylist = self.categoryDenylist; + imageClassifierOptions.categoryAllowlist = self.categoryAllowlist; + imageClassifierOptions.displayNamesLocale = self.displayNamesLocale; + imageClassifierOptions.completion = self.completion; + + return imageClassifierOptions; +} + +@end diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h new file mode 100644 index 00000000..92fdb13c --- /dev/null +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h @@ -0,0 +1,44 @@ +// Copyright 2023 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. + +#import +#import "mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.h" +#import "mediapipe/tasks/ios/core/sources/MPPTaskResult.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Represents the classification results generated by `MPPImageClassifier`. **/ +NS_SWIFT_NAME(ImageClassifierResult) +@interface MPPImageClassifierResult : MPPTaskResult + +/** The `MPPClassificationResult` instance containing one set of results per classifier head. **/ +@property(nonatomic, readonly) MPPClassificationResult *classificationResult; + +/** + * Initializes a new `MPPImageClassifierResult` with the given `MPPClassificationResult` and + * timestamp (in milliseconds). + * + * @param classificationResult The `MPPClassificationResult` instance containing one set of results + * per classifier head. + * @param timestampMs The timestamp for this result. + * + * @return An instance of `MPPImageClassifierResult` initialized with the given + * `MPPClassificationResult` and timestamp (in milliseconds). + */ +- (instancetype)initWithClassificationResult:(MPPClassificationResult *)classificationResult + timestampMs:(NSInteger)timestampMs; + +@end + +NS_ASSUME_NONNULL_END diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.m b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.m new file mode 100644 index 00000000..6dcd064e --- /dev/null +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.m @@ -0,0 +1,28 @@ +// Copyright 2023 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. + +#import "mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h" + +@implementation MPPImageClassifierResult + +- (instancetype)initWithClassificationResult:(MPPClassificationResult *)classificationResult + timestampMs:(NSInteger)timestampMs { + self = [super initWithTimestampMs:timestampMs]; + if (self) { + _classificationResult = classificationResult; + } + return self; +} + +@end diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/BUILD b/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/BUILD index 50ee56f6..7f936334 100644 --- a/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/BUILD +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/BUILD @@ -101,9 +101,7 @@ android_library( "//mediapipe/tasks/cc/core/proto:base_options_java_proto_lite", "//mediapipe/tasks/java/com/google/mediapipe/tasks/audio:libmediapipe_tasks_audio_jni_lib", "//mediapipe/tasks/java/com/google/mediapipe/tasks/components/containers:audiodata", - "//mediapipe/tasks/java/com/google/mediapipe/tasks/components/containers:embedding", "//mediapipe/tasks/java/com/google/mediapipe/tasks/components/containers:embeddingresult", - "//mediapipe/tasks/java/com/google/mediapipe/tasks/components/utils:cosinesimilarity", "//mediapipe/tasks/java/com/google/mediapipe/tasks/core", "//third_party:autovalue", "@maven//:com_google_guava_guava", diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/audioembedder/AudioEmbedder.java b/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/audioembedder/AudioEmbedder.java index 077f28ca..67d3f8b5 100644 --- a/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/audioembedder/AudioEmbedder.java +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/audio/audioembedder/AudioEmbedder.java @@ -26,10 +26,8 @@ import com.google.mediapipe.tasks.audio.audioembedder.proto.AudioEmbedderGraphOp import com.google.mediapipe.tasks.audio.core.BaseAudioTaskApi; import com.google.mediapipe.tasks.audio.core.RunningMode; import com.google.mediapipe.tasks.components.containers.AudioData; -import com.google.mediapipe.tasks.components.containers.Embedding; import com.google.mediapipe.tasks.components.containers.proto.EmbeddingsProto; import com.google.mediapipe.tasks.components.processors.proto.EmbedderOptionsProto; -import com.google.mediapipe.tasks.components.utils.CosineSimilarity; import com.google.mediapipe.tasks.core.BaseOptions; import com.google.mediapipe.tasks.core.ErrorListener; import com.google.mediapipe.tasks.core.OutputHandler; @@ -273,17 +271,6 @@ public final class AudioEmbedder extends BaseAudioTaskApi { sendAudioStreamData(audioBlock, timestampMs); } - /** - * Utility function to compute cosine - * similarity between two {@link Embedding} objects. - * - * @throws IllegalArgumentException if the embeddings are of different types (float vs. - * quantized), have different sizes, or have an L2-norm of 0. - */ - public static double cosineSimilarity(Embedding u, Embedding v) { - return CosineSimilarity.compute(u, v); - } - /** Options for setting up and {@link AudioEmbedder}. */ @AutoValue public abstract static class AudioEmbedderOptions extends TaskOptions { diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenter.java b/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenter.java index 8d07b7c6..f4faf411 100644 --- a/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenter.java +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenter.java @@ -47,10 +47,14 @@ import java.util.Optional; /** * Performs image segmentation on images. * - *

Note that, unlike other vision tasks, the output of ImageSegmenter is provided through a - * user-defined callback function even for the synchronous API. This makes it possible for - * ImageSegmenter to return the output masks without any copy. {@link ResultListener} must be set in - * the {@link ImageSegmenterOptions} for all {@link RunningMode}. + *

Note that, in addition to the standard segmentation API, {@link segment} and {@link + * segmentForVideo}, that take an input image and return the outputs, but involves deep copy of the + * returns, ImageSegmenter also supports the callback API, {@link segmentWithResultListener} and + * {@link segmentForVideoWithResultListener}, which allow you to access the outputs through zero + * copy. + * + *

The callback API is available for all {@link RunningMode} in ImageSegmenter. Set {@link + * ResultListener} in {@link ImageSegmenterOptions} properly to use the callback API. * *

The API expects a TFLite model with,TFLite Model Metadata.. @@ -85,6 +89,8 @@ public final class ImageSegmenter extends BaseVisionTaskApi { private static final String TASK_GRAPH_NAME = "mediapipe.tasks.vision.image_segmenter.ImageSegmenterGraph"; + private boolean hasResultListener = false; + /** * Creates an {@link ImageSegmenter} instance from an {@link ImageSegmenterOptions}. * @@ -116,8 +122,19 @@ public final class ImageSegmenter extends BaseVisionTaskApi { int imageListSize = PacketGetter.getImageListSize(packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX)); ByteBuffer[] buffersArray = new ByteBuffer[imageListSize]; + // If resultListener is not provided, the resulted MPImage is deep copied from mediapipe + // graph. If provided, the result MPImage is wrapping the mediapipe packet memory. + if (!segmenterOptions.resultListener().isPresent()) { + for (int i = 0; i < imageListSize; i++) { + buffersArray[i] = + ByteBuffer.allocateDirect( + width * height * (imageFormat == MPImage.IMAGE_FORMAT_VEC32F1 ? 4 : 1)); + } + } if (!PacketGetter.getImageList( - packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX), buffersArray, false)) { + packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX), + buffersArray, + !segmenterOptions.resultListener().isPresent())) { throw new MediaPipeException( MediaPipeException.StatusCode.INTERNAL.ordinal(), "There is an error getting segmented masks. It usually results from incorrect" @@ -143,7 +160,7 @@ public final class ImageSegmenter extends BaseVisionTaskApi { .build(); } }); - handler.setResultListener(segmenterOptions.resultListener()); + segmenterOptions.resultListener().ifPresent(handler::setResultListener); segmenterOptions.errorListener().ifPresent(handler::setErrorListener); TaskRunner runner = TaskRunner.create( @@ -158,7 +175,8 @@ public final class ImageSegmenter extends BaseVisionTaskApi { .setEnableFlowLimiting(segmenterOptions.runningMode() == RunningMode.LIVE_STREAM) .build(), handler); - return new ImageSegmenter(runner, segmenterOptions.runningMode()); + return new ImageSegmenter( + runner, segmenterOptions.runningMode(), segmenterOptions.resultListener().isPresent()); } /** @@ -168,16 +186,17 @@ public final class ImageSegmenter extends BaseVisionTaskApi { * @param taskRunner a {@link TaskRunner}. * @param runningMode a mediapipe vision task {@link RunningMode}. */ - private ImageSegmenter(TaskRunner taskRunner, RunningMode runningMode) { + private ImageSegmenter( + TaskRunner taskRunner, RunningMode runningMode, boolean hasResultListener) { super(taskRunner, runningMode, IMAGE_IN_STREAM_NAME, NORM_RECT_IN_STREAM_NAME); + this.hasResultListener = hasResultListener; } /** * Performs image segmentation on the provided single image with default image processing options, - * i.e. without any rotation applied, and the results will be available via the {@link - * ResultListener} provided in the {@link ImageSegmenterOptions}. Only use this method when the - * {@link ImageSegmenter} is created with {@link RunningMode.IMAGE}. TODO update java - * doc for input image format. + * i.e. without any rotation applied. Only use this method when the {@link ImageSegmenter} is + * created with {@link RunningMode.IMAGE}. TODO update java doc for input image + * format. * *

{@link ImageSegmenter} supports the following color space types: * @@ -186,19 +205,19 @@ public final class ImageSegmenter extends BaseVisionTaskApi { * * * @param image a MediaPipe {@link MPImage} object for processing. - * @throws MediaPipeException if there is an internal error. + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is + * created with a {@link ResultListener}. */ - public void segment(MPImage image) { - segment(image, ImageProcessingOptions.builder().build()); + public ImageSegmenterResult segment(MPImage image) { + return segment(image, ImageProcessingOptions.builder().build()); } /** - * Performs image segmentation on the provided single image, and the results will be available via - * the {@link ResultListener} provided in the {@link ImageSegmenterOptions}. Only use this method - * when the {@link ImageSegmenter} is created with {@link RunningMode.IMAGE}. TODO - * update java doc for input image format. + * Performs image segmentation on the provided single image. Only use this method when the {@link + * ImageSegmenter} is created with {@link RunningMode.IMAGE}. TODO update java doc + * for input image format. * - *

{@link HandLandmarker} supports the following color space types: + *

{@link ImageSegmenter} supports the following color space types: * *

    *
  • {@link Bitmap.Config.ARGB_8888} @@ -211,9 +230,76 @@ public final class ImageSegmenter extends BaseVisionTaskApi { * this method throwing an IllegalArgumentException. * @throws IllegalArgumentException if the {@link ImageProcessingOptions} specify a * region-of-interest. - * @throws MediaPipeException if there is an internal error. + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is + * created with a {@link ResultListener}. */ - public void segment(MPImage image, ImageProcessingOptions imageProcessingOptions) { + public ImageSegmenterResult segment( + MPImage image, ImageProcessingOptions imageProcessingOptions) { + if (hasResultListener) { + throw new MediaPipeException( + MediaPipeException.StatusCode.FAILED_PRECONDITION.ordinal(), + "ResultListener is provided in the ImageSegmenterOptions, but this method will return an" + + " ImageSegmentationResult."); + } + validateImageProcessingOptions(imageProcessingOptions); + return (ImageSegmenterResult) processImageData(image, imageProcessingOptions); + } + + /** + * Performs image segmentation on the provided single image with default image processing options, + * i.e. without any rotation applied, and provides zero-copied results via {@link ResultListener} + * in {@link ImageSegmenterOptions}. Only use this method when the {@link ImageSegmenter} is + * created with {@link RunningMode.IMAGE}. + * + *

    TODO update java doc for input image format. + * + *

    {@link ImageSegmenter} supports the following color space types: + * + *

      + *
    • {@link Bitmap.Config.ARGB_8888} + *
    + * + * @param image a MediaPipe {@link MPImage} object for processing. + * @throws IllegalArgumentException if the {@link ImageProcessingOptions} specify a + * region-of-interest. + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is not + * created wtih {@link ResultListener} set in {@link ImageSegmenterOptions}. + */ + public void segmentWithResultListener(MPImage image) { + segmentWithResultListener(image, ImageProcessingOptions.builder().build()); + } + + /** + * Performs image segmentation on the provided single image, and provides zero-copied results via + * {@link ResultListener} in {@link ImageSegmenterOptions}. Only use this method when the {@link + * ImageSegmenter} is created with {@link RunningMode.IMAGE}. + * + *

    TODO update java doc for input image format. + * + *

    {@link ImageSegmenter} supports the following color space types: + * + *

      + *
    • {@link Bitmap.Config.ARGB_8888} + *
    + * + * @param image a MediaPipe {@link MPImage} object for processing. + * @param imageProcessingOptions the {@link ImageProcessingOptions} specifying how to process the + * input image before running inference. Note that region-of-interest is not supported + * by this task: specifying {@link ImageProcessingOptions#regionOfInterest()} will result in + * this method throwing an IllegalArgumentException. + * @throws IllegalArgumentException if the {@link ImageProcessingOptions} specify a + * region-of-interest. + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is not + * created wtih {@link ResultListener} set in {@link ImageSegmenterOptions}. + */ + public void segmentWithResultListener( + MPImage image, ImageProcessingOptions imageProcessingOptions) { + if (!hasResultListener) { + throw new MediaPipeException( + MediaPipeException.StatusCode.FAILED_PRECONDITION.ordinal(), + "ResultListener is not set in the ImageSegmenterOptions, but this method expects a" + + " ResultListener to process ImageSegmentationResult."); + } validateImageProcessingOptions(imageProcessingOptions); ImageSegmenterResult unused = (ImageSegmenterResult) processImageData(image, imageProcessingOptions); @@ -221,9 +307,8 @@ public final class ImageSegmenter extends BaseVisionTaskApi { /** * Performs image segmentation on the provided video frame with default image processing options, - * i.e. without any rotation applied, and the results will be available via the {@link - * ResultListener} provided in the {@link ImageSegmenterOptions}. Only use this method when the - * {@link HandLandmarker} is created with {@link RunningMode.VIDEO}. + * i.e. without any rotation applied. Only use this method when the {@link ImageSegmenter} is + * created with {@link RunningMode.VIDEO}. * *

    It's required to provide the video frame's timestamp (in milliseconds). The input timestamps * must be monotonically increasing. @@ -236,21 +321,21 @@ public final class ImageSegmenter extends BaseVisionTaskApi { * * @param image a MediaPipe {@link MPImage} object for processing. * @param timestampMs the input timestamp (in milliseconds). - * @throws MediaPipeException if there is an internal error. + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is + * created with a {@link ResultListener}. */ - public void segmentForVideo(MPImage image, long timestampMs) { - segmentForVideo(image, ImageProcessingOptions.builder().build(), timestampMs); + public ImageSegmenterResult segmentForVideo(MPImage image, long timestampMs) { + return segmentForVideo(image, ImageProcessingOptions.builder().build(), timestampMs); } /** - * Performs image segmentation on the provided video frame, and the results will be available via - * the {@link ResultListener} provided in the {@link ImageSegmenterOptions}. Only use this method - * when the {@link ImageSegmenter} is created with {@link RunningMode.VIDEO}. + * Performs image segmentation on the provided video frame. Only use this method when the {@link + * ImageSegmenter} is created with {@link RunningMode.VIDEO}. * *

    It's required to provide the video frame's timestamp (in milliseconds). The input timestamps * must be monotonically increasing. * - *

    {@link HandLandmarker} supports the following color space types: + *

    {@link ImageSegmenter} supports the following color space types: * *

      *
    • {@link Bitmap.Config.ARGB_8888} @@ -264,20 +349,82 @@ public final class ImageSegmenter extends BaseVisionTaskApi { * @param timestampMs the input timestamp (in milliseconds). * @throws IllegalArgumentException if the {@link ImageProcessingOptions} specify a * region-of-interest. - * @throws MediaPipeException if there is an internal error. + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is + * created with a {@link ResultListener}. */ - public void segmentForVideo( + public ImageSegmenterResult segmentForVideo( MPImage image, ImageProcessingOptions imageProcessingOptions, long timestampMs) { + if (hasResultListener) { + throw new MediaPipeException( + MediaPipeException.StatusCode.FAILED_PRECONDITION.ordinal(), + "ResultListener is provided in the ImageSegmenterOptions, but this method will return an" + + " ImageSegmentationResult."); + } + validateImageProcessingOptions(imageProcessingOptions); + return (ImageSegmenterResult) processVideoData(image, imageProcessingOptions, timestampMs); + } + + /** + * Performs image segmentation on the provided video frame with default image processing options, + * i.e. without any rotation applied, and provides zero-copied results via {@link ResultListener} + * in {@link ImageSegmenterOptions}. Only use this method when the {@link ImageSegmenter} is + * created with {@link RunningMode.VIDEO}. + * + *

      It's required to provide the video frame's timestamp (in milliseconds). The input timestamps + * must be monotonically increasing. + * + *

      {@link ImageSegmenter} supports the following color space types: + * + *

        + *
      • {@link Bitmap.Config.ARGB_8888} + *
      + * + * @param image a MediaPipe {@link MPImage} object for processing. + * @param timestampMs the input timestamp (in milliseconds). + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is not + * created wtih {@link ResultListener} set in {@link ImageSegmenterOptions}. + */ + public void segmentForVideoWithResultListener(MPImage image, long timestampMs) { + segmentForVideoWithResultListener(image, ImageProcessingOptions.builder().build(), timestampMs); + } + + /** + * Performs image segmentation on the provided video frame, and provides zero-copied results via + * {@link ResultListener} in {@link ImageSegmenterOptions}. Only use this method when the {@link + * ImageSegmenter} is created with {@link RunningMode.VIDEO}. + * + *

      It's required to provide the video frame's timestamp (in milliseconds). The input timestamps + * must be monotonically increasing. + * + *

      {@link ImageSegmenter} supports the following color space types: + * + *

        + *
      • {@link Bitmap.Config.ARGB_8888} + *
      + * + * @param image a MediaPipe {@link MPImage} object for processing. + * @param timestampMs the input timestamp (in milliseconds). + * @throws MediaPipeException if there is an internal error. Or if {@link ImageSegmenter} is not + * created wtih {@link ResultListener} set in {@link ImageSegmenterOptions}. + */ + public void segmentForVideoWithResultListener( + MPImage image, ImageProcessingOptions imageProcessingOptions, long timestampMs) { + if (!hasResultListener) { + throw new MediaPipeException( + MediaPipeException.StatusCode.FAILED_PRECONDITION.ordinal(), + "ResultListener is not set in the ImageSegmenterOptions, but this method expects a" + + " ResultListener to process ImageSegmentationResult."); + } validateImageProcessingOptions(imageProcessingOptions); ImageSegmenterResult unused = (ImageSegmenterResult) processVideoData(image, imageProcessingOptions, timestampMs); } /** - * Sends live image data to perform hand landmarks detection with default image processing - * options, i.e. without any rotation applied, and the results will be available via the {@link - * ResultListener} provided in the {@link ImageSegmenterOptions}. Only use this method when the - * {@link ImageSegmenter } is created with {@link RunningMode.LIVE_STREAM}. + * Sends live image data to perform image segmentation with default image processing options, i.e. + * without any rotation applied, and the results will be available via the {@link ResultListener} + * provided in the {@link ImageSegmenterOptions}. Only use this method when the {@link + * ImageSegmenter } is created with {@link RunningMode.LIVE_STREAM}. * *

      It's required to provide a timestamp (in milliseconds) to indicate when the input image is * sent to the image segmenter. The input timestamps must be monotonically increasing. @@ -360,8 +507,8 @@ public final class ImageSegmenter extends BaseVisionTaskApi { public abstract Builder setOutputType(OutputType value); /** - * Sets the {@link ResultListener} to receive the segmentation results when the graph pipeline - * is done processing an image. + * Sets an optional {@link ResultListener} to receive the segmentation results when the graph + * pipeline is done processing an image. */ public abstract Builder setResultListener( ResultListener value); @@ -375,11 +522,18 @@ public final class ImageSegmenter extends BaseVisionTaskApi { * Validates and builds the {@link ImageSegmenterOptions} instance. * * @throws IllegalArgumentException if the result listener and the running mode are not - * properly configured. The result listener should only be set when the image segmenter is - * in the live stream mode. + * properly configured. The result listener must be set when the image segmenter is in the + * live stream mode. */ public final ImageSegmenterOptions build() { ImageSegmenterOptions options = autoBuild(); + if (options.runningMode() == RunningMode.LIVE_STREAM) { + if (!options.resultListener().isPresent()) { + throw new IllegalArgumentException( + "The image segmenter is in the live stream mode, a user-defined result listener" + + " must be provided in ImageSegmenterOptions."); + } + } return options; } } @@ -392,7 +546,7 @@ public final class ImageSegmenter extends BaseVisionTaskApi { abstract OutputType outputType(); - abstract ResultListener resultListener(); + abstract Optional> resultListener(); abstract Optional errorListener(); @@ -410,8 +564,7 @@ public final class ImageSegmenter extends BaseVisionTaskApi { return new AutoValue_ImageSegmenter_ImageSegmenterOptions.Builder() .setRunningMode(RunningMode.IMAGE) .setDisplayNamesLocale("en") - .setOutputType(OutputType.CATEGORY_MASK) - .setResultListener((result, image) -> {}); + .setOutputType(OutputType.CATEGORY_MASK); } /** @@ -437,6 +590,7 @@ public final class ImageSegmenter extends BaseVisionTaskApi { segmenterOptionsBuilder.setOutputType( SegmenterOptionsProto.SegmenterOptions.OutputType.CATEGORY_MASK); } + // TODO: remove this once activation is handled in metadata and grpah level. segmenterOptionsBuilder.setActivation( SegmenterOptionsProto.SegmenterOptions.Activation.SOFTMAX); diff --git a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenterTest.java b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenterTest.java index c11bb1f3..16f591c4 100644 --- a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenterTest.java +++ b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/imagesegmenter/ImageSegmenterTest.java @@ -53,112 +53,108 @@ public class ImageSegmenterTest { @RunWith(AndroidJUnit4.class) public static final class General extends ImageSegmenterTest { - @Test public void segment_successWithCategoryMask() throws Exception { final String inputImageName = "segmentation_input_rotation0.jpg"; final String goldenImageName = "segmentation_golden_rotation0.png"; - MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); ImageSegmenterOptions options = ImageSegmenterOptions.builder() .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) .setOutputType(ImageSegmenterOptions.OutputType.CATEGORY_MASK) - .setResultListener( - (actualResult, inputImage) -> { - List segmentations = actualResult.segmentations(); - assertThat(segmentations.size()).isEqualTo(1); - MPImage actualMaskBuffer = actualResult.segmentations().get(0); - verifyCategoryMask( - actualMaskBuffer, - expectedMaskBuffer, - GOLDEN_MASK_SIMILARITY, - MAGNIFICATION_FACTOR); - }) .build(); ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); - imageSegmenter.segment(getImageFromAsset(inputImageName)); + ImageSegmenterResult actualResult = imageSegmenter.segment(getImageFromAsset(inputImageName)); + List segmentations = actualResult.segmentations(); + assertThat(segmentations.size()).isEqualTo(1); + MPImage actualMaskBuffer = actualResult.segmentations().get(0); + MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); + verifyCategoryMask( + actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY, MAGNIFICATION_FACTOR); } @Test public void segment_successWithConfidenceMask() throws Exception { final String inputImageName = "cat.jpg"; final String goldenImageName = "cat_mask.jpg"; - MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); ImageSegmenterOptions options = ImageSegmenterOptions.builder() .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) - .setResultListener( - (actualResult, inputImage) -> { - List segmentations = actualResult.segmentations(); - assertThat(segmentations.size()).isEqualTo(21); - // Cat category index 8. - MPImage actualMaskBuffer = actualResult.segmentations().get(8); - verifyConfidenceMask( - actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); - }) .build(); ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); - imageSegmenter.segment(getImageFromAsset(inputImageName)); + ImageSegmenterResult actualResult = imageSegmenter.segment(getImageFromAsset(inputImageName)); + List segmentations = actualResult.segmentations(); + assertThat(segmentations.size()).isEqualTo(21); + // Cat category index 8. + MPImage actualMaskBuffer = actualResult.segmentations().get(8); + MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); + verifyConfidenceMask(actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); } @Test public void segment_successWith128x128Segmentation() throws Exception { final String inputImageName = "mozart_square.jpg"; final String goldenImageName = "selfie_segm_128_128_3_expected_mask.jpg"; - MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); ImageSegmenterOptions options = ImageSegmenterOptions.builder() .setBaseOptions( BaseOptions.builder().setModelAssetPath(SELFIE_128x128_MODEL_FILE).build()) .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) - .setResultListener( - (actualResult, inputImage) -> { - List segmentations = actualResult.segmentations(); - assertThat(segmentations.size()).isEqualTo(2); - // Selfie category index 1. - MPImage actualMaskBuffer = actualResult.segmentations().get(1); - verifyConfidenceMask( - actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); - }) .build(); ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); - imageSegmenter.segment(getImageFromAsset(inputImageName)); + ImageSegmenterResult actualResult = imageSegmenter.segment(getImageFromAsset(inputImageName)); + List segmentations = actualResult.segmentations(); + assertThat(segmentations.size()).isEqualTo(2); + // Selfie category index 1. + MPImage actualMaskBuffer = actualResult.segmentations().get(1); + MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); + verifyConfidenceMask(actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); } // TODO: enable this unit test once activation option is supported in metadata. - // @Test - // public void segment_successWith144x256Segmentation() throws Exception { - // final String inputImageName = "mozart_square.jpg"; - // final String goldenImageName = "selfie_segm_144_256_3_expected_mask.jpg"; - // MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); - // ImageSegmenterOptions options = - // ImageSegmenterOptions.builder() - // .setBaseOptions( - // BaseOptions.builder().setModelAssetPath(SELFIE_144x256_MODEL_FILE).build()) - // .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) - // .setActivation(ImageSegmenterOptions.Activation.NONE) - // .setResultListener( - // (actualResult, inputImage) -> { - // List segmentations = actualResult.segmentations(); - // assertThat(segmentations.size()).isEqualTo(1); - // MPImage actualMaskBuffer = actualResult.segmentations().get(0); - // verifyConfidenceMask( - // actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); - // }) - // .build(); - // ImageSegmenter imageSegmenter = - // ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), - // options); - // imageSegmenter.segment(getImageFromAsset(inputImageName)); - // } + // @Test + // public void segment_successWith144x256Segmentation() throws Exception { + // final String inputImageName = "mozart_square.jpg"; + // final String goldenImageName = "selfie_segm_144_256_3_expected_mask.jpg"; + // ImageSegmenterOptions options = + // ImageSegmenterOptions.builder() + // .setBaseOptions( + // BaseOptions.builder().setModelAssetPath(SELFIE_144x256_MODEL_FILE).build()) + // .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) + // .build(); + // ImageSegmenter imageSegmenter = + // ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); + // ImageSegmenterResult actualResult = + // imageSegmenter.segment(getImageFromAsset(inputImageName)); + // List segmentations = actualResult.segmentations(); + // assertThat(segmentations.size()).isEqualTo(1); + // MPImage actualMaskBuffer = actualResult.segmentations().get(0); + // MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); + // verifyConfidenceMask(actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); + // } } @RunWith(AndroidJUnit4.class) public static final class RunningModeTest extends ImageSegmenterTest { + @Test + public void create_failsWithMissingResultListenerInLiveSteamMode() throws Exception { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ImageSegmenterOptions.builder() + .setBaseOptions( + BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) + .setRunningMode(RunningMode.LIVE_STREAM) + .build()); + assertThat(exception) + .hasMessageThat() + .contains("a user-defined result listener must be provided"); + } + @Test public void segment_failsWithCallingWrongApiInImageMode() throws Exception { ImageSegmenterOptions options = @@ -166,7 +162,6 @@ public class ImageSegmenterTest { .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) .setRunningMode(RunningMode.IMAGE) .build(); - ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); MediaPipeException exception = @@ -182,6 +177,13 @@ public class ImageSegmenterTest { () -> imageSegmenter.segmentAsync(getImageFromAsset(CAT_IMAGE), /* timestampsMs= */ 0)); assertThat(exception).hasMessageThat().contains("not initialized with the live stream mode"); + exception = + assertThrows( + MediaPipeException.class, + () -> imageSegmenter.segmentWithResultListener(getImageFromAsset(CAT_IMAGE))); + assertThat(exception) + .hasMessageThat() + .contains("ResultListener is not set in the ImageSegmenterOptions"); } @Test @@ -191,7 +193,6 @@ public class ImageSegmenterTest { .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) .setRunningMode(RunningMode.VIDEO) .build(); - ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); MediaPipeException exception = @@ -204,6 +205,15 @@ public class ImageSegmenterTest { () -> imageSegmenter.segmentAsync(getImageFromAsset(CAT_IMAGE), /* timestampsMs= */ 0)); assertThat(exception).hasMessageThat().contains("not initialized with the live stream mode"); + exception = + assertThrows( + MediaPipeException.class, + () -> + imageSegmenter.segmentForVideoWithResultListener( + getImageFromAsset(CAT_IMAGE), /* timestampsMs= */ 0)); + assertThat(exception) + .hasMessageThat() + .contains("ResultListener is not set in the ImageSegmenterOptions"); } @Test @@ -214,18 +224,18 @@ public class ImageSegmenterTest { .setRunningMode(RunningMode.LIVE_STREAM) .setResultListener((result, inputImage) -> {}) .build(); - ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); MediaPipeException exception = assertThrows( - MediaPipeException.class, () -> imageSegmenter.segment(getImageFromAsset(CAT_IMAGE))); + MediaPipeException.class, + () -> imageSegmenter.segmentWithResultListener(getImageFromAsset(CAT_IMAGE))); assertThat(exception).hasMessageThat().contains("not initialized with the image mode"); exception = assertThrows( MediaPipeException.class, () -> - imageSegmenter.segmentForVideo( + imageSegmenter.segmentForVideoWithResultListener( getImageFromAsset(CAT_IMAGE), /* timestampsMs= */ 0)); assertThat(exception).hasMessageThat().contains("not initialized with the video mode"); } @@ -234,51 +244,94 @@ public class ImageSegmenterTest { public void segment_successWithImageMode() throws Exception { final String inputImageName = "cat.jpg"; final String goldenImageName = "cat_mask.jpg"; + ImageSegmenterOptions options = + ImageSegmenterOptions.builder() + .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) + .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) + .setRunningMode(RunningMode.IMAGE) + .build(); + ImageSegmenter imageSegmenter = + ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); + ImageSegmenterResult actualResult = imageSegmenter.segment(getImageFromAsset(inputImageName)); + List segmentations = actualResult.segmentations(); + assertThat(segmentations.size()).isEqualTo(21); + // Cat category index 8. + MPImage actualMaskBuffer = actualResult.segmentations().get(8); MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); + verifyConfidenceMask(actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); + } + + @Test + public void segment_successWithImageModeWithResultListener() throws Exception { + final String inputImageName = "cat.jpg"; + final String goldenImageName = "cat_mask.jpg"; + MPImage expectedResult = getImageFromAsset(goldenImageName); ImageSegmenterOptions options = ImageSegmenterOptions.builder() .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) .setRunningMode(RunningMode.IMAGE) .setResultListener( - (actualResult, inputImage) -> { - List segmentations = actualResult.segmentations(); - assertThat(segmentations.size()).isEqualTo(21); - // Cat category index 8. - MPImage actualMaskBuffer = actualResult.segmentations().get(8); + (segmenterResult, inputImage) -> { verifyConfidenceMask( - actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); + segmenterResult.segmentations().get(8), + expectedResult, + GOLDEN_MASK_SIMILARITY); }) .build(); ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); - imageSegmenter.segment(getImageFromAsset(inputImageName)); + imageSegmenter.segmentWithResultListener(getImageFromAsset(inputImageName)); } @Test public void segment_successWithVideoMode() throws Exception { final String inputImageName = "cat.jpg"; final String goldenImageName = "cat_mask.jpg"; + ImageSegmenterOptions options = + ImageSegmenterOptions.builder() + .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) + .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) + .setRunningMode(RunningMode.VIDEO) + .build(); + ImageSegmenter imageSegmenter = + ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); MPImage expectedMaskBuffer = getImageFromAsset(goldenImageName); + for (int i = 0; i < 3; i++) { + ImageSegmenterResult actualResult = + imageSegmenter.segmentForVideo( + getImageFromAsset(inputImageName), /* timestampsMs= */ i); + List segmentations = actualResult.segmentations(); + assertThat(segmentations.size()).isEqualTo(21); + // Cat category index 8. + MPImage actualMaskBuffer = actualResult.segmentations().get(8); + verifyConfidenceMask(actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); + } + } + + @Test + public void segment_successWithVideoModeWithResultListener() throws Exception { + final String inputImageName = "cat.jpg"; + final String goldenImageName = "cat_mask.jpg"; + MPImage expectedResult = getImageFromAsset(goldenImageName); ImageSegmenterOptions options = ImageSegmenterOptions.builder() .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) .setOutputType(ImageSegmenterOptions.OutputType.CONFIDENCE_MASK) .setRunningMode(RunningMode.VIDEO) .setResultListener( - (actualResult, inputImage) -> { - List segmentations = actualResult.segmentations(); - assertThat(segmentations.size()).isEqualTo(21); - // Cat category index 8. - MPImage actualMaskBuffer = actualResult.segmentations().get(8); + (segmenterResult, inputImage) -> { verifyConfidenceMask( - actualMaskBuffer, expectedMaskBuffer, GOLDEN_MASK_SIMILARITY); + segmenterResult.segmentations().get(8), + expectedResult, + GOLDEN_MASK_SIMILARITY); }) .build(); ImageSegmenter imageSegmenter = ImageSegmenter.createFromOptions(ApplicationProvider.getApplicationContext(), options); for (int i = 0; i < 3; i++) { - imageSegmenter.segmentForVideo(getImageFromAsset(inputImageName), /* timestampsMs= */ i); + imageSegmenter.segmentForVideoWithResultListener( + getImageFromAsset(inputImageName), /* timestampsMs= */ i); } } diff --git a/mediapipe/tasks/python/audio/BUILD b/mediapipe/tasks/python/audio/BUILD index 6dda7a53..9d8af146 100644 --- a/mediapipe/tasks/python/audio/BUILD +++ b/mediapipe/tasks/python/audio/BUILD @@ -56,7 +56,6 @@ py_library( "//mediapipe/tasks/python/audio/core:base_audio_task_api", "//mediapipe/tasks/python/components/containers:audio_data", "//mediapipe/tasks/python/components/containers:embedding_result", - "//mediapipe/tasks/python/components/utils:cosine_similarity", "//mediapipe/tasks/python/core:base_options", "//mediapipe/tasks/python/core:optional_dependencies", "//mediapipe/tasks/python/core:task_info", diff --git a/mediapipe/tasks/python/audio/audio_embedder.py b/mediapipe/tasks/python/audio/audio_embedder.py index 4c37783e..835dd0e3 100644 --- a/mediapipe/tasks/python/audio/audio_embedder.py +++ b/mediapipe/tasks/python/audio/audio_embedder.py @@ -26,7 +26,6 @@ from mediapipe.tasks.python.audio.core import audio_task_running_mode as running from mediapipe.tasks.python.audio.core import base_audio_task_api from mediapipe.tasks.python.components.containers import audio_data as audio_data_module from mediapipe.tasks.python.components.containers import embedding_result as embedding_result_module -from mediapipe.tasks.python.components.utils import cosine_similarity from mediapipe.tasks.python.core import base_options as base_options_module from mediapipe.tasks.python.core import task_info as task_info_module from mediapipe.tasks.python.core.optional_dependencies import doc_controls @@ -284,26 +283,3 @@ class AudioEmbedder(base_audio_task_api.BaseAudioTaskApi): packet_creator.create_matrix(audio_block.buffer, transpose=True).at( timestamp_ms * _MICRO_SECONDS_PER_MILLISECOND) }) - - @classmethod - def cosine_similarity(cls, u: embedding_result_module.Embedding, - v: embedding_result_module.Embedding) -> float: - """Utility function to compute cosine similarity between two embedding entries. - - May return an InvalidArgumentError if e.g. the feature vectors are - of different types (quantized vs. float), have different sizes, or have a - an L2-norm of 0. - - Args: - u: An embedding entry. - v: An embedding entry. - - Returns: - The cosine similarity for the two embeddings. - - Raises: - ValueError: May return an error if e.g. the feature vectors are of - different types (quantized vs. float), have different sizes, or have - an L2-norm of 0. - """ - return cosine_similarity.cosine_similarity(u, v) diff --git a/mediapipe/tasks/python/test/audio/audio_embedder_test.py b/mediapipe/tasks/python/test/audio/audio_embedder_test.py index f280235d..934cdc8d 100644 --- a/mediapipe/tasks/python/test/audio/audio_embedder_test.py +++ b/mediapipe/tasks/python/test/audio/audio_embedder_test.py @@ -42,13 +42,10 @@ _SPEECH_WAV_16K_MONO = 'speech_16000_hz_mono.wav' _SPEECH_WAV_48K_MONO = 'speech_48000_hz_mono.wav' _TWO_HEADS_WAV_16K_MONO = 'two_heads_16000_hz_mono.wav' _TEST_DATA_DIR = 'mediapipe/tasks/testdata/audio' -_SPEECH_SIMILARITIES = [0.985359, 0.994349, 0.993227, 0.996658, 0.996384] _YAMNET_NUM_OF_SAMPLES = 15600 _MILLSECONDS_PER_SECOND = 1000 # Tolerance for embedding vector coordinate values. _EPSILON = 3e-6 -# Tolerance for cosine similarity evaluation. -_SIMILARITY_TOLERANCE = 1e-6 class ModelFileType(enum.Enum): @@ -98,27 +95,6 @@ class AudioEmbedderTest(parameterized.TestCase): else: self.assertEqual(embedding_result.embedding.dtype, float) - def _check_cosine_similarity(self, result0, result1, expected_similarity): - # Checks cosine similarity. - similarity = _AudioEmbedder.cosine_similarity(result0.embeddings[0], - result1.embeddings[0]) - self.assertAlmostEqual( - similarity, expected_similarity, delta=_SIMILARITY_TOLERANCE) - - def _check_yamnet_result(self, - embedding_result0_list: List[_AudioEmbedderResult], - embedding_result1_list: List[_AudioEmbedderResult], - expected_similarities: List[float]): - expected_size = len(expected_similarities) - self.assertLen(embedding_result0_list, expected_size) - self.assertLen(embedding_result1_list, expected_size) - - for idx in range(expected_size): - embedding_result0 = embedding_result0_list[idx] - embedding_result1 = embedding_result1_list[idx] - self._check_cosine_similarity(embedding_result0, embedding_result1, - expected_similarities[idx]) - def test_create_from_file_succeeds_with_valid_model_path(self): # Creates with default option and valid model file successfully. with _AudioEmbedder.create_from_model_path( @@ -176,7 +152,7 @@ class AudioEmbedderTest(parameterized.TestCase): embedding_result0_list = embedder.embed(self._read_wav_file(audio_file0)) embedding_result1_list = embedder.embed(self._read_wav_file(audio_file1)) - # Checks embeddings and cosine similarity. + # Checks embeddings. expected_result0_value, expected_result1_value = expected_first_values self._check_embedding_size(embedding_result0_list[0], quantize, expected_size) @@ -186,10 +162,8 @@ class AudioEmbedderTest(parameterized.TestCase): expected_result0_value) self._check_embedding_value(embedding_result1_list[0], expected_result1_value) - self._check_yamnet_result( - embedding_result0_list, - embedding_result1_list, - expected_similarities=_SPEECH_SIMILARITIES) + self.assertLen(embedding_result0_list, 5) + self.assertLen(embedding_result1_list, 5) def test_embed_with_yamnet_model_and_different_inputs(self): with _AudioEmbedder.create_from_model_path( @@ -200,10 +174,6 @@ class AudioEmbedderTest(parameterized.TestCase): self._read_wav_file(_TWO_HEADS_WAV_16K_MONO)) self.assertLen(embedding_result0_list, 5) self.assertLen(embedding_result1_list, 1) - self._check_cosine_similarity( - embedding_result0_list[0], - embedding_result1_list[0], - expected_similarity=0.09017) def test_missing_sample_rate_in_audio_clips_mode(self): options = _AudioEmbedderOptions( @@ -304,10 +274,8 @@ class AudioEmbedderTest(parameterized.TestCase): embedder.embed_async(audio_data, timestamp_ms) embedding_result1_list = embedding_result_list - self._check_yamnet_result( - embedding_result0_list, - embedding_result1_list, - expected_similarities=_SPEECH_SIMILARITIES) + self.assertLen(embedding_result0_list, 5) + self.assertLen(embedding_result1_list, 5) if __name__ == '__main__': diff --git a/mediapipe/tasks/testdata/vision/hand_landmarker.task b/mediapipe/tasks/testdata/vision/hand_landmarker.task index 1ae9f7f6..748b2f01 100644 Binary files a/mediapipe/tasks/testdata/vision/hand_landmarker.task and b/mediapipe/tasks/testdata/vision/hand_landmarker.task differ diff --git a/mediapipe/tasks/web/audio/audio_embedder/BUILD b/mediapipe/tasks/web/audio/audio_embedder/BUILD index 68a7f7bd..69b0761d 100644 --- a/mediapipe/tasks/web/audio/audio_embedder/BUILD +++ b/mediapipe/tasks/web/audio/audio_embedder/BUILD @@ -21,10 +21,8 @@ mediapipe_ts_library( "//mediapipe/tasks/cc/components/containers/proto:embeddings_jspb_proto", "//mediapipe/tasks/cc/core/proto:base_options_jspb_proto", "//mediapipe/tasks/web/audio/core:audio_task_runner", - "//mediapipe/tasks/web/components/containers:embedding_result", "//mediapipe/tasks/web/components/processors:embedder_options", "//mediapipe/tasks/web/components/processors:embedder_result", - "//mediapipe/tasks/web/components/utils:cosine_similarity", "//mediapipe/tasks/web/core", "//mediapipe/tasks/web/core:embedder_options", "//mediapipe/tasks/web/core:task_runner", diff --git a/mediapipe/tasks/web/audio/audio_embedder/audio_embedder.ts b/mediapipe/tasks/web/audio/audio_embedder/audio_embedder.ts index 7d8c7a5b..e6d659b9 100644 --- a/mediapipe/tasks/web/audio/audio_embedder/audio_embedder.ts +++ b/mediapipe/tasks/web/audio/audio_embedder/audio_embedder.ts @@ -20,10 +20,8 @@ import {AudioEmbedderGraphOptions as AudioEmbedderGraphOptionsProto} from '../.. import {EmbeddingResult} from '../../../../tasks/cc/components/containers/proto/embeddings_pb'; import {BaseOptions as BaseOptionsProto} from '../../../../tasks/cc/core/proto/base_options_pb'; import {AudioTaskRunner} from '../../../../tasks/web/audio/core/audio_task_runner'; -import {Embedding} from '../../../../tasks/web/components/containers/embedding_result'; import {convertEmbedderOptionsToProto} from '../../../../tasks/web/components/processors/embedder_options'; import {convertFromEmbeddingResultProto} from '../../../../tasks/web/components/processors/embedder_result'; -import {computeCosineSimilarity} from '../../../../tasks/web/components/utils/cosine_similarity'; import {CachedGraphRunner} from '../../../../tasks/web/core/task_runner'; import {WasmFileset} from '../../../../tasks/web/core/wasm_fileset'; import {WasmModule} from '../../../../web/graph_runner/graph_runner'; @@ -145,19 +143,6 @@ export class AudioEmbedder extends AudioTaskRunner { return this.processAudioClip(audioData, sampleRate); } - /** - * Utility function to compute cosine similarity[1] between two `Embedding` - * objects. - * - * [1]: https://en.wikipedia.org/wiki/Cosine_similarity - * - * @throws if the embeddings are of different types(float vs. quantized), have - * different sizes, or have an L2-norm of 0. - */ - static cosineSimilarity(u: Embedding, v: Embedding): number { - return computeCosineSimilarity(u, v); - } - protected override process( audioData: Float32Array, sampleRate: number, timestampMs: number): AudioEmbedderResult[] { diff --git a/mediapipe/tasks/web/core/BUILD b/mediapipe/tasks/web/core/BUILD index 371c75da..ec65548d 100644 --- a/mediapipe/tasks/web/core/BUILD +++ b/mediapipe/tasks/web/core/BUILD @@ -56,6 +56,7 @@ mediapipe_ts_library( deps = [ ":core", ":task_runner", + "//mediapipe/calculators/tensor:inference_calculator_jspb_proto", "//mediapipe/tasks/cc/core/proto:base_options_jspb_proto", "//mediapipe/web/graph_runner:graph_runner_ts", ], diff --git a/mediapipe/tasks/web/core/fileset_resolver.ts b/mediapipe/tasks/web/core/fileset_resolver.ts index 9917035a..ae17c577 100644 --- a/mediapipe/tasks/web/core/fileset_resolver.ts +++ b/mediapipe/tasks/web/core/fileset_resolver.ts @@ -44,7 +44,7 @@ async function isSimdSupported(): Promise { } async function createFileset( - taskName: string, basePath: string = ''): Promise { + taskName: string, basePath = ''): Promise { const suffix = await isSimdSupported() ? 'wasm_internal' : 'wasm_nosimd_internal'; diff --git a/mediapipe/tasks/web/core/task_runner.ts b/mediapipe/tasks/web/core/task_runner.ts index b6babe73..79b2ca17 100644 --- a/mediapipe/tasks/web/core/task_runner.ts +++ b/mediapipe/tasks/web/core/task_runner.ts @@ -208,13 +208,23 @@ export abstract class TaskRunner { /** Configures the `acceleration` option. */ private setAcceleration(options: BaseOptions) { - const acceleration = - this.baseOptions.getAcceleration() ?? new Acceleration(); - if (options.delegate === 'GPU') { - acceleration.setGpu(new InferenceCalculatorOptions.Delegate.Gpu()); - } else { + let acceleration = this.baseOptions.getAcceleration(); + + if (!acceleration) { + // Create default instance for the initial configuration. + acceleration = new Acceleration(); acceleration.setTflite(new InferenceCalculatorOptions.Delegate.TfLite()); } + + if ('delegate' in options) { + if (options.delegate === 'GPU') { + acceleration.setGpu(new InferenceCalculatorOptions.Delegate.Gpu()); + } else { + acceleration.setTflite( + new InferenceCalculatorOptions.Delegate.TfLite()); + } + } + this.baseOptions.setAcceleration(acceleration); } } diff --git a/mediapipe/tasks/web/core/task_runner_test.ts b/mediapipe/tasks/web/core/task_runner_test.ts index 1276c2c9..41ac7695 100644 --- a/mediapipe/tasks/web/core/task_runner_test.ts +++ b/mediapipe/tasks/web/core/task_runner_test.ts @@ -16,6 +16,7 @@ import 'jasmine'; // Placeholder for internal dependency on encodeByteArray +import {InferenceCalculatorOptions} from '../../../calculators/tensor/inference_calculator_pb'; import {BaseOptions as BaseOptionsProto} from '../../../tasks/cc/core/proto/base_options_pb'; import {TaskRunner} from '../../../tasks/web/core/task_runner'; import {ErrorListener} from '../../../web/graph_runner/graph_runner'; @@ -97,6 +98,23 @@ describe('TaskRunner', () => { tflite: {}, }, }; + const mockBytesResultWithGpuDelegate = { + ...mockBytesResult, + acceleration: { + xnnpack: undefined, + gpu: { + useAdvancedGpuApi: false, + api: InferenceCalculatorOptions.Delegate.Gpu.Api.ANY, + allowPrecisionLoss: true, + cachedKernelPath: undefined, + serializedModelDir: undefined, + modelToken: undefined, + usage: InferenceCalculatorOptions.Delegate.Gpu.InferenceUsage + .SUSTAINED_SPEED, + }, + tflite: undefined, + }, + }; let fetchSpy: jasmine.Spy; let taskRunner: TaskRunnerFake; @@ -224,22 +242,8 @@ describe('TaskRunner', () => { delegate: 'GPU', } }); - expect(taskRunner.baseOptions.toObject()).toEqual({ - ...mockBytesResult, - acceleration: { - xnnpack: undefined, - gpu: { - useAdvancedGpuApi: false, - api: 0, - allowPrecisionLoss: true, - cachedKernelPath: undefined, - serializedModelDir: undefined, - modelToken: undefined, - usage: 2, - }, - tflite: undefined, - }, - }); + expect(taskRunner.baseOptions.toObject()) + .toEqual(mockBytesResultWithGpuDelegate); }); it('can reset delegate', async () => { @@ -249,8 +253,20 @@ describe('TaskRunner', () => { delegate: 'GPU', } }); - // Clear backend + // Clear delegate await taskRunner.setOptions({baseOptions: {delegate: undefined}}); expect(taskRunner.baseOptions.toObject()).toEqual(mockBytesResult); }); + + it('keeps delegate if not provided', async () => { + await taskRunner.setOptions({ + baseOptions: { + modelAssetBuffer: new Uint8Array(mockBytes), + delegate: 'GPU', + } + }); + await taskRunner.setOptions({baseOptions: {}}); + expect(taskRunner.baseOptions.toObject()) + .toEqual(mockBytesResultWithGpuDelegate); + }); }); diff --git a/third_party/external_files.bzl b/third_party/external_files.bzl index 7122c677..f446b372 100644 --- a/third_party/external_files.bzl +++ b/third_party/external_files.bzl @@ -306,8 +306,8 @@ def external_files(): http_file( name = "com_google_mediapipe_gesture_recognizer_task", - sha256 = "a966b1d4e774e0423c19c8aa71f070e5a72fe7a03c2663dd2f3cb0b0095ee3e1", - urls = ["https://storage.googleapis.com/mediapipe-assets/gesture_recognizer.task?generation=1668100501451433"], + sha256 = "d48562f535fd4ecd3cfea739d9663dd818eeaf6a8afb1b5e6f8f4747661f73d9", + urls = ["https://storage.googleapis.com/mediapipe-assets/gesture_recognizer.task?generation=1677051715043311"], ) http_file( @@ -342,8 +342,8 @@ def external_files(): http_file( name = "com_google_mediapipe_hand_landmarker_task", - sha256 = "2ed44f10872e87a5834b9b1130fb9ada30e107af2c6fcc4562ad788aca4e7bc4", - urls = ["https://storage.googleapis.com/mediapipe-assets/hand_landmarker.task?generation=1666153732577904"], + sha256 = "32d1eab97e80a9a20edb29231e15301ce65abfd0fa9d41cf1757e0ecc8078a4e", + urls = ["https://storage.googleapis.com/mediapipe-assets/hand_landmarker.task?generation=1677051718270846"], ) http_file(